|
| 1 | +use alloy_primitives::Bytes; |
| 2 | +use alloy_sol_types::SolInterface; |
1 | 3 | use serde::{
|
2 | 4 | de::{DeserializeOwned, MapAccess, Visitor},
|
3 | 5 | Deserialize, Deserializer, Serialize,
|
4 | 6 | };
|
5 |
| -use serde_json::value::RawValue; |
| 7 | +use serde_json::{value::RawValue, Value}; |
6 | 8 | use std::{borrow::Borrow, fmt, marker::PhantomData};
|
7 | 9 |
|
8 | 10 | /// A JSONRPC-2.0 error object.
|
@@ -67,6 +69,18 @@ impl<E> ErrorPayload<E> {
|
67 | 69 | }
|
68 | 70 | }
|
69 | 71 |
|
| 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 | + |
70 | 84 | impl<ErrData> fmt::Display for ErrorPayload<ErrData> {
|
71 | 85 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
72 | 86 | write!(f, "error code {}: {}", self.code, self.message)
|
@@ -224,10 +238,38 @@ where
|
224 | 238 | _ => Err(self),
|
225 | 239 | }
|
226 | 240 | }
|
| 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 | + } |
227 | 266 | }
|
228 | 267 |
|
229 | 268 | #[cfg(test)]
|
230 | 269 | mod test {
|
| 270 | + use alloy_primitives::U256; |
| 271 | + use alloy_sol_types::sol; |
| 272 | + |
231 | 273 | use super::BorrowedErrorPayload;
|
232 | 274 | use crate::ErrorPayload;
|
233 | 275 |
|
@@ -265,4 +307,21 @@ mod test {
|
265 | 307 | assert_eq!(payload.message, "20/second request limit reached - reduce calls per second or upgrade your account at quicknode.com");
|
266 | 308 | assert!(payload.data.is_none());
|
267 | 309 | }
|
| 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 | + } |
268 | 327 | }
|
0 commit comments