diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e544b0b..6aecddaa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next release +- fix(rpc): handle batched requests in middleware - chore: padded devnet address display with 64 chars - feat(script): added more capabilities to the launcher script - fix(fgw): sync from other nodes and block signature diff --git a/crates/node/src/service/rpc/middleware.rs b/crates/node/src/service/rpc/middleware.rs index 3c072eddc..e1aeef3b8 100644 --- a/crates/node/src/service/rpc/middleware.rs +++ b/crates/node/src/service/rpc/middleware.rs @@ -170,8 +170,6 @@ enum VersionMiddlewareError { BodyReadError(#[from] hyper::Error), #[error("Failed to parse JSON: {0}")] JsonParseError(#[from] serde_json::Error), - #[error("Invalid request format")] - InvalidRequestFormat, #[error("Invalid URL format")] InvalidUrlFormat, #[error("Invalid version specified")] @@ -236,9 +234,6 @@ where VersionMiddlewareError::InvalidVersion => { ErrorObject::owned(-32600, "Invalid RPC version specified", None::<()>) } - VersionMiddlewareError::InvalidRequestFormat => { - ErrorObject::owned(-32600, "Invalid JSON-RPC request format", None::<()>) - } VersionMiddlewareError::UnsupportedVersion => { ErrorObject::owned(-32601, "Unsupported RPC version specified", None::<()>) } @@ -248,7 +243,7 @@ where let body = json!({ "jsonrpc": "2.0", "error": error, - "id": 0 + "id": null }) .to_string(); @@ -267,18 +262,29 @@ async fn add_rpc_version_to_method(req: &mut hyper::Request) -> Result<(), let version = RpcVersion::from_request_path(&path)?; let whole_body = hyper::body::to_bytes(req.body_mut()).await?; - let mut json: Value = serde_json::from_slice(&whole_body)?; - - if let Some(method) = json.get_mut("method").as_deref().and_then(Value::as_str) { - let new_method = format!("starknet_{}_{}", version.name(), method.strip_prefix("starknet_").unwrap_or(method)); + let json: Value = serde_json::from_slice(&whole_body)?; - json["method"] = Value::String(new_method); + // in case of batched requests, the request is an array of JSON-RPC requests + let mut batched_request = false; + let mut items = if let Value::Array(items) = json { + batched_request = true; + items } else { - return Err(VersionMiddlewareError::InvalidRequestFormat); + vec![json] + }; + + for item in items.iter_mut() { + if let Some(method) = item.get_mut("method").as_deref().and_then(Value::as_str) { + let new_method = + format!("starknet_{}_{}", version.name(), method.strip_prefix("starknet_").unwrap_or(method)); + + item["method"] = Value::String(new_method); + } + // we don't need to throw an error here, the request will be rejected later if the method is not supported } - let new_body = Body::from(serde_json::to_vec(&json)?); - *req.body_mut() = new_body; + let response = if batched_request { serde_json::to_vec(&items)? } else { serde_json::to_vec(&items[0])? }; + *req.body_mut() = Body::from(response); Ok(()) } diff --git a/crates/tests/src/rpc/read.rs b/crates/tests/src/rpc/read.rs index 79fa8233b..18480e0d2 100644 --- a/crates/tests/src/rpc/read.rs +++ b/crates/tests/src/rpc/read.rs @@ -138,6 +138,88 @@ mod test_rpc_read_calls { assert_eq!(result, 1); } + /// Fetches the latest block hash and number. + /// + /// Example curl command: + /// + /// ```bash + /// curl --location 'https://free-rpc.nethermind.io/sepolia-juno/' \ + /// --header 'Content-Type: application/json' \ + /// --data '[ + /// { + /// "jsonrpc": "2.0", + /// "method": "starknet_blockHashAndNumber", + /// "params": {}, + /// "id": 0 + /// }, + /// { + /// "jsonrpc": "2.0", + /// "method": "starknet_getBlockTransactionCount", + /// "params": { + /// "block_id": { + /// "block_number": 2 + /// } + /// }, + /// "id": 1 + /// } + /// ]' + /// ``` + #[rstest] + #[tokio::test] + async fn test_batched_requests_work() { + let madara = get_shared_state().await; + + // use reqwest to send a batch request to the madara rpc. + // TODO: use a jsonrpc client instead of reqwest when we move + // to starknet-providers 0.12.0 + let client = reqwest::Client::new(); + let res = client + .post(madara.rpc_url.clone()) + .json(&[ + serde_json::json!({ + "jsonrpc": "2.0", + "method": "starknet_blockHashAndNumber", + "params": {}, + "id": 0 + }), + serde_json::json!({ + "jsonrpc": "2.0", + "method": "starknet_getBlockTransactionCount", + "params": { + "block_id": { + "block_number": 2 + } + }, + "id": 1 + }), + ]) + .send() + .await + .unwrap(); + + let result = res.json::().await.unwrap(); + + assert_eq!( + result[0], + serde_json::json!({ + "jsonrpc": "2.0", + "result": { + "block_hash": "0x4177d1ba942a4ab94f86a476c06f0f9e02363ad410cdf177c54064788c9bcb5", + "block_number": 19 + }, + "id": 0 + }) + ); + assert_eq!( + result[1], + serde_json::json!({ + "jsonrpc": "2.0", + "result": 1, + "id": 1 + }) + ); + } + /// Fetches a block with its transactions and receipts. /// /// Example curl command: