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