use std::{cmp::min, io, path::Path};
use bytes::{BufMut, Bytes, BytesMut};
use flate2::{write::GzEncoder, Compression};
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
Client, ClientBuilder, StatusCode,
};
use tar::Builder as TarBuilder;
use tokio::time::{sleep, Duration};
use crate::{
verification_types::{
VerificationDetails, VerificationRequest, VerificationResult, VerificationStatus,
},
Error, Verbosity,
};
const MAX_RETRIES: u32 = 10;
const BASE_DELAY: Duration = Duration::from_secs(3);
const MAX_DELAY: Duration = Duration::from_secs(300);
static GIT_DIR_NAME: &str = ".git";
static TARGET_DIR_NAME: &str = "target";
pub fn build_archive(path: &Path) -> Result<Bytes, io::Error> {
let buffer = BytesMut::new().writer();
let encoder = GzEncoder::new(buffer, Compression::best());
let mut archive = TarBuilder::new(encoder);
for entry in path.read_dir()?.flatten() {
let file_name = entry.file_name();
if file_name == TARGET_DIR_NAME || file_name == GIT_DIR_NAME {
continue;
}
let full_path = entry.path();
if full_path.is_dir() {
archive.append_dir_all(&file_name, &full_path)?;
} else {
archive.append_path_with_name(&full_path, &file_name)?;
}
}
let encoder = archive.into_inner()?;
let buffer = encoder.finish()?;
Ok(buffer.into_inner().freeze())
}
pub async fn send_verification_request(
hash_str: &str,
base_url: &str,
code_archive: String,
verbosity: Verbosity,
) -> Result<VerificationDetails, Error> {
let verification_request = VerificationRequest {
hash: hash_str.to_string(),
code_archive,
};
fn make_client() -> reqwest::Result<Client> {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let builder = ClientBuilder::new()
.default_headers(headers)
.user_agent("casper-client-rs");
#[cfg(not(target_arch = "wasm32"))]
let builder = builder.pool_max_idle_per_host(0);
builder.build()
}
let Ok(http_client) = make_client() else {
eprintln!("Failed to build HTTP client");
return Err(Error::FailedToConstructHttpClient);
};
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Sending verification request");
}
let url = base_url.to_string() + "/verification";
let response = match http_client
.post(url)
.json(&verification_request)
.send()
.await
{
Ok(response) => response,
Err(error) => {
eprintln!("Cannot send verification request: {error:?}");
return Err(Error::ContractVerificationFailed);
}
};
match response.status() {
StatusCode::OK => {
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Sent verification request",);
}
}
status => {
eprintln!("Verification failed with status {status}");
}
}
wait_for_verification_finished(base_url, &http_client, hash_str, verbosity).await;
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Getting verification details...");
}
let details_url = format!("{}/verification/{}/details", base_url, hash_str);
match http_client.get(details_url).send().await {
Ok(response) => response.json().await.map_err(|err| {
eprintln!("Failed to parse JSON {err}");
Error::ContractVerificationFailed
}),
Err(error) => {
eprintln!("Cannot get verification details: {error:?}");
Err(Error::ContractVerificationFailed)
}
}
}
async fn wait_for_verification_finished(
base_url: &str,
http_client: &Client,
hash_str: &str,
verbosity: Verbosity,
) {
let mut retries = MAX_RETRIES;
let mut delay = BASE_DELAY;
while retries != 0 {
sleep(delay).await;
match get_verification_status(base_url, http_client, hash_str).await {
Ok(status) => {
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Verification status: {status:?}");
}
if status == VerificationStatus::Verified || status == VerificationStatus::Failed {
break;
}
}
Err(error) => {
eprintln!("Cannot get verification status: {error:?}");
break;
}
};
retries -= 1;
delay = min(delay * 2, MAX_DELAY);
}
}
async fn get_verification_status(
base_url: &str,
http_client: &Client,
hash_str: &str,
) -> Result<VerificationStatus, Error> {
let status_url = format!("{}/verification/{}/status", base_url, hash_str);
let response = match http_client.get(status_url).send().await {
Ok(response) => response,
Err(error) => {
eprintln!("Failed to fetch verification status: {error:?}");
return Err(Error::ContractVerificationFailed);
}
};
match response.status() {
StatusCode::OK => {
let result: VerificationResult = response.json().await.map_err(|err| {
eprintln!("Failed to parse JSON for verification status, {err}");
Error::ContractVerificationFailed
})?;
Ok(result.status)
}
status => {
eprintln!("Verification status not found, {status}");
Err(Error::ContractVerificationFailed)
}
}
}