unc_jsonrpc_client/
lib.rs

1//! Lower-level API for interfacing with the unc Protocol via JSONRPC.
2//!
3//! ## Layout
4//!
5//! Each one the valid *public* JSON RPC methods are pre-defined in specialized modules within the `methods` module.
6//!
7//! Inside every method module (e.g [`methods::query`]) there's;
8//!   - a `Request` type (e.g [`methods::query::RpcQueryRequest`])
9//!   - a `Response` type (e.g [`methods::query::RpcQueryResponse`])
10//!   - and an `Error` type (e.g [`methods::query::RpcQueryError`])
11//!
12//! Calling a constructed request on a client returns with the response and error types for that method.
13//!
14//! ## Examples
15//!
16//! 1. Request server status from testnet RPC
17//!
18//!    ```
19//!    # #![allow(deprecated)]
20//!    use unc_jsonrpc_client::{methods, JsonRpcClient};
21//!
22//!    # #[tokio::main]
23//!    # async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//!    let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
25//!
26//!    let request = methods::status::RpcStatusRequest; // no params
27//!
28//!    // call a method on the server via the connected client
29//!    let server_status = client.call(request).await?;
30//!
31//!    println!("{:?}", server_status);
32//!    # Ok(())
33//!    # }
34//!    ```
35//!
36//! 2. Query transaction status from mainnet RPC
37//!
38//!    ```no_run
39//!    use unc_jsonrpc_client::{methods, JsonRpcClient};
40//!    use unc_jsonrpc_primitives::types::transactions::TransactionInfo;
41//!
42//!    # #[tokio::main]
43//!    # async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
44//!    let client = JsonRpcClient::connect("https://archival-rpc.mainnet.unc.org");
45//!
46//!    let tx_status_request = methods::tx::RpcTransactionStatusRequest {
47//!        transaction_info: TransactionInfo::TransactionId {
48//!            tx_hash: "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U".parse()?,
49//!            sender_account_id: "miraclx.unc".parse()?,
50//!        },
51//!    };
52//!
53//!    let tx_status = client.call(tx_status_request).await?;
54//!
55//!    println!("{:?}", tx_status);
56//!    # Ok(())
57//!    # }
58//!    ```
59use std::{fmt, sync::Arc};
60
61use lazy_static::lazy_static;
62
63pub mod auth;
64pub mod errors;
65pub mod header;
66pub mod methods;
67
68use errors::*;
69
70pub const UNC_MAINNET_RPC_URL: &str = "https://rpc.mainnet.unc.org";
71pub const UNC_TESTNET_RPC_URL: &str = "https://rpc.testnet.unc.org";
72pub const UNC_MAINNET_ARCHIVAL_RPC_URL: &str = "https://archival-rpc.mainnet.unc.org";
73pub const UNC_TESTNET_ARCHIVAL_RPC_URL: &str = "https://archival-rpc.testnet.unc.org";
74
75lazy_static! {
76    static ref DEFAULT_CONNECTOR: JsonRpcClientConnector = JsonRpcClient::new_client();
77}
78
79/// unc JSON RPC client connector.
80#[derive(Clone)]
81pub struct JsonRpcClientConnector {
82    client: reqwest::Client,
83}
84
85impl JsonRpcClientConnector {
86    /// Return a JsonRpcClient that connects to the specified server.
87    pub fn connect<U: AsUrl>(&self, server_addr: U) -> JsonRpcClient {
88        log::debug!("returned a new JSONRPC client handle");
89
90        JsonRpcClient {
91            inner: Arc::new(JsonRpcInnerClient {
92                server_addr: server_addr.to_string(),
93                client: self.client.clone(),
94            }),
95            headers: reqwest::header::HeaderMap::new(),
96        }
97    }
98}
99
100struct JsonRpcInnerClient {
101    server_addr: String,
102    client: reqwest::Client,
103}
104
105#[derive(Clone)]
106/// A unc JSON RPC Client.
107///
108/// This is the main struct that you will use to interact with the unc JSON RPC API.
109///
110/// ## Example
111///
112///```
113/// use unc_jsonrpc_client::{methods, JsonRpcClient};
114/// use unc_primitives::types::{BlockReference, Finality};
115///
116/// # #[tokio::main]
117/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
118/// let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
119///
120/// let request = methods::block::RpcBlockRequest {
121///     block_reference: BlockReference::Finality(Finality::Final),
122/// };
123///
124/// let server_status = client.call(request).await?;
125///
126/// println!("{:?}", server_status);
127/// # Ok(())
128/// # }
129/// ```
130pub struct JsonRpcClient {
131    inner: Arc<JsonRpcInnerClient>,
132    headers: reqwest::header::HeaderMap,
133}
134
135pub type MethodCallResult<T, E> = Result<T, JsonRpcError<E>>;
136
137impl JsonRpcClient {
138    /// Connect to a JSON RPC server using the default connector.
139    ///
140    /// It's virtually the same as calling `new_client().connect(server_addr)`.
141    /// Only, this method optimally reuses the same connector across invocations.
142    ///
143    /// ## Example
144    ///
145    /// ```
146    /// use unc_jsonrpc_client::JsonRpcClient;
147    ///
148    /// let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
149    /// ```
150    pub fn connect<U: AsUrl>(server_addr: U) -> JsonRpcClient {
151        DEFAULT_CONNECTOR.connect(server_addr)
152    }
153
154    /// Get the server address the client connects to.
155    ///
156    /// It basically returns the server address passed to `connect()`.
157    ///
158    /// ## Example
159    ///
160    /// ```
161    /// # use unc_jsonrpc_client::JsonRpcClient;
162    /// let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
163    ///
164    /// assert_eq!(client.server_addr(), "https://rpc.testnet.unc.org");
165    /// ```
166    pub fn server_addr(&self) -> &str {
167        &self.inner.server_addr
168    }
169
170    /// RPC method executor for the client.
171    ///
172    /// ## Example
173    ///
174    /// ```
175    /// use unc_jsonrpc_client::{methods, JsonRpcClient};
176    ///
177    /// # #[tokio::main]
178    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
179    /// let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
180    ///
181    /// let request = methods::status::RpcStatusRequest;
182    /// let response = client.call(request).await?;
183    ///
184    /// assert!(matches!(
185    ///     response,
186    ///     methods::status::RpcStatusResponse { .. }
187    /// ));
188    /// # Ok(())
189    /// # }
190    /// ```
191    pub async fn call<M>(&self, method: M) -> MethodCallResult<M::Response, M::Error>
192    where
193        M: methods::RpcMethod,
194    {
195        let request_payload = methods::to_json(&method).map_err(|err| {
196            JsonRpcError::TransportError(RpcTransportError::SendError(
197                JsonRpcTransportSendError::PayloadSerializeError(err),
198            ))
199        })?;
200
201        log::debug!("request payload: {:#}", request_payload);
202        log::debug!("request headers: {:#?}", self.headers());
203
204        let request_payload = serde_json::to_vec(&request_payload).map_err(|err| {
205            JsonRpcError::TransportError(RpcTransportError::SendError(
206                JsonRpcTransportSendError::PayloadSerializeError(err.into()),
207            ))
208        })?;
209
210        let request = self
211            .inner
212            .client
213            .post(&self.inner.server_addr)
214            .headers(self.headers.clone())
215            .body(request_payload);
216
217        let response = request.send().await.map_err(|err| {
218            JsonRpcError::TransportError(RpcTransportError::SendError(
219                JsonRpcTransportSendError::PayloadSendError(err),
220            ))
221        })?;
222        log::debug!("response headers: {:#?}", response.headers());
223        match response.status() {
224            reqwest::StatusCode::OK => {}
225            non_ok_status => {
226                return Err(JsonRpcError::ServerError(
227                    JsonRpcServerError::ResponseStatusError(match non_ok_status {
228                        reqwest::StatusCode::UNAUTHORIZED => {
229                            JsonRpcServerResponseStatusError::Unauthorized
230                        }
231                        reqwest::StatusCode::TOO_MANY_REQUESTS => {
232                            JsonRpcServerResponseStatusError::TooManyRequests
233                        }
234                        unexpected => {
235                            JsonRpcServerResponseStatusError::Unexpected { status: unexpected }
236                        }
237                    }),
238                ));
239            }
240        }
241        let response_payload = response.bytes().await.map_err(|err| {
242            JsonRpcError::TransportError(RpcTransportError::RecvError(
243                JsonRpcTransportRecvError::PayloadRecvError(err),
244            ))
245        })?;
246        let response_payload = serde_json::from_slice::<serde_json::Value>(&response_payload);
247
248        if let Ok(ref response_payload) = response_payload {
249            log::debug!("response payload: {:#}", response_payload);
250        }
251
252        let response_message = unc_jsonrpc_primitives::message::decoded_to_parsed(
253            response_payload.and_then(serde_json::from_value),
254        )
255        .map_err(|err| {
256            JsonRpcError::TransportError(RpcTransportError::RecvError(
257                JsonRpcTransportRecvError::PayloadParseError(err),
258            ))
259        })?;
260
261        if let unc_jsonrpc_primitives::message::Message::Response(response) = response_message {
262            return M::parse_handler_response(response.result?)
263                .map_err(|err| {
264                    JsonRpcError::TransportError(RpcTransportError::RecvError(
265                        JsonRpcTransportRecvError::ResponseParseError(
266                            JsonRpcTransportHandlerResponseError::ResultParseError(err),
267                        ),
268                    ))
269                })?
270                .map_err(|err| JsonRpcError::ServerError(JsonRpcServerError::HandlerError(err)));
271        }
272        Err(JsonRpcError::TransportError(RpcTransportError::RecvError(
273            JsonRpcTransportRecvError::UnexpectedServerResponse(response_message),
274        )))
275    }
276
277    /// Add a header to this request.
278    ///
279    /// Depending on the header specified, this method either returns back
280    /// the client, or a result containing the client.
281    ///
282    /// ### Example
283    ///
284    /// ```
285    /// use unc_jsonrpc_client::JsonRpcClient;
286    ///
287    /// # #[tokio::main]
288    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
289    /// let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
290    /// let client = client.header(("user-agent", "someclient/0.1.0"))?; // <- returns a result
291    ///
292    /// use unc_jsonrpc_client::auth;
293    ///
294    /// let client = client.header(
295    ///     auth::ApiKey::new("cadc4c83-5566-4c94-aa36-773605150f44")?, // <- error handling here
296    /// ); // <- returns the client
297    /// # Ok(())
298    /// # }
299    /// ```
300    pub fn header<H, D>(self, entry: H) -> D::Output
301    where
302        H: header::HeaderEntry<D>,
303        D: header::HeaderEntryDiscriminant<H>,
304    {
305        D::apply(self, entry)
306    }
307
308    /// Get a shared reference to the headers.
309    pub fn headers(&self) -> &reqwest::header::HeaderMap {
310        &self.headers
311    }
312
313    /// Get an exclusive reference to the headers.
314    pub fn headers_mut(&mut self) -> &mut reqwest::header::HeaderMap {
315        &mut self.headers
316    }
317
318    /// Manually create a new client connector.
319    ///
320    /// It's recommended to use the [`connect`](JsonRpcClient::connect) method instead as that method optimally
321    /// reuses the default connector across invocations.
322    ///
323    /// However, if for some reason you still need to manually create a new connector, you can do so.
324    /// Just remember to properly **reuse** it as much as possible.
325    ///
326    /// ## Example
327    ///
328    /// ```
329    /// # use unc_jsonrpc_client::JsonRpcClient;
330    /// let client_connector = JsonRpcClient::new_client();
331    ///
332    /// let mainnet_client = client_connector.connect("https://rpc.mainnet.unc.org");
333    /// let testnet_client = client_connector.connect("https://rpc.testnet.unc.org");
334    /// ```
335    pub fn new_client() -> JsonRpcClientConnector {
336        let mut headers = reqwest::header::HeaderMap::with_capacity(2);
337        headers.insert(
338            reqwest::header::CONTENT_TYPE,
339            reqwest::header::HeaderValue::from_static("application/json"),
340        );
341
342        log::debug!("initialized a new JSONRPC client connector");
343        JsonRpcClientConnector {
344            client: reqwest::Client::builder()
345                .default_headers(headers)
346                .build()
347                .unwrap(),
348        }
349    }
350
351    /// Create a new client constructor using a custom web client.
352    ///
353    /// This is useful if you want to customize the `reqwest::Client` instance used by the JsonRpcClient.
354    ///
355    /// ## Example
356    ///
357    /// ```
358    /// use unc_jsonrpc_client::JsonRpcClient;
359    ///
360    /// # #[tokio::main]
361    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
362    /// let web_client = reqwest::Client::builder()
363    ///     .proxy(reqwest::Proxy::all("https://192.168.1.1:4825")?)
364    ///     .build()?;
365    ///
366    /// let testnet_client = JsonRpcClient::with(web_client).connect("https://rpc.testnet.unc.org");
367    /// # Ok(())
368    /// # }
369    /// ```
370    pub fn with(client: reqwest::Client) -> JsonRpcClientConnector {
371        JsonRpcClientConnector { client }
372    }
373}
374
375impl fmt::Debug for JsonRpcClient {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        let mut builder = f.debug_struct("JsonRpcClient");
378        builder.field("server_addr", &self.inner.server_addr);
379        builder.field("headers", &self.headers);
380        builder.field("client", &self.inner.client);
381        builder.finish()
382    }
383}
384
385mod private {
386    pub trait Sealed: ToString {}
387}
388
389pub trait AsUrl: private::Sealed {}
390
391impl private::Sealed for String {}
392
393impl AsUrl for String {}
394
395impl private::Sealed for &String {}
396
397impl AsUrl for &String {}
398
399impl private::Sealed for &str {}
400
401impl AsUrl for &str {}
402
403impl private::Sealed for reqwest::Url {}
404
405impl AsUrl for reqwest::Url {}
406
407#[cfg(test)]
408mod tests {
409    use crate::{methods, JsonRpcClient};
410
411    #[tokio::test]
412    async fn chk_status_testnet() {
413        let client = JsonRpcClient::connect("https://rpc.testnet.unc.org");
414
415        let status = client.call(methods::status::RpcStatusRequest).await;
416
417        assert!(
418            matches!(status, Ok(methods::status::RpcStatusResponse { .. })),
419            "expected an Ok(RpcStatusResponse), found [{:?}]",
420            status
421        );
422    }
423
424    #[tokio::test]
425    #[cfg(feature = "any")]
426    async fn any_typed_ok() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
427        let client = JsonRpcClient::connect("https://archival-rpc.mainnet.unc.org");
428
429        let tx_status = client
430            .call(methods::any::<methods::tx::RpcTransactionStatusRequest>(
431                "tx",
432                serde_json::json!([
433                    "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
434                    "miraclx.unc",
435                ]),
436            ))
437            .await;
438
439        assert!(
440            matches!(
441                tx_status,
442                Ok(methods::tx::RpcTransactionStatusResponse { ref transaction, .. })
443                if transaction.signer_id == "miraclx.unc"
444                && transaction.hash == "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U".parse()?
445            ),
446            "expected an Ok(RpcTransactionStatusResponse) with matching signer_id + hash, found [{:?}]",
447            tx_status
448        );
449
450        Ok(())
451    }
452
453    #[tokio::test]
454    #[cfg(feature = "any")]
455    async fn any_typed_err() -> Result<(), Box<dyn std::error::Error>> {
456        let client = JsonRpcClient::connect("https://archival-rpc.mainnet.unc.org");
457
458        let tx_error = client
459            .call(methods::any::<methods::tx::RpcTransactionStatusRequest>(
460                "tx",
461                serde_json::json!([
462                    "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8D",
463                    "youser.unc",
464                ]),
465            ))
466            .await
467            .expect_err("request must not succeed");
468
469        assert!(
470            matches!(
471                tx_error.handler_error(),
472                Some(methods::tx::RpcTransactionError::UnknownTransaction {
473                    requested_transaction_hash
474                })
475                if requested_transaction_hash.to_string() == "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8D"
476            ),
477            "expected an Ok(RpcTransactionError::UnknownTransaction) with matching hash, found [{:?}]",
478            tx_error
479        );
480
481        Ok(())
482    }
483
484    #[tokio::test]
485    #[cfg(feature = "any")]
486    async fn any_untyped_ok() {
487        let client = JsonRpcClient::connect("https://archival-rpc.mainnet.unc.org");
488
489        let status = client
490            .call(
491                methods::any::<Result<serde_json::Value, serde_json::Value>>(
492                    "tx",
493                    serde_json::json!([
494                        "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
495                        "miraclx.unc",
496                    ]),
497                ),
498            )
499            .await
500            .expect("request must not fail");
501
502        assert_eq!(
503            status["transaction"]["signer_id"], "miraclx.unc",
504            "expected a tx_status with matching signer_id, [{:#}]",
505            status
506        );
507        assert_eq!(
508            status["transaction"]["hash"], "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8U",
509            "expected a tx_status with matching hash, [{:#}]",
510            status
511        );
512    }
513
514    #[tokio::test]
515    #[cfg(feature = "any")]
516    async fn any_untyped_err() {
517        let client = JsonRpcClient::connect("https://archival-rpc.mainnet.unc.org");
518
519        let tx_error = client
520            .call(
521                methods::any::<Result<serde_json::Value, serde_json::Value>>(
522                    "tx",
523                    serde_json::json!([
524                        "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8D",
525                        "youser.unc",
526                    ]),
527                ),
528            )
529            .await
530            .expect_err("request must not succeed");
531        let tx_error = tx_error
532            .handler_error()
533            .expect("expected a handler error from query request");
534
535        assert_eq!(
536            tx_error["info"]["requested_transaction_hash"],
537            "9FtHUFBQsZ2MG77K3x3MJ9wjX3UT8zE1TczCrhZEcG8D",
538            "expected an error with matching hash, [{:#}]",
539            tx_error
540        );
541        assert_eq!(
542            tx_error["name"], "UNKNOWN_TRANSACTION",
543            "expected an UnknownTransaction, [{:#}]",
544            tx_error
545        );
546    }
547}