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#[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 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}