amp-rust 0.0.9

A Rust client for the Blockstream AMP API, providing interfaces for asset management, user operations, and token handling on the Liquid Network.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
use thiserror::Error;

/// Comprehensive error types for signer operations
///
/// This enum covers all possible error scenarios that can occur during
/// transaction signing operations, providing detailed context for debugging
/// and proper error handling in client applications.
#[derive(Error, Debug)]
pub enum SignerError {
    /// LWK-specific errors from the Liquid Wallet Kit
    ///
    /// This variant captures errors from LWK operations including:
    /// - `SwSigner` creation failures
    /// - Transaction signing failures  
    /// - PSET (Partially Signed Element Transaction) operations
    /// - Key derivation and cryptographic operations
    #[error("LWK signing operation failed: {0}")]
    Lwk(String),
    #[error("LWK signing operation failed: {error_message}\n\nOperation: {operation}\n\nContext: {context}")]
    LwkDetailed {
        /// The LWK operation that failed (e.g., "`sign_transaction`", "`create_signer`")
        operation: String,
        /// Additional context about the operation
        context: String,
        /// The error message from LWK
        error_message: String,
    },

    /// Invalid mnemonic phrase errors
    ///
    /// This variant captures mnemonic-related errors including:
    /// - Invalid word count (not 12, 15, 18, 21, or 24 words)
    /// - Invalid characters or formatting
    /// - BIP39 checksum validation failures
    /// - Empty or malformed mnemonic phrases
    #[error("Invalid mnemonic phrase: {0}")]
    InvalidMnemonic(String),

