use crate::error::TaiError;
use reqwest::Client;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug)]
pub struct RpcClient {
http: Client,
endpoint: String,
}
#[derive(Serialize)]
struct Request<'a, P> {
jsonrpc: &'a str,
id: u64,
method: &'a str,
params: P,
}
#[derive(Deserialize)]
struct Response<R> {
#[serde(default, rename = "jsonrpc")]
_jsonrpc: Option<String>,
#[serde(default, rename = "id")]
_id: Option<u64>,
result: Option<R>,
error: Option<JsonRpcError>,
}
#[derive(Deserialize, Debug)]
struct JsonRpcError {
code: i64,
message: String,
#[serde(default)]
_data: Option<Value>,
}
const DEFAULT_RPC_TIMEOUT_SECS: u64 = 30;
impl RpcClient {
pub fn new(endpoint: impl Into<String>) -> Self {
Self::with_timeout(
endpoint,
std::time::Duration::from_secs(DEFAULT_RPC_TIMEOUT_SECS),
)
}
pub fn with_timeout(endpoint: impl Into<String>, timeout: std::time::Duration) -> Self {
RpcClient {
http: Client::builder()
.user_agent(concat!("tai-core/", env!("CARGO_PKG_VERSION")))
.timeout(timeout)
.build()
.expect("reqwest client construction"),
endpoint: endpoint.into(),
}
}
pub async fn call<P, R>(&self, method: &str, params: P) -> Result<R, TaiError>
where
P: Serialize,
R: DeserializeOwned,
{
let req = Request {
jsonrpc: "2.0",
id: 1,
method,
params,
};
let resp = self
.http
.post(&self.endpoint)
.json(&req)
.send()
.await?
.error_for_status()?
.json::<Response<R>>()
.await?;
if let Some(err) = resp.error {
return Err(TaiError::Rpc(format!("code {}: {}", err.code, err.message)));
}
resp.result
.ok_or_else(|| TaiError::RpcShape("missing `result` field".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn constructs_without_panicking() {
let _c = RpcClient::new("https://fullnode.testnet.sui.io");
}
}