use alloy::transports::{RpcError, TransportErrorKind};
use std::error::Error;
const PROVIDER_FAULT_CODES: [i64; 3] = [-32002, -32005, -32603];
const PROVIDER_FAULT_MESSAGES: [&str; 3] = ["rate limit", "too many requests", "limit exceeded"];
pub fn provider_answered(error: &(dyn Error + 'static)) -> bool {
for cause in std::iter::successors(Some(error), |&cause| cause.source()) {
if let Some(transport) = cause.downcast_ref::<RpcError<TransportErrorKind>>() {
return transport_answered(transport);
}
if let Some(contract) = cause.downcast_ref::<alloy::contract::Error>() {
return match contract {
alloy::contract::Error::TransportError(transport) => transport_answered(transport),
_ => true,
};
}
}
false
}
fn transport_answered(error: &RpcError<TransportErrorKind>) -> bool {
let Some(payload) = error.as_error_resp() else {
return false;
};
if PROVIDER_FAULT_CODES.contains(&payload.code) {
return false;
}
let message = payload.message.to_lowercase();
!PROVIDER_FAULT_MESSAGES
.iter()
.any(|fragment| message.contains(fragment))
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::rpc::json_rpc::ErrorPayload;
fn response(code: i64, message: &'static str) -> RpcError<TransportErrorKind> {
RpcError::ErrorResp(ErrorPayload {
code,
message: message.into(),
data: None,
})
}
#[test]
fn request_errors_count_as_answered() {
assert!(provider_answered(&response(3, "execution reverted")));
assert!(provider_answered(&response(-32000, "nonce too low")));
assert!(provider_answered(&response(-32000, "already known")));
assert!(provider_answered(&response(
-32000,
"insufficient funds for gas * price + value"
)));
}
#[test]
fn provider_faults_count_as_not_answered() {
assert!(!provider_answered(&response(-32005, "limit exceeded")));
assert!(!provider_answered(&response(-32603, "internal error")));
assert!(!provider_answered(&response(-32000, "Too Many Requests")));
}
#[test]
fn transport_failures_count_as_not_answered() {
let error: RpcError<TransportErrorKind> = TransportErrorKind::custom_str("connection refused");
assert!(!provider_answered(&error));
assert!(!provider_answered(&RpcError::<TransportErrorKind>::NullResp));
}
#[test]
fn contract_errors_delegate_to_the_transport_error() {
let revert = alloy::contract::Error::TransportError(response(3, "execution reverted"));
assert!(provider_answered(&revert));
let down = alloy::contract::Error::TransportError(TransportErrorKind::custom_str("connection reset"));
assert!(!provider_answered(&down));
}
#[test]
fn wrapped_errors_are_classified_through_the_source_chain() {
let report = eyre::Report::new(response(3, "execution reverted")).wrap_err("planning failed");
assert!(provider_answered(report.as_ref()));
let unrelated = eyre::eyre!("destination RPC returned no latest block");
assert!(!provider_answered(unrelated.as_ref()));
}
}