Skip to main content

chia_peer/
error.rs

1//! [`ChiaPeerError`] — the crate's own transport/protocol error, and its fail-closed mapping onto
2//! the canonical [`dig_chainsource_interface::ChainSourceError`].
3//!
4//! Every variant means the same thing to a consumer: **the peer could not reliably answer**. None of
5//! them is ever collapsed into an absence (`Ok(None)`) — that distinction is the crux of the
6//! [`ChainSource`](dig_chainsource_interface::ChainSource) fail-closed contract, so the mapping below
7//! turns every `ChiaPeerError` into an `Err`, never a value.
8
9use dig_chainsource_interface::ChainSourceError;
10use thiserror::Error;
11
12/// The reason a Chia light-client peer read could not complete reliably.
13#[derive(Debug, Error, Clone, PartialEq, Eq)]
14pub enum ChiaPeerError {
15    /// A transport/connection failure reaching the peer (socket, websocket, TLS). Carries the
16    /// backend's own message for diagnostics.
17    #[error("chia peer transport error: {0}")]
18    Transport(String),
19
20    /// The peer explicitly rejected the request (e.g. a reorg or subscription-limit rejection). The
21    /// answer is unknown, so the consumer must fail closed.
22    #[error("chia peer rejected the request: {0}")]
23    Rejected(String),
24
25    /// The peer responded but the payload could not be parsed into the expected chain type. The read
26    /// is untrustworthy, so fail closed.
27    #[error("malformed chia peer response: {0}")]
28    Malformed(String),
29
30    /// A request did not complete within the configured deadline. Whether the answer would have been
31    /// present is unknown, so fail closed.
32    #[error("chia peer request timed out")]
33    Timeout,
34
35    /// No usable full-node peer could be discovered/connected.
36    #[error("chia peer discovery failed")]
37    PeerDiscoveryFailed,
38
39    /// The light client is not currently connected to any peer.
40    #[error("chia light client is not connected")]
41    NotConnected,
42
43    /// Setting up the TLS connector failed.
44    #[error("chia peer TLS setup error: {0}")]
45    Tls(String),
46}
47
48impl ChiaPeerError {
49    /// Whether this error's message names a timeout, so a timed-out transport string classifies as
50    /// [`ChainSourceError::Timeout`] rather than a generic transport failure.
51    fn looks_like_timeout(message: &str) -> bool {
52        let lower = message.to_ascii_lowercase();
53        lower.contains("timed out") || lower.contains("timeout")
54    }
55}
56
57impl From<ChiaPeerError> for ChainSourceError {
58    /// Maps every peer error to a fail-closed [`ChainSourceError`]. Each variant is a "could not
59    /// reliably answer" signal — NEVER an `Ok(None)`; only the reason class differs, for diagnostics.
60    fn from(error: ChiaPeerError) -> Self {
61        match error {
62            ChiaPeerError::Timeout => ChainSourceError::Timeout,
63            ChiaPeerError::Transport(msg) if ChiaPeerError::looks_like_timeout(&msg) => {
64                ChainSourceError::Timeout
65            }
66            ChiaPeerError::Transport(msg) => ChainSourceError::Transport(msg),
67            ChiaPeerError::Rejected(msg) => ChainSourceError::Transport(msg),
68            ChiaPeerError::Malformed(msg) => ChainSourceError::Malformed(msg),
69            ChiaPeerError::PeerDiscoveryFailed => {
70                ChainSourceError::Transport("peer discovery failed".to_string())
71            }
72            ChiaPeerError::NotConnected => {
73                ChainSourceError::Transport("light client is not connected".to_string())
74            }
75            ChiaPeerError::Tls(msg) => ChainSourceError::Transport(format!("TLS: {msg}")),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn timeout_variant_maps_to_timeout() {
86        assert_eq!(
87            ChainSourceError::from(ChiaPeerError::Timeout),
88            ChainSourceError::Timeout
89        );
90    }
91
92    #[test]
93    fn timed_out_transport_message_maps_to_timeout() {
94        let mapped = ChainSourceError::from(ChiaPeerError::Transport("request timed out".into()));
95        assert_eq!(mapped, ChainSourceError::Timeout);
96    }
97
98    #[test]
99    fn plain_transport_maps_to_transport_never_absence() {
100        let mapped = ChainSourceError::from(ChiaPeerError::Transport("socket reset".into()));
101        assert_eq!(mapped, ChainSourceError::Transport("socket reset".into()));
102    }
103
104    #[test]
105    fn rejected_maps_to_transport() {
106        let mapped = ChainSourceError::from(ChiaPeerError::Rejected("reorg".into()));
107        assert!(matches!(mapped, ChainSourceError::Transport(_)));
108    }
109
110    #[test]
111    fn malformed_maps_to_malformed() {
112        let mapped = ChainSourceError::from(ChiaPeerError::Malformed("bad bytes".into()));
113        assert!(matches!(mapped, ChainSourceError::Malformed(_)));
114    }
115
116    #[test]
117    fn not_connected_and_discovery_and_tls_map_to_transport() {
118        assert!(matches!(
119            ChainSourceError::from(ChiaPeerError::NotConnected),
120            ChainSourceError::Transport(_)
121        ));
122        assert!(matches!(
123            ChainSourceError::from(ChiaPeerError::PeerDiscoveryFailed),
124            ChainSourceError::Transport(_)
125        ));
126        assert!(matches!(
127            ChainSourceError::from(ChiaPeerError::Tls("x".into())),
128            ChainSourceError::Transport(_)
129        ));
130    }
131}