1use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use base64::{Engine as _, engine::general_purpose::STANDARD};
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8
9use crate::error::RpcError;
10use crate::types::{
11 AccessKeyListView, AccessKeyView, AccountId, AccountView, BlockReference, BlockView,
12 CryptoHash, GasPrice, PublicKey, SendTxResponse, SendTxWithReceiptsResponse, SignedTransaction,
13 StatusResponse, TxExecutionStatus, ViewFunctionResult,
14};
15
16pub struct NetworkConfig {
18 pub rpc_url: &'static str,
20 #[allow(dead_code)]
23 pub network_id: &'static str,
24}
25
26pub const MAINNET: NetworkConfig = NetworkConfig {
28 rpc_url: "https://free.rpc.fastnear.com",
29 network_id: "mainnet",
30};
31
32pub const TESTNET: NetworkConfig = NetworkConfig {
34 rpc_url: "https://test.rpc.fastnear.com",
35 network_id: "testnet",
36};
37
38#[derive(Clone, Debug)]
40pub struct RetryConfig {
41 pub max_retries: u32,
43 pub initial_delay_ms: u64,
45 pub max_delay_ms: u64,
47}
48
49impl Default for RetryConfig {
50 fn default() -> Self {
51 Self {
52 max_retries: 3,
53 initial_delay_ms: 500,
54 max_delay_ms: 5000,
55 }
56 }
57}
58
59#[derive(Serialize)]
61struct JsonRpcRequest<'a, P: Serialize> {
62 jsonrpc: &'static str,
63 id: u64,
64 method: &'a str,
65 params: P,
66}
67
68#[derive(Deserialize)]
74struct JsonRpcResponse {
75 #[allow(dead_code)]
76 jsonrpc: String,
77 #[allow(dead_code)]
78 id: u64,
79 result: Option<serde_json::Value>,
80 error: Option<JsonRpcError>,
81}
82
83#[derive(Debug, Deserialize)]
86struct JsonRpcError {
87 code: i64,
88 message: String,
89 #[serde(default)]
90 data: Option<serde_json::Value>,
91 #[serde(default)]
92 cause: Option<ErrorCause>,
93 #[serde(default)]
94 #[allow(dead_code)]
95 name: Option<String>,
96}
97
98#[derive(Debug, Deserialize)]
100struct ErrorCause {
101 name: String,
102 #[serde(default)]
103 info: Option<serde_json::Value>,
104}
105
106#[derive(Debug, Deserialize)]
110struct CallFunctionResponse {
111 result: Vec<u8>,
112 #[serde(default)]
113 logs: Vec<String>,
114 block_height: u64,
115 block_hash: CryptoHash,
116}
117
118pub struct RpcClient {
120 url: String,
121 client: reqwest::Client,
122 retry_config: RetryConfig,
123 request_id: AtomicU64,
124}
125
126impl RpcClient {
127 pub fn new(url: impl Into<String>) -> Self {
129 Self {
130 url: url.into(),
131 client: reqwest::Client::new(),
132 retry_config: RetryConfig::default(),
133 request_id: AtomicU64::new(0),
134 }
135 }
136
137 pub fn with_retry_config(url: impl Into<String>, retry_config: RetryConfig) -> Self {
139 Self {
140 url: url.into(),
141 client: reqwest::Client::new(),
142 retry_config,
143 request_id: AtomicU64::new(0),
144 }
145 }
146
147 pub fn url(&self) -> &str {
149 &self.url
150 }
151
152 #[tracing::instrument(skip(self, params), fields(rpc.method = method))]
154 pub async fn call<P: Serialize, R: DeserializeOwned>(
155 &self,
156 method: &str,
157 params: P,
158 ) -> Result<R, RpcError> {
159 let total_attempts = self.retry_config.max_retries + 1;
160
161 for attempt in 0..total_attempts {
162 let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
163
164 let request = JsonRpcRequest {
165 jsonrpc: "2.0",
166 id: request_id,
167 method,
168 params: ¶ms,
169 };
170
171 match self.try_call::<R>(&request).await {
172 Ok(result) => return Ok(result),
173 Err(e) if e.is_retryable() && attempt < total_attempts - 1 => {
174 let delay = std::cmp::min(
175 self.retry_config.initial_delay_ms * 2u64.pow(attempt),
176 self.retry_config.max_delay_ms,
177 );
178 tracing::warn!(
179 attempt = attempt + 1,
180 max_attempts = total_attempts,
181 delay_ms = delay,
182 error = %e,
183 "RPC request failed, retrying"
184 );
185 tokio::time::sleep(Duration::from_millis(delay)).await;
186 continue;
187 }
188 Err(e) => {
189 tracing::error!(error = %e, "RPC request failed");
190 return Err(e);
191 }
192 }
193 }
194
195 unreachable!("all loop iterations return")
196 }
197
198 async fn try_call<R: DeserializeOwned>(
200 &self,
201 request: &JsonRpcRequest<'_, impl Serialize>,
202 ) -> Result<R, RpcError> {
203 let response = self
204 .client
205 .post(&self.url)
206 .header("Content-Type", "application/json")
207 .json(request)
208 .send()
209 .await?;
210
211 let status = response.status();
212 let body = response.text().await?;
213
214 if !status.is_success() {
215 let retryable = is_retryable_status(status.as_u16());
216 return Err(RpcError::network(
217 format!("HTTP {}: {}", status, body),
218 Some(status.as_u16()),
219 retryable,
220 ));
221 }
222
223 let rpc_response: JsonRpcResponse = serde_json::from_str(&body).map_err(RpcError::Json)?;
224
225 if let Some(error) = rpc_response.error {
226 return Err(self.parse_rpc_error(&error));
227 }
228
229 let result_value = rpc_response
230 .result
231 .ok_or_else(|| RpcError::InvalidResponse("Missing result in response".to_string()))?;
232
233 if request.method == "query" {
238 if let Some(error_str) = result_value.get("error").and_then(|e| e.as_str()) {
239 let synthetic = JsonRpcError {
240 code: -32600,
243 message: error_str.to_string(),
244 data: Some(serde_json::Value::String(error_str.to_string())),
245 cause: None,
246 name: None,
247 };
248 return Err(self.parse_rpc_error(&synthetic));
249 }
250 }
251
252 serde_json::from_value(result_value).map_err(RpcError::Json)
253 }
254
255 fn parse_rpc_error(&self, error: &JsonRpcError) -> RpcError {
257 if let Some(cause) = &error.cause {
259 let cause_name = cause.name.as_str();
260 let info = cause.info.as_ref();
261 let data = &error.data;
262
263 match cause_name {
264 "UNKNOWN_ACCOUNT" => {
265 if let Some(account_id) = info
266 .and_then(|i| i.get("requested_account_id"))
267 .and_then(|a| a.as_str())
268 {
269 if let Ok(account_id) = account_id.parse() {
270 return RpcError::AccountNotFound(account_id);
271 }
272 }
273 }
274 "INVALID_ACCOUNT" => {
275 let account_id = info
276 .and_then(|i| i.get("requested_account_id"))
277 .and_then(|a| a.as_str())
278 .unwrap_or("unknown");
279 return RpcError::InvalidAccount(account_id.to_string());
280 }
281 "UNKNOWN_ACCESS_KEY" => {
282 if let Some(public_key) = info
283 .and_then(|i| i.get("public_key"))
284 .and_then(|k| k.as_str())
285 .and_then(|k| k.parse().ok())
286 {
287 let account_id = info
291 .and_then(|i| i.get("requested_account_id"))
292 .and_then(|a| a.as_str())
293 .and_then(|a| a.parse().ok())
294 .unwrap_or_else(|| "unknown".parse().unwrap());
295 return RpcError::AccessKeyNotFound {
296 account_id,
297 public_key,
298 };
299 }
300 }
301 "UNKNOWN_BLOCK" => {
302 let block_ref = data
303 .as_ref()
304 .and_then(|d| d.as_str())
305 .unwrap_or(&error.message);
306 return RpcError::UnknownBlock(block_ref.to_string());
307 }
308 "UNKNOWN_CHUNK" => {
309 let chunk_ref = info
310 .and_then(|i| i.get("chunk_hash"))
311 .and_then(|c| c.as_str())
312 .unwrap_or(&error.message);
313 return RpcError::UnknownChunk(chunk_ref.to_string());
314 }
315 "UNKNOWN_EPOCH" => {
316 let block_ref = data
317 .as_ref()
318 .and_then(|d| d.as_str())
319 .unwrap_or(&error.message);
320 return RpcError::UnknownEpoch(block_ref.to_string());
321 }
322 "UNKNOWN_RECEIPT" => {
323 let receipt_id = info
324 .and_then(|i| i.get("receipt_id"))
325 .and_then(|r| r.as_str())
326 .unwrap_or("unknown");
327 return RpcError::UnknownReceipt(receipt_id.to_string());
328 }
329 "NO_CONTRACT_CODE" => {
330 let account_id = info
331 .and_then(|i| {
332 i.get("contract_account_id")
333 .or_else(|| i.get("account_id"))
334 .or_else(|| i.get("contract_id"))
335 })
336 .and_then(|a| a.as_str())
337 .unwrap_or("unknown");
338 if let Ok(account_id) = account_id.parse() {
339 return RpcError::ContractNotDeployed(account_id);
340 }
341 }
342 "TOO_LARGE_CONTRACT_STATE" => {
343 let account_id = info
344 .and_then(|i| i.get("account_id").or_else(|| i.get("contract_id")))
345 .and_then(|a| a.as_str())
346 .unwrap_or("unknown");
347 if let Ok(account_id) = account_id.parse() {
348 return RpcError::ContractStateTooLarge(account_id);
349 }
350 }
351 "CONTRACT_EXECUTION_ERROR" => {
352 if let Some(vm_error) = info.and_then(|i| i.get("vm_error")) {
355 if let Some(compilation_err) = vm_error.get("CompilationError") {
356 if let Some(code_not_exist) = compilation_err.get("CodeDoesNotExist") {
357 if let Some(account_id) = code_not_exist
358 .get("account_id")
359 .and_then(|a| a.as_str())
360 .and_then(|a| a.parse().ok())
361 {
362 return RpcError::ContractNotDeployed(account_id);
363 }
364 }
365 }
366 }
367
368 let contract_id = info
372 .and_then(|i| i.get("contract_id"))
373 .and_then(|c| c.as_str())
374 .unwrap_or("unknown");
375 let method_name = info
376 .and_then(|i| i.get("method_name"))
377 .and_then(|m| m.as_str())
378 .map(String::from);
379 let message = data
380 .as_ref()
381 .and_then(|d| d.as_str())
382 .map(|s| s.to_string())
383 .or_else(|| {
384 info.and_then(|i| i.get("vm_error")).map(|v| v.to_string())
387 })
388 .unwrap_or_else(|| error.message.clone());
389 if let Ok(contract_id) = contract_id.parse() {
390 return RpcError::ContractExecution {
391 contract_id,
392 method_name,
393 message,
394 };
395 }
396 }
397 "UNAVAILABLE_SHARD" => {
398 return RpcError::ShardUnavailable(error.message.clone());
399 }
400 "NO_SYNCED_BLOCKS" | "NOT_SYNCED_YET" => {
401 return RpcError::NodeNotSynced(error.message.clone());
402 }
403 "INVALID_SHARD_ID" => {
404 let shard_id = info
405 .and_then(|i| i.get("shard_id"))
406 .map(|s| s.to_string())
407 .unwrap_or_else(|| "unknown".to_string());
408 return RpcError::InvalidShardId(shard_id);
409 }
410 "INVALID_TRANSACTION" => {
411 return RpcError::invalid_transaction(&error.message, data.clone());
412 }
413 "TIMEOUT_ERROR" => {
414 let tx_hash = info
415 .and_then(|i| i.get("transaction_hash"))
416 .and_then(|h| h.as_str())
417 .map(String::from);
418 return RpcError::RequestTimeout {
419 message: error.message.clone(),
420 transaction_hash: tx_hash,
421 };
422 }
423 "PARSE_ERROR" => {
424 return RpcError::ParseError(error.message.clone());
425 }
426 "INTERNAL_ERROR" => {
427 return RpcError::InternalError(error.message.clone());
428 }
429 _ => {}
430 }
431 }
432
433 if let Some(data) = &error.data {
435 if let Some(error_str) = data.as_str() {
436 if error_str.contains("does not exist") {
437 if let Some(start) = error_str.strip_prefix("account ") {
440 if let Some(account_str) = start.split_whitespace().next() {
441 if let Ok(account_id) = account_str.parse() {
442 return RpcError::AccountNotFound(account_id);
443 }
444 }
445 }
446 }
447 }
448 }
449
450 RpcError::Rpc {
451 code: error.code,
452 message: error.message.clone(),
453 data: error.data.clone(),
454 }
455 }
456
457 #[tracing::instrument(skip(self, block), fields(%account_id))]
463 pub async fn view_account(
464 &self,
465 account_id: &AccountId,
466 block: BlockReference,
467 ) -> Result<AccountView, RpcError> {
468 let mut params = serde_json::json!({
469 "account_id": account_id.to_string(),
470 });
471 self.merge_block_reference(&mut params, &block);
472 self.call("EXPERIMENTAL_view_account", params).await
473 }
474
475 #[tracing::instrument(skip(self, block), fields(%account_id, %public_key))]
477 pub async fn view_access_key(
478 &self,
479 account_id: &AccountId,
480 public_key: &PublicKey,
481 block: BlockReference,
482 ) -> Result<AccessKeyView, RpcError> {
483 let mut params = serde_json::json!({
484 "account_id": account_id.to_string(),
485 "public_key": public_key.to_string(),
486 });
487 self.merge_block_reference(&mut params, &block);
488 self.call("EXPERIMENTAL_view_access_key", params)
489 .await
490 .map_err(|e| match e {
491 RpcError::AccessKeyNotFound { public_key, .. } => RpcError::AccessKeyNotFound {
495 account_id: account_id.clone(),
496 public_key,
497 },
498 other => other,
499 })
500 }
501
502 #[tracing::instrument(skip(self, block), fields(%account_id))]
504 pub async fn view_access_key_list(
505 &self,
506 account_id: &AccountId,
507 block: BlockReference,
508 ) -> Result<AccessKeyListView, RpcError> {
509 let mut params = serde_json::json!({
510 "account_id": account_id.to_string(),
511 });
512 self.merge_block_reference(&mut params, &block);
513 self.call("EXPERIMENTAL_view_access_key_list", params).await
514 }
515
516 #[tracing::instrument(skip(self, args, block), fields(contract_id = %account_id, method = method_name))]
518 pub async fn view_function(
519 &self,
520 account_id: &AccountId,
521 method_name: &str,
522 args: &[u8],
523 block: BlockReference,
524 ) -> Result<ViewFunctionResult, RpcError> {
525 let mut params = serde_json::json!({
526 "account_id": account_id.to_string(),
527 "method_name": method_name,
528 "args_base64": STANDARD.encode(args),
529 });
530 self.merge_block_reference(&mut params, &block);
531
532 let response: CallFunctionResponse = self
537 .call("EXPERIMENTAL_call_function", params)
538 .await
539 .map_err(|e| match e {
540 RpcError::ContractExecution { message, .. } => RpcError::ContractExecution {
541 contract_id: account_id.clone(),
542 method_name: Some(method_name.to_string()),
543 message,
544 },
545 other => other,
546 })?;
547
548 Ok(ViewFunctionResult {
549 result: response.result,
550 logs: response.logs,
551 block_height: response.block_height,
552 block_hash: response.block_hash,
553 })
554 }
555
556 #[tracing::instrument(skip(self, block))]
558 pub async fn block(&self, block: BlockReference) -> Result<BlockView, RpcError> {
559 let params = block.to_rpc_params();
560 self.call("block", params).await
561 }
562
563 #[tracing::instrument(skip(self))]
565 pub async fn status(&self) -> Result<StatusResponse, RpcError> {
566 self.call("status", serde_json::json!([])).await
567 }
568
569 #[tracing::instrument(skip(self))]
571 pub async fn gas_price(&self, block_hash: Option<&CryptoHash>) -> Result<GasPrice, RpcError> {
572 let params = match block_hash {
573 Some(hash) => serde_json::json!([hash.to_string()]),
574 None => serde_json::json!([serde_json::Value::Null]),
575 };
576 self.call("gas_price", params).await
577 }
578
579 #[tracing::instrument(skip(self, signed_tx), fields(
581 tx_hash = tracing::field::Empty,
582 sender = %signed_tx.transaction.signer_id,
583 receiver = %signed_tx.transaction.receiver_id,
584 ?wait_until,
585 ))]
586 pub async fn send_tx(
587 &self,
588 signed_tx: &SignedTransaction,
589 wait_until: TxExecutionStatus,
590 ) -> Result<SendTxResponse, RpcError> {
591 let tx_hash = signed_tx.get_hash();
592 tracing::Span::current().record("tx_hash", tracing::field::display(&tx_hash));
593 let params = serde_json::json!({
594 "signed_tx_base64": signed_tx.to_base64(),
595 "wait_until": wait_until.as_str(),
596 });
597 let mut response: SendTxResponse = self.call("send_tx", params).await?;
598 response.transaction_hash = tx_hash;
599 Ok(response)
600 }
601
602 #[tracing::instrument(skip(self), fields(%tx_hash, sender = %sender_id, ?wait_until))]
606 pub async fn tx_status(
607 &self,
608 tx_hash: &CryptoHash,
609 sender_id: &AccountId,
610 wait_until: TxExecutionStatus,
611 ) -> Result<SendTxWithReceiptsResponse, RpcError> {
612 let params = serde_json::json!({
613 "tx_hash": tx_hash.to_string(),
614 "sender_account_id": sender_id.to_string(),
615 "wait_until": wait_until.as_str(),
616 });
617 self.call("EXPERIMENTAL_tx_status", params).await
618 }
619
620 fn merge_block_reference(&self, params: &mut serde_json::Value, block: &BlockReference) {
622 if let serde_json::Value::Object(block_params) = block.to_rpc_params() {
623 if let serde_json::Value::Object(map) = params {
624 map.extend(block_params);
625 }
626 }
627 }
628
629 pub async fn sandbox_fast_forward(&self, delta_height: u64) -> Result<(), RpcError> {
677 let params = serde_json::json!({
678 "delta_height": delta_height,
679 });
680
681 let _: serde_json::Value = self.call("sandbox_fast_forward", params).await?;
682 Ok(())
683 }
684
685 pub async fn sandbox_patch_state(&self, records: serde_json::Value) -> Result<(), RpcError> {
686 let params = serde_json::json!({
687 "records": records,
688 });
689
690 let _: serde_json::Value = self.call("sandbox_patch_state", params).await?;
692
693 let _: serde_json::Value = self
697 .call(
698 "sandbox_patch_state",
699 serde_json::json!({
700 "records": records,
701 }),
702 )
703 .await?;
704
705 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
707
708 Ok(())
709 }
710}
711
712impl Clone for RpcClient {
713 fn clone(&self) -> Self {
714 Self {
715 url: self.url.clone(),
716 client: self.client.clone(),
717 retry_config: self.retry_config.clone(),
718 request_id: AtomicU64::new(0),
719 }
720 }
721}
722
723impl std::fmt::Debug for RpcClient {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 f.debug_struct("RpcClient")
726 .field("url", &self.url)
727 .field("retry_config", &self.retry_config)
728 .finish()
729 }
730}
731
732fn is_retryable_status(status: u16) -> bool {
738 status == 408 || status == 429 || status == 503 || (500..600).contains(&status)
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
754 fn test_retry_config_default() {
755 let config = RetryConfig::default();
756 assert_eq!(config.max_retries, 3);
757 assert_eq!(config.initial_delay_ms, 500);
758 assert_eq!(config.max_delay_ms, 5000);
759 }
760
761 #[test]
762 fn test_retry_config_clone() {
763 let config = RetryConfig {
764 max_retries: 5,
765 initial_delay_ms: 100,
766 max_delay_ms: 1000,
767 };
768 let cloned = config.clone();
769 assert_eq!(cloned.max_retries, 5);
770 assert_eq!(cloned.initial_delay_ms, 100);
771 assert_eq!(cloned.max_delay_ms, 1000);
772 }
773
774 #[test]
775 fn test_retry_config_debug() {
776 let config = RetryConfig::default();
777 let debug = format!("{:?}", config);
778 assert!(debug.contains("RetryConfig"));
779 assert!(debug.contains("max_retries"));
780 }
781
782 #[test]
787 fn test_rpc_client_new() {
788 let client = RpcClient::new("https://rpc.testnet.near.org");
789 assert_eq!(client.url(), "https://rpc.testnet.near.org");
790 }
791
792 #[test]
793 fn test_rpc_client_with_retry_config() {
794 let config = RetryConfig {
795 max_retries: 5,
796 initial_delay_ms: 100,
797 max_delay_ms: 1000,
798 };
799 let client = RpcClient::with_retry_config("https://rpc.example.com", config);
800 assert_eq!(client.url(), "https://rpc.example.com");
801 }
802
803 #[test]
804 fn test_rpc_client_clone() {
805 let client = RpcClient::new("https://rpc.testnet.near.org");
806 let cloned = client.clone();
807 assert_eq!(cloned.url(), client.url());
808 }
809
810 #[test]
811 fn test_rpc_client_debug() {
812 let client = RpcClient::new("https://rpc.testnet.near.org");
813 let debug = format!("{:?}", client);
814 assert!(debug.contains("RpcClient"));
815 assert!(debug.contains("rpc.testnet.near.org"));
816 }
817
818 #[test]
823 fn test_is_retryable_status() {
824 assert!(is_retryable_status(408)); assert!(is_retryable_status(429)); assert!(is_retryable_status(500)); assert!(is_retryable_status(502)); assert!(is_retryable_status(503)); assert!(is_retryable_status(504)); assert!(is_retryable_status(599)); assert!(!is_retryable_status(200)); assert!(!is_retryable_status(201)); assert!(!is_retryable_status(400)); assert!(!is_retryable_status(401)); assert!(!is_retryable_status(403)); assert!(!is_retryable_status(404)); assert!(!is_retryable_status(422)); }
842
843 #[test]
848 fn test_invalid_transaction_parses_invalid_nonce() {
849 use crate::types::InvalidTxError;
850 let data = serde_json::json!({
851 "TxExecutionError": {
852 "InvalidTxError": {
853 "InvalidNonce": {
854 "tx_nonce": 5,
855 "ak_nonce": 10
856 }
857 }
858 }
859 });
860 let err = RpcError::invalid_transaction("invalid nonce", Some(data));
861 match err {
862 RpcError::InvalidTx(InvalidTxError::InvalidNonce { tx_nonce, ak_nonce }) => {
863 assert_eq!(tx_nonce, 5);
864 assert_eq!(ak_nonce, 10);
865 }
866 other => panic!("Expected InvalidTx(InvalidNonce), got: {other:?}"),
867 }
868 }
869
870 #[test]
871 fn test_invalid_transaction_parses_top_level_invalid_tx() {
872 use crate::types::InvalidTxError;
873 let data = serde_json::json!({
875 "InvalidTxError": {
876 "NotEnoughBalance": {
877 "signer_id": "alice.near",
878 "balance": "1000000000000000000000000",
879 "cost": "9000000000000000000000000"
880 }
881 }
882 });
883 let err = RpcError::invalid_transaction("insufficient balance", Some(data));
884 assert!(
885 matches!(
886 err,
887 RpcError::InvalidTx(InvalidTxError::NotEnoughBalance { .. })
888 ),
889 "Expected InvalidTx(NotEnoughBalance), got: {err:?}"
890 );
891 }
892
893 #[test]
894 fn test_invalid_transaction_falls_back_on_unparseable() {
895 let data = serde_json::json!({ "SomeOtherError": {} });
897 let err = RpcError::invalid_transaction("some error", Some(data));
898 assert!(matches!(err, RpcError::InvalidTransaction { .. }));
899 }
900
901 #[test]
906 fn test_mainnet_config() {
907 assert!(MAINNET.rpc_url.contains("fastnear"));
908 assert_eq!(MAINNET.network_id, "mainnet");
909 }
910
911 #[test]
912 fn test_testnet_config() {
913 assert!(TESTNET.rpc_url.contains("fastnear") || TESTNET.rpc_url.contains("test"));
914 assert_eq!(TESTNET.network_id, "testnet");
915 }
916
917 #[test]
922 fn test_parse_rpc_error_unknown_account() {
923 let client = RpcClient::new("https://example.com");
924 let error = JsonRpcError {
925 code: -32000,
926 message: "Server error".to_string(),
927 data: None,
928 cause: Some(ErrorCause {
929 name: "UNKNOWN_ACCOUNT".to_string(),
930 info: Some(serde_json::json!({
931 "requested_account_id": "nonexistent.near"
932 })),
933 }),
934 name: None,
935 };
936 let result = client.parse_rpc_error(&error);
937 assert!(matches!(result, RpcError::AccountNotFound(_)));
938 }
939
940 #[test]
941 fn test_parse_rpc_error_unknown_access_key_legacy() {
942 let client = RpcClient::new("https://example.com");
944 let error = JsonRpcError {
945 code: -32000,
946 message: "Server error".to_string(),
947 data: None,
948 cause: Some(ErrorCause {
949 name: "UNKNOWN_ACCESS_KEY".to_string(),
950 info: Some(serde_json::json!({
951 "requested_account_id": "alice.near",
952 "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
953 })),
954 }),
955 name: None,
956 };
957 let result = client.parse_rpc_error(&error);
958 match result {
959 RpcError::AccessKeyNotFound {
960 account_id,
961 public_key,
962 } => {
963 assert_eq!(account_id.as_str(), "alice.near");
964 assert!(public_key.to_string().contains("ed25519:"));
965 }
966 _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
967 }
968 }
969
970 #[test]
971 fn test_parse_rpc_error_unknown_access_key_experimental() {
972 let client = RpcClient::new("https://example.com");
974 let error = JsonRpcError {
975 code: -32000,
976 message: "Server error".to_string(),
977 data: Some(serde_json::Value::String(
978 "Access key for public key ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp does not exist while viewing".to_string()
979 )),
980 cause: Some(ErrorCause {
981 name: "UNKNOWN_ACCESS_KEY".to_string(),
982 info: Some(serde_json::json!({
983 "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
984 "block_height": 243789592,
985 "block_hash": "EC5A7qc6rixfN8T4T9Gkt78H5pAsvdcjAos8Z7kFLJgi"
986 })),
987 }),
988 name: Some("HANDLER_ERROR".to_string()),
989 };
990 let result = client.parse_rpc_error(&error);
991 match result {
992 RpcError::AccessKeyNotFound {
993 account_id,
994 public_key,
995 } => {
996 assert_eq!(account_id.as_str(), "unknown");
998 assert!(public_key.to_string().contains("ed25519:"));
999 }
1000 _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_parse_rpc_error_invalid_account() {
1006 let client = RpcClient::new("https://example.com");
1007 let error = JsonRpcError {
1008 code: -32000,
1009 message: "Server error".to_string(),
1010 data: None,
1011 cause: Some(ErrorCause {
1012 name: "INVALID_ACCOUNT".to_string(),
1013 info: Some(serde_json::json!({
1014 "requested_account_id": "invalid@account"
1015 })),
1016 }),
1017 name: None,
1018 };
1019 let result = client.parse_rpc_error(&error);
1020 assert!(matches!(result, RpcError::InvalidAccount(_)));
1021 }
1022
1023 #[test]
1024 fn test_parse_rpc_error_unknown_block() {
1025 let client = RpcClient::new("https://example.com");
1026 let error = JsonRpcError {
1027 code: -32000,
1028 message: "Block not found".to_string(),
1029 data: Some(serde_json::json!("12345")),
1030 cause: Some(ErrorCause {
1031 name: "UNKNOWN_BLOCK".to_string(),
1032 info: None,
1033 }),
1034 name: None,
1035 };
1036 let result = client.parse_rpc_error(&error);
1037 assert!(matches!(result, RpcError::UnknownBlock(_)));
1038 }
1039
1040 #[test]
1041 fn test_parse_rpc_error_unknown_chunk() {
1042 let client = RpcClient::new("https://example.com");
1043 let error = JsonRpcError {
1044 code: -32000,
1045 message: "Chunk not found".to_string(),
1046 data: None,
1047 cause: Some(ErrorCause {
1048 name: "UNKNOWN_CHUNK".to_string(),
1049 info: Some(serde_json::json!({
1050 "chunk_hash": "abc123"
1051 })),
1052 }),
1053 name: None,
1054 };
1055 let result = client.parse_rpc_error(&error);
1056 assert!(matches!(result, RpcError::UnknownChunk(_)));
1057 }
1058
1059 #[test]
1060 fn test_parse_rpc_error_unknown_epoch() {
1061 let client = RpcClient::new("https://example.com");
1062 let error = JsonRpcError {
1063 code: -32000,
1064 message: "Epoch not found".to_string(),
1065 data: Some(serde_json::json!("epoch123")),
1066 cause: Some(ErrorCause {
1067 name: "UNKNOWN_EPOCH".to_string(),
1068 info: None,
1069 }),
1070 name: None,
1071 };
1072 let result = client.parse_rpc_error(&error);
1073 assert!(matches!(result, RpcError::UnknownEpoch(_)));
1074 }
1075
1076 #[test]
1077 fn test_parse_rpc_error_unknown_receipt() {
1078 let client = RpcClient::new("https://example.com");
1079 let error = JsonRpcError {
1080 code: -32000,
1081 message: "Receipt not found".to_string(),
1082 data: None,
1083 cause: Some(ErrorCause {
1084 name: "UNKNOWN_RECEIPT".to_string(),
1085 info: Some(serde_json::json!({
1086 "receipt_id": "receipt123"
1087 })),
1088 }),
1089 name: None,
1090 };
1091 let result = client.parse_rpc_error(&error);
1092 assert!(matches!(result, RpcError::UnknownReceipt(_)));
1093 }
1094
1095 #[test]
1096 fn test_parse_rpc_error_no_contract_code() {
1097 let client = RpcClient::new("https://example.com");
1098 let error = JsonRpcError {
1099 code: -32000,
1100 message: "No contract code".to_string(),
1101 data: None,
1102 cause: Some(ErrorCause {
1103 name: "NO_CONTRACT_CODE".to_string(),
1104 info: Some(serde_json::json!({
1105 "contract_account_id": "no-contract.near"
1106 })),
1107 }),
1108 name: None,
1109 };
1110 let result = client.parse_rpc_error(&error);
1111 assert!(matches!(result, RpcError::ContractNotDeployed(_)));
1112 }
1113
1114 #[test]
1115 fn test_parse_rpc_error_too_large_contract_state() {
1116 let client = RpcClient::new("https://example.com");
1117 let error = JsonRpcError {
1118 code: -32000,
1119 message: "Contract state too large".to_string(),
1120 data: None,
1121 cause: Some(ErrorCause {
1122 name: "TOO_LARGE_CONTRACT_STATE".to_string(),
1123 info: Some(serde_json::json!({
1124 "account_id": "large-state.near"
1125 })),
1126 }),
1127 name: None,
1128 };
1129 let result = client.parse_rpc_error(&error);
1130 assert!(matches!(result, RpcError::ContractStateTooLarge(_)));
1131 }
1132
1133 #[test]
1134 fn test_parse_rpc_error_unavailable_shard() {
1135 let client = RpcClient::new("https://example.com");
1136 let error = JsonRpcError {
1137 code: -32000,
1138 message: "Shard unavailable".to_string(),
1139 data: None,
1140 cause: Some(ErrorCause {
1141 name: "UNAVAILABLE_SHARD".to_string(),
1142 info: None,
1143 }),
1144 name: None,
1145 };
1146 let result = client.parse_rpc_error(&error);
1147 assert!(matches!(result, RpcError::ShardUnavailable(_)));
1148 }
1149
1150 #[test]
1151 fn test_parse_rpc_error_not_synced() {
1152 let client = RpcClient::new("https://example.com");
1153
1154 let error = JsonRpcError {
1156 code: -32000,
1157 message: "No synced blocks".to_string(),
1158 data: None,
1159 cause: Some(ErrorCause {
1160 name: "NO_SYNCED_BLOCKS".to_string(),
1161 info: None,
1162 }),
1163 name: None,
1164 };
1165 let result = client.parse_rpc_error(&error);
1166 assert!(matches!(result, RpcError::NodeNotSynced(_)));
1167
1168 let error = JsonRpcError {
1170 code: -32000,
1171 message: "Not synced yet".to_string(),
1172 data: None,
1173 cause: Some(ErrorCause {
1174 name: "NOT_SYNCED_YET".to_string(),
1175 info: None,
1176 }),
1177 name: None,
1178 };
1179 let result = client.parse_rpc_error(&error);
1180 assert!(matches!(result, RpcError::NodeNotSynced(_)));
1181 }
1182
1183 #[test]
1184 fn test_parse_rpc_error_invalid_shard_id() {
1185 let client = RpcClient::new("https://example.com");
1186 let error = JsonRpcError {
1187 code: -32000,
1188 message: "Invalid shard ID".to_string(),
1189 data: None,
1190 cause: Some(ErrorCause {
1191 name: "INVALID_SHARD_ID".to_string(),
1192 info: Some(serde_json::json!({
1193 "shard_id": 99
1194 })),
1195 }),
1196 name: None,
1197 };
1198 let result = client.parse_rpc_error(&error);
1199 assert!(matches!(result, RpcError::InvalidShardId(_)));
1200 }
1201
1202 #[test]
1203 fn test_parse_rpc_error_invalid_transaction() {
1204 let client = RpcClient::new("https://example.com");
1205 let error = JsonRpcError {
1206 code: -32000,
1207 message: "Invalid transaction".to_string(),
1208 data: None,
1209 cause: Some(ErrorCause {
1210 name: "INVALID_TRANSACTION".to_string(),
1211 info: None,
1212 }),
1213 name: None,
1214 };
1215 let result = client.parse_rpc_error(&error);
1216 assert!(matches!(result, RpcError::InvalidTransaction { .. }));
1217 }
1218
1219 #[test]
1220 fn test_parse_rpc_error_timeout() {
1221 let client = RpcClient::new("https://example.com");
1222 let error = JsonRpcError {
1223 code: -32000,
1224 message: "Request timed out".to_string(),
1225 data: None,
1226 cause: Some(ErrorCause {
1227 name: "TIMEOUT_ERROR".to_string(),
1228 info: Some(serde_json::json!({
1229 "transaction_hash": "tx123"
1230 })),
1231 }),
1232 name: None,
1233 };
1234 let result = client.parse_rpc_error(&error);
1235 assert!(matches!(result, RpcError::RequestTimeout { .. }));
1236 }
1237
1238 #[test]
1239 fn test_parse_rpc_error_parse_error() {
1240 let client = RpcClient::new("https://example.com");
1241 let error = JsonRpcError {
1242 code: -32700,
1243 message: "Parse error".to_string(),
1244 data: None,
1245 cause: Some(ErrorCause {
1246 name: "PARSE_ERROR".to_string(),
1247 info: None,
1248 }),
1249 name: None,
1250 };
1251 let result = client.parse_rpc_error(&error);
1252 assert!(matches!(result, RpcError::ParseError(_)));
1253 }
1254
1255 #[test]
1256 fn test_parse_rpc_error_internal_error() {
1257 let client = RpcClient::new("https://example.com");
1258 let error = JsonRpcError {
1259 code: -32603,
1260 message: "Internal error".to_string(),
1261 data: None,
1262 cause: Some(ErrorCause {
1263 name: "INTERNAL_ERROR".to_string(),
1264 info: None,
1265 }),
1266 name: None,
1267 };
1268 let result = client.parse_rpc_error(&error);
1269 assert!(matches!(result, RpcError::InternalError(_)));
1270 }
1271
1272 #[test]
1273 fn test_parse_rpc_error_contract_execution_legacy() {
1274 let client = RpcClient::new("https://example.com");
1276 let error = JsonRpcError {
1277 code: -32000,
1278 message: "Contract execution failed".to_string(),
1279 data: None,
1280 cause: Some(ErrorCause {
1281 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1282 info: Some(serde_json::json!({
1283 "contract_id": "contract.near",
1284 "method_name": "my_method"
1285 })),
1286 }),
1287 name: None,
1288 };
1289 let result = client.parse_rpc_error(&error);
1290 match result {
1291 RpcError::ContractExecution {
1292 contract_id,
1293 method_name,
1294 ..
1295 } => {
1296 assert_eq!(contract_id.as_str(), "contract.near");
1297 assert_eq!(method_name.as_deref(), Some("my_method"));
1298 }
1299 _ => panic!("Expected ContractExecution error, got {:?}", result),
1300 }
1301 }
1302
1303 #[test]
1304 fn test_parse_rpc_error_contract_execution_experimental() {
1305 let client = RpcClient::new("https://example.com");
1308 let error = JsonRpcError {
1309 code: -32000,
1310 message: "Server error".to_string(),
1311 data: Some(serde_json::json!(
1312 "Function call returned an error: MethodResolveError(MethodNotFound)"
1313 )),
1314 cause: Some(ErrorCause {
1315 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1316 info: Some(serde_json::json!({
1317 "vm_error": { "MethodResolveError": "MethodNotFound" },
1318 "block_height": 243803767,
1319 "block_hash": "Et7So7jtsorkYLdVMMgV8gxA3Cfaztp75Ti6TPv2A"
1320 })),
1321 }),
1322 name: Some("HANDLER_ERROR".to_string()),
1323 };
1324 let result = client.parse_rpc_error(&error);
1325 match result {
1326 RpcError::ContractExecution {
1327 contract_id,
1328 message,
1329 ..
1330 } => {
1331 assert_eq!(contract_id.as_str(), "unknown");
1333 assert!(message.contains("MethodResolveError"));
1334 }
1335 _ => panic!("Expected ContractExecution error, got {:?}", result),
1336 }
1337 }
1338
1339 #[test]
1340 fn test_parse_rpc_error_code_does_not_exist_experimental() {
1341 let client = RpcClient::new("https://example.com");
1343 let error = JsonRpcError {
1344 code: -32000,
1345 message: "Server error".to_string(),
1346 data: Some(serde_json::json!(
1347 "Function call returned an error: CompilationError(CodeDoesNotExist { account_id: AccountId(\"nonexistent.testnet\") })"
1348 )),
1349 cause: Some(ErrorCause {
1350 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1351 info: Some(serde_json::json!({
1352 "vm_error": {
1353 "CompilationError": {
1354 "CodeDoesNotExist": {
1355 "account_id": "nonexistent.testnet"
1356 }
1357 }
1358 },
1359 "block_height": 243803764,
1360 "block_hash": "H33oNAtVZDJjhpncQb5LY6NxYzQLMMVLptq99mwmLmnj"
1361 })),
1362 }),
1363 name: Some("HANDLER_ERROR".to_string()),
1364 };
1365 let result = client.parse_rpc_error(&error);
1366 match result {
1367 RpcError::ContractNotDeployed(account_id) => {
1368 assert_eq!(account_id.as_str(), "nonexistent.testnet");
1369 }
1370 _ => panic!("Expected ContractNotDeployed error, got {:?}", result),
1371 }
1372 }
1373
1374 #[test]
1375 fn test_parse_rpc_error_fallback_account_not_exist() {
1376 let client = RpcClient::new("https://example.com");
1377 let error = JsonRpcError {
1378 code: -32000,
1379 message: "Error".to_string(),
1380 data: Some(serde_json::json!(
1381 "account missing.near does not exist while viewing"
1382 )),
1383 cause: None,
1384 name: None,
1385 };
1386 let result = client.parse_rpc_error(&error);
1387 assert!(matches!(result, RpcError::AccountNotFound(_)));
1388 }
1389
1390 #[test]
1391 fn test_parse_rpc_error_unknown_cause_fallback_to_generic() {
1392 let client = RpcClient::new("https://example.com");
1393 let error = JsonRpcError {
1394 code: -32000,
1395 message: "Some error".to_string(),
1396 data: Some(serde_json::json!("some data")),
1397 cause: Some(ErrorCause {
1398 name: "UNKNOWN_ERROR_TYPE".to_string(),
1399 info: None,
1400 }),
1401 name: None,
1402 };
1403 let result = client.parse_rpc_error(&error);
1404 assert!(matches!(result, RpcError::Rpc { .. }));
1405 }
1406
1407 #[test]
1408 fn test_parse_rpc_error_no_cause_fallback_to_generic() {
1409 let client = RpcClient::new("https://example.com");
1410 let error = JsonRpcError {
1411 code: -32600,
1412 message: "Invalid request".to_string(),
1413 data: None,
1414 cause: None,
1415 name: None,
1416 };
1417 let result = client.parse_rpc_error(&error);
1418 match result {
1419 RpcError::Rpc { code, message, .. } => {
1420 assert_eq!(code, -32600);
1421 assert_eq!(message, "Invalid request");
1422 }
1423 _ => panic!("Expected generic Rpc error"),
1424 }
1425 }
1426}