From 231c870ff3813a946cd778133e3ffb3cdf7b4b46 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 24 Oct 2025 12:48:56 +0300 Subject: [PATCH 1/6] add trait for error registry --- Cargo.lock | 1 + Cargo.toml | 1 + src/error.rs | 183 +++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 137 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15b6f89..55c8cd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2570,6 +2570,7 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", + "async-trait", "getrandom 0.2.16", "once_cell", "reqwest 0.11.27", diff --git a/Cargo.toml b/Cargo.toml index b368aab..488fa4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ reqwest = { version = "0.11.27", features = ["json"] } serde_json = "1.0.111" thiserror = "1.0.56" serde = "1.0.195" +async-trait = "0.1" [target.'cfg(target_family = "wasm")'.dependencies] getrandom = { version = "0.2.11", features = ["js", "js-sys"] } diff --git a/src/error.rs b/src/error.rs index 5e3e33a..51ed658 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ use alloy::json_abi::Error as AlloyError; use alloy::primitives::hex::{decode, encode, encode_prefixed, FromHexError}; use alloy::primitives::U256; use alloy::rpc::json_rpc::ErrorPayload; +use async_trait::async_trait; use once_cell::sync::Lazy; use reqwest::{Client, Error as ReqwestError}; use serde_json::Value; @@ -14,6 +15,62 @@ use thiserror::Error; pub const SELECTOR_REGISTRY_URL: &str = "https://api.openchain.xyz/signature-database/v1/lookup"; +/// Trait for pluggable error selector registries. +/// +/// Implement this trait to provide alternative lookup sources +/// (e.g. local cache, different HTTP service, bundled table). +#[async_trait] +pub trait ErrorRegistry: Send + Sync { + /// Lookup candidate ABI errors for a given 4-byte selector. + async fn lookup(&self, selector: [u8; 4]) -> Result, AbiDecodeFailedErrors>; +} + +/// Default OpenChain-backed registry implementation. +pub struct OpenChainRegistry { + client: Client, + url: String, +} + +impl Default for OpenChainRegistry { + fn default() -> Self { + Self { + client: Client::builder().build().expect("reqwest client"), + url: SELECTOR_REGISTRY_URL.to_string(), + } + } +} + +#[async_trait] +impl ErrorRegistry for OpenChainRegistry { + async fn lookup(&self, selector: [u8; 4]) -> Result, AbiDecodeFailedErrors> { + let selector_hash = alloy::primitives::hex::encode_prefixed(selector); + let response = self + .client + .get(&self.url) + .query(&vec![ + ("function", selector_hash.as_str()), + ("filter", "true"), + ]) + .header("accept", "application/json") + .send() + .await? + .json::() + .await?; + + let mut out: Vec = Vec::new(); + if let Some(selectors) = response["result"]["function"][selector_hash].as_array() { + for opt_selector in selectors { + if let Some(name) = opt_selector["name"].as_str() { + if let Ok(err) = name.parse::() { + out.push(err); + } + } + } + } + Ok(out) + } +} + // panic selector pub const PANIC_SIG: &str = "Panic(uint256)"; pub const PANIC_SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71]; // 0x4e487b71 @@ -63,9 +120,10 @@ impl AbiDecodedErrorType { Ok(selectors.get(&selector_hash).cloned()) } - /// decodes an error returned from calling a contract by searching its selector in registry - pub async fn selector_registry_abi_decode( + /// Decode an error using an injected registry for selector lookups. + pub async fn decode_with_registry( error_data: &[u8], + registry: &dyn ErrorRegistry, ) -> Result { if error_data.is_empty() { return Err(AbiDecodeFailedErrors::NoData); @@ -76,7 +134,6 @@ impl AbiDecodedErrorType { )); } let (hash_bytes, args_data) = error_data.split_at(4); - let selector_hash = alloy::primitives::hex::encode_prefixed(hash_bytes); let selector_hash_bytes: [u8; 4] = hash_bytes .try_into() .map_err(|_| AbiDecodeFailedErrors::InvalidSelectorHash(hash_bytes.to_vec()))?; @@ -99,44 +156,33 @@ impl AbiDecodedErrorType { } return Ok(Self::Unknown(error_data.to_vec())); } - - let client = Client::builder().build()?; - let response = client - .get(SELECTOR_REGISTRY_URL) - .query(&vec![ - ("function", selector_hash.as_str()), - ("filter", "true"), - ]) - .header("accept", "application/json") - .send() - .await? - .json::() - .await?; - - if let Some(selectors) = response["result"]["function"][selector_hash].as_array() { - for opt_selector in selectors { - if let Some(selector) = opt_selector["name"].as_str() { - if let Ok(error) = selector.parse::() { - if let Ok(result) = error.abi_decode_input(args_data) { - // cache the fetched selector - { - let mut cached_selectors = SELECTORS.lock()?; - cached_selectors.insert(selector_hash_bytes, error.clone()); - }; - return Ok(Self::Known { - sig: error.signature(), - name: error.name, - args: result.iter().map(|v| format!("{:?}", v)).collect(), - data: error_data.to_vec(), - }); - } - } + // consult the injected registry + let candidates = registry.lookup(selector_hash_bytes).await?; + for error in candidates { + if let Ok(result) = error.abi_decode_input(args_data) { + // cache the fetched selector + { + let mut cached_selectors = SELECTORS.lock()?; + cached_selectors.insert(selector_hash_bytes, error.clone()); } + return Ok(Self::Known { + sig: error.signature(), + name: error.name, + args: result.iter().map(|v| format!("{:?}", v)).collect(), + data: error_data.to_vec(), + }); } - Ok(Self::Unknown(error_data.to_vec())) - } else { - Ok(Self::Unknown(error_data.to_vec())) } + + Ok(Self::Unknown(error_data.to_vec())) + } + + /// Backwards-compatible decode that uses the default OpenChain registry. + pub async fn selector_registry_abi_decode( + error_data: &[u8], + ) -> Result { + let registry = OpenChainRegistry::default(); + Self::decode_with_registry(error_data, ®istry).await } /// Decodes an error by checking if it is a Panic(uint256) and returns `None` if @@ -191,6 +237,25 @@ impl AbiDecodedErrorType { err.to_string(), )) } + + /// Variant of JSON-RPC error decoding that accepts an injected registry. + pub async fn try_from_json_rpc_error_with_registry( + err: ErrorPayload, + registry: &dyn ErrorRegistry, + ) -> Result { + if err.message.contains("revert") { + if let Some(val) = &err.data { + let unwrapped: String = serde_json::from_str(val.get()).map_err(|_| { + AbiDecodeFailedErrors::InvalidJsonRpcResponse(val.get().to_string()) + })?; + let decoded_data = decode(unwrapped.as_bytes())?; + return Self::decode_with_registry(&decoded_data, registry).await; + } + } + Err(AbiDecodeFailedErrors::InvalidJsonRpcResponse( + err.to_string(), + )) + } } #[derive(Debug, Error)] @@ -218,12 +283,31 @@ impl<'a> From>>> for Abi #[cfg(test)] mod tests { use super::*; + use async_trait::async_trait; use serde_json::value::RawValue; + struct FakeRegistry; + + #[async_trait] + impl ErrorRegistry for FakeRegistry { + async fn lookup( + &self, + selector: [u8; 4], + ) -> Result, AbiDecodeFailedErrors> { + // 0x1ac66908 + if selector == [0x1a, 0xc6, 0x69, 0x08] { + let e: AlloyError = "UnexpectedOperandValue()".parse().unwrap(); + Ok(vec![e]) + } else { + Ok(vec![]) + } + } + } + #[tokio::test] async fn test_error_decoder() { let data = vec![26, 198, 105, 8]; - let res = AbiDecodedErrorType::selector_registry_abi_decode(&data.clone()) + let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) .await .expect("failed to get error selector"); assert_eq!( @@ -240,7 +324,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_unknown() { let data = vec![26, 198, 105, 9]; - let res = AbiDecodedErrorType::selector_registry_abi_decode(&data.clone()) + let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) .await .expect("failed to get error selector"); assert_eq!(AbiDecodedErrorType::Unknown(data), res); @@ -249,7 +333,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_invalid_selector() { let data = vec![26, 198, 105]; - let res = AbiDecodedErrorType::selector_registry_abi_decode(&data.clone()) + let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) .await .expect_err("expected error"); match res { @@ -261,7 +345,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_no_data() { let data = vec![]; - let res = AbiDecodedErrorType::selector_registry_abi_decode(&data.clone()) + let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) .await .expect_err("expected error"); match res { @@ -273,7 +357,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_cache() { let data = vec![26, 198, 105, 8]; - let res = AbiDecodedErrorType::selector_registry_abi_decode(&data.clone()) + let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) .await .expect("failed to get error selector"); assert_eq!( @@ -308,11 +392,14 @@ mod tests { async fn test_error_decoder_json_rpc_error() { let data = vec![26, 198, 105, 8]; let encoded = encode(&data); - let res = AbiDecodedErrorType::try_from_json_rpc_error(ErrorPayload { - code: 3, - data: Some(RawValue::from_string(format!(r#""{encoded}""#)).unwrap()), - message: "execution reverted".into(), - }) + let res = AbiDecodedErrorType::try_from_json_rpc_error_with_registry( + ErrorPayload { + code: 3, + data: Some(RawValue::from_string(format!(r#""{encoded}""#)).unwrap()), + message: "execution reverted".into(), + }, + &FakeRegistry, + ) .await .expect("failed to get error selector"); assert_eq!( From bb6a2fd068a7b5677974f88ecc95ea9107511434 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 24 Oct 2025 12:59:30 +0300 Subject: [PATCH 2/6] fix --- src/error.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index 51ed658..e3cf025 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,7 +19,7 @@ pub const SELECTOR_REGISTRY_URL: &str = "https://api.openchain.xyz/signature-dat /// /// Implement this trait to provide alternative lookup sources /// (e.g. local cache, different HTTP service, bundled table). -#[async_trait] +#[async_trait(?Send)] pub trait ErrorRegistry: Send + Sync { /// Lookup candidate ABI errors for a given 4-byte selector. async fn lookup(&self, selector: [u8; 4]) -> Result, AbiDecodeFailedErrors>; @@ -34,13 +34,13 @@ pub struct OpenChainRegistry { impl Default for OpenChainRegistry { fn default() -> Self { Self { - client: Client::builder().build().expect("reqwest client"), + client: Client::new(), url: SELECTOR_REGISTRY_URL.to_string(), } } } -#[async_trait] +#[async_trait(?Send)] impl ErrorRegistry for OpenChainRegistry { async fn lookup(&self, selector: [u8; 4]) -> Result, AbiDecodeFailedErrors> { let selector_hash = alloy::primitives::hex::encode_prefixed(selector); @@ -288,7 +288,7 @@ mod tests { struct FakeRegistry; - #[async_trait] + #[async_trait(?Send)] impl ErrorRegistry for FakeRegistry { async fn lookup( &self, From c97742d820aa94e825c373f226b004a8d258c30d Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 24 Oct 2025 13:12:05 +0300 Subject: [PATCH 3/6] ai comment update --- src/error.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/error.rs b/src/error.rs index e3cf025..879b137 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,6 @@ use alloy::json_abi::Error as AlloyError; use alloy::primitives::hex::{decode, encode, encode_prefixed, FromHexError}; use alloy::primitives::U256; use alloy::rpc::json_rpc::ErrorPayload; -use async_trait::async_trait; use once_cell::sync::Lazy; use reqwest::{Client, Error as ReqwestError}; use serde_json::Value; @@ -19,7 +18,8 @@ pub const SELECTOR_REGISTRY_URL: &str = "https://api.openchain.xyz/signature-dat /// /// Implement this trait to provide alternative lookup sources /// (e.g. local cache, different HTTP service, bundled table). -#[async_trait(?Send)] +#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)] pub trait ErrorRegistry: Send + Sync { /// Lookup candidate ABI errors for a given 4-byte selector. async fn lookup(&self, selector: [u8; 4]) -> Result, AbiDecodeFailedErrors>; @@ -40,7 +40,8 @@ impl Default for OpenChainRegistry { } } -#[async_trait(?Send)] +#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)] impl ErrorRegistry for OpenChainRegistry { async fn lookup(&self, selector: [u8; 4]) -> Result, AbiDecodeFailedErrors> { let selector_hash = alloy::primitives::hex::encode_prefixed(selector); @@ -79,6 +80,10 @@ pub const PANIC_SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71]; // 0x4e487b71 pub static SELECTORS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +/// Default registry instance reused across calls to avoid repeatedly +/// constructing a reqwest::Client. +pub static DEFAULT_REGISTRY: Lazy = Lazy::new(OpenChainRegistry::default); + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Error)] pub enum AbiDecodedErrorType { Unknown(Vec), @@ -181,8 +186,7 @@ impl AbiDecodedErrorType { pub async fn selector_registry_abi_decode( error_data: &[u8], ) -> Result { - let registry = OpenChainRegistry::default(); - Self::decode_with_registry(error_data, ®istry).await + Self::decode_with_registry(error_data, &*DEFAULT_REGISTRY).await } /// Decodes an error by checking if it is a Panic(uint256) and returns `None` if @@ -283,12 +287,12 @@ impl<'a> From>>> for Abi #[cfg(test)] mod tests { use super::*; - use async_trait::async_trait; use serde_json::value::RawValue; struct FakeRegistry; - #[async_trait(?Send)] + #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))] + #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)] impl ErrorRegistry for FakeRegistry { async fn lookup( &self, @@ -307,7 +311,7 @@ mod tests { #[tokio::test] async fn test_error_decoder() { let data = vec![26, 198, 105, 8]; - let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) + let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) .await .expect("failed to get error selector"); assert_eq!( @@ -324,7 +328,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_unknown() { let data = vec![26, 198, 105, 9]; - let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) + let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) .await .expect("failed to get error selector"); assert_eq!(AbiDecodedErrorType::Unknown(data), res); @@ -333,7 +337,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_invalid_selector() { let data = vec![26, 198, 105]; - let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) + let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) .await .expect_err("expected error"); match res { @@ -345,7 +349,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_no_data() { let data = vec![]; - let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) + let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) .await .expect_err("expected error"); match res { @@ -356,8 +360,10 @@ mod tests { #[tokio::test] async fn test_error_decoder_cache() { + // ensure cache is empty for this test + clear_cache(); let data = vec![26, 198, 105, 8]; - let res = AbiDecodedErrorType::decode_with_registry(&data.clone(), &FakeRegistry) + let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) .await .expect("failed to get error selector"); assert_eq!( @@ -388,6 +394,13 @@ mod tests { assert_eq!(None, res); } + fn clear_cache() { + let mut cache = SELECTORS + .lock() + .expect("failed to lock selectors cache for clearing"); + cache.clear(); + } + #[tokio::test] async fn test_error_decoder_json_rpc_error() { let data = vec![26, 198, 105, 8]; From b732939033149eecc2394272c57f376bb51c56e5 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 24 Oct 2025 14:27:20 +0300 Subject: [PATCH 4/6] add real test --- src/error.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/error.rs b/src/error.rs index 879b137..2a6535a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -552,4 +552,31 @@ mod tests { res ); } + + #[tokio::test] + async fn test_openchain_registry_live_lookup_known_selector() { + clear_cache(); + + let data = vec![0x1a, 0xc6, 0x69, 0x08]; + + let registry = OpenChainRegistry::default(); + let res = AbiDecodedErrorType::decode_with_registry(&data, ®istry) + .await + .expect("OpenChain lookup failed"); + + match res { + AbiDecodedErrorType::Known { + name, + args, + sig, + data: decoded, + } => { + assert_eq!(decoded, data); + assert!(args.is_empty(), "expected zero-arg error match"); + assert!(!name.is_empty(), "expected non-empty error name"); + assert!(sig.ends_with(')'), "expected error-like signature"); + } + AbiDecodedErrorType::Unknown(_) => panic!("expected a known error from OpenChain"), + } + } } From 4f14533fc4190dc5cf4d8a44437cc4920011fcf5 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 24 Oct 2025 15:41:57 +0300 Subject: [PATCH 5/6] make the registry optional --- src/error.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2a6535a..91e4ef2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -125,10 +125,11 @@ impl AbiDecodedErrorType { Ok(selectors.get(&selector_hash).cloned()) } - /// Decode an error using an injected registry for selector lookups. - pub async fn decode_with_registry( + /// Decode an error selector with optional registry injection. + /// If `registry` is `None`, uses the default OpenChain-backed registry. + pub async fn selector_registry_abi_decode( error_data: &[u8], - registry: &dyn ErrorRegistry, + registry: Option<&dyn ErrorRegistry>, ) -> Result { if error_data.is_empty() { return Err(AbiDecodeFailedErrors::NoData); @@ -161,7 +162,13 @@ impl AbiDecodedErrorType { } return Ok(Self::Unknown(error_data.to_vec())); } - // consult the injected registry + + let registry = match registry { + Some(r) => r, + None => &*DEFAULT_REGISTRY as &dyn ErrorRegistry, + }; + + // consult the registry let candidates = registry.lookup(selector_hash_bytes).await?; for error in candidates { if let Ok(result) = error.abi_decode_input(args_data) { @@ -182,13 +189,6 @@ impl AbiDecodedErrorType { Ok(Self::Unknown(error_data.to_vec())) } - /// Backwards-compatible decode that uses the default OpenChain registry. - pub async fn selector_registry_abi_decode( - error_data: &[u8], - ) -> Result { - Self::decode_with_registry(error_data, &*DEFAULT_REGISTRY).await - } - /// Decodes an error by checking if it is a Panic(uint256) and returns `None` if /// it is not and returns `Some(Self::Known)` if it is, with decoding error args /// into the reason specified in specs: @@ -234,7 +234,7 @@ impl AbiDecodedErrorType { AbiDecodeFailedErrors::InvalidJsonRpcResponse(val.get().to_string()) })?; let decoded_data = decode(unwrapped.as_bytes())?; - return Self::selector_registry_abi_decode(&decoded_data).await; + return Self::selector_registry_abi_decode(&decoded_data, None).await; } } Err(AbiDecodeFailedErrors::InvalidJsonRpcResponse( @@ -253,7 +253,7 @@ impl AbiDecodedErrorType { AbiDecodeFailedErrors::InvalidJsonRpcResponse(val.get().to_string()) })?; let decoded_data = decode(unwrapped.as_bytes())?; - return Self::decode_with_registry(&decoded_data, registry).await; + return Self::selector_registry_abi_decode(&decoded_data, Some(registry)).await; } } Err(AbiDecodeFailedErrors::InvalidJsonRpcResponse( @@ -311,7 +311,7 @@ mod tests { #[tokio::test] async fn test_error_decoder() { let data = vec![26, 198, 105, 8]; - let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) + let res = AbiDecodedErrorType::selector_registry_abi_decode(&data, Some(&FakeRegistry)) .await .expect("failed to get error selector"); assert_eq!( @@ -328,7 +328,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_unknown() { let data = vec![26, 198, 105, 9]; - let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) + let res = AbiDecodedErrorType::selector_registry_abi_decode(&data, Some(&FakeRegistry)) .await .expect("failed to get error selector"); assert_eq!(AbiDecodedErrorType::Unknown(data), res); @@ -337,7 +337,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_invalid_selector() { let data = vec![26, 198, 105]; - let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) + let res = AbiDecodedErrorType::selector_registry_abi_decode(&data, Some(&FakeRegistry)) .await .expect_err("expected error"); match res { @@ -349,7 +349,7 @@ mod tests { #[tokio::test] async fn test_error_decoder_no_data() { let data = vec![]; - let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) + let res = AbiDecodedErrorType::selector_registry_abi_decode(&data, Some(&FakeRegistry)) .await .expect_err("expected error"); match res { @@ -363,7 +363,7 @@ mod tests { // ensure cache is empty for this test clear_cache(); let data = vec![26, 198, 105, 8]; - let res = AbiDecodedErrorType::decode_with_registry(&data, &FakeRegistry) + let res = AbiDecodedErrorType::selector_registry_abi_decode(&data, Some(&FakeRegistry)) .await .expect("failed to get error selector"); assert_eq!( @@ -560,7 +560,7 @@ mod tests { let data = vec![0x1a, 0xc6, 0x69, 0x08]; let registry = OpenChainRegistry::default(); - let res = AbiDecodedErrorType::decode_with_registry(&data, ®istry) + let res = AbiDecodedErrorType::selector_registry_abi_decode(&data, Some(®istry)) .await .expect("OpenChain lookup failed"); From 94ff809ce52be447bbbe08ebbbbdbee714e8b6fb Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 24 Oct 2025 17:11:33 +0300 Subject: [PATCH 6/6] update --- Cargo.lock | 18 ++++++++++-------- Cargo.toml | 1 + src/error.rs | 53 ++++++++++++++++++++++++++-------------------------- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55c8cd1..c90798b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1393,9 +1393,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1924,9 +1924,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2379,9 +2379,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" @@ -2578,6 +2578,7 @@ dependencies = [ "serde_json", "thiserror 1.0.69", "tokio", + "url", ] [[package]] @@ -3629,13 +3630,14 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 488fa4e..7093cb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0.111" thiserror = "1.0.56" serde = "1.0.195" async-trait = "0.1" +url = "2.5.7" [target.'cfg(target_family = "wasm")'.dependencies] getrandom = { version = "0.2.11", features = ["js", "js-sys"] } diff --git a/src/error.rs b/src/error.rs index 91e4ef2..4c11c90 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,6 +11,7 @@ use std::{ sync::{Mutex, MutexGuard, PoisonError}, }; use thiserror::Error; +use url::Url; pub const SELECTOR_REGISTRY_URL: &str = "https://api.openchain.xyz/signature-database/v1/lookup"; @@ -28,14 +29,14 @@ pub trait ErrorRegistry: Send + Sync { /// Default OpenChain-backed registry implementation. pub struct OpenChainRegistry { client: Client, - url: String, + url: Url, } impl Default for OpenChainRegistry { fn default() -> Self { Self { client: Client::new(), - url: SELECTOR_REGISTRY_URL.to_string(), + url: Url::parse(SELECTOR_REGISTRY_URL).unwrap(), } } } @@ -47,7 +48,7 @@ impl ErrorRegistry for OpenChainRegistry { let selector_hash = alloy::primitives::hex::encode_prefixed(selector); let response = self .client - .get(&self.url) + .get(self.url.as_ref()) .query(&vec![ ("function", selector_hash.as_str()), ("filter", "true"), @@ -58,17 +59,13 @@ impl ErrorRegistry for OpenChainRegistry { .json::() .await?; - let mut out: Vec = Vec::new(); - if let Some(selectors) = response["result"]["function"][selector_hash].as_array() { - for opt_selector in selectors { - if let Some(name) = opt_selector["name"].as_str() { - if let Ok(err) = name.parse::() { - out.push(err); - } - } - } - } - Ok(out) + Ok(response["result"]["function"][selector_hash] + .as_array() + .into_iter() + .flat_map(|selectors| selectors.iter()) + .filter_map(|opt_selector| opt_selector["name"].as_str()) + .filter_map(|name| name.parse::().ok()) + .collect()) } } @@ -170,23 +167,27 @@ impl AbiDecodedErrorType { // consult the registry let candidates = registry.lookup(selector_hash_bytes).await?; - for error in candidates { - if let Ok(result) = error.abi_decode_input(args_data) { + Ok(candidates + .into_iter() + .find_map(|error| { + let result = error.abi_decode_input(args_data).ok()?; + // cache the fetched selector - { - let mut cached_selectors = SELECTORS.lock()?; - cached_selectors.insert(selector_hash_bytes, error.clone()); - } - return Ok(Self::Known { + let mut cached_selectors = match SELECTORS.lock() { + Ok(lock) => lock, + Err(e) => return Some(Err(e)), + }; + cached_selectors.insert(selector_hash_bytes, error.clone()); + + Some(Ok(Self::Known { sig: error.signature(), name: error.name, args: result.iter().map(|v| format!("{:?}", v)).collect(), data: error_data.to_vec(), - }); - } - } - - Ok(Self::Unknown(error_data.to_vec())) + })) + }) + .transpose()? + .unwrap_or_else(|| Self::Unknown(error_data.to_vec()))) } /// Decodes an error by checking if it is a Panic(uint256) and returns `None` if