Skip to content

Commit

Permalink
feat: add helper for decoding custom errors (#1098)
Browse files Browse the repository at this point in the history
* feat: add helper for decoding custom errors

* use find_map
  • Loading branch information
klkvr authored Jul 24, 2024
1 parent 315f9a2 commit f2d8003
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 1 deletion.
1 change: 1 addition & 0 deletions crates/json-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
61 changes: 60 additions & 1 deletion crates/json-rpc/src/response/error.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -67,6 +69,18 @@ impl<E> ErrorPayload<E> {
}
}

/// Recursively traverses the value, looking for hex data that it can extract.
///
/// Inspired by ethers-js logic:
/// <https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53>
fn spelunk_revert(value: &Value) -> Option<Bytes> {
match value {
Value::String(s) => s.parse().ok(),
Value::Object(o) => o.values().find_map(spelunk_revert),
_ => None,
}
}

impl<ErrData> fmt::Display for ErrorPayload<ErrData> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error code {}: {}", self.code, self.message)
Expand Down Expand Up @@ -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:
/// <https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53>
pub fn as_revert_data(&self) -> Option<Bytes> {
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<E: SolInterface>(&self, validate: bool) -> Option<E> {
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;

Expand Down Expand Up @@ -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::<Errors::ErrorsErrors>(false).unwrap();

assert_eq!(value.a, U256::from(1));
}
}

0 comments on commit f2d8003

Please sign in to comment.