1use crate::{
2 etherscan::EtherscanVerificationProvider,
3 sourcify::SourcifyVerificationProvider,
4 verify::{VerifyArgs, VerifyCheckArgs},
5};
6use alloy_json_abi::JsonAbi;
7use async_trait::async_trait;
8use eyre::{Context, Result};
9use foundry_common::compile::compile_target_abi;
10use foundry_compilers::{
11 Project,
12 artifacts::{Source, StandardJsonCompilerInput, vyper::VyperInput},
13 compilers::solc::SolcCompiler,
14 multi::MultiCompilerSettings,
15 solc::{Solc, SolcLanguage},
16};
17use foundry_config::{Chain, Config, EtherscanConfigError};
18use semver::Version;
19use std::{
20 fmt,
21 future::Future as StdFuture,
22 path::{Path, PathBuf},
23 pin::Pin,
24 str::FromStr,
25 sync::Arc,
26};
27
28#[derive(Debug, Clone)]
30pub struct VerificationContext {
31 pub config: Config,
32 pub project: Project,
33 pub target_path: PathBuf,
34 pub target_name: String,
35 pub compiler_version: Version,
36 pub compiler_settings: MultiCompilerSettings,
37}
38
39#[derive(Debug, Clone)]
41pub struct ExternalVerificationContext {
42 pub config: Config,
43 pub compiler_version: Version,
44 pub standard_json_input: Arc<serde_json::Value>,
45 pub target: String,
46}
47
48impl VerificationContext {
49 pub fn new(
50 target_path: PathBuf,
51 target_name: String,
52 compiler_version: Version,
53 config: Config,
54 compiler_settings: MultiCompilerSettings,
55 ) -> Result<Self> {
56 let mut project = config.project()?;
57 project.no_artifacts = true;
58
59 let solc = Solc::find_or_install(&compiler_version)?;
60 project.compiler.solc = Some(SolcCompiler::Specific(solc));
61
62 Ok(Self { config, project, target_name, target_path, compiler_version, compiler_settings })
63 }
64
65 pub fn get_solc_standard_json_input(&self) -> Result<StandardJsonCompilerInput> {
66 let mut input: StandardJsonCompilerInput = self
67 .project
68 .standard_json_input(&self.target_path)
69 .wrap_err("Failed to get standard json input")?
70 .normalize_evm_version(&self.compiler_version);
71
72 let mut settings = self.compiler_settings.solc.settings.clone();
73 settings.libraries.libs = input
74 .settings
75 .libraries
76 .libs
77 .into_iter()
78 .map(|(f, libs)| {
79 (f.strip_prefix(self.project.root()).unwrap_or(&f).to_path_buf(), libs)
80 })
81 .collect();
82
83 settings.remappings = input.settings.remappings;
84 settings.sanitize(&self.compiler_version, SolcLanguage::Solidity);
85 input.settings = settings;
86
87 Ok(input)
88 }
89
90 pub fn get_vyper_standard_json_input(&self) -> Result<VyperInput> {
92 let path = Path::new(&self.target_path);
93 let sources = Source::read_all_from(path, &["vy", "vyi"])?;
94 Ok(VyperInput::new(sources, self.compiler_settings.vyper.clone(), &self.compiler_version))
95 }
96
97 pub fn get_target_abi(&self) -> Result<JsonAbi> {
99 let mut project = self.project.clone();
100 compile_target_abi(&mut project, &self.target_path, &self.target_name)
101 }
102}
103
104#[async_trait]
106pub trait VerificationProvider {
107 fn provider_type(&self) -> VerificationProviderType;
109
110 async fn preflight_verify_check(
118 &mut self,
119 args: VerifyArgs,
120 context: VerificationContext,
121 ) -> Result<()>;
122
123 async fn submit(
129 &mut self,
130 args: VerifyArgs,
131 context: VerificationContext,
132 ) -> Result<Option<VerifyCheckArgs>>;
133
134 fn submit_external(
136 &mut self,
137 _args: VerifyArgs,
138 _context: ExternalVerificationContext,
139 ) -> Pin<Box<dyn StdFuture<Output = Result<Option<VerifyCheckArgs>>> + '_>> {
140 Box::pin(async {
141 Err(eyre::eyre!("external verification is unsupported by this provider"))
142 })
143 }
144
145 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
148 let watch = args.watch;
149 let check_args = self.submit(args, context).await?;
150 if watch && let Some(check_args) = check_args {
151 return self.check(check_args).await;
152 }
153 Ok(())
154 }
155
156 async fn check(&self, args: VerifyCheckArgs) -> Result<()>;
158}
159
160impl FromStr for VerificationProviderType {
161 type Err = String;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 match s {
165 "e" | "etherscan" => Ok(Self::Etherscan),
166 "s" | "sourcify" => Ok(Self::Sourcify),
167 "b" | "blockscout" => Ok(Self::Blockscout),
168 "o" | "oklink" => Ok(Self::Oklink),
169 "c" | "custom" => Ok(Self::Custom),
170 _ => Err(format!("Unknown provider: {s}")),
171 }
172 }
173}
174
175impl fmt::Display for VerificationProviderType {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self {
178 Self::Etherscan => {
179 write!(f, "etherscan")?;
180 }
181 Self::Sourcify => {
182 write!(f, "sourcify")?;
183 }
184 Self::Blockscout => {
185 write!(f, "blockscout")?;
186 }
187 Self::Oklink => {
188 write!(f, "oklink")?;
189 }
190 Self::Custom => {
191 write!(f, "custom")?;
192 }
193 };
194 Ok(())
195 }
196}
197
198#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
199pub enum VerificationProviderType {
200 Etherscan,
201 #[default]
202 Sourcify,
203 Blockscout,
204 Oklink,
205 Custom,
207}
208
209impl VerificationProviderType {
210 pub fn client(
216 &self,
217 key: Option<&str>,
218 chain: Option<Chain>,
219 has_url: bool,
220 is_explicit: bool,
221 ) -> Result<Box<dyn VerificationProvider>> {
222 let has_key = key.is_some_and(|k| !k.is_empty());
223
224 if is_explicit && self.is_sourcify() {
226 return Ok(Box::<SourcifyVerificationProvider>::default());
227 }
228
229 if self.is_etherscan() {
231 if let Some(chain) = chain
232 && (chain.etherscan_urls().is_none() || chain.is_custom_sourcify())
233 && !has_url
234 {
235 eyre::bail!(EtherscanConfigError::UnknownChain(
236 "when using Etherscan verifier".to_string(),
237 chain
238 ));
239 }
240 if !has_key {
241 eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier");
242 }
243 return Ok(Box::<EtherscanVerificationProvider>::default());
244 }
245
246 if is_explicit && matches!(self, Self::Blockscout | Self::Oklink | Self::Custom) {
248 if !has_url {
249 eyre::bail!("No verifier URL specified for verifier {}", self);
250 }
251 return Ok(Box::<EtherscanVerificationProvider>::default());
252 }
253
254 if has_key {
259 if let Some(chain) = chain
260 && (chain.is_custom_sourcify() || chain.etherscan_urls().is_none() && !has_url)
261 {
262 if chain.is_custom_sourcify() {
263 sh_warn!(
264 "ETHERSCAN_API_KEY is set but chain {chain} uses a Sourcify-compatible \
265 API. Falling back to Sourcify. Pass `--verifier sourcify` to suppress \
266 this warning."
267 )?;
268 } else {
269 sh_warn!(
270 "ETHERSCAN_API_KEY is set but chain {chain} has no known Etherscan API \
271 URL. Falling back to Sourcify. Pass --verifier-url <URL> or \
272 `--verifier <provider>` to override."
273 )?;
274 }
275 } else {
277 return Ok(Box::<EtherscanVerificationProvider>::default());
278 }
279 }
280
281 if self.is_sourcify() {
283 return Ok(Box::<SourcifyVerificationProvider>::default());
284 }
285
286 eyre::bail!(
288 "No valid verification provider specified. Pass the --verifier flag to specify a provider or set the ETHERSCAN_API_KEY environment variable to use Etherscan as a verifier."
289 );
290 }
291
292 pub const fn is_sourcify(&self) -> bool {
293 matches!(self, Self::Sourcify)
294 }
295
296 pub const fn is_etherscan(&self) -> bool {
297 matches!(self, Self::Etherscan)
298 }
299
300 pub const fn is_custom(&self) -> bool {
301 matches!(self, Self::Custom)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn etherscan_allows_unknown_chain_with_verifier_url() {
311 let chain = Chain::from(3658348u64);
312 let provider = VerificationProviderType::Etherscan
313 .client(Some("key"), Some(chain), true, true)
314 .unwrap();
315 assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
316 }
317
318 #[test]
319 fn etherscan_rejects_unknown_chain_without_verifier_url() {
320 let chain = Chain::from(3658348u64);
321 let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), false, true);
322 match res {
323 Ok(_) => panic!("expected unknown-chain error"),
324 Err(err) => {
325 assert!(err.to_string().contains("No known Etherscan API URL"));
326 }
327 }
328 }
329
330 #[test]
333 fn explicit_etherscan_on_custom_sourcify_chain_without_url_bails() {
334 let tempo = Chain::from(4217u64); let res = VerificationProviderType::Etherscan.client(Some("key"), Some(tempo), false, true);
336 assert!(res.is_err(), "expected error for Etherscan on custom-Sourcify chain w/o URL");
337 }
338
339 #[test]
341 fn explicit_etherscan_on_custom_sourcify_chain_with_url_is_ok() {
342 let tempo = Chain::from(4217u64);
343 let provider = VerificationProviderType::Etherscan
344 .client(Some("key"), Some(tempo), true, true)
345 .unwrap();
346 assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
347 }
348
349 #[test]
352 fn implicit_etherscan_custom_sourcify_chain_falls_back_to_sourcify() {
353 let provider = VerificationProviderType::Sourcify
355 .client(Some("mykey"), Some(Chain::mainnet()), false, false)
356 .unwrap();
357 assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
358
359 let provider = VerificationProviderType::Sourcify
361 .client(Some("mykey"), Some(Chain::from(4217u64)), false, false)
362 .expect("expected fallback to Sourcify, got error");
363 assert_eq!(provider.provider_type(), VerificationProviderType::Sourcify);
364
365 let provider = VerificationProviderType::Sourcify
367 .client(Some("mykey"), Some(Chain::from(4217u64)), true, false)
368 .expect("expected fallback to Sourcify, got error");
369 assert_eq!(provider.provider_type(), VerificationProviderType::Sourcify);
370 }
371
372 #[test]
377 fn implicit_etherscan_unknown_chain_falls_back_to_sourcify() {
378 let provider = VerificationProviderType::Sourcify
380 .client(Some("mykey"), Some(Chain::mainnet()), false, false)
381 .unwrap();
382 assert_eq!(provider.provider_type(), VerificationProviderType::Etherscan);
383
384 let provider = VerificationProviderType::Sourcify
386 .client(Some("mykey"), Some(Chain::from(3658348u64)), false, false)
387 .expect("expected fallback to Sourcify, got error");
388 assert_eq!(provider.provider_type(), VerificationProviderType::Sourcify);
389 }
390}