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)]
70struct JsonRpcResponse<T> {
71 #[allow(dead_code)]
72 jsonrpc: String,
73 #[allow(dead_code)]
74 id: u64,
75 result: Option<T>,
76 error: Option<JsonRpcError>,
77}
78
79#[derive(Debug, Deserialize)]
82struct JsonRpcError {
83 code: i64,
84 message: String,
85 #[serde(default)]
86 data: Option<serde_json::Value>,
87 #[serde(default)]
88 cause: Option<ErrorCause>,
89 #[serde(default)]
90 #[allow(dead_code)]
91 name: Option<String>,
92}
93
94#[derive(Debug, Deserialize)]
96struct ErrorCause {
97 name: String,
98 #[serde(default)]
99 info: Option<serde_json::Value>,
100}
101
102#[derive(Debug, Deserialize)]
105struct QueryResponse {
106 #[serde(default)]
107 result: Option<Vec<u8>>,
108 #[serde(default)]
109 logs: Vec<String>,
110 block_height: u64,
111 block_hash: CryptoHash,
112 #[serde(default)]
113 error: Option<String>,
114}
115
116pub struct RpcClient {
118 url: String,
119 client: reqwest::Client,
120 retry_config: RetryConfig,
121 request_id: AtomicU64,
122}
123
124impl RpcClient {
125 pub fn new(url: impl Into<String>) -> Self {
127 Self {
128 url: url.into(),
129 client: reqwest::Client::new(),
130 retry_config: RetryConfig::default(),
131 request_id: AtomicU64::new(0),
132 }
133 }
134
135 pub fn with_retry_config(url: impl Into<String>, retry_config: RetryConfig) -> Self {
137 Self {
138 url: url.into(),
139 client: reqwest::Client::new(),
140 retry_config,
141 request_id: AtomicU64::new(0),
142 }
143 }
144
145 pub fn url(&self) -> &str {
147 &self.url
148 }
149
150 pub async fn call<P: Serialize, R: DeserializeOwned>(
152 &self,
153 method: &str,
154 params: P,
155 ) -> Result<R, RpcError> {
156 let total_attempts = self.retry_config.max_retries + 1;
157
158 for attempt in 0..total_attempts {
159 let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
160
161 let request = JsonRpcRequest {
162 jsonrpc: "2.0",
163 id: request_id,
164 method,
165 params: ¶ms,
166 };
167
168 match self.try_call::<R>(&request).await {
169 Ok(result) => return Ok(result),
170 Err(e) if e.is_retryable() && attempt < total_attempts - 1 => {
171 let delay = std::cmp::min(
172 self.retry_config.initial_delay_ms * 2u64.pow(attempt),
173 self.retry_config.max_delay_ms,
174 );
175 tokio::time::sleep(Duration::from_millis(delay)).await;
176 continue;
177 }
178 Err(e) => return Err(e),
179 }
180 }
181
182 Err(RpcError::Timeout(total_attempts))
183 }
184
185 async fn try_call<R: DeserializeOwned>(
187 &self,
188 request: &JsonRpcRequest<'_, impl Serialize>,
189 ) -> Result<R, RpcError> {
190 let response = self
191 .client
192 .post(&self.url)
193 .header("Content-Type", "application/json")
194 .json(request)
195 .send()
196 .await?;
197
198 let status = response.status();
199 let body = response.text().await?;
200
201 if !status.is_success() {
202 let retryable = is_retryable_status(status.as_u16());
203 return Err(RpcError::network(
204 format!("HTTP {}: {}", status, body),
205 Some(status.as_u16()),
206 retryable,
207 ));
208 }
209
210 let rpc_response: JsonRpcResponse<R> =
211 serde_json::from_str(&body).map_err(RpcError::Json)?;
212
213 if let Some(error) = rpc_response.error {
214 return Err(self.parse_rpc_error(&error));
215 }
216
217 rpc_response
218 .result
219 .ok_or_else(|| RpcError::InvalidResponse("Missing result in response".to_string()))
220 }
221
222 fn parse_rpc_error(&self, error: &JsonRpcError) -> RpcError {
224 if let Some(cause) = &error.cause {
226 let cause_name = cause.name.as_str();
227 let info = cause.info.as_ref();
228 let data = &error.data;
229
230 match cause_name {
231 "UNKNOWN_ACCOUNT" => {
232 if let Some(account_id) = info
233 .and_then(|i| i.get("requested_account_id"))
234 .and_then(|a| a.as_str())
235 {
236 if let Ok(account_id) = account_id.parse() {
237 return RpcError::AccountNotFound(account_id);
238 }
239 }
240 }
241 "INVALID_ACCOUNT" => {
242 let account_id = info
243 .and_then(|i| i.get("requested_account_id"))
244 .and_then(|a| a.as_str())
245 .unwrap_or("unknown");
246 return RpcError::InvalidAccount(account_id.to_string());
247 }
248 "UNKNOWN_ACCESS_KEY" => {
249 if let (Some(account_id), Some(public_key)) = (
250 info.and_then(|i| i.get("requested_account_id"))
251 .and_then(|a| a.as_str()),
252 info.and_then(|i| i.get("public_key"))
253 .and_then(|k| k.as_str()),
254 ) {
255 if let (Ok(account_id), Ok(public_key)) =
256 (account_id.parse(), public_key.parse())
257 {
258 return RpcError::AccessKeyNotFound {
259 account_id,
260 public_key,
261 };
262 }
263 }
264 }
265 "UNKNOWN_BLOCK" => {
266 let block_ref = data
267 .as_ref()
268 .and_then(|d| d.as_str())
269 .unwrap_or(&error.message);
270 return RpcError::UnknownBlock(block_ref.to_string());
271 }
272 "UNKNOWN_CHUNK" => {
273 let chunk_ref = info
274 .and_then(|i| i.get("chunk_hash"))
275 .and_then(|c| c.as_str())
276 .unwrap_or(&error.message);
277 return RpcError::UnknownChunk(chunk_ref.to_string());
278 }
279 "UNKNOWN_EPOCH" => {
280 let block_ref = data
281 .as_ref()
282 .and_then(|d| d.as_str())
283 .unwrap_or(&error.message);
284 return RpcError::UnknownEpoch(block_ref.to_string());
285 }
286 "UNKNOWN_RECEIPT" => {
287 let receipt_id = info
288 .and_then(|i| i.get("receipt_id"))
289 .and_then(|r| r.as_str())
290 .unwrap_or("unknown");
291 return RpcError::UnknownReceipt(receipt_id.to_string());
292 }
293 "NO_CONTRACT_CODE" => {
294 let account_id = info
295 .and_then(|i| {
296 i.get("contract_account_id")
297 .or_else(|| i.get("account_id"))
298 .or_else(|| i.get("contract_id"))
299 })
300 .and_then(|a| a.as_str())
301 .unwrap_or("unknown");
302 if let Ok(account_id) = account_id.parse() {
303 return RpcError::ContractNotDeployed(account_id);
304 }
305 }
306 "TOO_LARGE_CONTRACT_STATE" => {
307 let account_id = info
308 .and_then(|i| i.get("account_id").or_else(|| i.get("contract_id")))
309 .and_then(|a| a.as_str())
310 .unwrap_or("unknown");
311 if let Ok(account_id) = account_id.parse() {
312 return RpcError::ContractStateTooLarge(account_id);
313 }
314 }
315 "CONTRACT_EXECUTION_ERROR" => {
316 let contract_id = info
317 .and_then(|i| i.get("contract_id"))
318 .and_then(|c| c.as_str())
319 .unwrap_or("unknown");
320 let method_name = info
321 .and_then(|i| i.get("method_name"))
322 .and_then(|m| m.as_str())
323 .map(String::from);
324 if let Ok(contract_id) = contract_id.parse() {
325 return RpcError::ContractExecution {
326 contract_id,
327 method_name,
328 message: error.message.clone(),
329 };
330 }
331 }
332 "UNAVAILABLE_SHARD" => {
333 return RpcError::ShardUnavailable(error.message.clone());
334 }
335 "NO_SYNCED_BLOCKS" | "NOT_SYNCED_YET" => {
336 return RpcError::NodeNotSynced(error.message.clone());
337 }
338 "INVALID_SHARD_ID" => {
339 let shard_id = info
340 .and_then(|i| i.get("shard_id"))
341 .map(|s| s.to_string())
342 .unwrap_or_else(|| "unknown".to_string());
343 return RpcError::InvalidShardId(shard_id);
344 }
345 "INVALID_TRANSACTION" => {
346 if let Some(invalid_nonce) = data.as_ref().and_then(extract_invalid_nonce) {
347 return invalid_nonce;
348 }
349 return RpcError::invalid_transaction(&error.message, data.clone());
350 }
351 "TIMEOUT_ERROR" => {
352 let tx_hash = info
353 .and_then(|i| i.get("transaction_hash"))
354 .and_then(|h| h.as_str())
355 .map(String::from);
356 return RpcError::RequestTimeout {
357 message: error.message.clone(),
358 transaction_hash: tx_hash,
359 };
360 }
361 "PARSE_ERROR" => {
362 return RpcError::ParseError(error.message.clone());
363 }
364 "INTERNAL_ERROR" => {
365 return RpcError::InternalError(error.message.clone());
366 }
367 _ => {}
368 }
369 }
370
371 if let Some(data) = &error.data {
373 if let Some(error_str) = data.as_str() {
374 if error_str.contains("does not exist") {
375 if let Some(start) = error_str.strip_prefix("account ") {
378 if let Some(account_str) = start.split_whitespace().next() {
379 if let Ok(account_id) = account_str.parse() {
380 return RpcError::AccountNotFound(account_id);
381 }
382 }
383 }
384 }
385 }
386 }
387
388 RpcError::Rpc {
389 code: error.code,
390 message: error.message.clone(),
391 data: error.data.clone(),
392 }
393 }
394
395 pub async fn view_account(
401 &self,
402 account_id: &AccountId,
403 block: BlockReference,
404 ) -> Result<AccountView, RpcError> {
405 let mut params = serde_json::json!({
406 "request_type": "view_account",
407 "account_id": account_id.to_string(),
408 });
409
410 self.merge_block_reference(&mut params, &block);
411 self.call("query", params).await
412 }
413
414 pub async fn view_access_key(
416 &self,
417 account_id: &AccountId,
418 public_key: &PublicKey,
419 block: BlockReference,
420 ) -> Result<AccessKeyView, RpcError> {
421 let mut params = serde_json::json!({
422 "request_type": "view_access_key",
423 "account_id": account_id.to_string(),
424 "public_key": public_key.to_string(),
425 });
426
427 self.merge_block_reference(&mut params, &block);
428 self.call("query", params).await
429 }
430
431 pub async fn view_access_key_list(
433 &self,
434 account_id: &AccountId,
435 block: BlockReference,
436 ) -> Result<AccessKeyListView, RpcError> {
437 let mut params = serde_json::json!({
438 "request_type": "view_access_key_list",
439 "account_id": account_id.to_string(),
440 });
441
442 self.merge_block_reference(&mut params, &block);
443 self.call("query", params).await
444 }
445
446 pub async fn view_function(
448 &self,
449 account_id: &AccountId,
450 method_name: &str,
451 args: &[u8],
452 block: BlockReference,
453 ) -> Result<ViewFunctionResult, RpcError> {
454 let mut params = serde_json::json!({
455 "request_type": "call_function",
456 "account_id": account_id.to_string(),
457 "method_name": method_name,
458 "args_base64": STANDARD.encode(args),
459 });
460
461 self.merge_block_reference(&mut params, &block);
462
463 let response: QueryResponse = self.call("query", params).await?;
465
466 if let Some(error) = response.error {
467 if error.contains("CodeDoesNotExist") {
469 return Err(RpcError::ContractNotDeployed(account_id.clone()));
470 }
471 if error.contains("MethodNotFound") || error.contains("MethodResolveError") {
472 return Err(RpcError::ContractExecution {
473 contract_id: account_id.clone(),
474 method_name: Some(method_name.to_string()),
475 message: error,
476 });
477 }
478 return Err(RpcError::ContractExecution {
479 contract_id: account_id.clone(),
480 method_name: Some(method_name.to_string()),
481 message: error,
482 });
483 }
484
485 Ok(ViewFunctionResult {
486 result: response.result.unwrap_or_default(),
487 logs: response.logs,
488 block_height: response.block_height,
489 block_hash: response.block_hash,
490 })
491 }
492
493 pub async fn block(&self, block: BlockReference) -> Result<BlockView, RpcError> {
495 let params = block.to_rpc_params();
496 self.call("block", params).await
497 }
498
499 pub async fn status(&self) -> Result<StatusResponse, RpcError> {
501 self.call("status", serde_json::json!([])).await
502 }
503
504 pub async fn gas_price(&self, block_hash: Option<&CryptoHash>) -> Result<GasPrice, RpcError> {
506 let params = match block_hash {
507 Some(hash) => serde_json::json!([hash.to_string()]),
508 None => serde_json::json!([serde_json::Value::Null]),
509 };
510 self.call("gas_price", params).await
511 }
512
513 pub async fn send_tx(
515 &self,
516 signed_tx: &SignedTransaction,
517 wait_until: TxExecutionStatus,
518 ) -> Result<SendTxResponse, RpcError> {
519 let tx_hash = signed_tx.get_hash();
520 let params = serde_json::json!({
521 "signed_tx_base64": signed_tx.to_base64(),
522 "wait_until": wait_until.as_str(),
523 });
524 let mut response: SendTxResponse = self.call("send_tx", params).await?;
525 response.transaction_hash = tx_hash;
526 Ok(response)
527 }
528
529 pub async fn tx_status(
533 &self,
534 tx_hash: &CryptoHash,
535 sender_id: &AccountId,
536 wait_until: TxExecutionStatus,
537 ) -> Result<SendTxWithReceiptsResponse, RpcError> {
538 let params = serde_json::json!({
539 "tx_hash": tx_hash.to_string(),
540 "sender_account_id": sender_id.to_string(),
541 "wait_until": wait_until.as_str(),
542 });
543 self.call("EXPERIMENTAL_tx_status", params).await
544 }
545
546 fn merge_block_reference(&self, params: &mut serde_json::Value, block: &BlockReference) {
548 if let serde_json::Value::Object(block_params) = block.to_rpc_params() {
549 if let serde_json::Value::Object(map) = params {
550 map.extend(block_params);
551 }
552 }
553 }
554
555 pub async fn sandbox_patch_state(&self, records: serde_json::Value) -> Result<(), RpcError> {
588 let params = serde_json::json!({
589 "records": records,
590 });
591
592 let _: serde_json::Value = self.call("sandbox_patch_state", params).await?;
594
595 let _: serde_json::Value = self
599 .call(
600 "sandbox_patch_state",
601 serde_json::json!({
602 "records": records,
603 }),
604 )
605 .await?;
606
607 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
609
610 Ok(())
611 }
612}
613
614impl Clone for RpcClient {
615 fn clone(&self) -> Self {
616 Self {
617 url: self.url.clone(),
618 client: self.client.clone(),
619 retry_config: self.retry_config.clone(),
620 request_id: AtomicU64::new(0),
621 }
622 }
623}
624
625impl std::fmt::Debug for RpcClient {
626 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627 f.debug_struct("RpcClient")
628 .field("url", &self.url)
629 .field("retry_config", &self.retry_config)
630 .finish()
631 }
632}
633
634fn is_retryable_status(status: u16) -> bool {
640 status == 408 || status == 429 || status == 503 || (500..600).contains(&status)
645}
646
647fn extract_invalid_nonce(data: &serde_json::Value) -> Option<RpcError> {
649 let tx_exec_error = data.get("TxExecutionError")?;
651 let invalid_tx_error = tx_exec_error
652 .get("InvalidTxError")
653 .or_else(|| data.get("InvalidTxError"))?;
654 let invalid_nonce = invalid_tx_error.get("InvalidNonce")?;
655
656 let ak_nonce = invalid_nonce.get("ak_nonce")?.as_u64()?;
657 let tx_nonce = invalid_nonce.get("tx_nonce")?.as_u64()?;
658
659 Some(RpcError::InvalidNonce { tx_nonce, ak_nonce })
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
671 fn test_retry_config_default() {
672 let config = RetryConfig::default();
673 assert_eq!(config.max_retries, 3);
674 assert_eq!(config.initial_delay_ms, 500);
675 assert_eq!(config.max_delay_ms, 5000);
676 }
677
678 #[test]
679 fn test_retry_config_clone() {
680 let config = RetryConfig {
681 max_retries: 5,
682 initial_delay_ms: 100,
683 max_delay_ms: 1000,
684 };
685 let cloned = config.clone();
686 assert_eq!(cloned.max_retries, 5);
687 assert_eq!(cloned.initial_delay_ms, 100);
688 assert_eq!(cloned.max_delay_ms, 1000);
689 }
690
691 #[test]
692 fn test_retry_config_debug() {
693 let config = RetryConfig::default();
694 let debug = format!("{:?}", config);
695 assert!(debug.contains("RetryConfig"));
696 assert!(debug.contains("max_retries"));
697 }
698
699 #[test]
704 fn test_rpc_client_new() {
705 let client = RpcClient::new("https://rpc.testnet.near.org");
706 assert_eq!(client.url(), "https://rpc.testnet.near.org");
707 }
708
709 #[test]
710 fn test_rpc_client_with_retry_config() {
711 let config = RetryConfig {
712 max_retries: 5,
713 initial_delay_ms: 100,
714 max_delay_ms: 1000,
715 };
716 let client = RpcClient::with_retry_config("https://rpc.example.com", config);
717 assert_eq!(client.url(), "https://rpc.example.com");
718 }
719
720 #[test]
721 fn test_rpc_client_clone() {
722 let client = RpcClient::new("https://rpc.testnet.near.org");
723 let cloned = client.clone();
724 assert_eq!(cloned.url(), client.url());
725 }
726
727 #[test]
728 fn test_rpc_client_debug() {
729 let client = RpcClient::new("https://rpc.testnet.near.org");
730 let debug = format!("{:?}", client);
731 assert!(debug.contains("RpcClient"));
732 assert!(debug.contains("rpc.testnet.near.org"));
733 }
734
735 #[test]
740 fn test_is_retryable_status() {
741 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)); }
759
760 #[test]
765 fn test_extract_invalid_nonce_success() {
766 let data = serde_json::json!({
767 "TxExecutionError": {
768 "InvalidTxError": {
769 "InvalidNonce": {
770 "tx_nonce": 5,
771 "ak_nonce": 10
772 }
773 }
774 }
775 });
776 let result = extract_invalid_nonce(&data);
777 assert!(result.is_some());
778 match result.unwrap() {
779 RpcError::InvalidNonce { tx_nonce, ak_nonce } => {
780 assert_eq!(tx_nonce, 5);
781 assert_eq!(ak_nonce, 10);
782 }
783 _ => panic!("Expected InvalidNonce error"),
784 }
785 }
786
787 #[test]
788 fn test_extract_invalid_nonce_missing_fields() {
789 let data = serde_json::json!({
791 "SomeOtherError": {}
792 });
793 assert!(extract_invalid_nonce(&data).is_none());
794
795 let data = serde_json::json!({
797 "TxExecutionError": {
798 "SomeOtherError": {}
799 }
800 });
801 assert!(extract_invalid_nonce(&data).is_none());
802
803 let data = serde_json::json!({
805 "TxExecutionError": {
806 "InvalidTxError": {
807 "SomeOtherError": {}
808 }
809 }
810 });
811 assert!(extract_invalid_nonce(&data).is_none());
812
813 let data = serde_json::json!({
815 "TxExecutionError": {
816 "InvalidTxError": {
817 "InvalidNonce": {
818 "ak_nonce": 10
819 }
820 }
821 }
822 });
823 assert!(extract_invalid_nonce(&data).is_none());
824
825 let data = serde_json::json!({
827 "TxExecutionError": {
828 "InvalidTxError": {
829 "InvalidNonce": {
830 "tx_nonce": 5
831 }
832 }
833 }
834 });
835 assert!(extract_invalid_nonce(&data).is_none());
836 }
837
838 #[test]
843 fn test_mainnet_config() {
844 assert!(MAINNET.rpc_url.contains("fastnear"));
845 assert_eq!(MAINNET.network_id, "mainnet");
846 }
847
848 #[test]
849 fn test_testnet_config() {
850 assert!(TESTNET.rpc_url.contains("fastnear") || TESTNET.rpc_url.contains("test"));
851 assert_eq!(TESTNET.network_id, "testnet");
852 }
853
854 #[test]
859 fn test_parse_rpc_error_unknown_account() {
860 let client = RpcClient::new("https://example.com");
861 let error = JsonRpcError {
862 code: -32000,
863 message: "Server error".to_string(),
864 data: None,
865 cause: Some(ErrorCause {
866 name: "UNKNOWN_ACCOUNT".to_string(),
867 info: Some(serde_json::json!({
868 "requested_account_id": "nonexistent.near"
869 })),
870 }),
871 name: None,
872 };
873 let result = client.parse_rpc_error(&error);
874 assert!(matches!(result, RpcError::AccountNotFound(_)));
875 }
876
877 #[test]
878 fn test_parse_rpc_error_unknown_access_key() {
879 let client = RpcClient::new("https://example.com");
880 let error = JsonRpcError {
881 code: -32000,
882 message: "Server error".to_string(),
883 data: None,
884 cause: Some(ErrorCause {
885 name: "UNKNOWN_ACCESS_KEY".to_string(),
886 info: Some(serde_json::json!({
887 "requested_account_id": "alice.near",
888 "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
889 })),
890 }),
891 name: None,
892 };
893 let result = client.parse_rpc_error(&error);
894 match result {
895 RpcError::AccessKeyNotFound {
896 account_id,
897 public_key,
898 } => {
899 assert_eq!(account_id.as_ref(), "alice.near");
900 assert!(public_key.to_string().contains("ed25519:"));
901 }
902 _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
903 }
904 }
905
906 #[test]
907 fn test_parse_rpc_error_invalid_account() {
908 let client = RpcClient::new("https://example.com");
909 let error = JsonRpcError {
910 code: -32000,
911 message: "Server error".to_string(),
912 data: None,
913 cause: Some(ErrorCause {
914 name: "INVALID_ACCOUNT".to_string(),
915 info: Some(serde_json::json!({
916 "requested_account_id": "invalid@account"
917 })),
918 }),
919 name: None,
920 };
921 let result = client.parse_rpc_error(&error);
922 assert!(matches!(result, RpcError::InvalidAccount(_)));
923 }
924
925 #[test]
926 fn test_parse_rpc_error_unknown_block() {
927 let client = RpcClient::new("https://example.com");
928 let error = JsonRpcError {
929 code: -32000,
930 message: "Block not found".to_string(),
931 data: Some(serde_json::json!("12345")),
932 cause: Some(ErrorCause {
933 name: "UNKNOWN_BLOCK".to_string(),
934 info: None,
935 }),
936 name: None,
937 };
938 let result = client.parse_rpc_error(&error);
939 assert!(matches!(result, RpcError::UnknownBlock(_)));
940 }
941
942 #[test]
943 fn test_parse_rpc_error_unknown_chunk() {
944 let client = RpcClient::new("https://example.com");
945 let error = JsonRpcError {
946 code: -32000,
947 message: "Chunk not found".to_string(),
948 data: None,
949 cause: Some(ErrorCause {
950 name: "UNKNOWN_CHUNK".to_string(),
951 info: Some(serde_json::json!({
952 "chunk_hash": "abc123"
953 })),
954 }),
955 name: None,
956 };
957 let result = client.parse_rpc_error(&error);
958 assert!(matches!(result, RpcError::UnknownChunk(_)));
959 }
960
961 #[test]
962 fn test_parse_rpc_error_unknown_epoch() {
963 let client = RpcClient::new("https://example.com");
964 let error = JsonRpcError {
965 code: -32000,
966 message: "Epoch not found".to_string(),
967 data: Some(serde_json::json!("epoch123")),
968 cause: Some(ErrorCause {
969 name: "UNKNOWN_EPOCH".to_string(),
970 info: None,
971 }),
972 name: None,
973 };
974 let result = client.parse_rpc_error(&error);
975 assert!(matches!(result, RpcError::UnknownEpoch(_)));
976 }
977
978 #[test]
979 fn test_parse_rpc_error_unknown_receipt() {
980 let client = RpcClient::new("https://example.com");
981 let error = JsonRpcError {
982 code: -32000,
983 message: "Receipt not found".to_string(),
984 data: None,
985 cause: Some(ErrorCause {
986 name: "UNKNOWN_RECEIPT".to_string(),
987 info: Some(serde_json::json!({
988 "receipt_id": "receipt123"
989 })),
990 }),
991 name: None,
992 };
993 let result = client.parse_rpc_error(&error);
994 assert!(matches!(result, RpcError::UnknownReceipt(_)));
995 }
996
997 #[test]
998 fn test_parse_rpc_error_no_contract_code() {
999 let client = RpcClient::new("https://example.com");
1000 let error = JsonRpcError {
1001 code: -32000,
1002 message: "No contract code".to_string(),
1003 data: None,
1004 cause: Some(ErrorCause {
1005 name: "NO_CONTRACT_CODE".to_string(),
1006 info: Some(serde_json::json!({
1007 "contract_account_id": "no-contract.near"
1008 })),
1009 }),
1010 name: None,
1011 };
1012 let result = client.parse_rpc_error(&error);
1013 assert!(matches!(result, RpcError::ContractNotDeployed(_)));
1014 }
1015
1016 #[test]
1017 fn test_parse_rpc_error_too_large_contract_state() {
1018 let client = RpcClient::new("https://example.com");
1019 let error = JsonRpcError {
1020 code: -32000,
1021 message: "Contract state too large".to_string(),
1022 data: None,
1023 cause: Some(ErrorCause {
1024 name: "TOO_LARGE_CONTRACT_STATE".to_string(),
1025 info: Some(serde_json::json!({
1026 "account_id": "large-state.near"
1027 })),
1028 }),
1029 name: None,
1030 };
1031 let result = client.parse_rpc_error(&error);
1032 assert!(matches!(result, RpcError::ContractStateTooLarge(_)));
1033 }
1034
1035 #[test]
1036 fn test_parse_rpc_error_unavailable_shard() {
1037 let client = RpcClient::new("https://example.com");
1038 let error = JsonRpcError {
1039 code: -32000,
1040 message: "Shard unavailable".to_string(),
1041 data: None,
1042 cause: Some(ErrorCause {
1043 name: "UNAVAILABLE_SHARD".to_string(),
1044 info: None,
1045 }),
1046 name: None,
1047 };
1048 let result = client.parse_rpc_error(&error);
1049 assert!(matches!(result, RpcError::ShardUnavailable(_)));
1050 }
1051
1052 #[test]
1053 fn test_parse_rpc_error_not_synced() {
1054 let client = RpcClient::new("https://example.com");
1055
1056 let error = JsonRpcError {
1058 code: -32000,
1059 message: "No synced blocks".to_string(),
1060 data: None,
1061 cause: Some(ErrorCause {
1062 name: "NO_SYNCED_BLOCKS".to_string(),
1063 info: None,
1064 }),
1065 name: None,
1066 };
1067 let result = client.parse_rpc_error(&error);
1068 assert!(matches!(result, RpcError::NodeNotSynced(_)));
1069
1070 let error = JsonRpcError {
1072 code: -32000,
1073 message: "Not synced yet".to_string(),
1074 data: None,
1075 cause: Some(ErrorCause {
1076 name: "NOT_SYNCED_YET".to_string(),
1077 info: None,
1078 }),
1079 name: None,
1080 };
1081 let result = client.parse_rpc_error(&error);
1082 assert!(matches!(result, RpcError::NodeNotSynced(_)));
1083 }
1084
1085 #[test]
1086 fn test_parse_rpc_error_invalid_shard_id() {
1087 let client = RpcClient::new("https://example.com");
1088 let error = JsonRpcError {
1089 code: -32000,
1090 message: "Invalid shard ID".to_string(),
1091 data: None,
1092 cause: Some(ErrorCause {
1093 name: "INVALID_SHARD_ID".to_string(),
1094 info: Some(serde_json::json!({
1095 "shard_id": 99
1096 })),
1097 }),
1098 name: None,
1099 };
1100 let result = client.parse_rpc_error(&error);
1101 assert!(matches!(result, RpcError::InvalidShardId(_)));
1102 }
1103
1104 #[test]
1105 fn test_parse_rpc_error_invalid_transaction() {
1106 let client = RpcClient::new("https://example.com");
1107 let error = JsonRpcError {
1108 code: -32000,
1109 message: "Invalid transaction".to_string(),
1110 data: None,
1111 cause: Some(ErrorCause {
1112 name: "INVALID_TRANSACTION".to_string(),
1113 info: None,
1114 }),
1115 name: None,
1116 };
1117 let result = client.parse_rpc_error(&error);
1118 assert!(matches!(result, RpcError::InvalidTransaction { .. }));
1119 }
1120
1121 #[test]
1122 fn test_parse_rpc_error_timeout() {
1123 let client = RpcClient::new("https://example.com");
1124 let error = JsonRpcError {
1125 code: -32000,
1126 message: "Request timed out".to_string(),
1127 data: None,
1128 cause: Some(ErrorCause {
1129 name: "TIMEOUT_ERROR".to_string(),
1130 info: Some(serde_json::json!({
1131 "transaction_hash": "tx123"
1132 })),
1133 }),
1134 name: None,
1135 };
1136 let result = client.parse_rpc_error(&error);
1137 assert!(matches!(result, RpcError::RequestTimeout { .. }));
1138 }
1139
1140 #[test]
1141 fn test_parse_rpc_error_parse_error() {
1142 let client = RpcClient::new("https://example.com");
1143 let error = JsonRpcError {
1144 code: -32700,
1145 message: "Parse error".to_string(),
1146 data: None,
1147 cause: Some(ErrorCause {
1148 name: "PARSE_ERROR".to_string(),
1149 info: None,
1150 }),
1151 name: None,
1152 };
1153 let result = client.parse_rpc_error(&error);
1154 assert!(matches!(result, RpcError::ParseError(_)));
1155 }
1156
1157 #[test]
1158 fn test_parse_rpc_error_internal_error() {
1159 let client = RpcClient::new("https://example.com");
1160 let error = JsonRpcError {
1161 code: -32603,
1162 message: "Internal error".to_string(),
1163 data: None,
1164 cause: Some(ErrorCause {
1165 name: "INTERNAL_ERROR".to_string(),
1166 info: None,
1167 }),
1168 name: None,
1169 };
1170 let result = client.parse_rpc_error(&error);
1171 assert!(matches!(result, RpcError::InternalError(_)));
1172 }
1173
1174 #[test]
1175 fn test_parse_rpc_error_contract_execution() {
1176 let client = RpcClient::new("https://example.com");
1177 let error = JsonRpcError {
1178 code: -32000,
1179 message: "Contract execution failed".to_string(),
1180 data: None,
1181 cause: Some(ErrorCause {
1182 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1183 info: Some(serde_json::json!({
1184 "contract_id": "contract.near",
1185 "method_name": "my_method"
1186 })),
1187 }),
1188 name: None,
1189 };
1190 let result = client.parse_rpc_error(&error);
1191 assert!(matches!(result, RpcError::ContractExecution { .. }));
1192 }
1193
1194 #[test]
1195 fn test_parse_rpc_error_fallback_account_not_exist() {
1196 let client = RpcClient::new("https://example.com");
1197 let error = JsonRpcError {
1198 code: -32000,
1199 message: "Error".to_string(),
1200 data: Some(serde_json::json!(
1201 "account missing.near does not exist while viewing"
1202 )),
1203 cause: None,
1204 name: None,
1205 };
1206 let result = client.parse_rpc_error(&error);
1207 assert!(matches!(result, RpcError::AccountNotFound(_)));
1208 }
1209
1210 #[test]
1211 fn test_parse_rpc_error_unknown_cause_fallback_to_generic() {
1212 let client = RpcClient::new("https://example.com");
1213 let error = JsonRpcError {
1214 code: -32000,
1215 message: "Some error".to_string(),
1216 data: Some(serde_json::json!("some data")),
1217 cause: Some(ErrorCause {
1218 name: "UNKNOWN_ERROR_TYPE".to_string(),
1219 info: None,
1220 }),
1221 name: None,
1222 };
1223 let result = client.parse_rpc_error(&error);
1224 assert!(matches!(result, RpcError::Rpc { .. }));
1225 }
1226
1227 #[test]
1228 fn test_parse_rpc_error_no_cause_fallback_to_generic() {
1229 let client = RpcClient::new("https://example.com");
1230 let error = JsonRpcError {
1231 code: -32600,
1232 message: "Invalid request".to_string(),
1233 data: None,
1234 cause: None,
1235 name: None,
1236 };
1237 let result = client.parse_rpc_error(&error);
1238 match result {
1239 RpcError::Rpc { code, message, .. } => {
1240 assert_eq!(code, -32600);
1241 assert_eq!(message, "Invalid request");
1242 }
1243 _ => panic!("Expected generic Rpc error"),
1244 }
1245 }
1246}