1use crate::error::TaiError;
8use reqwest::Client;
9use serde::de::DeserializeOwned;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(Clone, Debug)]
15pub struct RpcClient {
16 http: Client,
17 endpoint: String,
18}
19
20#[derive(Serialize)]
21struct Request<'a, P> {
22 jsonrpc: &'a str,
23 id: u64,
24 method: &'a str,
25 params: P,
26}
27
28#[derive(Deserialize)]
29struct Response<R> {
30 #[serde(default, rename = "jsonrpc")]
31 _jsonrpc: Option<String>,
32 #[serde(default, rename = "id")]
33 _id: Option<u64>,
34 result: Option<R>,
35 error: Option<JsonRpcError>,
36}
37
38#[derive(Deserialize, Debug)]
39struct JsonRpcError {
40 code: i64,
41 message: String,
42 #[serde(default)]
43 _data: Option<Value>,
44}
45
46const DEFAULT_RPC_TIMEOUT_SECS: u64 = 30;
50
51impl RpcClient {
52 pub fn new(endpoint: impl Into<String>) -> Self {
56 Self::with_timeout(
57 endpoint,
58 std::time::Duration::from_secs(DEFAULT_RPC_TIMEOUT_SECS),
59 )
60 }
61
62 pub fn with_timeout(endpoint: impl Into<String>, timeout: std::time::Duration) -> Self {
64 RpcClient {
65 http: Client::builder()
66 .user_agent(concat!("tai-core/", env!("CARGO_PKG_VERSION")))
67 .timeout(timeout)
68 .build()
69 .expect("reqwest client construction"),
70 endpoint: endpoint.into(),
71 }
72 }
73
74 pub async fn call<P, R>(&self, method: &str, params: P) -> Result<R, TaiError>
76 where
77 P: Serialize,
78 R: DeserializeOwned,
79 {
80 let req = Request {
81 jsonrpc: "2.0",
82 id: 1,
83 method,
84 params,
85 };
86 let resp = self
87 .http
88 .post(&self.endpoint)
89 .json(&req)
90 .send()
91 .await?
92 .error_for_status()?
93 .json::<Response<R>>()
94 .await?;
95 if let Some(err) = resp.error {
96 return Err(TaiError::Rpc(format!("code {}: {}", err.code, err.message)));
97 }
98 resp.result
99 .ok_or_else(|| TaiError::RpcShape("missing `result` field".into()))
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[tokio::test]
108 async fn constructs_without_panicking() {
109 let _c = RpcClient::new("https://fullnode.testnet.sui.io");
112 }
113}