From 042a7d9221861e5241159dc2a9589bbd3d2c06f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Wed, 4 Mar 2026 22:12:10 -0800 Subject: [PATCH 1/9] refactor: use `hex_utils` helpers for `0x` prefix handling --- src/utils/contract_resolver.rs | 3 ++- src/utils/selector_resolver.rs | 3 ++- src/utils/trace_renderer.rs | 12 ++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index 31884a7..b13d88b 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -1,4 +1,5 @@ use super::disk_cache::{CacheLookup, DiskCache}; +use super::hex_utils; use reqwest::blocking::Client; const DEFAULT_SOURCIFY_SERVER_URL: &str = "https://sourcify.dev/server/"; @@ -40,7 +41,7 @@ impl ContractResolver { return None; } let chain_id = self.chain_id.as_deref()?; - if !address.starts_with("0x") || address.len() != 42 { + if hex_utils::require_0x(address).is_none() || address.len() != 42 { return None; } diff --git a/src/utils/selector_resolver.rs b/src/utils/selector_resolver.rs index 308783d..a3b2383 100644 --- a/src/utils/selector_resolver.rs +++ b/src/utils/selector_resolver.rs @@ -1,6 +1,7 @@ use super::{ abi_decoder, disk_cache::{CacheLookup, DiskCache}, + hex_utils, }; use reqwest::blocking::Client; @@ -59,7 +60,7 @@ impl SelectorResolver { expected_len: usize, calldata: Option<&str>, ) -> Option { - if !self.enabled || !key.starts_with("0x") || key.len() != expected_len { + if !self.enabled || hex_utils::require_0x(key).is_none() || key.len() != expected_len { return None; } diff --git a/src/utils/trace_renderer.rs b/src/utils/trace_renderer.rs index 1126e49..90d4ba9 100644 --- a/src/utils/trace_renderer.rs +++ b/src/utils/trace_renderer.rs @@ -196,7 +196,7 @@ fn print_call( } } else if decode_attempted { if let Some(input) = &node.input { - let s = input.strip_prefix("0x").unwrap_or(input); + let s = hex_utils::strip_0x(input); if s.len() > 8 { println!( "{meta_prefix}{}", @@ -265,7 +265,7 @@ fn format_call_desc( } pub(crate) fn extract_selector(input: &str) -> Option { - let s = input.strip_prefix("0x").unwrap_or(input); + let s = hex_utils::strip_0x(input); s.get(..8).map(|sel| format!("0x{sel}")) } @@ -325,4 +325,12 @@ mod tests { ); assert_eq!(extract_selector("0x123"), None); } + + #[test] + fn test_extract_selector_uppercase_prefix() { + assert_eq!( + extract_selector("0Xa9059cbb000000"), + Some("0xa9059cbb".to_string()) + ); + } } From 1e5e954ef5be8a5217bb4000f2dd8b738570efea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Wed, 4 Mar 2026 22:12:10 -0800 Subject: [PATCH 2/9] fix: use `parse_rpc_response` in `fetch_chain_id` --- src/cli/trace.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/cli/trace.rs b/src/cli/trace.rs index ef56b6c..1d92393 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -216,20 +216,8 @@ pub fn prestate_tracer_config() -> serde_json::Value { /// Fetch the chain ID from the RPC node as a decimal string (e.g. `"1"`). fn fetch_chain_id(client: &Client, rpc_url: &str) -> Result { let payload = rpc_payload(1, "eth_chainId", serde_json::json!([])); - let resp = client.post(rpc_url).json(&payload).send()?; - let body: RpcResponse = resp - .json() - .map_err(|e| TraceError::Decode(format!("eth_chainId: invalid JSON: {e}")))?; - - if let Some(err) = body.error { - return Err(TraceError::Rpc(err.message, err.code)); - } - - let hex_str = body - .result - .ok_or_else(|| TraceError::Decode("eth_chainId: missing result".into()))?; - + let hex_str: String = parse_rpc_response(resp)?; parse_chain_id_hex(&hex_str) } From b6c5d7a29e4a3b2eafc9169481a07c8eb4d1fc0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Wed, 4 Mar 2026 22:13:10 -0800 Subject: [PATCH 3/9] fix: validate hex digits in address and tx hash checks --- src/cli/trace.rs | 16 ++++++++------- src/cli/tx.rs | 8 +------- src/utils/contract_resolver.rs | 2 +- src/utils/hex_utils.rs | 36 ++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/cli/trace.rs b/src/cli/trace.rs index 1d92393..8f72433 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -150,17 +150,19 @@ fn resolve_rpc_url(url_or_alias: Option) -> Result { /// Validate that a string is a 0x-prefixed Ethereum address (40 hex chars). pub fn validate_address(addr: &str, field: &str) -> Result<(), TraceError> { - let hex = hex_utils::require_0x(addr) - .ok_or_else(|| TraceError::InvalidInput(format!("{field}: missing 0x prefix")))?; - if hex.len() != 40 { + if !hex_utils::is_valid_address(addr) { return Err(TraceError::InvalidInput(format!( - "{field}: expected 40 hex chars, got {}", - hex.len() + "{field}: expected 0x-prefixed 40-char hex address" ))); } - if !hex.chars().all(|c| c.is_ascii_hexdigit()) { + Ok(()) +} + +/// Validate that a string is a 0x-prefixed transaction hash (64 hex chars). +pub fn validate_tx_hash(hash: &str, field: &str) -> Result<(), TraceError> { + if !hex_utils::is_valid_tx_hash(hash) { return Err(TraceError::InvalidInput(format!( - "{field}: invalid hex characters" + "{field}: expected 0x-prefixed 64-char hex hash" ))); } Ok(()) diff --git a/src/cli/tx.rs b/src/cli/tx.rs index e7b04b8..73c310d 100644 --- a/src/cli/tx.rs +++ b/src/cli/tx.rs @@ -12,13 +12,7 @@ pub struct TxArgs { } pub fn run(args: TxArgs) -> Result<(), TraceError> { - trace::validate_hex(&args.tx_hash, "tx_hash")?; - if args.tx_hash.len() != 66 { - return Err(TraceError::InvalidInput(format!( - "tx_hash: expected 64 hex chars, got {}", - args.tx_hash.len() - 2 - ))); - } + trace::validate_tx_hash(&args.tx_hash, "tx_hash")?; let TxArgs { tx_hash, opts } = args; diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index b13d88b..2d7c532 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -41,7 +41,7 @@ impl ContractResolver { return None; } let chain_id = self.chain_id.as_deref()?; - if hex_utils::require_0x(address).is_none() || address.len() != 42 { + if !hex_utils::is_valid_address(address) { return None; } diff --git a/src/utils/hex_utils.rs b/src/utils/hex_utils.rs index 6d222ce..1119a56 100644 --- a/src/utils/hex_utils.rs +++ b/src/utils/hex_utils.rs @@ -10,6 +10,14 @@ pub fn require_0x(s: &str) -> Option<&str> { s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) } +pub fn is_valid_address(s: &str) -> bool { + require_0x(s).is_some_and(|h| h.len() == 40 && h.chars().all(|c| c.is_ascii_hexdigit())) +} + +pub fn is_valid_tx_hash(s: &str) -> bool { + require_0x(s).is_some_and(|h| h.len() == 64 && h.chars().all(|c| c.is_ascii_hexdigit())) +} + pub fn parse_hex_u256(s: &str) -> Option { let s = strip_0x(s); U256::from_str_radix(s, 16).ok() @@ -36,6 +44,34 @@ mod tests { assert_eq!(require_0x("0x"), Some("")); } + #[test] + fn test_is_valid_address() { + assert!(is_valid_address( + "0xdAC17F958D2ee523a2206206994597C13D831ec7" + )); + assert!(is_valid_address( + "0XdAC17F958D2ee523a2206206994597C13D831ec7" + )); + assert!(!is_valid_address( + "dAC17F958D2ee523a2206206994597C13D831ec7" + )); + assert!(!is_valid_address("0x1234")); + assert!(!is_valid_address( + "0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" + )); + } + + #[test] + fn test_is_valid_tx_hash() { + assert!(is_valid_tx_hash( + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + )); + assert!(!is_valid_tx_hash("0x1234")); + assert!(!is_valid_tx_hash( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + )); + } + #[test] fn test_parse_hex_u256() { assert_eq!(parse_hex_u256("0x10"), Some(U256::from(16))); From 85ae03f362cc75ac4b21a09f91df9eefb73091da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Wed, 4 Mar 2026 22:14:34 -0800 Subject: [PATCH 4/9] refactor: typed structs for sourcify API responses --- src/utils/contract_resolver.rs | 33 ++++++++++++------ src/utils/selector_resolver.rs | 64 +++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index 2d7c532..c2a927b 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -1,9 +1,20 @@ use super::disk_cache::{CacheLookup, DiskCache}; use super::hex_utils; use reqwest::blocking::Client; +use serde::Deserialize; const DEFAULT_SOURCIFY_SERVER_URL: &str = "https://sourcify.dev/server/"; +#[derive(Deserialize)] +struct ContractInfo { + compilation: Option, +} + +#[derive(Deserialize)] +struct Compilation { + name: Option, +} + /// Best-effort contract name resolver using Sourcify's v2 API with disk caching. pub struct ContractResolver { client: Client, @@ -76,21 +87,21 @@ impl ContractResolver { } }; - let Some(body) = resp.json::().ok() else { + let Ok(info) = resp.json::() else { self.disk_cache.insert_transient_miss(cache_key); return None; }; - match body["compilation"]["name"].as_str() { - Some(name) if !name.is_empty() => { - let name = name.to_owned(); - self.disk_cache.insert(cache_key, name.clone()); - Some(name) - } - _ => { - self.disk_cache.insert_miss(cache_key); - None - } + if let Some(name) = info + .compilation + .and_then(|c| c.name) + .filter(|n| !n.is_empty()) + { + self.disk_cache.insert(cache_key, name.clone()); + Some(name) + } else { + self.disk_cache.insert_miss(cache_key); + None } } } diff --git a/src/utils/selector_resolver.rs b/src/utils/selector_resolver.rs index a3b2383..8428233 100644 --- a/src/utils/selector_resolver.rs +++ b/src/utils/selector_resolver.rs @@ -4,10 +4,21 @@ use super::{ hex_utils, }; use reqwest::blocking::Client; +use serde::Deserialize; /// Default Sourcify API base URL for 4byte mirror. const DEFAULT_SOURCIFY_4BYTE_URL: &str = "https://api.4byte.sourcify.dev/"; +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SelectorEntry { + name: Option, + #[serde(default)] + has_verified_contract: bool, + #[serde(default)] + filtered: bool, +} + /// Best-effort function selector resolver using Sourcify's 4byte mirror with disk caching. pub struct SelectorResolver { client: Client, @@ -88,7 +99,7 @@ impl SelectorResolver { } }; - let Some(body) = resp.json::().ok() else { + let Ok(mut body) = resp.json::() else { self.disk_cache.insert_transient_miss(key.to_owned()); return None; }; @@ -98,15 +109,15 @@ impl SelectorResolver { return None; } - let entries = match body["result"][kind][key].as_array() { - Some(e) if !e.is_empty() => e, - _ => { - self.disk_cache.insert_miss(key.to_owned()); - return None; - } - }; + let entries: Vec = + serde_json::from_value(body["result"][kind][key].take()).unwrap_or_default(); - let Some(sig) = select_best_entry(entries, calldata) else { + if entries.is_empty() { + self.disk_cache.insert_miss(key.to_owned()); + return None; + } + + let Some(sig) = select_best_entry(&entries, calldata) else { self.disk_cache.insert_miss(key.to_owned()); return None; }; @@ -122,14 +133,14 @@ impl SelectorResolver { /// decodable > first entry. /// /// When `calldata` is `None` (event lookups), returns the first entry. -fn select_best_entry(entries: &[serde_json::Value], calldata: Option<&str>) -> Option { - let name = |e: &serde_json::Value| e["name"].as_str().map(str::to_owned); - let verified = |e: &serde_json::Value| e["hasVerifiedContract"].as_bool().unwrap_or(false); - let filtered = |e: &serde_json::Value| e["filtered"].as_bool().unwrap_or(false); +fn select_best_entry(entries: &[SelectorEntry], calldata: Option<&str>) -> Option { + let name = |e: &SelectorEntry| e.name.clone(); + let verified = |e: &SelectorEntry| e.has_verified_contract; + let filtered = |e: &SelectorEntry| e.filtered; if let Some(calldata) = calldata { let decodable = - |e: &serde_json::Value| name(e).is_some_and(|n| abi_decoder::can_decode(&n, calldata)); + |e: &SelectorEntry| name(e).is_some_and(|n| abi_decoder::can_decode(&n, calldata)); entries .iter() @@ -158,15 +169,18 @@ fn select_best_entry(entries: &[serde_json::Value], calldata: Option<&str>) -> O #[cfg(test)] mod tests { use super::*; - use serde_json::json; const DECODABLE_SIG: &str = "transfer(address,uint256)"; const DECODABLE_SIG_ALT: &str = "approve(address,uint256)"; const NON_DECODABLE_SIG: &str = "foo(address,address,address)"; const CALLDATA: &str = "0xa9059cbb000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef00000000000000000000000000000000000000000000000000000000000003e8"; - fn entry(name: &str, verified: bool, filtered: bool) -> serde_json::Value { - json!({"name": name, "hasVerifiedContract": verified, "filtered": filtered}) + fn entry(name: &str, verified: bool, filtered: bool) -> SelectorEntry { + SelectorEntry { + name: Some(name.to_owned()), + has_verified_contract: verified, + filtered, + } } #[test] @@ -209,7 +223,11 @@ mod tests { #[test] fn test_select_best_entry_missing_name() { - let entries = vec![json!({"hasVerifiedContract": true, "filtered": false})]; + let entries = vec![SelectorEntry { + name: None, + has_verified_contract: true, + filtered: false, + }]; assert_eq!(select_best_entry(&entries, Some(CALLDATA)), None); } @@ -271,7 +289,7 @@ mod tests { #[test] fn test_select_best_entry_empty() { - let entries: Vec = vec![]; + let entries: Vec = vec![]; assert_eq!(select_best_entry(&entries, Some(CALLDATA)), None); } @@ -286,7 +304,11 @@ mod tests { #[test] fn test_select_best_entry_event_no_name() { - let entries = vec![json!({"hasVerifiedContract": true, "filtered": false})]; + let entries = vec![SelectorEntry { + name: None, + has_verified_contract: true, + filtered: false, + }]; assert_eq!(select_best_entry(&entries, None), None); } @@ -304,7 +326,7 @@ mod tests { #[test] fn test_select_best_entry_event_empty() { - let entries: Vec = vec![]; + let entries: Vec = vec![]; assert_eq!(select_best_entry(&entries, None), None); } } From 32ea8d87c32a9c6bf9b336ffee6d999492192554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Thu, 5 Mar 2026 08:19:16 -0800 Subject: [PATCH 5/9] refactor: inject sourcify base URLs via constructor --- src/cli/trace.rs | 5 +++-- src/utils/abi_decoder.rs | 2 ++ src/utils/contract_resolver.rs | 12 +++++++++--- src/utils/selector_resolver.rs | 7 ++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/cli/trace.rs b/src/cli/trace.rs index 8f72433..c260b32 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -288,8 +288,9 @@ pub fn execute_and_print( Palette::auto() }; - let mut selector_resolver = SelectorResolver::new(client.clone(), opts.resolve_selectors); - let mut contract_resolver = ContractResolver::new(client, chain_id, opts.resolve_contracts); + let mut selector_resolver = SelectorResolver::new(client.clone(), opts.resolve_selectors, None); + let mut contract_resolver = + ContractResolver::new(client, chain_id, opts.resolve_contracts, None); trace_renderer::print_trace( &call_trace, &mut selector_resolver, diff --git a/src/utils/abi_decoder.rs b/src/utils/abi_decoder.rs index 386cb4c..4528619 100644 --- a/src/utils/abi_decoder.rs +++ b/src/utils/abi_decoder.rs @@ -266,6 +266,7 @@ mod tests { let mut resolver = crate::utils::selector_resolver::SelectorResolver::new( reqwest::blocking::Client::new(), false, + None, ); let output = "0x08c379a0\ 0000000000000000000000000000000000000000000000000000000000000020\ @@ -279,6 +280,7 @@ mod tests { let mut resolver = crate::utils::selector_resolver::SelectorResolver::new( reqwest::blocking::Client::new(), false, + None, ); let output = "0xdeadbeef0000000000000000000000000000000000000000000000000000000000000001"; assert!(decode_custom_revert(output, &mut resolver).is_none()); diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index c2a927b..1a79e1e 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -26,9 +26,15 @@ pub struct ContractResolver { } impl ContractResolver { - pub fn new(client: Client, chain_id: Option, enabled: bool) -> Self { - let mut base_url = std::env::var("SOURCIFY_SERVER_URL") - .unwrap_or_else(|_| DEFAULT_SOURCIFY_SERVER_URL.to_owned()); + pub fn new( + client: Client, + chain_id: Option, + enabled: bool, + base_url: Option, + ) -> Self { + let mut base_url = base_url + .or_else(|| std::env::var("SOURCIFY_SERVER_URL").ok()) + .unwrap_or_else(|| DEFAULT_SOURCIFY_SERVER_URL.to_owned()); if !base_url.ends_with('/') { base_url.push('/'); } diff --git a/src/utils/selector_resolver.rs b/src/utils/selector_resolver.rs index 8428233..0634d31 100644 --- a/src/utils/selector_resolver.rs +++ b/src/utils/selector_resolver.rs @@ -29,9 +29,10 @@ pub struct SelectorResolver { } impl SelectorResolver { - pub fn new(client: Client, enabled: bool) -> Self { - let mut base_url = std::env::var("SOURCIFY_4BYTE_URL") - .unwrap_or_else(|_| DEFAULT_SOURCIFY_4BYTE_URL.to_owned()); + pub fn new(client: Client, enabled: bool, base_url: Option) -> Self { + let mut base_url = base_url + .or_else(|| std::env::var("SOURCIFY_4BYTE_URL").ok()) + .unwrap_or_else(|| DEFAULT_SOURCIFY_4BYTE_URL.to_owned()); if !base_url.ends_with('/') { base_url.push('/'); } From 0ca6e615177730414ddb83bd8a1c4854b7f72c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Thu, 5 Mar 2026 08:19:26 -0800 Subject: [PATCH 6/9] refactor: centralize cache kind constants --- src/cli/clean.rs | 9 ++++----- src/utils/contract_resolver.rs | 4 ++-- src/utils/disk_cache.rs | 4 ++++ src/utils/selector_resolver.rs | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/cli/clean.rs b/src/cli/clean.rs index 9bdec67..ef9d6c9 100644 --- a/src/cli/clean.rs +++ b/src/cli/clean.rs @@ -1,11 +1,9 @@ -use crate::utils::disk_cache::{CacheError, DiskCache}; +use crate::utils::disk_cache::{CacheError, DiskCache, ALL_CACHE_KINDS}; use clap::Parser; use std::fs; use std::path::Path; use thiserror::Error; -const CACHE_KINDS: &[&str] = &["selectors", "contracts"]; - #[derive(Parser, Debug)] pub struct CleanArgs { /// Only remove unknown (unresolved) entries from the cache. @@ -26,7 +24,7 @@ pub enum CleanError { pub fn run(args: CleanArgs) -> Result<(), CleanError> { let mut found_any = false; - for kind in CACHE_KINDS { + for kind in ALL_CACHE_KINDS { let Some(path) = DiskCache::cache_path(kind) else { continue; }; @@ -49,7 +47,8 @@ pub fn run(args: CleanArgs) -> Result<(), CleanError> { } if !found_any { - match DiskCache::cache_path(CACHE_KINDS[0]).and_then(|p| p.parent().map(Path::to_owned)) { + match DiskCache::cache_path(ALL_CACHE_KINDS[0]).and_then(|p| p.parent().map(Path::to_owned)) + { Some(dir) => println!("no cache files found in {}", dir.display()), None => println!("no cache files found (could not determine cache directory)"), } diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index 1a79e1e..fe4f543 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -1,4 +1,4 @@ -use super::disk_cache::{CacheLookup, DiskCache}; +use super::disk_cache::{CacheLookup, DiskCache, CONTRACT_CACHE}; use super::hex_utils; use reqwest::blocking::Client; use serde::Deserialize; @@ -40,7 +40,7 @@ impl ContractResolver { } Self { client, - disk_cache: DiskCache::load("contracts"), + disk_cache: DiskCache::load(CONTRACT_CACHE), base_url, chain_id, enabled, diff --git a/src/utils/disk_cache.rs b/src/utils/disk_cache.rs index b96386d..83773b2 100644 --- a/src/utils/disk_cache.rs +++ b/src/utils/disk_cache.rs @@ -7,6 +7,10 @@ use thiserror::Error; const CACHE_MISS_MARKER: &str = ""; +pub const SELECTOR_CACHE: &str = "selectors"; +pub const CONTRACT_CACHE: &str = "contracts"; +pub const ALL_CACHE_KINDS: &[&str] = &[SELECTOR_CACHE, CONTRACT_CACHE]; + /// Result of looking up a key in the cache. #[derive(Debug)] pub enum CacheLookup<'a> { diff --git a/src/utils/selector_resolver.rs b/src/utils/selector_resolver.rs index 0634d31..8dc834b 100644 --- a/src/utils/selector_resolver.rs +++ b/src/utils/selector_resolver.rs @@ -1,6 +1,6 @@ use super::{ abi_decoder, - disk_cache::{CacheLookup, DiskCache}, + disk_cache::{CacheLookup, DiskCache, SELECTOR_CACHE}, hex_utils, }; use reqwest::blocking::Client; @@ -38,7 +38,7 @@ impl SelectorResolver { } Self { client, - disk_cache: DiskCache::load("selectors"), + disk_cache: DiskCache::load(SELECTOR_CACHE), base_url, enabled, warning: None, From 1dc16a1d728a1783069316c51e74057a3ac1fb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Thu, 5 Mar 2026 08:20:00 -0800 Subject: [PATCH 7/9] refactor: move `CallTrace` to `trace_renderer` --- src/cli/trace.rs | 21 +-------------------- src/utils/trace_renderer.rs | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/cli/trace.rs b/src/cli/trace.rs index c260b32..38588d2 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -1,11 +1,10 @@ use crate::utils::{ color::Palette, contract_resolver::ContractResolver, - event_formatter::Log, hex_utils, rpc_url, selector_resolver::SelectorResolver, storage_diff::{self, PrestateDiff}, - trace_renderer, + trace_renderer::{self, CallTrace}, }; use clap::Parser; use reqwest::blocking::Client; @@ -111,24 +110,6 @@ struct RpcError { message: String, } -/// Result shape for geth-style `callTracer`. -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CallTrace { - #[serde(rename = "type")] - pub call_type: Option, - pub to: Option, - pub value: Option, - pub gas_used: Option, - pub input: Option, - pub output: Option, - pub error: Option, - #[serde(default)] - pub logs: Vec, - #[serde(default)] - pub calls: Vec, -} - /// Create a pre-configured HTTP client for RPC calls. fn create_client(no_proxy: bool) -> Result { let mut builder = Client::builder().timeout(Duration::from_secs(60)); diff --git a/src/utils/trace_renderer.rs b/src/utils/trace_renderer.rs index 90d4ba9..3d1f916 100644 --- a/src/utils/trace_renderer.rs +++ b/src/utils/trace_renderer.rs @@ -1,9 +1,31 @@ -use crate::cli::trace::CallTrace; use crate::utils::{ - abi_decoder, color::Palette, contract_resolver::ContractResolver, event_formatter::print_log, - hex_utils, precompiles, selector_resolver::SelectorResolver, + abi_decoder, + color::Palette, + contract_resolver::ContractResolver, + event_formatter::{self, print_log}, + hex_utils, precompiles, + selector_resolver::SelectorResolver, }; use alloy_dyn_abi::{DynSolType, DynSolValue}; +use serde::Deserialize; + +/// Result shape for geth-style `callTracer`. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallTrace { + #[serde(rename = "type")] + pub call_type: Option, + pub to: Option, + pub value: Option, + pub gas_used: Option, + pub input: Option, + pub output: Option, + pub error: Option, + #[serde(default)] + pub logs: Vec, + #[serde(default)] + pub calls: Vec, +} pub(crate) fn print_trace( root: &CallTrace, From 66db3e69e38caf663c6834c6cd26f5a21504b29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Thu, 5 Mar 2026 08:20:00 -0800 Subject: [PATCH 8/9] fix: warn on sourcify JSON parse failures --- src/utils/contract_resolver.rs | 5 +++++ src/utils/selector_resolver.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index fe4f543..8d7b1fb 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -94,6 +94,11 @@ impl ContractResolver { }; let Ok(info) = resp.json::() else { + if self.warning.is_none() { + self.warning = Some(format!( + "sourcify contract response parse failed for {address}, results may be incomplete" + )); + } self.disk_cache.insert_transient_miss(cache_key); return None; }; diff --git a/src/utils/selector_resolver.rs b/src/utils/selector_resolver.rs index 8dc834b..ded3308 100644 --- a/src/utils/selector_resolver.rs +++ b/src/utils/selector_resolver.rs @@ -101,6 +101,11 @@ impl SelectorResolver { }; let Ok(mut body) = resp.json::() else { + if self.warning.is_none() { + self.warning = Some(format!( + "sourcify selector response parse failed for {key}, results may be incomplete" + )); + } self.disk_cache.insert_transient_miss(key.to_owned()); return None; }; From 169ed8b8868bd04710605831ec1ff2957eaa9115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tino=20Mart=C3=ADnez=20Molina?= Date: Thu, 5 Mar 2026 21:05:36 -0800 Subject: [PATCH 9/9] test: tx hash validation --- src/cli/trace.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/cli/trace.rs b/src/cli/trace.rs index 38588d2..37933bc 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -353,6 +353,21 @@ mod tests { assert!(validate_hex("0xab", "data").is_ok()); } + #[test] + fn test_validate_tx_hash() { + assert!(validate_tx_hash( + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "hash" + ) + .is_ok()); + assert!(validate_tx_hash("0x1234", "hash").is_err()); + assert!(validate_tx_hash( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "hash" + ) + .is_err()); + } + #[test] fn test_validate_address_uppercase_prefix() { assert!(validate_address("0XdAC17F958D2ee523a2206206994597C13D831ec7", "to").is_ok());