From 1c5d89e92b47d988970caae5349476f259d7391c Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 5 Feb 2026 15:19:10 +0400 Subject: [PATCH 1/7] chain/ethereum: add json_patch module for unified type field patching Add a dedicated module for JSON patching utilities that handle missing `type` fields in Ethereum transactions and receipts. This consolidates patching logic that will be used by both the HTTP transport layer (for RPC responses) and cache deserialization (for stored blocks). The module provides: - patch_type_field: Adds "type": "0x0" to JSON objects missing the field - patch_block_transactions: Patches all transactions in a block - patch_receipts: Patches single receipts or arrays of receipts --- chain/ethereum/src/json_patch.rs | 123 +++++++++++++++++++++++++++++++ chain/ethereum/src/lib.rs | 1 + 2 files changed, 124 insertions(+) create mode 100644 chain/ethereum/src/json_patch.rs diff --git a/chain/ethereum/src/json_patch.rs b/chain/ethereum/src/json_patch.rs new file mode 100644 index 00000000000..d6a46f79ceb --- /dev/null +++ b/chain/ethereum/src/json_patch.rs @@ -0,0 +1,123 @@ +//! JSON patching utilities for Ethereum blocks and receipts. +//! +//! Some cached blocks are missing the transaction `type` field because +//! graph-node's rust-web3 fork didn't capture it. Alloy requires this field for +//! deserialization. These utilities patch the JSON to add `type: "0x0"` (legacy +//! transaction) where missing. +//! +//! Also used by `PatchingHttp` for chains that don't support EIP-2718 typed transactions. + +use graph::prelude::serde_json::Value; + +pub(crate) fn patch_type_field(obj: &mut Value) -> bool { + if let Value::Object(map) = obj { + if !map.contains_key("type") { + map.insert("type".to_string(), Value::String("0x0".to_string())); + return true; + } + } + false +} + +pub(crate) fn patch_block_transactions(block: &mut Value) -> bool { + let Some(txs) = block.get_mut("transactions").and_then(|t| t.as_array_mut()) else { + return false; + }; + let mut patched = false; + for tx in txs { + patched |= patch_type_field(tx); + } + patched +} + +pub(crate) fn patch_receipts(result: &mut Value) -> bool { + match result { + Value::Object(_) => patch_type_field(result), + Value::Array(arr) => { + let mut patched = false; + for r in arr { + patched |= patch_type_field(r); + } + patched + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use graph::prelude::serde_json::json; + + #[test] + fn patch_type_field_adds_missing_type() { + let mut obj = json!({"status": "0x1", "gasUsed": "0x5208"}); + assert!(patch_type_field(&mut obj)); + assert_eq!(obj["type"], "0x0"); + } + + #[test] + fn patch_type_field_preserves_existing_type() { + let mut obj = json!({"status": "0x1", "type": "0x2"}); + assert!(!patch_type_field(&mut obj)); + assert_eq!(obj["type"], "0x2"); + } + + #[test] + fn patch_type_field_handles_non_object() { + let mut val = json!("not an object"); + assert!(!patch_type_field(&mut val)); + } + + #[test] + fn patch_block_transactions_patches_all() { + let mut block = json!({ + "hash": "0x123", + "transactions": [ + {"hash": "0xabc", "nonce": "0x1"}, + {"hash": "0xdef", "nonce": "0x2", "type": "0x2"}, + {"hash": "0xghi", "nonce": "0x3"} + ] + }); + assert!(patch_block_transactions(&mut block)); + assert_eq!(block["transactions"][0]["type"], "0x0"); + assert_eq!(block["transactions"][1]["type"], "0x2"); + assert_eq!(block["transactions"][2]["type"], "0x0"); + } + + #[test] + fn patch_block_transactions_handles_empty() { + let mut block = json!({"hash": "0x123", "transactions": []}); + assert!(!patch_block_transactions(&mut block)); + } + + #[test] + fn patch_block_transactions_handles_missing_field() { + let mut block = json!({"hash": "0x123"}); + assert!(!patch_block_transactions(&mut block)); + } + + #[test] + fn patch_receipts_single() { + let mut receipt = json!({"status": "0x1"}); + assert!(patch_receipts(&mut receipt)); + assert_eq!(receipt["type"], "0x0"); + } + + #[test] + fn patch_receipts_array() { + let mut receipts = json!([ + {"status": "0x1"}, + {"status": "0x1", "type": "0x2"} + ]); + assert!(patch_receipts(&mut receipts)); + assert_eq!(receipts[0]["type"], "0x0"); + assert_eq!(receipts[1]["type"], "0x2"); + } + + #[test] + fn patch_receipts_handles_null() { + let mut val = Value::Null; + assert!(!patch_receipts(&mut val)); + } +} diff --git a/chain/ethereum/src/lib.rs b/chain/ethereum/src/lib.rs index 8850764d63b..69cdc4e456a 100644 --- a/chain/ethereum/src/lib.rs +++ b/chain/ethereum/src/lib.rs @@ -7,6 +7,7 @@ mod data_source; mod env; mod ethereum_adapter; mod ingestor; +mod json_patch; mod polling_block_stream; pub mod runtime; mod transport; From f2dd5f96229c94917a864d888d21a06a97b61997 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 5 Feb 2026 15:19:18 +0400 Subject: [PATCH 2/7] chain/ethereum: refactor PatchingHttp to use json_patch module Refactor the HTTP transport's receipt patching to use the shared json_patch module instead of duplicating the patching logic. This removes the patch_receipt and patch_result methods from PatchingHttp and replaces them with calls to json_patch::patch_receipts. The patch_rpc_response and patch_response methods remain as they handle RPC-specific JSON-RPC response structure. --- chain/ethereum/src/transport.rs | 42 ++------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/chain/ethereum/src/transport.rs b/chain/ethereum/src/transport.rs index 4923e9c7b82..6c85b20841d 100644 --- a/chain/ethereum/src/transport.rs +++ b/chain/ethereum/src/transport.rs @@ -1,3 +1,4 @@ +use crate::json_patch; use alloy::transports::{TransportError, TransportErrorKind, TransportFut}; use graph::components::network_provider::ProviderName; use graph::endpoint::{ConnectionType, EndpointMetrics, RequestLabels}; @@ -148,34 +149,10 @@ impl PatchingHttp { method == "eth_getTransactionReceipt" || method == "eth_getBlockReceipts" } - fn patch_receipt(receipt: &mut Value) -> bool { - if let Value::Object(obj) = receipt { - if !obj.contains_key("type") { - obj.insert("type".to_string(), Value::String("0x0".to_string())); - return true; - } - } - false - } - - fn patch_result(result: &mut Value) -> bool { - match result { - Value::Object(_) => Self::patch_receipt(result), - Value::Array(arr) => { - let mut patched = false; - for r in arr { - patched |= Self::patch_receipt(r); - } - patched - } - _ => false, - } - } - fn patch_rpc_response(response: &mut Value) -> bool { response .get_mut("result") - .map(Self::patch_result) + .map(json_patch::patch_receipts) .unwrap_or(false) } @@ -262,21 +239,6 @@ impl Service for PatchingHttp { #[cfg(test)] mod tests { use super::*; - use serde_json::json; - - #[test] - fn patch_receipt_adds_missing_type() { - let mut receipt = json!({"status": "0x1", "gasUsed": "0x5208"}); - assert!(PatchingHttp::patch_receipt(&mut receipt)); - assert_eq!(receipt["type"], "0x0"); - } - - #[test] - fn patch_receipt_skips_existing_type() { - let mut receipt = json!({"status": "0x1", "type": "0x2"}); - assert!(!PatchingHttp::patch_receipt(&mut receipt)); - assert_eq!(receipt["type"], "0x2"); - } #[test] fn patch_response_single() { From c52a74861830535237df8c085dd3809e7841df47 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 5 Feb 2026 15:19:26 +0400 Subject: [PATCH 3/7] chain/ethereum: patch missing type field in cached blocks Add patching for cached blocks before deserialization to handle blocks that were cached before March 2022 when graph-node's rust-web3 fork didn't capture the transaction type field. This patches transactions and receipts in cached blocks at two locations: - ancestor_block(): For full block deserialization (EthereumBlock), patches both transactions and transaction_receipts - parent_ptr(): For light block deserialization (LightEthereumBlock), patches only transactions (light blocks don't include receipts) The patching adds type: 0x0 (legacy) to transactions/receipts missing the field, allowing alloy to deserialize blocks that would otherwise fail due to the missing required field. --- chain/ethereum/src/chain.rs | 81 ++++++++++++++++++-------- chain/ethereum/src/ethereum_adapter.rs | 27 +++++---- 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 85103cc5498..8d35cdef1fb 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -46,6 +46,7 @@ use crate::codec::HeaderOnlyBlock; use crate::data_source::DataSourceTemplate; use crate::data_source::UnresolvedDataSourceTemplate; use crate::ingestor::PollingBlockIngestor; +use crate::json_patch; use crate::network::EthereumNetworkAdapters; use crate::polling_block_stream::PollingBlockStream; use crate::runtime::runtime_adapter::eth_call_gas; @@ -1076,22 +1077,42 @@ impl TriggersAdapterTrait for TriggersAdapter { .await?; // First check if we have the ancestor in cache and can deserialize it. - // recent_blocks_cache can have full format {"block": {...}, "transaction_receipts": [...]} - // or light format (just block fields). We need full format with receipts for - // ancestor_block since it's used for trigger processing. + // The cached JSON can be in one of three formats: + // 1. Full RPC format: {"block": {...}, "transaction_receipts": [...]} + // 2. Shallow/header-only: {"timestamp": "...", "data": null} - only timestamp, no block data + // 3. Legacy direct: block fields at root level {hash, number, transactions, ...} + // We need full format with receipts for ancestor_block (used for trigger processing). let block_ptr = match cached { Some((json, ptr)) => { - if json.get("block").is_none() { - warn!( + // Shallow blocks have "data": null - no block data to deserialize + if json.get("data") == Some(&json::Value::Null) { + trace!( + self.logger, + "Cached block #{} {} is shallow (header-only). Falling back to Firehose/RPC.", + ptr.number, + ptr.hash_hex(), + ); + ptr + } else if json.get("block").is_none() { + trace!( self.logger, - "Cached ancestor block #{} {} has light format without receipts. \ - Falling back to Firehose/RPC.", + "Cached block #{} {} is legacy light format. Falling back to Firehose/RPC.", ptr.number, ptr.hash_hex(), ); ptr } else { - match json::from_value::(json.clone()) { + // Some cached blocks are missing the transaction `type` field + // because graph-node's rust-web3 fork didn't have that field. Patch them + // with type: 0x0 (legacy) so alloy can deserialize them. + let mut json = json; + if let Some(block) = json.get_mut("block") { + json_patch::patch_block_transactions(block); + } + if let Some(receipts) = json.get_mut("transaction_receipts") { + json_patch::patch_receipts(receipts); + } + match json::from_value::(json) { Ok(block) => { return Ok(Some(BlockFinality::NonFinal(EthereumBlockWithCalls { ethereum_block: block, @@ -1169,24 +1190,36 @@ impl TriggersAdapterTrait for TriggersAdapter { ChainClient::Firehose(endpoints) => { let chain_store = self.chain_store.cheap_clone(); // First try to get the block from the store + // See ancestor_block() for documentation of the 3 cached JSON formats. if let Ok(blocks) = chain_store.blocks(vec![block.hash.clone()]).await { if let Some(cached_json) = blocks.first() { - // recent_blocks_cache can contain full format {"block": {...}, "transaction_receipts": [...]} - // or light format (just block fields). Extract block data for deserialization. - let inner = cached_json.get("block").unwrap_or(cached_json); - match json::from_value::(inner.clone()) { - Ok(light_block) => { - return Ok(light_block.parent_ptr()); - } - Err(e) => { - warn!( - self.logger, - "Failed to deserialize cached block #{} {}: {}. \ - Falling back to Firehose.", - block.number, - block.hash_hex(), - e - ); + // Shallow blocks have "data": null - no block data to deserialize + if cached_json.get("data") == Some(&json::Value::Null) { + trace!( + self.logger, + "Cached block #{} {} is shallow. Falling back to Firehose.", + block.number, + block.hash_hex(), + ); + } else { + let mut inner = cached_json.get("block").unwrap_or(cached_json).clone(); + // Some cached blocks are missing the transaction `type` + // field. Patch with type: 0x0 (legacy) so alloy can deserialize. + json_patch::patch_block_transactions(&mut inner); + match json::from_value::(inner) { + Ok(light_block) => { + return Ok(light_block.parent_ptr()); + } + Err(e) => { + warn!( + self.logger, + "Failed to deserialize cached block #{} {}: {}. \ + Falling back to Firehose.", + block.number, + block.hash_hex(), + e + ); + } } } } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 64affbeec0b..97965b7fdac 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -65,6 +65,7 @@ use crate::adapter::EthereumRpcError; use crate::adapter::ProviderStatus; use crate::call_helper::interpret_eth_call_error; use crate::chain::BlockFinality; +use crate::json_patch; use crate::trigger::{LogPosition, LogRef}; use crate::Chain; use crate::NodeCapabilities; @@ -1613,20 +1614,24 @@ impl EthereumAdapterTrait for EthereumAdapter { .map_err(|e| error!(&logger, "Error accessing block cache {}", e)) .unwrap_or_default() .into_iter() - .filter_map(|value| { - // recent_blocks_cache can contain full format {"block": {...}, "transaction_receipts": [...]} - // or light format (just block fields). Extract block data for deserialization. - let inner = value.get("block").unwrap_or(&value); - json::from_value(inner.clone()) + .filter_map(|mut value| { + // Cached JSON formats: see chain.rs ancestor_block() for documentation. + // Shallow blocks have "data": null - skip silently. + if value.get("data") == Some(&json::Value::Null) { + return None; + } + let mut inner = value + .as_object_mut() + .and_then(|obj| obj.remove("block")) + .unwrap_or(value); + // Some cached blocks are missing the transaction `type` field. + // Patch with type: 0x0 (legacy) so alloy can deserialize. + json_patch::patch_block_transactions(&mut inner); + json::from_value(inner) .map_err(|e| { - let block_num = inner.get("number").and_then(|n| n.as_str()); - let block_hash = inner.get("hash").and_then(|h| h.as_str()); warn!( &logger, - "Failed to deserialize cached block #{:?} {:?}: {}. \ - Block will be re-fetched from RPC.", - block_num, - block_hash, + "Failed to deserialize cached block: {}. Block will be re-fetched from RPC.", e ); }) From 7fa4a5413263c842105018661e360ff7c0a6b1c5 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 6 Feb 2026 16:48:50 +0400 Subject: [PATCH 4/7] chain/ethereum: add EthereumJsonBlock newtype for cached block handling Introduce EthereumJsonBlock newtype with helper methods for format detection (is_shallow, is_legacy_format) and deserialization (into_full_block, into_light_block). Cleans up repeated logic and avoids unnecessary clones by taking ownership of the JSON data. --- chain/ethereum/src/chain.rs | 32 +++++----------- chain/ethereum/src/ethereum_adapter.rs | 23 ++++-------- chain/ethereum/src/json_block.rs | 51 ++++++++++++++++++++++++++ chain/ethereum/src/lib.rs | 1 + 4 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 chain/ethereum/src/json_block.rs diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 8d35cdef1fb..0cd69188c41 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -46,7 +46,7 @@ use crate::codec::HeaderOnlyBlock; use crate::data_source::DataSourceTemplate; use crate::data_source::UnresolvedDataSourceTemplate; use crate::ingestor::PollingBlockIngestor; -use crate::json_patch; +use crate::json_block::EthereumJsonBlock; use crate::network::EthereumNetworkAdapters; use crate::polling_block_stream::PollingBlockStream; use crate::runtime::runtime_adapter::eth_call_gas; @@ -1084,8 +1084,8 @@ impl TriggersAdapterTrait for TriggersAdapter { // We need full format with receipts for ancestor_block (used for trigger processing). let block_ptr = match cached { Some((json, ptr)) => { - // Shallow blocks have "data": null - no block data to deserialize - if json.get("data") == Some(&json::Value::Null) { + let json_block = EthereumJsonBlock::new(json); + if json_block.is_shallow() { trace!( self.logger, "Cached block #{} {} is shallow (header-only). Falling back to Firehose/RPC.", @@ -1093,7 +1093,7 @@ impl TriggersAdapterTrait for TriggersAdapter { ptr.hash_hex(), ); ptr - } else if json.get("block").is_none() { + } else if json_block.is_legacy_format() { trace!( self.logger, "Cached block #{} {} is legacy light format. Falling back to Firehose/RPC.", @@ -1102,17 +1102,7 @@ impl TriggersAdapterTrait for TriggersAdapter { ); ptr } else { - // Some cached blocks are missing the transaction `type` field - // because graph-node's rust-web3 fork didn't have that field. Patch them - // with type: 0x0 (legacy) so alloy can deserialize them. - let mut json = json; - if let Some(block) = json.get_mut("block") { - json_patch::patch_block_transactions(block); - } - if let Some(receipts) = json.get_mut("transaction_receipts") { - json_patch::patch_receipts(receipts); - } - match json::from_value::(json) { + match json_block.into_full_block::() { Ok(block) => { return Ok(Some(BlockFinality::NonFinal(EthereumBlockWithCalls { ethereum_block: block, @@ -1192,9 +1182,9 @@ impl TriggersAdapterTrait for TriggersAdapter { // First try to get the block from the store // See ancestor_block() for documentation of the 3 cached JSON formats. if let Ok(blocks) = chain_store.blocks(vec![block.hash.clone()]).await { - if let Some(cached_json) = blocks.first() { - // Shallow blocks have "data": null - no block data to deserialize - if cached_json.get("data") == Some(&json::Value::Null) { + if let Some(cached_json) = blocks.into_iter().next() { + let json_block = EthereumJsonBlock::new(cached_json); + if json_block.is_shallow() { trace!( self.logger, "Cached block #{} {} is shallow. Falling back to Firehose.", @@ -1202,11 +1192,7 @@ impl TriggersAdapterTrait for TriggersAdapter { block.hash_hex(), ); } else { - let mut inner = cached_json.get("block").unwrap_or(cached_json).clone(); - // Some cached blocks are missing the transaction `type` - // field. Patch with type: 0x0 (legacy) so alloy can deserialize. - json_patch::patch_block_transactions(&mut inner); - match json::from_value::(inner) { + match json_block.into_light_block::() { Ok(light_block) => { return Ok(light_block.parent_ptr()); } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 97965b7fdac..cb3764b2ca6 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -46,8 +46,8 @@ use graph::{ blockchain::{block_stream::BlockWithTriggers, BlockPtr, IngestorError}, prelude::{ anyhow::{self, anyhow, bail, ensure, Context}, - debug, error, hex, info, retry, serde_json as json, trace, warn, BlockNumber, ChainStore, - CheapClone, DynTryFuture, Error, EthereumCallCache, Logger, TimeoutError, + debug, error, hex, info, retry, trace, warn, BlockNumber, ChainStore, CheapClone, + DynTryFuture, Error, EthereumCallCache, Logger, TimeoutError, }, }; use itertools::Itertools; @@ -65,7 +65,7 @@ use crate::adapter::EthereumRpcError; use crate::adapter::ProviderStatus; use crate::call_helper::interpret_eth_call_error; use crate::chain::BlockFinality; -use crate::json_patch; +use crate::json_block::EthereumJsonBlock; use crate::trigger::{LogPosition, LogRef}; use crate::Chain; use crate::NodeCapabilities; @@ -1614,20 +1614,13 @@ impl EthereumAdapterTrait for EthereumAdapter { .map_err(|e| error!(&logger, "Error accessing block cache {}", e)) .unwrap_or_default() .into_iter() - .filter_map(|mut value| { - // Cached JSON formats: see chain.rs ancestor_block() for documentation. - // Shallow blocks have "data": null - skip silently. - if value.get("data") == Some(&json::Value::Null) { + .filter_map(|value| { + let json_block = EthereumJsonBlock::new(value); + if json_block.is_shallow() { return None; } - let mut inner = value - .as_object_mut() - .and_then(|obj| obj.remove("block")) - .unwrap_or(value); - // Some cached blocks are missing the transaction `type` field. - // Patch with type: 0x0 (legacy) so alloy can deserialize. - json_patch::patch_block_transactions(&mut inner); - json::from_value(inner) + json_block + .into_light_block() .map_err(|e| { warn!( &logger, diff --git a/chain/ethereum/src/json_block.rs b/chain/ethereum/src/json_block.rs new file mode 100644 index 00000000000..168172fe8de --- /dev/null +++ b/chain/ethereum/src/json_block.rs @@ -0,0 +1,51 @@ +use graph::prelude::serde_json::{self as json, Value}; +use serde::de::DeserializeOwned; + +use crate::json_patch; + +#[derive(Debug)] +pub struct EthereumJsonBlock(Value); + +impl EthereumJsonBlock { + pub fn new(value: Value) -> Self { + Self(value) + } + + pub fn is_shallow(&self) -> bool { + self.0.get("data") == Some(&Value::Null) + } + + pub fn is_legacy_format(&self) -> bool { + self.0.get("block").is_none() + } + + pub fn patch(&mut self) { + if let Some(block) = self.0.get_mut("block") { + json_patch::patch_block_transactions(block); + } + if let Some(receipts) = self.0.get_mut("transaction_receipts") { + json_patch::patch_receipts(receipts); + } + } + + pub fn into_full_block(mut self) -> Result { + self.patch(); + json::from_value(self.0) + } + + pub fn into_light_block(mut self) -> Result { + let mut inner = self + .0 + .as_object_mut() + .and_then(|obj| obj.remove("block")) + .unwrap_or(self.0); + json_patch::patch_block_transactions(&mut inner); + json::from_value(inner) + } +} + +impl From for EthereumJsonBlock { + fn from(value: Value) -> Self { + Self::new(value) + } +} diff --git a/chain/ethereum/src/lib.rs b/chain/ethereum/src/lib.rs index 69cdc4e456a..2bbc53fa327 100644 --- a/chain/ethereum/src/lib.rs +++ b/chain/ethereum/src/lib.rs @@ -7,6 +7,7 @@ mod data_source; mod env; mod ethereum_adapter; mod ingestor; +mod json_block; mod json_patch; mod polling_block_stream; pub mod runtime; From 5f522067196b0ead89bcd9f589dee34aef4a18ce Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 6 Feb 2026 21:37:21 +0400 Subject: [PATCH 5/7] chain/ethereum: use concrete types in EthereumJsonBlock methods Remove generic type parameters from into_full_block() and into_light_block(), returning EthereumBlock and LightEthereumBlock directly instead. --- chain/ethereum/src/chain.rs | 4 ++-- chain/ethereum/src/ethereum_adapter.rs | 2 +- chain/ethereum/src/json_block.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 0cd69188c41..6cc098651a2 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -1102,7 +1102,7 @@ impl TriggersAdapterTrait for TriggersAdapter { ); ptr } else { - match json_block.into_full_block::() { + match json_block.into_full_block() { Ok(block) => { return Ok(Some(BlockFinality::NonFinal(EthereumBlockWithCalls { ethereum_block: block, @@ -1192,7 +1192,7 @@ impl TriggersAdapterTrait for TriggersAdapter { block.hash_hex(), ); } else { - match json_block.into_light_block::() { + match json_block.into_light_block() { Ok(light_block) => { return Ok(light_block.parent_ptr()); } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index cb3764b2ca6..6a811cd134d 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1630,7 +1630,7 @@ impl EthereumAdapterTrait for EthereumAdapter { }) .ok() }) - .map(|b| Arc::new(LightEthereumBlock::new(b))) + .map(Arc::new) .collect(); let missing_blocks = Vec::from_iter( diff --git a/chain/ethereum/src/json_block.rs b/chain/ethereum/src/json_block.rs index 168172fe8de..6d8bea9d210 100644 --- a/chain/ethereum/src/json_block.rs +++ b/chain/ethereum/src/json_block.rs @@ -1,5 +1,5 @@ use graph::prelude::serde_json::{self as json, Value}; -use serde::de::DeserializeOwned; +use graph::prelude::{EthereumBlock, LightEthereumBlock}; use crate::json_patch; @@ -28,12 +28,12 @@ impl EthereumJsonBlock { } } - pub fn into_full_block(mut self) -> Result { + pub fn into_full_block(mut self) -> Result { self.patch(); json::from_value(self.0) } - pub fn into_light_block(mut self) -> Result { + pub fn into_light_block(mut self) -> Result { let mut inner = self .0 .as_object_mut() From b1a9cc66910aa0555dec87b212c8b9ddba1b6f60 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 6 Feb 2026 21:41:43 +0400 Subject: [PATCH 6/7] chain/ethereum: add doc comments to EthereumJsonBlock methods --- chain/ethereum/src/json_block.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/chain/ethereum/src/json_block.rs b/chain/ethereum/src/json_block.rs index 6d8bea9d210..0e0236adfce 100644 --- a/chain/ethereum/src/json_block.rs +++ b/chain/ethereum/src/json_block.rs @@ -11,14 +11,19 @@ impl EthereumJsonBlock { Self(value) } + /// Returns true if this is a shallow/header-only block (no full block data). pub fn is_shallow(&self) -> bool { self.0.get("data") == Some(&Value::Null) } + /// Returns true if this block is in the legacy format (direct block JSON + /// rather than wrapped in a `block` field). pub fn is_legacy_format(&self) -> bool { self.0.get("block").is_none() } + /// Patches missing `type` fields in transactions and receipts. + /// Required for alloy compatibility with cached blocks from older graph-node versions. pub fn patch(&mut self) { if let Some(block) = self.0.get_mut("block") { json_patch::patch_block_transactions(block); @@ -28,11 +33,13 @@ impl EthereumJsonBlock { } } + /// Patches and deserializes into a full `EthereumBlock` with receipts. pub fn into_full_block(mut self) -> Result { self.patch(); json::from_value(self.0) } + /// Extracts and patches the inner block, deserializing into a `LightEthereumBlock`. pub fn into_light_block(mut self) -> Result { let mut inner = self .0 From 9d5203ba4a4fe235efaf06dc096de7684e3e19cd Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Fri, 6 Feb 2026 21:43:28 +0400 Subject: [PATCH 7/7] chain/ethereum: remove unused From impl for EthereumJsonBlock --- chain/ethereum/src/json_block.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/chain/ethereum/src/json_block.rs b/chain/ethereum/src/json_block.rs index 0e0236adfce..5525e7fb7a8 100644 --- a/chain/ethereum/src/json_block.rs +++ b/chain/ethereum/src/json_block.rs @@ -50,9 +50,3 @@ impl EthereumJsonBlock { json::from_value(inner) } } - -impl From for EthereumJsonBlock { - fn from(value: Value) -> Self { - Self::new(value) - } -}