Skip to content

Commit 69d53a6

Browse files
authored
fix: handle legacy nearcore error responses for query methods (#17)
1 parent 5029064 commit 69d53a6

File tree

2 files changed

+139
-5
lines changed

2 files changed

+139
-5
lines changed

src/client.rs

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Async JSON-RPC client for NEAR Protocol.
22
3-
use crate::errors::RpcError;
3+
use crate::errors::{LegacyQueryError, RpcError};
44
use crate::types::*;
55
use reqwest::Client;
66
use serde::{Deserialize, Serialize};
@@ -42,6 +42,13 @@ pub enum Error {
4242
Rpc(#[from] RpcError),
4343
#[error("JSON error: {0}")]
4444
Json(#[from] serde_json::Error),
45+
/// Legacy error from nearcore's backward-compatible query handling.
46+
///
47+
/// Returned when nearcore sends errors like `UnknownAccessKey` or
48+
/// `ContractExecutionError` as fake success responses inside the `"result"`
49+
/// field instead of as proper JSON-RPC errors.
50+
#[error("Legacy RPC query error: {0}")]
51+
LegacyQueryResult(LegacyQueryError),
4552
}
4653

4754
/// Result type alias for client operations.
@@ -114,7 +121,7 @@ impl NearRpcClient {
114121
params,
115122
};
116123

117-
let response: RpcResponse<R> = self
124+
let raw: serde_json::Value = self
118125
.client
119126
.post(&self.url)
120127
.json(&request)
@@ -123,9 +130,27 @@ impl NearRpcClient {
123130
.json()
124131
.await?;
125132

126-
match response.result {
127-
RpcResult::Ok { result } => Ok(result),
128-
RpcResult::Err { error } => Err(Error::Rpc(error)),
133+
match serde_json::from_value::<RpcResponse<R>>(raw.clone()) {
134+
Ok(response) => match response.result {
135+
RpcResult::Ok { result } => Ok(result),
136+
RpcResult::Err { error } => Err(Error::Rpc(error)),
137+
},
138+
Err(deser_err) => {
139+
// Nearcore returns UnknownAccessKey and ContractExecutionError as
140+
// fake success responses for backward compatibility:
141+
// {"result": {"error": "...", "logs": [], "block_height": ..., "block_hash": "..."}}
142+
// These fail normal deserialization because they sit in the "result"
143+
// field but don't match any valid response type.
144+
if let Some(legacy) = raw.get("result").and_then(|result| {
145+
result.get("error").and_then(|_| {
146+
serde_json::from_value::<LegacyQueryError>(result.clone()).ok()
147+
})
148+
}) {
149+
Err(Error::LegacyQueryResult(legacy))
150+
} else {
151+
Err(Error::Json(deser_err))
152+
}
153+
}
129154
}
130155
}
131156

@@ -358,6 +383,76 @@ impl NearRpcClient {
358383
mod tests {
359384
use super::*;
360385

386+
#[test]
387+
fn test_legacy_query_error_deserialization() {
388+
// Simulates the legacy nearcore response for UnknownAccessKey
389+
let legacy_result = serde_json::json!({
390+
"error": "access key ed25519:5BGSaf6YjVm7565VzWQHNxoyEjwr3jUpRJSGjREvU9dB does not exist while viewing",
391+
"logs": [],
392+
"block_height": 12345,
393+
"block_hash": "9FMnGHBEfJ3PoKzSaq7EwCotanD3RLGA9UFqEjB3hrN1"
394+
});
395+
396+
let legacy: LegacyQueryError =
397+
serde_json::from_value(legacy_result).expect("should parse legacy error");
398+
assert!(legacy.error.contains("does not exist while viewing"));
399+
assert_eq!(legacy.block_height, Some(12345));
400+
assert_eq!(
401+
legacy.block_hash.map(|h| h.0),
402+
Some("9FMnGHBEfJ3PoKzSaq7EwCotanD3RLGA9UFqEjB3hrN1".to_string())
403+
);
404+
assert!(legacy.logs.is_empty());
405+
}
406+
407+
#[test]
408+
fn test_legacy_contract_execution_error_deserialization() {
409+
// Simulates the legacy nearcore response for ContractExecutionError
410+
let legacy_result = serde_json::json!({
411+
"error": "wasm execution failed with error: FunctionCallError(HostError(GasExceeded))",
412+
"logs": ["log1", "log2"],
413+
"block_height": 99999,
414+
"block_hash": "4reLvkAWfqk5fsqio1KLudk46cqRz9erQdaHkWZKMJDZ"
415+
});
416+
417+
let legacy: LegacyQueryError =
418+
serde_json::from_value(legacy_result).expect("should parse legacy error");
419+
assert!(legacy.error.contains("wasm execution failed"));
420+
assert_eq!(legacy.logs, vec!["log1", "log2"]);
421+
assert_eq!(legacy.block_height, Some(99999));
422+
}
423+
424+
#[test]
425+
fn test_rpc_result_falls_through_to_legacy_check() {
426+
// A full JSON-RPC response with the legacy error shape in "result"
427+
let raw = serde_json::json!({
428+
"jsonrpc": "2.0",
429+
"id": 1,
430+
"result": {
431+
"error": "access key ed25519:5BGSaf6YjVm7565VzWQHNxoyEjwr3jUpRJSGjREvU9dB does not exist while viewing",
432+
"logs": [],
433+
"block_height": 12345,
434+
"block_hash": "9FMnGHBEfJ3PoKzSaq7EwCotanD3RLGA9UFqEjB3hrN1"
435+
}
436+
});
437+
438+
// Normal deserialization as RpcResponse<RpcViewAccessKeyResponse> should fail
439+
let normal_result =
440+
serde_json::from_value::<RpcResponse<RpcViewAccessKeyResponse>>(raw.clone());
441+
assert!(normal_result.is_err());
442+
443+
// But the legacy fallback should parse successfully
444+
let legacy = raw
445+
.get("result")
446+
.and_then(|r| serde_json::from_value::<LegacyQueryError>(r.clone()).ok());
447+
assert!(legacy.is_some());
448+
assert!(
449+
legacy
450+
.unwrap()
451+
.error
452+
.contains("does not exist while viewing")
453+
);
454+
}
455+
361456
#[test]
362457
fn test_client_creation() {
363458
let client = NearRpcClient::mainnet();

src/errors.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,30 @@ use serde::{Deserialize, Serialize};
1111

1212
use crate::types::{AccountId, CryptoHash, PublicKey, ShardId};
1313

14+
/// Legacy error response from nearcore's backward-compatible query handling.
15+
///
16+
/// Nearcore returns `UnknownAccessKey` and `ContractExecutionError` as fake success
17+
/// responses (inside the `"result"` field) instead of proper JSON-RPC errors, for
18+
/// backward compatibility. This type captures that legacy shape so callers get a
19+
/// meaningful error instead of a confusing deserialization failure.
20+
///
21+
/// See: <https://github.com/near/nearcore/blob/master/chain/jsonrpc/src/lib.rs>
22+
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
23+
#[error("{error}")]
24+
pub struct LegacyQueryError {
25+
/// The error message, e.g. "access key ed25519:... does not exist while viewing"
26+
pub error: String,
27+
/// Logs from contract execution (empty for access key errors)
28+
#[serde(default)]
29+
pub logs: Vec<String>,
30+
/// Block height at which the query was executed
31+
#[serde(default)]
32+
pub block_height: Option<u64>,
33+
/// Block hash at which the query was executed
34+
#[serde(default)]
35+
pub block_hash: Option<CryptoHash>,
36+
}
37+
1438
/// JSON-RPC error returned by the NEAR node.
1539
///
1640
/// NEAR's RPC extends the standard JSON-RPC error with `name` and `cause` fields
@@ -439,6 +463,21 @@ mod tests {
439463
}
440464
}
441465

466+
#[test]
467+
fn deserialize_legacy_query_error() {
468+
let json = serde_json::json!({
469+
"error": "access key ed25519:abc does not exist while viewing",
470+
"logs": [],
471+
"block_height": 100,
472+
"block_hash": "9FMnGHBEfJ3PoKzSaq7EwCotanD3RLGA9UFqEjB3hrN1"
473+
});
474+
475+
let err: super::LegacyQueryError = serde_json::from_value(json).unwrap();
476+
assert!(err.error.contains("does not exist"));
477+
assert_eq!(err.block_height, Some(100));
478+
assert!(err.logs.is_empty());
479+
}
480+
442481
#[test]
443482
fn deserialize_transaction_invalid() {
444483
let json = serde_json::json!({

0 commit comments

Comments
 (0)