diff --git a/src/cli/trace.rs b/src/cli/trace.rs index f479b20..5c6f448 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -255,6 +255,21 @@ pub fn execute_and_print(payload: &serde_json::Value, opts: TraceOpts) -> Result palette, ); + let warnings: Vec = [ + selector_resolver.take_warning(), + contract_resolver.take_warning(), + ] + .into_iter() + .flatten() + .collect(); + + if !warnings.is_empty() { + eprintln!(); + for w in warnings { + eprintln!("{}", palette.yellow(&format!("warning: {w}"))); + } + } + Ok(()) } diff --git a/src/utils/contract_resolver.rs b/src/utils/contract_resolver.rs index 5c0a1b2..31884a7 100644 --- a/src/utils/contract_resolver.rs +++ b/src/utils/contract_resolver.rs @@ -10,6 +10,7 @@ pub struct ContractResolver { base_url: String, chain_id: Option, enabled: bool, + warning: Option, } impl ContractResolver { @@ -25,9 +26,14 @@ impl ContractResolver { base_url, chain_id, enabled, + warning: None, } } + pub fn take_warning(&mut self) -> Option { + self.warning.take() + } + /// Resolve a contract address to its name via Sourcify's v2 contract lookup. pub fn resolve(&mut self, address: &str) -> Option { if !self.enabled { @@ -52,19 +58,25 @@ impl ContractResolver { self.base_url ); - let Some(resp) = self - .client - .get(&url) - .send() - .ok() - .and_then(|r| r.error_for_status().ok()) - else { - self.disk_cache.insert_miss(cache_key); - return None; + let resp = match self.client.get(&url).send() { + Ok(r) if r.status().is_success() => r, + Ok(r) if r.status() == reqwest::StatusCode::NOT_FOUND => { + self.disk_cache.insert_miss(cache_key); + return None; + } + Ok(_) | Err(_) => { + if self.warning.is_none() { + self.warning = Some(format!( + "sourcify contract lookup failed for {address}, results may be incomplete" + )); + } + self.disk_cache.insert_transient_miss(cache_key); + return None; + } }; let Some(body) = resp.json::().ok() else { - self.disk_cache.insert_miss(cache_key); + self.disk_cache.insert_transient_miss(cache_key); return None; }; diff --git a/src/utils/disk_cache.rs b/src/utils/disk_cache.rs index d945d4c..b96386d 100644 --- a/src/utils/disk_cache.rs +++ b/src/utils/disk_cache.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fs, path::{Path, PathBuf}, }; @@ -35,6 +35,7 @@ pub enum CacheError { /// used for serialization, keeping backward compatibility with existing cache files. pub struct DiskCache { entries: HashMap, + transient_misses: HashSet, kind: String, dirty: bool, disabled: bool, @@ -57,6 +58,7 @@ impl DiskCache { let disabled = std::env::var("TORGE_DISABLE_CACHE").is_ok(); let empty = Self { entries: HashMap::new(), + transient_misses: HashSet::new(), kind: kind.to_owned(), dirty: false, disabled, @@ -74,6 +76,7 @@ impl DiskCache { Self { entries, + transient_misses: HashSet::new(), kind: kind.to_owned(), dirty: false, disabled: false, @@ -124,6 +127,9 @@ impl DiskCache { /// Look up a key, distinguishing between a resolved hit, a cached miss, and not-in-cache. pub fn lookup(&self, key: &str) -> CacheLookup<'_> { + if self.transient_misses.contains(key) { + return CacheLookup::Miss; + } match self.entries.get(key) { Some(v) if v == CACHE_MISS_MARKER => CacheLookup::Miss, Some(v) => CacheLookup::Hit(v), @@ -140,6 +146,11 @@ impl DiskCache { self.insert(key, CACHE_MISS_MARKER.to_owned()); } + /// Record a miss that should not be persisted to disk (e.g. transient network errors). + pub fn insert_transient_miss(&mut self, key: String) { + self.transient_misses.insert(key); + } + /// Remove all unknown (unresolved) entries from the persisted cache. /// Returns `(kept, removed)` counts. pub fn remove_unknown(kind: &str) -> Result<(usize, usize), CacheError> { @@ -179,6 +190,7 @@ mod tests { fn empty_cache() -> DiskCache { DiskCache { entries: HashMap::new(), + transient_misses: HashSet::new(), kind: "test".to_owned(), dirty: false, disabled: true, @@ -206,4 +218,12 @@ mod tests { cache.insert_miss("0xdeadbeef".to_owned()); assert!(matches!(cache.lookup("0xdeadbeef"), CacheLookup::Miss)); } + + #[test] + fn test_transient_miss() { + let mut cache = empty_cache(); + cache.insert_transient_miss("0xdeadbeef".to_owned()); + assert!(matches!(cache.lookup("0xdeadbeef"), CacheLookup::Miss)); + assert!(!cache.entries.contains_key("0xdeadbeef")); + } } diff --git a/src/utils/selector_resolver.rs b/src/utils/selector_resolver.rs index 7536a0b..de58b73 100644 --- a/src/utils/selector_resolver.rs +++ b/src/utils/selector_resolver.rs @@ -13,6 +13,7 @@ pub struct SelectorResolver { disk_cache: DiskCache, base_url: String, enabled: bool, + warning: Option, } impl SelectorResolver { @@ -27,6 +28,7 @@ impl SelectorResolver { disk_cache: DiskCache::load("selectors"), base_url, enabled, + warning: None, } } @@ -35,6 +37,10 @@ impl SelectorResolver { self.enabled } + pub fn take_warning(&mut self) -> Option { + self.warning.take() + } + /// Resolve a 4-byte function selector to a text signature. pub fn resolve(&mut self, selector: &str, calldata: Option<&str>) -> Option { self.lookup(selector, "function", 10, calldata) @@ -68,19 +74,21 @@ impl SelectorResolver { self.base_url ); - let Some(resp) = self - .client - .get(&url) - .send() - .ok() - .and_then(|r| r.error_for_status().ok()) - else { - self.disk_cache.insert_miss(key.to_owned()); - return None; + let resp = match self.client.get(&url).send() { + Ok(r) if r.status().is_success() => r, + Ok(_) | Err(_) => { + if self.warning.is_none() { + self.warning = Some(format!( + "sourcify selector lookup failed for {key}, results may be incomplete" + )); + } + self.disk_cache.insert_transient_miss(key.to_owned()); + return None; + } }; let Some(body) = resp.json::().ok() else { - self.disk_cache.insert_miss(key.to_owned()); + self.disk_cache.insert_transient_miss(key.to_owned()); return None; };