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