Skip to main content

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 futures::FutureExt;
12use reqwest::StatusCode;
13use serde::{Deserialize, Serialize};
14use url::Url;
15
16pub static SOURCIFY_URL: &str = "https://sourcify.dev/server/";
17
18/// The type that can verify a contract on `sourcify`
19#[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        // note(onbjerg): a little ugly but makes this infallible as we guarantee `SOURCIFY_URL` to
188        // be well formatted
189        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    /// Configures the API request to the sourcify API using the given [`VerifyArgs`].
217    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}