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, not retryable
51    #[error("Obtained failure status({0}): {1}")]
52    Status(u16, String),
53
54    /// Error decoding the response, retry might not help
55    #[error("Malformed Response: {0}")]
56    MalformedResponse(String),
57
58    /// Connection error, retry might help
59    #[error("Could not connect: {0}")]
60    Connection(String),
61
62    /// Timeout error, retry might help
63    #[error("Timeout")]
64    Timeout,
65
66    /// Redirect error, not retryable
67    #[error("HttpRedirect: {0}")]
68    HttpRedirect(String),
69
70    /// Error building the request, unlikely to be recoverable
71    #[error("Could not build request: {0}")]
72    ReqBuilder(String),
73
74    /// Maximum retries exceeded, not retryable
75    #[error("Max retries {0} exceeded")]
76    MaxRetriesExceeded(u8),
77
78    /// General request error, retry might help
79    #[error("Could not create request: {0}")]
80    Request(String),
81
82    /// Wrong network address
83    #[error("Network address: {0}")]
84    WrongNetworkAddress(Network),
85
86    /// Server version is unexpected or incompatible
87    #[error(transparent)]
88    UnexpectedServerVersion(#[from] UnexpectedServerVersionError),
89
90    /// Could not sign raw transaction
91    #[error(transparent)]
92    Sign(#[from] SignRawTransactionWithWalletError),
93
94    /// Could not get a [`Xpriv`](bitcoin::bip32::Xpriv) from the wallet
95    #[error("Could not get xpriv from wallet")]
96    Xpriv,
97
98    /// Unknown error, unlikely to be recoverable
99    #[error("{0}")]
100    Other(String),
101}
102
103impl ClientError {
104    /// Returns `true` when the RPC server reports an invalid address, key, or missing
105    /// transaction/block identifier (`RPC_INVALID_ADDRESS_OR_KEY`, code `-5`).
106    pub fn is_tx_not_found(&self) -> bool {
107        matches!(self, Self::Server(-5, _))
108    }
109
110    /// Returns `true` when the RPC server reports an invalid address, key, or missing
111    /// transaction/block identifier (`RPC_INVALID_ADDRESS_OR_KEY`, code `-5`).
112    pub fn is_block_not_found(&self) -> bool {
113        matches!(self, Self::Server(-5, _))
114    }
115
116    /// Returns `true` when the RPC server reports a general transaction or block
117    /// submission verification error (`RPC_VERIFY_ERROR`, code `-25`).
118    pub fn is_rpc_verify_error(&self) -> bool {
119        matches!(self, Self::Server(RPC_VERIFY_ERROR, _))
120    }
121
122    /// Returns `true` when the RPC server reports a transaction or block rejected
123    /// by network rules (`RPC_VERIFY_REJECTED`, code `-26`).
124    pub fn is_rpc_verify_rejected(&self) -> bool {
125        matches!(self, Self::Server(RPC_VERIFY_REJECTED, _))
126    }
127
128    /// Returns `true` when the RPC server reports a transaction already present in
129    /// the UTXO set (`RPC_VERIFY_ALREADY_IN_UTXO_SET`, code `-27`).
130    pub fn is_rpc_verify_already_in_utxo_set(&self) -> bool {
131        matches!(self, Self::Server(RPC_VERIFY_ALREADY_IN_UTXO_SET, _))
132    }
133
134    /// Returns `true` when the RPC server reports missing or invalid transaction
135    /// inputs (`RPC_VERIFY_ERROR`, code `-25`).
136    #[deprecated(
137        since = "0.10.4",
138        note = "use is_rpc_verify_error() to detect RPC_VERIFY_ERROR (-25)"
139    )]
140    pub fn is_missing_or_invalid_input(&self) -> bool {
141        self.is_rpc_verify_error()
142    }
143}
144
145impl From<BitreqError> for ClientError {
146    fn from(value: BitreqError) -> Self {
147        match value {
148            // Connection errors
149            BitreqError::AddressNotFound
150            | BitreqError::IoError(_)
151            | BitreqError::RustlsCreateConnection(_) => ClientError::Connection(value.to_string()),
152
153            // Redirect errors
154            BitreqError::RedirectLocationMissing
155            | BitreqError::InfiniteRedirectionLoop
156            | BitreqError::TooManyRedirections => ClientError::HttpRedirect(value.to_string()),
157
158            // Size/parsing errors
159            BitreqError::HeadersOverflow
160            | BitreqError::StatusLineOverflow
161            | BitreqError::BodyOverflow
162            | BitreqError::MalformedChunkLength
163            | BitreqError::MalformedChunkEnd
164            | BitreqError::MalformedContentLength
165            | BitreqError::InvalidUtf8InResponse
166            | BitreqError::InvalidUtf8InBody(_) => {
167                ClientError::MalformedResponse(value.to_string())
168            }
169
170            // Other errors
171            _ => ClientError::Other(value.to_string()),
172        }
173    }
174}
175
176impl From<SerdeJsonError> for ClientError {
177    fn from(value: SerdeJsonError) -> Self {
178        Self::Parse(format!("Could not parse {value}"))
179    }
180}
181
182/// `bitcoind` RPC server error.
183#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub struct BitcoinRpcError {
185    pub code: i32,
186    pub message: String,
187}
188
189impl fmt::Display for BitcoinRpcError {
190    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
191        write!(f, "RPC error {}: {}", self.code, self.message)
192    }
193}
194
195impl From<BitcoinRpcError> for ClientError {
196    fn from(value: BitcoinRpcError) -> Self {
197        Self::Server(value.code, value.message)
198    }
199}
200
201/// Error returned when signing a raw transaction with a wallet fails.
202#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct SignRawTransactionWithWalletError {
204    /// The transaction ID.
205    txid: String,
206    /// The index of the input.
207    vout: u32,
208    /// The script signature.
209    #[serde(rename = "scriptSig")]
210    script_sig: String,
211    /// The sequence number.
212    sequence: u32,
213    /// The error message.
214    error: String,
215}
216
217impl fmt::Display for SignRawTransactionWithWalletError {
218    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
219        write!(
220            f,
221            "error signing raw transaction with wallet: {}",
222            self.error
223        )
224    }
225}
226
227/// Error returned when RPC client expects a different version than bitcoind reports.
228#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct UnexpectedServerVersionError {
230    /// Version from server.
231    pub got: usize,
232    /// Expected server version.
233    pub expected: Vec<usize>,
234}
235
236impl fmt::Display for UnexpectedServerVersionError {
237    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
238        let mut expected = String::new();
239        for version in &self.expected {
240            let v = format!(" {version} ");
241            expected.push_str(&v);
242        }
243        write!(
244            f,
245            "unexpected bitcoind version, got: {} expected one of: {}",
246            self.got, expected
247        )
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    #![allow(deprecated)]
254
255    use super::ClientError;
256
257    #[test]
258    fn classifies_rpc_verify_error() {
259        let error = ClientError::Server(-25, "Input not found or already spent".to_string());
260
261        assert!(error.is_rpc_verify_error());
262        assert!(error.is_missing_or_invalid_input());
263        assert!(!error.is_rpc_verify_rejected());
264        assert!(!error.is_rpc_verify_already_in_utxo_set());
265    }
266
267    #[test]
268    fn classifies_rpc_verify_rejected() {
269        let error = ClientError::Server(-26, "txn-already-in-mempool".to_string());
270
271        assert!(error.is_rpc_verify_rejected());
272        assert!(!error.is_missing_or_invalid_input());
273        assert!(!error.is_rpc_verify_error());
274        assert!(!error.is_rpc_verify_already_in_utxo_set());
275    }
276
277    #[test]
278    fn classifies_rpc_verify_already_in_utxo_set() {
279        let error = ClientError::Server(-27, "transaction already in block chain".to_string());
280
281        assert!(error.is_rpc_verify_already_in_utxo_set());
282        assert!(!error.is_rpc_verify_error());
283        assert!(!error.is_rpc_verify_rejected());
284        assert!(!error.is_missing_or_invalid_input());
285    }
286
287    #[test]
288    fn non_server_errors_do_not_match_rpc_code_helpers() {
289        let error = ClientError::Timeout;
290
291        assert!(!error.is_rpc_verify_error());
292        assert!(!error.is_rpc_verify_rejected());
293        assert!(!error.is_rpc_verify_already_in_utxo_set());
294        assert!(!error.is_missing_or_invalid_input());
295    }
296}