#[derive(Debug, Clone, Copy)]
pub struct ExternalError {
pub selector: [u8; 4],
pub name: &'static str,
pub description: &'static str,
}
impl ExternalError {
pub fn selector_hex(&self) -> String {
hex::encode(self.selector)
}
}
pub mod task_lib {
use super::ExternalError;
pub const TASK_RESPONSE_TOO_LATE: ExternalError = ExternalError {
selector: [0xef, 0x58, 0xcb, 0x64],
name: "TaskResponseTooLate",
description: "Response submitted after taskCreatedBlock + responseWindowBlock",
};
pub const TASK_CREATED_BLOCK_TOO_OLD: ExternalError = ExternalError {
selector: [0xeb, 0x79, 0x0a, 0x32],
name: "TaskCreatedBlockTooOld",
description: "taskCreatedBlock is older than currentBlock - bufferWindow (30 blocks on L1, 180 on L2)",
};
pub const TASK_RESPONSE_WINDOW_PASSED: ExternalError = ExternalError {
selector: [0x8a, 0xb0, 0xa8, 0x9e],
name: "TaskResponseWindowPassed",
description: "Task response window has passed (current block > taskCreatedBlock + responseWindow)",
};
}
pub mod policy_validation {
use super::ExternalError;
pub const INVALID_SOURCE_OR_DESTINATION: ExternalError = ExternalError {
selector: [0x01, 0xa2, 0x35, 0x62],
name: "InvalidSourceOrDestination",
description: "Intent.from or intent.to is zero address",
};
pub const POLICY_ID_MISMATCH: ExternalError = ExternalError {
selector: [0xac, 0x0b, 0x52, 0xa7],
name: "PolicyIdMismatch",
description: "Policy ID mismatch",
};
pub const POLICY_ADDRESS_MISMATCH: ExternalError = ExternalError {
selector: [0xd6, 0x48, 0xc1, 0x8f],
name: "PolicyAddressMismatch",
description: "Policy address mismatch",
};
pub const POLICY_DATA_LENGTH_MISMATCH: ExternalError = ExternalError {
selector: [0x33, 0x17, 0xd5, 0x04],
name: "PolicyDataLengthMismatch",
description: "Policy data length mismatch",
};
pub const POLICY_DATA_ADDRESS_MISMATCH: ExternalError = ExternalError {
selector: [0xce, 0x35, 0x71, 0x3f],
name: "PolicyDataAddressMismatch",
description: "Policy data address mismatch",
};
pub const POLICY_DATA_EXPIRED: ExternalError = ExternalError {
selector: [0x6d, 0x58, 0x81, 0x5b],
name: "PolicyDataExpired",
description: "Policy data expired",
};
}
pub mod allocation_manager {
use super::ExternalError;
pub const SAME_MAGNITUDE: ExternalError = ExternalError {
selector: [0x8c, 0x0c, 0x2f, 0x26],
name: "SameMagnitude",
description: "Allocation magnitude is same as current value",
};
pub const INSUFFICIENT_MAGNITUDE: ExternalError = ExternalError {
selector: [0x4e, 0x23, 0xd0, 0x35],
name: "InsufficientMagnitude",
description: "Insufficient magnitude for allocation",
};
pub const INVALID_OPERATOR_SET: ExternalError = ExternalError {
selector: [0x7e, 0x0e, 0x1e, 0x8e],
name: "InvalidOperatorSet",
description: "Invalid operator set",
};
}
pub mod delegation_manager {
use super::ExternalError;
pub const OPERATOR_NOT_REGISTERED: ExternalError = ExternalError {
selector: [0x25, 0xec, 0x6c, 0x1f],
name: "OperatorNotRegistered",
description: "Operator is not registered with EigenLayer",
};
pub const ALREADY_REGISTERED: ExternalError = ExternalError {
selector: [0x0c, 0x74, 0xa0, 0xe1],
name: "AlreadyRegistered",
description: "Operator is already registered",
};
}
pub mod newton_errors {
pub const TASK_ALREADY_RESPONDED_NAME: &str = "TaskAlreadyResponded";
pub const TASK_ALREADY_RESPONDED_SELECTOR: &str = "68905dff";
pub const TASK_ALREADY_EXISTS_NAME: &str = "TaskAlreadyExists";
pub const TASK_ALREADY_EXISTS_SELECTOR: &str = "2e98c533";
pub const BATCH_PARTIAL_FAILURE_NAME: &str = "BatchPartialFailure";
pub const BATCH_PARTIAL_FAILURE_SELECTOR: &str = "19ed9977";
}
pub fn is_task_already_exists(error_str: &str) -> bool {
error_str.contains(newton_errors::TASK_ALREADY_EXISTS_NAME)
|| error_str.contains(newton_errors::TASK_ALREADY_EXISTS_SELECTOR)
}
pub fn is_task_already_responded(error_str: &str) -> bool {
error_str.contains(newton_errors::TASK_ALREADY_RESPONDED_NAME)
|| error_str.contains(newton_errors::TASK_ALREADY_RESPONDED_SELECTOR)
}
pub fn is_batch_partial_failure(error_str: &str) -> bool {
error_str.contains(newton_errors::BATCH_PARTIAL_FAILURE_NAME)
|| error_str.contains(newton_errors::BATCH_PARTIAL_FAILURE_SELECTOR)
}
pub fn is_transient_rpc_error(error_str: &str) -> bool {
let lower = error_str.to_lowercase();
let revert_patterns = ["execution reverted", "error code: 3,", "contracterror", "revert 0x"];
if revert_patterns.iter().any(|p| lower.contains(p)) {
return false;
}
let transient_patterns = [
"nonce",
"timeout",
"connection",
"network",
"server error",
"internal error",
"rate limit",
"429",
"503",
];
transient_patterns.iter().any(|p| lower.contains(p))
}
pub mod selectors {
pub const TASK_ALREADY_EXISTS: [u8; 4] = [0x2e, 0x98, 0xc5, 0x33];
pub const TASK_ALREADY_RESPONDED: [u8; 4] = [0x68, 0x90, 0x5d, 0xff];
pub const BATCH_PARTIAL_FAILURE: [u8; 4] = [0x19, 0xed, 0x99, 0x77];
pub const ITEM_LIKELY_OUT_OF_GAS: [u8; 4] = [0xb5, 0x2c, 0xd8, 0x90];
pub const INSUFFICIENT_GAS_FOR_ITEM: [u8; 4] = [0x06, 0xdf, 0xb4, 0x51];
pub const PANIC: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
}
pub fn panic_name(code: u8) -> &'static str {
match code {
0x00 => "GenericCompilerPanic",
0x01 => "AssertFalse",
0x11 => "ArithmeticOverflow",
0x12 => "DivisionByZero",
0x21 => "EnumOutOfRange",
0x22 => "StorageBytesEncodingError",
0x31 => "PopOnEmptyArray",
0x32 => "ArrayOutOfBounds",
0x41 => "OutOfMemory",
0x51 => "InvalidInternalFunction",
_ => "UnknownPanicCode",
}
}
pub fn decode_panic_code(data: &[u8]) -> Option<u8> {
if data.len() != 36 || data[0..4] != selectors::PANIC {
return None;
}
Some(data[35])
}
pub fn decode_uint_param(data: &[u8], param_index: usize) -> Option<u64> {
let start = 4 + param_index * 32;
let end = start + 32;
if data.len() < end {
return None;
}
let word = &data[start..end];
let mut buf = [0u8; 8];
buf.copy_from_slice(&word[24..32]);
Some(u64::from_be_bytes(buf))
}
pub fn classify_batch_item_revert(data: &[u8]) -> crate::error::BatchItemError {
use crate::error::BatchItemError;
if data.len() < 4 {
return BatchItemError::LikelyOutOfGas { gas_forwarded: None };
}
let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
match selector {
selectors::TASK_ALREADY_EXISTS => BatchItemError::TaskAlreadyExists,
selectors::TASK_ALREADY_RESPONDED => BatchItemError::TaskAlreadyResponded,
selectors::ITEM_LIKELY_OUT_OF_GAS => BatchItemError::LikelyOutOfGas {
gas_forwarded: decode_uint_param(data, 2),
},
selectors::INSUFFICIENT_GAS_FOR_ITEM => BatchItemError::InsufficientGasForItem {
gas_left: decode_uint_param(data, 2),
},
selectors::PANIC => {
let name = match decode_panic_code(data) {
Some(code) => format!("Panic({}): {}", code, panic_name(code)),
None => "Panic(uint256): malformed".to_string(),
};
BatchItemError::ContractRevert {
selector: hex::encode(selector),
name,
}
}
_ => {
let name = if let Some(ext) = lookup_external_error(&selector) {
ext.name.to_string()
} else {
format!("0x{}", hex::encode(selector))
};
BatchItemError::ContractRevert {
selector: hex::encode(selector),
name,
}
}
}
}
pub fn classify_top_level_revert(data: &[u8]) -> crate::error::ChainIoError {
use crate::error::ChainIoError;
if data.len() < 4 {
return ChainIoError::ContractRevert {
selector: String::new(),
name: "empty revert".to_string(),
raw_data: data.to_vec(),
};
}
let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
match selector {
selectors::TASK_ALREADY_EXISTS => {
let task_id = if data.len() >= 36 {
alloy::primitives::B256::from_slice(&data[4..36])
} else {
alloy::primitives::B256::ZERO
};
ChainIoError::TaskAlreadyExists { task_id }
}
selectors::TASK_ALREADY_RESPONDED => {
let task_id = if data.len() >= 36 {
alloy::primitives::B256::from_slice(&data[4..36])
} else {
alloy::primitives::B256::ZERO
};
ChainIoError::TaskAlreadyResponded { task_id }
}
selectors::PANIC => {
let name = match decode_panic_code(data) {
Some(code) => format!("Panic({}): {}", code, panic_name(code)),
None => "Panic(uint256): malformed".to_string(),
};
ChainIoError::ContractRevert {
selector: hex::encode(selector),
name,
raw_data: data.to_vec(),
}
}
_ => {
let name = if let Some(ext) = lookup_external_error(&selector) {
ext.name.to_string()
} else {
format!("0x{}", hex::encode(selector))
};
ChainIoError::ContractRevert {
selector: hex::encode(selector),
name,
raw_data: data.to_vec(),
}
}
}
}
static EXTERNAL_ERRORS: &[&ExternalError] = &[
&task_lib::TASK_RESPONSE_TOO_LATE,
&task_lib::TASK_CREATED_BLOCK_TOO_OLD,
&task_lib::TASK_RESPONSE_WINDOW_PASSED,
&policy_validation::INVALID_SOURCE_OR_DESTINATION,
&policy_validation::POLICY_ID_MISMATCH,
&policy_validation::POLICY_ADDRESS_MISMATCH,
&policy_validation::POLICY_DATA_LENGTH_MISMATCH,
&policy_validation::POLICY_DATA_ADDRESS_MISMATCH,
&policy_validation::POLICY_DATA_EXPIRED,
&allocation_manager::SAME_MAGNITUDE,
&allocation_manager::INSUFFICIENT_MAGNITUDE,
&allocation_manager::INVALID_OPERATOR_SET,
&delegation_manager::OPERATOR_NOT_REGISTERED,
&delegation_manager::ALREADY_REGISTERED,
];
pub fn lookup_external_error(selector: &[u8; 4]) -> Option<&'static ExternalError> {
EXTERNAL_ERRORS.iter().find(|e| &e.selector == selector).copied()
}
pub fn error_matches_selector(error_str: &str, error: &ExternalError) -> bool {
let hex_selector = error.selector_hex();
let prefixed = format!("0x{}", hex_selector);
error_str.contains(&prefixed) || error_str.contains(&hex_selector)
}
pub fn extract_selector_from_error(error_str: &str) -> Option<[u8; 4]> {
if let Some(pos) = error_str.find("0x") {
let hex_part = &error_str[pos + 2..];
if hex_part.len() >= 8 {
let hex_bytes = &hex_part[..8];
if let Ok(bytes) = hex::decode(hex_bytes) {
if bytes.len() == 4 {
return Some([bytes[0], bytes[1], bytes[2], bytes[3]]);
}
}
}
}
None
}
pub fn decode_error_from_string(error_str: &str) -> Option<String> {
if let Some(selector) = extract_selector_from_error(error_str) {
if let Some(ext_error) = lookup_external_error(&selector) {
return Some(format!("{} - {}", ext_error.name, ext_error.description));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lookup_task_response_too_late() {
let selector = [0xef, 0x58, 0xcb, 0x64];
let error = lookup_external_error(&selector);
assert!(error.is_some());
assert_eq!(error.unwrap().name, "TaskResponseTooLate");
}
#[test]
fn test_lookup_task_created_block_too_old() {
let selector = [0xeb, 0x79, 0x0a, 0x32];
let error = lookup_external_error(&selector);
assert!(error.is_some());
assert_eq!(error.unwrap().name, "TaskCreatedBlockTooOld");
}
#[test]
fn test_lookup_task_response_window_passed() {
let selector = [0x8a, 0xb0, 0xa8, 0x9e];
let error = lookup_external_error(&selector);
assert!(error.is_some());
assert_eq!(error.unwrap().name, "TaskResponseWindowPassed");
}
#[test]
fn test_lookup_unknown_selector() {
let selector = [0x00, 0x00, 0x00, 0x00];
let error = lookup_external_error(&selector);
assert!(error.is_none());
}
#[test]
fn test_selector_hex() {
assert_eq!(task_lib::TASK_RESPONSE_TOO_LATE.selector_hex(), "ef58cb64");
assert_eq!(task_lib::TASK_CREATED_BLOCK_TOO_OLD.selector_hex(), "eb790a32");
}
#[test]
fn test_is_task_already_exists_by_name() {
assert!(is_task_already_exists(
"TaskAlreadyExists(0x1234) - task with this ID already exists"
));
}
#[test]
fn test_is_task_already_exists_by_selector() {
assert!(is_task_already_exists("revert 0x2e98c533"));
}
#[test]
fn test_is_task_already_exists_negative() {
assert!(!is_task_already_exists("TaskMismatch"));
}
#[test]
fn test_is_task_already_responded_by_name() {
assert!(is_task_already_responded(
"TaskAlreadyResponded(0xabcd) - task already has a response"
));
}
#[test]
fn test_is_task_already_responded_by_selector() {
assert!(is_task_already_responded("revert 0x68905dff"));
}
#[test]
fn test_is_batch_partial_failure() {
assert!(is_batch_partial_failure("BatchPartialFailure: 3 items failed"));
assert!(is_batch_partial_failure("revert 0x19ed9977"));
assert!(!is_batch_partial_failure("nonce too low"));
}
#[test]
fn test_is_transient_rpc_error_transient() {
assert!(is_transient_rpc_error("nonce too low"));
assert!(is_transient_rpc_error("connection refused"));
assert!(is_transient_rpc_error("HTTP 429 Too Many Requests"));
assert!(is_transient_rpc_error("HTTP 503 Service Unavailable"));
assert!(is_transient_rpc_error("request timeout after 30s"));
assert!(is_transient_rpc_error("rate limit exceeded"));
}
#[test]
fn test_classify_task_already_exists() {
let mut data = vec![0x2e, 0x98, 0xc5, 0x33];
data.extend_from_slice(&[0u8; 32]);
let result = classify_batch_item_revert(&data);
assert_eq!(result, crate::error::BatchItemError::TaskAlreadyExists);
assert!(result.is_idempotent());
}
#[test]
fn test_classify_task_already_responded() {
let mut data = vec![0x68, 0x90, 0x5d, 0xff];
data.extend_from_slice(&[0u8; 32]);
let result = classify_batch_item_revert(&data);
assert_eq!(result, crate::error::BatchItemError::TaskAlreadyResponded);
assert!(result.is_idempotent());
}
#[test]
fn test_classify_unknown_selector() {
let data = vec![0xde, 0xad, 0xbe, 0xef];
let result = classify_batch_item_revert(&data);
match &result {
crate::error::BatchItemError::ContractRevert { selector, .. } => {
assert_eq!(selector, "deadbeef");
}
_ => panic!("expected ContractRevert"),
}
assert!(!result.is_idempotent());
}
#[test]
fn test_classify_short_data() {
let result = classify_batch_item_revert(&[0x01, 0x02]);
assert!(matches!(
result,
crate::error::BatchItemError::LikelyOutOfGas { gas_forwarded: None }
));
assert!(result.is_retryable());
}
#[test]
fn test_classify_panic_array_oob() {
let mut data = vec![0x4e, 0x48, 0x7b, 0x71];
data.extend_from_slice(&[0u8; 31]);
data.push(0x32);
let result = classify_batch_item_revert(&data);
match &result {
crate::error::BatchItemError::ContractRevert { selector, name } => {
assert_eq!(selector, "4e487b71");
assert!(name.contains("ArrayOutOfBounds"), "got: {name}");
assert!(name.contains("50"), "panic code 0x32 = 50: {name}");
}
_ => panic!("expected ContractRevert with decoded panic"),
}
assert!(!result.is_idempotent());
}
#[test]
fn test_decode_panic_code_codes() {
let mut data = vec![0x4e, 0x48, 0x7b, 0x71];
data.extend_from_slice(&[0u8; 31]);
data.push(0x11);
assert_eq!(decode_panic_code(&data), Some(0x11));
assert_eq!(panic_name(0x11), "ArithmeticOverflow");
assert_eq!(panic_name(0x12), "DivisionByZero");
assert_eq!(panic_name(0x32), "ArrayOutOfBounds");
}
#[test]
fn test_decode_panic_code_rejects_non_panic_selector() {
let mut data = vec![0xde, 0xad, 0xbe, 0xef];
data.extend_from_slice(&[0u8; 32]);
assert_eq!(decode_panic_code(&data), None);
}
#[test]
fn test_classify_known_external_error() {
let data = vec![0xef, 0x58, 0xcb, 0x64];
let result = classify_batch_item_revert(&data);
match result {
crate::error::BatchItemError::ContractRevert { name, .. } => {
assert_eq!(name, "TaskResponseTooLate");
}
_ => panic!("expected ContractRevert with known name"),
}
}
#[test]
fn test_classify_top_level_task_already_exists() {
let mut data = vec![0x2e, 0x98, 0xc5, 0x33];
data.extend_from_slice(&[0xAB; 32]); let result = classify_top_level_revert(&data);
match result {
crate::error::ChainIoError::TaskAlreadyExists { task_id } => {
assert_eq!(task_id[0], 0xAB);
}
_ => panic!("expected TaskAlreadyExists"),
}
}
#[test]
fn test_is_transient_rpc_error_non_transient() {
assert!(!is_transient_rpc_error("TaskAlreadyExists(0x1234)"));
assert!(!is_transient_rpc_error("revert 0x2e98c533"));
assert!(!is_transient_rpc_error(
"Internal server error: Failed to batch create tasks: \
Alloy Contract Error TransportError(ErrorResp(execution reverted))"
));
assert!(!is_transient_rpc_error("execution reverted: TaskCreatedBlockTooOld"));
assert!(!is_transient_rpc_error(
"ErrorResp { code: 3, message: \"execution reverted\", data: Some(\"0xeb790a32\") }"
));
assert!(!is_transient_rpc_error("Alloy Contract Error ContractError(...)"));
}
}