1use std::fmt;
3
4use bitcoin::Network;
5use bitreq::Error as BitreqError;
6use serde::{Deserialize, Serialize};
7use serde_json::Error as SerdeJsonError;
8use thiserror::Error;
9
10const RPC_VERIFY_ERROR: i32 = -25;
13const RPC_VERIFY_REJECTED: i32 = -26;
16const RPC_VERIFY_ALREADY_IN_UTXO_SET: i32 = -27;
19
20#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum ClientError {
23 #[error("Missing username or password")]
25 MissingUserPassword,
26
27 #[error("RPC server returned error '{1}' (code {0})")]
36 Server(i32, String),
37
38 #[error("Error parsing rpc response: {0}")]
40 Parse(String),
41
42 #[error("Could not create RPC Param")]
44 Param(String),
45
46 #[error("{0}")]
48 Body(String),
49
50 #[error("Obtained failure status({0}): {1}")]
55 Status(u16, String),
56
57 #[error("Malformed Response: {0}")]
59 MalformedResponse(String),
60
61 #[error("Could not connect: {0}")]
63 Connection(String),
64
65 #[error("Timeout")]
67 Timeout,
68
69 #[error("HttpRedirect: {0}")]
71 HttpRedirect(String),
72
73 #[error("Could not build request: {0}")]
75 ReqBuilder(String),
76
77 #[error("Max retries {0} exceeded")]
79 MaxRetriesExceeded(u8),
80
81 #[error("Could not create request: {0}")]
83 Request(String),
84
85 #[error("Network address: {0}")]
87 WrongNetworkAddress(Network),
88
89 #[error(transparent)]
91 UnexpectedServerVersion(#[from] UnexpectedServerVersionError),
92
93 #[error(transparent)]
95 Sign(#[from] SignRawTransactionWithWalletError),
96
97 #[error("Could not get xpriv from wallet")]
99 Xpriv,
100
101 #[error("{0}")]
103 Other(String),
104}
105
106impl ClientError {
107 pub fn is_tx_not_found(&self) -> bool {
110 matches!(self, Self::Server(-5, _))
111 }
112
113 pub fn is_block_not_found(&self) -> bool {
116 matches!(self, Self::Server(-5, _))
117 }
118
119 pub fn is_rpc_verify_error(&self) -> bool {
122 matches!(self, Self::Server(RPC_VERIFY_ERROR, _))
123 }
124
125 pub fn is_rpc_verify_rejected(&self) -> bool {
128 matches!(self, Self::Server(RPC_VERIFY_REJECTED, _))
129 }
130
131 pub fn is_rpc_verify_already_in_utxo_set(&self) -> bool {
134 matches!(self, Self::Server(RPC_VERIFY_ALREADY_IN_UTXO_SET, _))
135 }
136
137 pub fn is_retriable(&self) -> bool {
144 matches!(
145 self,
146 Self::Connection(_)
147 | Self::Timeout
148 | Self::Request(_)
149 | Self::Param(_)
150 | Self::MaxRetriesExceeded(_)
151 | Self::Status(500..=599, _)
152 )
153 }
154
155 #[deprecated(
158 since = "0.10.4",
159 note = "use is_rpc_verify_error() to detect RPC_VERIFY_ERROR (-25)"
160 )]
161 pub fn is_missing_or_invalid_input(&self) -> bool {
162 self.is_rpc_verify_error()
163 }
164}
165
166impl From<BitreqError> for ClientError {
167 fn from(value: BitreqError) -> Self {
168 match value {
169 BitreqError::AddressNotFound
171 | BitreqError::IoError(_)
172 | BitreqError::RustlsCreateConnection(_) => ClientError::Connection(value.to_string()),
173
174 BitreqError::RedirectLocationMissing
176 | BitreqError::InfiniteRedirectionLoop
177 | BitreqError::TooManyRedirections => ClientError::HttpRedirect(value.to_string()),
178
179 BitreqError::HeadersOverflow
181 | BitreqError::StatusLineOverflow
182 | BitreqError::BodyOverflow
183 | BitreqError::MalformedChunkLength
184 | BitreqError::MalformedChunkEnd
185 | BitreqError::MalformedContentLength
186 | BitreqError::InvalidUtf8InResponse
187 | BitreqError::InvalidUtf8InBody(_) => {
188 ClientError::MalformedResponse(value.to_string())
189 }
190
191 _ => ClientError::Other(value.to_string()),
193 }
194 }
195}
196
197impl From<SerdeJsonError> for ClientError {
198 fn from(value: SerdeJsonError) -> Self {
199 Self::Parse(format!("Could not parse {value}"))
200 }
201}
202
203#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct BitcoinRpcError {
206 pub code: i32,
207 pub message: String,
208}
209
210impl fmt::Display for BitcoinRpcError {
211 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212 write!(f, "RPC error {}: {}", self.code, self.message)
213 }
214}
215
216impl From<BitcoinRpcError> for ClientError {
217 fn from(value: BitcoinRpcError) -> Self {
218 Self::Server(value.code, value.message)
219 }
220}
221
222#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
224pub struct SignRawTransactionWithWalletError {
225 txid: String,
227 vout: u32,
229 #[serde(rename = "scriptSig")]
231 script_sig: String,
232 sequence: u32,
234 error: String,
236}
237
238impl fmt::Display for SignRawTransactionWithWalletError {
239 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
240 write!(
241 f,
242 "error signing raw transaction with wallet: {}",
243 self.error
244 )
245 }
246}
247
248#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct UnexpectedServerVersionError {
251 pub got: usize,
253 pub expected: Vec<usize>,
255}
256
257impl fmt::Display for UnexpectedServerVersionError {
258 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
259 let mut expected = String::new();
260 for version in &self.expected {
261 let v = format!(" {version} ");
262 expected.push_str(&v);
263 }
264 write!(
265 f,
266 "unexpected bitcoind version, got: {} expected one of: {}",
267 self.got, expected
268 )
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 #![allow(deprecated)]
275
276 use super::ClientError;
277
278 #[test]
279 fn classifies_rpc_verify_error() {
280 let error = ClientError::Server(-25, "Input not found or already spent".to_string());
281
282 assert!(error.is_rpc_verify_error());
283 assert!(error.is_missing_or_invalid_input());
284 assert!(!error.is_rpc_verify_rejected());
285 assert!(!error.is_rpc_verify_already_in_utxo_set());
286 }
287
288 #[test]
289 fn classifies_rpc_verify_rejected() {
290 let error = ClientError::Server(-26, "txn-already-in-mempool".to_string());
291
292 assert!(error.is_rpc_verify_rejected());
293 assert!(!error.is_missing_or_invalid_input());
294 assert!(!error.is_rpc_verify_error());
295 assert!(!error.is_rpc_verify_already_in_utxo_set());
296 }
297
298 #[test]
299 fn classifies_rpc_verify_already_in_utxo_set() {
300 let error = ClientError::Server(-27, "transaction already in block chain".to_string());
301
302 assert!(error.is_rpc_verify_already_in_utxo_set());
303 assert!(!error.is_rpc_verify_error());
304 assert!(!error.is_rpc_verify_rejected());
305 assert!(!error.is_missing_or_invalid_input());
306 }
307
308 #[test]
309 fn non_server_errors_do_not_match_rpc_code_helpers() {
310 let error = ClientError::Timeout;
311
312 assert!(!error.is_rpc_verify_error());
313 assert!(!error.is_rpc_verify_rejected());
314 assert!(!error.is_rpc_verify_already_in_utxo_set());
315 assert!(!error.is_missing_or_invalid_input());
316 }
317
318 #[test]
319 fn classifies_retriable_client_errors() {
320 assert!(ClientError::Connection("connection refused".to_string()).is_retriable());
321 assert!(ClientError::Timeout.is_retriable());
322 assert!(ClientError::Request("request failed".to_string()).is_retriable());
323 assert!(ClientError::Param("failed to create params".to_string()).is_retriable());
324 assert!(ClientError::MaxRetriesExceeded(3).is_retriable());
325 assert!(ClientError::Status(500, "internal server error".to_string()).is_retriable());
326 assert!(ClientError::Status(503, "service unavailable".to_string()).is_retriable());
327 assert!(ClientError::Status(599, "network connect timeout".to_string()).is_retriable());
328 }
329
330 #[test]
331 fn classifies_non_retriable_client_errors() {
332 assert!(!ClientError::MissingUserPassword.is_retriable());
333 assert!(
334 !ClientError::Server(-25, "bad-txns-inputs-missingorspent".to_string()).is_retriable()
335 );
336 assert!(!ClientError::Parse("bad json".to_string()).is_retriable());
337 assert!(!ClientError::Body("body error".to_string()).is_retriable());
338 assert!(!ClientError::MalformedResponse("bad response".to_string()).is_retriable());
339 assert!(!ClientError::Status(400, "bad request".to_string()).is_retriable());
340 assert!(!ClientError::Status(401, "unauthorized".to_string()).is_retriable());
341 assert!(!ClientError::Status(499, "client closed request".to_string()).is_retriable());
342 assert!(!ClientError::HttpRedirect("too many redirects".to_string()).is_retriable());
343 assert!(!ClientError::ReqBuilder("invalid request".to_string()).is_retriable());
344 assert!(!ClientError::WrongNetworkAddress(bitcoin::Network::Regtest).is_retriable());
345 assert!(!ClientError::Xpriv.is_retriable());
346 assert!(!ClientError::Other("unknown".to_string()).is_retriable());
347 }
348}