Skip to main content

aztec_rpc/
rpc.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Duration;
3
4use reqwest::Client;
5use serde::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(serde::Deserialize)]
18struct RpcError {
19    code: i64,
20    message: String,
21}
22
23/// Internal JSON-RPC 2.0 transport over HTTP.
24pub struct RpcTransport {
25    client: Client,
26    url: String,
27    timeout: Duration,
28    next_id: AtomicU64,
29}
30
31impl RpcTransport {
32    /// Get the URL this transport connects to.
33    pub fn url(&self) -> &str {
34        &self.url
35    }
36
37    /// Get the timeout duration for this transport.
38    pub fn timeout(&self) -> Duration {
39        self.timeout
40    }
41
42    pub fn new(url: String, timeout: Duration) -> Self {
43        let client = Client::builder()
44            .timeout(timeout)
45            .build()
46            .unwrap_or_else(|_| Client::new());
47        Self {
48            client,
49            url,
50            timeout,
51            next_id: AtomicU64::new(1),
52        }
53    }
54
55    pub async fn call<T: serde::de::DeserializeOwned>(
56        &self,
57        method: &str,
58        params: serde_json::Value,
59    ) -> Result<T, Error> {
60        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
61        let request = Request {
62            jsonrpc: "2.0",
63            method,
64            params,
65            id,
66        };
67
68        let resp = self.client.post(&self.url).json(&request).send().await?;
69        let body: serde_json::Value = resp.json().await?;
70        let error: Option<RpcError> = body
71            .get("error")
72            .filter(|value| !value.is_null())
73            .cloned()
74            .map(serde_json::from_value)
75            .transpose()?;
76
77        if let Some(err) = error {
78            return Err(Error::Rpc {
79                code: err.code,
80                message: err.message,
81            });
82        }
83
84        let result = body.get("result").cloned().ok_or_else(|| {
85            Error::InvalidData(format!(
86                "JSON-RPC response for method '{method}' missing both result and error"
87            ))
88        })?;
89
90        serde_json::from_value(result).map_err(Into::into)
91    }
92
93    /// Call a JSON-RPC method that may legitimately return no value.
94    ///
95    /// Treats both `"result": null` and a missing `result` field as `Ok(None)`,
96    /// while still propagating JSON-RPC errors.
97    pub async fn call_optional<T: serde::de::DeserializeOwned>(
98        &self,
99        method: &str,
100        params: serde_json::Value,
101    ) -> Result<Option<T>, Error> {
102        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
103        let request = Request {
104            jsonrpc: "2.0",
105            method,
106            params,
107            id,
108        };
109
110        let resp = self.client.post(&self.url).json(&request).send().await?;
111        let body: serde_json::Value = resp.json().await?;
112        let error: Option<RpcError> = body
113            .get("error")
114            .filter(|value| !value.is_null())
115            .cloned()
116            .map(serde_json::from_value)
117            .transpose()?;
118
119        if let Some(err) = error {
120            return Err(Error::Rpc {
121                code: err.code,
122                message: err.message,
123            });
124        }
125
126        let Some(result) = body.get("result").cloned() else {
127            return Ok(None);
128        };
129
130        if result.is_null() {
131            return Ok(None);
132        }
133
134        serde_json::from_value(result).map(Some).map_err(Into::into)
135    }
136
137    /// Call a JSON-RPC method that returns no meaningful value.
138    ///
139    /// A null or missing result is accepted without error. Only JSON-RPC
140    /// error responses are propagated.
141    pub async fn call_void(&self, method: &str, params: serde_json::Value) -> Result<(), Error> {
142        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
143        let request = Request {
144            jsonrpc: "2.0",
145            method,
146            params,
147            id,
148        };
149
150        let resp = self.client.post(&self.url).json(&request).send().await?;
151        let body: serde_json::Value = resp.json().await?;
152        let error: Option<RpcError> = body
153            .get("error")
154            .filter(|value| !value.is_null())
155            .cloned()
156            .map(serde_json::from_value)
157            .transpose()?;
158
159        if let Some(err) = error {
160            return Err(Error::Rpc {
161                code: err.code,
162                message: err.message,
163            });
164        }
165
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171#[allow(clippy::unwrap_used)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn request_envelope_serializes() {
177        let req = Request {
178            jsonrpc: "2.0",
179            method: "test_method",
180            params: serde_json::json!([1, "two"]),
181            id: 42,
182        };
183        let json = serde_json::to_value(&req).unwrap();
184        assert_eq!(json["jsonrpc"], "2.0");
185        assert_eq!(json["method"], "test_method");
186        assert_eq!(json["id"], 42);
187        assert_eq!(json["params"][0], 1);
188        assert_eq!(json["params"][1], "two");
189    }
190
191    #[test]
192    fn response_with_result_parses() {
193        let body: serde_json::Value =
194            serde_json::from_str(r#"{"jsonrpc":"2.0","result":42,"id":1}"#).unwrap();
195        assert_eq!(body["result"], serde_json::json!(42));
196        assert!(body.get("error").is_none());
197    }
198
199    #[test]
200    fn response_with_error_parses() {
201        let body: serde_json::Value = serde_json::from_str(
202            r#"{"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":1}"#,
203        )
204        .unwrap();
205        assert!(body.get("result").is_none());
206        let err: RpcError = serde_json::from_value(body["error"].clone()).unwrap();
207        assert_eq!(err.code, -32600);
208        assert_eq!(err.message, "Invalid Request");
209    }
210
211    #[test]
212    fn response_with_null_result_parses() {
213        let body: serde_json::Value =
214            serde_json::from_str(r#"{"jsonrpc":"2.0","result":null,"id":1}"#).unwrap();
215        assert!(body.get("result").is_some());
216        assert!(body["result"].is_null());
217        assert!(body.get("error").is_none());
218    }
219}