use candid::{CandidType, Principal, decode_args, encode_args, utils::ArgumentEncoder};
use ciborium::from_reader;
use http::header;
use ic_auth_types::{ByteBufB64, deterministic_cbor_into_vec};
use reqwest::Client;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::fmt::Display;
pub static CONTENT_TYPE_CBOR: &str = "application/cbor";
pub static CONTENT_TYPE_JSON: &str = "application/json";
pub static CONTENT_TYPE_TEXT: &str = "text/plain";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RPCRequest {
pub method: String,
pub params: ByteBufB64,
}
#[derive(Clone, Debug, Serialize)]
pub struct RPCRequestRef<'a> {
pub method: &'a str,
pub params: &'a ByteBufB64,
}
#[derive(Clone, Debug, Serialize)]
pub struct CanisterRequestRef<'a> {
pub canister: &'a Principal,
pub method: &'a str,
pub params: &'a ByteBufB64,
}
pub type RPCResponse = Result<ByteBufB64, String>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ListObject<T> {
pub data: Vec<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum HttpRPCError {
#[error("http_rpc({endpoint:?}, {path:?}): send error: {error}")]
RequestError {
endpoint: String,
path: String,
error: String,
},
#[error("http_rpc({endpoint:?}, {path:?}): response status {status}, error: {error}")]
ResponseError {
endpoint: String,
path: String,
status: u16,
error: String,
},
#[error("http_rpc({endpoint:?}, {path:?}): parse result error: {error}")]
ResultError {
endpoint: String,
path: String,
error: String,
},
}
pub async fn http_rpc<T>(
client: &Client,
endpoint: &str,
method: &str,
args: &impl Serialize,
) -> Result<T, HttpRPCError>
where
T: DeserializeOwned,
{
let args = deterministic_cbor_into_vec(args).map_err(|e| HttpRPCError::RequestError {
endpoint: endpoint.to_string(),
path: method.to_string(),
error: format!("{e:?}"),
})?;
let req = RPCRequestRef {
method,
params: &args.into(),
};
let req = deterministic_cbor_into_vec(&req).map_err(|e| HttpRPCError::RequestError {
endpoint: endpoint.to_string(),
path: method.to_string(),
error: format!("{e:?}"),
})?;
let res = cbor_rpc(client, endpoint, method, None, req).await?;
from_reader(&res[..]).map_err(|e| HttpRPCError::ResultError {
endpoint: endpoint.to_string(),
path: method.to_string(),
error: format!("{e:?}"),
})
}
pub async fn canister_rpc<In, Out>(
client: &Client,
endpoint: &str,
canister: &Principal,
method: &str,
args: In,
) -> Result<Out, HttpRPCError>
where
In: ArgumentEncoder,
Out: CandidType + for<'a> candid::Deserialize<'a>,
{
let args = encode_args(args).map_err(|e| HttpRPCError::RequestError {
endpoint: format!("{endpoint}/{canister}"),
path: method.to_string(),
error: format!("{e:?}"),
})?;
let req = deterministic_cbor_into_vec(&CanisterRequestRef {
canister,
method,
params: &ByteBufB64::from(args),
})
.map_err(|e| HttpRPCError::RequestError {
endpoint: endpoint.to_string(),
path: method.to_string(),
error: format!("{e:?}"),
})?;
let res = cbor_rpc(client, endpoint, canister, None, req).await?;
let res: (Out,) = decode_args(&res).map_err(|e| HttpRPCError::ResultError {
endpoint: format!("{endpoint}/{canister}"),
path: method.to_string(),
error: format!("{e:?}"),
})?;
Ok(res.0)
}
pub async fn cbor_rpc(
client: &Client,
endpoint: &str,
path: impl Display,
headers: Option<http::HeaderMap>,
body: Vec<u8>,
) -> Result<ByteBufB64, HttpRPCError> {
let mut headers = headers.unwrap_or_default();
let ct: http::HeaderValue = http::HeaderValue::from_static(CONTENT_TYPE_CBOR);
headers.insert(header::CONTENT_TYPE, ct.clone());
headers.insert(header::ACCEPT, ct);
let res = client
.post(endpoint)
.headers(headers)
.body(body)
.send()
.await
.map_err(|e| HttpRPCError::RequestError {
endpoint: endpoint.to_string(),
path: path.to_string(),
error: format!("{e:?}"),
})?;
let status = res.status().as_u16();
if status != 200 {
return Err(HttpRPCError::ResponseError {
endpoint: endpoint.to_string(),
path: path.to_string(),
status,
error: res.text().await.unwrap_or_default(),
});
}
let data = res.bytes().await.map_err(|e| HttpRPCError::ResultError {
endpoint: endpoint.to_string(),
path: path.to_string(),
error: format!("{e:?}"),
})?;
let res: RPCResponse = from_reader(&data[..]).map_err(|e| HttpRPCError::ResultError {
endpoint: endpoint.to_string(),
path: path.to_string(),
error: format!("{e:?}"),
})?;
res.map_err(|e| HttpRPCError::ResultError {
endpoint: endpoint.to_string(),
path: path.to_string(),
error: format!("{e:?}"),
})
}