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::{OptionExt, Result};
9use foundry_common::compile::ProjectCompiler;
10use foundry_compilers::{
11 Project, artifacts::output_selection::OutputSelection, compilers::solc::SolcCompiler,
12 multi::MultiCompilerSettings, solc::Solc,
13};
14use foundry_config::{Chain, Config, EtherscanConfigError};
15use semver::Version;
16use std::{fmt, path::PathBuf, str::FromStr};
17
18#[derive(Debug, Clone)]
20pub struct VerificationContext {
21 pub config: Config,
22 pub project: Project,
23 pub target_path: PathBuf,
24 pub target_name: String,
25 pub compiler_version: Version,
26 pub compiler_settings: MultiCompilerSettings,
27}
28
29impl VerificationContext {
30 pub fn new(
31 target_path: PathBuf,
32 target_name: String,
33 compiler_version: Version,
34 config: Config,
35 compiler_settings: MultiCompilerSettings,
36 ) -> Result<Self> {
37 let mut project = config.project()?;
38 project.no_artifacts = true;
39
40 let solc = Solc::find_or_install(&compiler_version)?;
41 project.compiler.solc = Some(SolcCompiler::Specific(solc));
42
43 Ok(Self { config, project, target_name, target_path, compiler_version, compiler_settings })
44 }
45
46 pub fn get_target_abi(&self) -> Result<JsonAbi> {
48 let mut project = self.project.clone();
49 project.update_output_selection(|selection| {
50 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
51 });
52
53 let output = ProjectCompiler::new()
54 .quiet(true)
55 .files([self.target_path.clone()])
56 .compile(&project)?;
57
58 let artifact = output
59 .find(&self.target_path, &self.target_name)
60 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
61
62 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
63 }
64}
65
66#[async_trait]
68pub trait VerificationProvider {
69 async fn preflight_verify_check(
77 &mut self,
78 args: VerifyArgs,
79 context: VerificationContext,
80 ) -> Result<()>;
81
82 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()>;
84
85 async fn check(&self, args: VerifyCheckArgs) -> Result<()>;
87}
88
89impl FromStr for VerificationProviderType {
90 type Err = String;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 match s {
94 "e" | "etherscan" => Ok(Self::Etherscan),
95 "s" | "sourcify" => Ok(Self::Sourcify),
96 "b" | "blockscout" => Ok(Self::Blockscout),
97 "o" | "oklink" => Ok(Self::Oklink),
98 "c" | "custom" => Ok(Self::Custom),
99 _ => Err(format!("Unknown provider: {s}")),
100 }
101 }
102}
103
104impl fmt::Display for VerificationProviderType {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Etherscan => {
108 write!(f, "etherscan")?;
109 }
110 Self::Sourcify => {
111 write!(f, "sourcify")?;
112 }
113 Self::Blockscout => {
114 write!(f, "blockscout")?;
115 }
116 Self::Oklink => {
117 write!(f, "oklink")?;
118 }
119 Self::Custom => {
120 write!(f, "custom")?;
121 }
122 };
123 Ok(())
124 }
125}
126
127#[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
128pub enum VerificationProviderType {
129 Etherscan,
130 #[default]
131 Sourcify,
132 Blockscout,
133 Oklink,
134 Custom,
136}
137
138impl VerificationProviderType {
139 pub fn client(
141 &self,
142 key: Option<&str>,
143 chain: Option<Chain>,
144 has_url: bool,
145 ) -> Result<Box<dyn VerificationProvider>> {
146 let has_key = key.as_ref().is_some_and(|k| !k.is_empty());
147 if !has_key && self.is_sourcify() {
149 sh_println!(
150 "Attempting to verify on Sourcify. Pass the --etherscan-api-key <API_KEY> to verify on Etherscan, \
151 or use the --verifier flag to verify on another provider."
152 )?;
153 return Ok(Box::<SourcifyVerificationProvider>::default());
154 }
155
156 if self.is_etherscan() {
159 if let Some(chain) = chain
160 && chain.etherscan_urls().is_none()
161 && !has_url
162 {
163 eyre::bail!(EtherscanConfigError::UnknownChain(
164 "when using Etherscan verifier".to_string(),
165 chain
166 ))
167 }
168 if !has_key {
169 eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier")
170 }
171 return Ok(Box::<EtherscanVerificationProvider>::default());
172 }
173
174 if matches!(self, Self::Blockscout | Self::Oklink | Self::Custom) {
177 if !has_url {
178 eyre::bail!("No verifier URL specified for verifier {}", self);
179 }
180 return Ok(Box::<EtherscanVerificationProvider>::default());
181 }
182
183 if has_key {
185 return Ok(Box::<EtherscanVerificationProvider>::default());
186 }
187
188 eyre::bail!(
190 "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."
191 )
192 }
193
194 pub fn is_sourcify(&self) -> bool {
195 matches!(self, Self::Sourcify)
196 }
197
198 pub fn is_etherscan(&self) -> bool {
199 matches!(self, Self::Etherscan)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn etherscan_allows_unknown_chain_with_verifier_url() {
209 let chain = Chain::from(3658348u64);
210 let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), true);
211 assert!(res.is_ok());
212 }
213
214 #[test]
215 fn etherscan_rejects_unknown_chain_without_verifier_url() {
216 let chain = Chain::from(3658348u64);
217 let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), false);
218 match res {
219 Ok(_) => panic!("expected unknown-chain error"),
220 Err(err) => {
221 assert!(err.to_string().contains("No known Etherscan API URL"));
222 }
223 }
224 }
225}