@@ -1016,31 +1016,16 @@ impl EthereumAdapter {
10161016 + ' _ ,
10171017 > ,
10181018 > {
1019- // Create a HashMap of block numbers to Vec<EthereumBlockTriggerType>
1019+ // Create a HashMap of block numbers to Vec<EthereumBlockTriggerType>.
10201020 let matching_blocks = ( from..=to)
10211021 . filter_map ( |block_number| {
1022- filter
1023- . polling_intervals
1024- . iter ( )
1025- . find_map ( |( start_block, interval) | {
1026- let has_once_trigger = ( * interval == 0 ) && ( block_number == * start_block) ;
1027- let has_polling_trigger = block_number >= * start_block
1028- && * interval > 0
1029- && ( ( block_number - start_block) % * interval) == 0 ;
1030-
1031- if has_once_trigger || has_polling_trigger {
1032- let mut triggers = Vec :: new ( ) ;
1033- if has_once_trigger {
1034- triggers. push ( EthereumBlockTriggerType :: Start ) ;
1035- }
1036- if has_polling_trigger {
1037- triggers. push ( EthereumBlockTriggerType :: End ) ;
1038- }
1039- Some ( ( block_number, triggers) )
1040- } else {
1041- None
1042- }
1043- } )
1022+ let triggers =
1023+ block_trigger_types_from_intervals ( block_number, & filter. polling_intervals ) ;
1024+ if triggers. is_empty ( ) {
1025+ None
1026+ } else {
1027+ Some ( ( block_number, triggers) )
1028+ }
10441029 } )
10451030 . collect :: < HashMap < _ , _ > > ( ) ;
10461031
@@ -2009,6 +1994,36 @@ pub(crate) fn parse_call_triggers(
20091994 }
20101995}
20111996
1997+ /// For a given `block_number`, return the block trigger types that fire
1998+ /// based on the rules in `polling_intervals`. Each entry is `(start_block,
1999+ /// interval)` where `interval == 0` encodes a `once` rule and `interval > 0`
2000+ /// encodes a `polling every interval` rule. Both rule kinds can fire at the
2001+ /// same block (e.g. a once and polling rule sharing a `start_block`), so the
2002+ /// returned Vec may contain `Start`, `End`, both, or neither.
2003+ pub ( crate ) fn block_trigger_types_from_intervals (
2004+ block_number : i32 ,
2005+ polling_intervals : & HashSet < ( i32 , i32 ) > ,
2006+ ) -> Vec < EthereumBlockTriggerType > {
2007+ let has_once_trigger = polling_intervals
2008+ . iter ( )
2009+ . any ( |( start_block, interval) | * interval == 0 && block_number == * start_block) ;
2010+
2011+ let has_polling_trigger = polling_intervals. iter ( ) . any ( |( start_block, interval) | {
2012+ * interval > 0
2013+ && block_number >= * start_block
2014+ && ( block_number - start_block) % * interval == 0
2015+ } ) ;
2016+
2017+ let mut triggers = Vec :: new ( ) ;
2018+ if has_once_trigger {
2019+ triggers. push ( EthereumBlockTriggerType :: Start ) ;
2020+ }
2021+ if has_polling_trigger {
2022+ triggers. push ( EthereumBlockTriggerType :: End ) ;
2023+ }
2024+ triggers
2025+ }
2026+
20122027/// This method does not parse block triggers with `once` filters.
20132028/// This is because it is to be run before any other triggers are run.
20142029/// So we have `parse_initialization_triggers` for that.
@@ -2050,38 +2065,12 @@ pub(crate) fn parse_block_triggers(
20502065 EthereumBlockTriggerType :: End ,
20512066 ) ) ;
20522067 } else if !block_filter. polling_intervals . is_empty ( ) {
2053- let has_polling_trigger =
2054- & block_filter
2055- . polling_intervals
2056- . iter ( )
2057- . any ( |( start_block, interval) | match interval {
2058- 0 => false ,
2059- _ => {
2060- block_number >= * start_block
2061- && ( block_number - * start_block) % * interval == 0
2062- }
2063- } ) ;
2064-
2065- let has_once_trigger =
2066- & block_filter
2067- . polling_intervals
2068- . iter ( )
2069- . any ( |( start_block, interval) | match interval {
2070- 0 => block_number == * start_block,
2071- _ => false ,
2072- } ) ;
2073-
2074- if * has_once_trigger {
2075- triggers. push ( EthereumTrigger :: Block (
2076- block_ptr3. clone ( ) ,
2077- EthereumBlockTriggerType :: Start ,
2078- ) ) ;
2079- }
2080-
2081- if * has_polling_trigger {
2068+ for trigger_type in
2069+ block_trigger_types_from_intervals ( block_number, & block_filter. polling_intervals )
2070+ {
20822071 triggers. push ( EthereumTrigger :: Block (
2083- block_ptr3,
2084- EthereumBlockTriggerType :: End ,
2072+ block_ptr3. cheap_clone ( ) ,
2073+ trigger_type ,
20852074 ) ) ;
20862075 }
20872076 }
@@ -2704,8 +2693,8 @@ mod tests {
27042693 use crate :: trigger:: { EthereumBlockTriggerType , EthereumTrigger } ;
27052694
27062695 use super :: {
2707- EthereumBlock , EthereumBlockFilter , EthereumBlockWithCalls , check_block_receipt_support ,
2708- parse_block_triggers,
2696+ EthereumBlock , EthereumBlockFilter , EthereumBlockWithCalls ,
2697+ block_trigger_types_from_intervals , check_block_receipt_support , parse_block_triggers,
27092698 } ;
27102699 use graph:: blockchain:: BlockPtr ;
27112700 use graph:: components:: ethereum:: AnyNetworkBare ;
@@ -2926,6 +2915,115 @@ mod tests {
29262915 ) ;
29272916 }
29282917
2918+ #[ test]
2919+ fn block_trigger_types_once_only ( ) {
2920+ let intervals = HashSet :: from_iter ( vec ! [ ( 100 , 0 ) ] ) ;
2921+
2922+ assert_eq ! (
2923+ vec![ EthereumBlockTriggerType :: Start ] ,
2924+ block_trigger_types_from_intervals( 100 , & intervals) ,
2925+ "once rule fires Start at start_block"
2926+ ) ;
2927+ assert_eq ! (
2928+ Vec :: <EthereumBlockTriggerType >:: new( ) ,
2929+ block_trigger_types_from_intervals( 99 , & intervals) ,
2930+ "once rule does not fire before start_block"
2931+ ) ;
2932+ assert_eq ! (
2933+ Vec :: <EthereumBlockTriggerType >:: new( ) ,
2934+ block_trigger_types_from_intervals( 101 , & intervals) ,
2935+ "once rule does not fire after start_block"
2936+ ) ;
2937+ }
2938+
2939+ #[ test]
2940+ fn block_trigger_types_polling_only ( ) {
2941+ let intervals = HashSet :: from_iter ( vec ! [ ( 100 , 10 ) ] ) ;
2942+
2943+ assert_eq ! (
2944+ vec![ EthereumBlockTriggerType :: End ] ,
2945+ block_trigger_types_from_intervals( 100 , & intervals) ,
2946+ "polling rule fires End at start_block"
2947+ ) ;
2948+ assert_eq ! (
2949+ vec![ EthereumBlockTriggerType :: End ] ,
2950+ block_trigger_types_from_intervals( 110 , & intervals) ,
2951+ "polling rule fires End at start_block + interval"
2952+ ) ;
2953+ assert_eq ! (
2954+ Vec :: <EthereumBlockTriggerType >:: new( ) ,
2955+ block_trigger_types_from_intervals( 105 , & intervals) ,
2956+ "polling rule does not fire off-interval"
2957+ ) ;
2958+ assert_eq ! (
2959+ Vec :: <EthereumBlockTriggerType >:: new( ) ,
2960+ block_trigger_types_from_intervals( 90 , & intervals) ,
2961+ "polling rule does not fire before start_block"
2962+ ) ;
2963+ }
2964+
2965+ #[ test]
2966+ fn block_trigger_types_once_and_polling_same_start_block ( ) {
2967+ // A single data source with both a `once` handler and a `polling` handler
2968+ // contributes both entries at its start_block. Both must fire at start_block.
2969+ let intervals = HashSet :: from_iter ( vec ! [ ( 100 , 0 ) , ( 100 , 10 ) ] ) ;
2970+
2971+ assert_eq ! (
2972+ vec![
2973+ EthereumBlockTriggerType :: Start ,
2974+ EthereumBlockTriggerType :: End ,
2975+ ] ,
2976+ block_trigger_types_from_intervals( 100 , & intervals) ,
2977+ "both Start and End should fire when once and polling rules share start_block"
2978+ ) ;
2979+ assert_eq ! (
2980+ vec![ EthereumBlockTriggerType :: End ] ,
2981+ block_trigger_types_from_intervals( 110 , & intervals) ,
2982+ "only polling fires at later interval matches"
2983+ ) ;
2984+ }
2985+
2986+ #[ test]
2987+ fn block_trigger_types_cross_datasource_collision ( ) {
2988+ // Two data sources: DS-A with once at 100, DS-B with polling every 10 from 50.
2989+ // Block 100 satisfies DS-A's once rule and also (100-50) % 10 == 0, so both fire.
2990+ let intervals = HashSet :: from_iter ( vec ! [ ( 100 , 0 ) , ( 50 , 10 ) ] ) ;
2991+
2992+ assert_eq ! (
2993+ vec![
2994+ EthereumBlockTriggerType :: Start ,
2995+ EthereumBlockTriggerType :: End ,
2996+ ] ,
2997+ block_trigger_types_from_intervals( 100 , & intervals) ,
2998+ "both triggers fire when a once rule and an unrelated polling rule collide"
2999+ ) ;
3000+ assert_eq ! (
3001+ vec![ EthereumBlockTriggerType :: End ] ,
3002+ block_trigger_types_from_intervals( 60 , & intervals) ,
3003+ "only polling fires at a block where only the polling rule matches"
3004+ ) ;
3005+ }
3006+
3007+ #[ test]
3008+ fn block_trigger_types_no_match ( ) {
3009+ let intervals = HashSet :: from_iter ( vec ! [ ( 100 , 0 ) , ( 100 , 10 ) ] ) ;
3010+ assert_eq ! (
3011+ Vec :: <EthereumBlockTriggerType >:: new( ) ,
3012+ block_trigger_types_from_intervals( 99 , & intervals) ,
3013+ "no triggers when block matches neither rule"
3014+ ) ;
3015+ }
3016+
3017+ #[ test]
3018+ fn block_trigger_types_empty_intervals ( ) {
3019+ let intervals: HashSet < ( i32 , i32 ) > = HashSet :: new ( ) ;
3020+ assert_eq ! (
3021+ Vec :: <EthereumBlockTriggerType >:: new( ) ,
3022+ block_trigger_types_from_intervals( 100 , & intervals) ,
3023+ "empty intervals yields no triggers"
3024+ ) ;
3025+ }
3026+
29293027 fn address ( id : u64 ) -> Address {
29303028 Address :: left_padding_from ( & id. to_be_bytes ( ) )
29313029 }
0 commit comments