use std::time::Duration;
use dusk_core::abi::ContractId;
use reqwest::{Body, Response};
use rkyv::Archive;
use crate::Error;
const REQUIRED_RUSK_VERSION: &str = "1.0.0-rc.0";
pub const CONTRACTS_TARGET: &str = "contracts";
#[derive(Clone)]
pub struct HttpClient {
client: reqwest::Client,
uri: String,
}
impl HttpClient {
pub fn new<S: Into<String>>(uri: S) -> Result<Self, Error> {
let client = reqwest::ClientBuilder::new()
.connect_timeout(Duration::from_secs(30))
.build();
match client {
Ok(client) => Ok(Self {
uri: uri.into(),
client,
}),
Err(_) => Err(Error::HttpClient),
}
}
pub async fn contract_query<I, C, const N: usize>(
&self,
contract: C,
method: &str,
value: &I,
) -> Result<Vec<u8>, Error>
where
I: Archive,
I: rkyv::Serialize<rkyv::ser::serializers::AllocSerializer<N>>,
C: Into<Option<&'static str>>,
{
let data = rkyv::to_bytes(value).map_err(|_| Error::Rkyv)?.to_vec();
let response = self
.call_raw(CONTRACTS_TARGET, contract.into(), method, &data, false)
.await?;
Ok(response.bytes().await?.to_vec())
}
pub async fn check_connection(&self) -> Result<(), reqwest::Error> {
self.client.post(&self.uri).send().await?;
Ok(())
}
pub async fn call<E>(
&self,
target: &str,
entity: E,
topic: &str,
request: &[u8],
) -> Result<Vec<u8>, Error>
where
E: Into<Option<&'static str>>,
{
let response =
self.call_raw(target, entity, topic, request, false).await?;
let data = response.bytes().await?;
Ok(data.to_vec())
}
pub async fn call_raw<E>(
&self,
target: &str,
entity: E,
topic: &str,
data: &[u8],
feed: bool,
) -> Result<Response, Error>
where
E: Into<Option<&'static str>>,
{
let uri = &self.uri;
let entity = entity.into().map(|e| format!(":{e}")).unwrap_or_default();
let rues_prefix = if uri.ends_with('/') { "on" } else { "/on" };
let mut request = self
.client
.post(format!("{uri}{rues_prefix}/{target}{entity}/{topic}"))
.body(Body::from(data.to_vec()))
.header("Content-Type", "application/octet-stream")
.header("rusk-version", REQUIRED_RUSK_VERSION);
if feed {
request = request.header("Rusk-Feeder", "1");
}
let response = request.send().await?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let error = &response.bytes().await?;
let error = String::from_utf8(error.to_vec())
.unwrap_or("unparsable error".into());
let msg = if error.contains("Value spent larger than account holds")
{
"Balance is not enough to cover the transaction max fees".into()
} else {
error
};
Err(Error::Rusk(msg))
} else {
Ok(response)
}
}
pub async fn upload_driver(
&self,
driver_bytecode: impl AsRef<[u8]>,
contract_id: &ContractId,
signature: impl AsRef<[u8]>,
) -> Result<Vec<u8>, Error> {
let uri = &self.uri;
let client = reqwest::Client::new();
let target = "contract";
let entity = hex::encode(contract_id.as_bytes());
let entity = if entity.is_empty() {
entity.clone()
} else {
format!(":{entity}")
};
let topic = "upload_driver";
let rues_prefix = if uri.ends_with('/') { "on" } else { "/on" };
let request = client
.post(format!("{uri}{rues_prefix}/{target}{entity}/{topic}"))
.body(Body::from(driver_bytecode.as_ref().to_vec()))
.header("Content-Type", "application/octet-stream")
.header("rusk-version", REQUIRED_RUSK_VERSION)
.header("sign", hex::encode(signature.as_ref()));
let response = request.send().await?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let error = &response.bytes().await?;
let error = String::from_utf8(error.to_vec())
.unwrap_or("unparsable error".into());
let msg = format!("{status}: {error}");
Err(Error::Rusk(msg))
} else {
let data = response.bytes().await?;
Ok(data.to_vec())
}
}
}