spark_rust/
error.rs

1//! # Error handling for the Spark Wallet SDK
2//!
3//! This module defines the error types used throughout the Spark Wallet SDK.
4//! The error system uses a hierarchical structure, with the top-level `SparkSdkError`
5//! enum wrapping various subcategory errors.
6//!
7//! ## Error Structure
8//!
9//! The error structure follows a domain-driven design:
10//!
11//! ```text
12//! SparkSdkError
13//! ├── NetworkError - Network communication and RPC issues
14//! ├── WalletError - Wallet operations and leaf management
15//! ├── CryptoError - Cryptographic and signing operations
16//! ├── ValidationError - Input validation and verification
17//! ├── TransactionError - Bitcoin transaction handling
18//! ├── IoError - Input/output and serialization
19//! ├── InternalError - SDK initialization and internal processing
20//! └── General - General errors with a string message
21//! ```
22//!
23//! ## Error Handling
24//!
25//! Most functions in the SDK return `Result<T, SparkSdkError>`, allowing you to handle
26//! errors with standard Rust error handling patterns:
27//!
28//! ```rust
29//! use spark_rust::SparkSdk;
30//! use spark_rust::SparkNetwork;
31//! use spark_rust::error::SparkSdkError;
32//! use spark_rust::signer::default_signer::DefaultSigner;
33//!
34//! async fn example() -> Result<(), SparkSdkError> {
35//!     let mnemonic = "abandon ability able about above absent absorb abstract absurd abuse access accident";
36//!     let signer = DefaultSigner::from_mnemonic(mnemonic, SparkNetwork::Regtest).await?;
37//!     let sdk = SparkSdk::new(SparkNetwork::Regtest, signer).await?;
38//!     
39//!     // When you need to handle specific error categories
40//!     match sdk.generate_deposit_address().await {
41//!         Ok(address) => println!("Deposit address: {}", address.deposit_address),
42//!         Err(SparkSdkError::Network(e)) => println!("Network error: {}", e),
43//!         Err(SparkSdkError::Validation(e)) => println!("Validation error: {}", e),
44//!         Err(e) => println!("Other error: {}", e),
45//!     }
46//!     
47//!     Ok(())
48//! }
49//! ```
50//!
51//! ## Error Conversion
52//!
53//! The error system implements `From` traits for common error types, allowing
54//! automatic conversion using the `?` operator:
55//!
56//! - `String` and `&str` convert to `SparkSdkError::General`
57//! - `tonic::transport::Error` converts to `SparkSdkError::Network`
58//! - `secp256k1::Error` converts to `SparkSdkError::Crypto`
59//!
60//! This makes error propagation and handling more ergonomic throughout the SDK.
61
62use bitcoin::secp256k1;
63use lightning_invoice::ParseOrSemanticError;
64use std::io;
65use thiserror::Error;
66use tonic::codegen::http::uri::InvalidUri;
67
68// Re-export all errors from subcategories defined below
69pub use self::crypto::CryptoError;
70pub use self::internal::InternalError;
71pub use self::io_error::IoError;
72pub use self::network::NetworkError;
73pub use self::transaction::TransactionError;
74pub use self::validation::ValidationError;
75pub use self::wallet::WalletError;
76
77/// Main error type for the Spark Wallet SDK.
78///
79/// This enum wraps all subcategory errors and provides a uniform error
80/// interface for SDK functions. Most SDK methods return `Result<T, SparkSdkError>`.
81///
82/// When handling errors, you can either match on the specific error type:
83///
84/// ```rust
85/// # use spark_rust::error::SparkSdkError;
86/// # fn example(error: SparkSdkError) {
87/// match error {
88///     SparkSdkError::Network(network_error) => {
89///         // Handle network-specific errors
90///         println!("Network error: {}", network_error);
91///     },
92///     SparkSdkError::Wallet(wallet_error) => {
93///         // Handle wallet-specific errors
94///         println!("Wallet error: {}", wallet_error);
95///     },
96///     // Other error categories...
97///     _ => println!("Other error: {}", error),
98/// }
99/// # }
100/// ```
101///
102/// Or simply use the error's `Display` implementation for general handling:
103///
104/// ```rust
105/// # use spark_rust::error::SparkSdkError;
106/// # fn example(error: SparkSdkError) {
107/// println!("Error occurred: {}", error);
108/// # }
109/// ```
110#[derive(Debug, Error)]
111pub enum SparkSdkError {
112    /// Network communication errors, including RPC calls and HTTP requests.
113    #[error(transparent)]
114    Network(#[from] NetworkError),
115
116    /// Wallet operation errors, including leaf management and funds handling.
117    #[error(transparent)]
118    Wallet(#[from] WalletError),
119
120    /// Cryptographic operation errors, including signing and key management.
121    #[error(transparent)]
122    Crypto(#[from] CryptoError),
123
124    /// Input validation errors, parameter checking, and verification issues.
125    #[error(transparent)]
126    Validation(#[from] ValidationError),
127
128    /// Bitcoin transaction errors, including creation and processing issues.
129    #[error(transparent)]
130    Transaction(#[from] TransactionError),
131
132    /// Input/output errors, including serialization and file operations.
133    #[error(transparent)]
134    Io(#[from] IoError),
135
136    /// SDK internal errors, typically for initialization or processing problems.
137    #[error(transparent)]
138    Internal(#[from] InternalError),
139
140    /// General errors with a string message for cases that don't fit other categories.
141    #[error("General error: {0}")]
142    General(String),
143}
144
145/// Network-related errors for RPC, HTTP, and other communication issues.
146pub mod network {
147    use super::*;
148
149    /// Errors related to network communication, including RPC calls and HTTP requests.
150    #[derive(Debug, Error)]
151    pub enum NetworkError {
152        /// gRPC transport layer errors.
153        #[error("Transport error: {0}")]
154        Transport(#[from] tonic::transport::Error),
155
156        /// HTTP client errors from reqwest.
157        #[error("HTTP error: {0}")]
158        Http(#[from] reqwest::Error),
159
160        /// gRPC status errors.
161        #[error("Status error: {0}")]
162        Status(#[from] tonic::Status),
163
164        /// Invalid URI format errors.
165        #[error("Invalid URI: {0}")]
166        InvalidUri(#[from] InvalidUri),
167
168        /// Invalid URL format with optional details.
169        #[error("Invalid URL: {url}")]
170        InvalidUrl {
171            url: String,
172            details: Option<String>,
173        },
174
175        /// RPC connection failures with endpoint information.
176        #[error("RPC connection error with endpoint: {uri}")]
177        RpcConnection {
178            uri: String,
179            details: Option<String>,
180        },
181
182        /// Bitcoin RPC specific errors.
183        #[error("Bitcoin RPC error: {0}")]
184        BitcoinRpc(String),
185
186        /// Invalid GraphQL operation format.
187        #[error("Invalid GraphQL operation: {0}")]
188        InvalidGraphQLOperation(String),
189
190        /// GraphQL query errors.
191        #[error("GraphQL error with query: {0}")]
192        GraphQL(String),
193
194        /// GraphQL request failures with status code.
195        #[error("GraphQL request failed: {status_code}")]
196        GraphQLRequestFailed { status_code: u16 },
197
198        /// Faucet request failures with status code.
199        #[error("Faucet request failed with status: {status_code}")]
200        FaucetRequestFailed { status_code: u16 },
201
202        /// Invalid responses from the server that can't be parsed.
203        #[error("Invalid response from server")]
204        InvalidResponse,
205    }
206}
207
208/// Wallet-related errors for leaf management and funds handling.
209pub mod wallet {
210    use super::*;
211
212    /// Errors related to wallet operations, leaf management, and funds handling.
213    #[derive(Debug, Error)]
214    pub enum WalletError {
215        /// No leaves (UTXOs) found in the wallet.
216        #[error("No leaves found for wallet")]
217        NoLeavesFound,
218
219        /// No leaf with the exact amount requested.
220        #[error("No leaf with exact amount")]
221        NoLeafWithExactAmount,
222
223        /// Insufficient funds during leaf selection.
224        #[error("Leaf selection failed: insufficient funds")]
225        LeafSelectionInsufficientFunds,
226
227        /// No suitable leaves available for the requested operation.
228        #[error("Leaf selection failed: no suitable leaves available")]
229        LeafSelectionNoSuitableLeaves,
230
231        /// Leaf not found with the specified ID.
232        #[error("Leaf not found with id: {0}")]
233        LeafNotFound(String),
234
235        /// Leaf was used for an operation but not found afterward.
236        #[error("Leaf is used for operation but not found after. Leaf ID: {leaf_id}")]
237        LeafNotFoundAfterOperation { leaf_id: String },
238
239        /// Leaf is not of Bitcoin type.
240        #[error("Leaf is not Bitcoin type: {leaf_id}")]
241        LeafIsNotBitcoin { leaf_id: String },
242
243        /// Leaf already exists in the wallet.
244        #[error("Leaf already exists in wallet: {leaf_id}")]
245        LeafAlreadyExistsInWallet { leaf_id: String },
246
247        /// Leaf is not available for use (e.g., locked or in use).
248        #[error("Leaf not available for use: {leaf_id}")]
249        LeafNotAvailableForUse { leaf_id: String },
250
251        /// Leaf is not using the expected lock.
252        #[error("Leaf not using expected lock. Expected: {expected}, Found: {actual}")]
253        LeafNotUsingExpectedLock { expected: String, actual: String },
254
255        /// Leaf not found in the wallet.
256        #[error("Leaf not found in wallet: {leaf_id}")]
257        LeafNotFoundInWallet { leaf_id: String },
258
259        /// Invalid account specified.
260        #[error("Invalid account: {account_id}")]
261        InvalidAccount { account_id: String },
262
263        /// Cooperative exit failed.
264        #[error("Cooperative exit failed: {reason}")]
265        CooperativeExit { reason: String },
266
267        /// Leaf has no parent.
268        #[error("Leaf has no parent: {leaf_id}")]
269        LeafHasNoParent { leaf_id: String },
270
271        /// Leaf parent not found.
272        #[error("Leaf parent not found: {leaf_id}")]
273        LeafParentNotFound { leaf_id: String },
274
275        /// Post refresh node signature length mismatch.
276        #[error("Post refresh node signature length mismatch")]
277        PostRefreshNodeSignatureLengthMismatch,
278    }
279}
280
281/// Cryptographic operation errors for signing and key management.
282pub mod crypto {
283    use super::*;
284
285    /// Errors related to cryptographic operations, including signing and key management.
286    #[derive(Debug, Error)]
287    pub enum CryptoError {
288        /// Secp256k1 cryptographic errors.
289        #[error("Signature error: {0}")]
290        Secp256k1(#[from] secp256k1::Error),
291
292        /// FROST threshold signing failures.
293        #[error("FROST signing failed with job id: {job_id}")]
294        FrostSigning { job_id: String },
295
296        /// FROST signature aggregation failures.
297        #[error("FROST aggregation failed")]
298        FrostAggregation,
299
300        /// Invalid hash value.
301        #[error("Invalid hash: {hash}")]
302        InvalidHash { hash: String },
303
304        /// Decryption failures.
305        #[error("Decryption failed")]
306        Decryption,
307
308        /// Secret key not found for a public key.
309        #[error("Secret key not found for public key: {public_key}")]
310        SecretKeyNotFound { public_key: String },
311
312        /// Missing keyshare information for threshold signing.
313        #[error("Missing keyshare information")]
314        MissingKeyshareInfo,
315
316        /// FROST signature not found with the specified job ID.
317        #[error("Could not find FROST signature with job id: {job_id}")]
318        FrostSignatureNotFound { job_id: String },
319
320        /// Invalid key type for the operation.
321        #[error("Invalid key type: {key_type}")]
322        InvalidKeyType { key_type: String },
323
324        /// Invalid seed for key derivation.
325        #[error("Invalid seed")]
326        InvalidSeed,
327
328        /// Child key derivation failures.
329        #[error("Child key derivation failed with path: {derivation_path}")]
330        ChildKeyDerivationError { derivation_path: String },
331
332        /// Missing root node signature shares.
333        #[error("Missing root node signature shares")]
334        MissingRootNodeSignatureShares,
335
336        /// Invalid cryptographic input for operations.
337        #[error("Invalid crypto input: {field}")]
338        InvalidInput { field: String },
339    }
340}
341
342/// Validation errors for input checking and verification.
343pub mod validation {
344    use super::*;
345
346    /// Errors related to input validation, parameter checking, and verification.
347    #[derive(Debug, Error)]
348    pub enum ValidationError {
349        /// Invalid configuration parameter.
350        #[error("Invalid configuration: {parameter}")]
351        Config { parameter: String },
352
353        /// Invalid input field value.
354        #[error("Invalid input: {field}")]
355        InvalidInput { field: String },
356
357        /// Invalid function argument.
358        #[error("Invalid argument: {argument}")]
359        InvalidArgument { argument: String },
360
361        /// Invalid Bitcoin address format.
362        #[error("Invalid address: {address}")]
363        InvalidAddress { address: String },
364
365        /// Deposit address validation failures.
366        #[error("Deposit address validation failed: {address}")]
367        DepositAddressValidationFailed { address: String },
368
369        /// Deposit transaction not found in the Bitcoin network.
370        #[error("Deposit transaction not found in network: {txid}")]
371        DepositTransactionNotFoundInNetwork { txid: String },
372
373        /// Invalid keyshare configuration.
374        #[error("Invalid keyshare config with id: {config_id}")]
375        InvalidKeyshareConfig { config_id: String },
376
377        /// Invalid sequence number.
378        #[error("Invalid sequence number: {sequence}")]
379        InvalidSequence { sequence: u64 },
380
381        /// Invalid UUID format.
382        #[error("Invalid UUID: {0}")]
383        InvalidUuid(#[from] uuid::Error),
384
385        /// Invalid BOLT11 lightning invoice format.
386        #[error("Invalid Bolt11 invoice: {0}")]
387        InvalidBolt11Invoice(String),
388
389        /// Transfer claim did not update balance (integration tests only).
390        #[cfg(feature = "integration-tests")]
391        #[error("Transfer claim did not update balance")]
392        TransferClaimDidNotUpdateBalance,
393    }
394}
395
396/// Transaction errors for Bitcoin transaction handling.
397pub mod transaction {
398    use super::*;
399
400    /// Errors related to Bitcoin transaction creation, signing, and processing.
401    #[derive(Debug, Error)]
402    pub enum TransactionError {
403        /// Transfer operation failures.
404        #[error("Transfer failed: {reason}")]
405        Transfer { reason: String },
406
407        /// Invalid Bitcoin transaction format.
408        #[error("Invalid bitcoin transaction: {0}")]
409        InvalidBitcoinTransaction(String),
410
411        /// Invalid deposit transaction format.
412        #[error("Invalid deposit transaction: {0}")]
413        InvalidDepositTx(String),
414    }
415}
416
417/// IO errors for serialization and file operations.
418pub mod io_error {
419
420    use super::*;
421
422    /// Errors related to input/output, serialization, and file operations.
423    #[derive(Debug, Error)]
424    pub enum IoError {
425        /// Standard IO errors.
426        #[error("IO error: {0}")]
427        Io(#[from] io::Error),
428
429        /// JSON serialization/deserialization errors.
430        #[error("JSON serialization error: {0}")]
431        SerdeJson(#[from] serde_json::Error),
432
433        /// Hex decoding errors.
434        #[error("Hex decoding error: {0}")]
435        Decoding(#[from] hex::FromHexError),
436
437        /// BOLT11 invoice decoding errors.
438        #[error("Bolt11 invoice decoding error: {0}")]
439        Bolt11InvoiceDecoding(String),
440    }
441}
442
443/// Internal errors for SDK initialization and processing.
444pub mod internal {
445    use super::*;
446
447    /// Errors related to SDK initialization and internal processing.
448    #[derive(Debug, Error)]
449    pub enum InternalError {
450        /// Initialization failures for SDK components.
451        #[error("Initialization failed: {component}")]
452        Initialization { component: String },
453
454        /// Tree creation failures.
455        #[error("Tree creation failed: {reason}")]
456        TreeCreation { reason: String },
457
458        /// Test database query errors (for testing only).
459        #[error("Test database query error: {query}")]
460        TestDatabaseQuery { query: String },
461
462        /// Async task failures.
463        #[error("Async task failed: {task}")]
464        AsyncTask { task: String },
465    }
466}
467
468// Implement From traits for common conversions
469impl From<String> for SparkSdkError {
470    /// Converts a String to a SparkSdkError::General.
471    ///
472    /// This allows passing a string directly as an error:
473    /// ```rust
474    /// # use spark_rust::error::SparkSdkError;
475    /// # fn example() -> Result<(), SparkSdkError> {
476    /// if something_is_wrong() {
477    ///     return Err("Something went wrong".to_string().into());
478    /// }
479    /// # Ok(())
480    /// # }
481    /// # fn something_is_wrong() -> bool { false }
482    /// ```
483    fn from(s: String) -> Self {
484        Self::General(s)
485    }
486}
487
488impl From<&str> for SparkSdkError {
489    /// Converts a string slice to a SparkSdkError::General.
490    ///
491    /// This allows passing a string literal directly as an error:
492    /// ```rust
493    /// # use spark_rust::error::SparkSdkError;
494    /// # fn example() -> Result<(), SparkSdkError> {
495    /// if something_is_wrong() {
496    ///     return Err("Something went wrong".into());
497    /// }
498    /// # Ok(())
499    /// # }
500    /// # fn something_is_wrong() -> bool { false }
501    /// ```
502    fn from(s: &str) -> Self {
503        Self::General(s.to_string())
504    }
505}
506
507// Add direct conversion from tonic transport errors
508impl From<tonic::transport::Error> for SparkSdkError {
509    /// Converts a tonic transport error to a SparkSdkError::Network.
510    fn from(err: tonic::transport::Error) -> Self {
511        Self::Network(NetworkError::Transport(err))
512    }
513}
514
515// Add direct conversion from secp256k1 errors
516impl From<secp256k1::Error> for SparkSdkError {
517    /// Converts a secp256k1 error to a SparkSdkError::Crypto.
518    fn from(err: secp256k1::Error) -> Self {
519        Self::Crypto(CryptoError::Secp256k1(err))
520    }
521}
522
523// Add manual conversion from ParseOrSemanticError
524impl From<ParseOrSemanticError> for IoError {
525    /// Converts a BOLT11 invoice parsing error to an IoError.
526    fn from(err: ParseOrSemanticError) -> Self {
527        Self::Bolt11InvoiceDecoding(err.to_string())
528    }
529}