From 2ac7b54be5eff14aeefb22f5201c96a88dd2dc61 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:58:52 +0100 Subject: [PATCH 01/13] First version of optional event processing on historical indexing --- codegenerator/cli/npm/envio/evm.schema.json | 7 +++++++ codegenerator/cli/npm/envio/fuel.schema.json | 7 +++++++ codegenerator/cli/npm/envio/src/Internal.res | 2 ++ codegenerator/cli/src/cli_args/init_config.rs | 1 + .../cli_args/interactive_init/fuel_prompts.rs | 2 ++ .../src/config_parsing/graph_migration/mod.rs | 1 + .../cli/src/config_parsing/human_config.rs | 18 ++++++++++++++++++ .../cli/src/config_parsing/system_config.rs | 7 +++++++ .../src/hbs_templating/codegen_templates.rs | 9 ++++++++- .../codegen/src/eventFetching/ChainFetcher.res | 14 ++++++++++++-- scenarios/test_codegen/config.yaml | 2 ++ scenarios/test_codegen/pnpm-lock.yaml | 4 ++-- 12 files changed, 69 insertions(+), 5 deletions(-) diff --git a/codegenerator/cli/npm/envio/evm.schema.json b/codegenerator/cli/npm/envio/evm.schema.json index abc00c1bf..1e07c9910 100644 --- a/codegenerator/cli/npm/envio/evm.schema.json +++ b/codegenerator/cli/npm/envio/evm.schema.json @@ -204,6 +204,13 @@ "type": "null" } ] + }, + "only_when_ready": { + "description": "If true, this event will only be tracked when the chain is ready (after historical backfill is complete). No queries will be made for this event during historical sync. Useful for speeding up indexing when historical data is not needed. (default: false)", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false, diff --git a/codegenerator/cli/npm/envio/fuel.schema.json b/codegenerator/cli/npm/envio/fuel.schema.json index 3851fd8c4..1e37a3e24 100644 --- a/codegenerator/cli/npm/envio/fuel.schema.json +++ b/codegenerator/cli/npm/envio/fuel.schema.json @@ -142,6 +142,13 @@ "string", "null" ] + }, + "onlyWhenReady": { + "description": "If true, this event will only be tracked when the chain is ready (after historical backfill is complete). No queries will be made for this event during historical sync. Useful for speeding up indexing when historical data is not needed. (default: false)", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false, diff --git a/codegenerator/cli/npm/envio/src/Internal.res b/codegenerator/cli/npm/envio/src/Internal.res index 0749b76ad..a1aa001cc 100644 --- a/codegenerator/cli/npm/envio/src/Internal.res +++ b/codegenerator/cli/npm/envio/src/Internal.res @@ -103,6 +103,8 @@ type eventConfig = private { handler: option, contractRegister: option, paramsRawEventSchema: S.schema, + // If true, this event will only be tracked when the chain is ready (after historical backfill) + onlyWhenReady: bool, } type fuelEventKind = diff --git a/codegenerator/cli/src/cli_args/init_config.rs b/codegenerator/cli/src/cli_args/init_config.rs index d4df21e05..24b07a4b5 100644 --- a/codegenerator/cli/src/cli_args/init_config.rs +++ b/codegenerator/cli/src/cli_args/init_config.rs @@ -58,6 +58,7 @@ pub mod evm { event: EvmAbi::event_signature_from_abi_event(&event), name: None, field_selection: None, + only_when_ready: None, }) .collect(); diff --git a/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs b/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs index 5e01498b1..345ac1b1c 100644 --- a/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs +++ b/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs @@ -134,6 +134,7 @@ async fn get_contract_import_selection(args: ContractImportArgs) -> Result Result, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[schemars( + description = "If true, this event will only be tracked when the chain is ready (after \ + historical backfill is complete). No queries will be made for this event \ + during historical sync. Useful for speeding up indexing when historical \ + data is not needed. (default: false)" + )] + pub only_when_ready: Option, } } @@ -701,6 +710,15 @@ pub mod fuel { logged struct/enum name." )] pub log_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[schemars( + description = "If true, this event will only be tracked when the chain is ready (after \ + historical backfill is complete). No queries will be made for this event \ + during historical sync. Useful for speeding up indexing when historical \ + data is not needed. (default: false)" + )] + pub only_when_ready: Option, } } diff --git a/codegenerator/cli/src/config_parsing/system_config.rs b/codegenerator/cli/src/config_parsing/system_config.rs index 706447a33..fef87ce60 100644 --- a/codegenerator/cli/src/config_parsing/system_config.rs +++ b/codegenerator/cli/src/config_parsing/system_config.rs @@ -1244,6 +1244,7 @@ pub struct Event { pub name: String, pub sighash: String, pub field_selection: Option, + pub only_when_ready: bool, } impl Event { @@ -1335,6 +1336,7 @@ impl Event { } None => None, }, + only_when_ready: event_config.only_when_ready.unwrap_or(false), }) } @@ -1414,6 +1416,7 @@ impl Event { kind: EventKind::Fuel(FuelEventKind::LogData(log.data_type)), sighash: log.id, field_selection: None, + only_when_ready: event_config.only_when_ready.unwrap_or(false), } } EventType::Mint => Event { @@ -1421,24 +1424,28 @@ impl Event { kind: EventKind::Fuel(FuelEventKind::Mint), sighash: "mint".to_string(), field_selection: None, + only_when_ready: event_config.only_when_ready.unwrap_or(false), }, EventType::Burn => Event { name: event_config.name.clone(), kind: EventKind::Fuel(FuelEventKind::Burn), sighash: "burn".to_string(), field_selection: None, + only_when_ready: event_config.only_when_ready.unwrap_or(false), }, EventType::Transfer => Event { name: event_config.name.clone(), kind: EventKind::Fuel(FuelEventKind::Transfer), sighash: "transfer".to_string(), field_selection: None, + only_when_ready: event_config.only_when_ready.unwrap_or(false), }, EventType::Call => Event { name: event_config.name.clone(), kind: EventKind::Fuel(FuelEventKind::Call), sighash: "call".to_string(), field_selection: None, + only_when_ready: event_config.only_when_ready.unwrap_or(false), }, }; diff --git a/codegenerator/cli/src/hbs_templating/codegen_templates.rs b/codegenerator/cli/src/hbs_templating/codegen_templates.rs index e128b30a0..bc41765b6 100644 --- a/codegenerator/cli/src/hbs_templating/codegen_templates.rs +++ b/codegenerator/cli/src/hbs_templating/codegen_templates.rs @@ -338,6 +338,7 @@ pub struct EventMod { pub custom_field_selection: Option, pub fuel_event_kind: Option, pub preload_handlers: bool, + pub only_when_ready: bool, } impl Display for EventMod { @@ -417,6 +418,7 @@ impl EventMod { ), }; + let only_when_ready_code = if self.only_when_ready { "true" } else { "false" }; let base_event_config_code = format!( r#"id, name, @@ -424,7 +426,8 @@ impl EventMod { isWildcard: (handlerRegister->EventRegister.isWildcard), handler: handlerRegister->EventRegister.getHandler, contractRegister: handlerRegister->EventRegister.getContractRegister, - paramsRawEventSchema: paramsRawEventSchema->(Utils.magic: S.t => S.t),"# + paramsRawEventSchema: paramsRawEventSchema->(Utils.magic: S.t => S.t), + onlyWhenReady: {only_when_ready_code},"# ); let non_event_mod_code = match fuel_event_kind_code { @@ -655,6 +658,7 @@ impl EventTemplate { custom_field_selection: config_event.field_selection.clone(), fuel_event_kind: Some(fuel_event_kind), preload_handlers: preload_handlers, + only_when_ready: config_event.only_when_ready, }; EventTemplate { name: event_name, @@ -682,6 +686,7 @@ impl EventTemplate { custom_field_selection: config_event.field_selection.clone(), fuel_event_kind: Some(fuel_event_kind), preload_handlers: preload_handlers, + only_when_ready: config_event.only_when_ready, }; EventTemplate { name: event_name, @@ -745,6 +750,7 @@ impl EventTemplate { custom_field_selection: config_event.field_selection.clone(), fuel_event_kind: None, preload_handlers: preload_handlers, + only_when_ready: config_event.only_when_ready, }; Ok(EventTemplate { @@ -773,6 +779,7 @@ impl EventTemplate { custom_field_selection: config_event.field_selection.clone(), fuel_event_kind: Some(fuel_event_kind), preload_handlers: preload_handlers, + only_when_ready: config_event.only_when_ready, }; Ok(EventTemplate { diff --git a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res index 312cb2a37..c6cdadbe4 100644 --- a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res +++ b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res @@ -55,11 +55,14 @@ let make = ( let notRegisteredEvents = [] + // Check if the chain is ready (has caught up to head or endblock) + let isReady = timestampCaughtUpToHeadOrEndblock !== None + chainConfig.contracts->Array.forEach(contract => { let contractName = contract.name contract.events->Array.forEach(eventConfig => { - let {isWildcard} = eventConfig + let {isWildcard, onlyWhenReady} = eventConfig let hasContractRegister = eventConfig.contractRegister->Option.isSome // Should validate the events @@ -74,6 +77,7 @@ let make = ( // Filter out non-preRegistration events on preRegistration phase // so we don't care about it in fetch state and workers anymore + // Also filter out events that are only tracked when ready if the chain is not ready yet let shouldBeIncluded = if config.enableRawEvents { true } else { @@ -81,7 +85,13 @@ let make = ( if !isRegistered { notRegisteredEvents->Array.push(eventConfig) } - isRegistered + // Skip events marked as onlyWhenReady if the chain is not ready yet + let shouldIncludeBasedOnReadiness = if onlyWhenReady && !isReady { + false + } else { + true + } + isRegistered && shouldIncludeBasedOnReadiness } if shouldBeIncluded { diff --git a/scenarios/test_codegen/config.yaml b/scenarios/test_codegen/config.yaml index b5cbb282f..74a329813 100644 --- a/scenarios/test_codegen/config.yaml +++ b/scenarios/test_codegen/config.yaml @@ -12,6 +12,8 @@ contracts: handler: ./src/EventHandlers.res.js events: - event: "EmptyEvent()" + - event: "OnlyWhenReadyEvent()" + only_when_ready: true - name: EventFiltersTest handler: ./src/EventHandlers.res.js events: diff --git a/scenarios/test_codegen/pnpm-lock.yaml b/scenarios/test_codegen/pnpm-lock.yaml index 60f01d22d..1a60d11d2 100644 --- a/scenarios/test_codegen/pnpm-lock.yaml +++ b/scenarios/test_codegen/pnpm-lock.yaml @@ -143,7 +143,7 @@ importers: specifier: 16.4.5 version: 16.4.5 envio: - specifier: file:/Users/dzakh/code/envio/hyperindex/codegenerator/target/debug/envio/../../../cli/npm/envio + specifier: file:/Users/enguerrand/dev/nim/hyperindex/codegenerator/target/debug/envio/../../../cli/npm/envio version: file:../../codegenerator/cli/npm/envio(typescript@5.5.4) ethers: specifier: 6.8.0 @@ -1644,7 +1644,7 @@ packages: envio@file:../../codegenerator/cli/npm/envio: resolution: {directory: ../../codegenerator/cli/npm/envio, type: directory} - engines: {node: '>=22.0.0'} + engines: {node: '>=22.0.0 <=22.10.0'} hasBin: true error-ex@1.3.2: From b98fd35b1991b4c655d787abb86edba64929e0f4 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:17:08 +0100 Subject: [PATCH 02/13] Fixed tests. Added new test file --- .../cli/src/config_parsing/human_config.rs | 2 + .../src/hbs_templating/codegen_templates.rs | 5 + .../test_codegen/test/OnlyWhenReady_test.res | 247 ++++++++++++++++++ scenarios/test_codegen/test/helpers/Mock.res | 2 + 4 files changed, 256 insertions(+) create mode 100644 scenarios/test_codegen/test/OnlyWhenReady_test.res diff --git a/codegenerator/cli/src/config_parsing/human_config.rs b/codegenerator/cli/src/config_parsing/human_config.rs index 2fa0cc01e..dd3933f7e 100644 --- a/codegenerator/cli/src/config_parsing/human_config.rs +++ b/codegenerator/cli/src/config_parsing/human_config.rs @@ -944,11 +944,13 @@ address: ["0x2E645469f354BB4F5c8a05B3b30A929361cf77eC"] name: "NewGreeting".to_string(), log_id: None, type_: None, + only_when_ready: None, }, fuel::EventConfig { name: "ClearGreeting".to_string(), log_id: None, type_: None, + only_when_ready: None, }, ], }), diff --git a/codegenerator/cli/src/hbs_templating/codegen_templates.rs b/codegenerator/cli/src/hbs_templating/codegen_templates.rs index bc41765b6..684f1af10 100644 --- a/codegenerator/cli/src/hbs_templating/codegen_templates.rs +++ b/codegenerator/cli/src/hbs_templating/codegen_templates.rs @@ -1782,6 +1782,7 @@ let register = (): Internal.evmEventConfig => {{ handler: handlerRegister->EventRegister.getHandler, contractRegister: handlerRegister->EventRegister.getContractRegister, paramsRawEventSchema: paramsRawEventSchema->(Utils.magic: S.t => S.t), + onlyWhenReady: false, }} }}"# ), @@ -1797,6 +1798,7 @@ let register = (): Internal.evmEventConfig => {{ sighash: "0x50f7d27e90d1a5a38aeed4ceced2e8ec1ff185737aca96d15791b470d3f17363" .to_string(), field_selection: None, + only_when_ready: false, }, false, ) @@ -1877,6 +1879,7 @@ let register = (): Internal.evmEventConfig => { handler: handlerRegister->EventRegister.getHandler, contractRegister: handlerRegister->EventRegister.getContractRegister, paramsRawEventSchema: paramsRawEventSchema->(Utils.magic: S.t => S.t), + onlyWhenReady: false, } }"#.to_string(), } @@ -1898,6 +1901,7 @@ let register = (): Internal.evmEventConfig => { data_type: RescriptTypeIdent::option(RescriptTypeIdent::Address), }], }), + only_when_ready: false, }, false, ) @@ -1978,6 +1982,7 @@ let register = (): Internal.evmEventConfig => { handler: handlerRegister->EventRegister.getHandler, contractRegister: handlerRegister->EventRegister.getContractRegister, paramsRawEventSchema: paramsRawEventSchema->(Utils.magic: S.t => S.t), + onlyWhenReady: false, } }"#.to_string(), } diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res new file mode 100644 index 000000000..a6955a13a --- /dev/null +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -0,0 +1,247 @@ +open Belt +open RescriptMocha + +describe("OnlyWhenReady Event Filtering", () => { + let chainId = 1337 + let mockAddress = TestHelpers.Addresses.mockAddresses[0]->Option.getExn + + let makeChainConfig = ( + ~eventConfigs: array, + ~startBlock=0, + ): Config.chain => { + { + id: chainId, + startBlock, + endBlock: None, + confirmedBlockThreshold: 0, + syncSource: Config.HyperSync({ + endpointUrl: "http://localhost:8080", + }), + sources: [ + Config.HyperSync({ + endpointUrl: "http://localhost:8080", + }), + ], + maxReorgDepth: 100, + contracts: [ + { + name: "TestContract", + addresses: [mockAddress], + events: eventConfigs, + startBlock: None, + }, + ], + } + } + + let makeConfig = (~enableRawEvents=false): Config.t => { + { + shouldRollbackOnReorg: true, + isUnorderedMultichainMode: false, + enableRawEvents, + maxAddrInPartition: 100, + syncConfig: Rpc, + dbWriteBatchSize: 100, + numEventsProcessedUntilThrottle: 1000, + eventBatchSize: 1000, + defaultStartBlock: 0, + chainMap: ChainMap.empty(), + } + } + + let makeRegistrations = (): EventRegister.registrations => { + { + onBlockByChainId: Js.Dict.empty(), + } + } + + describe("ChainFetcher event filtering based on onlyWhenReady", () => { + it("should filter out onlyWhenReady events when chain is not ready", () => { + let regularEvent = (Mock.evmEventConfig( + ~id="regular", + ~contractName="TestContract", + ~onlyWhenReady=false, + ) :> Internal.eventConfig) + + let onlyWhenReadyEvent = (Mock.evmEventConfig( + ~id="onlyWhenReady", + ~contractName="TestContract", + ~onlyWhenReady=true, + ) :> Internal.eventConfig) + + let chainConfig = makeChainConfig(~eventConfigs=[regularEvent, onlyWhenReadyEvent]) + let config = makeConfig() + let registrations = makeRegistrations() + + // Chain is NOT ready (timestampCaughtUpToHeadOrEndblock = None) + let chainFetcher = ChainFetcher.make( + ~chainConfig, + ~config, + ~registrations, + ~dynamicContracts=[], + ~startBlock=0, + ~endBlock=None, + ~firstEventBlockNumber=None, + ~progressBlockNumber=-1, + ~timestampCaughtUpToHeadOrEndblock=None, // Chain not ready + ~numEventsProcessed=0, + ~numBatchesFetched=0, + ~targetBufferSize=5000, + ~logger=Logging.logger, + ~isInReorgThreshold=false, + ~reorgCheckpoints=[], + ~maxReorgDepth=100, + ) + + // Only the regular event should be included + let eventConfigs = chainFetcher.fetchState.eventConfigs + Assert.deep_equal(eventConfigs->Array.length, 1) + Assert.deep_equal(eventConfigs[0]->Option.getExn.id, "regular") + }) + + it("should include onlyWhenReady events when chain is ready", () => { + let regularEvent = (Mock.evmEventConfig( + ~id="regular", + ~contractName="TestContract", + ~onlyWhenReady=false, + ) :> Internal.eventConfig) + + let onlyWhenReadyEvent = (Mock.evmEventConfig( + ~id="onlyWhenReady", + ~contractName="TestContract", + ~onlyWhenReady=true, + ) :> Internal.eventConfig) + + let chainConfig = makeChainConfig(~eventConfigs=[regularEvent, onlyWhenReadyEvent]) + let config = makeConfig() + let registrations = makeRegistrations() + + // Chain IS ready (timestampCaughtUpToHeadOrEndblock = Some) + let chainFetcher = ChainFetcher.make( + ~chainConfig, + ~config, + ~registrations, + ~dynamicContracts=[], + ~startBlock=0, + ~endBlock=None, + ~firstEventBlockNumber=None, + ~progressBlockNumber=-1, + ~timestampCaughtUpToHeadOrEndblock=Some(Js.Date.make()), // Chain ready + ~numEventsProcessed=0, + ~numBatchesFetched=0, + ~targetBufferSize=5000, + ~logger=Logging.logger, + ~isInReorgThreshold=false, + ~reorgCheckpoints=[], + ~maxReorgDepth=100, + ) + + // Both events should be included + let eventConfigs = chainFetcher.fetchState.eventConfigs + Assert.deep_equal(eventConfigs->Array.length, 2) + + // Verify both event IDs are present + let eventIds = eventConfigs->Array.map(ec => ec.id)->Array.sort((a, b) => { + if a < b { -1 } else if a > b { 1 } else { 0 } + }) + Assert.deep_equal(eventIds, ["onlyWhenReady", "regular"]) + }) + + it("should always include regular events regardless of ready state", () => { + let regularEvent1 = (Mock.evmEventConfig( + ~id="regular1", + ~contractName="TestContract", + ~onlyWhenReady=false, + ) :> Internal.eventConfig) + + let regularEvent2 = (Mock.evmEventConfig( + ~id="regular2", + ~contractName="TestContract", + ~onlyWhenReady=false, + ) :> Internal.eventConfig) + + let chainConfig = makeChainConfig(~eventConfigs=[regularEvent1, regularEvent2]) + let config = makeConfig() + let registrations = makeRegistrations() + + // Test with chain NOT ready + let chainFetcherNotReady = ChainFetcher.make( + ~chainConfig, + ~config, + ~registrations, + ~dynamicContracts=[], + ~startBlock=0, + ~endBlock=None, + ~firstEventBlockNumber=None, + ~progressBlockNumber=-1, + ~timestampCaughtUpToHeadOrEndblock=None, + ~numEventsProcessed=0, + ~numBatchesFetched=0, + ~targetBufferSize=5000, + ~logger=Logging.logger, + ~isInReorgThreshold=false, + ~reorgCheckpoints=[], + ~maxReorgDepth=100, + ) + + Assert.deep_equal(chainFetcherNotReady.fetchState.eventConfigs->Array.length, 2) + + // Test with chain ready + let chainFetcherReady = ChainFetcher.make( + ~chainConfig, + ~config, + ~registrations, + ~dynamicContracts=[], + ~startBlock=0, + ~endBlock=None, + ~firstEventBlockNumber=None, + ~progressBlockNumber=-1, + ~timestampCaughtUpToHeadOrEndblock=Some(Js.Date.make()), + ~numEventsProcessed=0, + ~numBatchesFetched=0, + ~targetBufferSize=5000, + ~logger=Logging.logger, + ~isInReorgThreshold=false, + ~reorgCheckpoints=[], + ~maxReorgDepth=100, + ) + + Assert.deep_equal(chainFetcherReady.fetchState.eventConfigs->Array.length, 2) + }) + + it("should work correctly with enableRawEvents mode", () => { + let onlyWhenReadyEvent = (Mock.evmEventConfig( + ~id="onlyWhenReady", + ~contractName="TestContract", + ~onlyWhenReady=true, + ) :> Internal.eventConfig) + + let chainConfig = makeChainConfig(~eventConfigs=[onlyWhenReadyEvent]) + let config = makeConfig(~enableRawEvents=true) + let registrations = makeRegistrations() + + // With raw events enabled, events should be included even if not ready + let chainFetcher = ChainFetcher.make( + ~chainConfig, + ~config, + ~registrations, + ~dynamicContracts=[], + ~startBlock=0, + ~endBlock=None, + ~firstEventBlockNumber=None, + ~progressBlockNumber=-1, + ~timestampCaughtUpToHeadOrEndblock=None, // Not ready + ~numEventsProcessed=0, + ~numBatchesFetched=0, + ~targetBufferSize=5000, + ~logger=Logging.logger, + ~isInReorgThreshold=false, + ~reorgCheckpoints=[], + ~maxReorgDepth=100, + ) + + // Event should be included because raw events mode is enabled + Assert.deep_equal(chainFetcher.fetchState.eventConfigs->Array.length, 1) + }) + }) +}) diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index 653baff92..7924d5446 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -767,6 +767,7 @@ let evmEventConfig = ( ~isWildcard=false, ~dependsOnAddresses=?, ~filterByAddresses=false, + ~onlyWhenReady=false, ): Internal.evmEventConfig => { { id, @@ -781,6 +782,7 @@ let evmEventConfig = ( paramsRawEventSchema: S.literal(%raw(`null`)) ->S.shape(_ => ()) ->(Utils.magic: S.t => S.t), + onlyWhenReady, blockSchema: blockSchema ->Belt.Option.getWithDefault(S.object(_ => ())->Utils.magic) ->Utils.magic, From a63c98ac3ca7854e0ee3e3ff482bcc593375e140 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:21:07 +0100 Subject: [PATCH 03/13] Simplify using demorgan's law --- .../static/codegen/src/eventFetching/ChainFetcher.res | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res index c6cdadbe4..b811fd4cd 100644 --- a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res +++ b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res @@ -86,11 +86,7 @@ let make = ( notRegisteredEvents->Array.push(eventConfig) } // Skip events marked as onlyWhenReady if the chain is not ready yet - let shouldIncludeBasedOnReadiness = if onlyWhenReady && !isReady { - false - } else { - true - } + let shouldIncludeBasedOnReadiness = !onlyWhenReady || isReady isRegistered && shouldIncludeBasedOnReadiness } From a35f5d40c9621bf99559509ab5dd5f97e9a27627 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 09:33:09 +0100 Subject: [PATCH 04/13] Fixed test --- scenarios/test_codegen/test/helpers/Mock.res | 1 + 1 file changed, 1 insertion(+) diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index 7924d5446..586eaeaa7 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -668,6 +668,7 @@ module Source = { blockSchema: S.object(_ => ())->Utils.magic, transactionSchema: S.object(_ => ())->Utils.magic, convertHyperSyncEventArgs: _ => Js.Exn.raiseError("Not implemented"), + onlyWhenReady: false, }: Internal.evmEventConfig :> Internal.eventConfig), timestamp: item.blockNumber, chain, From 45a52a114009718b973059a3b3c7591e38da4b26 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 09:37:18 +0100 Subject: [PATCH 05/13] Configurable only when ready --- scenarios/test_codegen/test/helpers/Mock.res | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index 586eaeaa7..7eec363d1 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -471,6 +471,7 @@ module Source = { logIndex: int, handler?: Types.HandlerTypes.loader, contractRegister?: Types.HandlerTypes.contractRegister, + onlyWhenReady?: bool, } type t = { @@ -668,7 +669,7 @@ module Source = { blockSchema: S.object(_ => ())->Utils.magic, transactionSchema: S.object(_ => ())->Utils.magic, convertHyperSyncEventArgs: _ => Js.Exn.raiseError("Not implemented"), - onlyWhenReady: false, + onlyWhenReady: item.onlyWhenReady->Option.getWithDefault(false), }: Internal.evmEventConfig :> Internal.eventConfig), timestamp: item.blockNumber, chain, From bb2c54486c1f68f738abfa9a70def4afd7758a19 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 10:34:30 +0100 Subject: [PATCH 06/13] Fixed test --- scenarios/test_codegen/test/OnlyWhenReady_test.res | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res index a6955a13a..8671e038d 100644 --- a/scenarios/test_codegen/test/OnlyWhenReady_test.res +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -13,10 +13,6 @@ describe("OnlyWhenReady Event Filtering", () => { id: chainId, startBlock, endBlock: None, - confirmedBlockThreshold: 0, - syncSource: Config.HyperSync({ - endpointUrl: "http://localhost:8080", - }), sources: [ Config.HyperSync({ endpointUrl: "http://localhost:8080", From 08c55a4a7e310f6c2835bdb5c3422db41be3931a Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 11:08:50 +0100 Subject: [PATCH 07/13] Fixed test --- scenarios/test_codegen/test/OnlyWhenReady_test.res | 1 - 1 file changed, 1 deletion(-) diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res index 8671e038d..6e6b9b373 100644 --- a/scenarios/test_codegen/test/OnlyWhenReady_test.res +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -12,7 +12,6 @@ describe("OnlyWhenReady Event Filtering", () => { { id: chainId, startBlock, - endBlock: None, sources: [ Config.HyperSync({ endpointUrl: "http://localhost:8080", From caecaad24378d391cdcbb5d22aef0ca40f1e00f8 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 11:10:17 +0100 Subject: [PATCH 08/13] Proper test file fix --- .../test_codegen/test/OnlyWhenReady_test.res | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res index 6e6b9b373..0177f5408 100644 --- a/scenarios/test_codegen/test/OnlyWhenReady_test.res +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -29,19 +29,20 @@ describe("OnlyWhenReady Event Filtering", () => { } } - let makeConfig = (~enableRawEvents=false): Config.t => { - { - shouldRollbackOnReorg: true, - isUnorderedMultichainMode: false, - enableRawEvents, - maxAddrInPartition: 100, - syncConfig: Rpc, - dbWriteBatchSize: 100, - numEventsProcessedUntilThrottle: 1000, - eventBatchSize: 1000, - defaultStartBlock: 0, - chainMap: ChainMap.empty(), - } + let makeConfig = (~enableRawEvents=false, ~chains=[]): Config.t => { + Config.make( + ~shouldRollbackOnReorg=true, + ~shouldSaveFullHistory=false, + ~chains, + ~enableRawEvents, + ~preloadHandlers=false, + ~ecosystem=Platform.Evm, + ~batchSize=5000, + ~lowercaseAddresses=false, + ~multichain=Config.Unordered, + ~shouldUseHypersyncClientDecoder=true, + ~maxAddrInPartition=100, + ) } let makeRegistrations = (): EventRegister.registrations => { @@ -65,7 +66,7 @@ describe("OnlyWhenReady Event Filtering", () => { ) :> Internal.eventConfig) let chainConfig = makeChainConfig(~eventConfigs=[regularEvent, onlyWhenReadyEvent]) - let config = makeConfig() + let config = makeConfig(~chains=[chainConfig]) let registrations = makeRegistrations() // Chain is NOT ready (timestampCaughtUpToHeadOrEndblock = None) @@ -108,7 +109,7 @@ describe("OnlyWhenReady Event Filtering", () => { ) :> Internal.eventConfig) let chainConfig = makeChainConfig(~eventConfigs=[regularEvent, onlyWhenReadyEvent]) - let config = makeConfig() + let config = makeConfig(~chains=[chainConfig]) let registrations = makeRegistrations() // Chain IS ready (timestampCaughtUpToHeadOrEndblock = Some) @@ -156,7 +157,7 @@ describe("OnlyWhenReady Event Filtering", () => { ) :> Internal.eventConfig) let chainConfig = makeChainConfig(~eventConfigs=[regularEvent1, regularEvent2]) - let config = makeConfig() + let config = makeConfig(~chains=[chainConfig]) let registrations = makeRegistrations() // Test with chain NOT ready @@ -212,7 +213,7 @@ describe("OnlyWhenReady Event Filtering", () => { ) :> Internal.eventConfig) let chainConfig = makeChainConfig(~eventConfigs=[onlyWhenReadyEvent]) - let config = makeConfig(~enableRawEvents=true) + let config = makeConfig(~enableRawEvents=true, ~chains=[chainConfig]) let registrations = makeRegistrations() // With raw events enabled, events should be included even if not ready From 9a8746d633e1746f7e0ebafe2566fcfd43a81e79 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 11:40:06 +0100 Subject: [PATCH 09/13] Revamp test file --- scenarios/test_codegen/test/OnlyWhenReady_test.res | 1 + 1 file changed, 1 insertion(+) diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res index 0177f5408..568e1bd33 100644 --- a/scenarios/test_codegen/test/OnlyWhenReady_test.res +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -21,6 +21,7 @@ describe("OnlyWhenReady Event Filtering", () => { contracts: [ { name: "TestContract", + abi: [], addresses: [mockAddress], events: eventConfigs, startBlock: None, From d012cfc616417dc2b4651eea1ca42313ba7ea51e Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 13:25:30 +0100 Subject: [PATCH 10/13] Fixed test --- scenarios/test_codegen/test/OnlyWhenReady_test.res | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res index 568e1bd33..ae1784f4a 100644 --- a/scenarios/test_codegen/test/OnlyWhenReady_test.res +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -21,7 +21,7 @@ describe("OnlyWhenReady Event Filtering", () => { contracts: [ { name: "TestContract", - abi: [], + abi: Ethers.makeAbi(%raw("[]")), addresses: [mockAddress], events: eventConfigs, startBlock: None, From 28c5233374f4adefc5f2513ee8261f8f5cfa1b02 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 22 Nov 2025 13:43:58 +0100 Subject: [PATCH 11/13] Fixed test --- .../test_codegen/test/OnlyWhenReady_test.res | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res index ae1784f4a..aa3b8f4cd 100644 --- a/scenarios/test_codegen/test/OnlyWhenReady_test.res +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -3,18 +3,45 @@ open RescriptMocha describe("OnlyWhenReady Event Filtering", () => { let chainId = 1337 + let chain = ChainMap.Chain.makeUnsafe(~chainId) let mockAddress = TestHelpers.Addresses.mockAddresses[0]->Option.getExn let makeChainConfig = ( ~eventConfigs: array, ~startBlock=0, ): Config.chain => { + let evmEventConfigs = eventConfigs->( + Utils.magic: array => array + ) + let evmContracts = [ + { + Internal.name: "TestContract", + abi: Ethers.makeAbi(%raw("[]")), + events: evmEventConfigs, + }, + ] { id: chainId, startBlock, sources: [ - Config.HyperSync({ - endpointUrl: "http://localhost:8080", + RpcSource.make({ + chain, + contracts: evmContracts, + sourceFor: Sync, + syncConfig: NetworkSources.getSyncConfig({ + initialBlockInterval: 10000, + backoffMultiplicative: 1.0, + accelerationAdditive: 1000, + intervalCeiling: 10000, + backoffMillis: 1000, + queryTimeoutMillis: 10000, + fallbackStallTimeout: 5000, + }), + url: "http://localhost:8080", + eventRouter: evmEventConfigs->EventRouter.fromEvmEventModsOrThrow(~chain), + shouldUseHypersyncClientDecoder: false, + lowercaseAddresses: false, + allEventSignatures: [], }), ], maxReorgDepth: 100, From bd4d6e8a5f9def9d78a3fe0ea2b02b8507a13350 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:30:02 +0100 Subject: [PATCH 12/13] Block handler should also be able to be called only when ready --- codegenerator/cli/npm/envio/src/EventRegister.res | 3 +++ codegenerator/cli/npm/envio/src/Internal.res | 2 ++ .../codegen/src/eventFetching/ChainFetcher.res | 14 ++++++++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/codegenerator/cli/npm/envio/src/EventRegister.res b/codegenerator/cli/npm/envio/src/EventRegister.res index 32bdd3ed0..26ce38513 100644 --- a/codegenerator/cli/npm/envio/src/EventRegister.res +++ b/codegenerator/cli/npm/envio/src/EventRegister.res @@ -75,6 +75,7 @@ let onBlockOptionsSchema = S.schema(s => "interval": s.matches(S.option(S.int->S.intMin(1))->S.Option.getOr(1)), "startBlock": s.matches(S.option(S.int)), "endBlock": s.matches(S.option(S.int)), + "onlyWhenReady": s.matches(S.option(S.bool)->S.Option.getOr(false)), } ) @@ -123,6 +124,7 @@ let onBlock = (rawOptions: unknown, handler: Internal.onBlockArgs => promise promise, interval: int, handler: onBlockArgs => promise, + // If true, this block handler will only be invoked when the chain is ready (after historical backfill) + onlyWhenReady: bool, } @tag("kind") diff --git a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res index b811fd4cd..efb8899de 100644 --- a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res +++ b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res @@ -132,9 +132,10 @@ let make = ( let onBlockConfigs = registrations.onBlockByChainId->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString) - switch onBlockConfigs { + // Filter out onlyWhenReady block handlers if the chain is not ready yet + let filteredOnBlockConfigs = switch onBlockConfigs { | Some(onBlockConfigs) => - // TODO: Move it to the EventRegister module + // TODO: Move validation to the EventRegister module // so the error is thrown with better stack trace onBlockConfigs->Array.forEach(onBlockConfig => { if onBlockConfig.startBlock->Option.getWithDefault(startBlock) < startBlock { @@ -152,7 +153,12 @@ let make = ( | None => () } }) - | None => () + // Filter out block handlers marked as onlyWhenReady if the chain is not ready yet + let filtered = onBlockConfigs->Array.keep(onBlockConfig => { + !onBlockConfig.onlyWhenReady || isReady + }) + filtered->Utils.Array.notEmpty ? Some(filtered) : None + | None => None } let fetchState = FetchState.make( @@ -169,7 +175,7 @@ let make = ( !config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth, Env.indexingBlockLag->Option.getWithDefault(0), ), - ~onBlockConfigs?, + ~onBlockConfigs=?filteredOnBlockConfigs, ) let chainReorgCheckpoints = reorgCheckpoints->Array.keepMapU(reorgCheckpoint => { From cb2ddd57a52a527c7e84b0b4ea2bda757fb132ab Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:46:02 +0100 Subject: [PATCH 13/13] Make sure to properly cover transition to active chain --- .../cli/npm/envio/src/FetchState.res | 91 +++++++++++++++++++ .../src/eventFetching/ChainFetcher.res | 50 ++++++++++ .../codegen/src/globalState/GlobalState.res | 13 ++- 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/codegenerator/cli/npm/envio/src/FetchState.res b/codegenerator/cli/npm/envio/src/FetchState.res index 672dd6928..1bfc9515d 100644 --- a/codegenerator/cli/npm/envio/src/FetchState.res +++ b/codegenerator/cli/npm/envio/src/FetchState.res @@ -1262,3 +1262,94 @@ let getUnorderedMultichainProgressBlockNumberAt = ({buffer} as fetchState: t, ~i | _ => bufferBlockNumber } } + +/** +Activates deferred events and block handlers when the chain becomes ready. +This is called when a chain transitions from historical sync to realtime. +The deferred events will start being fetched from the current progress block. +*/ +let activateDeferredEventsAndHandlers = ( + fetchState: t, + ~deferredEventConfigs: array, + ~deferredOnBlockConfigs: array, +): t => { + if deferredEventConfigs->Array.length === 0 && deferredOnBlockConfigs->Array.length === 0 { + fetchState + } else { + // Separate events into those that depend on addresses and those that don't + let notDependingOnAddresses = [] + let dependingOnAddresses = [] + + deferredEventConfigs->Array.forEach(ec => { + if ec.dependsOnAddresses { + dependingOnAddresses->Array.push(ec) + } else { + notDependingOnAddresses->Array.push(ec) + } + }) + + // Start from the current latestFullyFetchedBlock + let newPartitions = [] + + // Create partition for events that don't depend on addresses + if notDependingOnAddresses->Array.length > 0 { + newPartitions->Array.push({ + id: (fetchState.nextPartitionIndex + newPartitions->Array.length)->Int.toString, + status: { + fetchingStateId: None, + }, + latestFetchedBlock: fetchState.latestFullyFetchedBlock, + selection: { + dependsOnAddresses: false, + eventConfigs: notDependingOnAddresses, + }, + addressesByContractName: Js.Dict.empty(), + }) + } + + // For events that depend on addresses, we need to: + // 1. Add them to normalSelection + // 2. Add them to existing partitions that depend on addresses + let updatedNormalSelection = if dependingOnAddresses->Array.length > 0 { + { + dependsOnAddresses: true, + eventConfigs: fetchState.normalSelection.eventConfigs->Array.concat(dependingOnAddresses), + } + } else { + fetchState.normalSelection + } + + // Update existing partitions that depend on addresses + let updatedPartitions = if dependingOnAddresses->Array.length > 0 { + fetchState.partitions->Array.map(p => { + if p.selection.dependsOnAddresses { + { + ...p, + selection: { + ...p.selection, + eventConfigs: p.selection.eventConfigs->Array.concat(dependingOnAddresses), + }, + } + } else { + p + } + }) + } else { + fetchState.partitions + } + + // Combine partitions + let allPartitions = updatedPartitions->Array.concat(newPartitions) + + // Add deferred block handlers + let updatedOnBlockConfigs = fetchState.onBlockConfigs->Array.concat(deferredOnBlockConfigs) + + { + ...fetchState, + partitions: allPartitions, + nextPartitionIndex: fetchState.nextPartitionIndex + newPartitions->Array.length, + normalSelection: updatedNormalSelection, + onBlockConfigs: updatedOnBlockConfigs, + } + } +} diff --git a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res index efb8899de..d26a51881 100644 --- a/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res +++ b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res @@ -22,6 +22,9 @@ type t = { numBatchesFetched: int, reorgDetection: ReorgDetection.t, safeCheckpointTracking: option, + // Events and block handlers that should only be activated when the chain is ready + deferredEventConfigs: array, + deferredOnBlockConfigs: array, } //CONSTRUCTION @@ -52,6 +55,8 @@ let make = ( // Aggregate events we want to fetch let contracts = [] let eventConfigs: array = [] + // Events deferred until chain is ready (onlyWhenReady: true) + let deferredEventConfigs: array = [] let notRegisteredEvents = [] @@ -90,8 +95,17 @@ let make = ( isRegistered && shouldIncludeBasedOnReadiness } + // Store deferred events (registered but onlyWhenReady and not ready yet) + let shouldBeDeferred = + !config.enableRawEvents && + (hasContractRegister || eventConfig.handler->Option.isSome) && + onlyWhenReady && + !isReady + if shouldBeIncluded { eventConfigs->Array.push(eventConfig) + } else if shouldBeDeferred { + deferredEventConfigs->Array.push(eventConfig) } }) @@ -133,6 +147,8 @@ let make = ( let onBlockConfigs = registrations.onBlockByChainId->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString) // Filter out onlyWhenReady block handlers if the chain is not ready yet + // and collect deferred block handlers + let deferredOnBlockConfigs: array = [] let filteredOnBlockConfigs = switch onBlockConfigs { | Some(onBlockConfigs) => // TODO: Move validation to the EventRegister module @@ -157,6 +173,14 @@ let make = ( let filtered = onBlockConfigs->Array.keep(onBlockConfig => { !onBlockConfig.onlyWhenReady || isReady }) + // Collect deferred block handlers + if !isReady { + onBlockConfigs->Array.forEach(onBlockConfig => { + if onBlockConfig.onlyWhenReady { + deferredOnBlockConfigs->Array.push(onBlockConfig) + } + }) + } filtered->Utils.Array.notEmpty ? Some(filtered) : None | None => None } @@ -211,6 +235,8 @@ let make = ( timestampCaughtUpToHeadOrEndblock, numEventsProcessed, numBatchesFetched, + deferredEventConfigs, + deferredOnBlockConfigs, } } @@ -474,3 +500,27 @@ let getLastKnownValidBlock = async ( } let isActivelyIndexing = (chainFetcher: t) => chainFetcher.fetchState->FetchState.isActivelyIndexing + +/** +Activates deferred events and block handlers when the chain becomes ready. +Returns the updated ChainFetcher with the deferred events activated and cleared. +*/ +let activateDeferredEventsAndHandlers = (chainFetcher: t): t => { + if ( + chainFetcher.deferredEventConfigs->Array.length === 0 && + chainFetcher.deferredOnBlockConfigs->Array.length === 0 + ) { + chainFetcher + } else { + let updatedFetchState = chainFetcher.fetchState->FetchState.activateDeferredEventsAndHandlers( + ~deferredEventConfigs=chainFetcher.deferredEventConfigs, + ~deferredOnBlockConfigs=chainFetcher.deferredOnBlockConfigs, + ) + { + ...chainFetcher, + fetchState: updatedFetchState, + deferredEventConfigs: [], + deferredOnBlockConfigs: [], + } + } +} diff --git a/codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res b/codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res index 7278d2608..5c9a9a775 100644 --- a/codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res +++ b/codegenerator/cli/templates/static/codegen/src/globalState/GlobalState.res @@ -289,7 +289,10 @@ let updateProgressedChains = ( * The given chain fetcher is fetching at the head or latest processed block >= endblock * The given chain has processed all events on the queue * see https://github.com/Float-Capital/indexer/pull/1388 */ - if cf->ChainFetcher.hasProcessedToEndblock { + // Track if this chain is transitioning to ready state + let wasNotReady = cf.timestampCaughtUpToHeadOrEndblock->Option.isNone + + let updatedCf = if cf->ChainFetcher.hasProcessedToEndblock { // in the case this is already set, don't reset and instead propagate the existing value let timestampCaughtUpToHeadOrEndblock = cf.timestampCaughtUpToHeadOrEndblock->Option.isSome @@ -330,6 +333,14 @@ let updateProgressedChains = ( //Default to just returning cf cf } + + // If transitioning to ready state, activate deferred events and handlers + let isNowReady = updatedCf.timestampCaughtUpToHeadOrEndblock->Option.isSome + if wasNotReady && isNowReady { + updatedCf->ChainFetcher.activateDeferredEventsAndHandlers + } else { + updatedCf + } }) let allChainsSyncedAtHead =