11//! Async JSON-RPC client for NEAR Protocol.
22
3- use crate :: errors:: RpcError ;
3+ use crate :: errors:: { LegacyQueryError , RpcError } ;
44use crate :: types:: * ;
55use reqwest:: Client ;
66use 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 {
358383mod 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 ( ) ;
0 commit comments