1use crate::{
2 ScriptArgs, ScriptConfig,
3 build::LinkedBuildData,
4 sequence::{ScriptSequenceKind, get_commit_hash},
5};
6use alloy_network::{Network, ReceiptResponse};
7use alloy_primitives::{Address, TxHash, hex};
8use eyre::{Result, eyre};
9use forge_script_sequence::{AdditionalContract, ScriptSequence};
10use forge_verify::{
11 RetryArgs, VerifierArgs, VerifyArgs,
12 provider::{ExternalVerificationContext, VerificationProviderType},
13 sourcify::SOURCIFY_URL,
14 verify::sourcify_api_url,
15};
16use foundry_cli::opts::{EtherscanOpts, ProjectPathOpts};
17use foundry_common::{ContractsByArtifact, FoundryReceiptResponse};
18use foundry_compilers::{Project, artifacts::EvmVersion, info::ContractInfo};
19use foundry_config::{Chain, Config};
20use foundry_evm::core::evm::FoundryEvmNetwork;
21use semver::Version;
22
23mod external;
24
25use external::{ExternalResolver, MAX_PROVENANCE_ADDRESSES, MatchResult, match_candidates};
26
27const MAX_EXTERNAL_JOBS: usize = 32;
28
29pub struct BroadcastedState<FEN: FoundryEvmNetwork> {
33 pub args: ScriptArgs,
34 pub script_config: ScriptConfig<FEN>,
35 pub build_data: LinkedBuildData,
36 pub sequence: ScriptSequenceKind<FEN::Network>,
37}
38
39impl<FEN: FoundryEvmNetwork> BroadcastedState<FEN> {
40 pub async fn verify(self) -> Result<()> {
41 let Self { args, script_config, build_data, mut sequence, .. } = self;
42
43 let verify = VerifyBundle::new(
44 &script_config.config.project()?,
45 &script_config.config,
46 build_data.known_contracts,
47 args.retry,
48 args.verifier,
49 args.verify_external,
50 );
51
52 for sequence in sequence.sequences_mut() {
53 verify_contracts::<FEN>(sequence, &script_config.config, verify.clone()).await?;
54 }
55
56 Ok(())
57 }
58}
59
60#[derive(Clone)]
62pub struct VerifyBundle {
63 pub num_of_optimizations: Option<usize>,
64 pub known_contracts: ContractsByArtifact,
65 pub project_paths: ProjectPathOpts,
66 pub etherscan: EtherscanOpts,
67 pub retry: RetryArgs,
68 pub verifier: VerifierArgs,
69 pub via_ir: bool,
70 pub verify_external: bool,
71 source_etherscan_url: Option<String>,
72 source_etherscan_key: Option<String>,
73 source_sourcify_url: Option<String>,
74}
75
76impl VerifyBundle {
77 pub fn new(
78 project: &Project,
79 config: &Config,
80 known_contracts: ContractsByArtifact,
81 retry: RetryArgs,
82 verifier: VerifierArgs,
83 verify_external: bool,
84 ) -> Self {
85 let num_of_optimizations =
86 if config.optimizer == Some(true) { config.optimizer_runs } else { None };
87
88 let config_path = config.get_config_path();
89
90 let project_paths = ProjectPathOpts {
91 root: Some(project.paths.root.clone()),
92 contracts: Some(project.paths.sources.clone()),
93 remappings: project.paths.remappings.clone(),
94 remappings_env: None,
95 cache_path: Some(project.paths.cache.clone()),
96 lib_paths: project.paths.libraries.clone(),
97 hardhat: config.profile == Config::HARDHAT_PROFILE,
98 config_path: config_path.exists().then_some(config_path),
99 };
100
101 let via_ir = config.via_ir;
102
103 Self {
104 num_of_optimizations,
105 known_contracts,
106 etherscan: Default::default(),
107 project_paths,
108 retry,
109 verifier,
110 via_ir,
111 verify_external,
112 source_etherscan_url: None,
113 source_etherscan_key: None,
114 source_sourcify_url: None,
115 }
116 }
117
118 pub fn set_chain(&mut self, config: &Config, chain: Chain) -> Result<()> {
120 let config_key = source_api_key(config, chain);
123 let resolved_key = self.verifier.resolve_api_key(config_key.as_deref()).map(str::to_owned);
124 let provider = self.verifier.resolve(resolved_key.as_deref(), Some(chain));
125
126 if provider.is_sourcify() {
129 self.source_sourcify_url = Some(
130 self.verifier
131 .verifier_url
132 .clone()
133 .or_else(|| sourcify_api_url(chain))
134 .unwrap_or_else(|| SOURCIFY_URL.to_string()),
135 );
136 self.source_etherscan_url = config
139 .get_etherscan_config_with_chain(Some(chain))
140 .ok()
141 .flatten()
142 .map(|config| config.api_url);
143 self.source_etherscan_key = config_key;
144 if self
145 .source_sourcify_url
146 .as_deref()
147 .zip(self.source_etherscan_url.as_deref())
148 .is_some_and(|(sourcify, etherscan)| same_endpoint(sourcify, etherscan))
149 {
150 self.source_etherscan_url = None;
151 self.source_etherscan_key = None;
152 }
153 } else if let Some(url) = &self.verifier.verifier_url {
154 self.source_sourcify_url = None;
157 self.source_etherscan_url = Some(url.clone());
158 self.source_etherscan_key = resolved_key.clone();
159 } else {
160 self.source_sourcify_url =
161 Some(sourcify_api_url(chain).unwrap_or_else(|| SOURCIFY_URL.to_string()));
162 self.source_etherscan_url = config
163 .get_etherscan_config_with_chain(Some(chain))?
164 .map(|config| config.api_url)
165 .or_else(|| {
166 if provider.is_etherscan() && !chain.is_custom_sourcify() {
167 chain.etherscan_urls().map(|(api_url, _)| api_url.to_string())
168 } else {
169 None
170 }
171 });
172 self.source_etherscan_key = resolved_key.clone();
173 }
174 self.etherscan.key = resolved_key;
175 self.etherscan.chain = Some(chain);
176 Ok(())
177 }
178
179 pub fn get_verify_args(
182 &self,
183 contract_address: Address,
184 create2_offset: usize,
185 data: &[u8],
186 libraries: &[String],
187 evm_version: EvmVersion,
188 ) -> Option<VerifyArgs> {
189 for (artifact, contract) in self.known_contracts.iter() {
190 let Some(bytecode) = contract.bytecode() else { continue };
191 if data.split_at(create2_offset).1.starts_with(bytecode) {
194 let constructor_args = data.split_at(create2_offset + bytecode.len()).1.to_vec();
195
196 if artifact.source.extension().is_some_and(|e| e.to_str() == Some("vy")) {
197 warn!("Skipping verification of Vyper contract: {}", artifact.name);
198 return None;
199 }
200
201 let contract = ContractInfo {
203 path: Some(artifact.source.to_string_lossy().to_string()),
204 name: artifact
205 .name
206 .strip_suffix(&format!(".{}", artifact.profile))
207 .unwrap_or_else(|| &artifact.name)
208 .to_string(),
209 };
210
211 let version = Version::new(
215 artifact.version.major,
216 artifact.version.minor,
217 artifact.version.patch,
218 );
219
220 let verify = VerifyArgs {
221 address: contract_address,
222 contract: Some(contract),
223 compiler_version: Some(version.to_string()),
224 constructor_args: Some(hex::encode(constructor_args)),
225 constructor_args_path: None,
226 no_auto_detect: false,
227 use_solc: None,
228 num_of_optimizations: self.num_of_optimizations,
229 etherscan: self.etherscan.clone(),
230 rpc: Default::default(),
231 flatten: false,
232 force: false,
233 skip_is_verified_check: true,
234 watch: true,
235 print_submission_result_to_stdout: false,
236 retry: self.retry,
237 libraries: libraries.to_vec(),
238 root: None,
239 verifier: self.verifier.clone(),
240 via_ir: self.via_ir,
241 license_type: None,
242 evm_version: Some(evm_version),
243 show_standard_json_input: false,
244 guess_constructor_args: false,
245 compilation_profile: Some(artifact.profile.clone()),
246 language: None,
247 creation_transaction_hash: None,
248 };
249
250 return Some(verify);
251 }
252 }
253 None
254 }
255}
256
257fn source_api_key(config: &Config, chain: Chain) -> Option<String> {
258 config.get_etherscan_api_key(Some(chain)).or_else(|| config.etherscan_api_key.clone())
259}
260
261enum VerificationJob {
262 Local(VerifyArgs),
263 External(VerifyArgs, Box<ExternalVerificationContext>),
264}
265
266impl VerificationJob {
267 async fn run(self) -> Result<()> {
268 match self {
269 Self::Local(args) => args.run().await,
270 Self::External(args, context) => args.run_with_external_context(*context).await,
271 }
272 }
273}
274
275#[allow(clippy::too_many_arguments)]
276async fn external_job(
277 resolver: &mut Option<ExternalResolver>,
278 config: &Config,
279 chain: Chain,
280 verify: &VerifyBundle,
281 address: Address,
282 init_code: &[u8],
283 creators: &[Address],
284 creation_transaction_hash: TxHash,
285) -> Result<VerificationJob, String> {
286 if resolver.is_none() {
287 *resolver = Some(ExternalResolver::new().map_err(|err| concise(&err.to_string()))?);
288 }
289 let resolver = resolver.as_mut().unwrap();
290 let mut candidate_sets = Vec::new();
291 let mut reasons = Vec::new();
292
293 for &creator in creators.iter().take(MAX_PROVENANCE_ADDRESSES) {
294 let sources = [
295 (
296 "Sourcify",
297 resolver
298 .resolve_sourcify(chain, creator, verify.source_sourcify_url.as_deref())
299 .await,
300 ),
301 (
302 "Etherscan",
303 resolver
304 .resolve_etherscan(
305 chain,
306 creator,
307 verify.source_etherscan_url.as_deref(),
308 verify.source_etherscan_key.as_deref(),
309 )
310 .await,
311 ),
312 ];
313 for (provider, source) in sources {
314 match source {
315 Ok(Some(source)) => match resolver.compile(&source).await {
316 Ok((compiled, has_unresolved_links)) => {
317 if has_unresolved_links {
318 reasons.push(format!(
319 "{} {creator}: contracts with unresolved library links are unsupported",
320 source.provider
321 ));
322 }
323 candidate_sets.push(compiled);
324 }
325 Err(err) => reasons.push(format!(
326 "{} {creator}: compile failed ({})",
327 source.provider,
328 concise(&err)
329 )),
330 },
331 Ok(None) => {}
332 Err(err) => reasons.push(format!("{provider} {creator}: {}", concise(&err))),
333 }
334 }
335 }
336
337 let matched = match match_candidates(
338 init_code,
339 candidate_sets.iter().flat_map(|candidates| candidates.iter()),
340 ) {
341 MatchResult::Unique(matched) => matched,
342 MatchResult::None => {
343 let context = if reasons.is_empty() {
344 "no matching candidates were found".to_string()
345 } else {
346 format!("no matching candidates were found; {}", reasons.join("; "))
347 };
348 return Err(context);
349 }
350 MatchResult::Ambiguous(matches) => {
351 let fqns = matches
352 .into_iter()
353 .map(|matched| format!("{}@{}", matched.fqn, matched.version))
354 .collect::<Vec<_>>();
355 return Err(format!("ambiguous external candidates: {}", fqns.join(", ")));
356 }
357 };
358
359 let mut pinned_config = config.clone();
360 pinned_config.chain = Some(chain);
361 let context = ExternalVerificationContext {
362 config: pinned_config,
363 compiler_version: matched.version.clone(),
364 standard_json_input: matched.input,
365 target: matched.fqn,
366 };
367 let args = VerifyArgs {
368 address,
369 contract: None,
370 compiler_version: Some(matched.version.to_string()),
371 constructor_args: Some(hex::encode(matched.constructor_args)),
372 constructor_args_path: None,
373 no_auto_detect: false,
374 use_solc: None,
375 num_of_optimizations: None,
376 etherscan: verify.etherscan.clone(),
377 rpc: Default::default(),
378 flatten: false,
379 force: false,
380 skip_is_verified_check: true,
381 watch: true,
382 print_submission_result_to_stdout: false,
383 retry: verify.retry,
384 libraries: Vec::new(),
385 root: None,
386 verifier: verify.verifier.clone(),
387 via_ir: false,
388 license_type: None,
389 evm_version: None,
390 show_standard_json_input: false,
391 guess_constructor_args: false,
392 compilation_profile: None,
393 language: None,
394 creation_transaction_hash: Some(creation_transaction_hash),
395 };
396 Ok(VerificationJob::External(args, Box::new(context)))
397}
398
399fn concise(reason: &str) -> String {
400 const LIMIT: usize = 160;
401 let mut chars = reason.chars().map(|ch| if ch.is_control() { ' ' } else { ch });
402 let reason = chars.by_ref().take(LIMIT).collect::<String>();
403 if chars.next().is_some() { format!("{reason}…") } else { reason }
404}
405
406fn take_matching_index<T>(
407 values: &[T],
408 consumed: &mut [bool],
409 predicate: impl Fn(&T) -> bool,
410) -> Option<usize> {
411 let index = values
412 .iter()
413 .enumerate()
414 .position(|(index, value)| !consumed[index] && predicate(value))?;
415 consumed[index] = true;
416 Some(index)
417}
418
419async fn verify_contracts<FEN: FoundryEvmNetwork>(
422 sequence: &mut ScriptSequence<FEN::Network>,
423 config: &Config,
424 mut verify: VerifyBundle,
425) -> Result<()> {
426 trace!(target: "script", "verifying {} contracts [{}]", verify.known_contracts.len(), sequence.chain);
427
428 verify.set_chain(config, sequence.chain.into())?;
429
430 if verify.etherscan.has_key()
431 || verify.verifier.effective_type() != VerificationProviderType::Etherscan
432 {
433 trace!(target: "script", "prepare future verifications");
434
435 let mut verification_jobs = Vec::with_capacity(sequence.receipts.len());
436 let mut unverifiable_contracts = vec![];
437 let mut resolver = None;
438 let mut external_jobs = 0;
439 let mut skipped_external = 0;
440 let mut warned_offline = false;
441 let mut consumed_receipts = vec![false; sequence.receipts.len()];
442
443 for tx in &sequence.transactions {
444 let Some(tx_hash) = tx.hash else {
445 let _ = sh_warn!("Skipping verification for transaction without a hash.");
446 continue;
447 };
448 let Some(receipt_index) =
449 take_matching_index(&sequence.receipts, &mut consumed_receipts, |receipt| {
450 receipt.transaction_hash() == tx_hash
451 })
452 else {
453 let _ = sh_warn!(
454 "Skipping verification for transaction {tx_hash}: receipt unavailable."
455 );
456 continue;
457 };
458 let receipt = &mut sequence.receipts[receipt_index];
459 let offset = if tx.is_create2()
461 && let Some(contract_address) = tx.contract_address
462 {
463 receipt.set_contract_address(contract_address);
464 32
465 } else {
466 0
467 };
468
469 if let (Some(address), Some(data)) = (receipt.contract_address(), tx.tx().input()) {
471 match verify.get_verify_args(
472 address,
473 offset,
474 data,
475 &sequence.libraries,
476 config.evm_version,
477 ) {
478 Some(verify) => verification_jobs.push(VerificationJob::Local(verify)),
479 None => unverifiable_contracts.push(address),
480 };
481 }
482
483 for AdditionalContract { address, init_code, creator_code_addresses, .. } in
485 &tx.additional_contracts
486 {
487 match verify.get_verify_args(
488 *address,
489 0,
490 init_code.as_ref(),
491 &sequence.libraries,
492 config.evm_version,
493 ) {
494 Some(args) => verification_jobs.push(VerificationJob::Local(args)),
495 None if !verify.verify_external => unverifiable_contracts.push(*address),
496 None if config.offline => {
497 skipped_external += 1;
498 if !warned_offline {
499 let _ = sh_warn!(
500 "Skipping external contract verification because offline mode is enabled."
501 );
502 warned_offline = true;
503 }
504 }
505 None if creator_code_addresses.is_empty() => {
506 skipped_external += 1;
507 let _ = sh_warn!(
508 "Skipping external verification for {address}: creator provenance is unavailable (old broadcast logs or skipped simulation)."
509 );
510 }
511 None if external_jobs >= MAX_EXTERNAL_JOBS => {
512 skipped_external += 1;
513 let _ = sh_warn!(
514 "Skipping external verification for {address}: external job limit exceeded."
515 );
516 }
517 None => {
518 external_jobs += 1;
519 match external_job(
520 &mut resolver,
521 config,
522 sequence.chain.into(),
523 &verify,
524 *address,
525 init_code,
526 creator_code_addresses,
527 receipt.transaction_hash(),
528 )
529 .await
530 {
531 Ok(job) => verification_jobs.push(job),
532 Err(reason) => {
533 skipped_external += 1;
534 let _ = sh_warn!(
535 "Skipping external verification for {address}: {reason}"
536 );
537 }
538 }
539 }
540 };
541 }
542 }
543
544 trace!(target: "script", "collected {} verification jobs and {} unverifiable contracts", verification_jobs.len(), unverifiable_contracts.len());
545
546 check_unverified(sequence, unverifiable_contracts, verify);
547
548 let num_verifications = verification_jobs.len();
549 let num_requested = num_verifications + skipped_external;
550 let mut num_of_successful_verifications = 0;
551 sh_status!("##\nStart verification for ({num_requested}) contracts")?;
552 for verification in verification_jobs {
553 match verification.run().await {
554 Ok(_) => {
555 num_of_successful_verifications += 1;
556 }
557 Err(err) => {
558 sh_err!("Failed to verify contract: {err:#}")?;
559 }
560 }
561 }
562
563 ensure_verification_complete(
564 num_of_successful_verifications,
565 num_verifications,
566 skipped_external,
567 )?;
568
569 sh_status!("All ({num_requested}) contracts were verified!")?;
570 }
571
572 Ok(())
573}
574
575fn ensure_verification_complete(
576 successful: usize,
577 submitted: usize,
578 skipped_external: usize,
579) -> Result<()> {
580 let requested = submitted + skipped_external;
581 if successful < requested {
582 let skipped = if skipped_external == 0 {
583 String::new()
584 } else {
585 format!("; {skipped_external} external verification(s) were skipped")
586 };
587 return Err(eyre!(
588 "Not all ({successful} / {requested}) contracts were verified{skipped}!"
589 ));
590 }
591 Ok(())
592}
593
594fn check_unverified<N: Network>(
595 sequence: &ScriptSequence<N>,
596 unverifiable_contracts: Vec<Address>,
597 verify: VerifyBundle,
598) {
599 if !unverifiable_contracts.is_empty() {
600 let _ = sh_warn!(
601 "We haven't found any matching bytecode for the following contracts: {:?}.\n\n\
602 This may occur when resuming a verification, but the underlying source code or compiler version has changed.\n\
603 Run `forge clean` to make sure builds are in sync with project files, then try again. Alternatively, use `forge verify-contract` to verify contracts that are already deployed.",
604 unverifiable_contracts
605 );
606
607 if let Some(commit) = &sequence.commit {
608 let current_commit = verify
609 .project_paths
610 .root
611 .map(|root| get_commit_hash(&root).unwrap_or_default())
612 .unwrap_or_default();
613
614 if ¤t_commit != commit {
615 let _ = sh_warn!(
616 "Script was broadcasted on commit `{commit}`, but we are at `{current_commit}`."
617 );
618 }
619 }
620 }
621}
622
623fn same_endpoint(left: &str, right: &str) -> bool {
624 let (Ok(left), Ok(right)) = (reqwest::Url::parse(left), reqwest::Url::parse(right)) else {
625 return false;
626 };
627 left == right
628}
629
630#[cfg(test)]
631mod tests {
632 use super::{
633 ContractsByArtifact, RetryArgs, SOURCIFY_URL, VerificationProviderType, VerifierArgs,
634 VerifyBundle, concise, ensure_verification_complete, same_endpoint, source_api_key,
635 sourcify_api_url, take_matching_index,
636 };
637 use alloy_chains::Chain;
638 use foundry_config::Config;
639
640 fn bundle(config: &Config, verifier: VerifierArgs) -> VerifyBundle {
641 let project = config.project().unwrap();
642 VerifyBundle::new(
643 &project,
644 config,
645 ContractsByArtifact::default(),
646 RetryArgs::default(),
647 verifier,
648 true,
649 )
650 }
651
652 #[test]
653 fn receipt_matching_is_hash_based_and_consumes_duplicate_hashes_in_order() {
654 let reversed = [(2, "second"), (1, "first")];
655 let mut consumed = [false; 2];
656 assert_eq!(take_matching_index(&reversed, &mut consumed, |(hash, _)| *hash == 1), Some(1));
657 assert_eq!(take_matching_index(&reversed, &mut consumed, |(hash, _)| *hash == 2), Some(0));
658 assert_eq!(consumed, [true, true]);
659
660 let batch = [(7, "first"), (7, "second")];
661 let mut consumed = [false; 2];
662 let first = take_matching_index(&batch, &mut consumed, |(hash, _)| *hash == 7).unwrap();
663 let second = take_matching_index(&batch, &mut consumed, |(hash, _)| *hash == 7).unwrap();
664 assert_eq!((batch[first].1, batch[second].1), ("first", "second"));
665 assert!(take_matching_index(&batch, &mut consumed, |(hash, _)| *hash == 7).is_none());
666 }
667
668 #[test]
669 fn source_key_reads_etherscan_config_fallback() {
670 let mut config = Config { etherscan_api_key: Some("source".into()), ..Default::default() };
671 assert_eq!(source_api_key(&config, Chain::mainnet()).as_deref(), Some("source"));
672 config.etherscan_api_key = None;
673 assert!(source_api_key(&config, Chain::mainnet()).is_none());
674 }
675
676 #[test]
677 fn source_endpoints_follow_selected_provider_privacy() {
678 let tempo = Chain::from(4217u64);
679 let config = Config { etherscan_api_key: Some("ambient".into()), ..Default::default() };
680 let mut verify = bundle(&config, VerifierArgs::default());
681 verify.set_chain(&config, tempo).unwrap();
682 assert_eq!(verify.source_sourcify_url, sourcify_api_url(tempo));
683 assert_ne!(verify.source_sourcify_url.as_deref(), Some(SOURCIFY_URL));
684 assert!(verify.source_etherscan_url.is_none());
685 assert!(verify.source_etherscan_key.is_none());
686
687 let config = Config::default();
688 let mut verify = bundle(
689 &config,
690 VerifierArgs {
691 verifier: Some(VerificationProviderType::Custom),
692 verifier_api_key: Some("private-key".into()),
693 verifier_url: Some("https://private.example/api".into()),
694 },
695 );
696 verify.set_chain(&config, Chain::mainnet()).unwrap();
697 assert!(verify.source_sourcify_url.is_none());
698 assert_eq!(verify.source_etherscan_url.as_deref(), Some("https://private.example/api"));
699
700 let mut verify = bundle(
701 &config,
702 VerifierArgs {
703 verifier: Some(VerificationProviderType::Etherscan),
704 ..Default::default()
705 },
706 );
707 verify.set_chain(&config, Chain::mainnet()).unwrap();
708 assert_eq!(verify.source_sourcify_url.as_deref(), Some(SOURCIFY_URL));
709 }
710
711 #[test]
712 fn source_endpoint_comparison_normalizes_urls_without_ignoring_routes() {
713 assert!(same_endpoint("https://CONTRACTS.tempo.xyz:443", "https://contracts.tempo.xyz/"));
714 assert!(!same_endpoint("https://contracts.tempo.xyz/api", "https://contracts.tempo.xyz/"));
715 assert!(!same_endpoint(
716 "https://contracts.tempo.xyz/?chainid=4217",
717 "https://contracts.tempo.xyz/"
718 ));
719 assert!(!same_endpoint("not a URL", "https://contracts.tempo.xyz/"));
720 }
721
722 #[test]
723 fn cli_only_etherscan_key_uses_chain_source_endpoint() {
724 let chain = Chain::mainnet();
725 let config = Config::default();
726 let mut verify = bundle(
727 &config,
728 VerifierArgs { verifier_api_key: Some("cli-key".into()), ..Default::default() },
729 );
730
731 verify.set_chain(&config, chain).unwrap();
732
733 assert_eq!(
734 verify.source_etherscan_url.as_deref(),
735 Some("https://api.etherscan.io/v2/api?chainid=1")
736 );
737 assert_eq!(verify.source_etherscan_key.as_deref(), Some("cli-key"));
738 assert_eq!(verify.etherscan.key.as_deref(), Some("cli-key"));
739 }
740
741 #[test]
742 fn chain_source_endpoint_requires_valid_etherscan_route() {
743 let config = Config::default();
744 for (chain, provider) in [
745 (Chain::mainnet(), VerificationProviderType::Custom),
746 (Chain::from(4217u64), VerificationProviderType::Etherscan),
747 ] {
748 let mut verify = bundle(
749 &config,
750 VerifierArgs {
751 verifier: Some(provider),
752 verifier_api_key: Some("private-key".into()),
753 ..Default::default()
754 },
755 );
756
757 verify.set_chain(&config, chain).unwrap();
758
759 assert!(verify.source_etherscan_url.is_none());
760 }
761 }
762
763 #[test]
764 fn skipped_external_verifications_make_the_summary_fail() {
765 let err = ensure_verification_complete(0, 0, 1).unwrap_err().to_string();
766 assert!(err.contains("0 / 1"));
767 assert!(err.contains("1 external verification(s) were skipped"));
768 ensure_verification_complete(1, 1, 0).unwrap();
769 }
770
771 #[test]
772 fn concise_sanitizes_and_bounds_remote_errors() {
773 let message = format!("remote\n\u{1b}[31m{}", "x".repeat(200));
774 let concise = concise(&message);
775 assert!(!concise.chars().any(char::is_control));
776 assert!(concise.chars().count() <= 161);
777 }
778}