Skip to main content

bitcoind_async_client/
error.rs

1//! Error types for the RPC client.
2use 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
10/// Bitcoin Core `RPC_VERIFY_ERROR`, defined in
11/// <https://github.com/bitcoin/bitcoin/blob/8f4a3ba8972dae9412ba975a040cea22c227f983/src/rpc/protocol.h#L47>.
12const RPC_VERIFY_ERROR: i32 = -25;
13/// Bitcoin Core `RPC_VERIFY_REJECTED`, defined in
14/// <https://github.com/bitcoin/bitcoin/blob/8f4a3ba8972dae9412ba975a040cea22c227f983/src/rpc/protocol.h#L48>.
15const RPC_VERIFY_REJECTED: i32 = -26;
16/// Bitcoin Core `RPC_VERIFY_ALREADY_IN_UTXO_SET`, defined in
17/// <https://github.com/bitcoin/bitcoin/blob/8f4a3ba8972dae9412ba975a040cea22c227f983/src/rpc/protocol.h#L49>.
18const RPC_VERIFY_ALREADY_IN_UTXO_SET: i32 = -27;
19
20/// The error type for errors produced in this library.
21#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum ClientError {
23    /// Missing username or password for the RPC server
24    #[error("Missing username or password")]
25    MissingUserPassword,
26
27    /// RPC server returned an error
28    ///
29    /// # Note
30    ///
31    /// These errors are ABSOLUTELY UNDOCUMENTED.
32    /// Check
33    /// <https://github.com/bitcoin/bitcoin/blob/96b0a8f858ab24f3672360b8c830553b963de726/src/rpc/protocol.h#L24>
34    /// and good luck!
35    #[error("RPC server returned error '{1}' (code {0})")]
36    Server(i32, String),
37
38    /// Error parsing the RPC response, unlikely to be recoverable by retrying
39    #[error("Error parsing rpc response: {0}")]
40    Parse(String),
41
42    /// Error creating the RPC request, retry might help
43    #[error("Could not create RPC Param")]
44    Param(String),
45
46    /// Body error, unlikely to be recoverable by retrying
47    #[error("{0}")]
48    Body(String),
49
50    /// HTTP status error.
51    ///
52    /// Server-side failures may be retriable, while client-side failures usually indicate
53    /// configuration or authorization problems.
54    #[error("Obtained failure status({0}): {1}")]
55    Status(u16, String),
56
57    /// Error decoding the response, retry might not help
58    #[error("Malformed Response: {0}")]
59    MalformedResponse(String),
60
61    /// Connection error, retry might help
62    #[error("Could not connect: {0}")]
63    Connection(String),
64
65    /// Timeout error, retry might help
66    #[error("Timeout")]
67    Timeout,
68
69    /// Redirect error, not retryable
70    #[error("HttpRedirect: {0}")]
71    HttpRedirect(String),
72
73    /// Error building the request, unlikely to be recoverable
74    #[error("Could not build request: {0}")]
75    ReqBuilder(String),
76
77    /// Maximum retries exceeded, not retryable
78    #[error("Max retries {0} exceeded")]
79    MaxRetriesExceeded(u8),
80
81    /// General request error, retry might help
82    #[error("Could not create request: {0}")]
83    Request(String),
84
85    /// Wrong network address
86    #[error("Network address: {0}")]
87    WrongNetworkAddress(Network),
88
89    /// Server version is unexpected or incompatible
90    #[error(transparent)]
91    UnexpectedServerVersion(#[from] UnexpectedServerVersionError),
92
93    /// Could not sign raw transaction
94    #[error(transparent)]
95    Sign(#[from] SignRawTransactionWithWalletError),
96
97    /// Could not get a [`Xpriv`](bitcoin::bip32::Xpriv) from the wallet
98    #[error("Could not get xpriv from wallet")]
99    Xpriv,
100
101    /// Unknown error, unlikely to be recoverable
102    #[error("{0}")]
103    Other(String),
104}
105
106impl ClientError {
107    /// Returns `true` when the RPC server reports an invalid address, key, or missing
108    /// transaction/block identifier (`RPC_INVALID_ADDRESS_OR_KEY`, code `-5`).
109    pub fn is_tx_not_found(&self) -> bool {
110        matches!(self, Self::Server(-5, _))
111    }
112
113    /// Returns `true` when the RPC server reports an invalid address, key, or missing
114    /// transaction/block identifier (`RPC_INVALID_ADDRESS_OR_KEY`, code `-5`).
115    pub fn is_block_not_found(&self) -> bool {
116        matches!(self, Self::Server(-5, _))
117    }
118
119    /// Returns `true` when the RPC server reports a general transaction or block
120    /// submission verification error (`RPC_VERIFY_ERROR`, code `-25`).
121    pub fn is_rpc_verify_error(&self) -> bool {
122        matches!(self, Self::Server(RPC_VERIFY_ERROR, _))
123    }
124
125    /// Returns `true` when the RPC server reports a transaction or block rejected
126    /// by network rules (`RPC_VERIFY_REJECTED`, code `-26`).
127    pub fn is_rpc_verify_rejected(&self) -> bool {
128        matches!(self, Self::Server(RPC_VERIFY_REJECTED, _))
129    }
130
131    /// Returns `true` when the RPC server reports a transaction already present in
132    /// the UTXO set (`RPC_VERIFY_ALREADY_IN_UTXO_SET`, code `-27`).
133    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    /// Returns `true` when retrying the same RPC request may succeed later.
138    ///
139    /// This classifies transport failures, timeouts, request-construction failures that can
140    /// depend on transient client state, exhausted client-side retries, and HTTP 5xx responses
141    /// as retriable. Bitcoin Core JSON-RPC server errors are operation-specific and return
142    /// `false`; callers should handle those according to the RPC they invoked.
143    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    /// Returns `true` when the RPC server reports missing or invalid transaction
156    /// inputs (`RPC_VERIFY_ERROR`, code `-25`).
157    #[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            // Connection errors
170            BitreqError::AddressNotFound
171            | BitreqError::IoError(_)
172            | BitreqError::RustlsCreateConnection(_) => ClientError::Connection(value.to_string()),
173
174            // Redirect errors
175            BitreqError::RedirectLocationMissing
176            | BitreqError::InfiniteRedirectionLoop
177            | BitreqError::TooManyRedirections => ClientError::HttpRedirect(value.to_string()),
178
179            // Size/parsing errors
180            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            // Other errors
192            _ => 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/// `bitcoind` RPC server error.
204#[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/// Error returned when signing a raw transaction with a wallet fails.
223#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
224pub struct SignRawTransactionWithWalletError {
225    /// The transaction ID.
226    txid: String,
227    /// The index of the input.
228    vout: u32,
229    /// The script signature.
230    #[serde(rename = "scriptSig")]
231    script_sig: String,
232    /// The sequence number.
233    sequence: u32,
234    /// The error message.
235    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/// Error returned when RPC client expects a different version than bitcoind reports.
249#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
250pub struct UnexpectedServerVersionError {
251    /// Version from server.
252    pub got: usize,
253    /// Expected server version.
254    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}