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/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 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/npm/envio/src/Internal.res b/codegenerator/cli/npm/envio/src/Internal.res index 0749b76ad..1e4196580 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 = @@ -187,6 +189,8 @@ type onBlockConfig = { endBlock: option, 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/src/cli_args/init_config.rs b/codegenerator/cli/src/cli_args/init_config.rs index a3047e35c..9b50a4609 100644 --- a/codegenerator/cli/src/cli_args/init_config.rs +++ b/codegenerator/cli/src/cli_args/init_config.rs @@ -60,6 +60,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, } } @@ -926,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/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..684f1af10 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 { @@ -1775,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, }} }}"# ), @@ -1790,6 +1798,7 @@ let register = (): Internal.evmEventConfig => {{ sighash: "0x50f7d27e90d1a5a38aeed4ceced2e8ec1ff185737aca96d15791b470d3f17363" .to_string(), field_selection: None, + only_when_ready: false, }, false, ) @@ -1870,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(), } @@ -1891,6 +1901,7 @@ let register = (): Internal.evmEventConfig => { data_type: RescriptTypeIdent::option(RescriptTypeIdent::Address), }], }), + only_when_ready: false, }, false, ) @@ -1971,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/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res b/codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res index 312cb2a37..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,14 +55,19 @@ 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 = [] + // 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 +82,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,11 +90,22 @@ let make = ( if !isRegistered { notRegisteredEvents->Array.push(eventConfig) } - isRegistered + // Skip events marked as onlyWhenReady if the chain is not ready yet + let shouldIncludeBasedOnReadiness = !onlyWhenReady || isReady + 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) } }) @@ -126,9 +146,12 @@ 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 + // and collect deferred block handlers + let deferredOnBlockConfigs: array = [] + 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 { @@ -146,7 +169,20 @@ 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 + }) + // 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 } let fetchState = FetchState.make( @@ -163,7 +199,7 @@ let make = ( !config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth, Env.indexingBlockLag->Option.getWithDefault(0), ), - ~onBlockConfigs?, + ~onBlockConfigs=?filteredOnBlockConfigs, ) let chainReorgCheckpoints = reorgCheckpoints->Array.keepMapU(reorgCheckpoint => { @@ -199,6 +235,8 @@ let make = ( timestampCaughtUpToHeadOrEndblock, numEventsProcessed, numBatchesFetched, + deferredEventConfigs, + deferredOnBlockConfigs, } } @@ -462,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 = 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: diff --git a/scenarios/test_codegen/test/OnlyWhenReady_test.res b/scenarios/test_codegen/test/OnlyWhenReady_test.res new file mode 100644 index 000000000..aa3b8f4cd --- /dev/null +++ b/scenarios/test_codegen/test/OnlyWhenReady_test.res @@ -0,0 +1,271 @@ +open Belt +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: [ + 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, + contracts: [ + { + name: "TestContract", + abi: Ethers.makeAbi(%raw("[]")), + addresses: [mockAddress], + events: eventConfigs, + startBlock: None, + }, + ], + } + } + + 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 => { + { + 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(~chains=[chainConfig]) + 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(~chains=[chainConfig]) + 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(~chains=[chainConfig]) + 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, ~chains=[chainConfig]) + 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..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,6 +669,7 @@ module Source = { blockSchema: S.object(_ => ())->Utils.magic, transactionSchema: S.object(_ => ())->Utils.magic, convertHyperSyncEventArgs: _ => Js.Exn.raiseError("Not implemented"), + onlyWhenReady: item.onlyWhenReady->Option.getWithDefault(false), }: Internal.evmEventConfig :> Internal.eventConfig), timestamp: item.blockNumber, chain, @@ -767,6 +769,7 @@ let evmEventConfig = ( ~isWildcard=false, ~dependsOnAddresses=?, ~filterByAddresses=false, + ~onlyWhenReady=false, ): Internal.evmEventConfig => { { id, @@ -781,6 +784,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,