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///
70/// The `result` field is deserialized as raw JSON first, then parsed into `T`
71/// only after confirming no error is present. This avoids deserialization
72/// failures when the RPC returns an error with a partial/unexpected `result`.
73#[derive(Deserialize)]
74struct JsonRpcResponse {
75    #[allow(dead_code)]
76    jsonrpc: String,
77    #[allow(dead_code)]
78    id: u64,
79    result: Option<serde_json::Value>,
80    error: Option<JsonRpcError>,
81}
82
83/// JSON-RPC error structure.
84/// NEAR RPC returns structured errors with name/cause/info pattern.
85#[derive(Debug, Deserialize)]
86struct JsonRpcError {
87    code: i64,
88    message: String,
89    #[serde(default)]
90    data: Option<serde_json::Value>,
91    #[serde(default)]
92    cause: Option<ErrorCause>,
93    #[serde(default)]
94    #[allow(dead_code)]
95    name: Option<String>,
96}
97
98/// Structured error cause from NEAR RPC.
99#[derive(Debug, Deserialize)]
100struct ErrorCause {
101    name: String,
102    #[serde(default)]
103    info: Option<serde_json::Value>,
104}
105
106/// Response from EXPERIMENTAL_call_function.
107/// Errors are returned through the JSON-RPC error envelope, so no `error`
108/// field is needed here.
109#[derive(Debug, Deserialize)]
110struct CallFunctionResponse {
111    result: Vec<u8>,
112    #[serde(default)]
113    logs: Vec<String>,
114    block_height: u64,
115    block_hash: CryptoHash,
116}
117
118/// Low-level JSON-RPC client for NEAR.
119pub struct RpcClient {
120    url: String,
121    client: reqwest::Client,
122    retry_config: RetryConfig,
123    request_id: AtomicU64,
124}
125
126impl RpcClient {
127    /// Create a new RPC client with the given URL.
128    pub fn new(url: impl Into<String>) -> Self {
129        Self {
130            url: url.into(),
131            client: reqwest::Client::new(),
132            retry_config: RetryConfig::default(),
133            request_id: AtomicU64::new(0),
134        }
135    }
136
137    /// Create a new RPC client with custom retry configuration.
138    pub fn with_retry_config(url: impl Into<String>, retry_config: RetryConfig) -> Self {
139        Self {
140            url: url.into(),
141            client: reqwest::Client::new(),
142            retry_config,
143            request_id: AtomicU64::new(0),
144        }
145    }
146
147    /// Get the RPC URL.
148    pub fn url(&self) -> &str {
149        &self.url
150    }
151
152    /// Make a raw RPC call with retries.
153    #[tracing::instrument(skip(self, params), fields(rpc.method = method))]
154    pub async fn call<P: Serialize, R: DeserializeOwned>(
155        &self,
156        method: &str,
157        params: P,
158    ) -> Result<R, RpcError> {
159        let total_attempts = self.retry_config.max_retries + 1;
160
161        for attempt in 0..total_attempts {
162            let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
163
164            let request = JsonRpcRequest {
165                jsonrpc: "2.0",
166                id: request_id,
167                method,
168                params: &params,
169            };
170
171            match self.try_call::<R>(&request).await {
172                Ok(result) => return Ok(result),
173                Err(e) if e.is_retryable() && attempt < total_attempts - 1 => {
174                    let delay = std::cmp::min(
175                        self.retry_config.initial_delay_ms * 2u64.pow(attempt),
176                        self.retry_config.max_delay_ms,
177                    );
178                    tracing::warn!(
179                        attempt = attempt + 1,
180                        max_attempts = total_attempts,
181                        delay_ms = delay,
182                        error = %e,
183                        "RPC request failed, retrying"
184                    );
185                    tokio::time::sleep(Duration::from_millis(delay)).await;
186                    continue;
187                }
188                Err(e) => {
189                    tracing::error!(error = %e, "RPC request failed");
190                    return Err(e);
191                }
192            }
193        }
194
195        unreachable!("all loop iterations return")
196    }
197
198    /// Single attempt to make an RPC call.
199    async fn try_call<R: DeserializeOwned>(
200        &self,
201        request: &JsonRpcRequest<'_, impl Serialize>,
202    ) -> Result<R, RpcError> {
203        let response = self
204            .client
205            .post(&self.url)
206            .header("Content-Type", "application/json")
207            .json(request)
208            .send()
209            .await?;
210
211        let status = response.status();
212        let body = response.text().await?;
213
214        if !status.is_success() {
215            let retryable = is_retryable_status(status.as_u16());
216            return Err(RpcError::network(
217                format!("HTTP {}: {}", status, body),
218                Some(status.as_u16()),
219                retryable,
220            ));
221        }
222
223        let rpc_response: JsonRpcResponse = serde_json::from_str(&body).map_err(RpcError::Json)?;
224
225        if let Some(error) = rpc_response.error {
226            return Err(self.parse_rpc_error(&error));
227        }
228
229        let result_value = rpc_response
230            .result
231            .ok_or_else(|| RpcError::InvalidResponse("Missing result in response".to_string()))?;
232
233        // NEAR's `query` endpoint sometimes returns errors inside the result
234        // object (with an "error" field) instead of in the JSON-RPC error
235        // envelope. Only check for this on the `query` method to avoid
236        // misclassifying legitimate results from other methods.
237        if request.method == "query" {
238            if let Some(error_str) = result_value.get("error").and_then(|e| e.as_str()) {
239                let synthetic = JsonRpcError {
240                    // Use -32600 (Invalid Request) rather than -32000 (Server Error)
241                    // so deterministic failures don't get retried.
242                    code: -32600,
243                    message: error_str.to_string(),
244                    data: Some(serde_json::Value::String(error_str.to_string())),
245                    cause: None,
246                    name: None,
247                };
248                return Err(self.parse_rpc_error(&synthetic));
249            }
250        }
251
252        serde_json::from_value(result_value).map_err(RpcError::Json)
253    }
254
255    /// Parse an RPC error into a specific error type.
256    fn parse_rpc_error(&self, error: &JsonRpcError) -> RpcError {
257        // First, check the direct cause field (NEAR RPC structured errors)
258        if let Some(cause) = &error.cause {
259            let cause_name = cause.name.as_str();
260            let info = cause.info.as_ref();
261            let data = &error.data;
262
263            match cause_name {
264                "UNKNOWN_ACCOUNT" => {
265                    if let Some(account_id) = info
266                        .and_then(|i| i.get("requested_account_id"))
267                        .and_then(|a| a.as_str())
268                    {
269                        if let Ok(account_id) = account_id.parse() {
270                            return RpcError::AccountNotFound(account_id);
271                        }
272                    }
273                }
274                "INVALID_ACCOUNT" => {
275                    let account_id = info
276                        .and_then(|i| i.get("requested_account_id"))
277                        .and_then(|a| a.as_str())
278                        .unwrap_or("unknown");
279                    return RpcError::InvalidAccount(account_id.to_string());
280                }
281                "UNKNOWN_ACCESS_KEY" => {
282                    if let Some(public_key) = info
283                        .and_then(|i| i.get("public_key"))
284                        .and_then(|k| k.as_str())
285                        .and_then(|k| k.parse().ok())
286                    {
287                        // Legacy `query` includes requested_account_id;
288                        // EXPERIMENTAL_view_access_key does not (the caller
289                        // already knows the account). Fall back to "unknown".
290                        let account_id = info
291                            .and_then(|i| i.get("requested_account_id"))
292                            .and_then(|a| a.as_str())
293                            .and_then(|a| a.parse().ok())
294                            .unwrap_or_else(|| "unknown".parse().unwrap());
295                        return RpcError::AccessKeyNotFound {
296                            account_id,
297                            public_key,
298                        };
299                    }
300                }
301                "UNKNOWN_BLOCK" => {
302                    let block_ref = data
303                        .as_ref()
304                        .and_then(|d| d.as_str())
305                        .unwrap_or(&error.message);
306                    return RpcError::UnknownBlock(block_ref.to_string());
307                }
308                "UNKNOWN_CHUNK" => {
309                    let chunk_ref = info
310                        .and_then(|i| i.get("chunk_hash"))
311                        .and_then(|c| c.as_str())
312                        .unwrap_or(&error.message);
313                    return RpcError::UnknownChunk(chunk_ref.to_string());
314                }
315                "UNKNOWN_EPOCH" => {
316                    let block_ref = data
317                        .as_ref()
318                        .and_then(|d| d.as_str())
319                        .unwrap_or(&error.message);
320                    return RpcError::UnknownEpoch(block_ref.to_string());
321                }
322                "UNKNOWN_RECEIPT" => {
323                    let receipt_id = info
324                        .and_then(|i| i.get("receipt_id"))
325                        .and_then(|r| r.as_str())
326                        .unwrap_or("unknown");
327                    return RpcError::UnknownReceipt(receipt_id.to_string());
328                }
329                "NO_CONTRACT_CODE" => {
330                    let account_id = info
331                        .and_then(|i| {
332                            i.get("contract_account_id")
333                                .or_else(|| i.get("account_id"))
334                                .or_else(|| i.get("contract_id"))
335                        })
336                        .and_then(|a| a.as_str())
337                        .unwrap_or("unknown");
338                    if let Ok(account_id) = account_id.parse() {
339                        return RpcError::ContractNotDeployed(account_id);
340                    }
341                }
342                "TOO_LARGE_CONTRACT_STATE" => {
343                    let account_id = info
344                        .and_then(|i| i.get("account_id").or_else(|| i.get("contract_id")))
345                        .and_then(|a| a.as_str())
346                        .unwrap_or("unknown");
347                    if let Ok(account_id) = account_id.parse() {
348                        return RpcError::ContractStateTooLarge(account_id);
349                    }
350                }
351                "CONTRACT_EXECUTION_ERROR" => {
352                    // Check for CodeDoesNotExist inside vm_error
353                    // (EXPERIMENTAL_call_function wraps this as CONTRACT_EXECUTION_ERROR)
354                    if let Some(vm_error) = info.and_then(|i| i.get("vm_error")) {
355                        if let Some(compilation_err) = vm_error.get("CompilationError") {
356                            if let Some(code_not_exist) = compilation_err.get("CodeDoesNotExist") {
357                                if let Some(account_id) = code_not_exist
358                                    .get("account_id")
359                                    .and_then(|a| a.as_str())
360                                    .and_then(|a| a.parse().ok())
361                                {
362                                    return RpcError::ContractNotDeployed(account_id);
363                                }
364                            }
365                        }
366                    }
367
368                    // Legacy `query` includes contract_id/method_name;
369                    // EXPERIMENTAL_call_function does not (the caller
370                    // already knows them). Fall back to "unknown".
371                    let contract_id = info
372                        .and_then(|i| i.get("contract_id"))
373                        .and_then(|c| c.as_str())
374                        .unwrap_or("unknown");
375                    let method_name = info
376                        .and_then(|i| i.get("method_name"))
377                        .and_then(|m| m.as_str())
378                        .map(String::from);
379                    let message = data
380                        .as_ref()
381                        .and_then(|d| d.as_str())
382                        .map(|s| s.to_string())
383                        .or_else(|| {
384                            // EXPERIMENTAL endpoint: use vm_error as fallback
385                            // when data isn't a string
386                            info.and_then(|i| i.get("vm_error")).map(|v| v.to_string())
387                        })
388                        .unwrap_or_else(|| error.message.clone());
389                    if let Ok(contract_id) = contract_id.parse() {
390                        return RpcError::ContractExecution {
391                            contract_id,
392                            method_name,
393                            message,
394                        };
395                    }
396                }
397                "UNAVAILABLE_SHARD" => {
398                    return RpcError::ShardUnavailable(error.message.clone());
399                }
400                "NO_SYNCED_BLOCKS" | "NOT_SYNCED_YET" => {
401                    return RpcError::NodeNotSynced(error.message.clone());
402                }
403                "INVALID_SHARD_ID" => {
404                    let shard_id = info
405                        .and_then(|i| i.get("shard_id"))
406                        .map(|s| s.to_string())
407                        .unwrap_or_else(|| "unknown".to_string());
408                    return RpcError::InvalidShardId(shard_id);
409                }
410                "INVALID_TRANSACTION" => {
411                    return RpcError::invalid_transaction(&error.message, data.clone());
412                }
413                "TIMEOUT_ERROR" => {
414                    let tx_hash = info
415                        .and_then(|i| i.get("transaction_hash"))
416                        .and_then(|h| h.as_str())
417                        .map(String::from);
418                    return RpcError::RequestTimeout {
419                        message: error.message.clone(),
420                        transaction_hash: tx_hash,
421                    };
422                }
423                "PARSE_ERROR" => {
424                    return RpcError::ParseError(error.message.clone());
425                }
426                "INTERNAL_ERROR" => {
427                    return RpcError::InternalError(error.message.clone());
428                }
429                _ => {}
430            }
431        }
432
433        // Fallback: check for string error messages in data field
434        if let Some(data) = &error.data {
435            if let Some(error_str) = data.as_str() {
436                if error_str.contains("does not exist") {
437                    // Try to extract account ID from error message
438                    // Format: "account X does not exist while viewing"
439                    if let Some(start) = error_str.strip_prefix("account ") {
440                        if let Some(account_str) = start.split_whitespace().next() {
441                            if let Ok(account_id) = account_str.parse() {
442                                return RpcError::AccountNotFound(account_id);
443                            }
444                        }
445                    }
446                }
447            }
448        }
449
450        RpcError::Rpc {
451            code: error.code,
452            message: error.message.clone(),
453            data: error.data.clone(),
454        }
455    }
456
457    // ========================================================================
458    // High-level RPC methods
459    // ========================================================================
460
461    /// View account information.
462    #[tracing::instrument(skip(self, block), fields(%account_id))]
463    pub async fn view_account(
464        &self,
465        account_id: &AccountId,
466        block: BlockReference,
467    ) -> Result<AccountView, RpcError> {
468        let mut params = serde_json::json!({
469            "account_id": account_id.to_string(),
470        });
471        self.merge_block_reference(&mut params, &block);
472        self.call("EXPERIMENTAL_view_account", params).await
473    }
474
475    /// View access key information.
476    #[tracing::instrument(skip(self, block), fields(%account_id, %public_key))]
477    pub async fn view_access_key(
478        &self,
479        account_id: &AccountId,
480        public_key: &PublicKey,
481        block: BlockReference,
482    ) -> Result<AccessKeyView, RpcError> {
483        let mut params = serde_json::json!({
484            "account_id": account_id.to_string(),
485            "public_key": public_key.to_string(),
486        });
487        self.merge_block_reference(&mut params, &block);
488        self.call("EXPERIMENTAL_view_access_key", params)
489            .await
490            .map_err(|e| match e {
491                // The EXPERIMENTAL endpoint's UNKNOWN_ACCESS_KEY error omits
492                // the account_id from its info payload. Patch it in from the
493                // request params so callers get a complete error.
494                RpcError::AccessKeyNotFound { public_key, .. } => RpcError::AccessKeyNotFound {
495                    account_id: account_id.clone(),
496                    public_key,
497                },
498                other => other,
499            })
500    }
501
502    /// View all access keys for an account.
503    #[tracing::instrument(skip(self, block), fields(%account_id))]
504    pub async fn view_access_key_list(
505        &self,
506        account_id: &AccountId,
507        block: BlockReference,
508    ) -> Result<AccessKeyListView, RpcError> {
509        let mut params = serde_json::json!({
510            "account_id": account_id.to_string(),
511        });
512        self.merge_block_reference(&mut params, &block);
513        self.call("EXPERIMENTAL_view_access_key_list", params).await
514    }
515
516    /// Call a view function on a contract.
517    #[tracing::instrument(skip(self, args, block), fields(contract_id = %account_id, method = method_name))]
518    pub async fn view_function(
519        &self,
520        account_id: &AccountId,
521        method_name: &str,
522        args: &[u8],
523        block: BlockReference,
524    ) -> Result<ViewFunctionResult, RpcError> {
525        let mut params = serde_json::json!({
526            "account_id": account_id.to_string(),
527            "method_name": method_name,
528            "args_base64": STANDARD.encode(args),
529        });
530        self.merge_block_reference(&mut params, &block);
531
532        // EXPERIMENTAL_call_function returns errors through the JSON-RPC
533        // error envelope, so `parse_rpc_error` handles them. Patch in
534        // the caller-known contract_id and method_name when they are
535        // missing from the error info (the experimental endpoint omits them).
536        let response: CallFunctionResponse = self
537            .call("EXPERIMENTAL_call_function", params)
538            .await
539            .map_err(|e| match e {
540                RpcError::ContractExecution { message, .. } => RpcError::ContractExecution {
541                    contract_id: account_id.clone(),
542                    method_name: Some(method_name.to_string()),
543                    message,
544                },
545                other => other,
546            })?;
547
548        Ok(ViewFunctionResult {
549            result: response.result,
550            logs: response.logs,
551            block_height: response.block_height,
552            block_hash: response.block_hash,
553        })
554    }
555
556    /// Get block information.
557    #[tracing::instrument(skip(self, block))]
558    pub async fn block(&self, block: BlockReference) -> Result<BlockView, RpcError> {
559        let params = block.to_rpc_params();
560        self.call("block", params).await
561    }
562
563    /// Get node status.
564    #[tracing::instrument(skip(self))]
565    pub async fn status(&self) -> Result<StatusResponse, RpcError> {
566        self.call("status", serde_json::json!([])).await
567    }
568
569    /// Get current gas price.
570    #[tracing::instrument(skip(self))]
571    pub async fn gas_price(&self, block_hash: Option<&CryptoHash>) -> Result<GasPrice, RpcError> {
572        let params = match block_hash {
573            Some(hash) => serde_json::json!([hash.to_string()]),
574            None => serde_json::json!([serde_json::Value::Null]),
575        };
576        self.call("gas_price", params).await
577    }
578
579    /// Send a signed transaction.
580    #[tracing::instrument(skip(self, signed_tx), fields(
581        tx_hash = tracing::field::Empty,
582        sender = %signed_tx.transaction.signer_id,
583        receiver = %signed_tx.transaction.receiver_id,
584        ?wait_until,
585    ))]
586    pub async fn send_tx(
587        &self,
588        signed_tx: &SignedTransaction,
589        wait_until: TxExecutionStatus,
590    ) -> Result<SendTxResponse, RpcError> {
591        let tx_hash = signed_tx.get_hash();
592        tracing::Span::current().record("tx_hash", tracing::field::display(&tx_hash));
593        let params = serde_json::json!({
594            "signed_tx_base64": signed_tx.to_base64(),
595            "wait_until": wait_until.as_str(),
596        });
597        let mut response: SendTxResponse = self.call("send_tx", params).await?;
598        response.transaction_hash = tx_hash;
599        Ok(response)
600    }
601
602    /// Get transaction status with full receipt details.
603    ///
604    /// Uses EXPERIMENTAL_tx_status which returns complete receipt information.
605    #[tracing::instrument(skip(self), fields(%tx_hash, sender = %sender_id, ?wait_until))]
606    pub async fn tx_status(
607        &self,
608        tx_hash: &CryptoHash,
609        sender_id: &AccountId,
610        wait_until: TxExecutionStatus,
611    ) -> Result<SendTxWithReceiptsResponse, RpcError> {
612        let params = serde_json::json!({
613            "tx_hash": tx_hash.to_string(),
614            "sender_account_id": sender_id.to_string(),
615            "wait_until": wait_until.as_str(),
616        });
617        self.call("EXPERIMENTAL_tx_status", params).await
618    }
619
620    /// Merge block reference parameters into a JSON object.
621    fn merge_block_reference(&self, params: &mut serde_json::Value, block: &BlockReference) {
622        if let serde_json::Value::Object(block_params) = block.to_rpc_params() {
623            if let serde_json::Value::Object(map) = params {
624                map.extend(block_params);
625            }
626        }
627    }
628
629    // ========================================================================
630    // Sandbox-only methods
631    // ========================================================================
632
633    /// Patch account state in sandbox.
634    ///
635    /// This is a sandbox-only method that allows modifying account state directly,
636    /// useful for testing scenarios that require specific account configurations
637    /// (e.g., setting a high balance for staking tests).
638    ///
639    /// # Arguments
640    ///
641    /// * `records` - State records to patch (Account, Data, Contract, AccessKey, etc.)
642    ///
643    /// # Example
644    ///
645    /// ```rust,ignore
646    /// // Set account balance to 1M NEAR
647    /// rpc.sandbox_patch_state(serde_json::json!([
648    ///     {
649    ///         "Account": {
650    ///             "account_id": "alice.sandbox",
651    ///             "account": {
652    ///                 "amount": "1000000000000000000000000000000",
653    ///                 "locked": "0",
654    ///                 "code_hash": "11111111111111111111111111111111",
655    ///                 "storage_usage": 182
656    ///             }
657    ///         }
658    ///     }
659    /// ])).await?;
660    /// ```
661    /// Fast-forward the sandbox by `delta_height` blocks.
662    ///
663    /// This is useful for testing time-dependent logic (e.g., lockups, staking
664    /// epoch changes) without waiting for real block production.
665    ///
666    /// **Note:** This can take a while for large deltas — the sandbox node
667    /// internally produces all intermediate blocks. The RPC call will block
668    /// until fast-forwarding completes (up to 1 hour server-side timeout).
669    ///
670    /// # Example
671    ///
672    /// ```rust,ignore
673    /// // Advance the sandbox by 1000 blocks
674    /// rpc.sandbox_fast_forward(1000).await?;
675    /// ```
676    pub async fn sandbox_fast_forward(&self, delta_height: u64) -> Result<(), RpcError> {
677        let params = serde_json::json!({
678            "delta_height": delta_height,
679        });
680
681        let _: serde_json::Value = self.call("sandbox_fast_forward", params).await?;
682        Ok(())
683    }
684
685    pub async fn sandbox_patch_state(&self, records: serde_json::Value) -> Result<(), RpcError> {
686        let params = serde_json::json!({
687            "records": records,
688        });
689
690        // The sandbox_patch_state method returns an empty result on success
691        let _: serde_json::Value = self.call("sandbox_patch_state", params).await?;
692
693        // NOTE: For some reason, patching account-related items sometimes requires
694        // sending the patch twice for it to take effect reliably.
695        // See: https://github.com/near/near-workspaces-rs/commit/2b72b9b8491c3140ff2d30b0c45d09b200cb027b
696        let _: serde_json::Value = self
697            .call(
698                "sandbox_patch_state",
699                serde_json::json!({
700                    "records": records,
701                }),
702            )
703            .await?;
704
705        // Small delay to allow state to propagate - sandbox patch_state has race conditions
706        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
707
708        Ok(())
709    }
710}
711
712impl Clone for RpcClient {
713    fn clone(&self) -> Self {
714        Self {
715            url: self.url.clone(),
716            client: self.client.clone(),
717            retry_config: self.retry_config.clone(),
718            request_id: AtomicU64::new(0),
719        }
720    }
721}
722
723impl std::fmt::Debug for RpcClient {
724    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725        f.debug_struct("RpcClient")
726            .field("url", &self.url)
727            .field("retry_config", &self.retry_config)
728            .finish()
729    }
730}
731
732// ============================================================================
733// Helper functions
734// ============================================================================
735
736/// Check if an HTTP status code is retryable.
737fn is_retryable_status(status: u16) -> bool {
738    // 408 Request Timeout - retryable
739    // 429 Too Many Requests - retryable (rate limiting)
740    // 503 Service Unavailable - retryable
741    // 5xx Server Errors - retryable
742    status == 408 || status == 429 || status == 503 || (500..600).contains(&status)
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    // ========================================================================
750    // RetryConfig tests
751    // ========================================================================
752
753    #[test]
754    fn test_retry_config_default() {
755        let config = RetryConfig::default();
756        assert_eq!(config.max_retries, 3);
757        assert_eq!(config.initial_delay_ms, 500);
758        assert_eq!(config.max_delay_ms, 5000);
759    }
760
761    #[test]
762    fn test_retry_config_clone() {
763        let config = RetryConfig {
764            max_retries: 5,
765            initial_delay_ms: 100,
766            max_delay_ms: 1000,
767        };
768        let cloned = config.clone();
769        assert_eq!(cloned.max_retries, 5);
770        assert_eq!(cloned.initial_delay_ms, 100);
771        assert_eq!(cloned.max_delay_ms, 1000);
772    }
773
774    #[test]
775    fn test_retry_config_debug() {
776        let config = RetryConfig::default();
777        let debug = format!("{:?}", config);
778        assert!(debug.contains("RetryConfig"));
779        assert!(debug.contains("max_retries"));
780    }
781
782    // ========================================================================
783    // RpcClient tests
784    // ========================================================================
785
786    #[test]
787    fn test_rpc_client_new() {
788        let client = RpcClient::new("https://rpc.testnet.near.org");
789        assert_eq!(client.url(), "https://rpc.testnet.near.org");
790    }
791
792    #[test]
793    fn test_rpc_client_with_retry_config() {
794        let config = RetryConfig {
795            max_retries: 5,
796            initial_delay_ms: 100,
797            max_delay_ms: 1000,
798        };
799        let client = RpcClient::with_retry_config("https://rpc.example.com", config);
800        assert_eq!(client.url(), "https://rpc.example.com");
801    }
802
803    #[test]
804    fn test_rpc_client_clone() {
805        let client = RpcClient::new("https://rpc.testnet.near.org");
806        let cloned = client.clone();
807        assert_eq!(cloned.url(), client.url());
808    }
809
810    #[test]
811    fn test_rpc_client_debug() {
812        let client = RpcClient::new("https://rpc.testnet.near.org");
813        let debug = format!("{:?}", client);
814        assert!(debug.contains("RpcClient"));
815        assert!(debug.contains("rpc.testnet.near.org"));
816    }
817
818    // ========================================================================
819    // is_retryable_status tests
820    // ========================================================================
821
822    #[test]
823    fn test_is_retryable_status() {
824        // Retryable statuses
825        assert!(is_retryable_status(408)); // Request Timeout
826        assert!(is_retryable_status(429)); // Too Many Requests
827        assert!(is_retryable_status(500)); // Internal Server Error
828        assert!(is_retryable_status(502)); // Bad Gateway
829        assert!(is_retryable_status(503)); // Service Unavailable
830        assert!(is_retryable_status(504)); // Gateway Timeout
831        assert!(is_retryable_status(599)); // Edge of 5xx range
832
833        // Non-retryable statuses
834        assert!(!is_retryable_status(200)); // OK
835        assert!(!is_retryable_status(201)); // Created
836        assert!(!is_retryable_status(400)); // Bad Request
837        assert!(!is_retryable_status(401)); // Unauthorized
838        assert!(!is_retryable_status(403)); // Forbidden
839        assert!(!is_retryable_status(404)); // Not Found
840        assert!(!is_retryable_status(422)); // Unprocessable Entity
841    }
842
843    // ========================================================================
844    // InvalidTxError parsing tests
845    // ========================================================================
846
847    #[test]
848    fn test_invalid_transaction_parses_invalid_nonce() {
849        use crate::types::InvalidTxError;
850        let data = serde_json::json!({
851            "TxExecutionError": {
852                "InvalidTxError": {
853                    "InvalidNonce": {
854                        "tx_nonce": 5,
855                        "ak_nonce": 10
856                    }
857                }
858            }
859        });
860        let err = RpcError::invalid_transaction("invalid nonce", Some(data));
861        match err {
862            RpcError::InvalidTx(InvalidTxError::InvalidNonce { tx_nonce, ak_nonce }) => {
863                assert_eq!(tx_nonce, 5);
864                assert_eq!(ak_nonce, 10);
865            }
866            other => panic!("Expected InvalidTx(InvalidNonce), got: {other:?}"),
867        }
868    }
869
870    #[test]
871    fn test_invalid_transaction_parses_top_level_invalid_tx() {
872        use crate::types::InvalidTxError;
873        // Some RPC versions put InvalidTxError at the top level
874        let data = serde_json::json!({
875            "InvalidTxError": {
876                "NotEnoughBalance": {
877                    "signer_id": "alice.near",
878                    "balance": "1000000000000000000000000",
879                    "cost": "9000000000000000000000000"
880                }
881            }
882        });
883        let err = RpcError::invalid_transaction("insufficient balance", Some(data));
884        assert!(
885            matches!(
886                err,
887                RpcError::InvalidTx(InvalidTxError::NotEnoughBalance { .. })
888            ),
889            "Expected InvalidTx(NotEnoughBalance), got: {err:?}"
890        );
891    }
892
893    #[test]
894    fn test_invalid_transaction_falls_back_on_unparseable() {
895        // When data doesn't contain a parseable InvalidTxError, falls back
896        let data = serde_json::json!({ "SomeOtherError": {} });
897        let err = RpcError::invalid_transaction("some error", Some(data));
898        assert!(matches!(err, RpcError::InvalidTransaction { .. }));
899    }
900
901    // ========================================================================
902    // NetworkConfig tests
903    // ========================================================================
904
905    #[test]
906    fn test_mainnet_config() {
907        assert!(MAINNET.rpc_url.contains("fastnear"));
908        assert_eq!(MAINNET.network_id, "mainnet");
909    }
910
911    #[test]
912    fn test_testnet_config() {
913        assert!(TESTNET.rpc_url.contains("fastnear") || TESTNET.rpc_url.contains("test"));
914        assert_eq!(TESTNET.network_id, "testnet");
915    }
916
917    // ========================================================================
918    // parse_rpc_error tests (via RpcClient)
919    // ========================================================================
920
921    #[test]
922    fn test_parse_rpc_error_unknown_account() {
923        let client = RpcClient::new("https://example.com");
924        let error = JsonRpcError {
925            code: -32000,
926            message: "Server error".to_string(),
927            data: None,
928            cause: Some(ErrorCause {
929                name: "UNKNOWN_ACCOUNT".to_string(),
930                info: Some(serde_json::json!({
931                    "requested_account_id": "nonexistent.near"
932                })),
933            }),
934            name: None,
935        };
936        let result = client.parse_rpc_error(&error);
937        assert!(matches!(result, RpcError::AccountNotFound(_)));
938    }
939
940    #[test]
941    fn test_parse_rpc_error_unknown_access_key_legacy() {
942        // Legacy `query` endpoint includes requested_account_id in info
943        let client = RpcClient::new("https://example.com");
944        let error = JsonRpcError {
945            code: -32000,
946            message: "Server error".to_string(),
947            data: None,
948            cause: Some(ErrorCause {
949                name: "UNKNOWN_ACCESS_KEY".to_string(),
950                info: Some(serde_json::json!({
951                    "requested_account_id": "alice.near",
952                    "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
953                })),
954            }),
955            name: None,
956        };
957        let result = client.parse_rpc_error(&error);
958        match result {
959            RpcError::AccessKeyNotFound {
960                account_id,
961                public_key,
962            } => {
963                assert_eq!(account_id.as_str(), "alice.near");
964                assert!(public_key.to_string().contains("ed25519:"));
965            }
966            _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
967        }
968    }
969
970    #[test]
971    fn test_parse_rpc_error_unknown_access_key_experimental() {
972        // EXPERIMENTAL_view_access_key omits requested_account_id from info
973        let client = RpcClient::new("https://example.com");
974        let error = JsonRpcError {
975            code: -32000,
976            message: "Server error".to_string(),
977            data: Some(serde_json::Value::String(
978                "Access key for public key ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp does not exist while viewing".to_string()
979            )),
980            cause: Some(ErrorCause {
981                name: "UNKNOWN_ACCESS_KEY".to_string(),
982                info: Some(serde_json::json!({
983                    "public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
984                    "block_height": 243789592,
985                    "block_hash": "EC5A7qc6rixfN8T4T9Gkt78H5pAsvdcjAos8Z7kFLJgi"
986                })),
987            }),
988            name: Some("HANDLER_ERROR".to_string()),
989        };
990        let result = client.parse_rpc_error(&error);
991        match result {
992            RpcError::AccessKeyNotFound {
993                account_id,
994                public_key,
995            } => {
996                // account_id falls back to "unknown" — caller enriches it
997                assert_eq!(account_id.as_str(), "unknown");
998                assert!(public_key.to_string().contains("ed25519:"));
999            }
1000            _ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
1001        }
1002    }
1003
1004    #[test]
1005    fn test_parse_rpc_error_invalid_account() {
1006        let client = RpcClient::new("https://example.com");
1007        let error = JsonRpcError {
1008            code: -32000,
1009            message: "Server error".to_string(),
1010            data: None,
1011            cause: Some(ErrorCause {
1012                name: "INVALID_ACCOUNT".to_string(),
1013                info: Some(serde_json::json!({
1014                    "requested_account_id": "invalid@account"
1015                })),
1016            }),
1017            name: None,
1018        };
1019        let result = client.parse_rpc_error(&error);
1020        assert!(matches!(result, RpcError::InvalidAccount(_)));
1021    }
1022
1023    #[test]
1024    fn test_parse_rpc_error_unknown_block() {
1025        let client = RpcClient::new("https://example.com");
1026        let error = JsonRpcError {
1027            code: -32000,
1028            message: "Block not found".to_string(),
1029            data: Some(serde_json::json!("12345")),
1030            cause: Some(ErrorCause {
1031                name: "UNKNOWN_BLOCK".to_string(),
1032                info: None,
1033            }),
1034            name: None,
1035        };
1036        let result = client.parse_rpc_error(&error);
1037        assert!(matches!(result, RpcError::UnknownBlock(_)));
1038    }
1039
1040    #[test]
1041    fn test_parse_rpc_error_unknown_chunk() {
1042        let client = RpcClient::new("https://example.com");
1043        let error = JsonRpcError {
1044            code: -32000,
1045            message: "Chunk not found".to_string(),
1046            data: None,
1047            cause: Some(ErrorCause {
1048                name: "UNKNOWN_CHUNK".to_string(),
1049                info: Some(serde_json::json!({
1050                    "chunk_hash": "abc123"
1051                })),
1052            }),
1053            name: None,
1054        };
1055        let result = client.parse_rpc_error(&error);
1056        assert!(matches!(result, RpcError::UnknownChunk(_)));
1057    }
1058
1059    #[test]
1060    fn test_parse_rpc_error_unknown_epoch() {
1061        let client = RpcClient::new("https://example.com");
1062        let error = JsonRpcError {
1063            code: -32000,
1064            message: "Epoch not found".to_string(),
1065            data: Some(serde_json::json!("epoch123")),
1066            cause: Some(ErrorCause {
1067                name: "UNKNOWN_EPOCH".to_string(),
1068                info: None,
1069            }),
1070            name: None,
1071        };
1072        let result = client.parse_rpc_error(&error);
1073        assert!(matches!(result, RpcError::UnknownEpoch(_)));
1074    }
1075
1076    #[test]
1077    fn test_parse_rpc_error_unknown_receipt() {
1078        let client = RpcClient::new("https://example.com");
1079        let error = JsonRpcError {
1080            code: -32000,
1081            message: "Receipt not found".to_string(),
1082            data: None,
1083            cause: Some(ErrorCause {
1084                name: "UNKNOWN_RECEIPT".to_string(),
1085                info: Some(serde_json::json!({
1086                    "receipt_id": "receipt123"
1087                })),
1088            }),
1089            name: None,
1090        };
1091        let result = client.parse_rpc_error(&error);
1092        assert!(matches!(result, RpcError::UnknownReceipt(_)));
1093    }
1094
1095    #[test]
1096    fn test_parse_rpc_error_no_contract_code() {
1097        let client = RpcClient::new("https://example.com");
1098        let error = JsonRpcError {
1099            code: -32000,
1100            message: "No contract code".to_string(),
1101            data: None,
1102            cause: Some(ErrorCause {
1103                name: "NO_CONTRACT_CODE".to_string(),
1104                info: Some(serde_json::json!({
1105                    "contract_account_id": "no-contract.near"
1106                })),
1107            }),
1108            name: None,
1109        };
1110        let result = client.parse_rpc_error(&error);
1111        assert!(matches!(result, RpcError::ContractNotDeployed(_)));
1112    }
1113
1114    #[test]
1115    fn test_parse_rpc_error_too_large_contract_state() {
1116        let client = RpcClient::new("https://example.com");
1117        let error = JsonRpcError {
1118            code: -32000,
1119            message: "Contract state too large".to_string(),
1120            data: None,
1121            cause: Some(ErrorCause {
1122                name: "TOO_LARGE_CONTRACT_STATE".to_string(),
1123                info: Some(serde_json::json!({
1124                    "account_id": "large-state.near"
1125                })),
1126            }),
1127            name: None,
1128        };
1129        let result = client.parse_rpc_error(&error);
1130        assert!(matches!(result, RpcError::ContractStateTooLarge(_)));
1131    }
1132
1133    #[test]
1134    fn test_parse_rpc_error_unavailable_shard() {
1135        let client = RpcClient::new("https://example.com");
1136        let error = JsonRpcError {
1137            code: -32000,
1138            message: "Shard unavailable".to_string(),
1139            data: None,
1140            cause: Some(ErrorCause {
1141                name: "UNAVAILABLE_SHARD".to_string(),
1142                info: None,
1143            }),
1144            name: None,
1145        };
1146        let result = client.parse_rpc_error(&error);
1147        assert!(matches!(result, RpcError::ShardUnavailable(_)));
1148    }
1149
1150    #[test]
1151    fn test_parse_rpc_error_not_synced() {
1152        let client = RpcClient::new("https://example.com");
1153
1154        // NO_SYNCED_BLOCKS
1155        let error = JsonRpcError {
1156            code: -32000,
1157            message: "No synced blocks".to_string(),
1158            data: None,
1159            cause: Some(ErrorCause {
1160                name: "NO_SYNCED_BLOCKS".to_string(),
1161                info: None,
1162            }),
1163            name: None,
1164        };
1165        let result = client.parse_rpc_error(&error);
1166        assert!(matches!(result, RpcError::NodeNotSynced(_)));
1167
1168        // NOT_SYNCED_YET
1169        let error = JsonRpcError {
1170            code: -32000,
1171            message: "Not synced yet".to_string(),
1172            data: None,
1173            cause: Some(ErrorCause {
1174                name: "NOT_SYNCED_YET".to_string(),
1175                info: None,
1176            }),
1177            name: None,
1178        };
1179        let result = client.parse_rpc_error(&error);
1180        assert!(matches!(result, RpcError::NodeNotSynced(_)));
1181    }
1182
1183    #[test]
1184    fn test_parse_rpc_error_invalid_shard_id() {
1185        let client = RpcClient::new("https://example.com");
1186        let error = JsonRpcError {
1187            code: -32000,
1188            message: "Invalid shard ID".to_string(),
1189            data: None,
1190            cause: Some(ErrorCause {
1191                name: "INVALID_SHARD_ID".to_string(),
1192                info: Some(serde_json::json!({
1193                    "shard_id": 99
1194                })),
1195            }),
1196            name: None,
1197        };
1198        let result = client.parse_rpc_error(&error);
1199        assert!(matches!(result, RpcError::InvalidShardId(_)));
1200    }
1201
1202    #[test]
1203    fn test_parse_rpc_error_invalid_transaction() {
1204        let client = RpcClient::new("https://example.com");
1205        let error = JsonRpcError {
1206            code: -32000,
1207            message: "Invalid transaction".to_string(),
1208            data: None,
1209            cause: Some(ErrorCause {
1210                name: "INVALID_TRANSACTION".to_string(),
1211                info: None,
1212            }),
1213            name: None,
1214        };
1215        let result = client.parse_rpc_error(&error);
1216        assert!(matches!(result, RpcError::InvalidTransaction { .. }));
1217    }
1218
1219    #[test]
1220    fn test_parse_rpc_error_timeout() {
1221        let client = RpcClient::new("https://example.com");
1222        let error = JsonRpcError {
1223            code: -32000,
1224            message: "Request timed out".to_string(),
1225            data: None,
1226            cause: Some(ErrorCause {
1227                name: "TIMEOUT_ERROR".to_string(),
1228                info: Some(serde_json::json!({
1229                    "transaction_hash": "tx123"
1230                })),
1231            }),
1232            name: None,
1233        };
1234        let result = client.parse_rpc_error(&error);
1235        assert!(matches!(result, RpcError::RequestTimeout { .. }));
1236    }
1237
1238    #[test]
1239    fn test_parse_rpc_error_parse_error() {
1240        let client = RpcClient::new("https://example.com");
1241        let error = JsonRpcError {
1242            code: -32700,
1243            message: "Parse error".to_string(),
1244            data: None,
1245            cause: Some(ErrorCause {
1246                name: "PARSE_ERROR".to_string(),
1247                info: None,
1248            }),
1249            name: None,
1250        };
1251        let result = client.parse_rpc_error(&error);
1252        assert!(matches!(result, RpcError::ParseError(_)));
1253    }
1254
1255    #[test]
1256    fn test_parse_rpc_error_internal_error() {
1257        let client = RpcClient::new("https://example.com");
1258        let error = JsonRpcError {
1259            code: -32603,
1260            message: "Internal error".to_string(),
1261            data: None,
1262            cause: Some(ErrorCause {
1263                name: "INTERNAL_ERROR".to_string(),
1264                info: None,
1265            }),
1266            name: None,
1267        };
1268        let result = client.parse_rpc_error(&error);
1269        assert!(matches!(result, RpcError::InternalError(_)));
1270    }
1271
1272    #[test]
1273    fn test_parse_rpc_error_contract_execution_legacy() {
1274        // Legacy `query` endpoint includes contract_id and method_name in info
1275        let client = RpcClient::new("https://example.com");
1276        let error = JsonRpcError {
1277            code: -32000,
1278            message: "Contract execution failed".to_string(),
1279            data: None,
1280            cause: Some(ErrorCause {
1281                name: "CONTRACT_EXECUTION_ERROR".to_string(),
1282                info: Some(serde_json::json!({
1283                    "contract_id": "contract.near",
1284                    "method_name": "my_method"
1285                })),
1286            }),
1287            name: None,
1288        };
1289        let result = client.parse_rpc_error(&error);
1290        match result {
1291            RpcError::ContractExecution {
1292                contract_id,
1293                method_name,
1294                ..
1295            } => {
1296                assert_eq!(contract_id.as_str(), "contract.near");
1297                assert_eq!(method_name.as_deref(), Some("my_method"));
1298            }
1299            _ => panic!("Expected ContractExecution error, got {:?}", result),
1300        }
1301    }
1302
1303    #[test]
1304    fn test_parse_rpc_error_contract_execution_experimental() {
1305        // EXPERIMENTAL_call_function omits contract_id/method_name from info,
1306        // but includes vm_error and a data string
1307        let client = RpcClient::new("https://example.com");
1308        let error = JsonRpcError {
1309            code: -32000,
1310            message: "Server error".to_string(),
1311            data: Some(serde_json::json!(
1312                "Function call returned an error: MethodResolveError(MethodNotFound)"
1313            )),
1314            cause: Some(ErrorCause {
1315                name: "CONTRACT_EXECUTION_ERROR".to_string(),
1316                info: Some(serde_json::json!({
1317                    "vm_error": { "MethodResolveError": "MethodNotFound" },
1318                    "block_height": 243803767,
1319                    "block_hash": "Et7So7jtsorkYLdVMMgV8gxA3Cfaztp75Ti6TPv2A"
1320                })),
1321            }),
1322            name: Some("HANDLER_ERROR".to_string()),
1323        };
1324        let result = client.parse_rpc_error(&error);
1325        match result {
1326            RpcError::ContractExecution {
1327                contract_id,
1328                message,
1329                ..
1330            } => {
1331                // contract_id falls back to "unknown" — caller enriches it
1332                assert_eq!(contract_id.as_str(), "unknown");
1333                assert!(message.contains("MethodResolveError"));
1334            }
1335            _ => panic!("Expected ContractExecution error, got {:?}", result),
1336        }
1337    }
1338
1339    #[test]
1340    fn test_parse_rpc_error_code_does_not_exist_experimental() {
1341        // EXPERIMENTAL_call_function returns CodeDoesNotExist as CONTRACT_EXECUTION_ERROR
1342        let client = RpcClient::new("https://example.com");
1343        let error = JsonRpcError {
1344            code: -32000,
1345            message: "Server error".to_string(),
1346            data: Some(serde_json::json!(
1347                "Function call returned an error: CompilationError(CodeDoesNotExist { account_id: AccountId(\"nonexistent.testnet\") })"
1348            )),
1349            cause: Some(ErrorCause {
1350                name: "CONTRACT_EXECUTION_ERROR".to_string(),
1351                info: Some(serde_json::json!({
1352                    "vm_error": {
1353                        "CompilationError": {
1354                            "CodeDoesNotExist": {
1355                                "account_id": "nonexistent.testnet"
1356                            }
1357                        }
1358                    },
1359                    "block_height": 243803764,
1360                    "block_hash": "H33oNAtVZDJjhpncQb5LY6NxYzQLMMVLptq99mwmLmnj"
1361                })),
1362            }),
1363            name: Some("HANDLER_ERROR".to_string()),
1364        };
1365        let result = client.parse_rpc_error(&error);
1366        match result {
1367            RpcError::ContractNotDeployed(account_id) => {
1368                assert_eq!(account_id.as_str(), "nonexistent.testnet");
1369            }
1370            _ => panic!("Expected ContractNotDeployed error, got {:?}", result),
1371        }
1372    }
1373
1374    #[test]
1375    fn test_parse_rpc_error_fallback_account_not_exist() {
1376        let client = RpcClient::new("https://example.com");
1377        let error = JsonRpcError {
1378            code: -32000,
1379            message: "Error".to_string(),
1380            data: Some(serde_json::json!(
1381                "account missing.near does not exist while viewing"
1382            )),
1383            cause: None,
1384            name: None,
1385        };
1386        let result = client.parse_rpc_error(&error);
1387        assert!(matches!(result, RpcError::AccountNotFound(_)));
1388    }
1389
1390    #[test]
1391    fn test_parse_rpc_error_unknown_cause_fallback_to_generic() {
1392        let client = RpcClient::new("https://example.com");
1393        let error = JsonRpcError {
1394            code: -32000,
1395            message: "Some error".to_string(),
1396            data: Some(serde_json::json!("some data")),
1397            cause: Some(ErrorCause {
1398                name: "UNKNOWN_ERROR_TYPE".to_string(),
1399                info: None,
1400            }),
1401            name: None,
1402        };
1403        let result = client.parse_rpc_error(&error);
1404        assert!(matches!(result, RpcError::Rpc { .. }));
1405    }
1406
1407    #[test]
1408    fn test_parse_rpc_error_no_cause_fallback_to_generic() {
1409        let client = RpcClient::new("https://example.com");
1410        let error = JsonRpcError {
1411            code: -32600,
1412            message: "Invalid request".to_string(),
1413            data: None,
1414            cause: None,
1415            name: None,
1416        };
1417        let result = client.parse_rpc_error(&error);
1418        match result {
1419            RpcError::Rpc { code, message, .. } => {
1420                assert_eq!(code, -32600);
1421                assert_eq!(message, "Invalid request");
1422            }
1423            _ => panic!("Expected generic Rpc error"),
1424        }
1425    }
1426}