use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum EvmToolError {
#[error("Provider error: {0}")]
ProviderError(String),
#[error("Transaction reverted: {reason}")]
TransactionReverted { reason: String },
#[error("Nonce too low")]
NonceTooLow,
#[error("Nonce mismatch")]
NonceMismatch,
#[error("Insufficient funds")]
InsufficientFunds,
#[error("Gas estimation failed")]
GasEstimationFailed,
#[error("Invalid address: {0}")]
InvalidAddress(String),
#[error("Contract error: {0}")]
ContractError(String),
#[error("Signer error: {0}")]
SignerError(#[from] riglr_core::SignerError),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Rate limit exceeded")]
RateLimited,
#[error("Unsupported chain: {0}")]
UnsupportedChain(u64),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("{0}")]
Generic(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorClass {
Permanent,
Retriable,
RateLimited,
}
fn classify_provider_message(msg: &str) -> ErrorClass {
if msg.contains("timeout") || msg.contains("connection") {
ErrorClass::Retriable
} else if msg.contains("rate") || msg.contains("quota") {
ErrorClass::RateLimited
} else {
ErrorClass::Permanent
}
}
fn classify_generic_message(msg: &str) -> ErrorClass {
if msg.contains("timeout") || msg.contains("connection") {
ErrorClass::Retriable
} else {
ErrorClass::Permanent
}
}
fn extract_error_code(msg: &str) -> Option<i32> {
if let Some(idx) = msg.find("code:") {
let code_str = &msg[idx + 5..].trim();
if let Some(end_idx) = code_str.find(|c: char| !c.is_ascii_digit() && c != '-') {
return code_str[..end_idx].parse::<i32>().ok();
}
return code_str.split_whitespace().next()?.parse::<i32>().ok();
}
if msg.starts_with("429") || msg.contains(" 429 ") {
return Some(429);
}
if let Some(idx) = msg.find("error ") {
let code_str = &msg[idx + 6..].trim();
if let Some(stripped) = code_str.strip_prefix('-') {
if let Some(end_idx) = stripped.find(|c: char| !c.is_ascii_digit()) {
return code_str[..=end_idx].parse::<i32>().ok();
}
}
}
None
}
fn classify_by_error_code(code: i32) -> Option<ErrorClass> {
match code {
-32700 | -32600 | -32601 | -32602 => Some(ErrorClass::Permanent),
-32603 => Some(ErrorClass::Retriable),
-32000 | -32002 | -32003 | -32004 => Some(ErrorClass::Permanent),
-32005 | 429 => Some(ErrorClass::RateLimited),
-32099..=-32001 => Some(ErrorClass::Retriable),
_ => None,
}
}
pub fn classify_evm_error(error: &EvmToolError) -> ErrorClass {
match error {
EvmToolError::NonceTooLow | EvmToolError::NonceMismatch => ErrorClass::Retriable,
EvmToolError::InsufficientFunds => ErrorClass::Permanent,
EvmToolError::TransactionReverted { .. } => ErrorClass::Permanent,
EvmToolError::RateLimited => ErrorClass::RateLimited,
EvmToolError::GasEstimationFailed => ErrorClass::Retriable,
EvmToolError::InvalidAddress(_) => ErrorClass::Permanent,
EvmToolError::ContractError(_) => ErrorClass::Permanent,
EvmToolError::SignerError(_) => ErrorClass::Permanent,
EvmToolError::UnsupportedChain(_) => ErrorClass::Permanent,
EvmToolError::InvalidParameter(_) => ErrorClass::Permanent,
EvmToolError::NetworkError(_msg) => ErrorClass::Retriable,
EvmToolError::ProviderError(msg) => {
if let Some(code) = extract_error_code(msg) {
if let Some(class) = classify_by_error_code(code) {
return class;
}
}
classify_provider_message(msg)
}
EvmToolError::Generic(msg) => classify_generic_message(msg),
}
}
impl From<EvmToolError> for riglr_core::ToolError {
fn from(err: EvmToolError) -> Self {
let class = classify_evm_error(&err);
match class {
ErrorClass::Permanent => {
riglr_core::ToolError::permanent_with_source(err, "EVM operation failed")
}
ErrorClass::Retriable => {
riglr_core::ToolError::retriable_with_source(err, "EVM operation can be retried")
}
ErrorClass::RateLimited => riglr_core::ToolError::rate_limited_with_source(
err,
"EVM rate limit exceeded",
None,
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_error_code() {
assert_eq!(extract_error_code("error code: -32000"), Some(-32000));
assert_eq!(extract_error_code("code: -32603"), Some(-32603));
assert_eq!(extract_error_code("429 Too Many Requests"), Some(429));
assert_eq!(extract_error_code("Status: 429 - Rate limited"), Some(429));
assert_eq!(
extract_error_code("error -32001: insufficient funds"),
Some(-32001)
);
assert_eq!(
extract_error_code("JSON-RPC error code: -32602 invalid params"),
Some(-32602)
);
assert_eq!(extract_error_code("connection timeout"), None);
assert_eq!(extract_error_code("invalid transaction"), None);
assert_eq!(extract_error_code("rate limit exceeded"), None);
}
#[test]
fn test_classify_by_error_code() {
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!(
classify_by_error_code(-32005),
Some(ErrorClass::RateLimited)
); assert_eq!(classify_by_error_code(429), Some(ErrorClass::RateLimited));
assert_eq!(classify_by_error_code(-32050), Some(ErrorClass::Retriable));
assert_eq!(classify_by_error_code(-32099), Some(ErrorClass::Retriable));
assert_eq!(classify_by_error_code(500), None);
assert_eq!(classify_by_error_code(-33000), None);
}
#[test]
fn test_classify_evm_error_with_codes() {
let error =
EvmToolError::ProviderError("error code: -32000 insufficient funds".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::ProviderError(
"JSON-RPC error code: -32603 internal server error".to_string(),
);
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error =
EvmToolError::ProviderError("429 Too Many Requests - please retry later".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
let error = EvmToolError::ProviderError("error code: -32602 invalid params".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::ProviderError("code: -32005 rate limit exceeded".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
}
#[test]
fn test_classify_evm_error_fallback_string_matching() {
let error = EvmToolError::NetworkError("connection timeout after 30 seconds".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::ProviderError("request timeout".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::ProviderError("quota exceeded for API key".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
let error = EvmToolError::NonceTooLow;
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::InsufficientFunds;
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::TransactionReverted {
reason: "Out of gas".to_string(),
};
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
}
#[test]
fn test_classify_evm_error_direct_variants() {
let error = EvmToolError::ContractError("method not found".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::InvalidParameter("invalid address format".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::InsufficientFunds;
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::InvalidAddress("not a valid hex address".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::UnsupportedChain(999);
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::SignerError(riglr_core::SignerError::NoSignerContext);
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::RateLimited;
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
let error = EvmToolError::GasEstimationFailed;
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
}
#[test]
fn test_classify_evm_error_code_priority() {
let error =
EvmToolError::ProviderError("connection timeout, error code: -32000".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::ProviderError("invalid request format code: 429".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
}
#[test]
fn test_from_evm_tool_error_to_tool_error() {
let evm_error = EvmToolError::ProviderError("error code: -32000".to_string());
let tool_error: riglr_core::ToolError = evm_error.into();
assert!(tool_error.to_string().contains("-32000"));
let evm_error = EvmToolError::RateLimited;
let tool_error: riglr_core::ToolError = evm_error.into();
assert!(tool_error.to_string().contains("Rate limit exceeded"));
let evm_error = EvmToolError::Generic("timeout".to_string());
let tool_error: riglr_core::ToolError = evm_error.into();
assert!(tool_error.to_string().contains("timeout"));
let evm_error = EvmToolError::InsufficientFunds;
let tool_error: riglr_core::ToolError = evm_error.clone().into();
assert!(tool_error.to_string().contains("Insufficient funds"));
}
#[test]
fn test_classify_generic_message() {
assert_eq!(
classify_generic_message("connection reset"),
ErrorClass::Retriable
);
assert_eq!(
classify_generic_message("request timeout"),
ErrorClass::Retriable
);
assert_eq!(
classify_generic_message("unknown error"),
ErrorClass::Permanent
);
}
#[test]
fn test_new_structured_error_variants() {
let error = EvmToolError::NonceTooLow;
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::NonceMismatch;
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::InsufficientFunds;
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::TransactionReverted {
reason: "Execution failed".to_string(),
};
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::RateLimited;
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
let error = EvmToolError::GasEstimationFailed;
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::NetworkError("timeout".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
}
#[test]
fn test_downcast_from_tool_error_source() {
let evm_error = EvmToolError::InsufficientFunds;
let tool_error: riglr_core::ToolError = evm_error.clone().into();
assert!(tool_error.to_string().contains("Insufficient funds"));
let evm_error = EvmToolError::TransactionReverted {
reason: "Out of gas".to_string(),
};
let tool_error: riglr_core::ToolError = evm_error.into();
assert!(tool_error.to_string().contains("Out of gas"));
}
#[test]
fn test_provider_error_classification_with_fallback() {
let error = EvmToolError::ProviderError("error code: -32000".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
let error = EvmToolError::ProviderError("connection timeout".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Retriable);
let error = EvmToolError::ProviderError("rate limit exceeded".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::RateLimited);
let error = EvmToolError::ProviderError("unknown error".to_string());
assert_eq!(classify_evm_error(&error), ErrorClass::Permanent);
}
}