use serde::Deserialize;
use df_hyperliquid_core::client::{Http, Request, Response, TransportError};
use crate::B256;
use crate::client::Network;
use crate::utils::{Address, Json};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Match {
Exact,
Partial,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Status {
pub address: Address,
pub matched: Option<Match>,
pub creation: Option<Match>,
pub runtime: Option<Match>,
pub verified_at: Option<String>,
}
impl Status {
#[must_use]
pub const fn is_verified(&self) -> bool {
self.matched.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Job {
pub id: String,
pub completed: bool,
pub outcome: Option<Result<Status, Refusal>>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{code}: {message}")]
pub struct Refusal {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum VerifyError {
#[error(transparent)]
Transport(#[from] TransportError),
#[error("Sourcify refused: {0}")]
Refused(Refusal),
#[error("Sourcify's answer could not be read: {0}")]
Unreadable(String),
}
#[must_use]
pub fn status_request(network: Network, address: Address) -> Request {
Request::get(format!("{}/v2/contract/{}/{address}", network.verifier_url(), network.chain_id()))
}
pub fn submit_request(
network: Network,
address: Address,
std_json_input: &str,
compiler_version: &str,
contract: &str,
creation: Option<B256>,
) -> Result<Request, VerifyError> {
let input = Json::parse(std_json_input).map_err(|error| VerifyError::Unreadable(error.to_string()))?;
let mut body = vec![
("stdJsonInput".to_owned(), input),
("compilerVersion".to_owned(), Json::String(compiler_version.to_owned())),
("contractIdentifier".to_owned(), Json::String(contract.to_owned())),
];
if let Some(hash) = creation {
body.push(("creationTransactionHash".to_owned(), Json::String(hash.to_hex())));
}
let url = format!("{}/v2/verify/{}/{address}", network.verifier_url(), network.chain_id());
Ok(Request::post_json(url, Json::Object(body).compact()))
}
#[must_use]
pub fn job_request(network: Network, id: &str) -> Request {
Request::get(format!("{}/v2/verify/{id}", network.verifier_url()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct StatusBody {
address: Address,
#[serde(rename = "match")]
matched: Option<String>,
creation_match: Option<String>,
runtime_match: Option<String>,
#[serde(default)]
verified_at: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RefusalBody {
custom_code: String,
message: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct JobBody {
verification_id: String,
is_job_completed: bool,
#[serde(default)]
contract: Option<StatusBody>,
#[serde(default)]
error: Option<RefusalBody>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Submitted {
verification_id: String,
}
fn level(text: Option<&str>) -> Option<Match> {
match text {
Some("exact_match") => Some(Match::Exact),
Some("match") => Some(Match::Partial),
_ => None,
}
}
impl From<StatusBody> for Status {
fn from(body: StatusBody) -> Status {
Status {
address: body.address,
matched: level(body.matched.as_deref()),
creation: level(body.creation_match.as_deref()),
runtime: level(body.runtime_match.as_deref()),
verified_at: body.verified_at,
}
}
}
fn unreadable(error: &serde_json::Error, text: &str) -> VerifyError {
VerifyError::Unreadable(format!("{error} in {}", text.chars().take(300).collect::<String>()))
}
fn refused(response: &Response) -> VerifyError {
let text = response.text();
match serde_json::from_str::<RefusalBody>(&text) {
Ok(body) => VerifyError::Refused(Refusal { code: body.custom_code, message: body.message }),
Err(_) => TransportError::Status { status: response.status, body: text }.into(),
}
}
pub fn read_status(response: &Response) -> Result<Status, VerifyError> {
if !response.is_success() && response.status != 404 {
return Err(refused(response));
}
let text = response.text();
serde_json::from_str::<StatusBody>(&text).map(Status::from).map_err(|error| match response.status {
404 => refused(response),
_ => unreadable(&error, &text),
})
}
pub fn read_submitted(response: &Response) -> Result<String, VerifyError> {
if !response.is_success() {
return Err(refused(response));
}
let text = response.text();
serde_json::from_str::<Submitted>(&text).map(|body| body.verification_id).map_err(|error| unreadable(&error, &text))
}
pub fn read_job(response: &Response) -> Result<Job, VerifyError> {
if !response.is_success() {
return Err(refused(response));
}
let text = response.text();
let body: JobBody = serde_json::from_str(&text).map_err(|error| unreadable(&error, &text))?;
let outcome = match (body.is_job_completed, body.error, body.contract) {
(false, _, _) => None,
(true, Some(error), _) => Some(Err(Refusal { code: error.custom_code, message: error.message })),
(true, None, Some(contract)) => Some(Ok(contract.into())),
(true, None, None) => None,
};
Ok(Job { id: body.verification_id, completed: body.is_job_completed, outcome })
}
pub async fn status<H: Http>(http: &H, network: Network, address: Address) -> Result<Status, VerifyError> {
read_status(&http.send(status_request(network, address)).await?)
}
pub async fn submit<H: Http>(
http: &H,
network: Network,
address: Address,
std_json_input: &str,
compiler_version: &str,
contract: &str,
creation: Option<B256>,
) -> Result<String, VerifyError> {
let request = submit_request(network, address, std_json_input, compiler_version, contract, creation)?;
read_submitted(&http.send(request).await?)
}
pub async fn job<H: Http>(http: &H, network: Network, id: &str) -> Result<Job, VerifyError> {
read_job(&http.send(job_request(network, id)).await?)
}
#[cfg(test)]
#[path = "tests/verify.rs"]
mod tests;