forge_verify/
sourcify.rs

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/// The type that can verify a contract on `sourcify`
24#[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        // note(onbjerg): a little ugly but makes this infallible as we guarantee `SOURCIFY_URL` to
193        // be well formatted
194        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    /// Configures the API request to the sourcify API using the given [`VerifyArgs`].
222    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                // remove all incompatible settings
261                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}