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 verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
38 let body = self.prepare_verify_request(&args, &context).await?;
39 let chain_id = args.etherscan.chain.unwrap_or_default().id();
40
41 if !args.skip_is_verified_check && self.is_contract_verified(&args).await? {
42 sh_status!(
43 "Contract [{}] {:?} is already verified. Skipping verification.",
44 context.target_name,
45 args.address.to_string()
46 )?;
47
48 return Ok(());
49 }
50
51 trace!("submitting verification request {:?}", body);
52
53 let client = reqwest::Client::new();
54 let url =
55 Self::get_verify_url(args.verifier.verifier_url.as_deref(), chain_id, args.address);
56
57 let resp = args
58 .retry
59 .into_retry()
60 .run_async(|| {
61 async {
62 sh_status!(
63 "Submitting verification for [{}] {:?}.",
64 context.target_name,
65 args.address.to_string()
66 )?;
67 let response = client
68 .post(&url)
69 .header("Content-Type", "application/json")
70 .body(serde_json::to_string(&body)?)
71 .send()
72 .await?;
73
74 let status = response.status();
75 match status {
76 StatusCode::CONFLICT => {
77 sh_status!("Contract source code already fully verified")?;
78 Ok(None)
79 }
80 StatusCode::ACCEPTED => {
81 let text = response.text().await?;
82 let verify_response: SourcifyVerificationResponse =
83 serde_json::from_str(&text)
84 .wrap_err("Failed to parse Sourcify verification response")?;
85 Ok(Some(verify_response))
86 }
87 _ => {
88 let error: serde_json::Value = response.json().await?;
89 eyre::bail!(
90 "Sourcify verification request for address ({}) \
91 failed with status code {status}\n\
92 Details: {error:#}",
93 args.address,
94 );
95 }
96 }
97 }
98 .boxed()
99 })
100 .await?;
101
102 if let Some(resp) = resp {
103 let job_url = Self::get_job_status_url(
104 args.verifier.verifier_url.as_deref(),
105 resp.verification_id.clone(),
106 );
107 sh_status!(
108 "Submitted contract for verification:\n\tVerification Job ID: `{}`\n\tURL: {}",
109 resp.verification_id,
110 job_url
111 )?;
112 sh_println!("{}\t{}", resp.verification_id, job_url)?;
113
114 if args.watch {
115 let check_args = VerifyCheckArgs {
116 id: resp.verification_id,
117 etherscan: args.etherscan,
118 retry: args.retry,
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_status!("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_status!(
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>) -> Url {
191 Url::parse(verifier_url.unwrap_or(SOURCIFY_URL))
194 .unwrap_or_else(|_| Url::parse(SOURCIFY_URL).unwrap())
195 }
196
197 fn get_verify_url(
198 verifier_url: Option<&str>,
199 chain_id: u64,
200 contract_address: Address,
201 ) -> String {
202 let base_url = Self::get_base_url(verifier_url);
203 format!("{base_url}v2/verify/{chain_id}/{contract_address}")
204 }
205
206 fn get_job_status_url(verifier_url: Option<&str>, job_id: String) -> String {
207 let base_url = Self::get_base_url(verifier_url);
208 format!("{base_url}v2/verify/{job_id}")
209 }
210
211 fn get_lookup_url(
212 verifier_url: Option<&str>,
213 chain_id: u64,
214 contract_address: Address,
215 ) -> String {
216 let base_url = Self::get_base_url(verifier_url);
217 format!("{base_url}v2/contract/{chain_id}/{contract_address}")
218 }
219
220 async fn prepare_verify_request(
222 &self,
223 args: &VerifyArgs,
224 context: &VerificationContext,
225 ) -> Result<SourcifyVerifyRequest> {
226 let lang = args.detect_language(context);
227 let contract_identifier = format!(
228 "{}:{}",
229 context
230 .target_path
231 .strip_prefix(context.project.root())
232 .unwrap_or(context.target_path.as_path())
233 .display(),
234 context.target_name
235 );
236 let creation_transaction_hash = args.creation_transaction_hash.map(|h| h.to_string());
237
238 match lang {
239 ContractLanguage::Solidity => {
240 let input = context.get_solc_standard_json_input()?;
241
242 let std_json_input = serde_json::to_value(&input)
243 .wrap_err("Failed to serialize standard json input")?;
244 let compiler_version =
245 ensure_solc_build_metadata(context.compiler_version.clone()).await?.to_string();
246
247 Ok(SourcifyVerifyRequest {
248 std_json_input,
249 compiler_version,
250 contract_identifier,
251 creation_transaction_hash,
252 })
253 }
254 ContractLanguage::Vyper => {
255 let input = context.get_vyper_standard_json_input()?;
256 let std_json_input = serde_json::to_value(&input)
257 .wrap_err("Failed to serialize vyper json input")?;
258
259 let compiler_version = context.compiler_version.to_string();
260
261 Ok(SourcifyVerifyRequest {
262 std_json_input,
263 compiler_version,
264 contract_identifier,
265 creation_transaction_hash,
266 })
267 }
268 }
269 }
270
271 async fn is_contract_verified(&self, args: &VerifyArgs) -> Result<bool> {
272 let chain_id = args.etherscan.chain.unwrap_or_default().id();
273 let url =
274 Self::get_lookup_url(args.verifier.verifier_url.as_deref(), chain_id, args.address);
275
276 match reqwest::get(&url).await {
277 Ok(response) => {
278 if response.status().is_success() {
279 let contract_response: SourcifyContractResponse =
280 response.json().await.wrap_err("Failed to parse contract response")?;
281
282 let creation_exact = contract_response
283 .creation_match
284 .as_ref()
285 .map(|s| s == "exact_match")
286 .unwrap_or(false);
287
288 let runtime_exact = contract_response
289 .runtime_match
290 .as_ref()
291 .map(|s| s == "exact_match")
292 .unwrap_or(false);
293
294 Ok(creation_exact && runtime_exact)
295 } else {
296 Ok(false)
297 }
298 }
299 Err(error) => Err(error).wrap_err_with(|| {
300 format!("Failed to query verification status for {}", args.address)
301 }),
302 }
303 }
304}
305
306#[derive(Debug, Serialize)]
307#[serde(rename_all = "camelCase")]
308pub struct SourcifyVerifyRequest {
309 std_json_input: serde_json::Value,
310 compiler_version: String,
311 contract_identifier: String,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 creation_transaction_hash: Option<String>,
314}
315
316#[derive(Debug, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct SourcifyVerificationResponse {
319 verification_id: String,
320}
321
322#[derive(Debug, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct SourcifyJobResponse {
325 is_job_completed: bool,
326 contract: SourcifyContractResponse,
327 error: Option<SourcifyErrorResponse>,
328}
329
330#[derive(Debug, Deserialize)]
331#[serde(rename_all = "camelCase")]
332pub struct SourcifyContractResponse {
333 #[serde(rename = "match")]
334 match_status: Option<String>,
335 creation_match: Option<String>,
336 runtime_match: Option<String>,
337}
338
339#[derive(Debug, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct SourcifyErrorResponse {
342 custom_code: String,
343 message: String,
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use clap::Parser;
350 use foundry_test_utils::forgetest_async;
351
352 forgetest_async!(creates_correct_verify_request_body, |prj, _cmd| {
353 prj.add_source("Counter", "contract Counter {}");
354
355 let args = VerifyArgs::parse_from([
356 "foundry-cli",
357 "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
358 "src/Counter.sol:Counter",
359 "--compiler-version",
360 "0.8.19",
361 "--root",
362 &prj.root().to_string_lossy(),
363 ]);
364
365 let context = args.resolve_context().await.unwrap();
366 let provider = SourcifyVerificationProvider::default();
367 let request = provider.prepare_verify_request(&args, &context).await.unwrap();
368
369 assert_eq!(request.compiler_version, "0.8.19+commit.7dd6d404");
370 assert_eq!(request.contract_identifier, "src/Counter.sol:Counter");
371 assert!(request.creation_transaction_hash.is_none());
372
373 assert!(request.std_json_input.is_object());
374 let json_obj = request.std_json_input.as_object().unwrap();
375 assert!(json_obj.contains_key("sources"));
376 assert!(json_obj.contains_key("settings"));
377
378 let sources = json_obj.get("sources").unwrap().as_object().unwrap();
379 assert!(sources.contains_key("src/Counter.sol"));
380 let counter_source = sources.get("src/Counter.sol").unwrap().as_object().unwrap();
381 let content = counter_source.get("content").unwrap().as_str().unwrap();
382 assert!(content.contains("contract Counter {}"));
383 });
384}