1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Match {
20 Exact,
22 Partial,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Status {
29 pub address: Address,
30 pub matched: Option<Match>,
32 pub creation: Option<Match>,
33 pub runtime: Option<Match>,
34 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#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Job {
48 pub id: String,
49 pub completed: bool,
50 pub outcome: Option<Result<Status, Refusal>>,
52}
53
54#[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#[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
79pub 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#[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
169fn 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
178pub 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
190pub 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
199pub 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
215pub 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
220pub 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
234pub 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;