Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions chain/ethereum/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ mod tests {
HeaderMap::new(),
metrics.clone(),
"",
false,
);
let provider_metrics = Arc::new(ProviderEthRpcMetrics::new(mock_registry.clone()));

Expand Down Expand Up @@ -497,6 +498,7 @@ mod tests {
HeaderMap::new(),
metrics.clone(),
"",
false,
);
let provider_metrics = Arc::new(ProviderEthRpcMetrics::new(mock_registry.clone()));

Expand Down Expand Up @@ -568,6 +570,7 @@ mod tests {
HeaderMap::new(),
metrics.clone(),
"",
false,
);
let provider_metrics = Arc::new(ProviderEthRpcMetrics::new(mock_registry.clone()));

Expand Down Expand Up @@ -632,6 +635,7 @@ mod tests {
HeaderMap::new(),
metrics.clone(),
"",
false,
);
let provider_metrics = Arc::new(ProviderEthRpcMetrics::new(mock_registry.clone()));

Expand Down Expand Up @@ -919,6 +923,7 @@ mod tests {
HeaderMap::new(),
endpoint_metrics.clone(),
"",
false,
);

Arc::new(
Expand Down
200 changes: 188 additions & 12 deletions chain/ethereum/src/transport.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use alloy::transports::{TransportError, TransportErrorKind, TransportFut};
use graph::components::network_provider::ProviderName;
use graph::endpoint::{ConnectionType, EndpointMetrics, RequestLabels};
use graph::prelude::alloy::rpc::json_rpc::{RequestPacket, ResponsePacket};
use graph::prelude::alloy::transports::{ipc::IpcConnect, ws::WsConnect};
use graph::prelude::*;
use graph::url::Url;
use serde_json::Value;
use std::sync::Arc;
use std::task::{Context, Poll};
use tower::Service;

use alloy::transports::{TransportError, TransportFut};

use graph::prelude::alloy::transports::{http::Http, ipc::IpcConnect, ws::WsConnect};

/// Abstraction over different transport types for Alloy providers.
#[derive(Clone, Debug)]
pub enum Transport {
Expand Down Expand Up @@ -41,19 +40,24 @@ impl Transport {
}

/// Creates a JSON-RPC over HTTP transport.
///
/// Set `no_eip2718` to true for chains that don't return the `type` field
/// in transaction receipts (pre-EIP-2718 chains). Use provider feature `no_eip2718`.
pub fn new_rpc(
rpc: Url,
headers: graph::http::HeaderMap,
metrics: Arc<EndpointMetrics>,
provider: impl AsRef<str>,
no_eip2718: bool,
) -> Self {
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.expect("Failed to build HTTP client");

let http_transport = Http::with_client(client, rpc);
let metrics_transport = MetricsHttp::new(http_transport, metrics, provider.as_ref().into());
let patching_transport = PatchingHttp::new(client, rpc, no_eip2718);
let metrics_transport =
MetricsHttp::new(patching_transport, metrics, provider.as_ref().into());
let rpc_client = alloy::rpc::client::RpcClient::new(metrics_transport, false);

Transport::RPC(rpc_client)
Expand All @@ -63,17 +67,13 @@ impl Transport {
/// Custom HTTP transport wrapper that collects metrics
#[derive(Clone)]
pub struct MetricsHttp {
inner: Http<reqwest::Client>,
inner: PatchingHttp,
metrics: Arc<EndpointMetrics>,
provider: ProviderName,
}

impl MetricsHttp {
pub fn new(
inner: Http<reqwest::Client>,
metrics: Arc<EndpointMetrics>,
provider: ProviderName,
) -> Self {
pub fn new(inner: PatchingHttp, metrics: Arc<EndpointMetrics>, provider: ProviderName) -> Self {
Self {
inner,
metrics,
Expand Down Expand Up @@ -125,3 +125,179 @@ impl Service<RequestPacket> for MetricsHttp {
})
}
}

/// HTTP transport that patches receipts for chains that don't support EIP-2718 (typed transactions).
/// When `no_eip2718` is set, adds missing `type` field to receipts.
#[derive(Clone)]
pub struct PatchingHttp {
client: reqwest::Client,
url: Url,
no_eip2718: bool,
}

impl PatchingHttp {
pub fn new(client: reqwest::Client, url: Url, no_eip2718: bool) -> Self {
Self {
client,
url,
no_eip2718,
}
}

fn is_receipt_method(method: &str) -> bool {
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)
.unwrap_or(false)
}

fn patch_response(body: &[u8]) -> Option<Vec<u8>> {
let mut json: Value = serde_json::from_slice(body).ok()?;

let patched = match &mut json {
Value::Object(_) => Self::patch_rpc_response(&mut json),
Value::Array(batch) => {
let mut patched = false;
for r in batch {
patched |= Self::patch_rpc_response(r);
}
patched
}
_ => false,
};

if patched {
serde_json::to_vec(&json).ok()
} else {
None
}
}
}

impl Service<RequestPacket> for PatchingHttp {
type Response = ResponsePacket;
type Error = TransportError;
type Future = TransportFut<'static>;

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}

fn call(&mut self, request: RequestPacket) -> Self::Future {
let client = self.client.clone();
let url = self.url.clone();
let no_eip2718 = self.no_eip2718;

let should_patch = if no_eip2718 {
match &request {
RequestPacket::Single(req) => Self::is_receipt_method(req.method()),
RequestPacket::Batch(reqs) => {
reqs.iter().any(|r| Self::is_receipt_method(r.method()))
}
}
} else {
false
};

Box::pin(async move {
let resp = client
.post(url)
.json(&request)
.headers(request.headers())
.send()
.await
.map_err(TransportErrorKind::custom)?;

let status = resp.status();
let body = resp.bytes().await.map_err(TransportErrorKind::custom)?;

if !status.is_success() {
return Err(TransportErrorKind::http_error(
status.as_u16(),
String::from_utf8_lossy(&body).into_owned(),
));
}

if should_patch {
if let Some(patched) = Self::patch_response(&body) {
return serde_json::from_slice(&patched).map_err(|err| {
TransportError::deser_err(err, String::from_utf8_lossy(&patched))
});
}
}
serde_json::from_slice(&body)
.map_err(|err| TransportError::deser_err(err, String::from_utf8_lossy(&body)))
})
}
}

#[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() {
let body = br#"{"jsonrpc":"2.0","id":1,"result":{"status":"0x1"}}"#;
let patched = PatchingHttp::patch_response(body).unwrap();
let json: Value = serde_json::from_slice(&patched).unwrap();
assert_eq!(json["result"]["type"], "0x0");
}

#[test]
fn patch_response_returns_none_when_type_exists() {
let body = br#"{"jsonrpc":"2.0","id":1,"result":{"status":"0x1","type":"0x2"}}"#;
assert!(PatchingHttp::patch_response(body).is_none());
}

#[test]
fn patch_response_batch() {
let body = br#"[{"jsonrpc":"2.0","id":1,"result":{"status":"0x1"}},{"jsonrpc":"2.0","id":2,"result":{"status":"0x1"}}]"#;
let patched = PatchingHttp::patch_response(body).unwrap();
let json: Value = serde_json::from_slice(&patched).unwrap();
assert_eq!(json[0]["result"]["type"], "0x0");
assert_eq!(json[1]["result"]["type"], "0x0");
}
}
11 changes: 9 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,15 @@ A `provider` is an object with the following characteristics:
- `transport`: one of `rpc`, `ws`, and `ipc`. Defaults to `rpc`.
- `url`: the URL for the provider
- `features`: an array of features that the provider supports, either empty
or any combination of `traces` and `archive` for Web3 providers, or
`compression` and `filters` for Firehose providers
or any combination of the following for Web3 providers:
- `traces`: provider supports `debug_traceBlockByNumber` for call tracing
- `archive`: provider is an archive node with full historical state
- `no_eip1898`: provider doesn't support EIP-1898 (block parameter by hash/number object)
- `no_eip2718`: provider doesn't return the `type` field in transaction receipts
(pre-EIP-2718 chains). When set, receipts are patched to add
`"type": "0x0"` for legacy transaction compatibility.

For Firehose providers: `compression` and `filters`
- `headers`: HTTP headers to be added on every request. Defaults to none.
- `limit`: the maximum number of subgraphs that can use this provider.
Defaults to unlimited. At least one provider should be unlimited,
Expand Down
2 changes: 2 additions & 0 deletions node/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,14 @@ pub async fn create_ethereum_networks_for_chain(

use crate::config::Transport::*;

let no_eip2718 = web3.features.contains("no_eip2718");
let transport = match web3.transport {
Rpc => Transport::new_rpc(
Url::parse(&web3.url)?,
web3.headers.clone(),
endpoint_metrics.cheap_clone(),
&provider.label,
no_eip2718,
),
Ipc => Transport::new_ipc(&web3.url).await,
Ws => Transport::new_ws(&web3.url).await,
Expand Down
8 changes: 7 additions & 1 deletion node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,13 @@ impl Web3Provider {
}
}

const PROVIDER_FEATURES: [&str; 3] = ["traces", "archive", "no_eip1898"];
/// Supported provider features:
/// - `traces`: Provider supports debug_traceBlockByNumber for call tracing
/// - `archive`: Provider is an archive node with full historical state
/// - `no_eip1898`: Provider doesn't support EIP-1898 (block parameter by hash/number object)
/// - `no_eip2718`: Provider doesn't return the `type` field in transaction receipts.
/// When set, receipts are patched to add `"type": "0x0"` for legacy transaction compatibility.
const PROVIDER_FEATURES: [&str; 4] = ["traces", "archive", "no_eip1898", "no_eip2718"];
const DEFAULT_PROVIDER_FEATURES: [&str; 2] = ["traces", "archive"];

impl Provider {
Expand Down