1+ use alloy:: transports:: { TransportError , TransportErrorKind , TransportFut } ;
12use graph:: components:: network_provider:: ProviderName ;
23use graph:: endpoint:: { ConnectionType , EndpointMetrics , RequestLabels } ;
34use graph:: prelude:: alloy:: rpc:: json_rpc:: { RequestPacket , ResponsePacket } ;
5+ use graph:: prelude:: alloy:: transports:: { ipc:: IpcConnect , ws:: WsConnect } ;
46use graph:: prelude:: * ;
57use graph:: url:: Url ;
8+ use serde_json:: Value ;
69use std:: sync:: Arc ;
710use std:: task:: { Context , Poll } ;
811use tower:: Service ;
912
10- use alloy:: transports:: { TransportError , TransportFut } ;
11-
12- use graph:: prelude:: alloy:: transports:: { http:: Http , ipc:: IpcConnect , ws:: WsConnect } ;
13-
1413/// Abstraction over different transport types for Alloy providers.
1514#[ derive( Clone , Debug ) ]
1615pub enum Transport {
@@ -41,19 +40,24 @@ impl Transport {
4140 }
4241
4342 /// Creates a JSON-RPC over HTTP transport.
43+ ///
44+ /// Set `no_eip2718` to true for chains that don't return the `type` field
45+ /// in transaction receipts (pre-EIP-2718 chains). Use provider feature `no_eip2718`.
4446 pub fn new_rpc (
4547 rpc : Url ,
4648 headers : graph:: http:: HeaderMap ,
4749 metrics : Arc < EndpointMetrics > ,
4850 provider : impl AsRef < str > ,
51+ no_eip2718 : bool ,
4952 ) -> Self {
5053 let client = reqwest:: Client :: builder ( )
5154 . default_headers ( headers)
5255 . build ( )
5356 . expect ( "Failed to build HTTP client" ) ;
5457
55- let http_transport = Http :: with_client ( client, rpc) ;
56- let metrics_transport = MetricsHttp :: new ( http_transport, metrics, provider. as_ref ( ) . into ( ) ) ;
58+ let patching_transport = PatchingHttp :: new ( client, rpc, no_eip2718) ;
59+ let metrics_transport =
60+ MetricsHttp :: new ( patching_transport, metrics, provider. as_ref ( ) . into ( ) ) ;
5761 let rpc_client = alloy:: rpc:: client:: RpcClient :: new ( metrics_transport, false ) ;
5862
5963 Transport :: RPC ( rpc_client)
@@ -63,17 +67,13 @@ impl Transport {
6367/// Custom HTTP transport wrapper that collects metrics
6468#[ derive( Clone ) ]
6569pub struct MetricsHttp {
66- inner : Http < reqwest :: Client > ,
70+ inner : PatchingHttp ,
6771 metrics : Arc < EndpointMetrics > ,
6872 provider : ProviderName ,
6973}
7074
7175impl MetricsHttp {
72- pub fn new (
73- inner : Http < reqwest:: Client > ,
74- metrics : Arc < EndpointMetrics > ,
75- provider : ProviderName ,
76- ) -> Self {
76+ pub fn new ( inner : PatchingHttp , metrics : Arc < EndpointMetrics > , provider : ProviderName ) -> Self {
7777 Self {
7878 inner,
7979 metrics,
@@ -125,3 +125,179 @@ impl Service<RequestPacket> for MetricsHttp {
125125 } )
126126 }
127127}
128+
129+ /// HTTP transport that patches receipts for chains that don't support EIP-2718 (typed transactions).
130+ /// When `no_eip2718` is set, adds missing `type` field to receipts.
131+ #[ derive( Clone ) ]
132+ pub struct PatchingHttp {
133+ client : reqwest:: Client ,
134+ url : Url ,
135+ no_eip2718 : bool ,
136+ }
137+
138+ impl PatchingHttp {
139+ pub fn new ( client : reqwest:: Client , url : Url , no_eip2718 : bool ) -> Self {
140+ Self {
141+ client,
142+ url,
143+ no_eip2718,
144+ }
145+ }
146+
147+ fn is_receipt_method ( method : & str ) -> bool {
148+ method == "eth_getTransactionReceipt" || method == "eth_getBlockReceipts"
149+ }
150+
151+ fn patch_receipt ( receipt : & mut Value ) -> bool {
152+ if let Value :: Object ( obj) = receipt {
153+ if !obj. contains_key ( "type" ) {
154+ obj. insert ( "type" . to_string ( ) , Value :: String ( "0x0" . to_string ( ) ) ) ;
155+ return true ;
156+ }
157+ }
158+ false
159+ }
160+
161+ fn patch_result ( result : & mut Value ) -> bool {
162+ match result {
163+ Value :: Object ( _) => Self :: patch_receipt ( result) ,
164+ Value :: Array ( arr) => {
165+ let mut patched = false ;
166+ for r in arr {
167+ patched |= Self :: patch_receipt ( r) ;
168+ }
169+ patched
170+ }
171+ _ => false ,
172+ }
173+ }
174+
175+ fn patch_rpc_response ( response : & mut Value ) -> bool {
176+ response
177+ . get_mut ( "result" )
178+ . map ( Self :: patch_result)
179+ . unwrap_or ( false )
180+ }
181+
182+ fn patch_response ( body : & [ u8 ] ) -> Option < Vec < u8 > > {
183+ let mut json: Value = serde_json:: from_slice ( body) . ok ( ) ?;
184+
185+ let patched = match & mut json {
186+ Value :: Object ( _) => Self :: patch_rpc_response ( & mut json) ,
187+ Value :: Array ( batch) => {
188+ let mut patched = false ;
189+ for r in batch {
190+ patched |= Self :: patch_rpc_response ( r) ;
191+ }
192+ patched
193+ }
194+ _ => false ,
195+ } ;
196+
197+ if patched {
198+ serde_json:: to_vec ( & json) . ok ( )
199+ } else {
200+ None
201+ }
202+ }
203+ }
204+
205+ impl Service < RequestPacket > for PatchingHttp {
206+ type Response = ResponsePacket ;
207+ type Error = TransportError ;
208+ type Future = TransportFut < ' static > ;
209+
210+ fn poll_ready ( & mut self , _cx : & mut Context < ' _ > ) -> Poll < Result < ( ) , Self :: Error > > {
211+ Poll :: Ready ( Ok ( ( ) ) )
212+ }
213+
214+ fn call ( & mut self , request : RequestPacket ) -> Self :: Future {
215+ let client = self . client . clone ( ) ;
216+ let url = self . url . clone ( ) ;
217+ let no_eip2718 = self . no_eip2718 ;
218+
219+ let should_patch = if no_eip2718 {
220+ match & request {
221+ RequestPacket :: Single ( req) => Self :: is_receipt_method ( req. method ( ) ) ,
222+ RequestPacket :: Batch ( reqs) => {
223+ reqs. iter ( ) . any ( |r| Self :: is_receipt_method ( r. method ( ) ) )
224+ }
225+ }
226+ } else {
227+ false
228+ } ;
229+
230+ Box :: pin ( async move {
231+ let resp = client
232+ . post ( url)
233+ . json ( & request)
234+ . headers ( request. headers ( ) )
235+ . send ( )
236+ . await
237+ . map_err ( TransportErrorKind :: custom) ?;
238+
239+ let status = resp. status ( ) ;
240+ let body = resp. bytes ( ) . await . map_err ( TransportErrorKind :: custom) ?;
241+
242+ if !status. is_success ( ) {
243+ return Err ( TransportErrorKind :: http_error (
244+ status. as_u16 ( ) ,
245+ String :: from_utf8_lossy ( & body) . into_owned ( ) ,
246+ ) ) ;
247+ }
248+
249+ if should_patch {
250+ if let Some ( patched) = Self :: patch_response ( & body) {
251+ return serde_json:: from_slice ( & patched) . map_err ( |err| {
252+ TransportError :: deser_err ( err, String :: from_utf8_lossy ( & patched) )
253+ } ) ;
254+ }
255+ }
256+ serde_json:: from_slice ( & body)
257+ . map_err ( |err| TransportError :: deser_err ( err, String :: from_utf8_lossy ( & body) ) )
258+ } )
259+ }
260+ }
261+
262+ #[ cfg( test) ]
263+ mod tests {
264+ use super :: * ;
265+ use serde_json:: json;
266+
267+ #[ test]
268+ fn patch_receipt_adds_missing_type ( ) {
269+ let mut receipt = json ! ( { "status" : "0x1" , "gasUsed" : "0x5208" } ) ;
270+ assert ! ( PatchingHttp :: patch_receipt( & mut receipt) ) ;
271+ assert_eq ! ( receipt[ "type" ] , "0x0" ) ;
272+ }
273+
274+ #[ test]
275+ fn patch_receipt_skips_existing_type ( ) {
276+ let mut receipt = json ! ( { "status" : "0x1" , "type" : "0x2" } ) ;
277+ assert ! ( !PatchingHttp :: patch_receipt( & mut receipt) ) ;
278+ assert_eq ! ( receipt[ "type" ] , "0x2" ) ;
279+ }
280+
281+ #[ test]
282+ fn patch_response_single ( ) {
283+ let body = br#"{"jsonrpc":"2.0","id":1,"result":{"status":"0x1"}}"# ;
284+ let patched = PatchingHttp :: patch_response ( body) . unwrap ( ) ;
285+ let json: Value = serde_json:: from_slice ( & patched) . unwrap ( ) ;
286+ assert_eq ! ( json[ "result" ] [ "type" ] , "0x0" ) ;
287+ }
288+
289+ #[ test]
290+ fn patch_response_returns_none_when_type_exists ( ) {
291+ let body = br#"{"jsonrpc":"2.0","id":1,"result":{"status":"0x1","type":"0x2"}}"# ;
292+ assert ! ( PatchingHttp :: patch_response( body) . is_none( ) ) ;
293+ }
294+
295+ #[ test]
296+ fn patch_response_batch ( ) {
297+ let body = br#"[{"jsonrpc":"2.0","id":1,"result":{"status":"0x1"}},{"jsonrpc":"2.0","id":2,"result":{"status":"0x1"}}]"# ;
298+ let patched = PatchingHttp :: patch_response ( body) . unwrap ( ) ;
299+ let json: Value = serde_json:: from_slice ( & patched) . unwrap ( ) ;
300+ assert_eq ! ( json[ 0 ] [ "result" ] [ "type" ] , "0x0" ) ;
301+ assert_eq ! ( json[ 1 ] [ "result" ] [ "type" ] , "0x0" ) ;
302+ }
303+ }
0 commit comments