amp_rs/signer/
error.rs

1use thiserror::Error;
2
3/// Comprehensive error types for signer operations
4///
5/// This enum covers all possible error scenarios that can occur during
6/// transaction signing operations, providing detailed context for debugging
7/// and proper error handling in client applications.
8#[derive(Error, Debug)]
9pub enum SignerError {
10    /// LWK-specific errors from the Liquid Wallet Kit
11    ///
12    /// This variant captures errors from LWK operations including:
13    /// - `SwSigner` creation failures
14    /// - Transaction signing failures  
15    /// - PSET (Partially Signed Element Transaction) operations
16    /// - Key derivation and cryptographic operations
17    #[error("LWK signing operation failed: {0}")]
18    Lwk(String),
19
20    /// Invalid mnemonic phrase errors
21    ///
22    /// This variant captures mnemonic-related errors including:
23    /// - Invalid word count (not 12, 15, 18, 21, or 24 words)
24    /// - Invalid characters or formatting
25    /// - BIP39 checksum validation failures
26    /// - Empty or malformed mnemonic phrases
27    #[error("Invalid mnemonic phrase: {0}")]
28    InvalidMnemonic(String),
29
30    /// Hex string parsing and decoding errors
31    ///
32    /// This variant captures errors when parsing hex-encoded data including:
33    /// - Invalid hex characters
34    /// - Odd-length hex strings
35    /// - Empty hex strings
36    /// - Malformed transaction hex data
37    #[error("Hex parsing failed: {0}")]
38    HexParse(#[from] hex::FromHexError),
39
40    /// Invalid transaction structure or content errors
41    ///
42    /// This variant captures transaction validation errors including:
43    /// - Malformed transaction structure
44    /// - Missing inputs or outputs
45    /// - Invalid transaction serialization
46    /// - PSET conversion failures
47    /// - Transaction size or format issues
48    #[error("Invalid transaction structure: {0}")]
49    InvalidTransaction(String),
50
51    /// Network-related communication errors
52    ///
53    /// This variant captures network errors that may occur during
54    /// remote operations or API calls (reserved for future use).
55    #[error("Network communication failed: {0}")]
56    Network(#[from] reqwest::Error),
57
58    /// JSON serialization and deserialization errors
59    ///
60    /// This variant captures JSON processing errors including:
61    /// - Mnemonic file parsing failures
62    /// - Invalid JSON structure in storage files
63    /// - Serialization failures when writing storage
64    #[error("JSON serialization failed: {0}")]
65    Serialization(#[from] serde_json::Error),
66
67    /// File system I/O operation errors
68    ///
69    /// This variant captures file operation errors including:
70    /// - Mnemonic file read/write failures
71    /// - Permission denied errors
72    /// - Disk space or filesystem issues
73    /// - Atomic write operation failures
74    #[error("File I/O operation failed: {0}")]
75    FileIo(#[from] std::io::Error),
76}
77
78// Additional error conversions for external library errors
79// These provide seamless integration with third-party error types
80
81/// Convert BIP39 mnemonic errors to `SignerError`
82///
83/// This conversion handles all BIP39-related errors including:
84/// - Invalid word count
85/// - Invalid words not in BIP39 wordlist  
86/// - Checksum validation failures
87/// - Language detection issues
88impl From<bip39::Error> for SignerError {
89    fn from(err: bip39::Error) -> Self {
90        Self::InvalidMnemonic(format!("BIP39 validation failed: {err}"))
91    }
92}
93
94/// Convert Elements transaction encoding errors to `SignerError`
95///
96/// This conversion handles transaction serialization/deserialization errors
97/// from the Elements library including:
98/// - Consensus encoding failures
99/// - Invalid transaction structure
100/// - Serialization format errors
101impl From<elements::encode::Error> for SignerError {
102    fn from(err: elements::encode::Error) -> Self {
103        Self::InvalidTransaction(format!("Elements transaction encoding failed: {err}"))
104    }
105}
106
107// Note: LWK errors are handled manually in the implementation code
108// rather than through automatic conversion. This provides better control
109// over error context and allows for operation-specific error messages
110// that help with debugging and troubleshooting.
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::io::{Error as IoError, ErrorKind};
116
117    #[test]
118    fn test_lwk_error_variant() {
119        let error_msg = "SwSigner creation failed";
120        let error = SignerError::Lwk(error_msg.to_string());
121
122        // Test error message formatting
123        let formatted = format!("{}", error);
124        assert_eq!(
125            formatted,
126            "LWK signing operation failed: SwSigner creation failed"
127        );
128
129        // Test debug formatting
130        let debug_formatted = format!("{:?}", error);
131        assert!(debug_formatted.contains("Lwk"));
132        assert!(debug_formatted.contains(error_msg));
133
134        // Test error source (should be None for this variant)
135        assert!(std::error::Error::source(&error).is_none());
136    }
137
138    #[test]
139    fn test_invalid_mnemonic_error_variant() {
140        let error_msg = "Invalid word count: expected 12, got 5";
141        let error = SignerError::InvalidMnemonic(error_msg.to_string());
142
143        // Test error message formatting
144        let formatted = format!("{}", error);
145        assert_eq!(
146            formatted,
147            "Invalid mnemonic phrase: Invalid word count: expected 12, got 5"
148        );
149
150        // Test debug formatting
151        let debug_formatted = format!("{:?}", error);
152        assert!(debug_formatted.contains("InvalidMnemonic"));
153        assert!(debug_formatted.contains(error_msg));
154
155        // Test error source (should be None for this variant)
156        assert!(std::error::Error::source(&error).is_none());
157    }
158
159    #[test]
160    fn test_hex_parse_error_conversion() {
161        // Create a hex parsing error by trying to decode invalid hex
162        let hex_result = hex::decode("invalid_hex_zz");
163        let hex_error = hex_result.unwrap_err();
164        let signer_error = SignerError::from(hex_error);
165
166        // Test error variant
167        match signer_error {
168            SignerError::HexParse(_) => {} // Expected
169            other => panic!("Expected HexParse variant, got: {:?}", other),
170        }
171
172        // Test error message formatting
173        let formatted = format!("{}", signer_error);
174        assert!(formatted.starts_with("Hex parsing failed:"));
175
176        // Test error source preservation
177        let source = std::error::Error::source(&signer_error);
178        assert!(source.is_some());
179    }
180
181    #[test]
182    fn test_invalid_transaction_error_variant() {
183        let error_msg = "Transaction deserialization failed: invalid input count";
184        let error = SignerError::InvalidTransaction(error_msg.to_string());
185
186        // Test error message formatting
187        let formatted = format!("{}", error);
188        assert_eq!(formatted, "Invalid transaction structure: Transaction deserialization failed: invalid input count");
189
190        // Test debug formatting
191        let debug_formatted = format!("{:?}", error);
192        assert!(debug_formatted.contains("InvalidTransaction"));
193        assert!(debug_formatted.contains(error_msg));
194
195        // Test error source (should be None for this variant)
196        assert!(std::error::Error::source(&error).is_none());
197    }
198
199    #[test]
200    fn test_network_error_conversion() {
201        // Create a reqwest error by making an invalid request
202        let client = reqwest::Client::new();
203        let request_result = client.get("http://").build();
204        let reqwest_error = request_result.unwrap_err();
205        let signer_error = SignerError::from(reqwest_error);
206
207        // Test error variant
208        match signer_error {
209            SignerError::Network(_) => {} // Expected
210            other => panic!("Expected Network variant, got: {:?}", other),
211        }
212
213        // Test error message formatting
214        let formatted = format!("{}", signer_error);
215        assert!(formatted.starts_with("Network communication failed:"));
216
217        // Test error source preservation
218        let source = std::error::Error::source(&signer_error);
219        assert!(source.is_some());
220    }
221
222    #[test]
223    fn test_serialization_error_conversion() {
224        // Create a JSON serialization error by trying to parse invalid JSON
225        let invalid_json = r#"{"invalid": json syntax"#;
226        let json_error = serde_json::from_str::<serde_json::Value>(invalid_json).unwrap_err();
227        let signer_error = SignerError::from(json_error);
228
229        // Test error variant
230        match signer_error {
231            SignerError::Serialization(_) => {} // Expected
232            other => panic!("Expected Serialization variant, got: {:?}", other),
233        }
234
235        // Test error message formatting
236        let formatted = format!("{}", signer_error);
237        assert!(formatted.starts_with("JSON serialization failed:"));
238
239        // Test error source preservation
240        let source = std::error::Error::source(&signer_error);
241        assert!(source.is_some());
242    }
243
244    #[test]
245    fn test_file_io_error_conversion() {
246        // Create an I/O error
247        let io_error = IoError::new(ErrorKind::NotFound, "File not found");
248        let signer_error = SignerError::from(io_error);
249
250        // Test error variant
251        match signer_error {
252            SignerError::FileIo(_) => {} // Expected
253            other => panic!("Expected FileIo variant, got: {:?}", other),
254        }
255
256        // Test error message formatting
257        let formatted = format!("{}", signer_error);
258        assert!(formatted.starts_with("File I/O operation failed:"));
259        assert!(formatted.contains("File not found"));
260
261        // Test error source preservation
262        let source = std::error::Error::source(&signer_error);
263        assert!(source.is_some());
264        assert_eq!(source.unwrap().to_string(), "File not found");
265    }
266
267    #[test]
268    fn test_bip39_error_conversion() {
269        // Create a BIP39 error (invalid word count)
270        let bip39_error = bip39::Error::BadWordCount(5);
271        let signer_error = SignerError::from(bip39_error);
272
273        // Test error variant
274        match &signer_error {
275            SignerError::InvalidMnemonic(msg) => {
276                assert!(msg.contains("BIP39 validation failed"));
277                assert!(msg.contains("word count"));
278            }
279            other => panic!("Expected InvalidMnemonic variant, got: {:?}", other),
280        }
281
282        // Test error message formatting
283        let formatted = format!("{}", signer_error);
284        assert!(formatted.starts_with("Invalid mnemonic phrase:"));
285        assert!(formatted.contains("BIP39 validation failed"));
286    }
287
288    #[test]
289    fn test_elements_encode_error_conversion() {
290        // Create an Elements encoding error
291        let elements_error = elements::encode::Error::ParseFailed("Invalid transaction format");
292        let signer_error = SignerError::from(elements_error);
293
294        // Test error variant
295        match &signer_error {
296            SignerError::InvalidTransaction(msg) => {
297                assert!(msg.contains("Elements transaction encoding failed"));
298                assert!(msg.contains("Invalid transaction format"));
299            }
300            other => panic!("Expected InvalidTransaction variant, got: {:?}", other),
301        }
302
303        // Test error message formatting
304        let formatted = format!("{}", signer_error);
305        assert!(formatted.starts_with("Invalid transaction structure:"));
306        assert!(formatted.contains("Elements transaction encoding failed"));
307    }
308
309    #[test]
310    fn test_error_chain_preservation() {
311        // Test that error chains are properly preserved through conversions
312
313        // Create a nested I/O error
314        let inner_error = IoError::new(ErrorKind::PermissionDenied, "Access denied");
315        let signer_error = SignerError::from(inner_error);
316
317        // Test that the source chain is preserved
318        let source = std::error::Error::source(&signer_error);
319        assert!(source.is_some());
320        assert_eq!(source.unwrap().to_string(), "Access denied");
321
322        // Test error message includes context
323        let formatted = format!("{}", signer_error);
324        assert!(formatted.contains("File I/O operation failed"));
325        assert!(formatted.contains("Access denied"));
326    }
327
328    #[test]
329    fn test_error_send_sync_traits() {
330        // Test that SignerError implements Send and Sync for async compatibility
331        fn assert_send<T: Send>() {}
332        fn assert_sync<T: Sync>() {}
333
334        assert_send::<SignerError>();
335        assert_sync::<SignerError>();
336    }
337
338    #[test]
339    fn test_error_static_lifetime() {
340        // Test that SignerError can be used with 'static lifetime
341        fn assert_static<T: 'static>() {}
342        assert_static::<SignerError>();
343    }
344
345    #[test]
346    fn test_all_error_variants_display() {
347        // Test that all error variants have proper Display implementations
348        let errors = vec![
349            SignerError::Lwk("test lwk error".to_string()),
350            SignerError::InvalidMnemonic("test mnemonic error".to_string()),
351            SignerError::HexParse(hex::decode("invalid_hex_zz").unwrap_err()),
352            SignerError::InvalidTransaction("test transaction error".to_string()),
353            SignerError::Network(reqwest::Client::new().get("http://").build().unwrap_err()),
354            SignerError::Serialization(
355                serde_json::from_str::<serde_json::Value>(r#"{"invalid": json"#).unwrap_err(),
356            ),
357            SignerError::FileIo(IoError::new(ErrorKind::NotFound, "test file error")),
358        ];
359
360        for error in errors {
361            let display_str = format!("{}", error);
362            assert!(
363                !display_str.is_empty(),
364                "Error display should not be empty: {:?}",
365                error
366            );
367
368            let debug_str = format!("{:?}", error);
369            assert!(
370                !debug_str.is_empty(),
371                "Error debug should not be empty: {:?}",
372                error
373            );
374        }
375    }
376
377    #[test]
378    fn test_error_message_preservation() {
379        // Test that custom error messages are preserved correctly
380        let test_cases = vec![
381            (
382                "LWK operation failed with code 123",
383                SignerError::Lwk("LWK operation failed with code 123".to_string()),
384            ),
385            (
386                "Mnemonic has invalid checksum",
387                SignerError::InvalidMnemonic("Mnemonic has invalid checksum".to_string()),
388            ),
389            (
390                "Transaction missing required inputs",
391                SignerError::InvalidTransaction("Transaction missing required inputs".to_string()),
392            ),
393        ];
394
395        for (expected_msg, error) in test_cases {
396            let formatted = format!("{}", error);
397            assert!(
398                formatted.contains(expected_msg),
399                "Error message should contain '{}', got: '{}'",
400                expected_msg,
401                formatted
402            );
403        }
404    }
405
406    #[test]
407    fn test_error_propagation_through_result() {
408        // Test error propagation through Result types (simulating call stack)
409        fn level_3() -> Result<(), SignerError> {
410            Err(SignerError::Lwk("Deep error".to_string()))
411        }
412
413        fn level_2() -> Result<(), SignerError> {
414            level_3()?;
415            Ok(())
416        }
417
418        fn level_1() -> Result<(), SignerError> {
419            level_2()?;
420            Ok(())
421        }
422
423        let result = level_1();
424        assert!(result.is_err());
425
426        match result.unwrap_err() {
427            SignerError::Lwk(msg) => assert_eq!(msg, "Deep error"),
428            other => panic!("Expected Lwk error, got: {:?}", other),
429        }
430    }
431
432    #[test]
433    fn test_error_conversion_preserves_context() {
434        // Test that automatic conversions preserve error context
435
436        // Test hex error conversion
437        let hex_result: Result<Vec<u8>, hex::FromHexError> = hex::decode("invalid_hex_zz");
438        let hex_error = hex_result.unwrap_err();
439        let signer_error = SignerError::from(hex_error);
440
441        let formatted = format!("{}", signer_error);
442        assert!(formatted.contains("Hex parsing failed"));
443
444        // Test I/O error conversion
445        let io_error = IoError::new(
446            ErrorKind::PermissionDenied,
447            "Cannot write to read-only file",
448        );
449        let signer_error = SignerError::from(io_error);
450
451        let formatted = format!("{}", signer_error);
452        assert!(formatted.contains("File I/O operation failed"));
453        assert!(formatted.contains("Cannot write to read-only file"));
454    }
455
456    #[test]
457    fn test_error_debug_format_completeness() {
458        // Test that debug format includes all relevant information
459        let error = SignerError::InvalidMnemonic("Test mnemonic error with details".to_string());
460        let debug_str = format!("{:#?}", error);
461
462        // Debug format should include variant name and message
463        assert!(debug_str.contains("InvalidMnemonic"));
464        assert!(debug_str.contains("Test mnemonic error with details"));
465    }
466
467    #[test]
468    fn test_multiple_error_conversions() {
469        // Test multiple error conversions in sequence
470        let test_cases = vec![
471            "invalid_hex_zz", // Invalid character
472            "abc",            // Odd length
473        ];
474
475        for invalid_hex in test_cases {
476            let hex_error = hex::decode(invalid_hex).unwrap_err();
477            let signer_error = SignerError::from(hex_error);
478            match signer_error {
479                SignerError::HexParse(_) => {} // Expected
480                other => panic!("Expected HexParse variant, got: {:?}", other),
481            }
482
483            // Verify error message is meaningful
484            let formatted = format!("{}", signer_error);
485            assert!(formatted.starts_with("Hex parsing failed:"));
486        }
487    }
488
489    #[test]
490    fn test_error_equality_and_comparison() {
491        // Test that errors can be compared for debugging purposes
492        let error1 = SignerError::Lwk("same message".to_string());
493        let error2 = SignerError::Lwk("same message".to_string());
494        let error3 = SignerError::Lwk("different message".to_string());
495
496        // Note: SignerError doesn't implement PartialEq by design (errors often contain
497        // non-comparable types), but we can test that the debug representations are consistent
498        let debug1 = format!("{:?}", error1);
499        let debug2 = format!("{:?}", error2);
500        let debug3 = format!("{:?}", error3);
501
502        assert_eq!(debug1, debug2);
503        assert_ne!(debug1, debug3);
504    }
505}