Skip to main content

forge_verify/
sourcify.rs

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