use std::fmt;
use bitcoin::Network;
use bitreq::Error as BitreqError;
use serde::{Deserialize, Serialize};
use serde_json::Error as SerdeJsonError;
use thiserror::Error;
const RPC_VERIFY_ERROR: i32 = -25;
const RPC_VERIFY_REJECTED: i32 = -26;
const RPC_VERIFY_ALREADY_IN_UTXO_SET: i32 = -27;
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ClientError {
#[error("Missing username or password")]
MissingUserPassword,
#[error("RPC server returned error '{1}' (code {0})")]
Server(i32, String),
#[error("Error parsing rpc response: {0}")]
Parse(String),
#[error("Could not create RPC Param")]
Param(String),
#[error("{0}")]
Body(String),
#[error("Obtained failure status({0}): {1}")]
Status(u16, String),
#[error("Malformed Response: {0}")]
MalformedResponse(String),
#[error("Could not connect: {0}")]
Connection(String),
#[error("Timeout")]
Timeout,
#[error("HttpRedirect: {0}")]
HttpRedirect(String),
#[error("Could not build request: {0}")]
ReqBuilder(String),
#[error("Max retries {0} exceeded")]
MaxRetriesExceeded(u16),
#[error("Could not create request: {0}")]
Request(String),
#[error("Network address: {0}")]
WrongNetworkAddress(Network),
#[error(transparent)]
UnexpectedServerVersion(#[from] UnexpectedServerVersionError),
#[error(transparent)]
Sign(#[from] SignRawTransactionWithWalletError),
#[error("Could not get xpriv from wallet")]
Xpriv,
#[error("{0}")]
Other(String),
}
impl ClientError {
pub fn is_tx_not_found(&self) -> bool {
matches!(self, Self::Server(-5, _))
}
pub fn is_block_not_found(&self) -> bool {
matches!(self, Self::Server(-5, _))
}
pub fn is_rpc_verify_error(&self) -> bool {
matches!(self, Self::Server(RPC_VERIFY_ERROR, _))
}
pub fn is_rpc_verify_rejected(&self) -> bool {
matches!(self, Self::Server(RPC_VERIFY_REJECTED, _))
}
pub fn is_rpc_verify_already_in_utxo_set(&self) -> bool {
matches!(self, Self::Server(RPC_VERIFY_ALREADY_IN_UTXO_SET, _))
}
pub fn is_retriable(&self) -> bool {
matches!(
self,
Self::Connection(_)
| Self::Timeout
| Self::Request(_)
| Self::Param(_)
| Self::MaxRetriesExceeded(_)
| Self::Status(500..=599, _)
)
}
#[deprecated(
since = "0.10.4",
note = "use is_rpc_verify_error() to detect RPC_VERIFY_ERROR (-25)"
)]
pub fn is_missing_or_invalid_input(&self) -> bool {
self.is_rpc_verify_error()
}
}
impl From<BitreqError> for ClientError {
fn from(value: BitreqError) -> Self {
match value {
BitreqError::AddressNotFound
| BitreqError::IoError(_)
| BitreqError::RustlsCreateConnection(_) => ClientError::Connection(value.to_string()),
BitreqError::RedirectLocationMissing
| BitreqError::InfiniteRedirectionLoop
| BitreqError::TooManyRedirections => ClientError::HttpRedirect(value.to_string()),
BitreqError::HeadersOverflow
| BitreqError::StatusLineOverflow
| BitreqError::BodyOverflow
| BitreqError::MalformedChunkLength
| BitreqError::MalformedChunkEnd
| BitreqError::MalformedContentLength
| BitreqError::InvalidUtf8InResponse
| BitreqError::InvalidUtf8InBody(_) => {
ClientError::MalformedResponse(value.to_string())
}
_ => ClientError::Other(value.to_string()),
}
}
}
impl From<SerdeJsonError> for ClientError {
fn from(value: SerdeJsonError) -> Self {
Self::Parse(format!("Could not parse {value}"))
}
}
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitcoinRpcError {
pub code: i32,
pub message: String,
}
impl fmt::Display for BitcoinRpcError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "RPC error {}: {}", self.code, self.message)
}
}
impl From<BitcoinRpcError> for ClientError {
fn from(value: BitcoinRpcError) -> Self {
Self::Server(value.code, value.message)
}
}
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SignRawTransactionWithWalletError {
txid: String,
vout: u32,
#[serde(rename = "scriptSig")]
script_sig: String,
sequence: u32,
error: String,
}
impl fmt::Display for SignRawTransactionWithWalletError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"error signing raw transaction with wallet: {}",
self.error
)
}
}
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnexpectedServerVersionError {
pub got: usize,
pub expected: Vec<usize>,
}
impl fmt::Display for UnexpectedServerVersionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut expected = String::new();
for version in &self.expected {
let v = format!(" {version} ");
expected.push_str(&v);
}
write!(
f,
"unexpected bitcoind version, got: {} expected one of: {}",
self.got, expected
)
}
}
#[cfg(test)]
mod tests {
#![allow(deprecated)]
use super::ClientError;
#[test]
fn classifies_rpc_verify_error() {
let error = ClientError::Server(-25, "Input not found or already spent".to_string());
assert!(error.is_rpc_verify_error());
assert!(error.is_missing_or_invalid_input());
assert!(!error.is_rpc_verify_rejected());
assert!(!error.is_rpc_verify_already_in_utxo_set());
}
#[test]
fn classifies_rpc_verify_rejected() {
let error = ClientError::Server(-26, "txn-already-in-mempool".to_string());
assert!(error.is_rpc_verify_rejected());
assert!(!error.is_missing_or_invalid_input());
assert!(!error.is_rpc_verify_error());
assert!(!error.is_rpc_verify_already_in_utxo_set());
}
#[test]
fn classifies_rpc_verify_already_in_utxo_set() {
let error = ClientError::Server(-27, "transaction already in block chain".to_string());
assert!(error.is_rpc_verify_already_in_utxo_set());
assert!(!error.is_rpc_verify_error());
assert!(!error.is_rpc_verify_rejected());
assert!(!error.is_missing_or_invalid_input());
}
#[test]
fn non_server_errors_do_not_match_rpc_code_helpers() {
let error = ClientError::Timeout;
assert!(!error.is_rpc_verify_error());
assert!(!error.is_rpc_verify_rejected());
assert!(!error.is_rpc_verify_already_in_utxo_set());
assert!(!error.is_missing_or_invalid_input());
}
#[test]
fn classifies_retriable_client_errors() {
assert!(ClientError::Connection("connection refused".to_string()).is_retriable());
assert!(ClientError::Timeout.is_retriable());
assert!(ClientError::Request("request failed".to_string()).is_retriable());
assert!(ClientError::Param("failed to create params".to_string()).is_retriable());
assert!(ClientError::MaxRetriesExceeded(3).is_retriable());
assert!(ClientError::Status(500, "internal server error".to_string()).is_retriable());
assert!(ClientError::Status(503, "service unavailable".to_string()).is_retriable());
assert!(ClientError::Status(599, "network connect timeout".to_string()).is_retriable());
}
#[test]
fn classifies_non_retriable_client_errors() {
assert!(!ClientError::MissingUserPassword.is_retriable());
assert!(
!ClientError::Server(-25, "bad-txns-inputs-missingorspent".to_string()).is_retriable()
);
assert!(!ClientError::Parse("bad json".to_string()).is_retriable());
assert!(!ClientError::Body("body error".to_string()).is_retriable());
assert!(!ClientError::MalformedResponse("bad response".to_string()).is_retriable());
assert!(!ClientError::Status(400, "bad request".to_string()).is_retriable());
assert!(!ClientError::Status(401, "unauthorized".to_string()).is_retriable());
assert!(!ClientError::Status(499, "client closed request".to_string()).is_retriable());
assert!(!ClientError::HttpRedirect("too many redirects".to_string()).is_retriable());
assert!(!ClientError::ReqBuilder("invalid request".to_string()).is_retriable());
assert!(!ClientError::WrongNetworkAddress(bitcoin::Network::Regtest).is_retriable());
assert!(!ClientError::Xpriv.is_retriable());
assert!(!ClientError::Other("unknown".to_string()).is_retriable());
}
}