@@ -47,12 +47,13 @@ use graph::{
4747 blockchain:: { BlockPtr , IngestorError , block_stream:: BlockWithTriggers } ,
4848 prelude:: {
4949 BlockNumber , ChainStore , CheapClone , DynTryFuture , Error , EthereumCallCache , Logger ,
50- TimeoutError ,
50+ TimeoutError , serde_json ,
5151 anyhow:: { self , Context , anyhow, bail, ensure} ,
5252 debug, error, hex, info, retry, trace, warn,
5353 } ,
5454} ;
5555use itertools:: Itertools ;
56+ use serde_json:: Value ;
5657use std:: collections:: { BTreeMap , BTreeSet , HashMap , HashSet } ;
5758use std:: convert:: TryFrom ;
5859use std:: iter:: FromIterator ;
@@ -91,6 +92,9 @@ type AlloyProvider = FillProvider<
9192 AnyNetworkBare ,
9293> ;
9394
95+ const MISSING_TRACE_OUTPUT_ERROR : & str =
96+ "data did not match any variant of untagged enum TraceOutput" ;
97+
9498#[ derive( Clone ) ]
9599pub struct EthereumAdapter {
96100 logger : Logger ,
@@ -256,7 +260,20 @@ impl EthereumAdapter {
256260 let alloy_trace_filter = Self :: build_trace_filter ( from, to, & addresses) ;
257261 let start = Instant :: now ( ) ;
258262
259- let result = self . alloy . trace_filter ( & alloy_trace_filter) . await ;
263+ let result = match self . alloy . trace_filter ( & alloy_trace_filter) . await {
264+ Ok ( traces) => Ok ( traces) ,
265+ Err ( error) if Self :: is_missing_trace_output_error ( & error) => {
266+ warn ! (
267+ & logger,
268+ "trace_filter returned traces with missing result.output; retrying with compatibility parser" ;
269+ "from" => from,
270+ "to" => to,
271+ ) ;
272+ self . trace_filter_with_compat_patch ( & alloy_trace_filter)
273+ . await
274+ }
275+ Err ( error) => Err ( Error :: from ( error) ) ,
276+ } ;
260277
261278 if let Ok ( traces) = & result {
262279 self . log_trace_results ( & logger, from, to, traces. len ( ) ) ;
@@ -271,7 +288,7 @@ impl EthereumAdapter {
271288 & logger,
272289 ) ;
273290
274- result. map_err ( Error :: from )
291+ result
275292 }
276293
277294 fn build_trace_filter (
@@ -313,7 +330,7 @@ impl EthereumAdapter {
313330 & self ,
314331 subgraph_metrics : & Arc < SubgraphEthRpcMetrics > ,
315332 elapsed : f64 ,
316- result : & Result < Vec < LocalizedTransactionTrace > , RpcError < TransportErrorKind > > ,
333+ result : & Result < Vec < LocalizedTransactionTrace > , Error > ,
317334 from : BlockNumber ,
318335 to : BlockNumber ,
319336 logger : & ProviderLogger ,
@@ -332,6 +349,46 @@ impl EthereumAdapter {
332349 }
333350 }
334351
352+ fn is_missing_trace_output_error ( error : & RpcError < TransportErrorKind > ) -> bool {
353+ error. to_string ( ) . contains ( MISSING_TRACE_OUTPUT_ERROR )
354+ }
355+
356+ async fn trace_filter_with_compat_patch (
357+ & self ,
358+ alloy_trace_filter : & AlloyTraceFilter ,
359+ ) -> Result < Vec < LocalizedTransactionTrace > , Error > {
360+ let mut batch = alloy:: rpc:: client:: BatchRequest :: new ( self . alloy . client ( ) ) ;
361+ let trace_future = batch
362+ . add_call :: < ( AlloyTraceFilter , ) , Value > ( "trace_filter" , & ( alloy_trace_filter. clone ( ) , ) )
363+ . map_err ( Error :: from) ?;
364+
365+ batch. send ( ) . await . map_err ( Error :: from) ?;
366+
367+ let mut raw_traces = trace_future. await . map_err ( Error :: from) ?;
368+ Self :: patch_missing_trace_output ( & mut raw_traces) ;
369+
370+ serde_json:: from_value ( raw_traces) . map_err ( Error :: from)
371+ }
372+
373+ fn patch_missing_trace_output ( raw_traces : & mut Value ) {
374+ let Some ( traces) = raw_traces. as_array_mut ( ) else {
375+ return ;
376+ } ;
377+
378+ for trace in traces {
379+ let Some ( result) = trace. get_mut ( "result" ) else {
380+ continue ;
381+ } ;
382+ let Some ( result_obj) = result. as_object_mut ( ) else {
383+ continue ;
384+ } ;
385+
386+ if result_obj. contains_key ( "gasUsed" ) && !result_obj. contains_key ( "output" ) {
387+ result_obj. insert ( "output" . to_owned ( ) , Value :: String ( "0x" . to_owned ( ) ) ) ;
388+ }
389+ }
390+ }
391+
335392 // This is a lazy check for block receipt support. It is only called once and then the result is
336393 // cached. The result is not used for anything critical, so it is fine to be lazy.
337394 async fn check_block_receipt_support_and_update_cache (
@@ -2710,6 +2767,7 @@ mod tests {
27102767 use graph:: blockchain:: BlockPtr ;
27112768 use graph:: components:: ethereum:: AnyNetworkBare ;
27122769 use graph:: prelude:: alloy:: primitives:: { Address , B256 , Bytes } ;
2770+ use graph:: prelude:: alloy:: providers:: ext:: TraceApi ;
27132771 use graph:: prelude:: alloy:: providers:: ProviderBuilder ;
27142772 use graph:: prelude:: alloy:: providers:: mock:: Asserter ;
27152773 use graph:: prelude:: { EthereumCall , LightEthereumBlock , create_minimal_block_for_test} ;
@@ -2861,6 +2919,56 @@ mod tests {
28612919 . unwrap ( ) ;
28622920 }
28632921
2922+ #[ graph:: test]
2923+ async fn missing_output_trace_repro ( ) {
2924+ let trace_filter_response = r#"[{
2925+ "action": {
2926+ "from": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
2927+ "value": "0x0",
2928+ "gas": "0x0",
2929+ "init": "0x",
2930+ "address": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
2931+ "refund_address": "0x4c3ccc98c01103be72bcfd29e1d2454c98d1a6e3",
2932+ "balance": "0x0"
2933+ },
2934+ "blockHash": "0x6b747793a61c3ce4e5f3355cf80edcb6aa465913ed43f4b0136d93803cf330f3",
2935+ "blockNumber": 66762070,
2936+ "result": {
2937+ "gasUsed": "0x0"
2938+ },
2939+ "subtraces": 0,
2940+ "traceAddress": [1, 1],
2941+ "transactionHash": "0x5b3dc50c4c7bd9b0e80469b21febbc5d1b54b364a01b22b1e9c426e4632e0b8f",
2942+ "transactionPosition": 0,
2943+ "type": "suicide"
2944+ }]"# ;
2945+
2946+ let json_value: Value = serde_json:: from_str ( trace_filter_response) . unwrap ( ) ;
2947+ let asserter = Asserter :: new ( ) ;
2948+ let provider = ProviderBuilder :: < _ , _ , AnyNetworkBare > :: default ( )
2949+ . network :: < AnyNetworkBare > ( )
2950+ . connect_mocked_client ( asserter. clone ( ) ) ;
2951+
2952+ asserter. push_success ( & json_value) ;
2953+
2954+ let err = provider
2955+ . trace_filter (
2956+ & graph:: prelude:: alloy:: rpc:: types:: trace:: filter:: TraceFilter :: default ( ) ,
2957+ )
2958+ . await
2959+ . expect_err ( "trace_filter should fail to deserialize when result.output is missing" ) ;
2960+
2961+ assert ! (
2962+ err. to_string( )
2963+ . contains( super :: MISSING_TRACE_OUTPUT_ERROR ) ,
2964+ "unexpected error: {err:#}"
2965+ ) ;
2966+
2967+ let mut patched: Value = serde_json:: from_str ( trace_filter_response) . unwrap ( ) ;
2968+ super :: EthereumAdapter :: patch_missing_trace_output ( & mut patched) ;
2969+ assert_eq ! ( patched[ 0 ] [ "result" ] [ "output" ] , Value :: String ( "0x" . to_string( ) ) ) ;
2970+ }
2971+
28642972 #[ test]
28652973 fn parse_block_triggers_specific_call_not_found ( ) {
28662974 let block = create_minimal_block_for_test ( 2 , hash ( 2 ) ) ;
0 commit comments