near_jsonrpc_client/
lib.rs

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