Skip to main content

aztec_rpc/
rpc.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Duration;
3
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7use crate::Error;
8
9#[derive(Serialize)]
10struct Request<'a> {
11    jsonrpc: &'static str,
12    method: &'a str,
13    params: serde_json::Value,
14    id: u64,
15}
16
17#[derive(Deserialize)]
18struct Response {
19    #[serde(default)]
20    result: Option<serde_json::Value>,
21    #[serde(default)]
22    error: Option<RpcError>,
23    #[allow(dead_code)]
24    id: u64,
25}
26
27#[derive(Deserialize)]
28struct RpcError {
29    code: i64,
30    message: String,
31}
32
33/// Internal JSON-RPC 2.0 transport over HTTP.
34pub struct RpcTransport {
35    client: Client,
36    url: String,
37    next_id: AtomicU64,
38}
39
40impl RpcTransport {
41    pub fn new(url: String, timeout: Duration) -> Self {
42        let client = Client::builder()
43            .timeout(timeout)
44            .build()
45            .unwrap_or_else(|_| Client::new());
46        Self {
47            client,
48            url,
49            next_id: AtomicU64::new(1),
50        }
51    }
52
53    pub async fn call<T: serde::de::DeserializeOwned>(
54        &self,
55        method: &str,
56        params: serde_json::Value,
57    ) -> Result<T, Error> {
58        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
59        let request = Request {
60            jsonrpc: "2.0",
61            method,
62            params,
63            id,
64        };
65
66        let resp = self.client.post(&self.url).json(&request).send().await?;
67        let body: Response = resp.json().await?;
68
69        if let Some(err) = body.error {
70            return Err(Error::Rpc {
71                code: err.code,
72                message: err.message,
73            });
74        }
75
76        let result = body.result.ok_or_else(|| {
77            Error::InvalidData("JSON-RPC response missing both result and error".into())
78        })?;
79
80        serde_json::from_value(result).map_err(Into::into)
81    }
82
83    /// Call a JSON-RPC method that returns no meaningful value.
84    ///
85    /// A null or missing result is accepted without error. Only JSON-RPC
86    /// error responses are propagated.
87    pub async fn call_void(&self, method: &str, params: serde_json::Value) -> Result<(), Error> {
88        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
89        let request = Request {
90            jsonrpc: "2.0",
91            method,
92            params,
93            id,
94        };
95
96        let resp = self.client.post(&self.url).json(&request).send().await?;
97        let body: Response = resp.json().await?;
98
99        if let Some(err) = body.error {
100            return Err(Error::Rpc {
101                code: err.code,
102                message: err.message,
103            });
104        }
105
106        Ok(())
107    }
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn request_envelope_serializes() {
117        let req = Request {
118            jsonrpc: "2.0",
119            method: "test_method",
120            params: serde_json::json!([1, "two"]),
121            id: 42,
122        };
123        let json = serde_json::to_value(&req).unwrap();
124        assert_eq!(json["jsonrpc"], "2.0");
125        assert_eq!(json["method"], "test_method");
126        assert_eq!(json["id"], 42);
127        assert_eq!(json["params"][0], 1);
128        assert_eq!(json["params"][1], "two");
129    }
130
131    #[test]
132    fn response_with_result_parses() {
133        let json = r#"{"jsonrpc":"2.0","result":42,"id":1}"#;
134        let resp: Response = serde_json::from_str(json).unwrap();
135        assert_eq!(resp.result.unwrap(), serde_json::json!(42));
136        assert!(resp.error.is_none());
137    }
138
139    #[test]
140    fn response_with_error_parses() {
141        let json =
142            r#"{"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":1}"#;
143        let resp: Response = serde_json::from_str(json).unwrap();
144        assert!(resp.result.is_none());
145        let err = resp.error.unwrap();
146        assert_eq!(err.code, -32600);
147        assert_eq!(err.message, "Invalid Request");
148    }
149
150    #[test]
151    fn response_with_null_result_parses() {
152        let json = r#"{"jsonrpc":"2.0","result":null,"id":1}"#;
153        let resp: Response = serde_json::from_str(json).unwrap();
154        // Option deserializes JSON null as None
155        assert!(resp.result.is_none());
156        assert!(resp.error.is_none());
157    }
158}