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, OptionExt, Result};
9use foundry_common::compile::ProjectCompiler;
10use foundry_compilers::{
11 Project,
12 artifacts::{
13 Source, StandardJsonCompilerInput, output_selection::OutputSelection, vyper::VyperInput,
14 },
15 compilers::solc::SolcCompiler,
16 multi::MultiCompilerSettings,
17 solc::{Solc, SolcLanguage},
18};
19use foundry_config::{Chain, Config, EtherscanConfigError};
20use semver::Version;
21use std::{
22 fmt,
23 path::{Path, PathBuf},
24 str::FromStr,
25};
26
27#[derive(Debug, Clone)]
29pub struct VerificationContext {
30 pub config: Config,
31 pub project: Project,
32 pub target_path: PathBuf,
33 pub target_name: String,
34 pub compiler_version: Version,
35 pub compiler_settings: MultiCompilerSettings,
36}
37
38impl VerificationContext {
39 pub fn new(
40 target_path: PathBuf,
41 target_name: String,
42 compiler_version: Version,
43 config: Config,
44 compiler_settings: MultiCompilerSettings,
45 ) -> Result<Self> {
46 let mut project = config.project()?;
47 project.no_artifacts = true;
48
49 let solc = Solc::find_or_install(&compiler_version)?;
50 project.compiler.solc = Some(SolcCompiler::Specific(solc));
51
52 Ok(Self { config, project, target_name, target_path, compiler_version, compiler_settings })
53 }
54
55 pub fn get_solc_standard_json_input(&self) -> Result<StandardJsonCompilerInput> {
56 let mut input: StandardJsonCompilerInput = self
57 .project
58 .standard_json_input(&self.target_path)
59 .wrap_err("Failed to get standard json input")?
60 .normalize_evm_version(&self.compiler_version);
61
62 let mut settings = self.compiler_settings.solc.settings.clone();
63 settings.libraries.libs = input
64 .settings
65 .libraries
66 .libs
67 .into_iter()
68 .map(|(f, libs)| {
69 (f.strip_prefix(self.project.root()).unwrap_or(&f).to_path_buf(), libs)
70 })
71 .collect();
72
73 settings.remappings = input.settings.remappings;
74 settings.sanitize(&self.compiler_version, SolcLanguage::Solidity);
75 input.settings = settings;
76
77 Ok(input)
78 }
79
80 pub fn get_vyper_standard_json_input(&self) -> Result<VyperInput> {
82 let path = Path::new(&self.target_path);
83 let sources = Source::read_all_from(path, &["vy", "vyi"])?;
84 Ok(VyperInput::new(sources, self.compiler_settings.vyper.clone(), &self.compiler_version))
85 }
86
87 pub fn get_target_abi(&self) -> Result<JsonAbi> {
89 let mut project = self.project.clone();
90 project.update_output_selection(|selection| {
91 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
92 });
93
94 let output = ProjectCompiler::new()
95 .quiet(true)
96 .files([self.target_path.clone()])
97 .compile(&project)?;
98
99 let artifact = output
100 .find(&self.target_path, &self.target_name)
101 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
102
103 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
104 }
105}
106
107#[async_trait]
109pub trait VerificationProvider {
110 async fn preflight_verify_check(
118 &mut self,
119 args: VerifyArgs,
120 context: VerificationContext,
121 ) -> Result<()>;
122
123 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()>;
125
126 async fn check(&self, args: VerifyCheckArgs) -> Result<()>;
128}
129
130impl FromStr for VerificationProviderType {
131 type Err = String;
132
133 fn from_str(s: &str) -> Result<Self, Self::Err> {
134 match s {
135 "e" | "etherscan" => Ok(Self::Etherscan),
136 "s" | "sourcify" => Ok(Self::Sourcify),
137 "b" | "blockscout" => Ok(Self::Blockscout),
138 "o" | "oklink" => Ok(Self::Oklink),
139 "c" | "custom" => Ok(Self::Custom),
140 _ => Err(format!("Unknown provider: {s}")),
141 }
142 }
143}
144
145impl fmt::Display for VerificationProviderType {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 match self {
148 Self::Etherscan => {
149 write!(f, "etherscan")?;
150 }
151 Self::Sourcify => {
152 write!(f, "sourcify")?;
153 }
154 Self::Blockscout => {
155 write!(f, "blockscout")?;
156 }
157 Self::Oklink => {
158 write!(f, "oklink")?;
159 }
160 Self::Custom => {
161 write!(f, "custom")?;
162 }
163 };
164 Ok(())
165 }
166}
167
168#[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
169pub enum VerificationProviderType {
170 Etherscan,
171 #[default]
172 Sourcify,
173 Blockscout,
174 Oklink,
175 Custom,
177}
178
179impl VerificationProviderType {
180 pub fn client(
182 &self,
183 key: Option<&str>,
184 chain: Option<Chain>,
185 has_url: bool,
186 ) -> Result<Box<dyn VerificationProvider>> {
187 let has_key = key.as_ref().is_some_and(|k| !k.is_empty());
188 if !has_key && self.is_sourcify() {
190 sh_println!(
191 "Attempting to verify on Sourcify. Pass the --etherscan-api-key <API_KEY> to verify on Etherscan, \
192 or use the --verifier flag to verify on another provider."
193 )?;
194 return Ok(Box::<SourcifyVerificationProvider>::default());
195 }
196
197 if self.is_etherscan() {
200 if let Some(chain) = chain
201 && chain.etherscan_urls().is_none()
202 && !has_url
203 {
204 eyre::bail!(EtherscanConfigError::UnknownChain(
205 "when using Etherscan verifier".to_string(),
206 chain
207 ))
208 }
209 if !has_key {
210 eyre::bail!("ETHERSCAN_API_KEY must be set to use Etherscan as a verifier")
211 }
212 return Ok(Box::<EtherscanVerificationProvider>::default());
213 }
214
215 if matches!(self, Self::Blockscout | Self::Oklink | Self::Custom) {
218 if !has_url {
219 eyre::bail!("No verifier URL specified for verifier {}", self);
220 }
221 return Ok(Box::<EtherscanVerificationProvider>::default());
222 }
223
224 if has_key {
226 return Ok(Box::<EtherscanVerificationProvider>::default());
227 }
228
229 eyre::bail!(
231 "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."
232 )
233 }
234
235 pub fn is_sourcify(&self) -> bool {
236 matches!(self, Self::Sourcify)
237 }
238
239 pub fn is_etherscan(&self) -> bool {
240 matches!(self, Self::Etherscan)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn etherscan_allows_unknown_chain_with_verifier_url() {
250 let chain = Chain::from(3658348u64);
251 let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), true);
252 assert!(res.is_ok());
253 }
254
255 #[test]
256 fn etherscan_rejects_unknown_chain_without_verifier_url() {
257 let chain = Chain::from(3658348u64);
258 let res = VerificationProviderType::Etherscan.client(Some("key"), Some(chain), false);
259 match res {
260 Ok(_) => panic!("expected unknown-chain error"),
261 Err(err) => {
262 assert!(err.to_string().contains("No known Etherscan API URL"));
263 }
264 }
265 }
266}