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