diff --git a/crates/json-rpc/Cargo.toml b/crates/json-rpc/Cargo.toml index 9a2deef43c8..81a2d6b9b28 100644 --- a/crates/json-rpc/Cargo.toml +++ b/crates/json-rpc/Cargo.toml @@ -24,3 +24,4 @@ serde.workspace = true serde_json = { workspace = true, features = ["std", "raw_value"] } thiserror.workspace = true tracing.workspace = true +alloy-sol-types.workspace = true diff --git a/crates/json-rpc/src/response/error.rs b/crates/json-rpc/src/response/error.rs index c6de4dba544..edf94315d26 100644 --- a/crates/json-rpc/src/response/error.rs +++ b/crates/json-rpc/src/response/error.rs @@ -1,8 +1,10 @@ +use alloy_primitives::Bytes; +use alloy_sol_types::SolInterface; use serde::{ de::{DeserializeOwned, MapAccess, Visitor}, Deserialize, Deserializer, Serialize, }; -use serde_json::value::RawValue; +use serde_json::{value::RawValue, Value}; use std::{borrow::Borrow, fmt, marker::PhantomData}; /// A JSONRPC-2.0 error object. @@ -67,6 +69,18 @@ impl ErrorPayload { } } +/// Recursively traverses the value, looking for hex data that it can extract. +/// +/// Inspired by ethers-js logic: +/// +fn spelunk_revert(value: &Value) -> Option { + match value { + Value::String(s) => s.parse().ok(), + Value::Object(o) => o.values().find_map(spelunk_revert), + _ => None, + } +} + impl fmt::Display for ErrorPayload { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "error code {}: {}", self.code, self.message) @@ -224,10 +238,38 @@ where _ => Err(self), } } + + /// Attempt to extract revert data from the JsonRpcError be recursively + /// traversing the error's data field + /// + /// This returns the first hex it finds in the data object, and its + /// behavior may change with `serde_json` internal changes. + /// + /// If no hex object is found, it will return an empty bytes IFF the error + /// is a revert + /// + /// Inspired by ethers-js logic: + /// + pub fn as_revert_data(&self) -> Option { + if self.message.contains("revert") { + let value = Value::deserialize(self.data.as_ref()?.borrow()).ok()?; + spelunk_revert(&value) + } else { + None + } + } + + /// Extracts revert data and tries decoding it into given custom errors set. + pub fn as_decoded_error(&self, validate: bool) -> Option { + self.as_revert_data().and_then(|data| E::abi_decode(&data, validate).ok()) + } } #[cfg(test)] mod test { + use alloy_primitives::U256; + use alloy_sol_types::sol; + use super::BorrowedErrorPayload; use crate::ErrorPayload; @@ -265,4 +307,21 @@ mod test { assert_eq!(payload.message, "20/second request limit reached - reduce calls per second or upgrade your account at quicknode.com"); assert!(payload.data.is_none()); } + + #[test] + fn custom_error_decoding() { + sol!( + library Errors { + error SomeCustomError(uint256 a); + } + ); + + let json = r#"{"code":3,"message":"execution reverted: ","data":"0x810f00230000000000000000000000000000000000000000000000000000000000000001"}"#; + let payload: ErrorPayload = serde_json::from_str(json).unwrap(); + + let Errors::ErrorsErrors::SomeCustomError(value) = + payload.as_decoded_error::(false).unwrap(); + + assert_eq!(value.a, U256::from(1)); + } }