Skip to content

Commit f2d8003

Browse files
authored
feat: add helper for decoding custom errors (#1098)
* feat: add helper for decoding custom errors * use find_map
1 parent 315f9a2 commit f2d8003

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

crates/json-rpc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ serde.workspace = true
2424
serde_json = { workspace = true, features = ["std", "raw_value"] }
2525
thiserror.workspace = true
2626
tracing.workspace = true
27+
alloy-sol-types.workspace = true

crates/json-rpc/src/response/error.rs

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
use alloy_primitives::Bytes;
2+
use alloy_sol_types::SolInterface;
13
use serde::{
24
de::{DeserializeOwned, MapAccess, Visitor},
35
Deserialize, Deserializer, Serialize,
46
};
5-
use serde_json::value::RawValue;
7+
use serde_json::{value::RawValue, Value};
68
use std::{borrow::Borrow, fmt, marker::PhantomData};
79

810
/// A JSONRPC-2.0 error object.
@@ -67,6 +69,18 @@ impl<E> ErrorPayload<E> {
6769
}
6870
}
6971

72+
/// Recursively traverses the value, looking for hex data that it can extract.
73+
///
74+
/// Inspired by ethers-js logic:
75+
/// <https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53>
76+
fn spelunk_revert(value: &Value) -> Option<Bytes> {
77+
match value {
78+
Value::String(s) => s.parse().ok(),
79+
Value::Object(o) => o.values().find_map(spelunk_revert),
80+
_ => None,
81+
}
82+
}
83+
7084
impl<ErrData> fmt::Display for ErrorPayload<ErrData> {
7185
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7286
write!(f, "error code {}: {}", self.code, self.message)
@@ -224,10 +238,38 @@ where
224238
_ => Err(self),
225239
}
226240
}
241+
242+
/// Attempt to extract revert data from the JsonRpcError be recursively
243+
/// traversing the error's data field
244+
///
245+
/// This returns the first hex it finds in the data object, and its
246+
/// behavior may change with `serde_json` internal changes.
247+
///
248+
/// If no hex object is found, it will return an empty bytes IFF the error
249+
/// is a revert
250+
///
251+
/// Inspired by ethers-js logic:
252+
/// <https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53>
253+
pub fn as_revert_data(&self) -> Option<Bytes> {
254+
if self.message.contains("revert") {
255+
let value = Value::deserialize(self.data.as_ref()?.borrow()).ok()?;
256+
spelunk_revert(&value)
257+
} else {
258+
None
259+
}
260+
}
261+
262+
/// Extracts revert data and tries decoding it into given custom errors set.
263+
pub fn as_decoded_error<E: SolInterface>(&self, validate: bool) -> Option<E> {
264+
self.as_revert_data().and_then(|data| E::abi_decode(&data, validate).ok())
265+
}
227266
}
228267

229268
#[cfg(test)]
230269
mod test {
270+
use alloy_primitives::U256;
271+
use alloy_sol_types::sol;
272+
231273
use super::BorrowedErrorPayload;
232274
use crate::ErrorPayload;
233275

@@ -265,4 +307,21 @@ mod test {
265307
assert_eq!(payload.message, "20/second request limit reached - reduce calls per second or upgrade your account at quicknode.com");
266308
assert!(payload.data.is_none());
267309
}
310+
311+
#[test]
312+
fn custom_error_decoding() {
313+
sol!(
314+
library Errors {
315+
error SomeCustomError(uint256 a);
316+
}
317+
);
318+
319+
let json = r#"{"code":3,"message":"execution reverted: ","data":"0x810f00230000000000000000000000000000000000000000000000000000000000000001"}"#;
320+
let payload: ErrorPayload = serde_json::from_str(json).unwrap();
321+
322+
let Errors::ErrorsErrors::SomeCustomError(value) =
323+
payload.as_decoded_error::<Errors::ErrorsErrors>(false).unwrap();
324+
325+
assert_eq!(value.a, U256::from(1));
326+
}
268327
}

0 commit comments

Comments
 (0)