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}