openzeppelin_monitor/services/blockchain/clients/stellar/
error.rs1use crate::utils::logging::error::{ErrorContext, TraceableError};
6use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum StellarClientError {
12 #[error("Data for '{ledger_info}' is outside of Stellar RPC retention window")]
14 OutsideRetentionWindow {
15 rpc_code: i64, rpc_message: String, ledger_info: String, context: Box<ErrorContext>,
19 },
20
21 #[error("Stellar RPC request failed: {0}")]
23 RpcError(Box<ErrorContext>),
24
25 #[error("Failed to parse Stellar RPC response: {0}")]
27 ResponseParseError(Box<ErrorContext>),
28
29 #[error("Invalid input: {0}")]
31 InvalidInput(Box<ErrorContext>),
32
33 #[error("Unexpected response structure from Stellar RPC: {0}")]
35 UnexpectedResponseStructure(Box<ErrorContext>),
36}
37
38impl StellarClientError {
39 pub fn outside_retention_window(
40 rpc_code: i64,
41 rpc_message: String,
42 ledger_info: String,
43 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
44 metadata: Option<HashMap<String, String>>,
45 ) -> Self {
46 let message = format!(
47 "Data for '{}' is outside of Stellar RPC retention window: {} (code {})",
48 &ledger_info.clone(),
49 &rpc_message.clone(),
50 &rpc_code
51 );
52 Self::OutsideRetentionWindow {
53 rpc_code,
54 rpc_message,
55 ledger_info,
56 context: Box::new(ErrorContext::new_with_log(message, source, metadata)),
57 }
58 }
59
60 pub fn rpc_error(
61 message: impl Into<String>,
62 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
63 metadata: Option<HashMap<String, String>>,
64 ) -> Self {
65 Self::RpcError(Box::new(ErrorContext::new_with_log(
66 message, source, metadata,
67 )))
68 }
69
70 pub fn response_parse_error(
71 message: impl Into<String>,
72 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
73 metadata: Option<HashMap<String, String>>,
74 ) -> Self {
75 Self::ResponseParseError(Box::new(ErrorContext::new_with_log(
76 message, source, metadata,
77 )))
78 }
79
80 pub fn invalid_input(
81 msg: impl Into<String>,
82 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
83 metadata: Option<HashMap<String, String>>,
84 ) -> Self {
85 Self::InvalidInput(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
86 }
87
88 pub fn unexpected_response_structure(
89 msg: impl Into<String>,
90 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
91 metadata: Option<HashMap<String, String>>,
92 ) -> Self {
93 Self::UnexpectedResponseStructure(Box::new(ErrorContext::new_with_log(
94 msg, source, metadata,
95 )))
96 }
97}
98
99impl TraceableError for StellarClientError {
100 fn trace_id(&self) -> String {
101 match self {
102 StellarClientError::OutsideRetentionWindow { context, .. } => context.trace_id.clone(),
103 StellarClientError::RpcError(context) => context.trace_id.clone(),
104 StellarClientError::ResponseParseError(context) => context.trace_id.clone(),
105 StellarClientError::InvalidInput(context) => context.trace_id.clone(),
106 StellarClientError::UnexpectedResponseStructure(context) => context.trace_id.clone(),
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_outside_retention_window_error_formatting() {
117 let rpc_error_code = 1234;
118 let rpc_error_message = "Random RPC error".to_string();
119 let error_ledger_info = "start_sequence=123 end_sequence=456".to_string();
120 let error = StellarClientError::outside_retention_window(
121 rpc_error_code,
122 rpc_error_message.clone(),
123 error_ledger_info.clone(),
124 None,
125 None,
126 );
127 assert_eq!(
128 error.to_string(),
129 format!(
130 "Data for '{}' is outside of Stellar RPC retention window",
131 error_ledger_info
132 )
133 );
134 if let StellarClientError::OutsideRetentionWindow {
135 rpc_code,
136 rpc_message,
137 ledger_info,
138 context,
139 } = error
140 {
141 assert_eq!(rpc_code, rpc_error_code);
142 assert_eq!(rpc_message, rpc_error_message);
143 assert_eq!(ledger_info, error_ledger_info);
144 assert!(!context.trace_id.is_empty());
145 } else {
146 panic!("Expected OutsideRetentionWindow variant");
147 }
148 }
149
150 #[test]
151 fn test_rpc_error_formatting() {
152 let error_message = "Random Stellar RPC error".to_string();
153 let error = StellarClientError::rpc_error(error_message.clone(), None, None);
154 assert_eq!(
155 error.to_string(),
156 format!("Stellar RPC request failed: {}", error_message)
157 );
158 if let StellarClientError::RpcError(context) = error {
159 assert_eq!(context.message, error_message);
160 assert!(!context.trace_id.is_empty());
161 } else {
162 panic!("Expected RpcError variant");
163 }
164 }
165
166 #[test]
167 fn test_response_parse_error_formatting() {
168 let error_message = "Failed to parse Stellar RPC response".to_string();
169 let error = StellarClientError::response_parse_error(error_message.clone(), None, None);
170 assert_eq!(
171 error.to_string(),
172 format!("Failed to parse Stellar RPC response: {}", error_message)
173 );
174 if let StellarClientError::ResponseParseError(context) = error {
175 assert_eq!(context.message, error_message);
176 assert!(!context.trace_id.is_empty());
177 } else {
178 panic!("Expected ResponseParseError variant");
179 }
180 }
181
182 #[test]
183 fn test_invalid_input_error_formatting() {
184 let error_message = "Invalid input provided to Stellar client".to_string();
185 let error = StellarClientError::invalid_input(error_message.clone(), None, None);
186 assert_eq!(
187 error.to_string(),
188 format!("Invalid input: {}", error_message)
189 );
190 if let StellarClientError::InvalidInput(context) = error {
191 assert_eq!(context.message, error_message);
192 assert!(!context.trace_id.is_empty());
193 } else {
194 panic!("Expected InvalidInput variant");
195 }
196 }
197
198 #[test]
199 fn test_unexpected_response_structure_error_formatting() {
200 let error_message = "Unexpected response structure from Stellar RPC".to_string();
201 let error =
202 StellarClientError::unexpected_response_structure(error_message.clone(), None, None);
203 assert_eq!(
204 error.to_string(),
205 format!(
206 "Unexpected response structure from Stellar RPC: {}",
207 error_message
208 )
209 );
210 if let StellarClientError::UnexpectedResponseStructure(context) = error {
211 assert_eq!(context.message, error_message);
212 assert!(!context.trace_id.is_empty());
213 } else {
214 panic!("Expected UnexpectedResponseStructure variant");
215 }
216 }
217
218 #[test]
219 fn test_error_source_chain() {
220 let io_error = std::io::Error::new(std::io::ErrorKind::Other, "while reading config");
221
222 let outer_error =
223 StellarClientError::rpc_error("Failed to initialize", Some(Box::new(io_error)), None);
224
225 assert!(outer_error.to_string().contains("Failed to initialize"));
227
228 if let StellarClientError::RpcError(context) = &outer_error {
230 assert_eq!(context.message, "Failed to initialize");
232
233 assert!(context.source.is_some());
235
236 if let Some(src) = &context.source {
237 assert_eq!(src.to_string(), "while reading config");
238 }
239 } else {
240 panic!("Expected RpcError variant");
241 }
242 }
243
244 #[test]
245 fn test_all_error_variants_have_and_propagate_consistent_trace_id() {
246 let create_context_with_id = || {
247 let context = ErrorContext::new("test message", None, None);
248 let original_id = context.trace_id.clone();
249 (context, original_id)
250 };
251
252 let errors_with_ids: Vec<(StellarClientError, String)> = vec![
253 {
254 let (ctx, id) = create_context_with_id();
255 (StellarClientError::RpcError(Box::new(ctx)), id)
256 },
257 {
258 let (ctx, id) = create_context_with_id();
259 (StellarClientError::ResponseParseError(Box::new(ctx)), id)
260 },
261 {
262 let (ctx, id) = create_context_with_id();
263 (StellarClientError::InvalidInput(Box::new(ctx)), id)
264 },
265 {
266 let (ctx, id) = create_context_with_id();
267 (
268 StellarClientError::OutsideRetentionWindow {
269 rpc_code: 0,
270 rpc_message: "".to_string(),
271 ledger_info: "".to_string(),
272 context: Box::new(ctx),
273 },
274 id,
275 )
276 },
277 {
278 let (ctx, id) = create_context_with_id();
279 (
280 StellarClientError::UnexpectedResponseStructure(Box::new(ctx)),
281 id,
282 )
283 },
284 ];
285
286 for (error, original_id) in errors_with_ids {
287 let propagated_id = error.trace_id();
288 assert!(
289 !propagated_id.is_empty(),
290 "Error {:?} should have a non-empty trace_id",
291 error
292 );
293 assert_eq!(
294 propagated_id, original_id,
295 "Trace ID for {:?} was not propagated consistently. Expected: {}, Got: {}",
296 error, original_id, propagated_id
297 );
298 }
299 }
300}