Skip to main content

near_kit/client/
rpc.rs

1//! Low-level JSON-RPC client for NEAR.
2
3use 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
16/// Network configuration presets.
17pub struct NetworkConfig {
18    /// The RPC URL for this network.
19    pub rpc_url: &'static str,
20    /// The network identifier (e.g., "mainnet", "testnet").
21    /// Reserved for future use in transaction signing.
22    #[allow(dead_code)]
23    pub network_id: &'static str,
24}
25
26/// Mainnet configuration.
27pub const MAINNET: NetworkConfig = NetworkConfig {
28    rpc_url: "https://free.rpc.fastnear.com",
29    network_id: "mainnet",
30};
31
32/// Testnet configuration.
33pub const TESTNET: NetworkConfig = NetworkConfig {
34    rpc_url: "https://test.rpc.fastnear.com",
35    network_id: "testnet",
36};
37
38/// Retry configuration for RPC calls.
39#[derive(Clone, Debug)]
40pub struct RetryConfig {
41    /// Maximum number of retries.
42    pub max_retries: u32,
43    /// Initial delay in milliseconds.
44    pub initial_delay_ms: u64,
45    /// Maximum delay in milliseconds.
46    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/// JSON-RPC request structure.
60#[derive(Serialize)]
61struct JsonRpcRequest<'a, P: Serialize> {
62    jsonrpc: &'static str,
63    id: u64,
64    method: &'a str,
65    params: P,
66}
67
68/// JSON-RPC response structure.
69#[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/// JSON-RPC error structure.
80/// NEAR RPC returns structured errors with name/cause/info pattern.
81#[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/// Structured error cause from NEAR RPC.
95#[derive(Debug, Deserialize)]
96struct ErrorCause {
97    name: String,
98    #[serde(default)]
99    info: Option<serde_json::Value>,
100}
101
102/// Query response for view function calls.
103/// NEAR RPC returns `result` on success or `error` on failure.
104#[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
116/// Low-level JSON-RPC client for NEAR.
117pub struct RpcClient {
118    url: String,
119    client: reqwest::Client,
120    retry_config: RetryConfig,
121    request_id: AtomicU64,
122}
123
124impl RpcClient {
125    /// Create a new RPC client with the given URL.
126    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    /// Create a new RPC client with custom retry configuration.
136    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    /// Get the RPC URL.
146    pub fn url(&self) -> &str {
147        &self.url
148    }
149
150    /// Make a raw RPC call with retries.
151    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: &params,
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    /// Single attempt to make an RPC call.
186    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    /// Parse an RPC error into a specific error type.
223    fn parse_rpc_error(&self, error: &JsonRpcError) -> RpcError {
224        // First, check the direct cause field (NEAR RPC structured errors)
225        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        // Fallback: check for string error messages in data field
372        if let Some(data) = &error.data {
373            if let Some(error_str) = data.as_str() {
374                if error_str.contains("does not exist") {
375                    // Try to extract account ID from error message
376                    // Format: "account X does not exist while viewing"
377                    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    // ========================================================================
396    // High-level RPC methods
397    // ========================================================================
398
399    /// View account information.
400    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    /// View access key information.
415    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    /// View all access keys for an account.
432    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    /// Call a view function on a contract.
447    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        // Query responses may have an error field instead of result
464        let response: QueryResponse = self.call("query", params).await?;
465
466        if let Some(error) = response.error {
467            // Parse the error message for known patterns
468            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    /// Get block information.
494    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    /// Get node status.
500    pub async fn status(&self) -> Result<StatusResponse, RpcError> {
501        self.call("status", serde_json::json!([])).await
502    }
503
504    /// Get current gas price.
505    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    /// Send a signed transaction.
514    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    /// Get transaction status with full receipt details.
530    ///
531    /// Uses EXPERIMENTAL_tx_status which returns complete receipt information.
532    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    /// Merge block reference parameters into a JSON object.
547    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    // ========================================================================
556    // Sandbox-only methods
557    // ========================================================================
558
559    /// Patch account state in sandbox.
560    ///
561    /// This is a sandbox-only method that allows modifying account state directly,
562    /// useful for testing scenarios that require specific account configurations
563    /// (e.g., setting a high balance for staking tests).
564    ///
565    /// # Arguments
566    ///
567    /// * `records` - State records to patch (Account, Data, Contract, AccessKey, etc.)
568    ///
569    /// # Example
570    ///
571    /// ```rust,ignore
572    /// // Set account balance to 1M NEAR
573    /// rpc.sandbox_patch_state(serde_json::json!([
574    ///     {
575    ///         "Account": {
576    ///             "account_id": "alice.sandbox",
577    ///             "account": {
578    ///                 "amount": "1000000000000000000000000000000",
579    ///                 "locked": "0",
580    ///                 "code_hash": "11111111111111111111111111111111",
581    ///                 "storage_usage": 182
582    ///             }
583    ///         }
584    ///     }
585    /// ])).await?;
586    /// ```
587    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        // The sandbox_patch_state method returns an empty result on success
593        let _: serde_json::Value = self.call("sandbox_patch_state", params).await?;
594
595        // NOTE: For some reason, patching account-related items sometimes requires
596        // sending the patch twice for it to take effect reliably.
597        // See: https://github.com/near/near-workspaces-rs/commit/2b72b9b8491c3140ff2d30b0c45d09b200cb027b
598        let _: serde_json::Value = self
599            .call(
600                "sandbox_patch_state",
601                serde_json::json!({
602                    "records": records,
603                }),
604            )
605            .await?;
606
607        // Small delay to allow state to propagate - sandbox patch_state has race conditions
608        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
634// ============================================================================
635// Helper functions
636// ============================================================================
637
638/// Check if an HTTP status code is retryable.
639fn is_retryable_status(status: u16) -> bool {
640    // 408 Request Timeout - retryable
641    // 429 Too Many Requests - retryable (rate limiting)
642    // 503 Service Unavailable - retryable
643    // 5xx Server Errors - retryable
644    status == 408 || status == 429 || status == 503 || (500..600).contains(&status)
645}
646
647/// Extract InvalidNonce error from data.
648fn extract_invalid_nonce(data: &serde_json::Value) -> Option<RpcError> {
649    // Navigate nested error structure: TxExecutionError.InvalidTxError.InvalidNonce
650    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    // ========================================================================
667    // RetryConfig tests
668    // ========================================================================
669
670    #[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    // ========================================================================
700    // RpcClient tests
701    // ========================================================================
702
703    #[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    // ========================================================================
736    // is_retryable_status tests
737    // ========================================================================
738
739    #[test]
740    fn test_is_retryable_status() {
741        // Retryable statuses
742        assert!(is_retryable_status(408)); // Request Timeout
743        assert!(is_retryable_status(429)); // Too Many Requests
744        assert!(is_retryable_status(500)); // Internal Server Error
745        assert!(is_retryable_status(502)); // Bad Gateway
746        assert!(is_retryable_status(503)); // Service Unavailable
747        assert!(is_retryable_status(504)); // Gateway Timeout
748        assert!(is_retryable_status(599)); // Edge of 5xx range
749
750        // Non-retryable statuses
751        assert!(!is_retryable_status(200)); // OK
752        assert!(!is_retryable_status(201)); // Created
753        assert!(!is_retryable_status(400)); // Bad Request
754        assert!(!is_retryable_status(401)); // Unauthorized
755        assert!(!is_retryable_status(403)); // Forbidden
756        assert!(!is_retryable_status(404)); // Not Found
757        assert!(!is_retryable_status(422)); // Unprocessable Entity
758    }
759
760    // ========================================================================
761    // extract_invalid_nonce tests
762    // ========================================================================
763
764    #[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        // Missing TxExecutionError
790        let data = serde_json::json!({
791            "SomeOtherError": {}
792        });
793        assert!(extract_invalid_nonce(&data).is_none());
794
795        // Missing InvalidTxError
796        let data = serde_json::json!({
797            "TxExecutionError": {
798                "SomeOtherError": {}
799            }
800        });
801        assert!(extract_invalid_nonce(&data).is_none());
802
803        // Missing InvalidNonce
804        let data = serde_json::json!({
805            "TxExecutionError": {
806                "InvalidTxError": {
807                    "SomeOtherError": {}
808                }
809            }
810        });
811        assert!(extract_invalid_nonce(&data).is_none());
812
813        // Missing tx_nonce
814        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        // Missing ak_nonce
826        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    // ========================================================================
839    // NetworkConfig tests
840    // ========================================================================
841
842    #[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    // ========================================================================
855    // parse_rpc_error tests (via RpcClient)
856    // ========================================================================
857
858    #[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        // NO_SYNCED_BLOCKS
1057        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        // NOT_SYNCED_YET
1071        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}