Skip to main content

ethrex_rpc/
utils.rs

1//! Utility types and error handling for JSON-RPC.
2//!
3//! This module provides common types used across all RPC handlers:
4//! - [`RpcErr`]: Error type for RPC failures with proper JSON-RPC error codes
5//! - [`RpcRequest`]: Parsed JSON-RPC request
6//! - [`RpcNamespace`]: RPC method namespace (eth, engine, debug, etc.)
7//! - Response types for success and error cases
8
9use ethrex_common::U256;
10use ethrex_storage::error::StoreError;
11use ethrex_vm::EvmError;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::{authentication::AuthenticationError, clients::EthClientError};
16use ethrex_blockchain::error::MempoolError;
17
18/// Error type for JSON-RPC method failures.
19///
20/// Each variant maps to a specific JSON-RPC error code when serialized:
21/// - `-32601`: Method not found
22/// - `-32602`: Invalid params
23/// - `-32603`: Internal error
24/// - `-32000`: Generic server error
25/// - `-38001` to `-38006`: Engine API specific errors
26/// - `3`: Execution reverted/halted
27#[derive(Debug, thiserror::Error)]
28pub enum RpcErr {
29    #[error("Method not found: {0}")]
30    MethodNotFound(String),
31    #[error("Wrong parameter: {0}")]
32    WrongParam(String),
33    #[error("Invalid params: {0}")]
34    BadParams(String),
35    #[error("Missing parameter: {0}")]
36    MissingParam(String),
37    #[error("Too large request")]
38    TooLargeRequest,
39    #[error("Bad hex format: {0}")]
40    BadHexFormat(u64),
41    #[error("Unsupported fork: {0}")]
42    UnsupportedFork(String),
43    #[error("Internal Error: {0}")]
44    Internal(String),
45    #[error("Vm execution error: {0}")]
46    Vm(String),
47    #[error("execution reverted: data={data}")]
48    Revert { data: String },
49    #[error("execution halted: reason={reason}, gas_used={gas_used}")]
50    Halt { reason: String, gas_used: u64 },
51    #[error("Authentication error: {0:?}")]
52    AuthenticationError(AuthenticationError),
53    /// JSON-RPC 2.0 ยง5.1: the JSON sent is not a valid Request object. Used
54    /// for malformed bodies on the auth port (e.g. empty batches) where we
55    /// also drop the request id (per spec, id MUST be null when it cannot
56    /// be detected).
57    #[error("Invalid Request: {0}")]
58    InvalidRequest(String),
59    #[error("Invalid forkchoice state: {0}")]
60    InvalidForkChoiceState(String),
61    #[error("Invalid payload attributes: {0}")]
62    InvalidPayloadAttributes(String),
63    #[error("Too deep reorg: {0}")]
64    TooDeepReorg(String),
65    #[error("Unknown payload: {0}")]
66    UnknownPayload(String),
67    // EIP-8025 proof errors (-39001 .. -39004)
68    #[error("Invalid proof format: {0}")]
69    InvalidProofFormat(String),
70    #[error("Invalid header format: {0}")]
71    InvalidHeaderFormat(String),
72    #[error("Invalid payload: {0}")]
73    InvalidPayload(String),
74    #[error("Proof generation unavailable: {0}")]
75    ProofGenerationUnavailable(String),
76}
77
78impl From<RpcErr> for RpcErrorMetadata {
79    fn from(value: RpcErr) -> Self {
80        match value {
81            RpcErr::MethodNotFound(bad_method) => RpcErrorMetadata {
82                code: -32601,
83                data: None,
84                message: format!("Method not found: {bad_method}"),
85            },
86            RpcErr::WrongParam(field) => RpcErrorMetadata {
87                code: -32602,
88                data: None,
89                message: format!("Field '{field}' is incorrect or has an unknown format"),
90            },
91            RpcErr::BadParams(context) => RpcErrorMetadata {
92                code: -32000,
93                data: None,
94                message: format!("Invalid params: {context}"),
95            },
96            RpcErr::InvalidRequest(context) => RpcErrorMetadata {
97                code: -32600,
98                data: None,
99                message: format!("Invalid Request: {context}"),
100            },
101            RpcErr::MissingParam(parameter_name) => RpcErrorMetadata {
102                code: -32000,
103                data: None,
104                message: format!("Expected parameter: {parameter_name} is missing"),
105            },
106            RpcErr::TooLargeRequest => RpcErrorMetadata {
107                code: -38004,
108                data: None,
109                message: "Too large request".to_string(),
110            },
111            RpcErr::UnsupportedFork(context) => RpcErrorMetadata {
112                code: -38005,
113                data: None,
114                message: format!("Unsupported fork: {context}"),
115            },
116            RpcErr::BadHexFormat(arg_number) => RpcErrorMetadata {
117                code: -32602,
118                data: None,
119                message: format!("invalid argument {arg_number} : hex string without 0x prefix"),
120            },
121            RpcErr::Internal(context) => RpcErrorMetadata {
122                code: -32603,
123                data: None,
124                message: format!("Internal Error: {context}"),
125            },
126            RpcErr::Vm(context) => RpcErrorMetadata {
127                code: -32015,
128                data: None,
129                message: format!("Vm execution error: {context}"),
130            },
131            RpcErr::Revert { data } => RpcErrorMetadata {
132                // This code (3) was hand-picked to match hive tests.
133                // Could not find proper documentation about it.
134                code: 3,
135                data: Some(data.clone()),
136                message: format!(
137                    "execution reverted: {}",
138                    get_message_from_revert_data(&data).unwrap_or_else(|err| format!(
139                        "tried to decode error from abi but failed: {err}"
140                    ))
141                ),
142            },
143            RpcErr::Halt { reason, gas_used } => RpcErrorMetadata {
144                // Just copy the `Revert` error code.
145                // Haven't found an example of this one yet.
146                code: 3,
147                data: None,
148                message: format!("execution halted: reason={reason}, gas_used={gas_used}"),
149            },
150            RpcErr::AuthenticationError(auth_error) => match auth_error {
151                AuthenticationError::InvalidIssuedAtClaim => RpcErrorMetadata {
152                    code: -32000,
153                    data: None,
154                    message: "Auth failed: Invalid iat claim".to_string(),
155                },
156                AuthenticationError::TokenDecodingError => RpcErrorMetadata {
157                    code: -32000,
158                    data: None,
159                    message: "Auth failed: Invalid or missing token".to_string(),
160                },
161                AuthenticationError::MissingAuthentication => RpcErrorMetadata {
162                    code: -32000,
163                    data: None,
164                    message: "Auth failed: Missing authentication header".to_string(),
165                },
166            },
167            RpcErr::InvalidForkChoiceState(data) => RpcErrorMetadata {
168                code: -38002,
169                data: Some(data),
170                message: "Invalid forkchoice state".to_string(),
171            },
172            RpcErr::InvalidPayloadAttributes(data) => RpcErrorMetadata {
173                code: -38003,
174                data: Some(data),
175                message: "Invalid payload attributes".to_string(),
176            },
177            RpcErr::TooDeepReorg(data) => RpcErrorMetadata {
178                code: -38006,
179                data: Some(data),
180                message: "Too deep reorg".to_string(),
181            },
182            RpcErr::UnknownPayload(context) => RpcErrorMetadata {
183                code: -38001,
184                data: None,
185                message: format!("Unknown payload: {context}"),
186            },
187            // EIP-8025 proof error codes
188            RpcErr::InvalidProofFormat(context) => RpcErrorMetadata {
189                code: -39001,
190                data: None,
191                message: format!("Invalid proof format: {context}"),
192            },
193            RpcErr::InvalidHeaderFormat(context) => RpcErrorMetadata {
194                code: -39002,
195                data: None,
196                message: format!("Invalid header format: {context}"),
197            },
198            RpcErr::InvalidPayload(context) => RpcErrorMetadata {
199                code: -39003,
200                data: None,
201                message: format!("Invalid payload: {context}"),
202            },
203            RpcErr::ProofGenerationUnavailable(context) => RpcErrorMetadata {
204                code: -39004,
205                data: None,
206                message: format!("Proof generation unavailable: {context}"),
207            },
208        }
209    }
210}
211
212impl From<serde_json::Error> for RpcErr {
213    fn from(error: serde_json::Error) -> Self {
214        Self::BadParams(error.to_string())
215    }
216}
217
218// TODO: Actually return different errors for each case
219// here we are returning a BadParams error
220impl From<MempoolError> for RpcErr {
221    fn from(err: MempoolError) -> Self {
222        match err {
223            MempoolError::StoreError(err) => Self::Internal(err.to_string()),
224            other_err => Self::BadParams(other_err.to_string()),
225        }
226    }
227}
228
229impl From<ethrex_crypto::CryptoError> for RpcErr {
230    fn from(err: ethrex_crypto::CryptoError) -> Self {
231        Self::Internal(format!("Cryptography error: {err}"))
232    }
233}
234
235/// JSON-RPC method namespace.
236///
237/// Methods are namespaced by prefix (e.g., `eth_getBalance` is in the `Eth` namespace).
238/// Different namespaces may have different authentication requirements.
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
240pub enum RpcNamespace {
241    /// Engine API methods for consensus client communication (requires JWT auth).
242    Engine,
243    /// Standard Ethereum methods for querying state and sending transactions.
244    Eth,
245    /// Node administration methods.
246    Admin,
247    /// Debugging and tracing methods.
248    Debug,
249    /// Web3 utility methods.
250    Web3,
251    /// Network information methods.
252    Net,
253    /// Transaction pool inspection methods (exposed as `txpool_*`).
254    Mempool,
255}
256
257impl RpcNamespace {
258    /// Parses a namespace name from its CLI/method-prefix form.
259    pub fn from_prefix(s: &str) -> Option<Self> {
260        match s {
261            "engine" => Some(RpcNamespace::Engine),
262            "eth" => Some(RpcNamespace::Eth),
263            "admin" => Some(RpcNamespace::Admin),
264            "debug" => Some(RpcNamespace::Debug),
265            "web3" => Some(RpcNamespace::Web3),
266            "net" => Some(RpcNamespace::Net),
267            "txpool" => Some(RpcNamespace::Mempool),
268            _ => None,
269        }
270    }
271}
272
273/// JSON-RPC request identifier.
274///
275/// Per the JSON-RPC 2.0 spec, request IDs can be either numbers or strings.
276/// The same ID must be returned in the response.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(untagged)]
279pub enum RpcRequestId {
280    /// Numeric request ID.
281    Number(u64),
282    /// String request ID.
283    String(String),
284}
285
286/// A parsed JSON-RPC 2.0 request.
287///
288/// # Example
289///
290/// ```json
291/// {
292///     "jsonrpc": "2.0",
293///     "id": 1,
294///     "method": "eth_getBalance",
295///     "params": ["0x...", "latest"]
296/// }
297/// ```
298#[derive(Serialize, Deserialize, Debug, Clone)]
299pub struct RpcRequest {
300    /// Request identifier, echoed back in the response.
301    pub id: RpcRequestId,
302    /// JSON-RPC version, must be "2.0".
303    pub jsonrpc: String,
304    /// Method name (e.g., "eth_getBalance").
305    pub method: String,
306    /// Optional array of method parameters.
307    pub params: Option<Vec<Value>>,
308}
309
310impl RpcRequest {
311    pub fn namespace(&self) -> Result<RpcNamespace, RpcErr> {
312        let mut parts = self.method.split('_');
313        let Some(namespace) = parts.next() else {
314            return Err(RpcErr::MethodNotFound(self.method.clone()));
315        };
316        resolve_namespace(namespace, self.method.clone())
317    }
318
319    pub fn new(method: &str, params: Option<Vec<Value>>) -> Self {
320        RpcRequest {
321            id: RpcRequestId::Number(1),
322            jsonrpc: "2.0".to_string(),
323            method: method.to_string(),
324            params,
325        }
326    }
327}
328
329pub fn resolve_namespace(maybe_namespace: &str, method: String) -> Result<RpcNamespace, RpcErr> {
330    RpcNamespace::from_prefix(maybe_namespace).ok_or(RpcErr::MethodNotFound(method))
331}
332
333impl Default for RpcRequest {
334    fn default() -> Self {
335        RpcRequest {
336            id: RpcRequestId::Number(1),
337            jsonrpc: "2.0".to_string(),
338            method: "".to_string(),
339            params: None,
340        }
341    }
342}
343
344/// Error metadata for JSON-RPC error responses.
345///
346/// Contains the error code, message, and optional additional data.
347/// Error codes follow the JSON-RPC 2.0 and Ethereum conventions.
348#[derive(Serialize, Deserialize, Debug, Clone)]
349pub struct RpcErrorMetadata {
350    /// Numeric error code (negative for standard errors).
351    pub code: i32,
352    /// Optional additional error data (e.g., revert reason).
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub data: Option<String>,
355    /// Human-readable error message.
356    pub message: String,
357}
358
359/// A successful JSON-RPC 2.0 response.
360#[derive(Serialize, Deserialize, Debug)]
361pub struct RpcSuccessResponse {
362    /// Request identifier from the original request.
363    pub id: RpcRequestId,
364    /// JSON-RPC version, always "2.0".
365    pub jsonrpc: String,
366    /// The result value returned by the method.
367    pub result: Value,
368}
369
370/// An error JSON-RPC 2.0 response.
371#[derive(Serialize, Deserialize, Debug)]
372pub struct RpcErrorResponse {
373    /// Request identifier from the original request.
374    pub id: RpcRequestId,
375    /// JSON-RPC version, always "2.0".
376    pub jsonrpc: String,
377    /// Error details including code and message.
378    pub error: RpcErrorMetadata,
379}
380
381/// A JSON-RPC 2.0 response, either success or error.
382#[derive(Deserialize, Debug)]
383#[serde(untagged)]
384pub enum RpcResponse {
385    Success(RpcSuccessResponse),
386    Error(RpcErrorResponse),
387}
388
389/// Failure to read from DB will always constitute an internal error
390impl From<StoreError> for RpcErr {
391    fn from(value: StoreError) -> Self {
392        RpcErr::Internal(value.to_string())
393    }
394}
395
396impl From<EvmError> for RpcErr {
397    fn from(value: EvmError) -> Self {
398        RpcErr::Vm(value.to_string())
399    }
400}
401
402pub fn get_message_from_revert_data(data: &str) -> Result<String, EthClientError> {
403    if data == "0x" {
404        Ok("Execution reverted without a reason string.".to_owned())
405    // 4 byte function signature 0xXXXXXXXX
406    } else if data.len() == 10 {
407        Ok(data.to_owned())
408    } else {
409        let abi_decoded_error_data =
410            hex::decode(data.strip_prefix("0x").ok_or(EthClientError::Custom(
411                "Failed to strip_prefix when getting message from revert data".to_owned(),
412            ))?)
413            .map_err(|_| {
414                EthClientError::Custom(
415                    "Failed to hex::decode when getting message from revert data".to_owned(),
416                )
417            })?;
418        let string_length = U256::from_big_endian(abi_decoded_error_data.get(36..68).ok_or(
419            EthClientError::Custom(
420                "Failed to slice index abi_decoded_error_data when getting message from revert data".to_owned(),
421            ),
422        )?);
423        let string_len = usize::try_from(string_length).map_err(|_| {
424            EthClientError::Custom(
425                "Failed to convert string_length to usize when getting message from revert data"
426                    .to_owned(),
427            )
428        })?;
429        let string_data = abi_decoded_error_data
430            .get(68..68 + string_len)
431            .ok_or(EthClientError::Custom(
432            "Failed to slice index abi_decoded_error_data when getting message from revert data"
433                .to_owned(),
434        ))?;
435        String::from_utf8(string_data.to_vec()).map_err(|_| {
436            EthClientError::Custom(
437                "Failed to String::from_utf8 when getting message from revert data".to_owned(),
438            )
439        })
440    }
441}
442
443pub fn parse_json_hex(hex: &serde_json::Value) -> Result<u64, String> {
444    if let Value::String(maybe_hex) = hex {
445        let trimmed = maybe_hex.trim_start_matches("0x");
446        let maybe_parsed = u64::from_str_radix(trimmed, 16);
447        maybe_parsed.map_err(|_| format!("Could not parse given hex {maybe_hex}"))
448    } else {
449        Err(format!("Could not parse given hex {hex}"))
450    }
451}