1use thiserror::Error;
4
5#[derive(Error, Debug, Clone)]
7pub enum EvmToolError {
8 #[error("Provider error: {0}")]
10 ProviderError(String),
11
12 #[error("Transaction reverted: {reason}")]
14 TransactionReverted { reason: String },
15
16 #[error("Nonce too low")]
18 NonceTooLow,
19
20 #[error("Nonce mismatch")]
22 NonceMismatch,
23
24 #[error("Insufficient funds")]
26 InsufficientFunds,
27
28 #[error("Gas estimation failed")]
30 GasEstimationFailed,
31
32 #[error("Invalid address: {0}")]
34 InvalidAddress(String),
35
36 #[error("Contract error: {0}")]
38 ContractError(String),
39
40 #[error("Signer error: {0}")]
42 SignerError(#[from] riglr_core::SignerError),
43
44 #[error("Network error: {0}")]
46 NetworkError(String),
47
48 #[error("Rate limit exceeded")]
50 RateLimited,
51
52 #[error("Unsupported chain: {0}")]
54 UnsupportedChain(u64),
55
56 #[error("Invalid parameter: {0}")]
58 InvalidParameter(String),
59
60 #[error("{0}")]
62 Generic(String),
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ErrorClass {
68 Permanent,
70 Retriable,
72 RateLimited,
74}
75
76fn classify_provider_message(msg: &str) -> ErrorClass {
78 if msg.contains("timeout") || msg.contains("connection") {
79 ErrorClass::Retriable
80 } else if msg.contains("rate") || msg.contains("quota") {
81 ErrorClass::RateLimited
82 } else {
83 ErrorClass::Permanent
84 }
85}
86
87fn classify_generic_message(msg: &str) -> ErrorClass {
89 if msg.contains("timeout") || msg.contains("connection") {
90 ErrorClass::Retriable
91 } else {
92 ErrorClass::Permanent
93 }
94}
95
96fn extract_error_code(msg: &str) -> Option<i32> {
103 if let Some(idx) = msg.find("code:") {
105 let code_str = &msg[idx + 5..].trim();
106 if let Some(end_idx) = code_str.find(|c: char| !c.is_ascii_digit() && c != '-') {
107 return code_str[..end_idx].parse::<i32>().ok();
108 }
109 return code_str.split_whitespace().next()?.parse::<i32>().ok();
110 }
111
112 if msg.starts_with("429") || msg.contains(" 429 ") {
114 return Some(429);
115 }
116
117 if let Some(idx) = msg.find("error ") {
119 let code_str = &msg[idx + 6..].trim();
120 if let Some(stripped) = code_str.strip_prefix('-') {
121 if let Some(end_idx) = stripped.find(|c: char| !c.is_ascii_digit()) {
122 return code_str[..=end_idx].parse::<i32>().ok();
123 }
124 }
125 }
126
127 None
128}
129
130fn classify_by_error_code(code: i32) -> Option<ErrorClass> {
148 match code {
149 -32700 | -32600 | -32601 | -32602 => Some(ErrorClass::Permanent),
151
152 -32603 => Some(ErrorClass::Retriable),
154
155 -32000 | -32002 | -32003 | -32004 => Some(ErrorClass::Permanent),
157
158 -32005 | 429 => Some(ErrorClass::RateLimited),
160
161 -32099..=-32001 => Some(ErrorClass::Retriable),
163
164 _ => None,
166 }
167}
168
169pub fn classify_evm_error(error: &EvmToolError) -> ErrorClass {
184 match error {
185 EvmToolError::NonceTooLow | EvmToolError::NonceMismatch => ErrorClass::Retriable,
187 EvmToolError::InsufficientFunds => ErrorClass::Permanent,
188 EvmToolError::TransactionReverted { .. } => ErrorClass::Permanent,
189 EvmToolError::RateLimited => ErrorClass::RateLimited,
190 EvmToolError::GasEstimationFailed => ErrorClass::Retriable,
191 EvmToolError::InvalidAddress(_) => ErrorClass::Permanent,
192 EvmToolError::ContractError(_) => ErrorClass::Permanent,
193 EvmToolError::SignerError(_) => ErrorClass::Permanent,
194 EvmToolError::UnsupportedChain(_) => ErrorClass::Permanent,
195 EvmToolError::InvalidParameter(_) => ErrorClass::Permanent,
196
197 EvmToolError::NetworkError(_msg) => ErrorClass::Retriable,
199
200 EvmToolError::ProviderError(msg) => {
202 if let Some(code) = extract_error_code(msg) {
204 if let Some(class) = classify_by_error_code(code) {
205 return class;
206 }
207 }
208 classify_provider_message(msg)
210 }
211
212 EvmToolError::Generic(msg) => classify_generic_message(msg),
214 }
215}
216
217impl From<EvmToolError> for riglr_core::ToolError {
218 fn from(err: EvmToolError) -> Self {
219 let class = classify_evm_error(&err);
220 match class {
221 ErrorClass::Permanent => {
222 riglr_core::ToolError::permanent_with_source(err, "EVM operation failed")
223 }
224 ErrorClass::Retriable => {
225 riglr_core::ToolError::retriable_with_source(err, "EVM operation can be retried")
226 }
227 ErrorClass::RateLimited => riglr_core::ToolError::rate_limited_with_source(
228 err,
229 "EVM rate limit exceeded",
230 None,
231 ),
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_extract_error_code() {
242 assert_eq!(extract_error_code("error code: -32000"), Some(-32000));
244 assert_eq!(extract_error_code("code: -32603"), Some(-32603));
245 assert_eq!(extract_error_code("429 Too Many Requests"), Some(429));
246 assert_eq!(extract_error_code("Status: 429 - Rate limited"), Some(429));
247 assert_eq!(
248 extract_error_code("error -32001: insufficient funds"),
249 Some(-32001)
250 );
251 assert_eq!(
252 extract_error_code("JSON-RPC error code: -32602 invalid params"),
253 Some(-32602)
254 );
255
256 assert_eq!(extract_error_code("connection timeout"), None);
258 assert_eq!(extract_error_code("invalid transaction"), None);
259 assert_eq!(extract_error_code("rate limit exceeded"), None);
260 }
261
262 #[test]
263 fn test_classify_by_error_code() {
264 assert_eq!(classify_by_error_code(-32700), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32600), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32601), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32602), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32603), Some(ErrorClass::Retriable)); assert_eq!(classify_by_error_code(-32000), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32002), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32003), Some(ErrorClass::Permanent)); assert_eq!(classify_by_error_code(-32004), Some(ErrorClass::Permanent)); assert_eq!(
281 classify_by_error_code(-32005),
282 Some(ErrorClass::RateLimited)
283 ); assert_eq!(classify_by_error_code(429), Some(ErrorClass::RateLimited)); assert_eq!(classify_by_error_code(-32050), Some(ErrorClass::Retriable));
288 assert_eq!(classify_by_error_code(-32099), Some(ErrorClass::Retriable));
289
290 assert_eq!(classify_by_error_code(500), None);
292 assert_eq!(classify_by_error_code(-33000), None);
293 }
294
295 #[test]
296 fn test_classify_evm_error_with_codes() {
297 let error =
299 EvmToolError::ProviderError("error code: -32000 insufficient funds".to_string());
300 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
301
302 let error = EvmToolError::ProviderError(
303 "JSON-RPC error code: -32603 internal server error".to_string(),
304 );
305 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
306
307 let error =
308 EvmToolError::ProviderError("429 Too Many Requests - please retry later".to_string());
309 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
310
311 let error = EvmToolError::ProviderError("error code: -32602 invalid params".to_string());
312 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
313
314 let error = EvmToolError::ProviderError("code: -32005 rate limit exceeded".to_string());
315 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
316 }
317
318 #[test]
319 fn test_classify_evm_error_fallback_string_matching() {
320 let error = EvmToolError::NetworkError("connection timeout after 30 seconds".to_string());
324 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
325
326 let error = EvmToolError::ProviderError("request timeout".to_string());
328 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
329
330 let error = EvmToolError::ProviderError("quota exceeded for API key".to_string());
331 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
332
333 let error = EvmToolError::NonceTooLow;
335 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
336
337 let error = EvmToolError::InsufficientFunds;
338 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
339
340 let error = EvmToolError::TransactionReverted {
341 reason: "Out of gas".to_string(),
342 };
343 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
344 }
345
346 #[test]
347 fn test_classify_evm_error_direct_variants() {
348 let error = EvmToolError::ContractError("method not found".to_string());
350 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
351
352 let error = EvmToolError::InvalidParameter("invalid address format".to_string());
353 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
354
355 let error = EvmToolError::InsufficientFunds;
356 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
357
358 let error = EvmToolError::InvalidAddress("not a valid hex address".to_string());
359 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
360
361 let error = EvmToolError::UnsupportedChain(999);
362 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
363
364 let error = EvmToolError::SignerError(riglr_core::SignerError::NoSignerContext);
365 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
366
367 let error = EvmToolError::RateLimited;
368 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
369
370 let error = EvmToolError::GasEstimationFailed;
371 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
372 }
373
374 #[test]
375 fn test_classify_evm_error_code_priority() {
376 let error =
380 EvmToolError::ProviderError("connection timeout, error code: -32000".to_string());
381 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
382
383 let error = EvmToolError::ProviderError("invalid request format code: 429".to_string());
385 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
386 }
387
388 #[test]
389 fn test_from_evm_tool_error_to_tool_error() {
390 let evm_error = EvmToolError::ProviderError("error code: -32000".to_string());
392 let tool_error: riglr_core::ToolError = evm_error.into();
393 assert!(tool_error.to_string().contains("-32000"));
395
396 let evm_error = EvmToolError::RateLimited;
397 let tool_error: riglr_core::ToolError = evm_error.into();
398 assert!(tool_error.to_string().contains("Rate limit exceeded"));
399
400 let evm_error = EvmToolError::Generic("timeout".to_string());
401 let tool_error: riglr_core::ToolError = evm_error.into();
402 assert!(tool_error.to_string().contains("timeout"));
403
404 let evm_error = EvmToolError::InsufficientFunds;
406 let tool_error: riglr_core::ToolError = evm_error.clone().into();
407 assert!(tool_error.to_string().contains("Insufficient funds"));
409 }
410
411 #[test]
412 fn test_classify_generic_message() {
413 assert_eq!(
415 classify_generic_message("connection reset"),
416 ErrorClass::Retriable
417 );
418 assert_eq!(
419 classify_generic_message("request timeout"),
420 ErrorClass::Retriable
421 );
422 assert_eq!(
423 classify_generic_message("unknown error"),
424 ErrorClass::Permanent
425 );
426 }
427
428 #[test]
429 fn test_new_structured_error_variants() {
430 let error = EvmToolError::NonceTooLow;
432 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
433
434 let error = EvmToolError::NonceMismatch;
435 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
436
437 let error = EvmToolError::InsufficientFunds;
438 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
439
440 let error = EvmToolError::TransactionReverted {
441 reason: "Execution failed".to_string(),
442 };
443 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
444
445 let error = EvmToolError::RateLimited;
446 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
447
448 let error = EvmToolError::GasEstimationFailed;
449 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
450
451 let error = EvmToolError::NetworkError("timeout".to_string());
452 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
453 }
454
455 #[test]
456 fn test_downcast_from_tool_error_source() {
457 let evm_error = EvmToolError::InsufficientFunds;
459
460 let tool_error: riglr_core::ToolError = evm_error.clone().into();
462
463 assert!(tool_error.to_string().contains("Insufficient funds"));
465
466 let evm_error = EvmToolError::TransactionReverted {
471 reason: "Out of gas".to_string(),
472 };
473 let tool_error: riglr_core::ToolError = evm_error.into();
474 assert!(tool_error.to_string().contains("Out of gas"));
475 }
476
477 #[test]
478 fn test_provider_error_classification_with_fallback() {
479 let error = EvmToolError::ProviderError("error code: -32000".to_string());
481 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
482
483 let error = EvmToolError::ProviderError("connection timeout".to_string());
485 assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
486
487 let error = EvmToolError::ProviderError("rate limit exceeded".to_string());
488 assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
489
490 let error = EvmToolError::ProviderError("unknown error".to_string());
491 assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
492 }
493}