use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::error::RpcError;
use crate::types::{
AccessKeyListView, AccessKeyView, AccountId, AccountView, BlockReference, BlockView,
CryptoHash, GasPrice, PublicKey, SendTxResponse, SendTxWithReceiptsResponse, SignedTransaction,
StatusResponse, TxExecutionStatus, ViewFunctionResult,
};
pub struct NetworkConfig {
pub rpc_url: &'static str,
#[allow(dead_code)]
pub network_id: &'static str,
}
pub const MAINNET: NetworkConfig = NetworkConfig {
rpc_url: "https://free.rpc.fastnear.com",
network_id: "mainnet",
};
pub const TESTNET: NetworkConfig = NetworkConfig {
rpc_url: "https://test.rpc.fastnear.com",
network_id: "testnet",
};
#[derive(Clone, Debug)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay_ms: 500,
max_delay_ms: 5000,
}
}
}
#[derive(Serialize)]
struct JsonRpcRequest<'a, P: Serialize> {
jsonrpc: &'static str,
id: u64,
method: &'a str,
params: P,
}
#[derive(Deserialize)]
struct JsonRpcResponse {
#[allow(dead_code)]
jsonrpc: String,
#[allow(dead_code)]
id: u64,
result: Option<serde_json::Value>,
error: Option<JsonRpcError>,
}
#[derive(Debug, Deserialize)]
struct JsonRpcError {
code: i64,
message: String,
#[serde(default)]
data: Option<serde_json::Value>,
#[serde(default)]
cause: Option<ErrorCause>,
#[serde(default)]
#[allow(dead_code)]
name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ErrorCause {
name: String,
#[serde(default)]
info: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct CallFunctionResponse {
result: Vec<u8>,
#[serde(default)]
logs: Vec<String>,
block_height: u64,
block_hash: CryptoHash,
}
pub struct RpcClient {
url: String,
client: reqwest::Client,
retry_config: RetryConfig,
request_id: AtomicU64,
}
impl RpcClient {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
client: reqwest::Client::new(),
retry_config: RetryConfig::default(),
request_id: AtomicU64::new(0),
}
}
pub fn with_retry_config(url: impl Into<String>, retry_config: RetryConfig) -> Self {
Self {
url: url.into(),
client: reqwest::Client::new(),
retry_config,
request_id: AtomicU64::new(0),
}
}
pub fn url(&self) -> &str {
&self.url
}
#[tracing::instrument(skip(self, params), fields(rpc.method = method))]
pub async fn call<P: Serialize, R: DeserializeOwned>(
&self,
method: &str,
params: P,
) -> Result<R, RpcError> {
let total_attempts = self.retry_config.max_retries + 1;
for attempt in 0..total_attempts {
let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
let request = JsonRpcRequest {
jsonrpc: "2.0",
id: request_id,
method,
params: ¶ms,
};
match self.try_call::<R>(&request).await {
Ok(result) => return Ok(result),
Err(e) if e.is_retryable() && attempt < total_attempts - 1 => {
let delay = std::cmp::min(
self.retry_config.initial_delay_ms * 2u64.pow(attempt),
self.retry_config.max_delay_ms,
);
tracing::warn!(
attempt = attempt + 1,
max_attempts = total_attempts,
delay_ms = delay,
error = %e,
"RPC request failed, retrying"
);
tokio::time::sleep(Duration::from_millis(delay)).await;
continue;
}
Err(e) => {
tracing::error!(error = %e, "RPC request failed");
return Err(e);
}
}
}
unreachable!("all loop iterations return")
}
async fn try_call<R: DeserializeOwned>(
&self,
request: &JsonRpcRequest<'_, impl Serialize>,
) -> Result<R, RpcError> {
let response = self
.client
.post(&self.url)
.header("Content-Type", "application/json")
.json(request)
.send()
.await?;
let status = response.status();
let body = response.text().await?;
if !status.is_success() {
let retryable = is_retryable_status(status.as_u16());
return Err(RpcError::network(
format!("HTTP {}: {}", status, body),
Some(status.as_u16()),
retryable,
));
}
let rpc_response: JsonRpcResponse = serde_json::from_str(&body).map_err(RpcError::Json)?;
if let Some(error) = rpc_response.error {
return Err(self.parse_rpc_error(&error));
}
let result_value = rpc_response
.result
.ok_or_else(|| RpcError::InvalidResponse("Missing result in response".to_string()))?;
if request.method == "query" {
if let Some(error_str) = result_value.get("error").and_then(|e| e.as_str()) {
let synthetic = JsonRpcError {
code: -32600,
message: error_str.to_string(),
data: Some(serde_json::Value::String(error_str.to_string())),
cause: None,
name: None,
};
return Err(self.parse_rpc_error(&synthetic));
}
}
serde_json::from_value(result_value).map_err(RpcError::Json)
}
fn parse_rpc_error(&self, error: &JsonRpcError) -> RpcError {
if let Some(cause) = &error.cause {
let cause_name = cause.name.as_str();
let info = cause.info.as_ref();
let data = &error.data;
match cause_name {
"UNKNOWN_ACCOUNT" => {
if let Some(account_id) = info
.and_then(|i| i.get("requested_account_id"))
.and_then(|a| a.as_str())
{
if let Ok(account_id) = account_id.parse() {
return RpcError::AccountNotFound(account_id);
}
}
}
"INVALID_ACCOUNT" => {
let account_id = info
.and_then(|i| i.get("requested_account_id"))
.and_then(|a| a.as_str())
.unwrap_or("unknown");
return RpcError::InvalidAccount(account_id.to_string());
}
"UNKNOWN_ACCESS_KEY" => {
if let Some(public_key) = info
.and_then(|i| i.get("public_key"))
.and_then(|k| k.as_str())
.and_then(|k| k.parse().ok())
{
let account_id = info
.and_then(|i| i.get("requested_account_id"))
.and_then(|a| a.as_str())
.and_then(|a| a.parse().ok())
.unwrap_or_else(|| "unknown".parse().unwrap());
return RpcError::AccessKeyNotFound {
account_id,
public_key,
};
}
}
"UNKNOWN_BLOCK" => {
let block_ref = data
.as_ref()
.and_then(|d| d.as_str())
.unwrap_or(&error.message);
return RpcError::UnknownBlock(block_ref.to_string());
}
"UNKNOWN_CHUNK" => {
let chunk_ref = info
.and_then(|i| i.get("chunk_hash"))
.and_then(|c| c.as_str())
.unwrap_or(&error.message);
return RpcError::UnknownChunk(chunk_ref.to_string());
}
"UNKNOWN_EPOCH" => {
let block_ref = data
.as_ref()
.and_then(|d| d.as_str())
.unwrap_or(&error.message);
return RpcError::UnknownEpoch(block_ref.to_string());
}
"UNKNOWN_RECEIPT" => {
let receipt_id = info
.and_then(|i| i.get("receipt_id"))
.and_then(|r| r.as_str())
.unwrap_or("unknown");
return RpcError::UnknownReceipt(receipt_id.to_string());
}
"NO_CONTRACT_CODE" => {
let account_id = info
.and_then(|i| {
i.get("contract_account_id")
.or_else(|| i.get("account_id"))
.or_else(|| i.get("contract_id"))
})
.and_then(|a| a.as_str())
.unwrap_or("unknown");
if let Ok(account_id) = account_id.parse() {
return RpcError::ContractNotDeployed(account_id);
}
}
"TOO_LARGE_CONTRACT_STATE" => {
let account_id = info
.and_then(|i| i.get("account_id").or_else(|| i.get("contract_id")))
.and_then(|a| a.as_str())
.unwrap_or("unknown");
if let Ok(account_id) = account_id.parse() {
return RpcError::ContractStateTooLarge(account_id);
}
}
"CONTRACT_EXECUTION_ERROR" => {
if let Some(vm_error) = info.and_then(|i| i.get("vm_error")) {
if let Some(compilation_err) = vm_error.get("CompilationError") {
if let Some(code_not_exist) = compilation_err.get("CodeDoesNotExist") {
if let Some(account_id) = code_not_exist
.get("account_id")
.and_then(|a| a.as_str())
.and_then(|a| a.parse().ok())
{
return RpcError::ContractNotDeployed(account_id);
}
}
}
}
let contract_id = info
.and_then(|i| i.get("contract_id"))
.and_then(|c| c.as_str())
.unwrap_or("unknown");
let method_name = info
.and_then(|i| i.get("method_name"))
.and_then(|m| m.as_str())
.map(String::from);
let message = data
.as_ref()
.and_then(|d| d.as_str())
.map(|s| s.to_string())
.or_else(|| {
info.and_then(|i| i.get("vm_error")).map(|v| v.to_string())
})
.unwrap_or_else(|| error.message.clone());
if let Ok(contract_id) = contract_id.parse() {
return RpcError::ContractExecution {
contract_id,
method_name,
message,
};
}
}
"UNAVAILABLE_SHARD" => {
return RpcError::ShardUnavailable(error.message.clone());
}
"NO_SYNCED_BLOCKS" | "NOT_SYNCED_YET" => {
return RpcError::NodeNotSynced(error.message.clone());
}
"INVALID_SHARD_ID" => {
let shard_id = info
.and_then(|i| i.get("shard_id"))
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string());
return RpcError::InvalidShardId(shard_id);
}
"INVALID_TRANSACTION" => {
return RpcError::invalid_transaction(&error.message, data.clone());
}
"TIMEOUT_ERROR" => {
let tx_hash = info
.and_then(|i| i.get("transaction_hash"))
.and_then(|h| h.as_str())
.map(String::from);
return RpcError::RequestTimeout {
message: error.message.clone(),
transaction_hash: tx_hash,
};
}
"PARSE_ERROR" => {
return RpcError::ParseError(error.message.clone());
}
"INTERNAL_ERROR" => {
return RpcError::InternalError(error.message.clone());
}
_ => {}
}
}
if let Some(data) = &error.data {
if let Some(error_str) = data.as_str() {
if error_str.contains("does not exist") {
if let Some(start) = error_str.strip_prefix("account ") {
if let Some(account_str) = start.split_whitespace().next() {
if let Ok(account_id) = account_str.parse() {
return RpcError::AccountNotFound(account_id);
}
}
}
}
}
}
RpcError::Rpc {
code: error.code,
message: error.message.clone(),
data: error.data.clone(),
}
}
#[tracing::instrument(skip(self, block), fields(%account_id))]
pub async fn view_account(
&self,
account_id: &AccountId,
block: BlockReference,
) -> Result<AccountView, RpcError> {
let mut params = serde_json::json!({
"account_id": account_id.to_string(),
});
self.merge_block_reference(&mut params, &block);
self.call("EXPERIMENTAL_view_account", params).await
}
#[tracing::instrument(skip(self, block), fields(%account_id, %public_key))]
pub async fn view_access_key(
&self,
account_id: &AccountId,
public_key: &PublicKey,
block: BlockReference,
) -> Result<AccessKeyView, RpcError> {
let mut params = serde_json::json!({
"account_id": account_id.to_string(),
"public_key": public_key.to_string(),
});
self.merge_block_reference(&mut params, &block);
self.call("EXPERIMENTAL_view_access_key", params)
.await
.map_err(|e| match e {
RpcError::AccessKeyNotFound { public_key, .. } => RpcError::AccessKeyNotFound {
account_id: account_id.clone(),
public_key,
},
other => other,
})
}
#[tracing::instrument(skip(self, block), fields(%account_id))]
pub async fn view_access_key_list(
&self,
account_id: &AccountId,
block: BlockReference,
) -> Result<AccessKeyListView, RpcError> {
let mut params = serde_json::json!({
"account_id": account_id.to_string(),
});
self.merge_block_reference(&mut params, &block);
self.call("EXPERIMENTAL_view_access_key_list", params).await
}
#[tracing::instrument(skip(self, args, block), fields(contract_id = %account_id, method = method_name))]
pub async fn view_function(
&self,
account_id: &AccountId,
method_name: &str,
args: &[u8],
block: BlockReference,
) -> Result<ViewFunctionResult, RpcError> {
let mut params = serde_json::json!({
"account_id": account_id.to_string(),
"method_name": method_name,
"args_base64": STANDARD.encode(args),
});
self.merge_block_reference(&mut params, &block);
let response: CallFunctionResponse = self
.call("EXPERIMENTAL_call_function", params)
.await
.map_err(|e| match e {
RpcError::ContractExecution { message, .. } => RpcError::ContractExecution {
contract_id: account_id.clone(),
method_name: Some(method_name.to_string()),
message,
},
other => other,
})?;
Ok(ViewFunctionResult {
result: response.result,
logs: response.logs,
block_height: response.block_height,
block_hash: response.block_hash,
})
}
#[tracing::instrument(skip(self, block))]
pub async fn block(&self, block: BlockReference) -> Result<BlockView, RpcError> {
let params = block.to_rpc_params();
self.call("block", params).await
}
#[tracing::instrument(skip(self))]
pub async fn status(&self) -> Result<StatusResponse, RpcError> {
self.call("status", serde_json::json!([])).await
}
#[tracing::instrument(skip(self))]
pub async fn gas_price(&self, block_hash: Option<&CryptoHash>) -> Result<GasPrice, RpcError> {
let params = match block_hash {
Some(hash) => serde_json::json!([hash.to_string()]),
None => serde_json::json!([serde_json::Value::Null]),
};
self.call("gas_price", params).await
}
#[tracing::instrument(skip(self, signed_tx), fields(
tx_hash = tracing::field::Empty,
sender = %signed_tx.transaction.signer_id,
receiver = %signed_tx.transaction.receiver_id,
?wait_until,
))]
pub async fn send_tx(
&self,
signed_tx: &SignedTransaction,
wait_until: TxExecutionStatus,
) -> Result<SendTxResponse, RpcError> {
let tx_hash = signed_tx.get_hash();
tracing::Span::current().record("tx_hash", tracing::field::display(&tx_hash));
let params = serde_json::json!({
"signed_tx_base64": signed_tx.to_base64(),
"wait_until": wait_until.as_str(),
});
let mut response: SendTxResponse = self.call("send_tx", params).await?;
response.transaction_hash = tx_hash;
Ok(response)
}
#[tracing::instrument(skip(self), fields(%tx_hash, sender = %sender_id, ?wait_until))]
pub async fn tx_status(
&self,
tx_hash: &CryptoHash,
sender_id: &AccountId,
wait_until: TxExecutionStatus,
) -> Result<SendTxWithReceiptsResponse, RpcError> {
let params = serde_json::json!({
"tx_hash": tx_hash.to_string(),
"sender_account_id": sender_id.to_string(),
"wait_until": wait_until.as_str(),
});
self.call("EXPERIMENTAL_tx_status", params).await
}
fn merge_block_reference(&self, params: &mut serde_json::Value, block: &BlockReference) {
if let serde_json::Value::Object(block_params) = block.to_rpc_params() {
if let serde_json::Value::Object(map) = params {
map.extend(block_params);
}
}
}
pub async fn sandbox_fast_forward(&self, delta_height: u64) -> Result<(), RpcError> {
let params = serde_json::json!({
"delta_height": delta_height,
});
let _: serde_json::Value = self.call("sandbox_fast_forward", params).await?;
Ok(())
}
pub async fn sandbox_patch_state(&self, records: serde_json::Value) -> Result<(), RpcError> {
let params = serde_json::json!({
"records": records,
});
let _: serde_json::Value = self.call("sandbox_patch_state", params).await?;
let _: serde_json::Value = self
.call(
"sandbox_patch_state",
serde_json::json!({
"records": records,
}),
)
.await?;
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(())
}
}
impl Clone for RpcClient {
fn clone(&self) -> Self {
Self {
url: self.url.clone(),
client: self.client.clone(),
retry_config: self.retry_config.clone(),
request_id: AtomicU64::new(0),
}
}
}
impl std::fmt::Debug for RpcClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RpcClient")
.field("url", &self.url)
.field("retry_config", &self.retry_config)
.finish()
}
}
fn is_retryable_status(status: u16) -> bool {
status == 408 || status == 429 || status == 503 || (500..600).contains(&status)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_config_default() {
let config = RetryConfig::default();
assert_eq!(config.max_retries, 3);
assert_eq!(config.initial_delay_ms, 500);
assert_eq!(config.max_delay_ms, 5000);
}
#[test]
fn test_retry_config_clone() {
let config = RetryConfig {
max_retries: 5,
initial_delay_ms: 100,
max_delay_ms: 1000,
};
let cloned = config.clone();
assert_eq!(cloned.max_retries, 5);
assert_eq!(cloned.initial_delay_ms, 100);
assert_eq!(cloned.max_delay_ms, 1000);
}
#[test]
fn test_retry_config_debug() {
let config = RetryConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("RetryConfig"));
assert!(debug.contains("max_retries"));
}
#[test]
fn test_rpc_client_new() {
let client = RpcClient::new("https://rpc.testnet.near.org");
assert_eq!(client.url(), "https://rpc.testnet.near.org");
}
#[test]
fn test_rpc_client_with_retry_config() {
let config = RetryConfig {
max_retries: 5,
initial_delay_ms: 100,
max_delay_ms: 1000,
};
let client = RpcClient::with_retry_config("https://rpc.example.com", config);
assert_eq!(client.url(), "https://rpc.example.com");
}
#[test]
fn test_rpc_client_clone() {
let client = RpcClient::new("https://rpc.testnet.near.org");
let cloned = client.clone();
assert_eq!(cloned.url(), client.url());
}
#[test]
fn test_rpc_client_debug() {
let client = RpcClient::new("https://rpc.testnet.near.org");
let debug = format!("{:?}", client);
assert!(debug.contains("RpcClient"));
assert!(debug.contains("rpc.testnet.near.org"));
}
#[test]
fn test_is_retryable_status() {
assert!(is_retryable_status(408)); assert!(is_retryable_status(429)); assert!(is_retryable_status(500)); assert!(is_retryable_status(502)); assert!(is_retryable_status(503)); assert!(is_retryable_status(504)); assert!(is_retryable_status(599));
assert!(!is_retryable_status(200)); assert!(!is_retryable_status(201)); assert!(!is_retryable_status(400)); assert!(!is_retryable_status(401)); assert!(!is_retryable_status(403)); assert!(!is_retryable_status(404)); assert!(!is_retryable_status(422)); }
#[test]
fn test_invalid_transaction_parses_invalid_nonce() {
use crate::types::InvalidTxError;
let data = serde_json::json!({
"TxExecutionError": {
"InvalidTxError": {
"InvalidNonce": {
"tx_nonce": 5,
"ak_nonce": 10
}
}
}
});
let err = RpcError::invalid_transaction("invalid nonce", Some(data));
match err {
RpcError::InvalidTx(InvalidTxError::InvalidNonce { tx_nonce, ak_nonce }) => {
assert_eq!(tx_nonce, 5);
assert_eq!(ak_nonce, 10);
}
other => panic!("Expected InvalidTx(InvalidNonce), got: {other:?}"),
}
}
#[test]
fn test_invalid_transaction_parses_top_level_invalid_tx() {
use crate::types::InvalidTxError;
let data = serde_json::json!({
"InvalidTxError": {
"NotEnoughBalance": {
"signer_id": "alice.near",
"balance": "1000000000000000000000000",
"cost": "9000000000000000000000000"
}
}
});
let err = RpcError::invalid_transaction("insufficient balance", Some(data));
assert!(
matches!(
err,
RpcError::InvalidTx(InvalidTxError::NotEnoughBalance { .. })
),
"Expected InvalidTx(NotEnoughBalance), got: {err:?}"
);
}
#[test]
fn test_invalid_transaction_falls_back_on_unparseable() {
let data = serde_json::json!({ "SomeOtherError": {} });
let err = RpcError::invalid_transaction("some error", Some(data));
assert!(matches!(err, RpcError::InvalidTransaction { .. }));
}
#[test]
fn test_mainnet_config() {
assert!(MAINNET.rpc_url.contains("fastnear"));
assert_eq!(MAINNET.network_id, "mainnet");
}
#[test]
fn test_testnet_config() {
assert!(TESTNET.rpc_url.contains("fastnear") || TESTNET.rpc_url.contains("test"));
assert_eq!(TESTNET.network_id, "testnet");
}
#[test]
fn test_parse_rpc_error_unknown_account() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Server error".to_string(),
data: None,
cause: Some(ErrorCause {
name: "UNKNOWN_ACCOUNT".to_string(),
info: Some(serde_json::json!({
"requested_account_id": "nonexistent.near"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::AccountNotFound(_)));
}
#[test]
fn test_parse_rpc_error_unknown_access_key_legacy() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Server error".to_string(),
data: None,
cause: Some(ErrorCause {
name: "UNKNOWN_ACCESS_KEY".to_string(),
info: Some(serde_json::json!({
"requested_account_id": "alice.near",
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
match result {
RpcError::AccessKeyNotFound {
account_id,
public_key,
} => {
assert_eq!(account_id.as_str(), "alice.near");
assert!(public_key.to_string().contains("ed25519:"));
}
_ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
}
}
#[test]
fn test_parse_rpc_error_unknown_access_key_experimental() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Server error".to_string(),
data: Some(serde_json::Value::String(
"Access key for public key ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp does not exist while viewing".to_string()
)),
cause: Some(ErrorCause {
name: "UNKNOWN_ACCESS_KEY".to_string(),
info: Some(serde_json::json!({
"public_key": "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp",
"block_height": 243789592,
"block_hash": "EC5A7qc6rixfN8T4T9Gkt78H5pAsvdcjAos8Z7kFLJgi"
})),
}),
name: Some("HANDLER_ERROR".to_string()),
};
let result = client.parse_rpc_error(&error);
match result {
RpcError::AccessKeyNotFound {
account_id,
public_key,
} => {
assert_eq!(account_id.as_str(), "unknown");
assert!(public_key.to_string().contains("ed25519:"));
}
_ => panic!("Expected AccessKeyNotFound error, got {:?}", result),
}
}
#[test]
fn test_parse_rpc_error_invalid_account() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Server error".to_string(),
data: None,
cause: Some(ErrorCause {
name: "INVALID_ACCOUNT".to_string(),
info: Some(serde_json::json!({
"requested_account_id": "invalid@account"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::InvalidAccount(_)));
}
#[test]
fn test_parse_rpc_error_unknown_block() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Block not found".to_string(),
data: Some(serde_json::json!("12345")),
cause: Some(ErrorCause {
name: "UNKNOWN_BLOCK".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::UnknownBlock(_)));
}
#[test]
fn test_parse_rpc_error_unknown_chunk() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Chunk not found".to_string(),
data: None,
cause: Some(ErrorCause {
name: "UNKNOWN_CHUNK".to_string(),
info: Some(serde_json::json!({
"chunk_hash": "abc123"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::UnknownChunk(_)));
}
#[test]
fn test_parse_rpc_error_unknown_epoch() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Epoch not found".to_string(),
data: Some(serde_json::json!("epoch123")),
cause: Some(ErrorCause {
name: "UNKNOWN_EPOCH".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::UnknownEpoch(_)));
}
#[test]
fn test_parse_rpc_error_unknown_receipt() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Receipt not found".to_string(),
data: None,
cause: Some(ErrorCause {
name: "UNKNOWN_RECEIPT".to_string(),
info: Some(serde_json::json!({
"receipt_id": "receipt123"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::UnknownReceipt(_)));
}
#[test]
fn test_parse_rpc_error_no_contract_code() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "No contract code".to_string(),
data: None,
cause: Some(ErrorCause {
name: "NO_CONTRACT_CODE".to_string(),
info: Some(serde_json::json!({
"contract_account_id": "no-contract.near"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::ContractNotDeployed(_)));
}
#[test]
fn test_parse_rpc_error_too_large_contract_state() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Contract state too large".to_string(),
data: None,
cause: Some(ErrorCause {
name: "TOO_LARGE_CONTRACT_STATE".to_string(),
info: Some(serde_json::json!({
"account_id": "large-state.near"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::ContractStateTooLarge(_)));
}
#[test]
fn test_parse_rpc_error_unavailable_shard() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Shard unavailable".to_string(),
data: None,
cause: Some(ErrorCause {
name: "UNAVAILABLE_SHARD".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::ShardUnavailable(_)));
}
#[test]
fn test_parse_rpc_error_not_synced() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "No synced blocks".to_string(),
data: None,
cause: Some(ErrorCause {
name: "NO_SYNCED_BLOCKS".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::NodeNotSynced(_)));
let error = JsonRpcError {
code: -32000,
message: "Not synced yet".to_string(),
data: None,
cause: Some(ErrorCause {
name: "NOT_SYNCED_YET".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::NodeNotSynced(_)));
}
#[test]
fn test_parse_rpc_error_invalid_shard_id() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Invalid shard ID".to_string(),
data: None,
cause: Some(ErrorCause {
name: "INVALID_SHARD_ID".to_string(),
info: Some(serde_json::json!({
"shard_id": 99
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::InvalidShardId(_)));
}
#[test]
fn test_parse_rpc_error_invalid_transaction() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Invalid transaction".to_string(),
data: None,
cause: Some(ErrorCause {
name: "INVALID_TRANSACTION".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::InvalidTransaction { .. }));
}
#[test]
fn test_parse_rpc_error_timeout() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Request timed out".to_string(),
data: None,
cause: Some(ErrorCause {
name: "TIMEOUT_ERROR".to_string(),
info: Some(serde_json::json!({
"transaction_hash": "tx123"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::RequestTimeout { .. }));
}
#[test]
fn test_parse_rpc_error_parse_error() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32700,
message: "Parse error".to_string(),
data: None,
cause: Some(ErrorCause {
name: "PARSE_ERROR".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::ParseError(_)));
}
#[test]
fn test_parse_rpc_error_internal_error() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32603,
message: "Internal error".to_string(),
data: None,
cause: Some(ErrorCause {
name: "INTERNAL_ERROR".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::InternalError(_)));
}
#[test]
fn test_parse_rpc_error_contract_execution_legacy() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Contract execution failed".to_string(),
data: None,
cause: Some(ErrorCause {
name: "CONTRACT_EXECUTION_ERROR".to_string(),
info: Some(serde_json::json!({
"contract_id": "contract.near",
"method_name": "my_method"
})),
}),
name: None,
};
let result = client.parse_rpc_error(&error);
match result {
RpcError::ContractExecution {
contract_id,
method_name,
..
} => {
assert_eq!(contract_id.as_str(), "contract.near");
assert_eq!(method_name.as_deref(), Some("my_method"));
}
_ => panic!("Expected ContractExecution error, got {:?}", result),
}
}
#[test]
fn test_parse_rpc_error_contract_execution_experimental() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Server error".to_string(),
data: Some(serde_json::json!(
"Function call returned an error: MethodResolveError(MethodNotFound)"
)),
cause: Some(ErrorCause {
name: "CONTRACT_EXECUTION_ERROR".to_string(),
info: Some(serde_json::json!({
"vm_error": { "MethodResolveError": "MethodNotFound" },
"block_height": 243803767,
"block_hash": "Et7So7jtsorkYLdVMMgV8gxA3Cfaztp75Ti6TPv2A"
})),
}),
name: Some("HANDLER_ERROR".to_string()),
};
let result = client.parse_rpc_error(&error);
match result {
RpcError::ContractExecution {
contract_id,
message,
..
} => {
assert_eq!(contract_id.as_str(), "unknown");
assert!(message.contains("MethodResolveError"));
}
_ => panic!("Expected ContractExecution error, got {:?}", result),
}
}
#[test]
fn test_parse_rpc_error_code_does_not_exist_experimental() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Server error".to_string(),
data: Some(serde_json::json!(
"Function call returned an error: CompilationError(CodeDoesNotExist { account_id: AccountId(\"nonexistent.testnet\") })"
)),
cause: Some(ErrorCause {
name: "CONTRACT_EXECUTION_ERROR".to_string(),
info: Some(serde_json::json!({
"vm_error": {
"CompilationError": {
"CodeDoesNotExist": {
"account_id": "nonexistent.testnet"
}
}
},
"block_height": 243803764,
"block_hash": "H33oNAtVZDJjhpncQb5LY6NxYzQLMMVLptq99mwmLmnj"
})),
}),
name: Some("HANDLER_ERROR".to_string()),
};
let result = client.parse_rpc_error(&error);
match result {
RpcError::ContractNotDeployed(account_id) => {
assert_eq!(account_id.as_str(), "nonexistent.testnet");
}
_ => panic!("Expected ContractNotDeployed error, got {:?}", result),
}
}
#[test]
fn test_parse_rpc_error_fallback_account_not_exist() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Error".to_string(),
data: Some(serde_json::json!(
"account missing.near does not exist while viewing"
)),
cause: None,
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::AccountNotFound(_)));
}
#[test]
fn test_parse_rpc_error_unknown_cause_fallback_to_generic() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32000,
message: "Some error".to_string(),
data: Some(serde_json::json!("some data")),
cause: Some(ErrorCause {
name: "UNKNOWN_ERROR_TYPE".to_string(),
info: None,
}),
name: None,
};
let result = client.parse_rpc_error(&error);
assert!(matches!(result, RpcError::Rpc { .. }));
}
#[test]
fn test_parse_rpc_error_no_cause_fallback_to_generic() {
let client = RpcClient::new("https://example.com");
let error = JsonRpcError {
code: -32600,
message: "Invalid request".to_string(),
data: None,
cause: None,
name: None,
};
let result = client.parse_rpc_error(&error);
match result {
RpcError::Rpc { code, message, .. } => {
assert_eq!(code, -32600);
assert_eq!(message, "Invalid request");
}
_ => panic!("Expected generic Rpc error"),
}
}
}