Skip to main content

df_elysium_core/deploy/
verify.rs

1//! Source verification on Conduit's Sourcify ([`Network::verifier_url`]), the one Elysium's
2//! explorer reads; its own reports no verification API. Sourcify's v2 API: a contract's status,
3//! a submission of its standard JSON input, and the job that verifies it. A submission is
4//! answered with a job id at once; the job finishes in seconds.
5//!
6//! Foundry does the same with `forge verify-contract --verifier sourcify --verifier-url
7//! https://contracts.conduit.xyz <address> src/<File>.sol:<Contract>`.
8
9use serde::Deserialize;
10
11use df_hyperliquid_core::client::{Http, Request, Response, TransportError};
12
13use crate::B256;
14use crate::client::Network;
15use crate::utils::{Address, Json};
16
17/// How well the deployed code matches the source.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Match {
20    /// Byte for byte, metadata hash included.
21    Exact,
22    /// All but the metadata hash.
23    Partial,
24}
25
26/// A contract's verification, as Sourcify keeps it.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Status {
29    pub address: Address,
30    /// `None` for a contract never verified.
31    pub matched: Option<Match>,
32    pub creation: Option<Match>,
33    pub runtime: Option<Match>,
34    /// ISO 8601, as Sourcify writes it.
35    pub verified_at: Option<String>,
36}
37
38impl Status {
39    #[must_use]
40    pub const fn is_verified(&self) -> bool {
41        self.matched.is_some()
42    }
43}
44
45/// A verification job.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Job {
48    pub id: String,
49    pub completed: bool,
50    /// Once completed: the contract as verified, or why not.
51    pub outcome: Option<Result<Status, Refusal>>,
52}
53
54/// Sourcify's refusal in its own words: `job_not_found`, `invalid_parameter` and the like.
55#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56#[error("{code}: {message}")]
57pub struct Refusal {
58    pub code: String,
59    pub message: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
63pub enum VerifyError {
64    #[error(transparent)]
65    Transport(#[from] TransportError),
66    #[error("Sourcify refused: {0}")]
67    Refused(Refusal),
68    #[error("Sourcify's answer could not be read: {0}")]
69    Unreadable(String),
70}
71
72/// `GET /v2/contract/{chainId}/{address}`: answered for a contract never verified too, with
73/// every match `null` (and HTTP 404).
74#[must_use]
75pub fn status_request(network: Network, address: Address) -> Request {
76    Request::get(format!("{}/v2/contract/{}/{address}", network.verifier_url(), network.chain_id()))
77}
78
79/// `POST /v2/verify/{chainId}/{address}`: the compiler's standard JSON input as written, its
80/// version (`0.8.37+commit.f401782d`), the contract (`src/Escrow.sol:Escrow`), and the creation's
81/// transaction when known, which lets the creation bytecode match too.
82pub fn submit_request(
83    network: Network,
84    address: Address,
85    std_json_input: &str,
86    compiler_version: &str,
87    contract: &str,
88    creation: Option<B256>,
89) -> Result<Request, VerifyError> {
90    let input = Json::parse(std_json_input).map_err(|error| VerifyError::Unreadable(error.to_string()))?;
91    let mut body = vec![
92        ("stdJsonInput".to_owned(), input),
93        ("compilerVersion".to_owned(), Json::String(compiler_version.to_owned())),
94        ("contractIdentifier".to_owned(), Json::String(contract.to_owned())),
95    ];
96    if let Some(hash) = creation {
97        body.push(("creationTransactionHash".to_owned(), Json::String(hash.to_hex())));
98    }
99    let url = format!("{}/v2/verify/{}/{address}", network.verifier_url(), network.chain_id());
100    Ok(Request::post_json(url, Json::Object(body).compact()))
101}
102
103/// `GET /v2/verify/{verificationId}`.
104#[must_use]
105pub fn job_request(network: Network, id: &str) -> Request {
106    Request::get(format!("{}/v2/verify/{id}", network.verifier_url()))
107}
108
109#[derive(Deserialize)]
110#[serde(rename_all = "camelCase")]
111struct StatusBody {
112    address: Address,
113    #[serde(rename = "match")]
114    matched: Option<String>,
115    creation_match: Option<String>,
116    runtime_match: Option<String>,
117    #[serde(default)]
118    verified_at: Option<String>,
119}
120
121#[derive(Deserialize)]
122#[serde(rename_all = "camelCase")]
123struct RefusalBody {
124    custom_code: String,
125    message: String,
126}
127
128#[derive(Deserialize)]
129#[serde(rename_all = "camelCase")]
130struct JobBody {
131    verification_id: String,
132    is_job_completed: bool,
133    #[serde(default)]
134    contract: Option<StatusBody>,
135    #[serde(default)]
136    error: Option<RefusalBody>,
137}
138
139#[derive(Deserialize)]
140#[serde(rename_all = "camelCase")]
141struct Submitted {
142    verification_id: String,
143}
144
145fn level(text: Option<&str>) -> Option<Match> {
146    match text {
147        Some("exact_match") => Some(Match::Exact),
148        Some("match") => Some(Match::Partial),
149        _ => None,
150    }
151}
152
153impl From<StatusBody> for Status {
154    fn from(body: StatusBody) -> Status {
155        Status {
156            address: body.address,
157            matched: level(body.matched.as_deref()),
158            creation: level(body.creation_match.as_deref()),
159            runtime: level(body.runtime_match.as_deref()),
160            verified_at: body.verified_at,
161        }
162    }
163}
164
165fn unreadable(error: &serde_json::Error, text: &str) -> VerifyError {
166    VerifyError::Unreadable(format!("{error} in {}", text.chars().take(300).collect::<String>()))
167}
168
169/// A refusal when the body is one, else the status as a transport error.
170fn refused(response: &Response) -> VerifyError {
171    let text = response.text();
172    match serde_json::from_str::<RefusalBody>(&text) {
173        Ok(body) => VerifyError::Refused(Refusal { code: body.custom_code, message: body.message }),
174        Err(_) => TransportError::Status { status: response.status, body: text }.into(),
175    }
176}
177
178/// A [`status_request`]'s answer.
179pub fn read_status(response: &Response) -> Result<Status, VerifyError> {
180    if !response.is_success() && response.status != 404 {
181        return Err(refused(response));
182    }
183    let text = response.text();
184    serde_json::from_str::<StatusBody>(&text).map(Status::from).map_err(|error| match response.status {
185        404 => refused(response),
186        _ => unreadable(&error, &text),
187    })
188}
189
190/// A [`submit_request`]'s answer: the job's id.
191pub fn read_submitted(response: &Response) -> Result<String, VerifyError> {
192    if !response.is_success() {
193        return Err(refused(response));
194    }
195    let text = response.text();
196    serde_json::from_str::<Submitted>(&text).map(|body| body.verification_id).map_err(|error| unreadable(&error, &text))
197}
198
199/// A [`job_request`]'s answer.
200pub fn read_job(response: &Response) -> Result<Job, VerifyError> {
201    if !response.is_success() {
202        return Err(refused(response));
203    }
204    let text = response.text();
205    let body: JobBody = serde_json::from_str(&text).map_err(|error| unreadable(&error, &text))?;
206    let outcome = match (body.is_job_completed, body.error, body.contract) {
207        (false, _, _) => None,
208        (true, Some(error), _) => Some(Err(Refusal { code: error.custom_code, message: error.message })),
209        (true, None, Some(contract)) => Some(Ok(contract.into())),
210        (true, None, None) => None,
211    };
212    Ok(Job { id: body.verification_id, completed: body.is_job_completed, outcome })
213}
214
215/// A contract's verification, read once.
216pub async fn status<H: Http>(http: &H, network: Network, address: Address) -> Result<Status, VerifyError> {
217    read_status(&http.send(status_request(network, address)).await?)
218}
219
220/// Submits a verification once; the job's id.
221pub async fn submit<H: Http>(
222    http: &H,
223    network: Network,
224    address: Address,
225    std_json_input: &str,
226    compiler_version: &str,
227    contract: &str,
228    creation: Option<B256>,
229) -> Result<String, VerifyError> {
230    let request = submit_request(network, address, std_json_input, compiler_version, contract, creation)?;
231    read_submitted(&http.send(request).await?)
232}
233
234/// A job, read once.
235pub async fn job<H: Http>(http: &H, network: Network, id: &str) -> Result<Job, VerifyError> {
236    read_job(&http.send(job_request(network, id)).await?)
237}
238
239#[cfg(test)]
240#[path = "tests/verify.rs"]
241mod tests;