Skip to main content

silent_payments_scan/
error.rs

1//! Error types for the Silent Payments scan crate.
2//!
3//! [`ScanError`] covers all failure modes during backend-driven scanning:
4//! connection failures, backend data errors, scanning logic errors (wrapping
5//! [`ReceiveError`]), and not-found conditions.
6
7use silent_payments_receive::ReceiveError;
8use thiserror::Error;
9
10/// Errors from scanning backends.
11///
12/// All backends return this same error type, regardless of data source.
13/// Connection and backend errors carry human-readable messages; scanning
14/// logic errors wrap [`ReceiveError`] transparently.
15#[derive(Debug, Error)]
16pub enum ScanError {
17    /// Network connection failure (timeout, DNS, TLS, refused).
18    #[error("connection failed: {0}")]
19    Connection(String),
20
21    /// Backend returned invalid or unexpected data.
22    #[error("backend error: {0}")]
23    Backend(String),
24
25    /// Scanning logic error (wraps receive crate errors).
26    #[error(transparent)]
27    Receive(#[from] ReceiveError),
28
29    /// Block or transaction not found on the backend.
30    #[error("not found: {0}")]
31    NotFound(String),
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37    use silent_payments_core::error::{CryptoError, InputError};
38
39    #[test]
40    fn connection_error_display() {
41        let err = ScanError::Connection("timeout after 30s".into());
42        assert_eq!(err.to_string(), "connection failed: timeout after 30s");
43    }
44
45    #[test]
46    fn backend_error_display() {
47        let err = ScanError::Backend("invalid block header".into());
48        assert_eq!(err.to_string(), "backend error: invalid block header");
49    }
50
51    #[test]
52    fn receive_error_wraps_transparently() {
53        let receive_err = ReceiveError::Scanning("output mismatch".into());
54        let err = ScanError::from(receive_err);
55        assert!(
56            matches!(err, ScanError::Receive(ReceiveError::Scanning(_))),
57            "should wrap ReceiveError::Scanning"
58        );
59        // Transparent display: shows the inner error's message
60        assert_eq!(
61            err.to_string(),
62            "failed to scan transaction outputs: output mismatch"
63        );
64    }
65
66    #[test]
67    fn receive_error_from_crypto() {
68        let crypto_err = CryptoError::InvalidSecretKey;
69        let receive_err = ReceiveError::from(crypto_err);
70        let err = ScanError::from(receive_err);
71        assert!(
72            matches!(err, ScanError::Receive(ReceiveError::Crypto(_))),
73            "should wrap ReceiveError::Crypto"
74        );
75    }
76
77    #[test]
78    fn receive_error_from_input() {
79        let input_err = InputError::NoEligibleInputs;
80        let receive_err = ReceiveError::from(input_err);
81        let err = ScanError::from(receive_err);
82        assert!(
83            matches!(err, ScanError::Receive(ReceiveError::Input(_))),
84            "should wrap ReceiveError::Input"
85        );
86    }
87
88    #[test]
89    fn not_found_error_display() {
90        let err = ScanError::NotFound("block 999999".into());
91        assert_eq!(err.to_string(), "not found: block 999999");
92    }
93
94    #[test]
95    fn error_is_send_and_sync() {
96        fn assert_send_sync<T: Send + Sync>() {}
97        // ScanError should be Send + Sync for use across threads
98        // This will fail at compile time if not satisfied
99        assert_send_sync::<ScanError>();
100    }
101}