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::rpc::RawTransactionResponse;
11use crate::types::{
12 AccessKeyListView, AccessKeyView, AccountId, AccountView, BlockReference, BlockView,
13 CryptoHash, EpochValidatorInfo, GasPrice, PublicKey, SignedTransaction, StatusResponse,
14 TxExecutionStatus, ViewFunctionResult,
15};
16
17pub struct NetworkConfig {
19 pub rpc_url: &'static str,
21 #[allow(dead_code)]
24 pub network_id: &'static str,
25}
26
27pub const MAINNET: NetworkConfig = NetworkConfig {
29 rpc_url: "https://free.rpc.fastnear.com",
30 network_id: "mainnet",
31};
32
33pub const TESTNET: NetworkConfig = NetworkConfig {
35 rpc_url: "https://test.rpc.fastnear.com",
36 network_id: "testnet",
37};
38
39#[derive(Clone, Debug)]
41pub struct RetryConfig {
42 pub max_retries: u32,
44 pub initial_delay_ms: u64,
46 pub max_delay_ms: u64,
48}
49
50impl Default for RetryConfig {
51 fn default() -> Self {
52 Self {
53 max_retries: 3,
54 initial_delay_ms: 500,
55 max_delay_ms: 5000,
56 }
57 }
58}
59
60#[derive(Serialize)]
62struct JsonRpcRequest<'a, P: Serialize> {
63 jsonrpc: &'static str,
64 id: u64,
65 method: &'a str,
66 params: P,
67}
68
69#[derive(Deserialize)]
75struct JsonRpcResponse {
76 #[allow(dead_code)]
77 jsonrpc: String,
78 #[allow(dead_code)]
79 id: u64,
80 result: Option<serde_json::Value>,
81 error: Option<JsonRpcError>,
82}
83
84#[derive(Debug, Deserialize)]
87struct JsonRpcError {
88 code: i64,
89 message: String,
90 #[serde(default)]
91 data: Option<serde_json::Value>,
92 #[serde(default)]
93 cause: Option<ErrorCause>,
94 #[serde(default)]
95 #[allow(dead_code)]
96 name: Option<String>,
97}
98
99#[derive(Debug, Deserialize)]
101struct ErrorCause {
102 name: String,
103 #[serde(default)]
104 info: Option<serde_json::Value>,
105}
106
107#[derive(Debug, Deserialize)]
111struct CallFunctionResponse {
112 result: Vec<u8>,
113 #[serde(default)]
114 logs: Vec<String>,
115 block_height: u64,
116 block_hash: CryptoHash,
117}
118
119pub struct RpcClient {
121 url: String,
122 client: reqwest::Client,
123 retry_config: RetryConfig,
124 request_id: AtomicU64,
125}
126
127impl RpcClient {
128 pub fn new(url: impl Into<String>) -> Self {
130 Self {
131 url: url.into(),
132 client: reqwest::Client::new(),
133 retry_config: RetryConfig::default(),
134 request_id: AtomicU64::new(0),
135 }
136 }
137
138 pub fn with_retry_config(url: impl Into<String>, retry_config: RetryConfig) -> Self {
140 Self {
141 url: url.into(),
142 client: reqwest::Client::new(),
143 retry_config,
144 request_id: AtomicU64::new(0),
145 }
146 }
147
148 pub fn url(&self) -> &str {
150 &self.url
151 }
152
153 #[tracing::instrument(skip(self, params), fields(rpc.method = method, rpc.url = %sanitize_url(&self.url)))]
155 pub async fn call<P: Serialize, R: DeserializeOwned>(
156 &self,
157 method: &str,
158 params: P,
159 ) -> Result<R, RpcError> {
160 let total_attempts = self.retry_config.max_retries + 1;
161
162 for attempt in 0..total_attempts {
163 let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
164
165 let request = JsonRpcRequest {
166 jsonrpc: "2.0",
167 id: request_id,
168 method,
169 params: ¶ms,
170 };
171
172 match self.try_call::<R>(&request).await {
173 Ok(result) => return Ok(result),
174 Err(e) if e.is_retryable() && attempt < total_attempts - 1 => {
175 let delay = std::cmp::min(
176 self.retry_config.initial_delay_ms * 2u64.pow(attempt),
177 self.retry_config.max_delay_ms,
178 );
179 tracing::warn!(
180 attempt = attempt + 1,
181 max_attempts = total_attempts,
182 delay_ms = delay,
183 error = %e,
184 "RPC request failed, retrying"
185 );
186 tokio::time::sleep(Duration::from_millis(delay)).await;
187 continue;
188 }
189 Err(e) => {
190 tracing::error!(error = %e, "RPC request failed");
191 return Err(e);
192 }
193 }
194 }
195
196 unreachable!("all loop iterations return")
197 }
198
199 async fn try_call<R: DeserializeOwned>(
201 &self,
202 request: &JsonRpcRequest<'_, impl Serialize>,
203 ) -> Result<R, RpcError> {
204 if tracing::enabled!(tracing::Level::TRACE) {
205 if let Ok(json) = serde_json::to_string(request) {
206 tracing::trace!(payload = %json, "RPC request");
207 }
208 }
209
210 let response = self
211 .client
212 .post(&self.url)
213 .header("Content-Type", "application/json")
214 .json(request)
215 .send()
216 .await?;
217
218 let status = response.status();
219 let body = response.text().await?;
220
221 tracing::trace!(payload = %body, "RPC response");
222
223 if !status.is_success() {
224 let retryable = is_retryable_status(status.as_u16());
225 return Err(RpcError::network(
226 format!("HTTP {}: {}", status, body),
227 Some(status.as_u16()),
228 retryable,
229 ));
230 }
231
232 let rpc_response: JsonRpcResponse = serde_json::from_str(&body).map_err(RpcError::Json)?;
233
234 if let Some(error) = rpc_response.error {
235 return Err(self.parse_rpc_error(&error));
236 }
237
238 let result_value = rpc_response
239 .result
240 .ok_or_else(|| RpcError::InvalidResponse("Missing result in response".to_string()))?;
241
242 if request.method == "query" {
247 if let Some(error_str) = result_value.get("error").and_then(|e| e.as_str()) {
248 let synthetic = JsonRpcError {
249 code: -32600,
252 message: error_str.to_string(),
253 data: Some(serde_json::Value::String(error_str.to_string())),
254 cause: None,
255 name: None,
256 };
257 return Err(self.parse_rpc_error(&synthetic));
258 }
259 }
260
261 serde_json::from_value(result_value).map_err(RpcError::Json)
262 }
263
264 fn parse_rpc_error(&self, error: &JsonRpcError) -> RpcError {
266 if let Some(cause) = &error.cause {
268 let cause_name = cause.name.as_str();
269 let info = cause.info.as_ref();
270 let data = &error.data;
271
272 match cause_name {
273 "UNKNOWN_ACCOUNT" => {
274 if let Some(account_id) = info
275 .and_then(|i| i.get("requested_account_id"))
276 .and_then(|a| a.as_str())
277 {
278 if let Ok(account_id) = account_id.parse() {
279 return RpcError::AccountNotFound(account_id);
280 }
281 }
282 }
283 "INVALID_ACCOUNT" => {
284 let account_id = info
285 .and_then(|i| i.get("requested_account_id"))
286 .and_then(|a| a.as_str())
287 .unwrap_or("unknown");
288 return RpcError::InvalidAccount(account_id.to_string());
289 }
290 "UNKNOWN_ACCESS_KEY" => {
291 if let Some(public_key) = info
292 .and_then(|i| i.get("public_key"))
293 .and_then(|k| k.as_str())
294 .and_then(|k| k.parse().ok())
295 {
296 let account_id = info
300 .and_then(|i| i.get("requested_account_id"))
301 .and_then(|a| a.as_str())
302 .and_then(|a| a.parse().ok())
303 .unwrap_or_else(|| "unknown".parse().unwrap());
304 return RpcError::AccessKeyNotFound {
305 account_id,
306 public_key,
307 };
308 }
309 }
310 "UNKNOWN_BLOCK" => {
311 let block_ref = data
312 .as_ref()
313 .and_then(|d| d.as_str())
314 .unwrap_or(&error.message);
315 return RpcError::UnknownBlock(block_ref.to_string());
316 }
317 "UNKNOWN_CHUNK" => {
318 let chunk_ref = info
319 .and_then(|i| i.get("chunk_hash"))
320 .and_then(|c| c.as_str())
321 .unwrap_or(&error.message);
322 return RpcError::UnknownChunk(chunk_ref.to_string());
323 }
324 "UNKNOWN_EPOCH" => {
325 let block_ref = data
326 .as_ref()
327 .and_then(|d| d.as_str())
328 .unwrap_or(&error.message);
329 return RpcError::UnknownEpoch(block_ref.to_string());
330 }
331 "UNKNOWN_RECEIPT" => {
332 let receipt_id = info
333 .and_then(|i| i.get("receipt_id"))
334 .and_then(|r| r.as_str())
335 .unwrap_or("unknown");
336 return RpcError::UnknownReceipt(receipt_id.to_string());
337 }
338 "NO_CONTRACT_CODE" => {
339 let account_id = info
340 .and_then(|i| {
341 i.get("contract_account_id")
342 .or_else(|| i.get("account_id"))
343 .or_else(|| i.get("contract_id"))
344 })
345 .and_then(|a| a.as_str())
346 .unwrap_or("unknown");
347 if let Ok(account_id) = account_id.parse() {
348 return RpcError::ContractNotDeployed(account_id);
349 }
350 }
351 "TOO_LARGE_CONTRACT_STATE" => {
352 let account_id = info
353 .and_then(|i| i.get("account_id").or_else(|| i.get("contract_id")))
354 .and_then(|a| a.as_str())
355 .unwrap_or("unknown");
356 if let Ok(account_id) = account_id.parse() {
357 return RpcError::ContractStateTooLarge(account_id);
358 }
359 }
360 "CONTRACT_EXECUTION_ERROR" => {
361 if let Some(vm_error) = info.and_then(|i| i.get("vm_error")) {
364 if let Some(compilation_err) = vm_error.get("CompilationError") {
365 if let Some(code_not_exist) = compilation_err.get("CodeDoesNotExist") {
366 if let Some(account_id) = code_not_exist
367 .get("account_id")
368 .and_then(|a| a.as_str())
369 .and_then(|a| a.parse().ok())
370 {
371 return RpcError::ContractNotDeployed(account_id);
372 }
373 }
374 }
375 }
376
377 let contract_id = info
381 .and_then(|i| i.get("contract_id"))
382 .and_then(|c| c.as_str())
383 .unwrap_or("unknown");
384 let method_name = info
385 .and_then(|i| i.get("method_name"))
386 .and_then(|m| m.as_str())
387 .map(String::from);
388 let message = data
389 .as_ref()
390 .and_then(|d| d.as_str())
391 .map(|s| s.to_string())
392 .or_else(|| {
393 info.and_then(|i| i.get("vm_error")).map(|v| v.to_string())
396 })
397 .unwrap_or_else(|| error.message.clone());
398 if let Ok(contract_id) = contract_id.parse() {
399 return RpcError::ContractExecution {
400 contract_id,
401 method_name,
402 message,
403 };
404 }
405 }
406 "UNAVAILABLE_SHARD" => {
407 return RpcError::ShardUnavailable(error.message.clone());
408 }
409 "NO_SYNCED_BLOCKS" | "NOT_SYNCED_YET" => {
410 return RpcError::NodeNotSynced(error.message.clone());
411 }
412 "INVALID_SHARD_ID" => {
413 let shard_id = info
414 .and_then(|i| i.get("shard_id"))
415 .map(|s| s.to_string())
416 .unwrap_or_else(|| "unknown".to_string());
417 return RpcError::InvalidShardId(shard_id);
418 }
419 "INVALID_TRANSACTION" => {
420 return RpcError::invalid_transaction(&error.message, data.clone());
421 }
422 "TIMEOUT_ERROR" => {
423 let tx_hash = info
424 .and_then(|i| i.get("transaction_hash"))
425 .and_then(|h| h.as_str())
426 .map(String::from);
427 return RpcError::RequestTimeout {
428 message: error.message.clone(),
429 transaction_hash: tx_hash,
430 };
431 }
432 "PARSE_ERROR" => {
433 return RpcError::ParseError(error.message.clone());
434 }
435 "INTERNAL_ERROR" => {
436 return RpcError::InternalError(error.message.clone());
437 }
438 _ => {}
439 }
440 }
441
442 if let Some(data) = &error.data {
444 if let Some(error_str) = data.as_str() {
445 if error_str.contains("does not exist") {
446 if let Some(start) = error_str.strip_prefix("account ") {
449 if let Some(account_str) = start.split_whitespace().next() {
450 if let Ok(account_id) = account_str.parse() {
451 return RpcError::AccountNotFound(account_id);
452 }
453 }
454 }
455 }
456 }
457 }
458
459 RpcError::Rpc {
460 code: error.code,
461 message: error.message.clone(),
462 data: error.data.clone(),
463 }
464 }
465
466 #[tracing::instrument(skip(self, block), fields(%account_id))]
472 pub async fn view_account(
473 &self,
474 account_id: &AccountId,
475 block: BlockReference,
476 ) -> Result<AccountView, RpcError> {
477 let mut params = serde_json::json!({
478 "account_id": account_id.to_string(),
479 });
480 self.merge_block_reference(&mut params, &block);
481 self.call("EXPERIMENTAL_view_account", params).await
482 }
483
484 #[tracing::instrument(skip(self, block), fields(%account_id, %public_key))]
486 pub async fn view_access_key(
487 &self,
488 account_id: &AccountId,
489 public_key: &PublicKey,
490 block: BlockReference,
491 ) -> Result<AccessKeyView, RpcError> {
492 let mut params = serde_json::json!({
493 "account_id": account_id.to_string(),
494 "public_key": public_key.to_string(),
495 });
496 self.merge_block_reference(&mut params, &block);
497 self.call("EXPERIMENTAL_view_access_key", params)
498 .await
499 .map_err(|e| match e {
500 RpcError::AccessKeyNotFound { public_key, .. } => RpcError::AccessKeyNotFound {
504 account_id: account_id.clone(),
505 public_key,
506 },
507 other => other,
508 })
509 }
510
511 #[tracing::instrument(skip(self, block), fields(%account_id))]
513 pub async fn view_access_key_list(
514 &self,
515 account_id: &AccountId,
516 block: BlockReference,
517 ) -> Result<AccessKeyListView, RpcError> {
518 let mut params = serde_json::json!({
519 "account_id": account_id.to_string(),
520 });
521 self.merge_block_reference(&mut params, &block);
522 self.call("EXPERIMENTAL_view_access_key_list", params).await
523 }
524
525 #[tracing::instrument(skip(self, args, block), fields(contract_id = %account_id, method = method_name))]
527 pub async fn view_function(
528 &self,
529 account_id: &AccountId,
530 method_name: &str,
531 args: &[u8],
532 block: BlockReference,
533 ) -> Result<ViewFunctionResult, RpcError> {
534 let mut params = serde_json::json!({
535 "account_id": account_id.to_string(),
536 "method_name": method_name,
537 "args_base64": STANDARD.encode(args),
538 });
539 self.merge_block_reference(&mut params, &block);
540
541 let response: CallFunctionResponse = self
546 .call("EXPERIMENTAL_call_function", params)
547 .await
548 .map_err(|e| match e {
549 RpcError::ContractExecution { message, .. } => RpcError::ContractExecution {
550 contract_id: account_id.clone(),
551 method_name: Some(method_name.to_string()),
552 message,
553 },
554 other => other,
555 })?;
556
557 Ok(ViewFunctionResult {
558 result: response.result,
559 logs: response.logs,
560 block_height: response.block_height,
561 block_hash: response.block_hash,
562 })
563 }
564
565 #[tracing::instrument(skip(self, block))]
567 pub async fn block(&self, block: BlockReference) -> Result<BlockView, RpcError> {
568 let params = block.to_rpc_params();
569 self.call("block", params).await
570 }
571
572 #[tracing::instrument(skip(self))]
574 pub async fn status(&self) -> Result<StatusResponse, RpcError> {
575 self.call("status", serde_json::json!([])).await
576 }
577
578 #[tracing::instrument(skip(self))]
580 pub async fn gas_price(&self, block_hash: Option<&CryptoHash>) -> Result<GasPrice, RpcError> {
581 let params = match block_hash {
582 Some(hash) => serde_json::json!([hash.to_string()]),
583 None => serde_json::json!([serde_json::Value::Null]),
584 };
585 self.call("gas_price", params).await
586 }
587
588 #[tracing::instrument(skip(self))]
594 pub async fn validators(
595 &self,
596 block: Option<BlockReference>,
597 ) -> Result<EpochValidatorInfo, RpcError> {
598 let params = serde_json::json!([block_id_or_null(block.as_ref())]);
599 self.call("validators", params).await
600 }
601
602 #[tracing::instrument(skip(self, signed_tx), fields(
604 tx_hash = tracing::field::Empty,
605 sender = %signed_tx.transaction.signer_id,
606 receiver = %signed_tx.transaction.receiver_id,
607 ?wait_until,
608 ))]
609 pub async fn send_tx(
610 &self,
611 signed_tx: &SignedTransaction,
612 wait_until: TxExecutionStatus,
613 ) -> Result<RawTransactionResponse, RpcError> {
614 let tx_hash = signed_tx.get_hash();
615 tracing::Span::current().record("tx_hash", tracing::field::display(&tx_hash));
616 let params = serde_json::json!({
617 "signed_tx_base64": signed_tx.to_base64(),
618 "wait_until": wait_until.as_str(),
619 });
620 let mut response: RawTransactionResponse = self.call("send_tx", params).await?;
621 response.transaction_hash = tx_hash;
622 Ok(response)
623 }
624
625 #[tracing::instrument(skip(self), fields(%tx_hash, sender = %sender_id, ?wait_until))]
631 pub async fn tx_status(
632 &self,
633 tx_hash: &CryptoHash,
634 sender_id: &AccountId,
635 wait_until: TxExecutionStatus,
636 ) -> Result<RawTransactionResponse, RpcError> {
637 let params = serde_json::json!({
638 "tx_hash": tx_hash.to_string(),
639 "sender_account_id": sender_id.to_string(),
640 "wait_until": wait_until.as_str(),
641 });
642 let mut response: RawTransactionResponse =
643 self.call("EXPERIMENTAL_tx_status", params).await?;
644 response.transaction_hash = response
645 .outcome
646 .as_ref()
647 .map(|o| *o.transaction_hash())
648 .unwrap_or(*tx_hash);
649 Ok(response)
650 }
651
652 fn merge_block_reference(&self, params: &mut serde_json::Value, block: &BlockReference) {
654 if let serde_json::Value::Object(block_params) = block.to_rpc_params() {
655 if let serde_json::Value::Object(map) = params {
656 map.extend(block_params);
657 }
658 }
659 }
660
661 pub async fn sandbox_fast_forward(&self, delta_height: u64) -> Result<(), RpcError> {
709 let params = serde_json::json!({
710 "delta_height": delta_height,
711 });
712
713 let _: serde_json::Value = self.call("sandbox_fast_forward", params).await?;
714 Ok(())
715 }
716
717 pub async fn sandbox_patch_state(&self, records: serde_json::Value) -> Result<(), RpcError> {
718 let params = serde_json::json!({
719 "records": records,
720 });
721
722 let _: serde_json::Value = self.call("sandbox_patch_state", params).await?;
724
725 let _: serde_json::Value = self
729 .call(
730 "sandbox_patch_state",
731 serde_json::json!({
732 "records": records,
733 }),
734 )
735 .await?;
736
737 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
739
740 Ok(())
741 }
742}
743
744impl Clone for RpcClient {
745 fn clone(&self) -> Self {
746 Self {
747 url: self.url.clone(),
748 client: self.client.clone(),
749 retry_config: self.retry_config.clone(),
750 request_id: AtomicU64::new(0),
751 }
752 }
753}
754
755impl std::fmt::Debug for RpcClient {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 f.debug_struct("RpcClient")
758 .field("url", &self.url)
759 .field("retry_config", &self.retry_config)
760 .finish()
761 }
762}
763
764fn block_id_or_null(block: Option<&BlockReference>) -> serde_json::Value {
775 match block {
776 Some(BlockReference::Height(h)) => serde_json::json!(*h),
777 Some(BlockReference::Hash(h)) => serde_json::json!(h.to_string()),
778 _ => serde_json::Value::Null,
779 }
780}
781
782fn sanitize_url(url: &str) -> &str {
787 let end = url.find('?').or_else(|| url.find('#')).unwrap_or(url.len());
789 &url[..end]
790}
791
792fn is_retryable_status(status: u16) -> bool {
794 status == 408 || status == 429 || status == 503 || (500..600).contains(&status)
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 #[test]
810 fn test_retry_config_default() {
811 let config = RetryConfig::default();
812 assert_eq!(config.max_retries, 3);
813 assert_eq!(config.initial_delay_ms, 500);
814 assert_eq!(config.max_delay_ms, 5000);
815 }
816
817 #[test]
818 fn test_retry_config_clone() {
819 let config = RetryConfig {
820 max_retries: 5,
821 initial_delay_ms: 100,
822 max_delay_ms: 1000,
823 };
824 let cloned = config.clone();
825 assert_eq!(cloned.max_retries, 5);
826 assert_eq!(cloned.initial_delay_ms, 100);
827 assert_eq!(cloned.max_delay_ms, 1000);
828 }
829
830 #[test]
831 fn test_retry_config_debug() {
832 let config = RetryConfig::default();
833 let debug = format!("{:?}", config);
834 assert!(debug.contains("RetryConfig"));
835 assert!(debug.contains("max_retries"));
836 }
837
838 #[test]
843 fn test_rpc_client_new() {
844 let client = RpcClient::new("https://rpc.testnet.near.org");
845 assert_eq!(client.url(), "https://rpc.testnet.near.org");
846 }
847
848 #[test]
849 fn test_rpc_client_with_retry_config() {
850 let config = RetryConfig {
851 max_retries: 5,
852 initial_delay_ms: 100,
853 max_delay_ms: 1000,
854 };
855 let client = RpcClient::with_retry_config("https://rpc.example.com", config);
856 assert_eq!(client.url(), "https://rpc.example.com");
857 }
858
859 #[test]
860 fn test_rpc_client_clone() {
861 let client = RpcClient::new("https://rpc.testnet.near.org");
862 let cloned = client.clone();
863 assert_eq!(cloned.url(), client.url());
864 }
865
866 #[test]
867 fn test_rpc_client_debug() {
868 let client = RpcClient::new("https://rpc.testnet.near.org");
869 let debug = format!("{:?}", client);
870 assert!(debug.contains("RpcClient"));
871 assert!(debug.contains("rpc.testnet.near.org"));
872 }
873
874 #[test]
879 fn test_sanitize_url_plain() {
880 assert_eq!(
881 sanitize_url("https://rpc.mainnet.near.org"),
882 "https://rpc.mainnet.near.org"
883 );
884 }
885
886 #[test]
887 fn test_sanitize_url_strips_query() {
888 assert_eq!(
889 sanitize_url("https://rpc.provider.com/v1?api_key=secret123"),
890 "https://rpc.provider.com/v1"
891 );
892 }
893
894 #[test]
895 fn test_sanitize_url_strips_fragment() {
896 assert_eq!(
897 sanitize_url("https://rpc.provider.com/v1#section"),
898 "https://rpc.provider.com/v1"
899 );
900 }
901
902 #[test]
903 fn test_sanitize_url_strips_query_and_fragment() {
904 assert_eq!(
905 sanitize_url("https://rpc.provider.com/v1?key=val#frag"),
906 "https://rpc.provider.com/v1"
907 );
908 }
909
910 #[test]
915 fn test_is_retryable_status() {
916 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)); }
934
935 #[test]
940 fn test_invalid_transaction_parses_invalid_nonce() {
941 use crate::types::InvalidTxError;
942 let data = serde_json::json!({
943 "TxExecutionError": {
944 "InvalidTxError": {
945 "InvalidNonce": {
946 "tx_nonce": 5,
947 "ak_nonce": 10
948 }
949 }
950 }
951 });
952 let err = RpcError::invalid_transaction("invalid nonce", Some(data));
953 match err {
954 RpcError::InvalidTx(InvalidTxError::InvalidNonce { tx_nonce, ak_nonce }) => {
955 assert_eq!(tx_nonce, 5);
956 assert_eq!(ak_nonce, 10);
957 }
958 other => panic!("Expected InvalidTx(InvalidNonce), got: {other:?}"),
959 }
960 }
961
962 #[test]
963 fn test_invalid_transaction_parses_top_level_invalid_tx() {
964 use crate::types::InvalidTxError;
965 let data = serde_json::json!({
967 "InvalidTxError": {
968 "NotEnoughBalance": {
969 "signer_id": "alice.near",
970 "balance": "1000000000000000000000000",
971 "cost": "9000000000000000000000000"
972 }
973 }
974 });
975 let err = RpcError::invalid_transaction("insufficient balance", Some(data));
976 assert!(
977 matches!(
978 err,
979 RpcError::InvalidTx(InvalidTxError::NotEnoughBalance { .. })
980 ),
981 "Expected InvalidTx(NotEnoughBalance), got: {err:?}"
982 );
983 }
984
985 #[test]
986 fn test_invalid_transaction_falls_back_on_unparseable() {
987 let data = serde_json::json!({ "SomeOtherError": {} });
989 let err = RpcError::invalid_transaction("some error", Some(data));
990 assert!(matches!(err, RpcError::InvalidTransaction { .. }));
991 }
992
993 #[test]
998 fn test_mainnet_config() {
999 assert!(MAINNET.rpc_url.contains("fastnear"));
1000 assert_eq!(MAINNET.network_id, "mainnet");
1001 }
1002
1003 #[test]
1004 fn test_testnet_config() {
1005 assert!(TESTNET.rpc_url.contains("fastnear") || TESTNET.rpc_url.contains("test"));
1006 assert_eq!(TESTNET.network_id, "testnet");
1007 }
1008
1009 #[test]
1014 fn test_parse_rpc_error_unknown_account() {
1015 let client = RpcClient::new("https://example.com");
1016 let error = JsonRpcError {
1017 code: -32000,
1018 message: "Server error".to_string(),
1019 data: None,
1020 cause: Some(ErrorCause {
1021 name: "UNKNOWN_ACCOUNT".to_string(),
1022 info: Some(serde_json::json!({
1023 "requested_account_id": "nonexistent.near"
1024 })),
1025 }),
1026 name: None,
1027 };
1028 let result = client.parse_rpc_error(&error);
1029 assert!(matches!(result, RpcError::AccountNotFound(_)));
1030 }
1031
1032 #[test]
1033 fn test_parse_rpc_error_unknown_access_key_legacy() {
1034 let client = RpcClient::new("https://example.com");
1036 let error = JsonRpcError {
1037 code: -32000,
1038 message: "Server error".to_string(),
1039 data: None,
1040 cause: Some(ErrorCause {
1041 name: "UNKNOWN_ACCESS_KEY".to_string(),
1042 info: Some(serde_json::json!({
1043 "requested_account_id": "alice.near",
1044 "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
1045 })),
1046 }),
1047 name: None,
1048 };
1049 let result = client.parse_rpc_error(&error);
1050 match result {
1051 RpcError::AccessKeyNotFound {
1052 account_id,
1053 public_key,
1054 } => {
1055 assert_eq!(account_id.as_str(), "alice.near");
1056 assert!(public_key.to_string().contains("ed25519:"));
1057 }
1058 _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
1059 }
1060 }
1061
1062 #[test]
1063 fn test_parse_rpc_error_unknown_access_key_experimental() {
1064 let client = RpcClient::new("https://example.com");
1066 let error = JsonRpcError {
1067 code: -32000,
1068 message: "Server error".to_string(),
1069 data: Some(serde_json::Value::String(
1070 "Access key for public key ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp does not exist while viewing".to_string()
1071 )),
1072 cause: Some(ErrorCause {
1073 name: "UNKNOWN_ACCESS_KEY".to_string(),
1074 info: Some(serde_json::json!({
1075 "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
1076 "block_height": 243789592,
1077 "block_hash": "EC5A7qc6rixfN8T4T9Gkt78H5pAsvdcjAos8Z7kFLJgi"
1078 })),
1079 }),
1080 name: Some("HANDLER_ERROR".to_string()),
1081 };
1082 let result = client.parse_rpc_error(&error);
1083 match result {
1084 RpcError::AccessKeyNotFound {
1085 account_id,
1086 public_key,
1087 } => {
1088 assert_eq!(account_id.as_str(), "unknown");
1090 assert!(public_key.to_string().contains("ed25519:"));
1091 }
1092 _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
1093 }
1094 }
1095
1096 #[test]
1097 fn test_parse_rpc_error_invalid_account() {
1098 let client = RpcClient::new("https://example.com");
1099 let error = JsonRpcError {
1100 code: -32000,
1101 message: "Server error".to_string(),
1102 data: None,
1103 cause: Some(ErrorCause {
1104 name: "INVALID_ACCOUNT".to_string(),
1105 info: Some(serde_json::json!({
1106 "requested_account_id": "invalid@account"
1107 })),
1108 }),
1109 name: None,
1110 };
1111 let result = client.parse_rpc_error(&error);
1112 assert!(matches!(result, RpcError::InvalidAccount(_)));
1113 }
1114
1115 #[test]
1116 fn test_parse_rpc_error_unknown_block() {
1117 let client = RpcClient::new("https://example.com");
1118 let error = JsonRpcError {
1119 code: -32000,
1120 message: "Block not found".to_string(),
1121 data: Some(serde_json::json!("12345")),
1122 cause: Some(ErrorCause {
1123 name: "UNKNOWN_BLOCK".to_string(),
1124 info: None,
1125 }),
1126 name: None,
1127 };
1128 let result = client.parse_rpc_error(&error);
1129 assert!(matches!(result, RpcError::UnknownBlock(_)));
1130 }
1131
1132 #[test]
1133 fn test_parse_rpc_error_unknown_chunk() {
1134 let client = RpcClient::new("https://example.com");
1135 let error = JsonRpcError {
1136 code: -32000,
1137 message: "Chunk not found".to_string(),
1138 data: None,
1139 cause: Some(ErrorCause {
1140 name: "UNKNOWN_CHUNK".to_string(),
1141 info: Some(serde_json::json!({
1142 "chunk_hash": "abc123"
1143 })),
1144 }),
1145 name: None,
1146 };
1147 let result = client.parse_rpc_error(&error);
1148 assert!(matches!(result, RpcError::UnknownChunk(_)));
1149 }
1150
1151 #[test]
1152 fn test_parse_rpc_error_unknown_epoch() {
1153 let client = RpcClient::new("https://example.com");
1154 let error = JsonRpcError {
1155 code: -32000,
1156 message: "Epoch not found".to_string(),
1157 data: Some(serde_json::json!("epoch123")),
1158 cause: Some(ErrorCause {
1159 name: "UNKNOWN_EPOCH".to_string(),
1160 info: None,
1161 }),
1162 name: None,
1163 };
1164 let result = client.parse_rpc_error(&error);
1165 assert!(matches!(result, RpcError::UnknownEpoch(_)));
1166 }
1167
1168 #[test]
1169 fn test_parse_rpc_error_unknown_receipt() {
1170 let client = RpcClient::new("https://example.com");
1171 let error = JsonRpcError {
1172 code: -32000,
1173 message: "Receipt not found".to_string(),
1174 data: None,
1175 cause: Some(ErrorCause {
1176 name: "UNKNOWN_RECEIPT".to_string(),
1177 info: Some(serde_json::json!({
1178 "receipt_id": "receipt123"
1179 })),
1180 }),
1181 name: None,
1182 };
1183 let result = client.parse_rpc_error(&error);
1184 assert!(matches!(result, RpcError::UnknownReceipt(_)));
1185 }
1186
1187 #[test]
1188 fn test_parse_rpc_error_no_contract_code() {
1189 let client = RpcClient::new("https://example.com");
1190 let error = JsonRpcError {
1191 code: -32000,
1192 message: "No contract code".to_string(),
1193 data: None,
1194 cause: Some(ErrorCause {
1195 name: "NO_CONTRACT_CODE".to_string(),
1196 info: Some(serde_json::json!({
1197 "contract_account_id": "no-contract.near"
1198 })),
1199 }),
1200 name: None,
1201 };
1202 let result = client.parse_rpc_error(&error);
1203 assert!(matches!(result, RpcError::ContractNotDeployed(_)));
1204 }
1205
1206 #[test]
1207 fn test_parse_rpc_error_too_large_contract_state() {
1208 let client = RpcClient::new("https://example.com");
1209 let error = JsonRpcError {
1210 code: -32000,
1211 message: "Contract state too large".to_string(),
1212 data: None,
1213 cause: Some(ErrorCause {
1214 name: "TOO_LARGE_CONTRACT_STATE".to_string(),
1215 info: Some(serde_json::json!({
1216 "account_id": "large-state.near"
1217 })),
1218 }),
1219 name: None,
1220 };
1221 let result = client.parse_rpc_error(&error);
1222 assert!(matches!(result, RpcError::ContractStateTooLarge(_)));
1223 }
1224
1225 #[test]
1226 fn test_parse_rpc_error_unavailable_shard() {
1227 let client = RpcClient::new("https://example.com");
1228 let error = JsonRpcError {
1229 code: -32000,
1230 message: "Shard unavailable".to_string(),
1231 data: None,
1232 cause: Some(ErrorCause {
1233 name: "UNAVAILABLE_SHARD".to_string(),
1234 info: None,
1235 }),
1236 name: None,
1237 };
1238 let result = client.parse_rpc_error(&error);
1239 assert!(matches!(result, RpcError::ShardUnavailable(_)));
1240 }
1241
1242 #[test]
1243 fn test_parse_rpc_error_not_synced() {
1244 let client = RpcClient::new("https://example.com");
1245
1246 let error = JsonRpcError {
1248 code: -32000,
1249 message: "No synced blocks".to_string(),
1250 data: None,
1251 cause: Some(ErrorCause {
1252 name: "NO_SYNCED_BLOCKS".to_string(),
1253 info: None,
1254 }),
1255 name: None,
1256 };
1257 let result = client.parse_rpc_error(&error);
1258 assert!(matches!(result, RpcError::NodeNotSynced(_)));
1259
1260 let error = JsonRpcError {
1262 code: -32000,
1263 message: "Not synced yet".to_string(),
1264 data: None,
1265 cause: Some(ErrorCause {
1266 name: "NOT_SYNCED_YET".to_string(),
1267 info: None,
1268 }),
1269 name: None,
1270 };
1271 let result = client.parse_rpc_error(&error);
1272 assert!(matches!(result, RpcError::NodeNotSynced(_)));
1273 }
1274
1275 #[test]
1276 fn test_parse_rpc_error_invalid_shard_id() {
1277 let client = RpcClient::new("https://example.com");
1278 let error = JsonRpcError {
1279 code: -32000,
1280 message: "Invalid shard ID".to_string(),
1281 data: None,
1282 cause: Some(ErrorCause {
1283 name: "INVALID_SHARD_ID".to_string(),
1284 info: Some(serde_json::json!({
1285 "shard_id": 99
1286 })),
1287 }),
1288 name: None,
1289 };
1290 let result = client.parse_rpc_error(&error);
1291 assert!(matches!(result, RpcError::InvalidShardId(_)));
1292 }
1293
1294 #[test]
1295 fn test_parse_rpc_error_invalid_transaction() {
1296 let client = RpcClient::new("https://example.com");
1297 let error = JsonRpcError {
1298 code: -32000,
1299 message: "Invalid transaction".to_string(),
1300 data: None,
1301 cause: Some(ErrorCause {
1302 name: "INVALID_TRANSACTION".to_string(),
1303 info: None,
1304 }),
1305 name: None,
1306 };
1307 let result = client.parse_rpc_error(&error);
1308 assert!(matches!(result, RpcError::InvalidTransaction { .. }));
1309 }
1310
1311 #[test]
1312 fn test_parse_rpc_error_timeout() {
1313 let client = RpcClient::new("https://example.com");
1314 let error = JsonRpcError {
1315 code: -32000,
1316 message: "Request timed out".to_string(),
1317 data: None,
1318 cause: Some(ErrorCause {
1319 name: "TIMEOUT_ERROR".to_string(),
1320 info: Some(serde_json::json!({
1321 "transaction_hash": "tx123"
1322 })),
1323 }),
1324 name: None,
1325 };
1326 let result = client.parse_rpc_error(&error);
1327 assert!(matches!(result, RpcError::RequestTimeout { .. }));
1328 }
1329
1330 #[test]
1331 fn test_parse_rpc_error_parse_error() {
1332 let client = RpcClient::new("https://example.com");
1333 let error = JsonRpcError {
1334 code: -32700,
1335 message: "Parse error".to_string(),
1336 data: None,
1337 cause: Some(ErrorCause {
1338 name: "PARSE_ERROR".to_string(),
1339 info: None,
1340 }),
1341 name: None,
1342 };
1343 let result = client.parse_rpc_error(&error);
1344 assert!(matches!(result, RpcError::ParseError(_)));
1345 }
1346
1347 #[test]
1348 fn test_parse_rpc_error_internal_error() {
1349 let client = RpcClient::new("https://example.com");
1350 let error = JsonRpcError {
1351 code: -32603,
1352 message: "Internal error".to_string(),
1353 data: None,
1354 cause: Some(ErrorCause {
1355 name: "INTERNAL_ERROR".to_string(),
1356 info: None,
1357 }),
1358 name: None,
1359 };
1360 let result = client.parse_rpc_error(&error);
1361 assert!(matches!(result, RpcError::InternalError(_)));
1362 }
1363
1364 #[test]
1365 fn test_parse_rpc_error_contract_execution_legacy() {
1366 let client = RpcClient::new("https://example.com");
1368 let error = JsonRpcError {
1369 code: -32000,
1370 message: "Contract execution failed".to_string(),
1371 data: None,
1372 cause: Some(ErrorCause {
1373 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1374 info: Some(serde_json::json!({
1375 "contract_id": "contract.near",
1376 "method_name": "my_method"
1377 })),
1378 }),
1379 name: None,
1380 };
1381 let result = client.parse_rpc_error(&error);
1382 match result {
1383 RpcError::ContractExecution {
1384 contract_id,
1385 method_name,
1386 ..
1387 } => {
1388 assert_eq!(contract_id.as_str(), "contract.near");
1389 assert_eq!(method_name.as_deref(), Some("my_method"));
1390 }
1391 _ => panic!("Expected ContractExecution error, got {:?}", result),
1392 }
1393 }
1394
1395 #[test]
1396 fn test_parse_rpc_error_contract_execution_experimental() {
1397 let client = RpcClient::new("https://example.com");
1400 let error = JsonRpcError {
1401 code: -32000,
1402 message: "Server error".to_string(),
1403 data: Some(serde_json::json!(
1404 "Function call returned an error: MethodResolveError(MethodNotFound)"
1405 )),
1406 cause: Some(ErrorCause {
1407 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1408 info: Some(serde_json::json!({
1409 "vm_error": { "MethodResolveError": "MethodNotFound" },
1410 "block_height": 243803767,
1411 "block_hash": "Et7So7jtsorkYLdVMMgV8gxA3Cfaztp75Ti6TPv2A"
1412 })),
1413 }),
1414 name: Some("HANDLER_ERROR".to_string()),
1415 };
1416 let result = client.parse_rpc_error(&error);
1417 match result {
1418 RpcError::ContractExecution {
1419 contract_id,
1420 message,
1421 ..
1422 } => {
1423 assert_eq!(contract_id.as_str(), "unknown");
1425 assert!(message.contains("MethodResolveError"));
1426 }
1427 _ => panic!("Expected ContractExecution error, got {:?}", result),
1428 }
1429 }
1430
1431 #[test]
1432 fn test_parse_rpc_error_code_does_not_exist_experimental() {
1433 let client = RpcClient::new("https://example.com");
1435 let error = JsonRpcError {
1436 code: -32000,
1437 message: "Server error".to_string(),
1438 data: Some(serde_json::json!(
1439 "Function call returned an error: CompilationError(CodeDoesNotExist { account_id: AccountId(\"nonexistent.testnet\") })"
1440 )),
1441 cause: Some(ErrorCause {
1442 name: "CONTRACT_EXECUTION_ERROR".to_string(),
1443 info: Some(serde_json::json!({
1444 "vm_error": {
1445 "CompilationError": {
1446 "CodeDoesNotExist": {
1447 "account_id": "nonexistent.testnet"
1448 }
1449 }
1450 },
1451 "block_height": 243803764,
1452 "block_hash": "H33oNAtVZDJjhpncQb5LY6NxYzQLMMVLptq99mwmLmnj"
1453 })),
1454 }),
1455 name: Some("HANDLER_ERROR".to_string()),
1456 };
1457 let result = client.parse_rpc_error(&error);
1458 match result {
1459 RpcError::ContractNotDeployed(account_id) => {
1460 assert_eq!(account_id.as_str(), "nonexistent.testnet");
1461 }
1462 _ => panic!("Expected ContractNotDeployed error, got {:?}", result),
1463 }
1464 }
1465
1466 #[test]
1467 fn test_parse_rpc_error_fallback_account_not_exist() {
1468 let client = RpcClient::new("https://example.com");
1469 let error = JsonRpcError {
1470 code: -32000,
1471 message: "Error".to_string(),
1472 data: Some(serde_json::json!(
1473 "account missing.near does not exist while viewing"
1474 )),
1475 cause: None,
1476 name: None,
1477 };
1478 let result = client.parse_rpc_error(&error);
1479 assert!(matches!(result, RpcError::AccountNotFound(_)));
1480 }
1481
1482 #[test]
1483 fn test_parse_rpc_error_unknown_cause_fallback_to_generic() {
1484 let client = RpcClient::new("https://example.com");
1485 let error = JsonRpcError {
1486 code: -32000,
1487 message: "Some error".to_string(),
1488 data: Some(serde_json::json!("some data")),
1489 cause: Some(ErrorCause {
1490 name: "UNKNOWN_ERROR_TYPE".to_string(),
1491 info: None,
1492 }),
1493 name: None,
1494 };
1495 let result = client.parse_rpc_error(&error);
1496 assert!(matches!(result, RpcError::Rpc { .. }));
1497 }
1498
1499 #[test]
1500 fn test_parse_rpc_error_no_cause_fallback_to_generic() {
1501 let client = RpcClient::new("https://example.com");
1502 let error = JsonRpcError {
1503 code: -32600,
1504 message: "Invalid request".to_string(),
1505 data: None,
1506 cause: None,
1507 name: None,
1508 };
1509 let result = client.parse_rpc_error(&error);
1510 match result {
1511 RpcError::Rpc { code, message, .. } => {
1512 assert_eq!(code, -32600);
1513 assert_eq!(message, "Invalid request");
1514 }
1515 _ => panic!("Expected generic Rpc error"),
1516 }
1517 }
1518
1519 #[test]
1524 fn test_block_id_or_null_with_none() {
1525 let result = block_id_or_null(None);
1526 assert!(result.is_null());
1527 }
1528
1529 #[test]
1530 fn test_block_id_or_null_with_height() {
1531 let block = BlockReference::at_height(12345);
1532 let result = block_id_or_null(Some(&block));
1533 assert_eq!(result, serde_json::json!(12345));
1534 }
1535
1536 #[test]
1537 fn test_block_id_or_null_with_hash() {
1538 let hash = CryptoHash::hash(b"test block");
1539 let block = BlockReference::at_hash(hash);
1540 let result = block_id_or_null(Some(&block));
1541 assert_eq!(result, serde_json::json!(hash.to_string()));
1542 }
1543
1544 #[test]
1545 fn test_block_id_or_null_with_finality_falls_back_to_null() {
1546 let block = BlockReference::final_();
1547 let result = block_id_or_null(Some(&block));
1548 assert!(result.is_null(), "finality variants should map to null");
1549
1550 let block = BlockReference::optimistic();
1551 let result = block_id_or_null(Some(&block));
1552 assert!(result.is_null(), "optimistic should map to null");
1553 }
1554
1555 #[test]
1556 fn test_block_id_or_null_with_sync_checkpoint_falls_back_to_null() {
1557 let block = BlockReference::genesis();
1558 let result = block_id_or_null(Some(&block));
1559 assert!(result.is_null(), "sync checkpoint should map to null");
1560 }
1561}