Skip to main content

foundry_evm_traces/identifier/
external.rs

1use super::{IdentifiedAddress, TraceIdentifier};
2use crate::debug::ContractSources;
3use alloy_json_abi::JsonAbi;
4use alloy_primitives::{
5    Address,
6    map::{AddressMap, AddressSet, Entry, HashMap, HashSet},
7};
8use eyre::WrapErr;
9use foundry_block_explorers::{contract::Metadata, errors::EtherscanError};
10use foundry_common::compile::etherscan_project;
11use foundry_config::{Chain, Config, EtherscanConfigs, NamedChain, ResolvedEtherscanConfig};
12use foundry_evm_core::constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS};
13use futures::{
14    future::{Either, join_all, select},
15    stream::{FuturesUnordered, Stream, StreamExt},
16    task::{Context, Poll},
17};
18use revm_inspectors::tracing::types::CallTraceNode;
19use serde::Deserialize;
20use std::{
21    borrow::Cow,
22    pin::{Pin, pin},
23    sync::{
24        Arc,
25        atomic::{AtomicBool, Ordering},
26    },
27    time::{Duration, Instant},
28};
29
30/// A trace identifier that tries to identify addresses using Etherscan.
31pub struct ExternalIdentifier {
32    fetchers: Vec<Arc<dyn ExternalFetcherT>>,
33    /// Cached contracts.
34    contracts: HashMap<Address, (FetcherKind, Option<Metadata>)>,
35    /// Remaining time external identification may block trace rendering.
36    remaining_budget: Duration,
37    clock: Arc<dyn Clock>,
38}
39
40/// The [`Config`] settings an [`ExternalIdentifier`] is built from, detached from the config
41/// itself.
42///
43/// Lets a consumer that keeps only a snapshot of the config — the cheatcode config — build an
44/// identifier for a chain it doesn't learn until runtime, when a test selects a fork.
45#[derive(Clone, Debug, Default)]
46pub struct ExternalIdentifierConfig {
47    /// Whether network access is disabled altogether.
48    offline: bool,
49    /// How long identification may block, in seconds. Zero disables it.
50    timeout: u64,
51    /// Whether to skip system proxy lookups when building the explorer client.
52    no_proxy: bool,
53    /// The `[etherscan]` table, and the settings that pick an entry out of it.
54    etherscan: EtherscanConfigs,
55    etherscan_alias: Option<String>,
56    etherscan_api_key: Option<String>,
57    /// The configured chain, used when the caller doesn't name one.
58    chain: Option<Chain>,
59}
60
61impl ExternalIdentifierConfig {
62    /// Takes the settings an [`ExternalIdentifier`] needs out of `config`.
63    pub fn new(config: &Config) -> Self {
64        Self {
65            offline: config.offline,
66            timeout: config.tracing.external_identification_timeout,
67            no_proxy: config.eth_rpc_no_proxy,
68            etherscan: config.etherscan.clone(),
69            etherscan_alias: config.etherscan_alias().map(str::to_string),
70            etherscan_api_key: config.etherscan_api_key.clone(),
71            chain: config.chain,
72        }
73    }
74
75    /// Builds an identifier that looks contracts up on `chain`.
76    ///
77    /// Returns `None` when there is nothing to look them up with: identification is off, or
78    /// neither Sourcify nor a block explorer is usable.
79    pub fn identifier(&self, chain: Option<Chain>) -> Option<ExternalIdentifier> {
80        self.identifier_with(
81            chain,
82            self.etherscan_alias.as_deref(),
83            self.etherscan_api_key.as_deref(),
84            true,
85        )
86    }
87
88    /// Builds an identifier for compiling storage layouts on exactly `chain`.
89    ///
90    /// Storage layouts are cached by chain, so an alias for another chain must not override the
91    /// chain that executed the recorded storage access. Sourcify is omitted because its compact
92    /// response does not contain the verified sources required for compilation.
93    pub fn storage_identifier(&self, chain: Chain) -> Option<ExternalIdentifier> {
94        self.identifier_with(Some(chain), None, self.literal_api_key(), false)
95    }
96
97    fn identifier_with(
98        &self,
99        chain: Option<Chain>,
100        etherscan_alias: Option<&str>,
101        etherscan_api_key: Option<&str>,
102        sourcify: bool,
103    ) -> Option<ExternalIdentifier> {
104        if self.offline || self.timeout == 0 {
105            return None;
106        }
107
108        let (chain, etherscan) = self.resolve_explorer(chain, etherscan_alias, etherscan_api_key);
109
110        let mut fetchers = Vec::<Arc<dyn ExternalFetcherT>>::new();
111        // Sourcify never indexes local development chains.
112        if sourcify
113            && let Some(chain) = chain
114            && !matches!(chain.named(), Some(NamedChain::AnvilHardhat | NamedChain::Dev))
115        {
116            debug!(target: "evm::traces::external", ?chain, "using sourcify identifier");
117            fetchers.push(Arc::new(SourcifyFetcher::new(chain)));
118        }
119        if let Some(config) = etherscan {
120            debug!(target: "evm::traces::external", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
121            match config.into_client_with_no_proxy(self.no_proxy) {
122                Ok(client) => {
123                    fetchers.push(Arc::new(EtherscanFetcher::new(client)));
124                }
125                Err(err) => {
126                    warn!(target: "evm::traces::external", ?err, "failed to create etherscan client");
127                }
128            }
129        }
130        if fetchers.is_empty() {
131            debug!(target: "evm::traces::external", "no fetchers enabled");
132            return None;
133        }
134
135        Some(ExternalIdentifier {
136            fetchers,
137            contracts: Default::default(),
138            remaining_budget: Duration::from_secs(self.timeout),
139            clock: Arc::new(TokioClock),
140        })
141    }
142
143    /// Picks the explorer config and the chain to look contracts up on.
144    fn resolve_explorer(
145        &self,
146        chain: Option<Chain>,
147        etherscan_alias: Option<&str>,
148        etherscan_api_key: Option<&str>,
149    ) -> (Option<Chain>, Option<ResolvedEtherscanConfig>) {
150        let mut resolved =
151            self.etherscan.resolve_for(etherscan_alias, etherscan_api_key, chain.or(self.chain));
152        // Only an alias can resolve to a chain other than the requested one. An alias pinned to
153        // another chain must not redirect lookups away from the traced chain.
154        if let Some(chain) = chain
155            && let Ok(Some(config)) = &resolved
156            && config.chain.is_some_and(|c| c != chain)
157        {
158            resolved = self.etherscan.resolve_for(None, self.literal_api_key(), Some(chain));
159        }
160        match resolved {
161            Ok(Some(config)) => (chain.or(config.chain), Some(config)),
162            Ok(None) => {
163                warn!(target: "evm::traces::external", "etherscan config not found");
164                (chain, None)
165            }
166            Err(err) => {
167                warn!(target: "evm::traces::external", ?err, "failed to get etherscan config");
168                (chain, None)
169            }
170        }
171    }
172
173    /// Maximum time a storage-layout lookup may block.
174    pub const fn storage_timeout(&self) -> Duration {
175        Duration::from_secs(self.timeout)
176    }
177
178    /// Returns `etherscan_api_key` unless it names an `[etherscan]` entry.
179    ///
180    /// `etherscan_api_key` also accepts a table alias. An alias name must not be sent as the
181    /// literal API key when the alias itself is not used.
182    fn literal_api_key(&self) -> Option<&str> {
183        self.etherscan_api_key.as_deref().filter(|key| !self.etherscan.contains_key(*key))
184    }
185}
186
187impl ExternalIdentifier {
188    /// Creates a new external identifier with the given client.
189    pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Option<Self>> {
190        Ok(ExternalIdentifierConfig::new(config).identifier(chain))
191    }
192
193    /// Goes over the list of contracts we have pulled from the traces, clones their source from
194    /// Etherscan and compiles them locally, for usage in the debugger.
195    pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
196        // Collect contract info upfront so we can reference it in error messages
197        let contracts_info: Vec<_> = self
198            .contracts
199            .iter()
200            // filter out vyper files and contracts without metadata
201            .filter_map(|(addr, (_, metadata))| {
202                if let Some(metadata) = metadata.as_ref()
203                    && !metadata.is_vyper()
204                {
205                    Some((*addr, metadata))
206                } else {
207                    None
208                }
209            })
210            .collect();
211
212        let outputs_fut = contracts_info
213            .iter()
214            .map(|(addr, metadata)| async move {
215                sh_println!("Compiling: {} {addr}", metadata.contract_name)?;
216                let root = tempfile::tempdir()?;
217                let root_path = root.path();
218                let project = etherscan_project(metadata, root_path)?;
219                let output = project.compile()?;
220                if output.has_compiler_errors() {
221                    eyre::bail!("{output}");
222                }
223
224                Ok((project, output, root))
225            })
226            .collect::<Vec<_>>();
227
228        // poll all the futures concurrently
229        let outputs = join_all(outputs_fut).await;
230
231        let mut sources: ContractSources = Default::default();
232
233        // construct the map
234        for (idx, res) in outputs.into_iter().enumerate() {
235            let (addr, metadata) = &contracts_info[idx];
236            let name = &metadata.contract_name;
237            let (project, output, _) =
238                res.wrap_err_with(|| format!("Failed to compile contract {name} at {addr}"))?;
239            sources
240                .insert(&output, project.root(), None)
241                .wrap_err_with(|| format!("Failed to insert contract {name} at {addr}"))?;
242        }
243
244        Ok(sources)
245    }
246
247    fn identify_from_metadata(
248        &self,
249        address: Address,
250        metadata: &Metadata,
251    ) -> IdentifiedAddress<'static> {
252        let label = metadata.contract_name.clone();
253        let abi = metadata.abi().ok().map(Cow::Owned);
254        IdentifiedAddress {
255            address,
256            label: Some(label.clone()),
257            contract: Some(label),
258            abi,
259            constructor_args_offset: None,
260            artifact_id: None,
261        }
262    }
263
264    fn cache_fetched(&mut self, address: Address, value: (FetcherKind, Option<Metadata>)) {
265        match self.contracts.entry(address) {
266            Entry::Occupied(mut occupied_entry) => {
267                let old = occupied_entry.get();
268                // Only override when the new result is strictly better:
269                // - new has metadata and old doesn't, OR
270                // - both have metadata but new is from Etherscan and old is not.
271                // Never downgrade a successful lookup to None.
272                let should_replace = match (&old.1, &value.1) {
273                    (None, Some(_)) => true,
274                    (Some(_), None) => false,
275                    _ => {
276                        matches!(value.0, FetcherKind::Etherscan)
277                            && !matches!(old.0, FetcherKind::Etherscan)
278                    }
279                };
280                if should_replace {
281                    occupied_entry.insert(value);
282                }
283            }
284            Entry::Vacant(vacant_entry) => {
285                vacant_entry.insert(value);
286            }
287        }
288    }
289
290    async fn fetch_addresses_async(&mut self, addresses: &[Address]) {
291        self.fetch_addresses_with_timeout(addresses, self.remaining_budget).await;
292    }
293
294    async fn fetch_addresses_with_timeout(&mut self, addresses: &[Address], timeout: Duration) {
295        let timeout = timeout.min(self.remaining_budget);
296        if addresses.is_empty() || timeout.is_zero() {
297            return;
298        }
299
300        let clock = Arc::clone(&self.clock);
301        let fetchers = self
302            .fetchers
303            .clone()
304            .into_iter()
305            .map(|fetcher| ExternalFetcher::new(fetcher, addresses, Arc::clone(&clock)));
306        let started = clock.now();
307        let deadline = clock.sleep(timeout);
308        let timed_out = {
309            let fetch = pin!(async {
310                let mut fetched = futures::stream::select_all(fetchers);
311                while let Some((address, value)) = fetched.next().await {
312                    self.cache_fetched(address, value);
313                }
314            });
315            matches!(select(fetch, deadline).await, Either::Right(_))
316        };
317        self.remaining_budget = self.remaining_budget.saturating_sub(clock.now() - started);
318        if timed_out && self.remaining_budget.is_zero() {
319            warn!(target: "evm::traces::external", "external identification timed out; disabling it for the remainder of this session");
320        }
321    }
322
323    /// Fetches all verified ABIs and whether each proxy chain was fully resolved.
324    pub async fn get_abis(
325        &mut self,
326        addresses: &[Address],
327    ) -> Vec<(Address, eyre::Result<(Vec<JsonAbi>, bool)>)> {
328        const MAX_PROXY_DEPTH: usize = 16;
329
330        struct Chain {
331            current: Option<Address>,
332            visited: HashSet<Address>,
333            abis: Vec<JsonAbi>,
334            complete: bool,
335        }
336
337        let mut chains = addresses
338            .iter()
339            .map(|&address| Chain {
340                current: Some(address),
341                visited: HashSet::default(),
342                abis: Vec::new(),
343                complete: true,
344            })
345            .collect::<Vec<_>>();
346
347        for _ in 0..MAX_PROXY_DEPTH {
348            let to_fetch = chains
349                .iter()
350                .filter_map(|chain| chain.current)
351                .filter(|address| !self.contracts.contains_key(address))
352                .collect::<HashSet<_>>()
353                .into_iter()
354                .collect::<Vec<_>>();
355            self.fetch_addresses_async(&to_fetch).await;
356
357            let mut has_next = false;
358            for chain in &mut chains {
359                let Some(current) = chain.current else { continue };
360                if !chain.visited.insert(current) {
361                    chain.current = None;
362                    chain.complete = false;
363                    continue;
364                }
365                let Some((_, Some(metadata))) = self.contracts.get(&current) else {
366                    chain.current = None;
367                    chain.complete = false;
368                    continue;
369                };
370                if let Ok(abi) = metadata.abi() {
371                    chain.abis.push(abi);
372                } else {
373                    chain.complete = false;
374                }
375                chain.current = (metadata.proxy != 0).then_some(metadata.implementation).flatten();
376                if metadata.proxy != 0 && chain.current.is_none() {
377                    chain.complete = false;
378                }
379                has_next |= chain.current.is_some();
380            }
381            if !has_next {
382                break;
383            }
384        }
385
386        chains
387            .into_iter()
388            .zip(addresses.iter().copied())
389            .map(|(mut chain, address)| {
390                chain.complete &= chain.current.is_none();
391                let result = if chain.abis.is_empty() {
392                    Err(eyre::eyre!("external ABI lookup failed"))
393                } else {
394                    Ok((chain.abis.into_iter().rev().collect(), chain.complete))
395                };
396                (address, result)
397            })
398            .collect()
399    }
400
401    /// Fetches metadata for the exact addresses supplied, without following explorer proxy hints.
402    ///
403    /// Storage decoding uses the implementation address recorded by the EVM, which is authoritative
404    /// for historical forks and proxies that upgrade over time.
405    pub async fn get_metadata(
406        &mut self,
407        addresses: &[Address],
408        timeout: Duration,
409    ) -> AddressMap<Option<Metadata>> {
410        let to_fetch = addresses
411            .iter()
412            .copied()
413            .filter(|address| !self.contracts.contains_key(address))
414            .collect::<Vec<_>>();
415        self.fetch_addresses_with_timeout(&to_fetch, timeout).await;
416        addresses
417            .iter()
418            .filter_map(|address| {
419                self.contracts.get(address).map(|(_, metadata)| (*address, metadata.clone()))
420            })
421            .collect()
422    }
423}
424
425impl TraceIdentifier for ExternalIdentifier {
426    fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
427        if nodes.is_empty() {
428            return Vec::new();
429        }
430
431        trace!(target: "evm::traces::external", "identify {} addresses", nodes.len());
432
433        let mut identities = Vec::new();
434        let mut to_fetch = AddressSet::default();
435
436        // Check cache first.
437        for &node in nodes {
438            let address = node.trace.address;
439            // Foundry-only addresses that are never deployed contracts on a real chain.
440            if matches!(address, CHEATCODE_ADDRESS | HARDHAT_CONSOLE_ADDRESS) {
441                continue;
442            }
443            if let Some((_, metadata)) = self.contracts.get(&address) {
444                if let Some(metadata) = metadata {
445                    identities.push(self.identify_from_metadata(address, metadata));
446                } else {
447                    // Do nothing. We know that this contract was not verified.
448                }
449            } else {
450                to_fetch.insert(address);
451            }
452        }
453
454        if to_fetch.is_empty() {
455            return identities;
456        }
457        if self.remaining_budget.is_zero() {
458            return identities;
459        }
460        trace!(target: "evm::traces::external", "fetching {} addresses", to_fetch.len());
461
462        let to_fetch = to_fetch.into_iter().collect::<Vec<_>>();
463        foundry_common::block_on(self.fetch_addresses_async(&to_fetch));
464
465        for address in to_fetch {
466            if let Some((_, Some(metadata))) = self.contracts.get(&address) {
467                identities.push(self.identify_from_metadata(address, metadata));
468            }
469        }
470        trace!(target: "evm::traces::external", "identified {} addresses", identities.len());
471        identities
472    }
473}
474
475type FetchFuture =
476    Pin<Box<dyn Future<Output = (Address, Result<Option<Metadata>, EtherscanError>)>>>;
477
478/// Maximum number of times a single address is retried after a rate limit or a transient
479/// Cloudflare block before we give up on it. Bounded so a persistent block can't loop forever.
480const MAX_TRANSIENT_RETRIES: u32 = 5;
481
482type Sleep = Pin<Box<dyn Future<Output = ()> + Send>>;
483
484/// The source of time for the identification budget and the rate limit backoff.
485///
486/// Tests replace it to drive time by hand.
487trait Clock: Send + Sync {
488    fn now(&self) -> Instant;
489    fn sleep(&self, duration: Duration) -> Sleep;
490}
491
492/// The [`Clock`] backed by tokio's timers.
493struct TokioClock;
494
495impl Clock for TokioClock {
496    fn now(&self) -> Instant {
497        Instant::now()
498    }
499
500    fn sleep(&self, duration: Duration) -> Sleep {
501        Box::pin(tokio::time::sleep(duration))
502    }
503}
504
505/// A rate limit aware fetcher.
506///
507/// Fetches information about multiple addresses concurrently, while respecting rate limits.
508struct ExternalFetcher {
509    /// The fetcher
510    fetcher: Arc<dyn ExternalFetcherT>,
511    /// The time we wait if we hit the rate limit
512    timeout: Duration,
513    /// The backoff we are currently waiting for before making a new request
514    backoff: Option<Sleep>,
515    clock: Arc<dyn Clock>,
516    /// The maximum amount of requests to send concurrently
517    concurrency: usize,
518    /// The addresses we have yet to make requests for
519    queue: Vec<Address>,
520    /// The in progress requests
521    in_progress: FuturesUnordered<FetchFuture>,
522    /// Per-address retry counter for rate limits and transient Cloudflare blocks.
523    attempts: HashMap<Address, u32>,
524}
525
526impl ExternalFetcher {
527    fn new(
528        fetcher: Arc<dyn ExternalFetcherT>,
529        to_fetch: &[Address],
530        clock: Arc<dyn Clock>,
531    ) -> Self {
532        Self {
533            timeout: fetcher.timeout(),
534            backoff: None,
535            clock,
536            concurrency: fetcher.concurrency(),
537            fetcher,
538            queue: to_fetch.to_vec(),
539            in_progress: FuturesUnordered::new(),
540            attempts: HashMap::default(),
541        }
542    }
543
544    fn queue_next_reqs(&mut self) {
545        while self.in_progress.len() < self.concurrency {
546            let Some(addr) = self.queue.pop() else { break };
547            let fetcher = Arc::clone(&self.fetcher);
548            self.in_progress.push(Box::pin(async move {
549                trace!(target: "evm::traces::external", ?addr, "fetching info");
550                let res = fetcher.fetch(addr).await;
551                (addr, res)
552            }));
553        }
554    }
555
556    /// Requeues `addr` after a backoff, unless it already used all of its retries.
557    ///
558    /// Returns `false` when the address is dropped. A dropped address is not cached, so a
559    /// transient failure does not become an "unverified" result.
560    fn retry(&mut self, addr: Address) -> bool {
561        let attempts = self.attempts.entry(addr).or_default();
562        *attempts += 1;
563        if *attempts > MAX_TRANSIENT_RETRIES {
564            return false;
565        }
566        self.backoff = Some(self.clock.sleep(self.timeout));
567        self.queue.push(addr);
568        true
569    }
570}
571
572impl Stream for ExternalFetcher {
573    type Item = (Address, (FetcherKind, Option<Metadata>));
574
575    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
576        let pin = self.get_mut();
577
578        let _guard =
579            info_span!("evm::traces::external", kind=?pin.fetcher.kind(), "ExternalFetcher")
580                .entered();
581
582        if pin.fetcher.invalid_api_key().load(Ordering::Relaxed) {
583            return Poll::Ready(None);
584        }
585
586        loop {
587            if let Some(mut backoff) = pin.backoff.take()
588                && backoff.as_mut().poll(cx).is_pending()
589            {
590                pin.backoff = Some(backoff);
591                return Poll::Pending;
592            }
593
594            pin.queue_next_reqs();
595
596            let mut made_progress_this_iter = false;
597            match pin.in_progress.poll_next_unpin(cx) {
598                Poll::Pending => {}
599                Poll::Ready(None) => return Poll::Ready(None),
600                Poll::Ready(Some((addr, res))) => {
601                    made_progress_this_iter = true;
602                    match res {
603                        Ok(metadata) => {
604                            return Poll::Ready(Some((addr, (pin.fetcher.kind(), metadata))));
605                        }
606                        Err(EtherscanError::ContractCodeNotVerified(_)) => {
607                            return Poll::Ready(Some((addr, (pin.fetcher.kind(), None))));
608                        }
609                        Err(EtherscanError::RateLimitExceeded) => {
610                            if pin.retry(addr) {
611                                warn!(target: "evm::traces::external", "rate limit exceeded, backing off");
612                            } else {
613                                warn!(target: "evm::traces::external", "rate limit exceeded, giving up on address");
614                            }
615                        }
616                        Err(EtherscanError::InvalidApiKey) => {
617                            warn!(target: "evm::traces::external", "invalid api key");
618                            // mark key as invalid
619                            pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
620                            return Poll::Ready(None);
621                        }
622                        Err(EtherscanError::ChainNotSupported(chain)) => {
623                            warn!(target: "evm::traces::external", %chain, "chain not supported");
624                            // No address on this chain can resolve, so stop using the fetcher.
625                            pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
626                            return Poll::Ready(None);
627                        }
628                        Err(EtherscanError::BlockedByCloudflare) => {
629                            // A Cloudflare block is transient rate limiting (often triggered
630                            // by request bursts), not a permanent failure like an invalid key.
631                            // Back off and retry the address a bounded number of times instead
632                            // of aborting the whole stream, which would abandon every still-
633                            // queued address and leave traces only partially decoded (#9880).
634                            if pin.retry(addr) {
635                                warn!(target: "evm::traces::external", "blocked by cloudflare, backing off");
636                            } else {
637                                warn!(target: "evm::traces::external", "blocked by cloudflare, giving up on address");
638                                // No conclusion: do not turn a transient outage into a cached
639                                // "unverified" result.
640                            }
641                        }
642                        Err(err) => {
643                            warn!(target: "evm::traces::external", ?err, "could not get info");
644                            // Only ContractCodeNotVerified is a conclusive negative result.
645                        }
646                    }
647                }
648            }
649
650            if !made_progress_this_iter {
651                return Poll::Pending;
652            }
653        }
654    }
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658enum FetcherKind {
659    Etherscan,
660    Sourcify,
661}
662
663#[async_trait::async_trait]
664trait ExternalFetcherT: Send + Sync {
665    fn kind(&self) -> FetcherKind;
666    fn timeout(&self) -> Duration;
667    fn concurrency(&self) -> usize;
668    fn invalid_api_key(&self) -> &AtomicBool;
669    async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError>;
670}
671
672struct EtherscanFetcher {
673    client: foundry_block_explorers::Client,
674    invalid_api_key: AtomicBool,
675}
676
677impl EtherscanFetcher {
678    const fn new(client: foundry_block_explorers::Client) -> Self {
679        Self { client, invalid_api_key: AtomicBool::new(false) }
680    }
681}
682
683#[async_trait::async_trait]
684impl ExternalFetcherT for EtherscanFetcher {
685    fn kind(&self) -> FetcherKind {
686        FetcherKind::Etherscan
687    }
688
689    fn timeout(&self) -> Duration {
690        Duration::from_secs(1)
691    }
692
693    fn concurrency(&self) -> usize {
694        5
695    }
696
697    fn invalid_api_key(&self) -> &AtomicBool {
698        &self.invalid_api_key
699    }
700
701    async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
702        self.client.contract_source_code(address).await.map(|mut metadata| metadata.items.pop())
703    }
704}
705
706struct SourcifyFetcher {
707    client: reqwest::Client,
708    chain: Chain,
709    url: String,
710    invalid_api_key: AtomicBool,
711}
712
713impl SourcifyFetcher {
714    fn new(chain: Chain) -> Self {
715        Self {
716            client: reqwest::Client::builder()
717                .user_agent(foundry_common::DEFAULT_USER_AGENT)
718                .build()
719                .expect("Client::builder() with static config cannot fail"),
720            chain,
721            url: format!("https://sourcify.dev/server/v2/contract/{}", chain.id()),
722            invalid_api_key: AtomicBool::new(false),
723        }
724    }
725}
726
727#[async_trait::async_trait]
728impl ExternalFetcherT for SourcifyFetcher {
729    fn kind(&self) -> FetcherKind {
730        FetcherKind::Sourcify
731    }
732
733    fn timeout(&self) -> Duration {
734        Duration::from_secs(1)
735    }
736
737    fn concurrency(&self) -> usize {
738        5
739    }
740
741    fn invalid_api_key(&self) -> &AtomicBool {
742        &self.invalid_api_key
743    }
744
745    async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
746        let url = format!("{url}/{address}?fields=abi,compilation", url = self.url);
747        let response = self
748            .client
749            .get(url)
750            .send()
751            .await
752            .map_err(|e| EtherscanError::Unknown(e.to_string()))?;
753        let code = response.status();
754        match code.as_u16() {
755            // Not verified.
756            404 => return Err(EtherscanError::ContractCodeNotVerified(address)),
757            // Too many requests.
758            429 => return Err(EtherscanError::RateLimitExceeded),
759            // Bad request. Only a recognized error code is final; anything else is not cached.
760            400 => {
761                let error = response
762                    .json::<SourcifyError>()
763                    .await
764                    .map_err(|e| EtherscanError::Unknown(e.to_string()))?;
765                return Err(match error.custom_code.as_str() {
766                    "unsupported_chain" => EtherscanError::ChainNotSupported(self.chain),
767                    "invalid_parameter" => EtherscanError::ContractCodeNotVerified(address),
768                    _ => EtherscanError::Unknown(format!("{error:#?}")),
769                });
770            }
771            _ => {}
772        }
773        let response: SourcifyResponse =
774            response.json().await.map_err(|e| EtherscanError::Unknown(e.to_string()))?;
775        trace!(target: "evm::traces::external", "Sourcify response for {address}: {response:#?}");
776        match response {
777            SourcifyResponse::Success(metadata) => Ok(Some(metadata.into())),
778            SourcifyResponse::Error(error) => Err(EtherscanError::Unknown(format!("{error:#?}"))),
779        }
780    }
781}
782
783/// Sourcify API response for `/v2/contract/{chainId}/{address}`.
784#[derive(Debug, Clone, Deserialize)]
785#[serde(untagged)]
786enum SourcifyResponse {
787    Success(SourcifyMetadata),
788    Error(SourcifyError),
789}
790
791#[derive(Debug, Clone, Deserialize)]
792#[serde(rename_all = "camelCase")]
793#[expect(dead_code)] // Used in Debug.
794struct SourcifyError {
795    custom_code: String,
796    message: String,
797    error_id: String,
798}
799
800#[derive(Debug, Clone, Deserialize)]
801#[serde(rename_all = "camelCase")]
802struct SourcifyMetadata {
803    #[serde(default)]
804    abi: Option<Box<serde_json::value::RawValue>>,
805    #[serde(default)]
806    compilation: Option<Compilation>,
807}
808
809#[derive(Debug, Clone, Deserialize)]
810#[serde(rename_all = "camelCase")]
811struct Compilation {
812    #[serde(default)]
813    compiler_version: String,
814    #[serde(default)]
815    name: String,
816}
817
818impl From<SourcifyMetadata> for Metadata {
819    fn from(metadata: SourcifyMetadata) -> Self {
820        let SourcifyMetadata { abi, compilation } = metadata;
821        let (contract_name, compiler_version) = compilation
822            .map(|c| (c.name, c.compiler_version))
823            .unwrap_or_else(|| (String::new(), String::new()));
824        // Defaulted fields may be fetched from sourcify but we don't make use of them.
825        Self {
826            source_code: foundry_block_explorers::contract::SourceCodeMetadata::Sources(
827                Default::default(),
828            ),
829            abi: Box::<str>::from(abi.unwrap_or_default()).into(),
830            contract_name,
831            compiler_version,
832            optimization_used: 0,
833            runs: 0,
834            constructor_arguments: Default::default(),
835            evm_version: String::new(),
836            library: String::new(),
837            license_type: String::new(),
838            proxy: 0,
839            implementation: None,
840            swarm_source: String::new(),
841        }
842    }
843}
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848    use futures::task::ArcWake;
849    use std::{
850        collections::HashSet as StdHashSet,
851        future::pending,
852        io::{Read, Write},
853        net::TcpListener,
854        sync::{
855            Mutex,
856            atomic::{AtomicUsize, Ordering as AtomicOrdering},
857        },
858        task::Waker,
859    };
860
861    /// A [`Clock`] that moves only when [`ManualClock::run`] has nothing else to do, then jumps to
862    /// the earliest pending sleep.
863    struct ManualClock {
864        start: Instant,
865        state: Arc<Mutex<ClockState>>,
866    }
867
868    #[derive(Default)]
869    struct ClockState {
870        elapsed: Duration,
871        sleepers: Vec<(Duration, Waker)>,
872    }
873
874    struct ManualSleep {
875        state: Arc<Mutex<ClockState>>,
876        deadline: Duration,
877    }
878
879    struct WakeFlag(AtomicBool);
880
881    impl ArcWake for WakeFlag {
882        fn wake_by_ref(arc_self: &Arc<Self>) {
883            arc_self.0.store(true, AtomicOrdering::Relaxed);
884        }
885    }
886
887    impl ManualClock {
888        fn new() -> Arc<Self> {
889            Arc::new(Self { start: Instant::now(), state: Default::default() })
890        }
891
892        /// Polls `fut` to completion, jumping to the earliest pending sleep whenever nothing else
893        /// can progress.
894        fn run<F: Future>(&self, fut: F) -> F::Output {
895            let mut fut = pin!(fut);
896            let woken = Arc::new(WakeFlag(AtomicBool::new(false)));
897            let waker = futures::task::waker(Arc::clone(&woken));
898            let mut cx = Context::from_waker(&waker);
899            loop {
900                woken.0.store(false, AtomicOrdering::Relaxed);
901                if let Poll::Ready(output) = fut.as_mut().poll(&mut cx) {
902                    return output;
903                }
904                if woken.0.load(AtomicOrdering::Relaxed) {
905                    continue;
906                }
907                let mut state = self.state.lock().unwrap();
908                let next = state.sleepers.iter().map(|(deadline, _)| *deadline).min();
909                state.elapsed = state.elapsed.max(next.expect("pending without a sleep"));
910                for (_, waker) in state.sleepers.drain(..) {
911                    waker.wake();
912                }
913            }
914        }
915    }
916
917    impl Clock for ManualClock {
918        fn now(&self) -> Instant {
919            self.start + self.state.lock().unwrap().elapsed
920        }
921
922        fn sleep(&self, duration: Duration) -> Sleep {
923            let deadline = self.state.lock().unwrap().elapsed + duration;
924            Box::pin(ManualSleep { state: Arc::clone(&self.state), deadline })
925        }
926    }
927
928    impl Future for ManualSleep {
929        type Output = ();
930
931        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
932            let mut state = self.state.lock().unwrap();
933            if state.elapsed >= self.deadline {
934                return Poll::Ready(());
935            }
936            state.sleepers.push((self.deadline, cx.waker().clone()));
937            Poll::Pending
938        }
939    }
940
941    struct TestFetcher {
942        kind: FetcherKind,
943        delay: Option<Duration>,
944        clock: Arc<dyn Clock>,
945        contract_name: Option<&'static str>,
946        calls: Arc<AtomicUsize>,
947        invalid: AtomicBool,
948    }
949
950    #[async_trait::async_trait]
951    impl ExternalFetcherT for TestFetcher {
952        fn kind(&self) -> FetcherKind {
953            self.kind
954        }
955
956        fn timeout(&self) -> Duration {
957            Duration::from_millis(1)
958        }
959
960        fn concurrency(&self) -> usize {
961            1
962        }
963
964        fn invalid_api_key(&self) -> &AtomicBool {
965            &self.invalid
966        }
967
968        async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
969            self.calls.fetch_add(1, AtomicOrdering::Relaxed);
970            let Some(delay) = self.delay else { return pending().await };
971            if !delay.is_zero() {
972                self.clock.sleep(delay).await;
973            }
974            Ok(self.contract_name.map(metadata))
975        }
976    }
977
978    struct RateLimitedFetcher {
979        calls: Arc<AtomicUsize>,
980        invalid: AtomicBool,
981    }
982
983    #[async_trait::async_trait]
984    impl ExternalFetcherT for RateLimitedFetcher {
985        fn kind(&self) -> FetcherKind {
986            FetcherKind::Sourcify
987        }
988
989        fn timeout(&self) -> Duration {
990            Duration::from_millis(5)
991        }
992
993        fn concurrency(&self) -> usize {
994            1
995        }
996
997        fn invalid_api_key(&self) -> &AtomicBool {
998            &self.invalid
999        }
1000
1001        async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
1002            self.calls.fetch_add(1, AtomicOrdering::Relaxed);
1003            Err(EtherscanError::RateLimitExceeded)
1004        }
1005    }
1006
1007    struct ErrorFetcher {
1008        calls: Arc<AtomicUsize>,
1009        invalid: AtomicBool,
1010    }
1011
1012    #[async_trait::async_trait]
1013    impl ExternalFetcherT for ErrorFetcher {
1014        fn kind(&self) -> FetcherKind {
1015            FetcherKind::Etherscan
1016        }
1017
1018        fn timeout(&self) -> Duration {
1019            Duration::ZERO
1020        }
1021
1022        fn concurrency(&self) -> usize {
1023            1
1024        }
1025
1026        fn invalid_api_key(&self) -> &AtomicBool {
1027            &self.invalid
1028        }
1029
1030        async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
1031            self.calls.fetch_add(1, AtomicOrdering::Relaxed);
1032            Err(EtherscanError::Unknown("temporary explorer failure".to_string()))
1033        }
1034    }
1035
1036    fn metadata(contract_name: &str) -> Metadata {
1037        SourcifyMetadata {
1038            abi: None,
1039            compilation: Some(Compilation {
1040                compiler_version: String::new(),
1041                name: contract_name.to_string(),
1042            }),
1043        }
1044        .into()
1045    }
1046
1047    fn test_identifier(
1048        fetchers: Vec<Arc<dyn ExternalFetcherT>>,
1049        remaining_budget: Duration,
1050    ) -> ExternalIdentifier {
1051        test_identifier_with_clock(fetchers, remaining_budget, Arc::new(TokioClock))
1052    }
1053
1054    fn test_identifier_with_clock(
1055        fetchers: Vec<Arc<dyn ExternalFetcherT>>,
1056        remaining_budget: Duration,
1057        clock: Arc<dyn Clock>,
1058    ) -> ExternalIdentifier {
1059        ExternalIdentifier { fetchers, contracts: Default::default(), remaining_budget, clock }
1060    }
1061
1062    #[test]
1063    fn zero_timeout_disables_external_identification() {
1064        let mut config = Config::default();
1065        config.tracing.external_identification_timeout = 0;
1066
1067        assert!(ExternalIdentifier::new(&config, Some(Chain::mainnet())).unwrap().is_none());
1068    }
1069
1070    #[test]
1071    fn alias_for_another_chain_does_not_redirect_lookups() {
1072        let config = ExternalIdentifierConfig {
1073            timeout: 1,
1074            etherscan: serde_json::from_value(serde_json::json!({
1075                "mainnet": { "key": "key" }
1076            }))
1077            .unwrap(),
1078            etherscan_alias: Some("mainnet".to_string()),
1079            etherscan_api_key: Some("mainnet".to_string()),
1080            ..Default::default()
1081        };
1082
1083        assert!(config.storage_identifier(Chain::from(8453)).is_none());
1084
1085        let kinds = |identifier: ExternalIdentifier| {
1086            identifier.fetchers.iter().map(|fetcher| fetcher.kind()).collect::<Vec<_>>()
1087        };
1088        assert_eq!(
1089            kinds(config.identifier(Some(Chain::from(8453))).unwrap()),
1090            [FetcherKind::Sourcify]
1091        );
1092        assert_eq!(
1093            kinds(config.identifier(Some(Chain::mainnet())).unwrap()),
1094            [FetcherKind::Sourcify, FetcherKind::Etherscan]
1095        );
1096
1097        // The traced chain's own entry is used instead of the foreign alias.
1098        let config = ExternalIdentifierConfig {
1099            etherscan: serde_json::from_value(serde_json::json!({
1100                "mainnet": { "key": "key" },
1101                "base": { "key": "key" }
1102            }))
1103            .unwrap(),
1104            ..config
1105        };
1106        let (chain, etherscan) =
1107            config.resolve_explorer(Some(Chain::from(8453)), Some("mainnet"), Some("mainnet"));
1108        assert_eq!(chain, Some(Chain::from(8453)));
1109        assert_eq!(etherscan.unwrap().chain, Some(Chain::from(8453)));
1110
1111        // A chainless alias keeps its explorer URL without clearing the traced chain.
1112        let config = ExternalIdentifierConfig {
1113            etherscan: serde_json::from_value(serde_json::json!({
1114                "custom": { "key": "key", "url": "https://explorer.invalid/api" }
1115            }))
1116            .unwrap(),
1117            etherscan_alias: Some("custom".to_string()),
1118            etherscan_api_key: Some("custom".to_string()),
1119            ..config
1120        };
1121        assert_eq!(
1122            kinds(config.identifier(Some(Chain::from(8453))).unwrap()),
1123            [FetcherKind::Sourcify, FetcherKind::Etherscan]
1124        );
1125    }
1126
1127    /// Fetcher that returns a transient Cloudflare block the first time it sees an address, then
1128    /// succeeds. Mirrors Etherscan/Cloudflare throttling a burst of concurrent requests.
1129    struct FlakyCloudflareFetcher {
1130        seen: Mutex<StdHashSet<Address>>,
1131        invalid: AtomicBool,
1132    }
1133
1134    #[async_trait::async_trait]
1135    impl ExternalFetcherT for FlakyCloudflareFetcher {
1136        fn kind(&self) -> FetcherKind {
1137            FetcherKind::Etherscan
1138        }
1139        fn timeout(&self) -> Duration {
1140            Duration::from_millis(1)
1141        }
1142        fn concurrency(&self) -> usize {
1143            1
1144        }
1145        fn invalid_api_key(&self) -> &AtomicBool {
1146            &self.invalid
1147        }
1148        async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
1149            let first_time = self.seen.lock().unwrap().insert(address);
1150            if first_time { Err(EtherscanError::BlockedByCloudflare) } else { Ok(None) }
1151        }
1152    }
1153
1154    /// Regression test for #9880: a transient Cloudflare block on one address must not abandon the
1155    /// rest of the queue. Before the fix the fetcher returned `Poll::Ready(None)` on the first
1156    /// block, ending the stream and leaving later addresses unidentified (partial trace decoding).
1157    #[tokio::test]
1158    async fn cloudflare_block_retries_instead_of_abandoning_queue() {
1159        let addrs: Vec<Address> = (1u8..=4).map(Address::with_last_byte).collect();
1160        let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(FlakyCloudflareFetcher {
1161            seen: Mutex::new(StdHashSet::new()),
1162            invalid: AtomicBool::new(false),
1163        });
1164
1165        let collected: Vec<_> =
1166            ExternalFetcher::new(fetcher, &addrs, Arc::new(TokioClock)).collect().await;
1167
1168        let got: StdHashSet<Address> = collected.into_iter().map(|(addr, _)| addr).collect();
1169        let want: StdHashSet<Address> = addrs.into_iter().collect();
1170        assert_eq!(got, want, "every address must be yielded despite a transient cloudflare block");
1171    }
1172
1173    #[test]
1174    fn timeout_keeps_partial_results_and_opens_circuit() {
1175        let clock = ManualClock::new();
1176        let successful_calls = Arc::new(AtomicUsize::new(0));
1177        let stalled_calls = Arc::new(AtomicUsize::new(0));
1178        let fetchers: Vec<Arc<dyn ExternalFetcherT>> = vec![
1179            Arc::new(TestFetcher {
1180                kind: FetcherKind::Sourcify,
1181                delay: Some(Duration::ZERO),
1182                clock: clock.clone(),
1183                contract_name: Some("PartialResult"),
1184                calls: Arc::clone(&successful_calls),
1185                invalid: AtomicBool::new(false),
1186            }),
1187            Arc::new(TestFetcher {
1188                kind: FetcherKind::Etherscan,
1189                delay: None,
1190                clock: clock.clone(),
1191                contract_name: None,
1192                calls: Arc::clone(&stalled_calls),
1193                invalid: AtomicBool::new(false),
1194            }),
1195        ];
1196        let mut identifier =
1197            test_identifier_with_clock(fetchers, Duration::from_millis(20), clock.clone());
1198        let address = Address::with_last_byte(1);
1199
1200        clock.run(identifier.fetch_addresses_async(&[address]));
1201
1202        assert!(identifier.remaining_budget.is_zero());
1203        assert_eq!(
1204            identifier.contracts[&address].1.as_ref().unwrap().contract_name,
1205            "PartialResult"
1206        );
1207        assert_eq!(successful_calls.load(AtomicOrdering::Relaxed), 1);
1208        assert_eq!(stalled_calls.load(AtomicOrdering::Relaxed), 1);
1209
1210        clock.run(identifier.fetch_addresses_async(&[Address::with_last_byte(2)]));
1211        assert_eq!(successful_calls.load(AtomicOrdering::Relaxed), 1);
1212        assert_eq!(stalled_calls.load(AtomicOrdering::Relaxed), 1);
1213    }
1214
1215    #[tokio::test(flavor = "multi_thread")]
1216    async fn timeout_returns_partial_identity() {
1217        let fetchers: Vec<Arc<dyn ExternalFetcherT>> = vec![
1218            Arc::new(TestFetcher {
1219                kind: FetcherKind::Sourcify,
1220                delay: Some(Duration::ZERO),
1221                clock: Arc::new(TokioClock),
1222                contract_name: Some("PartialResult"),
1223                calls: Arc::new(AtomicUsize::new(0)),
1224                invalid: AtomicBool::new(false),
1225            }),
1226            Arc::new(TestFetcher {
1227                kind: FetcherKind::Etherscan,
1228                delay: None,
1229                clock: Arc::new(TokioClock),
1230                contract_name: None,
1231                calls: Arc::new(AtomicUsize::new(0)),
1232                invalid: AtomicBool::new(false),
1233            }),
1234        ];
1235        let mut identifier = test_identifier(fetchers, Duration::from_millis(20));
1236        let mut node = CallTraceNode::default();
1237        node.trace.address = Address::with_last_byte(1);
1238
1239        let identities = identifier.identify_addresses(&[&node]);
1240
1241        assert_eq!(identities.len(), 1);
1242        assert_eq!(identities[0].label.as_deref(), Some("PartialResult"));
1243    }
1244
1245    #[test]
1246    fn timeout_budget_is_cumulative_across_fetches() {
1247        let clock = ManualClock::new();
1248        let calls = Arc::new(AtomicUsize::new(0));
1249        let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(TestFetcher {
1250            kind: FetcherKind::Sourcify,
1251            delay: Some(Duration::from_millis(20)),
1252            clock: clock.clone(),
1253            contract_name: Some("FirstResult"),
1254            calls: Arc::clone(&calls),
1255            invalid: AtomicBool::new(false),
1256        });
1257        let mut identifier =
1258            test_identifier_with_clock(vec![fetcher], Duration::from_millis(30), clock.clone());
1259        let first = Address::with_last_byte(1);
1260        let second = Address::with_last_byte(2);
1261
1262        clock.run(identifier.fetch_addresses_async(&[first]));
1263        assert!(identifier.contracts[&first].1.is_some());
1264        assert_eq!(identifier.remaining_budget, Duration::from_millis(10));
1265
1266        clock.run(identifier.fetch_addresses_async(&[second]));
1267        assert!(identifier.remaining_budget.is_zero());
1268        assert!(!identifier.contracts.contains_key(&second));
1269        assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
1270    }
1271
1272    #[test]
1273    fn metadata_requests_preserve_cumulative_budget() {
1274        let clock = ManualClock::new();
1275        let calls = Arc::new(AtomicUsize::new(0));
1276        let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(TestFetcher {
1277            kind: FetcherKind::Etherscan,
1278            delay: None,
1279            clock: clock.clone(),
1280            contract_name: None,
1281            calls: Arc::clone(&calls),
1282            invalid: AtomicBool::new(false),
1283        });
1284        let mut identifier =
1285            test_identifier_with_clock(vec![fetcher], Duration::from_millis(30), clock.clone());
1286        let address = Address::with_last_byte(1);
1287
1288        assert!(
1289            clock.run(identifier.get_metadata(&[address], Duration::from_millis(10))).is_empty()
1290        );
1291        assert_eq!(identifier.remaining_budget, Duration::from_millis(20));
1292        assert!(clock.run(identifier.get_metadata(&[address], Duration::from_secs(1))).is_empty());
1293        assert!(identifier.remaining_budget.is_zero());
1294        assert!(clock.run(identifier.get_metadata(&[address], Duration::from_secs(1))).is_empty());
1295        assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
1296    }
1297
1298    #[test]
1299    fn rate_limit_retries_cannot_escape_timeout_budget() {
1300        let clock = ManualClock::new();
1301        let calls = Arc::new(AtomicUsize::new(0));
1302        let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(RateLimitedFetcher {
1303            calls: Arc::clone(&calls),
1304            invalid: AtomicBool::new(false),
1305        });
1306        let mut identifier =
1307            test_identifier_with_clock(vec![fetcher], Duration::from_millis(20), clock.clone());
1308
1309        clock.run(identifier.fetch_addresses_async(&[Address::with_last_byte(1)]));
1310
1311        assert!(identifier.remaining_budget.is_zero());
1312        assert!(calls.load(AtomicOrdering::Relaxed) > 1);
1313    }
1314
1315    #[test]
1316    fn rate_limit_retries_are_bounded() {
1317        let clock = ManualClock::new();
1318        let calls = Arc::new(AtomicUsize::new(0));
1319        let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(RateLimitedFetcher {
1320            calls: Arc::clone(&calls),
1321            invalid: AtomicBool::new(false),
1322        });
1323
1324        let fetched = clock.run(
1325            ExternalFetcher::new(fetcher, &[Address::with_last_byte(1)], clock.clone())
1326                .collect::<Vec<_>>(),
1327        );
1328
1329        assert!(fetched.is_empty());
1330        assert_eq!(calls.load(AtomicOrdering::Relaxed), MAX_TRANSIENT_RETRIES as usize + 1);
1331    }
1332
1333    struct UnsupportedChainFetcher {
1334        calls: Arc<AtomicUsize>,
1335        invalid: AtomicBool,
1336    }
1337
1338    #[async_trait::async_trait]
1339    impl ExternalFetcherT for UnsupportedChainFetcher {
1340        fn kind(&self) -> FetcherKind {
1341            FetcherKind::Sourcify
1342        }
1343
1344        fn timeout(&self) -> Duration {
1345            Duration::ZERO
1346        }
1347
1348        fn concurrency(&self) -> usize {
1349            1
1350        }
1351
1352        fn invalid_api_key(&self) -> &AtomicBool {
1353            &self.invalid
1354        }
1355
1356        async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
1357            self.calls.fetch_add(1, AtomicOrdering::Relaxed);
1358            Err(EtherscanError::ChainNotSupported(Chain::from_id(12345)))
1359        }
1360    }
1361
1362    #[tokio::test]
1363    async fn unsupported_chain_disables_fetcher() {
1364        let calls = Arc::new(AtomicUsize::new(0));
1365        let mut identifier = test_identifier(
1366            vec![Arc::new(UnsupportedChainFetcher {
1367                calls: Arc::clone(&calls),
1368                invalid: AtomicBool::new(false),
1369            })],
1370            Duration::from_secs(1),
1371        );
1372
1373        identifier
1374            .fetch_addresses_async(&[Address::with_last_byte(1), Address::with_last_byte(2)])
1375            .await;
1376        identifier.fetch_addresses_async(&[Address::with_last_byte(3)]).await;
1377
1378        assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
1379    }
1380
1381    #[test]
1382    fn sourcify_skips_local_chains() {
1383        let config = ExternalIdentifierConfig { timeout: 1, ..Default::default() };
1384
1385        assert!(config.identifier(Some(Chain::from_id(31337))).is_none());
1386        assert!(config.identifier(Some(Chain::dev())).is_none());
1387        assert!(config.identifier(Some(Chain::mainnet())).is_some());
1388    }
1389
1390    #[tokio::test(flavor = "multi_thread")]
1391    async fn foundry_addresses_are_not_fetched() {
1392        let calls = Arc::new(AtomicUsize::new(0));
1393        let mut identifier = test_identifier(
1394            vec![Arc::new(TestFetcher {
1395                kind: FetcherKind::Sourcify,
1396                delay: Some(Duration::ZERO),
1397                clock: Arc::new(TokioClock),
1398                contract_name: None,
1399                calls: Arc::clone(&calls),
1400                invalid: AtomicBool::new(false),
1401            })],
1402            Duration::from_secs(1),
1403        );
1404        let nodes = [CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS].map(|address| {
1405            let mut node = CallTraceNode::default();
1406            node.trace.address = address;
1407            node
1408        });
1409
1410        assert!(identifier.identify_addresses(&[&nodes[0], &nodes[1]]).is_empty());
1411        assert_eq!(calls.load(AtomicOrdering::Relaxed), 0);
1412    }
1413
1414    /// Serves one HTTP response with status 400 and `body`, then returns the Sourcify result.
1415    async fn sourcify_bad_request(body: &'static str) -> EtherscanError {
1416        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1417        let addr = listener.local_addr().unwrap();
1418        std::thread::spawn(move || {
1419            let (mut stream, _) = listener.accept().unwrap();
1420            let _ = stream.read(&mut [0; 4096]);
1421            let response = format!(
1422                "HTTP/1.1 400 Bad Request\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
1423                body.len()
1424            );
1425            stream.write_all(response.as_bytes()).unwrap();
1426        });
1427        let mut fetcher = SourcifyFetcher::new(Chain::mainnet());
1428        fetcher.url = format!("http://{addr}");
1429        fetcher.fetch(Address::with_last_byte(1)).await.unwrap_err()
1430    }
1431
1432    #[tokio::test]
1433    async fn sourcify_bad_request_is_final_only_when_recognized() {
1434        let error =
1435            sourcify_bad_request(r#"{"customCode":"unsupported_chain","message":"","errorId":""}"#)
1436                .await;
1437        assert!(matches!(error, EtherscanError::ChainNotSupported(_)));
1438
1439        let error =
1440            sourcify_bad_request(r#"{"customCode":"invalid_parameter","message":"","errorId":""}"#)
1441                .await;
1442        assert!(matches!(error, EtherscanError::ContractCodeNotVerified(_)));
1443
1444        let error =
1445            sourcify_bad_request(r#"{"customCode":"other","message":"","errorId":""}"#).await;
1446        assert!(matches!(error, EtherscanError::Unknown(_)));
1447
1448        let error = sourcify_bad_request("<html>bad request</html>").await;
1449        assert!(matches!(error, EtherscanError::Unknown(_)));
1450    }
1451
1452    #[tokio::test]
1453    async fn transient_errors_are_not_cached_as_unverified() {
1454        let calls = Arc::new(AtomicUsize::new(0));
1455        let mut identifier = test_identifier(
1456            vec![Arc::new(ErrorFetcher {
1457                calls: Arc::clone(&calls),
1458                invalid: AtomicBool::new(false),
1459            })],
1460            Duration::from_secs(1),
1461        );
1462        let address = Address::with_last_byte(1);
1463
1464        assert!(identifier.get_metadata(&[address], Duration::from_secs(1)).await.is_empty());
1465        assert!(identifier.get_metadata(&[address], Duration::from_secs(1)).await.is_empty());
1466        assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
1467    }
1468
1469    #[test]
1470    fn etherscan_metadata_takes_precedence() {
1471        let address = Address::with_last_byte(1);
1472        let mut identifier = test_identifier(Vec::new(), Duration::ZERO);
1473
1474        identifier
1475            .cache_fetched(address, (FetcherKind::Sourcify, Some(metadata("SourcifyResult"))));
1476        identifier.cache_fetched(address, (FetcherKind::Etherscan, None));
1477        assert_eq!(
1478            identifier.contracts[&address].1.as_ref().unwrap().contract_name,
1479            "SourcifyResult"
1480        );
1481
1482        identifier
1483            .cache_fetched(address, (FetcherKind::Etherscan, Some(metadata("EtherscanResult"))));
1484        assert_eq!(
1485            identifier.contracts[&address].1.as_ref().unwrap().contract_name,
1486            "EtherscanResult"
1487        );
1488    }
1489
1490    #[tokio::test]
1491    async fn proxy_metadata_preserves_address_identity_and_all_abis() {
1492        let proxy = Address::with_last_byte(1);
1493        let implementation_address = Address::with_last_byte(2);
1494        let mut proxy_metadata = metadata("Proxy");
1495        proxy_metadata.abi =
1496            r#"[{"anonymous":false,"inputs":[],"name":"ProxyEvent","type":"event"}]"#.to_string();
1497        proxy_metadata.proxy = 1;
1498        proxy_metadata.implementation = Some(implementation_address);
1499        let mut implementation = metadata("Implementation");
1500        implementation.abi =
1501            r#"[{"anonymous":false,"inputs":[],"name":"ImplementationEvent","type":"event"}]"#
1502                .to_string();
1503        let mut identifier = test_identifier(Vec::new(), Duration::from_secs(1));
1504        let identity = identifier.identify_from_metadata(proxy, &proxy_metadata);
1505        assert_eq!(identity.contract.as_deref(), Some("Proxy"));
1506        identifier.cache_fetched(proxy, (FetcherKind::Etherscan, Some(proxy_metadata)));
1507        identifier
1508            .cache_fetched(implementation_address, (FetcherKind::Etherscan, Some(implementation)));
1509
1510        let mut results = identifier.get_abis(&[proxy]).await;
1511        let (result_address, result) = results.pop().unwrap();
1512        let (abis, complete) = result.unwrap();
1513        let event_names =
1514            abis.into_iter().map(|abi| abi.events.into_keys().next().unwrap()).collect::<Vec<_>>();
1515
1516        assert_eq!(result_address, proxy);
1517        assert!(complete);
1518        assert_eq!(event_names, ["ImplementationEvent", "ProxyEvent"]);
1519
1520        identifier.contracts.remove(&implementation_address);
1521        let (_, result) = identifier.get_abis(&[proxy]).await.pop().unwrap();
1522        let (abis, complete) = result.unwrap();
1523        assert_eq!(abis.len(), 1);
1524        assert!(!complete);
1525    }
1526
1527    #[tokio::test]
1528    async fn storage_metadata_does_not_follow_explorer_proxy_hints() {
1529        let proxy = Address::with_last_byte(1);
1530        let implementation_address = Address::with_last_byte(2);
1531        let mut proxy_metadata = metadata("Proxy");
1532        proxy_metadata.proxy = 1;
1533        proxy_metadata.implementation = Some(implementation_address);
1534        let mut identifier = test_identifier(Vec::new(), Duration::from_secs(1));
1535        identifier.cache_fetched(proxy, (FetcherKind::Etherscan, Some(proxy_metadata)));
1536        identifier.cache_fetched(
1537            implementation_address,
1538            (FetcherKind::Etherscan, Some(metadata("CurrentImplementation"))),
1539        );
1540
1541        let result = identifier.get_metadata(&[proxy], Duration::from_secs(1)).await;
1542
1543        assert_eq!(result[&proxy].as_ref().unwrap().contract_name, "Proxy");
1544        assert_eq!(result.len(), 1);
1545    }
1546}