Skip to main content

gl_util/
error.rs

1//! # Greenlight Error Module
2//!
3//! This module provides a comprehensive error handling system for
4//! greenlight. It features a generic error type that can be customized with
5//! module- or crate-specific error codes, while maintaining compatibility
6//! with gRPC status codes and providing rich error context.
7use bytes::Bytes;
8use core::error::Error as StdError;
9use serde::{Deserialize, Serialize};
10use serde_json;
11use std::sync::Arc;
12use tonic;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct ParsingError(String);
16
17impl core::fmt::Display for ParsingError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(f, "could not parse error: {}", self.0)
20    }
21}
22
23impl StdError for ParsingError {}
24
25// Convenient macro for creating ParsingError
26macro_rules! parsing_error {
27    ($($arg:tt)*) => {
28        ParsingError(format!($($arg)*))
29    };
30}
31
32/// Creates an `Error` with the specified `ErrorCode` and optional formatted
33/// message.
34///
35/// This macro provides a convenient and efficient way to construct `Error`
36/// instances with compile-time optimizations and meaningful default messages.
37///
38/// # Arguments
39///
40/// * `$code` - An `ErrorCode` variant that categorizes the error type
41/// * `$msg` - *(Optional)* A string literal for static messages
42/// * `$fmt` - *(Optional)* A format string literal with `{}` placeholders
43/// * `$args` - *(Optional)* Arguments corresponding to format placeholders
44///
45/// # See Also
46///
47/// - [`format!`] - For understanding format string syntax
48/// - [`Error::new`] - The underlying constructor being called
49/// - [`std::fmt`] - For advanced formatting options
50///
51/// [`format!`]: std::format
52/// [`Error::new`]: crate::Error::new
53/// [`std::fmt`]: std::fmt
54#[macro_export]
55macro_rules! error {
56    // Default message variant - generates meaningful message from error code
57    ($code:expr) => {
58        $crate::error::Error::new($code, concat!(stringify!($code), " occurred"))
59    };
60
61    // Static message variant - most efficient for literal strings
62    ($code:expr, $msg:literal) => {
63        $crate::error::Error::new($code, $msg)
64    };
65
66    // Formatted message variant - with compile-time format validation
67    ($code:expr, $fmt:literal, $($args:expr),+ $(,)?) => {
68        $crate::error::Error::new($code, format!($fmt, $($args),+))
69    };
70}
71
72/// Trait for defining module-specific error codes.
73///
74/// This trait should be implemented by enums that represent different error
75/// categories in greenlight. The error codes should be unique integers that
76/// can be serialized and transmitted over the network.
77pub trait ErrorCode: core::fmt::Debug + core::fmt::Display + Clone + Send + Sync + 'static {
78    /// Returns the numeric error code for this error type.
79    ///
80    /// This code should be unique within your application and stable
81    /// across versions for backward compatibility.
82    fn code(&self) -> i32;
83
84    /// Attempts to construct an error code from its numeric representation.
85    ///
86    /// Returns `None` if the code is not recognized.
87    fn from_code(code: i32) -> Option<Self>
88    where
89        Self: Sized;
90}
91
92/// JSON structure for transmitting error details over gRPC.
93///
94/// This structure is serialized into the `details` field of a
95/// `tonic::Status` to provide structured error information that can be
96/// parsed by clients.
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98pub struct GrpcErrorDetails {
99    pub code: i32,
100    /// Optional hint to help users resolve the issue
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub hint: Option<String>,
103}
104
105/// Extracted error information from a gRPC status.
106///
107/// This structure contains all the error information that was transmitted
108/// over gRPC, including both the standard gRPC fields and our custom
109/// structured error details.
110#[derive(Debug, Clone)]
111pub struct GrpcErrorInfo {
112    pub code: i32,
113    pub message: String,
114    pub hint: Option<String>,
115    pub grpc_code: tonic::Code,
116}
117
118/// Attempts to parse structured error information from a `tonic::Status`.
119///
120/// This implementation expects the status details to contain a JSON-encoded
121/// `GrpcErrorDetails` structure. If parsing fails, a `serde_json::Error` is
122/// returned.
123impl TryFrom<tonic::Status> for GrpcErrorInfo {
124    type Error = serde_json::Error;
125
126    fn try_from(value: tonic::Status) -> std::result::Result<Self, Self::Error> {
127        let parsed = serde_json::from_slice::<GrpcErrorDetails>(&value.details())?;
128        Ok(GrpcErrorInfo {
129            code: parsed.code,
130            message: value.message().to_owned(),
131            hint: parsed.hint,
132            grpc_code: value.code(),
133        })
134    }
135}
136
137/// Extension trait for mapping error codes to gRPC status codes.
138///
139/// This trait should be implemented alongside `ErrorCode` to define
140/// how your greenlight-specific errors map to standard gRPC status codes.
141pub trait ErrorStatusConversionExt: ErrorCode {
142    /// Maps this error to an appropriate gRPC status code.
143    ///
144    /// The returned status code should follow gRPC conventions:
145    /// See: https://grpc.io/docs/guides/status-codes/
146    fn status_code(&self) -> tonic::Code;
147}
148
149/// Generic error type that combines error codes with rich error context.
150#[derive(Debug, Clone)]
151pub struct Error<C: ErrorCode> {
152    /// Error code for categorization and programmatic handling
153    pub code: C,
154    /// User-facing error message
155    pub message: String,
156    /// Optional hint to help users resolve the issue
157    pub hint: Option<String>,
158    /// Context for debugging
159    pub context: Option<String>,
160    /// Source error chain for debugging
161    pub source: Option<Arc<dyn StdError + Send + Sync>>,
162}
163
164impl<C: ErrorCode> Error<C> {
165    /// Creates a new error with the given code and message.
166    pub fn new(code: C, message: impl Into<String>) -> Self {
167        Self {
168            code,
169            message: message.into(),
170            hint: None,
171            context: None,
172            source: None,
173        }
174    }
175
176    /// Adds a hint to help users resolve the issue.
177    ///
178    /// Hints should provide actionable guidance for end users.
179    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
180        self.hint = Some(hint.into());
181        self
182    }
183
184    /// Adds internal context for debugging.
185    ///
186    /// Context is meant for developers and should include information
187    /// about what the system was doing when the error occurred.
188    /// TODO: currently unarmed, but can be used to log errors in a
189    /// standardized way in the future.
190    pub fn with_context(mut self, context: impl Into<String>) -> Self {
191        self.context = Some(context.into());
192        self
193    }
194
195    /// Adds a source error for error chaining.
196    ///
197    /// This is useful for preserving the original error that caused this error.
198    ///
199    pub fn with_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
200        self.source = Some(Arc::new(source));
201        self
202    }
203
204    /// Adds a source error from an existing Arc.
205    ///
206    /// This is useful when forwarding errors that are already wrapped in an Arc,
207    /// avoiding unnecessary allocations.
208    pub fn with_source_arc(mut self, source: Arc<dyn StdError + Send + Sync>) -> Self {
209        self.source = Some(source);
210        self
211    }
212
213    /// Returns the numeric error code.
214    ///
215    /// This is a convenience method that calls `code()` on the error code.
216    pub fn code(&self) -> i32 {
217        self.code.code()
218    }
219
220    /// Converts this error to use a different error code type.
221    ///
222    /// This is useful when errors need to be converted between different
223    /// modules or layers that use different error code enums.
224    pub fn map_code<T: ErrorCode>(self, new_code: T) -> Error<T> {
225        Error {
226            code: new_code,
227            message: self.message,
228            hint: self.hint,
229            context: self.context,
230            source: self.source,
231        }
232    }
233}
234
235/// Displays the error message.
236impl<C: ErrorCode> core::fmt::Display for Error<C> {
237    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
238        write!(f, "{}", self.message)
239    }
240}
241
242/// Implements the standard error trait for error chaining.
243impl<C: ErrorCode> StdError for Error<C> {
244    fn source(&self) -> Option<&(dyn StdError + 'static)> {
245        self.source
246            .as_ref()
247            .map(|e| e.as_ref() as &(dyn StdError + 'static))
248    }
249}
250
251/// Converts a `GreenlightError` into a `tonic::Status` for gRPC transmission.
252///
253/// The error details are JSON-encoded and included in the status details
254/// field. If JSON serialization fails, a fallback JSON string is used.
255impl<C: ErrorCode + ErrorStatusConversionExt> From<Error<C>> for tonic::Status {
256    fn from(value: Error<C>) -> Self {
257        let code = value.code.status_code();
258        let details: Bytes = serde_json::to_vec(&GrpcErrorDetails {
259            code: value.code(),
260            hint: value.hint.clone(),
261        })
262        .unwrap_or_else(|_| {
263            // Fallback to simple JSON if serialization fails
264            // This ensures we always send valid JSON even if something goes wrong
265            format!(
266                "{{\"code\":{},\"message\":\"{}\"}}",
267                value.code(),
268                value.message,
269            )
270            .into_bytes()
271        })
272        .into();
273        tonic::Status::with_details(code, value.message, details)
274    }
275}
276
277/// Attempts to convert a `tonic::Status` back into a `GreenlightError`.
278///
279/// This requires that:
280/// 1. The status contains valid JSON details in the expected format
281/// 2. The error code in the details can be mapped to a valid `C` variant
282///
283/// Returns an `anyhow::Error` if parsing fails or the error code is unknown.
284impl<C: ErrorCode> TryFrom<tonic::Status> for Error<C> {
285    type Error = ParsingError;
286
287    fn try_from(value: tonic::Status) -> std::result::Result<Self, Self::Error> {
288        let grpc_err: GrpcErrorInfo = value
289            .try_into()
290            .map_err(|e| parsing_error!("failed to convert Status into GrpcErrorInfo {}", e))?;
291        let code = C::from_code(grpc_err.code)
292            .ok_or_else(|| parsing_error!("unknown error code: {}", grpc_err.code))?;
293        Ok(Self {
294            code,
295            message: grpc_err.message,
296            hint: grpc_err.hint,
297            context: None,
298            source: None,
299        })
300    }
301}
302
303/// Type alias for Core Lightning RPC error codes.
304///
305/// CLN uses specific numeric codes to indicate different types of failures
306/// in payment operations. This type preserves the original error code
307/// for debugging and logging purposes.
308pub type ClnRpcError = i32;
309
310/// Implementation of `ErrorCode` for CLN RPC errors.
311///
312/// This implementation treats all i32 values as valid error codes,
313/// allowing us to preserve any error code returned by CLN without loss.
314impl ErrorCode for ClnRpcError {
315    fn code(&self) -> i32 {
316        *self
317    }
318
319    fn from_code(code: i32) -> Option<Self>
320    where
321        Self: Sized,
322    {
323        Some(code)
324    }
325}
326
327/// Implementation of RPC error codes
328use cln_rpc::RpcError;
329
330pub trait RpcErrConversion: ErrorCode {
331    fn from_rpc_error(value: &RpcError) -> Self;
332}
333
334impl<C: ErrorCode + RpcErrConversion> From<RpcError> for Error<C> {
335    fn from(value: RpcError) -> Self {
336        let code = C::from_rpc_error(&value);
337        Self::new(code, value.message)
338    }
339}
340
341#[cfg(test)]
342mod test {
343    use super::*;
344
345    #[test]
346    fn test_status_conversion() {
347        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
348        enum TestErrorCodes {
349            FailedPrecondition = 101,
350            NotFound = 202,
351        }
352
353        impl ErrorCode for TestErrorCodes {
354            fn code(&self) -> i32 {
355                *self as i32
356            }
357
358            fn from_code(_code: i32) -> Option<Self>
359            where
360                Self: Sized,
361            {
362                unimplemented!()
363            }
364        }
365
366        impl core::fmt::Display for TestErrorCodes {
367            fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368                unimplemented!()
369            }
370        }
371
372        impl ErrorStatusConversionExt for TestErrorCodes {
373            fn status_code(&self) -> tonic::Code {
374                match self {
375                    TestErrorCodes::FailedPrecondition => tonic::Code::FailedPrecondition,
376                    TestErrorCodes::NotFound => tonic::Code::NotFound,
377                }
378            }
379        }
380
381        type TestError = Error<TestErrorCodes>;
382
383        let t_err = TestError::new(TestErrorCodes::FailedPrecondition, "a failed precondition")
384            .with_hint("How to resolve it");
385        let status: tonic::Status = t_err.clone().into();
386        assert_eq!(status.message(), t_err.message);
387
388        let mut details: serde_json::Value = serde_json::from_slice(status.details()).unwrap();
389        assert_eq!(
390            details["code"].take(),
391            TestErrorCodes::FailedPrecondition.code()
392        );
393        assert_eq!(details["hint"].take(), "How to resolve it");
394    }
395}