Skip to main content

tai_core/
rpc.rs

1//! Minimal Sui JSON-RPC 2.0 client.
2//!
3//! `tai-core` doesn't pull in the full `sui-sdk` crate; for the read surface
4//! we need (object reads + dev-inspect) a thin HTTP+JSON wrapper is enough.
5//! Pure-Rust BCS transaction construction lives in `ptb.rs` (Phase 11.5).
6
7use crate::error::TaiError;
8use reqwest::Client;
9use serde::de::DeserializeOwned;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13/// A Sui JSON-RPC 2.0 client over reqwest.
14#[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
46/// Default per-request timeout for the Sui RPC client. Slow public testnet
47/// fullnodes occasionally take a few seconds for `multiGetObjects`, but
48/// nothing should ever take 30+. Lets the CLI fail loud instead of hanging.
49const DEFAULT_RPC_TIMEOUT_SECS: u64 = 30;
50
51impl RpcClient {
52    /// Construct a client pointing at the given JSON-RPC endpoint URL.
53    /// Applies a 30-second per-request timeout. Use [`RpcClient::with_timeout`]
54    /// if you need a different cap.
55    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    /// Construct with an explicit per-request timeout.
63    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    /// Issue a JSON-RPC 2.0 call and deserialize the `result` field into `R`.
75    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        // Sanity: client is constructable. Network behavior covered by the
110        // testnet integration test in reads_tests.
111        let _c = RpcClient::new("https://fullnode.testnet.sui.io");
112    }
113}