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
30pub struct ExternalIdentifier {
32 fetchers: Vec<Arc<dyn ExternalFetcherT>>,
33 contracts: HashMap<Address, (FetcherKind, Option<Metadata>)>,
35 remaining_budget: Duration,
37 clock: Arc<dyn Clock>,
38}
39
40#[derive(Clone, Debug, Default)]
46pub struct ExternalIdentifierConfig {
47 offline: bool,
49 timeout: u64,
51 no_proxy: bool,
53 etherscan: EtherscanConfigs,
55 etherscan_alias: Option<String>,
56 etherscan_api_key: Option<String>,
57 chain: Option<Chain>,
59}
60
61impl ExternalIdentifierConfig {
62 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 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 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 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 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 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 pub const fn storage_timeout(&self) -> Duration {
175 Duration::from_secs(self.timeout)
176 }
177
178 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 pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Option<Self>> {
190 Ok(ExternalIdentifierConfig::new(config).identifier(chain))
191 }
192
193 pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
196 let contracts_info: Vec<_> = self
198 .contracts
199 .iter()
200 .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 let outputs = join_all(outputs_fut).await;
230
231 let mut sources: ContractSources = Default::default();
232
233 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 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 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(¤t) 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 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 for &node in nodes {
438 let address = node.trace.address;
439 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 }
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
478const MAX_TRANSIENT_RETRIES: u32 = 5;
481
482type Sleep = Pin<Box<dyn Future<Output = ()> + Send>>;
483
484trait Clock: Send + Sync {
488 fn now(&self) -> Instant;
489 fn sleep(&self, duration: Duration) -> Sleep;
490}
491
492struct 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
505struct ExternalFetcher {
509 fetcher: Arc<dyn ExternalFetcherT>,
511 timeout: Duration,
513 backoff: Option<Sleep>,
515 clock: Arc<dyn Clock>,
516 concurrency: usize,
518 queue: Vec<Address>,
520 in_progress: FuturesUnordered<FetchFuture>,
522 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 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 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 pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
626 return Poll::Ready(None);
627 }
628 Err(EtherscanError::BlockedByCloudflare) => {
629 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 }
641 }
642 Err(err) => {
643 warn!(target: "evm::traces::external", ?err, "could not get info");
644 }
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 404 => return Err(EtherscanError::ContractCodeNotVerified(address)),
757 429 => return Err(EtherscanError::RateLimitExceeded),
759 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#[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)] struct 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 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 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 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 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 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 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 #[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 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}