    /// Hex string parsing and decoding errors
    ///
    /// This variant captures errors when parsing hex-encoded data including:
    /// - Invalid hex characters
    /// - Odd-length hex strings
    /// - Empty hex strings
    /// - Malformed transaction hex data
    #[error("Hex parsing failed: {0}")]
    HexParse(#[from] hex::FromHexError),
    #[error("Hex parsing failed: {hex_error}\n\nParsing: {parsing_context}\nHex String (first 100 chars): {hex_preview}")]
    HexParseDetailed {
        /// Context about what was being parsed
        parsing_context: String,
        /// Preview of the hex string that failed to parse
        hex_preview: String,
        /// The original hex parsing error
        hex_error: String,
    },

    /// Invalid transaction structure or content errors
    ///
    /// This variant captures transaction validation errors including:
    /// - Malformed transaction structure
    /// - Missing inputs or outputs
    /// - Invalid transaction serialization
    /// - PSET conversion failures
    /// - Transaction size or format issues
    #[error("Invalid transaction structure: {0}")]
    InvalidTransaction(String),
    #[error("Invalid transaction: {error_message}\n\nTransaction ID: {txid}\nValidation Details: {validation_details}")]
    InvalidTransactionDetailed {
        /// The transaction ID if available
        txid: String,
        /// Specific validation failure details
        validation_details: String,
        /// The error message
        error_message: String,
    },

    /// Network-related communication errors
    ///
    /// This variant captures network errors that may occur during
    /// remote operations or API calls (reserved for future use).
    #[error("Network communication failed: {0}")]
    Network(#[from] reqwest::Error),

    /// JSON serialization and deserialization errors
    ///
    /// This variant captures JSON processing errors including:
    /// - Mnemonic file parsing failures
    /// - Invalid JSON structure in storage files
    /// - Serialization failures when writing storage
    #[error("JSON serialization failed: {0}")]
    Serialization(#[from] serde_json::Error),
    #[error("Serialization error: {serde_error}\n\nOperation: {operation}\nData Type: {data_type}\n\nContext: {context}")]
    SerializationDetailed {
        /// The serialization operation (serialize/deserialize)
        operation: String,
        /// The data type being processed
        data_type: String,
        /// Additional context about the operation
        context: String,
        /// The original serde error message
        serde_error: String,
    },

    /// File system I/O operation errors
    ///
    /// This variant captures file operation errors including:
    /// - Mnemonic file read/write failures
    /// - Permission denied errors
    /// - Disk space or filesystem issues
    /// - Atomic write operation failures
    #[error("File I/O operation failed: {0}")]
    FileIo(#[from] std::io::Error),
}

// Additional error conversions for external library errors
// These provide seamless integration with third-party error types

/// Convert BIP39 mnemonic errors to `SignerError`
///
/// This conversion handles all BIP39-related errors including:
/// - Invalid word count
/// - Invalid words not in BIP39 wordlist  
/// - Checksum validation failures
/// - Language detection issues
impl From<bip39::Error> for SignerError {
    fn from(err: bip39::Error) -> Self {
        Self::InvalidMnemonic(format!("BIP39 validation failed: {err}"))
    }
}

/// Convert Elements transaction encoding errors to `SignerError`
///
/// This conversion handles transaction serialization/deserialization errors
/// from the Elements library including:
/// - Consensus encoding failures
/// - Invalid transaction structure
/// - Serialization format errors
impl From<elements::encode::Error> for SignerError {
    fn from(err: elements::encode::Error) -> Self {
        Self::InvalidTransaction(format!("Elements transaction encoding failed: {err}"))
    }
}

// Note: LWK errors are handled manually in the implementation code
// rather than through automatic conversion. This provides better control
// over error context and allows for operation-specific error messages
// that help with debugging and troubleshooting.

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Error as IoError, ErrorKind};

    #[test]
    fn test_lwk_error_variant() {
        let error_msg = "SwSigner creation failed";
        let error = SignerError::Lwk(error_msg.to_string());

        // Test error message formatting
        let formatted = format!("{}", error);
        assert_eq!(
            formatted,
            "LWK signing operation failed: SwSigner creation failed"
        );

        // Test debug formatting
        let debug_formatted = format!("{:?}", error);
        assert!(debug_formatted.contains("Lwk"));
        assert!(debug_formatted.contains(error_msg));

        // Test error source (should be None for this variant)
        assert!(std::error::Error::source(&error).is_none());
    }

    #[test]
    fn test_invalid_mnemonic_error_variant() {
        let error_msg = "Invalid word count: expected 12, got 5";
        let error = SignerError::InvalidMnemonic(error_msg.to_string());

        // Test error message formatting
        let formatted = format!("{}", error);
        assert_eq!(
            formatted,
            "Invalid mnemonic phrase: Invalid word count: expected 12, got 5"
        );

        // Test debug formatting
        let debug_formatted = format!("{:?}", error);
        assert!(debug_formatted.contains("InvalidMnemonic"));
        assert!(debug_formatted.contains(error_msg));

        // Test error source (should be None for this variant)
        assert!(std::error::Error::source(&error).is_none());
    }

    #[test]
    fn test_hex_parse_error_conversion() {
        // Create a hex parsing error by trying to decode invalid hex
        let hex_result = hex::decode("invalid_hex_zz");
        let hex_error = hex_result.unwrap_err();
        let signer_error = SignerError::from(hex_error);

        // Test error variant
        match signer_error {
            SignerError::HexParse(_) => {} // Expected
            other => panic!("Expected HexParse variant, got: {:?}", other),
        }

        // Test error message formatting
        let formatted = format!("{}", signer_error);
        assert!(formatted.starts_with("Hex parsing failed:"));

        // Test error source preservation
        let source = std::error::Error::source(&signer_error);
        assert!(source.is_some());
    }

    #[test]
    fn test_invalid_transaction_error_variant() {
        let error_msg = "Transaction deserialization failed: invalid input count";
        let error = SignerError::InvalidTransaction(error_msg.to_string());

        // Test error message formatting
        let formatted = format!("{}", error);
        assert_eq!(formatted, "Invalid transaction structure: Transaction deserialization failed: invalid input count");

        // Test debug formatting
        let debug_formatted = format!("{:?}", error);
        assert!(debug_formatted.contains("InvalidTransaction"));
        assert!(debug_formatted.contains(error_msg));

        // Test error source (should be None for this variant)
        assert!(std::error::Error::source(&error).is_none());
    }

    #[test]
    fn test_network_error_conversion() {
        // Create a reqwest error by making an invalid request
        let client = reqwest::Client::new();
        let request_result = client.get("http://").build();
        let reqwest_error = request_result.unwrap_err();
        let signer_error = SignerError::from(reqwest_error);

        // Test error variant
        match signer_error {
            SignerError::Network(_) => {} // Expected
            other => panic!("Expected Network variant, got: {:?}", other),
        }

        // Test error message formatting
        let formatted = format!("{}", signer_error);
        assert!(formatted.starts_with("Network communication failed:"));

        // Test error source preservation
        let source = std::error::Error::source(&signer_error);
        assert!(source.is_some());
    }

    #[test]
    fn test_serialization_error_conversion() {
        // Create a JSON serialization error by trying to parse invalid JSON
        let invalid_json = r#"{"invalid": json syntax"#;
        let json_error = serde_json::from_str::<serde_json::Value>(invalid_json).unwrap_err();
        let signer_error = SignerError::from(json_error);

        // Test error variant
        match signer_error {
            SignerError::Serialization(_) => {} // Expected
            other => panic!("Expected Serialization variant, got: {:?}", other),
        }

        // Test error message formatting
        let formatted = format!("{}", signer_error);
        assert!(formatted.starts_with("JSON serialization failed:"));

        // Test error source preservation
        let source = std::error::Error::source(&signer_error);
        assert!(source.is_some());
    }

    #[test]
    fn test_file_io_error_conversion() {
        // Create an I/O error
        let io_error = IoError::new(ErrorKind::NotFound, "File not found");
        let signer_error = SignerError::from(io_error);

        // Test error variant
        match signer_error {
            SignerError::FileIo(_) => {} // Expected
            other => panic!("Expected FileIo variant, got: {:?}", other),
        }

        // Test error message formatting
        let formatted = format!("{}", signer_error);
        assert!(formatted.starts_with("File I/O operation failed:"));
        assert!(formatted.contains("File not found"));

        // Test error source preservation
        let source = std::error::Error::source(&signer_error);
        assert!(source.is_some());
        assert_eq!(source.unwrap().to_string(), "File not found");
    }

    #[test]
    fn test_bip39_error_conversion() {
        // Create a BIP39 error (invalid word count)
        let bip39_error = bip39::Error::BadWordCount(5);
        let signer_error = SignerError::from(bip39_error);

        // Test error variant
        match &signer_error {
            SignerError::InvalidMnemonic(msg) => {
                assert!(msg.contains("BIP39 validation failed"));
                assert!(msg.contains("word count"));
            }
            other => panic!("Expected InvalidMnemonic variant, got: {:?}", other),
        }

        // Test error message formatting
        let formatted = format!("{}", signer_error);
        assert!(formatted.starts_with("Invalid mnemonic phrase:"));
        assert!(formatted.contains("BIP39 validation failed"));
    }

    #[test]
    fn test_elements_encode_error_conversion() {
        // Create an Elements encoding error
        let elements_error = elements::encode::Error::ParseFailed("Invalid transaction format");
        let signer_error = SignerError::from(elements_error);

        // Test error variant
        match &signer_error {
            SignerError::InvalidTransaction(msg) => {
                assert!(msg.contains("Elements transaction encoding failed"));
                assert!(msg.contains("Invalid transaction format"));
            }
            other => panic!("Expected InvalidTransaction variant, got: {:?}", other),
        }

        // Test error message formatting
        let formatted = format!("{}", signer_error);
        assert!(formatted.starts_with("Invalid transaction structure:"));
        assert!(formatted.contains("Elements transaction encoding failed"));
    }

    #[test]
    fn test_error_chain_preservation() {
        // Test that error chains are properly preserved through conversions

        // Create a nested I/O error
        let inner_error = IoError::new(ErrorKind::PermissionDenied, "Access denied");
        let signer_error = SignerError::from(inner_error);

        // Test that the source chain is preserved
        let source = std::error::Error::source(&signer_error);
        assert!(source.is_some());
        assert_eq!(source.unwrap().to_string(), "Access denied");

        // Test error message includes context
        let formatted = format!("{}", signer_error);
        assert!(formatted.contains("File I/O operation failed"));
        assert!(formatted.contains("Access denied"));
    }

    #[test]
    fn test_error_send_sync_traits() {
        // Test that SignerError implements Send and Sync for async compatibility
        fn assert_send<T: Send>() {}
        fn assert_sync<T: Sync>() {}

        assert_send::<SignerError>();
        assert_sync::<SignerError>();
    }

    #[test]
    fn test_error_static_lifetime() {
        // Test that SignerError can be used with 'static lifetime
        fn assert_static<T: 'static>() {}
        assert_static::<SignerError>();
    }

    #[test]
    fn test_all_error_variants_display() {
        // Test that all error variants have proper Display implementations
        let errors = vec![
            SignerError::Lwk("test lwk error".to_string()),
            SignerError::InvalidMnemonic("test mnemonic error".to_string()),
            SignerError::HexParse(hex::decode("invalid_hex_zz").unwrap_err()),
            SignerError::InvalidTransaction("test transaction error".to_string()),
            SignerError::Network(reqwest::Client::new().get("http://").build().unwrap_err()),
            SignerError::Serialization(
                serde_json::from_str::<serde_json::Value>(r#"{"invalid": json"#).unwrap_err(),
            ),
            SignerError::FileIo(IoError::new(ErrorKind::NotFound, "test file error")),
        ];

        for error in errors {
            let display_str = format!("{}", error);
            assert!(
                !display_str.is_empty(),
                "Error display should not be empty: {:?}",
                error
            );

            let debug_str = format!("{:?}", error);
            assert!(
                !debug_str.is_empty(),
                "Error debug should not be empty: {:?}",
                error
            );
        }
    }

    #[test]
    fn test_error_message_preservation() {
        // Test that custom error messages are preserved correctly
        let test_cases = vec![
            (
                "LWK operation failed with code 123",
                SignerError::Lwk("LWK operation failed with code 123".to_string()),
            ),
            (
                "Mnemonic has invalid checksum",
                SignerError::InvalidMnemonic("Mnemonic has invalid checksum".to_string()),
            ),
            (
                "Transaction missing required inputs",
                SignerError::InvalidTransaction("Transaction missing required inputs".to_string()),
            ),
        ];

        for (expected_msg, error) in test_cases {
            let formatted = format!("{}", error);
            assert!(
                formatted.contains(expected_msg),
                "Error message should contain '{}', got: '{}'",
                expected_msg,
                formatted
            );
        }
    }

    #[test]
    fn test_error_propagation_through_result() {
        // Test error propagation through Result types (simulating call stack)
        fn level_3() -> Result<(), SignerError> {
            Err(SignerError::Lwk("Deep error".to_string()))
        }

        fn level_2() -> Result<(), SignerError> {
            level_3()?;
            Ok(())
        }

        fn level_1() -> Result<(), SignerError> {
            level_2()?;
            Ok(())
        }

        let result = level_1();
        assert!(result.is_err());

        match result.unwrap_err() {
            SignerError::Lwk(msg) => assert_eq!(msg, "Deep error"),
            other => panic!("Expected Lwk error, got: {:?}", other),
        }
    }

    #[test]
    fn test_error_conversion_preserves_context() {
        // Test that automatic conversions preserve error context

        // Test hex error conversion
        let hex_result: Result<Vec<u8>, hex::FromHexError> = hex::decode("invalid_hex_zz");
        let hex_error = hex_result.unwrap_err();
        let signer_error = SignerError::from(hex_error);

        let formatted = format!("{}", signer_error);
        assert!(formatted.contains("Hex parsing failed"));

        // Test I/O error conversion
        let io_error = IoError::new(
            ErrorKind::PermissionDenied,
            "Cannot write to read-only file",
        );
        let signer_error = SignerError::from(io_error);

        let formatted = format!("{}", signer_error);
        assert!(formatted.contains("File I/O operation failed"));
        assert!(formatted.contains("Cannot write to read-only file"));
    }

    #[test]
    fn test_error_debug_format_completeness() {
        // Test that debug format includes all relevant information
        let error = SignerError::InvalidMnemonic("Test mnemonic error with details".to_string());
        let debug_str = format!("{:#?}", error);

        // Debug format should include variant name and message
        assert!(debug_str.contains("InvalidMnemonic"));
        assert!(debug_str.contains("Test mnemonic error with details"));
    }

    #[test]
    fn test_multiple_error_conversions() {
        // Test multiple error conversions in sequence
        let test_cases = vec![
            "invalid_hex_zz", // Invalid character
            "abc",            // Odd length
        ];

        for invalid_hex in test_cases {
            let hex_error = hex::decode(invalid_hex).unwrap_err();
            let signer_error = SignerError::from(hex_error);
            match signer_error {
                SignerError::HexParse(_) => {} // Expected
                other => panic!("Expected HexParse variant, got: {:?}", other),
            }

            // Verify error message is meaningful
            let formatted = format!("{}", signer_error);
            assert!(formatted.starts_with("Hex parsing failed:"));
        }
    }

    #[test]
    fn test_error_equality_and_comparison() {
        // Test that errors can be compared for debugging purposes
        let error1 = SignerError::Lwk("same message".to_string());
        let error2 = SignerError::Lwk("same message".to_string());
        let error3 = SignerError::Lwk("different message".to_string());

        // Note: SignerError doesn't implement PartialEq by design (errors often contain
        // non-comparable types), but we can test that the debug representations are consistent
        let debug1 = format!("{:?}", error1);
        let debug2 = format!("{:?}", error2);
        let debug3 = format!("{:?}", error3);

        assert_eq!(debug1, debug2);
        assert_ne!(debug1, debug3);
    }
}