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