Skip to main content

bsv_wallet_toolbox/
error.rs

1//! Error types for the BSV Wallet Toolbox.
2//!
3//! Provides a unified `WalletError` enum with variants matching all WERR codes
4//! from the TypeScript wallet-toolbox. Each variant includes contextual data and
5//! displays with a WERR prefix for wire-format consistency.
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10/// Convenience type alias for Results using `WalletError`.
11pub type WalletResult<T> = Result<T, WalletError>;
12
13/// Unified error type for all wallet operations.
14///
15/// Each variant maps to a WERR error code from the TypeScript wallet-toolbox.
16/// Display output always includes the WERR prefix (e.g., "WERR_INTERNAL: description").
17#[derive(Debug, Error)]
18pub enum WalletError {
19    /// An internal error with a descriptive message.
20    #[error("WERR_INTERNAL: {0}")]
21    Internal(String),
22
23    /// A parameter has an invalid value.
24    #[error("WERR_INVALID_PARAMETER: The {parameter} parameter must be {must_be}")]
25    InvalidParameter {
26        /// The name of the invalid parameter.
27        parameter: String,
28        /// Description of the valid value constraint.
29        must_be: String,
30    },
31
32    /// The requested operation is not yet implemented.
33    #[error("WERR_NOT_IMPLEMENTED: {0}")]
34    NotImplemented(String),
35
36    /// The request was malformed or invalid.
37    #[error("WERR_BAD_REQUEST: {0}")]
38    BadRequest(String),
39
40    /// The caller is not authenticated or authorized.
41    #[error("WERR_UNAUTHORIZED: {0}")]
42    Unauthorized(String),
43
44    /// The wallet or resource is not in an active state.
45    #[error("WERR_NOT_ACTIVE: {0}")]
46    NotActive(String),
47
48    /// The operation is not valid in the current state.
49    #[error("WERR_INVALID_OPERATION: {0}")]
50    InvalidOperation(String),
51
52    /// A required parameter was not provided.
53    #[error("WERR_MISSING_PARAMETER: The required {0} parameter is missing.")]
54    MissingParameter(String),
55
56    /// The wallet does not have enough funds for the operation.
57    #[error("WERR_INSUFFICIENT_FUNDS: {message}")]
58    InsufficientFunds {
59        /// Human-readable description of the shortfall.
60        message: String,
61        /// Total satoshis required for the operation.
62        total_satoshis_needed: i64,
63        /// Additional satoshis needed beyond available balance.
64        more_satoshis_needed: i64,
65    },
66
67    /// No broadcast service is currently available.
68    #[error("WERR_BROADCAST_UNAVAILABLE: Unable to broadcast transaction at this time.")]
69    BroadcastUnavailable,
70
71    /// A network or chain-related error occurred.
72    #[error("WERR_NETWORK_CHAIN: {0}")]
73    NetworkChain(String),
74
75    /// A public key value is invalid.
76    #[error("WERR_INVALID_PUBLIC_KEY: {message}")]
77    InvalidPublicKey {
78        /// Description of why the key is invalid.
79        message: String,
80        /// The invalid key value.
81        key: String,
82    },
83
84    /// A database error from sqlx.
85    #[error("WERR_INTERNAL: {0}")]
86    Sqlx(#[from] sqlx::Error),
87
88    /// An I/O error.
89    #[error("WERR_INTERNAL: {0}")]
90    Io(#[from] std::io::Error),
91
92    /// A JSON serialization/deserialization error.
93    #[error("WERR_INTERNAL: {0}")]
94    SerdeJson(#[from] serde_json::Error),
95}
96
97impl WalletError {
98    /// Returns the WERR string code for wire serialization.
99    pub fn code(&self) -> &'static str {
100        match self {
101            Self::Internal(_) | Self::Sqlx(_) | Self::Io(_) | Self::SerdeJson(_) => "WERR_INTERNAL",
102            Self::InvalidParameter { .. } => "WERR_INVALID_PARAMETER",
103            Self::NotImplemented(_) => "WERR_NOT_IMPLEMENTED",
104            Self::BadRequest(_) => "WERR_BAD_REQUEST",
105            Self::Unauthorized(_) => "WERR_UNAUTHORIZED",
106            Self::NotActive(_) => "WERR_NOT_ACTIVE",
107            Self::InvalidOperation(_) => "WERR_INVALID_OPERATION",
108            Self::MissingParameter(_) => "WERR_MISSING_PARAMETER",
109            Self::InsufficientFunds { .. } => "WERR_INSUFFICIENT_FUNDS",
110            Self::BroadcastUnavailable => "WERR_BROADCAST_UNAVAILABLE",
111            Self::NetworkChain(_) => "WERR_NETWORK_CHAIN",
112            Self::InvalidPublicKey { .. } => "WERR_INVALID_PUBLIC_KEY",
113        }
114    }
115
116    /// Converts this error into a `WalletErrorObject` suitable for JSON wire format.
117    ///
118    /// This matches the TypeScript `WalletError.toJson()` output format.
119    pub fn to_wallet_error_object(&self) -> WalletErrorObject {
120        WalletErrorObject {
121            is_error: true,
122            name: self.code().to_string(),
123            message: self.to_string(),
124            code: None,
125            parameter: match self {
126                Self::InvalidParameter { parameter, .. } => Some(parameter.clone()),
127                Self::MissingParameter(p) => Some(p.clone()),
128                _ => None,
129            },
130            total_satoshis_needed: match self {
131                Self::InsufficientFunds {
132                    total_satoshis_needed,
133                    ..
134                } => Some(*total_satoshis_needed),
135                _ => None,
136            },
137            more_satoshis_needed: match self {
138                Self::InsufficientFunds {
139                    more_satoshis_needed,
140                    ..
141                } => Some(*more_satoshis_needed),
142                _ => None,
143            },
144        }
145    }
146}
147
148/// Convert a `WalletErrorObject` received over the wire into the appropriate `WalletError` variant.
149///
150/// Maps WERR error code strings (e.g. "WERR_INVALID_PARAMETER") from a JSON-RPC error
151/// response body onto the local `WalletError` enum. Used by `StorageClient::rpc_call`
152/// after deserializing a remote error payload.
153pub fn wallet_error_from_object(obj: WalletErrorObject) -> WalletError {
154    match obj.name.as_str() {
155        "WERR_INVALID_PARAMETER" => WalletError::InvalidParameter {
156            parameter: obj.parameter.unwrap_or_default(),
157            must_be: obj.message,
158        },
159        "WERR_NOT_IMPLEMENTED" => WalletError::NotImplemented(obj.message),
160        "WERR_BAD_REQUEST" => WalletError::BadRequest(obj.message),
161        "WERR_UNAUTHORIZED" => WalletError::Unauthorized(obj.message),
162        "WERR_NOT_ACTIVE" => WalletError::NotActive(obj.message),
163        "WERR_INVALID_OPERATION" => WalletError::InvalidOperation(obj.message),
164        "WERR_MISSING_PARAMETER" => {
165            WalletError::MissingParameter(obj.parameter.unwrap_or_else(|| obj.message.clone()))
166        }
167        "WERR_INSUFFICIENT_FUNDS" => WalletError::InsufficientFunds {
168            message: obj.message,
169            total_satoshis_needed: obj.total_satoshis_needed.unwrap_or(0),
170            more_satoshis_needed: obj.more_satoshis_needed.unwrap_or(0),
171        },
172        "WERR_BROADCAST_UNAVAILABLE" => WalletError::BroadcastUnavailable,
173        "WERR_NETWORK_CHAIN" => WalletError::NetworkChain(obj.message),
174        "WERR_INVALID_PUBLIC_KEY" => WalletError::InvalidPublicKey {
175            message: obj.message,
176            key: obj.parameter.unwrap_or_default(),
177        },
178        _ => WalletError::Internal(obj.message),
179    }
180}
181
182/// JSON wire format for wallet errors, matching the TypeScript `WalletError.toJson()` output.
183#[derive(Debug, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct WalletErrorObject {
186    /// Always `true` to indicate an error response.
187    pub is_error: bool,
188    /// The WERR error code string (e.g., "WERR_INTERNAL").
189    pub name: String,
190    /// Human-readable error message.
191    pub message: String,
192    /// Optional numeric error code.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub code: Option<u8>,
195    /// The parameter name, if the error relates to a specific parameter.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub parameter: Option<String>,
198    /// Total satoshis needed, present only for insufficient funds errors.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub total_satoshis_needed: Option<i64>,
201    /// Additional satoshis needed, present only for insufficient funds errors.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub more_satoshis_needed: Option<i64>,
204}