1use crate::{
2 provider::{VerificationContext, VerificationProvider},
3 retry::RETRY_CHECK_ON_VERIFY,
4 utils::ensure_solc_build_metadata,
5 verify::{ContractLanguage, VerifyArgs, VerifyCheckArgs},
6};
7use alloy_primitives::Address;
8use async_trait::async_trait;
9use eyre::{Context, Result, eyre};
10use foundry_common::retry::RetryError;
11use foundry_compilers::{
12 artifacts::{Source, StandardJsonCompilerInput, vyper::VyperInput},
13 solc::SolcLanguage,
14};
15use futures::FutureExt;
16use reqwest::StatusCode;
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19
20pub static SOURCIFY_URL: &str = "https://sourcify.dev/server/";
21
22#[derive(Clone, Debug, Default)]
24#[non_exhaustive]
25pub struct SourcifyVerificationProvider;
26
27#[async_trait]
28impl VerificationProvider for SourcifyVerificationProvider {
29 async fn preflight_verify_check(
30 &mut self,
31 args: VerifyArgs,
32 context: VerificationContext,
33 ) -> Result<()> {
34 let _ = self.prepare_verify_request(&args, &context).await?;
35 Ok(())
36 }
37
38 async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
39 let body = self.prepare_verify_request(&args, &context).await?;
40 let chain_id = args.etherscan.chain.unwrap_or_default().id();
41
42 if !args.skip_is_verified_check && self.is_contract_verified(&args).await? {
43 sh_println!(
44 "\nContract [{}] {:?} is already verified. Skipping verification.",
45 context.target_name,
46 args.address.to_string()
47 )?;
48
49 return Ok(());
50 }
51
52 trace!("submitting verification request {:?}", body);
53
54 let client = reqwest::Client::new();
55 let url =
56 Self::get_verify_url(args.verifier.verifier_url.as_deref(), chain_id, args.address);
57
58 let resp = args
59 .retry
60 .into_retry()
61 .run_async(|| {
62 async {
63 sh_println!(
64 "\nSubmitting verification for [{}] {:?}.",
65 context.target_name,
66 args.address.to_string()
67 )?;
68 let response = client
69 .post(&url)
70 .header("Content-Type", "application/json")
71 .body(serde_json::to_string(&body)?)
72 .send()
73 .await?;
74
75 let status = response.status();
76 match status {
77 StatusCode::CONFLICT => {
78 sh_println!("Contract source code already fully verified")?;
79 Ok(None)
80 }
81 StatusCode::ACCEPTED => {
82 let text = response.text().await?;
83 let verify_response: SourcifyVerificationResponse =
84 serde_json::from_str(&text)
85 .wrap_err("Failed to parse Sourcify verification response")?;
86 Ok(Some(verify_response))
87 }
88 _ => {
89 let error: serde_json::Value = response.json().await?;
90 eyre::bail!(
91 "Sourcify verification request for address ({}) \
92 failed with status code {status}\n\
93 Details: {error:#}",
94 args.address,
95 );
96 }
97 }
98 }
99 .boxed()
100 })
101 .await?;
102
103 if let Some(resp) = resp {
104 let job_url = Self::get_job_status_url(
105 args.verifier.verifier_url.as_deref(),
106 resp.verification_id.clone(),
107 );
108 sh_println!(
109 "Submitted contract for verification:\n\tVerification Job ID: `{}`\n\tURL: {}",
110 resp.verification_id,
111 job_url
112 )?;
113
114 if args.watch {
115 let check_args = VerifyCheckArgs {
116 id: resp.verification_id,
117 etherscan: args.etherscan,
118 retry: RETRY_CHECK_ON_VERIFY,
119 verifier: args.verifier,
120 };
121 return self.check(check_args).await;
122 }
123 }
124
125 Ok(())
126 }
127
128 async fn check(&self, args: VerifyCheckArgs) -> Result<()> {
129 let url = Self::get_job_status_url(args.verifier.verifier_url.as_deref(), args.id.clone());
130
131 args.retry
132 .into_retry()
133 .run_async_until_break(|| async {
134 let response = reqwest::get(&url)
135 .await
136 .wrap_err("Failed to request verification status")
137 .map_err(RetryError::Retry)?;
138
139 if response.status() == StatusCode::NOT_FOUND {
140 return Err(RetryError::Break(eyre!(
141 "No verification job found for ID {}",
142 args.id
143 )));
144 }
145
146 if !response.status().is_success() {
147 return Err(RetryError::Retry(eyre!(
148 "Failed to request verification status with status code {}",
149 response.status()
150 )));
151 }
152
153 let job_response: SourcifyJobResponse = response
154 .json()
155 .await
156 .wrap_err("Failed to parse job response")
157 .map_err(RetryError::Retry)?;
158
159 if !job_response.is_job_completed {
160 return Err(RetryError::Retry(eyre!("Verification is still pending...")));
161 }
162
163 if let Some(error) = job_response.error {
164 if error.custom_code == "already_verified" {
165 let _ = sh_println!("Contract source code already verified");
166 return Ok(());
167 }
168
169 return Err(RetryError::Break(eyre!(
170 "Verification job failed:\nError Code: `{}`\nMessage: `{}`",
171 error.custom_code,
172 error.message
173 )));
174 }
175
176 if let Some(contract_status) = job_response.contract.match_status {
177 let _ = sh_println!(
178 "Contract successfully verified:\nStatus: `{}`",
179 contract_status,
180 );
181 }
182 Ok(())
183 })
184 .await
185 .wrap_err("Checking verification result failed")
186 }
187}
188
189impl SourcifyVerificationProvider {
190 fn get_base_url(verifier_url: Option<&str>) -> &str {
191 verifier_url.unwrap_or(SOURCIFY_URL)
192 }
193
194 fn get_verify_url(
195 verifier_url: Option<&str>,
196 chain_id: u64,
197 contract_address: Address,
198 ) -> String {
199 let base_url = Self::get_base_url(verifier_url);
200 format!("{base_url}v2/verify/{chain_id}/{contract_address}")
201 }
202
203 fn get_job_status_url(verifier_url: Option<&str>, job_id: String) -> String {
204 let base_url = Self::get_base_url(verifier_url);
205 format!("{base_url}v2/verify/{job_id}")
206 }
207
208 fn get_lookup_url(
209 verifier_url: Option<&str>,
210 chain_id: u64,
211 contract_address: Address,
212 ) -> String {
213 let base_url = Self::get_base_url(verifier_url);
214 format!("{base_url}v2/contract/{chain_id}/{contract_address}")
215 }
216
217 async fn prepare_verify_request(
219 &self,
220 args: &VerifyArgs,
221 context: &VerificationContext,
222 ) -> Result<SourcifyVerifyRequest> {
223 let lang = args.detect_language(context);
224 let contract_identifier = format!(
225 "{}:{}",
226 context
227 .target_path
228 .strip_prefix(context.project.root())
229 .unwrap_or(context.target_path.as_path())
230 .display(),
231 context.target_name
232 );
233 let creation_transaction_hash = args.creation_transaction_hash.map(|h| h.to_string());
234
235 match lang {
236 ContractLanguage::Solidity => {
237 let mut input: StandardJsonCompilerInput = context
238 .project
239 .standard_json_input(&context.target_path)
240 .wrap_err("Failed to get standard json input")?
241 .normalize_evm_version(&context.compiler_version);
242
243 let mut settings = context.compiler_settings.solc.settings.clone();
244 settings.libraries.libs = input
245 .settings
246 .libraries
247 .libs
248 .into_iter()
249 .map(|(f, libs)| {
250 (f.strip_prefix(context.project.root()).unwrap_or(&f).to_path_buf(), libs)
251 })
252 .collect();
253
254 settings.remappings = input.settings.remappings;
255
256 settings.sanitize(&context.compiler_version, SolcLanguage::Solidity);
258
259 input.settings = settings;
260
261 let std_json_input = serde_json::to_value(&input)
262 .wrap_err("Failed to serialize standard json input")?;
263 let compiler_version =
264 ensure_solc_build_metadata(context.compiler_version.clone()).await?.to_string();
265
266 Ok(SourcifyVerifyRequest {
267 std_json_input,
268 compiler_version,
269 contract_identifier,
270 creation_transaction_hash,
271 })
272 }
273 ContractLanguage::Vyper => {
274 let path = Path::new(&context.target_path);
275 let sources = Source::read_all_from(path, &["vy", "vyi"])?;
276 let input = VyperInput::new(
277 sources,
278 context.clone().compiler_settings.vyper,
279 &context.compiler_version,
280 );
281 let std_json_input = serde_json::to_value(&input)
282 .wrap_err("Failed to serialize vyper json input")?;
283
284 let compiler_version = context.compiler_version.to_string();
285
286 Ok(SourcifyVerifyRequest {
287 std_json_input,
288 compiler_version,
289 contract_identifier,
290 creation_transaction_hash,
291 })
292 }
293 }
294 }
295
296 async fn is_contract_verified(&self, args: &VerifyArgs) -> Result<bool> {
297 let chain_id = args.etherscan.chain.unwrap_or_default().id();
298 let url =
299 Self::get_lookup_url(args.verifier.verifier_url.as_deref(), chain_id, args.address);
300
301 match reqwest::get(&url).await {
302 Ok(response) => {
303 if response.status().is_success() {
304 let contract_response: SourcifyContractResponse =
305 response.json().await.wrap_err("Failed to parse contract response")?;
306
307 let creation_exact = contract_response
308 .creation_match
309 .as_ref()
310 .map(|s| s == "exact_match")
311 .unwrap_or(false);
312
313 let runtime_exact = contract_response
314 .runtime_match
315 .as_ref()
316 .map(|s| s == "exact_match")
317 .unwrap_or(false);
318
319 Ok(creation_exact && runtime_exact)
320 } else {
321 Ok(false)
322 }
323 }
324 Err(error) => Err(error).wrap_err_with(|| {
325 format!("Failed to query verification status for {}", args.address)
326 }),
327 }
328 }
329}
330
331#[derive(Debug, Serialize)]
332#[serde(rename_all = "camelCase")]
333pub struct SourcifyVerifyRequest {
334 std_json_input: serde_json::Value,
335 compiler_version: String,
336 contract_identifier: String,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 creation_transaction_hash: Option<String>,
339}
340
341#[derive(Debug, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct SourcifyVerificationResponse {
344 verification_id: String,
345}
346
347#[derive(Debug, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct SourcifyJobResponse {
350 is_job_completed: bool,
351 contract: SourcifyContractResponse,
352 error: Option<SourcifyErrorResponse>,
353}
354
355#[derive(Debug, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct SourcifyContractResponse {
358 #[serde(rename = "match")]
359 match_status: Option<String>,
360 creation_match: Option<String>,
361 runtime_match: Option<String>,
362}
363
364#[derive(Debug, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct SourcifyErrorResponse {
367 custom_code: String,
368 message: String,
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use clap::Parser;
375 use foundry_test_utils::forgetest_async;
376
377 forgetest_async!(creates_correct_verify_request_body, |prj, _cmd| {
378 prj.add_source("Counter", "contract Counter {}");
379
380 let args = VerifyArgs::parse_from([
381 "foundry-cli",
382 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
383 "src/Counter.sol:Counter",
384 "--compiler-version",
385 "0.8.19",
386 "--root",
387 &prj.root().to_string_lossy(),
388 ]);
389
390 let context = args.resolve_context().await.unwrap();
391 let provider = SourcifyVerificationProvider::default();
392 let request = provider.prepare_verify_request(&args, &context).await.unwrap();
393
394 assert_eq!(request.compiler_version, "0.8.19+commit.7dd6d404");
395 assert_eq!(request.contract_identifier, "src/Counter.sol:Counter");
396 assert!(request.creation_transaction_hash.is_none());
397
398 assert!(request.std_json_input.is_object());
399 let json_obj = request.std_json_input.as_object().unwrap();
400 assert!(json_obj.contains_key("sources"));
401 assert!(json_obj.contains_key("settings"));
402
403 let sources = json_obj.get("sources").unwrap().as_object().unwrap();
404 assert!(sources.contains_key("src/Counter.sol"));
405 let counter_source = sources.get("src/Counter.sol").unwrap().as_object().unwrap();
406 let content = counter_source.get("content").unwrap().as_str().unwrap();
407 assert!(content.contains("contract Counter {}"));
408 });
409}