1use crate::utils::logging::error::{ErrorContext, TraceableError};
7use std::collections::HashMap;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum SolanaClientError {
13 #[error("Slot {slot} is not available: {reason}")]
15 SlotNotAvailable {
16 slot: u64,
17 reason: String,
18 context: Box<ErrorContext>,
19 },
20
21 #[error("Block not available for slot {slot}: {reason}")]
23 BlockNotAvailable {
24 slot: u64,
25 reason: String,
26 context: Box<ErrorContext>,
27 },
28
29 #[error("Transaction not found: {signature}")]
31 TransactionNotFound {
32 signature: String,
33 context: Box<ErrorContext>,
34 },
35
36 #[error("Solana RPC request failed: {0}")]
38 RpcError(Box<ErrorContext>),
39
40 #[error("Failed to parse Solana RPC response: {0}")]
42 ResponseParseError(Box<ErrorContext>),
43
44 #[error("Invalid input: {0}")]
46 InvalidInput(Box<ErrorContext>),
47
48 #[error("Unexpected response structure from Solana RPC: {0}")]
50 UnexpectedResponseStructure(Box<ErrorContext>),
51
52 #[error("Program IDL not found for: {program_id}")]
54 IdlNotFound {
55 program_id: String,
56 context: Box<ErrorContext>,
57 },
58
59 #[error("Failed to decode instruction: {0}")]
61 InstructionDecodeError(Box<ErrorContext>),
62}
63
64impl SolanaClientError {
65 pub fn slot_not_available(
67 slot: u64,
68 reason: impl Into<String>,
69 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
70 metadata: Option<HashMap<String, String>>,
71 ) -> Self {
72 let reason = reason.into();
73 let message = format!("Slot {} is not available: {}", slot, &reason);
74 Self::SlotNotAvailable {
75 slot,
76 reason,
77 context: Box::new(ErrorContext::new_with_log(message, source, metadata)),
78 }
79 }
80
81 pub fn block_not_available(
83 slot: u64,
84 reason: impl Into<String>,
85 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
86 metadata: Option<HashMap<String, String>>,
87 ) -> Self {
88 let reason = reason.into();
89 let message = format!("Block not available for slot {}: {}", slot, &reason);
90 Self::BlockNotAvailable {
91 slot,
92 reason,
93 context: Box::new(ErrorContext::new_with_log(message, source, metadata)),
94 }
95 }
96
97 pub fn transaction_not_found(
99 signature: impl Into<String>,
100 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
101 metadata: Option<HashMap<String, String>>,
102 ) -> Self {
103 let signature = signature.into();
104 let message = format!("Transaction not found: {}", &signature);
105 Self::TransactionNotFound {
106 signature,
107 context: Box::new(ErrorContext::new_with_log(message, source, metadata)),
108 }
109 }
110
111 pub fn rpc_error(
113 message: impl Into<String>,
114 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
115 metadata: Option<HashMap<String, String>>,
116 ) -> Self {
117 Self::RpcError(Box::new(ErrorContext::new_with_log(
118 message, source, metadata,
119 )))
120 }
121
122 pub fn response_parse_error(
124 message: impl Into<String>,
125 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
126 metadata: Option<HashMap<String, String>>,
127 ) -> Self {
128 Self::ResponseParseError(Box::new(ErrorContext::new_with_log(
129 message, source, metadata,
130 )))
131 }
132
133 pub fn invalid_input(
135 msg: impl Into<String>,
136 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
137 metadata: Option<HashMap<String, String>>,
138 ) -> Self {
139 Self::InvalidInput(Box::new(ErrorContext::new_with_log(msg, source, metadata)))
140 }
141
142 pub fn unexpected_response_structure(
144 msg: impl Into<String>,
145 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
146 metadata: Option<HashMap<String, String>>,
147 ) -> Self {
148 Self::UnexpectedResponseStructure(Box::new(ErrorContext::new_with_log(
149 msg, source, metadata,
150 )))
151 }
152
153 pub fn idl_not_found(
155 program_id: impl Into<String>,
156 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
157 metadata: Option<HashMap<String, String>>,
158 ) -> Self {
159 let program_id = program_id.into();
160 let message = format!("Program IDL not found for: {}", &program_id);
161 Self::IdlNotFound {
162 program_id,
163 context: Box::new(ErrorContext::new_with_log(message, source, metadata)),
164 }
165 }
166
167 pub fn instruction_decode_error(
169 message: impl Into<String>,
170 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
171 metadata: Option<HashMap<String, String>>,
172 ) -> Self {
173 Self::InstructionDecodeError(Box::new(ErrorContext::new_with_log(
174 message, source, metadata,
175 )))
176 }
177
178 pub fn is_slot_not_available(&self) -> bool {
180 matches!(self, Self::SlotNotAvailable { .. })
181 }
182
183 pub fn is_block_not_available(&self) -> bool {
185 matches!(self, Self::BlockNotAvailable { .. })
186 }
187
188 pub fn is_transaction_not_found(&self) -> bool {
190 matches!(self, Self::TransactionNotFound { .. })
191 }
192}
193
194impl TraceableError for SolanaClientError {
195 fn trace_id(&self) -> String {
196 match self {
197 SolanaClientError::SlotNotAvailable { context, .. } => context.trace_id.clone(),
198 SolanaClientError::BlockNotAvailable { context, .. } => context.trace_id.clone(),
199 SolanaClientError::TransactionNotFound { context, .. } => context.trace_id.clone(),
200 SolanaClientError::RpcError(context) => context.trace_id.clone(),
201 SolanaClientError::ResponseParseError(context) => context.trace_id.clone(),
202 SolanaClientError::InvalidInput(context) => context.trace_id.clone(),
203 SolanaClientError::UnexpectedResponseStructure(context) => context.trace_id.clone(),
204 SolanaClientError::IdlNotFound { context, .. } => context.trace_id.clone(),
205 SolanaClientError::InstructionDecodeError(context) => context.trace_id.clone(),
206 }
207 }
208}
209
210pub mod error_codes {
212 pub const BLOCK_NOT_AVAILABLE: i64 = -32004;
214 pub const SLOT_SKIPPED: i64 = -32007;
216 pub const LONG_TERM_STORAGE_SLOT_SKIPPED: i64 = -32009;
218 #[allow(dead_code)]
220 pub const UNSUPPORTED_TRANSACTION_VERSION: i64 = -32015;
221 #[allow(dead_code)]
223 pub const INVALID_PARAMS: i64 = -32602;
224 #[allow(dead_code)]
226 pub const INTERNAL_ERROR: i64 = -32603;
227}
228
229pub fn is_slot_unavailable_error(code: i64) -> bool {
231 matches!(
232 code,
233 error_codes::BLOCK_NOT_AVAILABLE
234 | error_codes::SLOT_SKIPPED
235 | error_codes::LONG_TERM_STORAGE_SLOT_SKIPPED
236 )
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_slot_not_available_error_formatting() {
245 let error = SolanaClientError::slot_not_available(12345, "Slot was skipped", None, None);
246 assert_eq!(
247 error.to_string(),
248 "Slot 12345 is not available: Slot was skipped"
249 );
250 if let SolanaClientError::SlotNotAvailable {
251 slot,
252 reason,
253 context,
254 } = error
255 {
256 assert_eq!(slot, 12345);
257 assert_eq!(reason, "Slot was skipped");
258 assert!(!context.trace_id.is_empty());
259 } else {
260 panic!("Expected SlotNotAvailable variant");
261 }
262 }
263
264 #[test]
265 fn test_block_not_available_error_formatting() {
266 let error = SolanaClientError::block_not_available(12345, "Block cleaned up", None, None);
267 assert_eq!(
268 error.to_string(),
269 "Block not available for slot 12345: Block cleaned up"
270 );
271 if let SolanaClientError::BlockNotAvailable {
272 slot,
273 reason,
274 context,
275 } = error
276 {
277 assert_eq!(slot, 12345);
278 assert_eq!(reason, "Block cleaned up");
279 assert!(!context.trace_id.is_empty());
280 } else {
281 panic!("Expected BlockNotAvailable variant");
282 }
283 }
284
285 #[test]
286 fn test_transaction_not_found_error_formatting() {
287 let sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
288 let error = SolanaClientError::transaction_not_found(sig, None, None);
289 assert_eq!(error.to_string(), format!("Transaction not found: {}", sig));
290 }
291
292 #[test]
293 fn test_rpc_error_formatting() {
294 let error_message = "Random Solana RPC error".to_string();
295 let error = SolanaClientError::rpc_error(error_message.clone(), None, None);
296 assert_eq!(
297 error.to_string(),
298 format!("Solana RPC request failed: {}", error_message)
299 );
300 if let SolanaClientError::RpcError(context) = error {
301 assert_eq!(context.message, error_message);
302 assert!(!context.trace_id.is_empty());
303 } else {
304 panic!("Expected RpcError variant");
305 }
306 }
307
308 #[test]
309 fn test_response_parse_error_formatting() {
310 let error_message = "Failed to parse Solana RPC response".to_string();
311 let error = SolanaClientError::response_parse_error(error_message.clone(), None, None);
312 assert_eq!(
313 error.to_string(),
314 format!("Failed to parse Solana RPC response: {}", error_message)
315 );
316 }
317
318 #[test]
319 fn test_invalid_input_error_formatting() {
320 let error_message = "Invalid input provided to Solana client".to_string();
321 let error = SolanaClientError::invalid_input(error_message.clone(), None, None);
322 assert_eq!(
323 error.to_string(),
324 format!("Invalid input: {}", error_message)
325 );
326 }
327
328 #[test]
329 fn test_idl_not_found_error_formatting() {
330 let program_id = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
331 let error = SolanaClientError::idl_not_found(program_id, None, None);
332 assert_eq!(
333 error.to_string(),
334 format!("Program IDL not found for: {}", program_id)
335 );
336 }
337
338 #[test]
339 fn test_error_type_checks() {
340 let slot_error = SolanaClientError::slot_not_available(123, "skipped", None, None);
341 assert!(slot_error.is_slot_not_available());
342 assert!(!slot_error.is_block_not_available());
343 assert!(!slot_error.is_transaction_not_found());
344
345 let block_error = SolanaClientError::block_not_available(123, "cleaned", None, None);
346 assert!(!block_error.is_slot_not_available());
347 assert!(block_error.is_block_not_available());
348
349 let tx_error = SolanaClientError::transaction_not_found("abc", None, None);
350 assert!(tx_error.is_transaction_not_found());
351 }
352
353 #[test]
354 fn test_is_slot_unavailable_error() {
355 assert!(is_slot_unavailable_error(error_codes::BLOCK_NOT_AVAILABLE));
356 assert!(is_slot_unavailable_error(error_codes::SLOT_SKIPPED));
357 assert!(is_slot_unavailable_error(
358 error_codes::LONG_TERM_STORAGE_SLOT_SKIPPED
359 ));
360 assert!(!is_slot_unavailable_error(error_codes::INVALID_PARAMS));
361 assert!(!is_slot_unavailable_error(error_codes::INTERNAL_ERROR));
362 }
363
364 #[test]
365 fn test_all_error_variants_have_trace_id() {
366 let create_context_with_id = || {
367 let context = ErrorContext::new("test message", None, None);
368 let original_id = context.trace_id.clone();
369 (context, original_id)
370 };
371
372 let errors_with_ids: Vec<(SolanaClientError, String)> = vec![
373 {
374 let (ctx, id) = create_context_with_id();
375 (
376 SolanaClientError::SlotNotAvailable {
377 slot: 0,
378 reason: "".to_string(),
379 context: Box::new(ctx),
380 },
381 id,
382 )
383 },
384 {
385 let (ctx, id) = create_context_with_id();
386 (
387 SolanaClientError::BlockNotAvailable {
388 slot: 0,
389 reason: "".to_string(),
390 context: Box::new(ctx),
391 },
392 id,
393 )
394 },
395 {
396 let (ctx, id) = create_context_with_id();
397 (
398 SolanaClientError::TransactionNotFound {
399 signature: "".to_string(),
400 context: Box::new(ctx),
401 },
402 id,
403 )
404 },
405 {
406 let (ctx, id) = create_context_with_id();
407 (SolanaClientError::RpcError(Box::new(ctx)), id)
408 },
409 {
410 let (ctx, id) = create_context_with_id();
411 (SolanaClientError::ResponseParseError(Box::new(ctx)), id)
412 },
413 {
414 let (ctx, id) = create_context_with_id();
415 (SolanaClientError::InvalidInput(Box::new(ctx)), id)
416 },
417 {
418 let (ctx, id) = create_context_with_id();
419 (
420 SolanaClientError::UnexpectedResponseStructure(Box::new(ctx)),
421 id,
422 )
423 },
424 {
425 let (ctx, id) = create_context_with_id();
426 (
427 SolanaClientError::IdlNotFound {
428 program_id: "".to_string(),
429 context: Box::new(ctx),
430 },
431 id,
432 )
433 },
434 {
435 let (ctx, id) = create_context_with_id();
436 (SolanaClientError::InstructionDecodeError(Box::new(ctx)), id)
437 },
438 ];
439
440 for (error, original_id) in errors_with_ids {
441 let propagated_id = error.trace_id();
442 assert!(
443 !propagated_id.is_empty(),
444 "Error {:?} should have a non-empty trace_id",
445 error
446 );
447 assert_eq!(
448 propagated_id, original_id,
449 "Trace ID for {:?} was not propagated consistently",
450 error
451 );
452 }
453 }
454}