1use 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#[derive(Clone)]
81pub struct JsonRpcClientConnector {
82 client: reqwest::Client,
83}
84
85impl JsonRpcClientConnector {
86 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)]
106pub 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 pub fn connect<U: AsUrl>(server_addr: U) -> JsonRpcClient {
151 DEFAULT_CONNECTOR.connect(server_addr)
152 }
153
154 pub fn server_addr(&self) -> &str {
167 &self.inner.server_addr
168 }
169
170 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 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 pub fn headers(&self) -> &reqwest::header::HeaderMap {
310 &self.headers
311 }
312
313 pub fn headers_mut(&mut self) -> &mut reqwest::header::HeaderMap {
315 &mut self.headers
316 }
317
318 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 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}