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
10use bsv::wallet::interfaces::{ReviewActionResult, SendWithResult};
11use bsv::wallet::types::OutpointString;
12
13/// Convenience type alias for Results using `WalletError`.
14pub type WalletResult<T> = Result<T, WalletError>;
15
16/// Unified error type for all wallet operations.
17///
18/// Each variant maps to a WERR error code from the TypeScript wallet-toolbox.
19/// Display output always includes the WERR prefix (e.g., "WERR_INTERNAL: description").
20#[derive(Debug, Error)]
21pub enum WalletError {
22    /// An internal error with a descriptive message.
23    #[error("WERR_INTERNAL: {0}")]
24    Internal(String),
25
26    /// A parameter has an invalid value.
27    #[error("WERR_INVALID_PARAMETER: The {parameter} parameter must be {must_be}")]
28    InvalidParameter {
29        /// The name of the invalid parameter.
30        parameter: String,
31        /// Description of the valid value constraint.
32        must_be: String,
33    },
34
35    /// The requested operation is not yet implemented.
36    #[error("WERR_NOT_IMPLEMENTED: {0}")]
37    NotImplemented(String),
38
39    /// The request was malformed or invalid.
40    #[error("WERR_BAD_REQUEST: {0}")]
41    BadRequest(String),
42
43    /// The caller is not authenticated or authorized.
44    #[error("WERR_UNAUTHORIZED: {0}")]
45    Unauthorized(String),
46
47    /// The wallet or resource is not in an active state.
48    #[error("WERR_NOT_ACTIVE: {0}")]
49    NotActive(String),
50
51    /// The operation is not valid in the current state.
52    #[error("WERR_INVALID_OPERATION: {0}")]
53    InvalidOperation(String),
54
55    /// A required parameter was not provided.
56    #[error("WERR_MISSING_PARAMETER: The required {0} parameter is missing.")]
57    MissingParameter(String),
58
59    /// The wallet does not have enough funds for the operation.
60    #[error("WERR_INSUFFICIENT_FUNDS: {message}")]
61    InsufficientFunds {
62        /// Human-readable description of the shortfall.
63        message: String,
64        /// Total satoshis required for the operation.
65        total_satoshis_needed: i64,
66        /// Additional satoshis needed beyond available balance.
67        more_satoshis_needed: i64,
68    },
69
70    /// No broadcast service is currently available.
71    #[error("WERR_BROADCAST_UNAVAILABLE: Unable to broadcast transaction at this time.")]
72    BroadcastUnavailable,
73
74    /// A network or chain-related error occurred.
75    #[error("WERR_NETWORK_CHAIN: {0}")]
76    NetworkChain(String),
77
78    /// A public key value is invalid.
79    #[error("WERR_INVALID_PUBLIC_KEY: {message}")]
80    InvalidPublicKey {
81        /// Description of why the key is invalid.
82        message: String,
83        /// The invalid key value.
84        key: String,
85    },
86
87    /// Undelayed broadcast results require review (code=5).
88    #[error("WERR_REVIEW_ACTIONS: {message}")]
89    ReviewActions {
90        /// Human-readable message describing the review requirement.
91        message: String,
92        /// Results from the undelayed broadcast review.
93        review_action_results: Vec<ReviewActionResult>,
94        /// Results from batch sending.
95        send_with_results: Vec<SendWithResult>,
96        /// The transaction ID, if available.
97        txid: Option<String>,
98        /// The raw transaction bytes, if available.
99        tx: Option<Vec<u8>>,
100        /// Outpoints of change outputs for noSend transactions.
101        no_send_change: Vec<OutpointString>,
102    },
103
104    /// Invalid merkle root detected during chain validation (code=8).
105    #[error("WERR_INVALID_MERKLE_ROOT: {message}")]
106    InvalidMerkleRoot {
107        /// Human-readable message describing the invalid merkle root.
108        message: String,
109        /// The hash of the block with the invalid merkle root.
110        block_hash: String,
111        /// The height of the block with the invalid merkle root.
112        block_height: u32,
113        /// The invalid merkle root value.
114        merkle_root: String,
115        /// The transaction ID involved, if available.
116        txid: Option<String>,
117    },
118
119    /// A database error from sqlx.
120    #[error("WERR_INTERNAL: {0}")]
121    Sqlx(#[from] sqlx::Error),
122
123    /// An I/O error.
124    #[error("WERR_INTERNAL: {0}")]
125    Io(#[from] std::io::Error),
126
127    /// A JSON serialization/deserialization error.
128    #[error("WERR_INTERNAL: {0}")]
129    SerdeJson(#[from] serde_json::Error),
130}
131
132impl WalletError {
133    /// Returns the WERR string code for wire serialization.
134    pub fn code(&self) -> &'static str {
135        match self {
136            Self::Internal(_) | Self::Sqlx(_) | Self::Io(_) | Self::SerdeJson(_) => "WERR_INTERNAL",
137            Self::InvalidParameter { .. } => "WERR_INVALID_PARAMETER",
138            Self::NotImplemented(_) => "WERR_NOT_IMPLEMENTED",
139            Self::BadRequest(_) => "WERR_BAD_REQUEST",
140            Self::Unauthorized(_) => "WERR_UNAUTHORIZED",
141            Self::NotActive(_) => "WERR_NOT_ACTIVE",
142            Self::InvalidOperation(_) => "WERR_INVALID_OPERATION",
143            Self::MissingParameter(_) => "WERR_MISSING_PARAMETER",
144            Self::InsufficientFunds { .. } => "WERR_INSUFFICIENT_FUNDS",
145            Self::BroadcastUnavailable => "WERR_BROADCAST_UNAVAILABLE",
146            Self::NetworkChain(_) => "WERR_NETWORK_CHAIN",
147            Self::InvalidPublicKey { .. } => "WERR_INVALID_PUBLIC_KEY",
148            Self::ReviewActions { .. } => "WERR_REVIEW_ACTIONS",
149            Self::InvalidMerkleRoot { .. } => "WERR_INVALID_MERKLE_ROOT",
150        }
151    }
152
153    /// Converts this error into a `WalletErrorObject` suitable for JSON wire format.
154    ///
155    /// This matches the TypeScript `WalletError.toJson()` output format.
156    pub fn to_wallet_error_object(&self) -> WalletErrorObject {
157        let mut obj = WalletErrorObject {
158            is_error: true,
159            name: self.code().to_string(),
160            message: self.to_string(),
161            code: None,
162            parameter: None,
163            total_satoshis_needed: None,
164            more_satoshis_needed: None,
165            review_action_results: None,
166            send_with_results: None,
167            txid: None,
168            tx: None,
169            no_send_change: None,
170            block_hash: None,
171            block_height: None,
172            merkle_root: None,
173            key: None,
174        };
175
176        match self {
177            Self::InvalidParameter { parameter, .. } => {
178                obj.parameter = Some(parameter.clone());
179            }
180            Self::MissingParameter(p) => {
181                obj.parameter = Some(p.clone());
182            }
183            Self::InsufficientFunds {
184                total_satoshis_needed,
185                more_satoshis_needed,
186                ..
187            } => {
188                obj.total_satoshis_needed = Some(*total_satoshis_needed);
189                obj.more_satoshis_needed = Some(*more_satoshis_needed);
190            }
191            Self::InvalidPublicKey { key, .. } => {
192                obj.key = Some(key.clone());
193            }
194            Self::ReviewActions {
195                review_action_results,
196                send_with_results,
197                txid,
198                tx,
199                no_send_change,
200                ..
201            } => {
202                obj.code = Some(5);
203                obj.review_action_results = Some(review_action_results.clone());
204                obj.send_with_results = Some(send_with_results.clone());
205                obj.txid = txid.clone();
206                obj.tx = tx.clone();
207                obj.no_send_change = Some(no_send_change.clone());
208            }
209            Self::InvalidMerkleRoot {
210                block_hash,
211                block_height,
212                merkle_root,
213                txid,
214                ..
215            } => {
216                obj.code = Some(8);
217                obj.block_hash = Some(block_hash.clone());
218                obj.block_height = Some(*block_height);
219                obj.merkle_root = Some(merkle_root.clone());
220                obj.txid = txid.clone();
221            }
222            _ => {}
223        }
224
225        obj
226    }
227}
228
229/// Convert a `WalletErrorObject` received over the wire into the appropriate `WalletError` variant.
230///
231/// Maps WERR error code strings (e.g. "WERR_INVALID_PARAMETER") from a JSON-RPC error
232/// response body onto the local `WalletError` enum. Used by `StorageClient::rpc_call`
233/// after deserializing a remote error payload.
234pub fn wallet_error_from_object(obj: WalletErrorObject) -> WalletError {
235    match obj.name.as_str() {
236        "WERR_INVALID_PARAMETER" => WalletError::InvalidParameter {
237            parameter: obj.parameter.unwrap_or_default(),
238            must_be: obj.message,
239        },
240        "WERR_NOT_IMPLEMENTED" => WalletError::NotImplemented(obj.message),
241        "WERR_BAD_REQUEST" => WalletError::BadRequest(obj.message),
242        "WERR_UNAUTHORIZED" => WalletError::Unauthorized(obj.message),
243        "WERR_NOT_ACTIVE" => WalletError::NotActive(obj.message),
244        "WERR_INVALID_OPERATION" => WalletError::InvalidOperation(obj.message),
245        "WERR_MISSING_PARAMETER" => {
246            WalletError::MissingParameter(obj.parameter.unwrap_or_else(|| obj.message.clone()))
247        }
248        "WERR_INSUFFICIENT_FUNDS" => WalletError::InsufficientFunds {
249            message: obj.message,
250            total_satoshis_needed: obj.total_satoshis_needed.unwrap_or(0),
251            more_satoshis_needed: obj.more_satoshis_needed.unwrap_or(0),
252        },
253        "WERR_BROADCAST_UNAVAILABLE" => WalletError::BroadcastUnavailable,
254        "WERR_NETWORK_CHAIN" => WalletError::NetworkChain(obj.message),
255        "WERR_INVALID_PUBLIC_KEY" => WalletError::InvalidPublicKey {
256            message: obj.message,
257            key: obj.key.or(obj.parameter).unwrap_or_default(),
258        },
259        "WERR_REVIEW_ACTIONS" => WalletError::ReviewActions {
260            message: obj.message,
261            review_action_results: obj.review_action_results.unwrap_or_default(),
262            send_with_results: obj.send_with_results.unwrap_or_default(),
263            txid: obj.txid,
264            tx: obj.tx,
265            no_send_change: obj.no_send_change.unwrap_or_default(),
266        },
267        "WERR_INVALID_MERKLE_ROOT" => WalletError::InvalidMerkleRoot {
268            message: obj.message,
269            block_hash: obj.block_hash.unwrap_or_default(),
270            block_height: obj.block_height.unwrap_or(0),
271            merkle_root: obj.merkle_root.unwrap_or_default(),
272            txid: obj.txid,
273        },
274        _ => WalletError::Internal(obj.message),
275    }
276}
277
278/// JSON wire format for wallet errors, matching the TypeScript `WalletError.toJson()` output.
279#[derive(Debug, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct WalletErrorObject {
282    /// Always `true` to indicate an error response.
283    pub is_error: bool,
284    /// The WERR error code string (e.g., "WERR_INTERNAL").
285    pub name: String,
286    /// Human-readable error message.
287    pub message: String,
288    /// Optional numeric error code.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub code: Option<u8>,
291    /// The parameter name, if the error relates to a specific parameter.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub parameter: Option<String>,
294    /// Total satoshis needed, present only for insufficient funds errors.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub total_satoshis_needed: Option<i64>,
297    /// Additional satoshis needed, present only for insufficient funds errors.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub more_satoshis_needed: Option<i64>,
300    /// Review action results for WERR_REVIEW_ACTIONS.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub review_action_results: Option<Vec<ReviewActionResult>>,
303    /// Send-with results for WERR_REVIEW_ACTIONS.
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub send_with_results: Option<Vec<SendWithResult>>,
306    /// Transaction ID for WERR_REVIEW_ACTIONS and WERR_INVALID_MERKLE_ROOT.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub txid: Option<String>,
309    /// Raw transaction bytes for WERR_REVIEW_ACTIONS.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub tx: Option<Vec<u8>>,
312    /// No-send change outpoints for WERR_REVIEW_ACTIONS.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub no_send_change: Option<Vec<OutpointString>>,
315    /// Block hash for WERR_INVALID_MERKLE_ROOT.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub block_hash: Option<String>,
318    /// Block height for WERR_INVALID_MERKLE_ROOT.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub block_height: Option<u32>,
321    /// Merkle root for WERR_INVALID_MERKLE_ROOT.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub merkle_root: Option<String>,
324    /// The invalid key value for WERR_INVALID_PUBLIC_KEY.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub key: Option<String>,
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use bsv::wallet::interfaces::{
333        ActionResultStatus, ReviewActionResult, ReviewActionResultStatus, SendWithResult,
334    };
335
336    #[test]
337    fn test_review_actions_error_roundtrip() {
338        let err = WalletError::ReviewActions {
339            message: "Undelayed results require review.".to_string(),
340            review_action_results: vec![ReviewActionResult {
341                txid: "aabb".to_string(),
342                status: ReviewActionResultStatus::DoubleSpend,
343                competing_txs: Some(vec!["ccdd".to_string()]),
344                competing_beef: None,
345            }],
346            send_with_results: vec![SendWithResult {
347                txid: "eeff".to_string(),
348                status: ActionResultStatus::Sending,
349            }],
350            txid: Some("aabb".to_string()),
351            tx: None,
352            no_send_change: vec![],
353        };
354        assert_eq!(err.code(), "WERR_REVIEW_ACTIONS");
355        let obj = err.to_wallet_error_object();
356        assert_eq!(obj.code, Some(5));
357        assert!(obj.review_action_results.is_some());
358        let err2 = wallet_error_from_object(obj);
359        assert_eq!(err2.code(), "WERR_REVIEW_ACTIONS");
360    }
361
362    #[test]
363    fn test_invalid_merkle_root_error_roundtrip() {
364        let err = WalletError::InvalidMerkleRoot {
365            message: "Invalid merkleRoot abc for block def at height 100".to_string(),
366            block_hash: "def".to_string(),
367            block_height: 100,
368            merkle_root: "abc".to_string(),
369            txid: Some("1234".to_string()),
370        };
371        assert_eq!(err.code(), "WERR_INVALID_MERKLE_ROOT");
372        let obj = err.to_wallet_error_object();
373        assert_eq!(obj.code, Some(8));
374        assert_eq!(obj.block_hash.as_deref(), Some("def"));
375        let err2 = wallet_error_from_object(obj);
376        assert_eq!(err2.code(), "WERR_INVALID_MERKLE_ROOT");
377    }
378
379    #[test]
380    fn test_invalid_public_key_key_field_roundtrip() {
381        let err = WalletError::InvalidPublicKey {
382            message: "not on curve".to_string(),
383            key: "02abcdef".to_string(),
384        };
385        let obj = err.to_wallet_error_object();
386        assert_eq!(obj.key.as_deref(), Some("02abcdef"));
387        let json = serde_json::to_string(&obj).unwrap();
388        assert!(json.contains("\"key\":\"02abcdef\""));
389        let obj2: WalletErrorObject = serde_json::from_str(&json).unwrap();
390        let err2 = wallet_error_from_object(obj2);
391        match err2 {
392            WalletError::InvalidPublicKey { key, .. } => assert_eq!(key, "02abcdef"),
393            _ => panic!("Expected InvalidPublicKey"),
394        }
395    }
396}