forge_verify/
sourcify.rs

1use crate::{
2    provider::{VerificationContext, VerificationProvider},
3    verify::{VerifyArgs, VerifyCheckArgs},
4};
5use alloy_primitives::map::HashMap;
6use async_trait::async_trait;
7use eyre::Result;
8use foundry_common::fs;
9use futures::FutureExt;
10use reqwest::Url;
11use serde::{Deserialize, Serialize};
12use std::str::FromStr;
13
14pub static SOURCIFY_URL: &str = "https://sourcify.dev/server/";
15
16/// The type that can verify a contract on `sourcify`
17#[derive(Clone, Debug, Default)]
18#[non_exhaustive]
19pub struct SourcifyVerificationProvider;
20
21#[async_trait]
22impl VerificationProvider for SourcifyVerificationProvider {
23    async fn preflight_verify_check(
24        &mut self,
25        args: VerifyArgs,
26        context: VerificationContext,
27    ) -> Result<()> {
28        let _ = self.prepare_request(&args, &context)?;
29        Ok(())
30    }
31
32    async fn verify(&mut self, args: VerifyArgs, context: VerificationContext) -> Result<()> {
33        let body = self.prepare_request(&args, &context)?;
34
35        trace!("submitting verification request {:?}", body);
36
37        let client = reqwest::Client::new();
38
39        let resp = args
40            .retry
41            .into_retry()
42            .run_async(|| {
43                async {
44                    sh_println!(
45                        "\nSubmitting verification for [{}] {:?}.",
46                        context.target_name,
47                        args.address.to_string()
48                    )?;
49                    let response = client
50                        .post(args.verifier.verifier_url.as_deref().unwrap_or(SOURCIFY_URL))
51                        .header("Content-Type", "application/json")
52                        .body(serde_json::to_string(&body)?)
53                        .send()
54                        .await?;
55
56                    let status = response.status();
57                    if !status.is_success() {
58                        let error: serde_json::Value = response.json().await?;
59                        eyre::bail!(
60                            "Sourcify verification request for address ({}) \
61                             failed with status code {status}\n\
62                             Details: {error:#}",
63                            args.address,
64                        );
65                    }
66
67                    let text = response.text().await?;
68                    Ok(Some(serde_json::from_str::<SourcifyVerificationResponse>(&text)?))
69                }
70                .boxed()
71            })
72            .await?;
73
74        self.process_sourcify_response(resp.map(|r| r.result))
75    }
76
77    async fn check(&self, args: VerifyCheckArgs) -> Result<()> {
78        let resp = args
79            .retry
80            .into_retry()
81            .run_async(|| {
82                async {
83                    let url = Url::from_str(
84                        args.verifier.verifier_url.as_deref().unwrap_or(SOURCIFY_URL),
85                    )?;
86                    let query = format!(
87                        "check-by-addresses?addresses={}&chainIds={}",
88                        args.id,
89                        args.etherscan.chain.unwrap_or_default().id(),
90                    );
91                    let url = url.join(&query)?;
92                    let response = reqwest::get(url).await?;
93                    if !response.status().is_success() {
94                        eyre::bail!(
95                            "Failed to request verification status with status code {}",
96                            response.status()
97                        );
98                    };
99
100                    Ok(Some(response.json::<Vec<SourcifyResponseElement>>().await?))
101                }
102                .boxed()
103            })
104            .await?;
105
106        self.process_sourcify_response(resp)
107    }
108}
109
110impl SourcifyVerificationProvider {
111    /// Configures the API request to the sourcify API using the given [`VerifyArgs`].
112    fn prepare_request(
113        &self,
114        args: &VerifyArgs,
115        context: &VerificationContext,
116    ) -> Result<SourcifyVerifyRequest> {
117        let metadata = context.get_target_metadata()?;
118        let imports = context.get_target_imports()?;
119
120        let mut files = HashMap::with_capacity_and_hasher(2 + imports.len(), Default::default());
121
122        let metadata = serde_json::to_string_pretty(&metadata)?;
123        files.insert("metadata.json".to_string(), metadata);
124
125        let contract_path = context.target_path.clone();
126        let filename = contract_path.file_name().unwrap().to_string_lossy().to_string();
127        files.insert(filename, fs::read_to_string(&contract_path)?);
128
129        for import in imports {
130            let import_entry = format!("{}", import.display());
131            files.insert(import_entry, fs::read_to_string(&import)?);
132        }
133
134        let req = SourcifyVerifyRequest {
135            address: args.address.to_string(),
136            chain: args.etherscan.chain.unwrap_or_default().id().to_string(),
137            files,
138            chosen_contract: None,
139        };
140
141        Ok(req)
142    }
143
144    fn process_sourcify_response(
145        &self,
146        response: Option<Vec<SourcifyResponseElement>>,
147    ) -> Result<()> {
148        let Some([response, ..]) = response.as_deref() else { return Ok(()) };
149        match response.status.as_str() {
150            "perfect" => {
151                if let Some(ts) = &response.storage_timestamp {
152                    sh_println!("Contract source code already verified. Storage Timestamp: {ts}")?;
153                } else {
154                    sh_println!("Contract successfully verified")?;
155                }
156            }
157            "partial" => {
158                sh_println!("The recompiled contract partially matches the deployed version")?;
159            }
160            "false" => sh_println!("Contract source code is not verified")?,
161            s => eyre::bail!("Unknown status from sourcify. Status: {s:?}"),
162        }
163        Ok(())
164    }
165}
166
167#[derive(Debug, Serialize)]
168pub struct SourcifyVerifyRequest {
169    address: String,
170    chain: String,
171    files: HashMap<String, String>,
172    #[serde(rename = "chosenContract", skip_serializing_if = "Option::is_none")]
173    chosen_contract: Option<String>,
174}
175
176#[derive(Debug, Deserialize)]
177pub struct SourcifyVerificationResponse {
178    result: Vec<SourcifyResponseElement>,
179}
180
181#[derive(Debug, Deserialize)]
182pub struct SourcifyResponseElement {
183    status: String,
184    #[serde(rename = "storageTimestamp")]
185    storage_timestamp: Option<String>,
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_check_addresses_url() {
194        let url = Url::from_str("https://server-verify.hashscan.io").unwrap();
195        let url = url.join("check-by-addresses?addresses=0x1234&chainIds=1").unwrap();
196        assert_eq!(
197            url.as_str(),
198            "https://server-verify.hashscan.io/check-by-addresses?addresses=0x1234&chainIds=1"
199        );
200    }
201}