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}