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