envio 3.3.0-alpha.8 → 3.3.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/ChainState.res +104 -37
- package/src/ChainState.res.mjs +80 -19
- package/src/ChainState.resi +4 -1
- package/src/CrossChainState.res +56 -29
- package/src/CrossChainState.res.mjs +11 -2
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +16 -17
- package/src/EventConfigBuilder.res.mjs +7 -6
- package/src/FetchState.res +151 -130
- package/src/FetchState.res.mjs +105 -88
- package/src/HandlerLoader.res +1 -1
- package/src/HandlerLoader.res.mjs +1 -1
- package/src/HandlerRegister.res +476 -305
- package/src/HandlerRegister.res.mjs +276 -209
- package/src/HandlerRegister.resi +11 -6
- package/src/Internal.res +23 -4
- package/src/LogSelection.res +102 -165
- package/src/LogSelection.res.mjs +101 -116
- package/src/Main.res +29 -143
- package/src/Main.res.mjs +25 -88
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateItems.res +1 -1
- package/src/sources/Evm.res +4 -1
- package/src/sources/Evm.res.mjs +1 -1
- package/src/sources/Fuel.res +3 -1
- package/src/sources/Fuel.res.mjs +1 -1
- package/src/sources/HyperSyncSource.res +22 -33
- package/src/sources/HyperSyncSource.res.mjs +17 -26
- package/src/sources/RpcSource.res +28 -39
- package/src/sources/RpcSource.res.mjs +18 -31
- package/src/sources/SourceManager.res +1 -7
- package/src/sources/SourceManager.res.mjs +1 -2
package/src/HandlerRegister.res
CHANGED
|
@@ -22,58 +22,26 @@ type chainRegistrations = {
|
|
|
22
22
|
// The finished registration state returned by `finishRegistration`.
|
|
23
23
|
type registrationsByChainId = dict<chainRegistrations>
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Incrementally built during registration: every `indexer.onEvent` /
|
|
26
|
+
// `.contractRegister` call resolves its `where` per chain right away and
|
|
27
|
+
// stores the resulting registration here, keyed by "Contract.Event", so
|
|
28
|
+
// invalid configuration throws at the user's registration call site.
|
|
29
|
+
type pendingChainRegistrations = {
|
|
30
|
+
onEventRegistrations: dict<Internal.onEventRegistration>,
|
|
31
|
+
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
32
|
+
}
|
|
26
33
|
|
|
27
34
|
type activeRegistration = {
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
config: Config.t,
|
|
36
|
+
registrationsByChainId: dict<pendingChainRegistrations>,
|
|
30
37
|
mutable finished: bool,
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Version-gated: the record shapes below can evolve between envio versions,
|
|
39
|
-
// so the guard uses strict full-version equality. On mismatch we throw with
|
|
40
|
-
// a deduplication hint instead of silently mixing shapes across builds.
|
|
41
|
-
type registryShape = {
|
|
42
|
-
version: string,
|
|
43
|
-
eventRegistrations: dict<eventRegistration>,
|
|
44
|
-
activeRegistration: ref<option<activeRegistration>>,
|
|
45
|
-
preRegistered: array<activeRegistration => unit>,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Record type with `mutable` so assignment typechecks; ReScript keeps the
|
|
49
|
-
// field name verbatim in the generated JS so the globalThis slot is
|
|
50
|
-
// `__envioRegistry`.
|
|
51
|
-
type globalThis = {mutable __envioRegistry: Nullable.t<registryShape>}
|
|
52
|
-
@val external globalThis: globalThis = "globalThis"
|
|
53
|
-
|
|
54
|
-
%%private(
|
|
55
|
-
let registry: registryShape = {
|
|
56
|
-
let version = Utils.EnvioPackage.value.version
|
|
57
|
-
switch globalThis.__envioRegistry->Nullable.toOption {
|
|
58
|
-
| Some(existing) if existing.version === version => existing
|
|
59
|
-
| Some(existing) =>
|
|
60
|
-
JsError.throwWithMessage(
|
|
61
|
-
`Multiple incompatible envio versions loaded in the same process: ${existing.version} and ${version}. Deduplicate the 'envio' dependency in your project.`,
|
|
62
|
-
)
|
|
63
|
-
| None =>
|
|
64
|
-
let fresh = {
|
|
65
|
-
version,
|
|
66
|
-
eventRegistrations: Dict.make(),
|
|
67
|
-
activeRegistration: ref(None),
|
|
68
|
-
preRegistered: [],
|
|
69
|
-
}
|
|
70
|
-
globalThis.__envioRegistry = Nullable.make(fresh)
|
|
71
|
-
fresh
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
let eventRegistrations = registry.eventRegistrations
|
|
40
|
+
// Registration state lives in the process-wide `EnvioGlobal` record (shared
|
|
41
|
+
// across duplicate envio module instances); the slots are opaque there, so
|
|
42
|
+
// cast them to the real types once here.
|
|
43
|
+
let eventRegistrations =
|
|
44
|
+
EnvioGlobal.value.eventRegistrations->(Utils.magic: dict<unknown> => dict<eventRegistration>)
|
|
77
45
|
|
|
78
46
|
let getKey = (~contractName, ~eventName) => contractName ++ "." ++ eventName
|
|
79
47
|
|
|
@@ -88,7 +56,8 @@ let set = (~contractName, ~eventName, registration) => {
|
|
|
88
56
|
eventRegistrations->Dict.set(getKey(~contractName, ~eventName), registration)
|
|
89
57
|
}
|
|
90
58
|
|
|
91
|
-
let
|
|
59
|
+
let getActiveRegistration = () =>
|
|
60
|
+
EnvioGlobal.value.activeRegistration->(Utils.magic: option<unknown> => option<activeRegistration>)
|
|
92
61
|
|
|
93
62
|
// Might happen for tests when the handler file
|
|
94
63
|
// is imported by a non-envio process (eg mocha)
|
|
@@ -97,10 +66,13 @@ let activeRegistration = registry.activeRegistration
|
|
|
97
66
|
// Theoretically we could keep preRegistration without an explicit start
|
|
98
67
|
// but I want it to be this way, so for the actual indexer run
|
|
99
68
|
// an error is thrown with the exact stack trace where the handler was registered.
|
|
100
|
-
let preRegistered =
|
|
69
|
+
let preRegistered =
|
|
70
|
+
EnvioGlobal.value.preRegistered->(
|
|
71
|
+
Utils.magic: array<unknown> => array<activeRegistration => unit>
|
|
72
|
+
)
|
|
101
73
|
|
|
102
74
|
let withRegistration = (fn: activeRegistration => unit) => {
|
|
103
|
-
switch
|
|
75
|
+
switch getActiveRegistration() {
|
|
104
76
|
| None => preRegistered->Array.push(fn)
|
|
105
77
|
| Some(r) =>
|
|
106
78
|
if r.finished {
|
|
@@ -113,15 +85,13 @@ let withRegistration = (fn: activeRegistration => unit) => {
|
|
|
113
85
|
}
|
|
114
86
|
}
|
|
115
87
|
|
|
116
|
-
let startRegistration = (~
|
|
88
|
+
let startRegistration = (~config: Config.t) => {
|
|
117
89
|
let r = {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
onBlockByChainId: Dict.make(),
|
|
121
|
-
},
|
|
90
|
+
config,
|
|
91
|
+
registrationsByChainId: Dict.make(),
|
|
122
92
|
finished: false,
|
|
123
93
|
}
|
|
124
|
-
activeRegistration
|
|
94
|
+
EnvioGlobal.value.activeRegistration = Some(r->(Utils.magic: activeRegistration => unknown))
|
|
125
95
|
while preRegistered->Array.length > 0 {
|
|
126
96
|
// Loop + cleanup in one go
|
|
127
97
|
switch preRegistered->Array.pop {
|
|
@@ -131,24 +101,30 @@ let startRegistration = (~ecosystem) => {
|
|
|
131
101
|
}
|
|
132
102
|
}
|
|
133
103
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let
|
|
104
|
+
let getPendingChainRegistrations = (r: activeRegistration, ~chainId: int) => {
|
|
105
|
+
let key = chainId->Int.toString
|
|
106
|
+
switch r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key) {
|
|
107
|
+
| Some(pending) => pending
|
|
108
|
+
| None =>
|
|
109
|
+
let fresh = {
|
|
110
|
+
onEventRegistrations: Dict.make(),
|
|
111
|
+
onBlockRegistrations: [],
|
|
112
|
+
}
|
|
113
|
+
r.registrationsByChainId->Dict.set(key, fresh)
|
|
114
|
+
fresh
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let buildOnEventRegistrationWith = (
|
|
140
119
|
~config: Config.t,
|
|
141
120
|
~chainId: int,
|
|
142
121
|
~eventConfig: Internal.eventConfig,
|
|
122
|
+
~isWildcard: bool,
|
|
123
|
+
~handler: option<Internal.handler>,
|
|
124
|
+
~contractRegister: option<Internal.contractRegister>,
|
|
125
|
+
~where: option<JSON.t>,
|
|
143
126
|
~startBlock=?,
|
|
144
127
|
): Internal.onEventRegistration => {
|
|
145
|
-
let contractName = eventConfig.contractName
|
|
146
|
-
let eventName = eventConfig.name
|
|
147
|
-
let t = get(~contractName, ~eventName)
|
|
148
|
-
let isWildcard = t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)
|
|
149
|
-
let handler = t.handler
|
|
150
|
-
let contractRegister = t.contractRegister
|
|
151
|
-
|
|
152
128
|
switch config.ecosystem.name {
|
|
153
129
|
| Fuel =>
|
|
154
130
|
(EventConfigBuilder.buildFuelOnEventRegistration(
|
|
@@ -174,217 +150,36 @@ let buildOnEventRegistration = (
|
|
|
174
150
|
~isWildcard,
|
|
175
151
|
~handler,
|
|
176
152
|
~contractRegister,
|
|
177
|
-
~
|
|
178
|
-
~
|
|
153
|
+
~where,
|
|
154
|
+
~chainId,
|
|
179
155
|
~onEventBlockFilterSchema=config.ecosystem.onEventBlockFilterSchema,
|
|
180
156
|
~startBlock?,
|
|
181
157
|
) :> Internal.onEventRegistration)
|
|
182
158
|
}
|
|
183
159
|
}
|
|
184
160
|
|
|
185
|
-
// Enrich
|
|
186
|
-
// handler/contractRegister/where
|
|
187
|
-
// the
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
// get added to `notRegisteredEventsByContract` (event names grouped by
|
|
192
|
-
// contract name) so the caller can report them once for the whole indexer
|
|
193
|
-
// instead of per chain.
|
|
194
|
-
let buildOnEventRegistrations = (
|
|
195
|
-
~chainConfig: Config.chain,
|
|
161
|
+
// Enrich one event definition into its (event, chain) registration using
|
|
162
|
+
// whatever handler/contractRegister/where the user registered for it. Shared
|
|
163
|
+
// by the incremental per-chain sync below, `simulate`, and test helpers so
|
|
164
|
+
// they stay in sync instead of re-deriving the per-ecosystem dispatch each
|
|
165
|
+
// place.
|
|
166
|
+
let buildOnEventRegistration = (
|
|
196
167
|
~config: Config.t,
|
|
197
|
-
~
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
let onEventRegistration = buildOnEventRegistration(
|
|
214
|
-
~config,
|
|
215
|
-
~chainId=chainConfig.id,
|
|
216
|
-
~eventConfig,
|
|
217
|
-
~startBlock=?contract.startBlock,
|
|
218
|
-
)
|
|
219
|
-
let {isWildcard, handler, contractRegister} = onEventRegistration
|
|
220
|
-
|
|
221
|
-
// Should validate the events
|
|
222
|
-
eventRouter->EventRouter.addOrThrow(
|
|
223
|
-
eventConfig.id,
|
|
224
|
-
(),
|
|
225
|
-
~contractName,
|
|
226
|
-
~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
|
|
227
|
-
~eventName,
|
|
228
|
-
~isWildcard,
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
// Filter out events without a handler/contractRegister so they aren't
|
|
232
|
-
// fetched or dispatched (unless raw events are enabled).
|
|
233
|
-
let shouldBeIncluded = if config.enableRawEvents {
|
|
234
|
-
true
|
|
235
|
-
} else {
|
|
236
|
-
let isRegistered = contractRegister->Option.isSome || handler->Option.isSome
|
|
237
|
-
if !isRegistered {
|
|
238
|
-
let eventNames = switch notRegisteredEventsByContract->Utils.Dict.dangerouslyGetNonOption(
|
|
239
|
-
contractName,
|
|
240
|
-
) {
|
|
241
|
-
| Some(set) => set
|
|
242
|
-
| None => {
|
|
243
|
-
let set = Utils.Set.make()
|
|
244
|
-
notRegisteredEventsByContract->Dict.set(contractName, set)
|
|
245
|
-
set
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
eventNames->Utils.Set.add(eventName)->ignore
|
|
249
|
-
}
|
|
250
|
-
isRegistered
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Check if event has Static([]) filters (from a dynamic where
|
|
254
|
-
// callback returning `false` / SkipAll for this chain).
|
|
255
|
-
// If so, skip it entirely - it should never be fetched
|
|
256
|
-
let shouldSkip = try {
|
|
257
|
-
let getEventFiltersOrThrow = (
|
|
258
|
-
onEventRegistration->(
|
|
259
|
-
Utils.magic: Internal.onEventRegistration => Internal.evmOnEventRegistration
|
|
260
|
-
)
|
|
261
|
-
).getEventFiltersOrThrow
|
|
262
|
-
|
|
263
|
-
// Check for non-evm chains
|
|
264
|
-
if (
|
|
265
|
-
getEventFiltersOrThrow->(Utils.magic: (ChainMap.Chain.t => Internal.eventFilters) => bool)
|
|
266
|
-
) {
|
|
267
|
-
switch getEventFiltersOrThrow(ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)) {
|
|
268
|
-
| Static([]) => true
|
|
269
|
-
| _ => false
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
false
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
// Can throw when filter is invalid
|
|
276
|
-
// Don't skip an event in this case. Let it throw in a better place - source code
|
|
277
|
-
| _ => false
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if shouldBeIncluded && !shouldSkip {
|
|
281
|
-
onEventRegistrations->Array.push(onEventRegistration)
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
onEventRegistrations
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
let finishRegistration = (~config: Config.t): registrationsByChainId => {
|
|
290
|
-
switch activeRegistration.contents {
|
|
291
|
-
| Some(r) => {
|
|
292
|
-
r.finished = true
|
|
293
|
-
let notRegisteredEventsByContract: dict<Utils.Set.t<string>> = Dict.make()
|
|
294
|
-
let registrationsByChainId: registrationsByChainId = Dict.make()
|
|
295
|
-
config.chainMap
|
|
296
|
-
->ChainMap.values
|
|
297
|
-
->Array.forEach(chainConfig => {
|
|
298
|
-
let key = chainConfig.id->Int.toString
|
|
299
|
-
registrationsByChainId->Dict.set(
|
|
300
|
-
key,
|
|
301
|
-
{
|
|
302
|
-
onEventRegistrations: buildOnEventRegistrations(
|
|
303
|
-
~chainConfig,
|
|
304
|
-
~config,
|
|
305
|
-
~notRegisteredEventsByContract,
|
|
306
|
-
),
|
|
307
|
-
onBlockRegistrations: r.registrations.onBlockByChainId
|
|
308
|
-
->Utils.Dict.dangerouslyGetNonOption(key)
|
|
309
|
-
->Option.getOr([]),
|
|
310
|
-
},
|
|
311
|
-
)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
// Reported once for the whole indexer (a shared contract on multiple
|
|
315
|
-
// chains would otherwise repeat the same message per chain).
|
|
316
|
-
let notRegisteredEntries = notRegisteredEventsByContract->Dict.toArray
|
|
317
|
-
if notRegisteredEntries->Utils.Array.notEmpty {
|
|
318
|
-
let groups =
|
|
319
|
-
notRegisteredEntries
|
|
320
|
-
->Array.map(((contractName, eventNames)) =>
|
|
321
|
-
`${contractName} (${eventNames->Utils.Set.toArray->Array.joinUnsafe(", ")})`
|
|
322
|
-
)
|
|
323
|
-
->Array.joinUnsafe(", ")
|
|
324
|
-
Logging.getLogger()->Logging.childInfo(
|
|
325
|
-
`Events without a handler, skipped for indexing: ${groups}`,
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
registrationsByChainId
|
|
330
|
-
}
|
|
331
|
-
| None =>
|
|
332
|
-
JsError.throwWithMessage(
|
|
333
|
-
"The indexer has not started registering handlers, so can't finish it.",
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
let isPendingRegistration = () => {
|
|
339
|
-
switch activeRegistration.contents {
|
|
340
|
-
| Some(r) => !r.finished
|
|
341
|
-
| None => false
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Early guard called from `indexer.onEvent` / `.contractRegister` / `.onBlock` /
|
|
346
|
-
// `.onSlot` so the user sees a method-specific error at the call site, instead
|
|
347
|
-
// of hitting the generic `withRegistration` throw deep inside `setHandler` etc.
|
|
348
|
-
let throwIfFinishedRegistration = (~methodName) => {
|
|
349
|
-
switch activeRegistration.contents {
|
|
350
|
-
| Some({finished: true}) =>
|
|
351
|
-
JsError.throwWithMessage(
|
|
352
|
-
`Cannot call \`indexer.${methodName}\` after the indexer has started. Make sure all handlers are registered at the top level of your handler module.`,
|
|
353
|
-
)
|
|
354
|
-
| _ => ()
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
let registerOnBlock = (
|
|
359
|
-
~name,
|
|
360
|
-
~chainId,
|
|
361
|
-
~interval,
|
|
362
|
-
~startBlock,
|
|
363
|
-
~endBlock,
|
|
364
|
-
~handler: Internal.onBlockArgs => promise<unit>,
|
|
365
|
-
) => {
|
|
366
|
-
withRegistration(registration => {
|
|
367
|
-
let onBlockByChainId = registration.registrations.onBlockByChainId
|
|
368
|
-
let key = chainId->Int.toString
|
|
369
|
-
let index =
|
|
370
|
-
onBlockByChainId
|
|
371
|
-
->Utils.Dict.dangerouslyGetNonOption(key)
|
|
372
|
-
->Option.mapOr(0, configs => configs->Array.length)
|
|
373
|
-
onBlockByChainId->Utils.Dict.push(
|
|
374
|
-
key,
|
|
375
|
-
(
|
|
376
|
-
{
|
|
377
|
-
index,
|
|
378
|
-
name,
|
|
379
|
-
startBlock,
|
|
380
|
-
endBlock,
|
|
381
|
-
interval,
|
|
382
|
-
chainId,
|
|
383
|
-
handler,
|
|
384
|
-
}: Internal.onBlockRegistration
|
|
385
|
-
),
|
|
386
|
-
)
|
|
387
|
-
})
|
|
168
|
+
~chainId: int,
|
|
169
|
+
~eventConfig: Internal.eventConfig,
|
|
170
|
+
~startBlock=?,
|
|
171
|
+
): Internal.onEventRegistration => {
|
|
172
|
+
let t = get(~contractName=eventConfig.contractName, ~eventName=eventConfig.name)
|
|
173
|
+
buildOnEventRegistrationWith(
|
|
174
|
+
~config,
|
|
175
|
+
~chainId,
|
|
176
|
+
~eventConfig,
|
|
177
|
+
~isWildcard=t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false),
|
|
178
|
+
~handler=t.handler,
|
|
179
|
+
~contractRegister=t.contractRegister,
|
|
180
|
+
~where=t.eventOptions->Option.flatMap(v => v.where),
|
|
181
|
+
~startBlock?,
|
|
182
|
+
)
|
|
388
183
|
}
|
|
389
184
|
|
|
390
185
|
let getHandler = (~contractName, ~eventName) => get(~contractName, ~eventName).handler
|
|
@@ -392,9 +187,6 @@ let getHandler = (~contractName, ~eventName) => get(~contractName, ~eventName).h
|
|
|
392
187
|
let getContractRegister = (~contractName, ~eventName) =>
|
|
393
188
|
get(~contractName, ~eventName).contractRegister
|
|
394
189
|
|
|
395
|
-
let getOnEventWhere = (~contractName, ~eventName) =>
|
|
396
|
-
get(~contractName, ~eventName).eventOptions->Option.flatMap(value => value.where)
|
|
397
|
-
|
|
398
190
|
let isWildcard = (~contractName, ~eventName) =>
|
|
399
191
|
get(~contractName, ~eventName).eventOptions
|
|
400
192
|
->Option.flatMap(value => value.wildcard)
|
|
@@ -413,35 +205,79 @@ let raiseDuplicateRegistration = (~contractName, ~eventName, ~msg, ~logger) => {
|
|
|
413
205
|
JsError.throwWithMessage(fullMsg)
|
|
414
206
|
}
|
|
415
207
|
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// back to referential equality on the function reference.
|
|
421
|
-
let whereMatch = (a: option<JSON.t>, b: option<JSON.t>) => {
|
|
422
|
-
switch (a, b) {
|
|
423
|
-
| (None, None) => true
|
|
424
|
-
| (Some(a), Some(b)) =>
|
|
425
|
-
if typeof(a) === #function || typeof(b) === #function {
|
|
426
|
-
a === b
|
|
427
|
-
} else {
|
|
428
|
-
a == b
|
|
429
|
-
}
|
|
430
|
-
| _ => false
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
208
|
+
// `where` equality is checked per chain on the resolved structure (see
|
|
209
|
+
// `syncOnEventRegistrations`), so registration options only need to agree on
|
|
210
|
+
// `wildcard` here — two callbacks that resolve to identical filters count as
|
|
211
|
+
// identical options even when the function references differ.
|
|
434
212
|
let eventOptionsMatch = (
|
|
435
213
|
existing: option<Internal.eventOptions<JSON.t>>,
|
|
436
214
|
incoming: option<Internal.eventOptions<JSON.t>>,
|
|
437
215
|
) => {
|
|
438
216
|
switch (existing, incoming) {
|
|
439
217
|
| (None, None) => true
|
|
440
|
-
| (Some(a), Some(b)) => a.wildcard === b.wildcard
|
|
218
|
+
| (Some(a), Some(b)) => a.wildcard === b.wildcard
|
|
441
219
|
| _ => false
|
|
442
220
|
}
|
|
443
221
|
}
|
|
444
222
|
|
|
223
|
+
let getResolvedWhere = (reg: Internal.onEventRegistration) =>
|
|
224
|
+
(
|
|
225
|
+
reg->(Utils.magic: Internal.onEventRegistration => Internal.evmOnEventRegistration)
|
|
226
|
+
).resolvedWhere
|
|
227
|
+
|
|
228
|
+
// Resolve the registration for every configured chain that defines the event
|
|
229
|
+
// and store it in the pending per-chain registry. When the chain already
|
|
230
|
+
// holds a registration for this event, the resolved `where` structures must
|
|
231
|
+
// deep-compare equal (`Values` by hex arrays, `ContractAddresses` by contract
|
|
232
|
+
// name, plus `startBlock`) — otherwise it's a conflicting duplicate
|
|
233
|
+
// registration. Both live registrations and `preRegistered` callbacks
|
|
234
|
+
// replayed by `startRegistration` run through this single code path.
|
|
235
|
+
let syncOnEventRegistrations = (
|
|
236
|
+
r: activeRegistration,
|
|
237
|
+
~contractName,
|
|
238
|
+
~eventName,
|
|
239
|
+
~where: option<JSON.t>,
|
|
240
|
+
~duplicateMsg,
|
|
241
|
+
~logger,
|
|
242
|
+
) => {
|
|
243
|
+
let config = r.config
|
|
244
|
+
let t = get(~contractName, ~eventName)
|
|
245
|
+
let isWildcard = t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)
|
|
246
|
+
let key = getKey(~contractName, ~eventName)
|
|
247
|
+
|
|
248
|
+
config.chainMap
|
|
249
|
+
->ChainMap.values
|
|
250
|
+
->Array.forEach(chainConfig => {
|
|
251
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
252
|
+
if contract.name === contractName {
|
|
253
|
+
switch contract.events->Array.find(e => e.name === eventName) {
|
|
254
|
+
| None => ()
|
|
255
|
+
| Some(eventConfig) =>
|
|
256
|
+
let newRegistration = buildOnEventRegistrationWith(
|
|
257
|
+
~config,
|
|
258
|
+
~chainId=chainConfig.id,
|
|
259
|
+
~eventConfig,
|
|
260
|
+
~isWildcard,
|
|
261
|
+
~handler=t.handler,
|
|
262
|
+
~contractRegister=t.contractRegister,
|
|
263
|
+
~where,
|
|
264
|
+
~startBlock=?contract.startBlock,
|
|
265
|
+
)
|
|
266
|
+
let pending = r->getPendingChainRegistrations(~chainId=chainConfig.id)
|
|
267
|
+
switch pending.onEventRegistrations->Utils.Dict.dangerouslyGetNonOption(key) {
|
|
268
|
+
| Some(existing) if config.ecosystem.name === Evm =>
|
|
269
|
+
if !(existing->getResolvedWhere == newRegistration->getResolvedWhere) {
|
|
270
|
+
raiseDuplicateRegistration(~contractName, ~eventName, ~msg=duplicateMsg, ~logger)
|
|
271
|
+
}
|
|
272
|
+
| _ => ()
|
|
273
|
+
}
|
|
274
|
+
pending.onEventRegistrations->Dict.set(key, newRegistration)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
445
281
|
let setEventOptions = (~contractName, ~eventName, ~eventOptions, ~logger=Logging.getLogger()) => {
|
|
446
282
|
switch eventOptions {
|
|
447
283
|
| Some(value) =>
|
|
@@ -470,9 +306,13 @@ let setHandler = (
|
|
|
470
306
|
~eventOptions,
|
|
471
307
|
~logger=Logging.getLogger(),
|
|
472
308
|
) => {
|
|
473
|
-
withRegistration(
|
|
309
|
+
withRegistration(registration => {
|
|
474
310
|
let t = get(~contractName, ~eventName)
|
|
475
311
|
let newHandler = handler->(Utils.magic: Internal.genericHandler<'args> => Internal.handler)
|
|
312
|
+
let incomingEventOptions =
|
|
313
|
+
eventOptions->Option.map(v =>
|
|
314
|
+
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
315
|
+
)
|
|
476
316
|
switch t.handler {
|
|
477
317
|
| None =>
|
|
478
318
|
setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
|
|
@@ -486,10 +326,6 @@ let setHandler = (
|
|
|
486
326
|
},
|
|
487
327
|
)
|
|
488
328
|
| Some(prevHandler) =>
|
|
489
|
-
let incomingEventOptions =
|
|
490
|
-
eventOptions->Option.map(v =>
|
|
491
|
-
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
492
|
-
)
|
|
493
329
|
if eventOptionsMatch(t.eventOptions, incomingEventOptions) {
|
|
494
330
|
let composedHandler: Internal.handler = async args => {
|
|
495
331
|
await prevHandler(args)
|
|
@@ -512,6 +348,13 @@ let setHandler = (
|
|
|
512
348
|
)
|
|
513
349
|
}
|
|
514
350
|
}
|
|
351
|
+
registration->syncOnEventRegistrations(
|
|
352
|
+
~contractName,
|
|
353
|
+
~eventName,
|
|
354
|
+
~where=incomingEventOptions->Option.flatMap(v => v.where),
|
|
355
|
+
~duplicateMsg="Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
|
|
356
|
+
~logger,
|
|
357
|
+
)
|
|
515
358
|
})
|
|
516
359
|
}
|
|
517
360
|
|
|
@@ -522,7 +365,7 @@ let setContractRegister = (
|
|
|
522
365
|
~eventOptions,
|
|
523
366
|
~logger=Logging.getLogger(),
|
|
524
367
|
) => {
|
|
525
|
-
withRegistration(
|
|
368
|
+
withRegistration(registration => {
|
|
526
369
|
let t = get(~contractName, ~eventName)
|
|
527
370
|
let newContractRegister =
|
|
528
371
|
contractRegister->(
|
|
@@ -530,6 +373,10 @@ let setContractRegister = (
|
|
|
530
373
|
Internal.genericContractRegisterArgs<'event, 'context>,
|
|
531
374
|
> => Internal.contractRegister
|
|
532
375
|
)
|
|
376
|
+
let incomingEventOptions =
|
|
377
|
+
eventOptions->Option.map(v =>
|
|
378
|
+
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
379
|
+
)
|
|
533
380
|
switch t.contractRegister {
|
|
534
381
|
| None =>
|
|
535
382
|
setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
|
|
@@ -543,10 +390,6 @@ let setContractRegister = (
|
|
|
543
390
|
},
|
|
544
391
|
)
|
|
545
392
|
| Some(prevContractRegister) =>
|
|
546
|
-
let incomingEventOptions =
|
|
547
|
-
eventOptions->Option.map(v =>
|
|
548
|
-
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
549
|
-
)
|
|
550
393
|
if eventOptionsMatch(t.eventOptions, incomingEventOptions) {
|
|
551
394
|
let composedContractRegister: Internal.contractRegister = async args => {
|
|
552
395
|
await prevContractRegister(args)
|
|
@@ -569,5 +412,333 @@ let setContractRegister = (
|
|
|
569
412
|
)
|
|
570
413
|
}
|
|
571
414
|
}
|
|
415
|
+
registration->syncOnEventRegistrations(
|
|
416
|
+
~contractName,
|
|
417
|
+
~eventName,
|
|
418
|
+
~where=incomingEventOptions->Option.flatMap(v => v.where),
|
|
419
|
+
~duplicateMsg="Cannot register a second contractRegister with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
|
|
420
|
+
~logger,
|
|
421
|
+
)
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Shape of the user-returned `{_gte?, _lte?, _every?}` filter chunk after
|
|
426
|
+
// the ecosystem-specific wrapper is stripped. Shared across all ecosystems —
|
|
427
|
+
// the outer `block.number` / `block.height` / `slot` unwrap lives on each
|
|
428
|
+
// ecosystem's `onBlockFilterSchema`, and the inner range fields are the
|
|
429
|
+
// same everywhere.
|
|
430
|
+
type blockRange = {
|
|
431
|
+
_gte: option<int>,
|
|
432
|
+
_lte: option<int>,
|
|
433
|
+
_every: int,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// `S.strict` rejects unknown fields so typos like `_gt` / `_evry` surface
|
|
437
|
+
// with a readable schema error pointing at the offending key, instead of
|
|
438
|
+
// silently registering a broken filter. `_every` defaults to 1 inside the
|
|
439
|
+
// schema so the caller always sees a plain `int`, and `intMin(1)` rejects
|
|
440
|
+
// zero/negative strides — `(blockNumber - startBlock) % 0` would crash and
|
|
441
|
+
// any negative stride would never match.
|
|
442
|
+
let blockRangeSchema: S.t<blockRange> = S.object(s => {
|
|
443
|
+
_gte: s.field("_gte", S.option(S.int)),
|
|
444
|
+
_lte: s.field("_lte", S.option(S.int)),
|
|
445
|
+
_every: s.field("_every", S.option(S.int->S.intMin(1))->S.Option.getOr(1)),
|
|
446
|
+
})->S.strict
|
|
447
|
+
|
|
448
|
+
let defaultBlockRange: blockRange = {_gte: None, _lte: None, _every: 1}
|
|
449
|
+
|
|
450
|
+
// Two-stage parse: first the ecosystem-specific outer schema unwraps the
|
|
451
|
+
// wrapper (`block.number` / `block.height` / `slot`) and surfaces the
|
|
452
|
+
// inner chunk as raw `unknown`; then the shared `blockRangeSchema`
|
|
453
|
+
// validates the `{_gte?, _lte?, _every?}` fields. Keeping the inner
|
|
454
|
+
// validation in one place means typos and shape mismatches surface with
|
|
455
|
+
// the same user-friendly error regardless of ecosystem.
|
|
456
|
+
let extractRange = (filter: unknown, ~name, ~ecosystem: Ecosystem.t): blockRange =>
|
|
457
|
+
try {
|
|
458
|
+
switch filter->S.parseOrThrow(ecosystem.onBlockFilterSchema) {
|
|
459
|
+
| None => defaultBlockRange
|
|
460
|
+
| Some(inner) => inner->S.parseOrThrow(blockRangeSchema)
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
| S.Raised(exn) =>
|
|
464
|
+
JsError.throwWithMessage(
|
|
465
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` returned an invalid filter: ${exn
|
|
466
|
+
->Utils.prettifyExn
|
|
467
|
+
->(Utils.magic: exn => string)}`,
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Mirrors `Envio.onBlockWhereArgs` without depending on the module.
|
|
472
|
+
type onBlockWhereArgs = {chain: unknown}
|
|
473
|
+
|
|
474
|
+
// `where` is evaluated once per configured chain at registration time.
|
|
475
|
+
// Decoded ranges/stride feed directly into the per-chain registration store
|
|
476
|
+
// so the fetcher's `(blockNumber - handlerStartBlock) % interval === 0`
|
|
477
|
+
// math in `FetchState` stays untouched. Deferred via `withRegistration` so
|
|
478
|
+
// the per-chain loop sees the registration's config, which may be a narrowed
|
|
479
|
+
// version of the generated one (TestIndexer runs with a per-test chain
|
|
480
|
+
// subset). `where` arrives unvalidated (`unknown`) straight from the user's
|
|
481
|
+
// options object.
|
|
482
|
+
let registerOnBlock = (
|
|
483
|
+
~name: string,
|
|
484
|
+
~where: unknown,
|
|
485
|
+
~handler: Internal.onBlockArgs => promise<unit>,
|
|
486
|
+
~getChainsObject: Config.t => dict<unknown>,
|
|
487
|
+
) => {
|
|
488
|
+
withRegistration(registration => {
|
|
489
|
+
let config = registration.config
|
|
490
|
+
let ecosystem = config.ecosystem
|
|
491
|
+
let chainsDict = getChainsObject(config)
|
|
492
|
+
let logger = Logging.createChild(~params={"onBlock": name})
|
|
493
|
+
|
|
494
|
+
// `where` must be a function (unlike onEvent, which also accepts a static
|
|
495
|
+
// value). A static value would have to be evaluated against every chain
|
|
496
|
+
// independently, which has no useful semantic for block handlers.
|
|
497
|
+
// Normalize undefined/null to None up front so the per-chain loop below
|
|
498
|
+
// can't accidentally call `null` as a predicate.
|
|
499
|
+
let where = switch where {
|
|
500
|
+
| w if w === %raw(`undefined`) || w === %raw(`null`) => None
|
|
501
|
+
| w if typeof(w) === #function => Some(w->(Utils.magic: unknown => onBlockWhereArgs => unknown))
|
|
502
|
+
| w =>
|
|
503
|
+
JsError.throwWithMessage(
|
|
504
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` expected \`where\` to be a function or omitted, but got ${(typeof(
|
|
505
|
+
w,
|
|
506
|
+
) :> string)}.`,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let matchedAny = ref(false)
|
|
511
|
+
|
|
512
|
+
config.chainMap
|
|
513
|
+
->ChainMap.values
|
|
514
|
+
->Array.forEach(chainConfig => {
|
|
515
|
+
let chainId = chainConfig.id
|
|
516
|
+
let chainObj = chainsDict->Dict.getUnsafe(chainId->Int.toString)
|
|
517
|
+
|
|
518
|
+
// Predicate returns `true` → match with no filter; `false` → skip;
|
|
519
|
+
// any plain object → structured filter. `undefined`/`null` returns
|
|
520
|
+
// are rejected — the TS type excludes `void`, so a missing return is
|
|
521
|
+
// a user bug we surface early rather than silently match-all.
|
|
522
|
+
let result = switch where {
|
|
523
|
+
| None => %raw(`true`)
|
|
524
|
+
| Some(predicate) => predicate({chain: chainObj})
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let (shouldRegister, range) = if result === %raw(`true`) {
|
|
528
|
+
(true, defaultBlockRange)
|
|
529
|
+
} else if result === %raw(`false`) {
|
|
530
|
+
(false, defaultBlockRange)
|
|
531
|
+
} else if typeof(result) === #object && !(result->Array.isArray) && result !== %raw(`null`) {
|
|
532
|
+
(true, extractRange(result, ~name, ~ecosystem))
|
|
533
|
+
} else {
|
|
534
|
+
// Reject numbers, strings, functions, arrays, undefined, null —
|
|
535
|
+
// anything that isn't bool or a plain object would silently
|
|
536
|
+
// misregister.
|
|
537
|
+
JsError.throwWithMessage(
|
|
538
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` predicate returned an invalid value of type ${(typeof(
|
|
539
|
+
result,
|
|
540
|
+
) :> string)}. Expected boolean or a filter object.`,
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if shouldRegister {
|
|
545
|
+
matchedAny := true
|
|
546
|
+
if range._gte->Option.getOr(chainConfig.startBlock) < chainConfig.startBlock {
|
|
547
|
+
JsError.throwWithMessage(
|
|
548
|
+
`The start block for onBlock handler "${name}" is less than the chain start block (${chainConfig.startBlock->Int.toString}). This is not supported yet.`,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
switch chainConfig.endBlock {
|
|
552
|
+
| Some(chainEndBlock) =>
|
|
553
|
+
if range._lte->Option.getOr(chainEndBlock) > chainEndBlock {
|
|
554
|
+
JsError.throwWithMessage(
|
|
555
|
+
`The end block for onBlock handler "${name}" is greater than the chain end block (${chainEndBlock->Int.toString}). This is not supported yet.`,
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
| None => ()
|
|
559
|
+
}
|
|
560
|
+
let pending = registration->getPendingChainRegistrations(~chainId)
|
|
561
|
+
pending.onBlockRegistrations
|
|
562
|
+
->Array.push(
|
|
563
|
+
(
|
|
564
|
+
{
|
|
565
|
+
index: pending.onBlockRegistrations->Array.length,
|
|
566
|
+
name,
|
|
567
|
+
startBlock: range._gte,
|
|
568
|
+
endBlock: range._lte,
|
|
569
|
+
interval: range._every,
|
|
570
|
+
chainId,
|
|
571
|
+
handler,
|
|
572
|
+
}: Internal.onBlockRegistration
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
->ignore
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// Catches misconfigured `where` predicates that return `false` for every
|
|
580
|
+
// configured chain — the handler would otherwise never fire with no hint.
|
|
581
|
+
// Includes the ecosystem-specific method name so SVM users see "onSlot"
|
|
582
|
+
// and don't get confused looking for a "Block handler" they never wrote.
|
|
583
|
+
if !matchedAny.contents {
|
|
584
|
+
logger->Logging.childWarn(
|
|
585
|
+
`\`indexer.${ecosystem.onBlockMethodName}\` matched 0 chains. Check the \`where\` predicate.`,
|
|
586
|
+
)
|
|
587
|
+
}
|
|
572
588
|
})
|
|
573
589
|
}
|
|
590
|
+
|
|
591
|
+
let finishRegistration = (~config: Config.t): registrationsByChainId => {
|
|
592
|
+
switch getActiveRegistration() {
|
|
593
|
+
| Some(r) => {
|
|
594
|
+
r.finished = true
|
|
595
|
+
let notRegisteredEventsByContract: dict<Utils.Set.t<string>> = Dict.make()
|
|
596
|
+
let registrationsByChainId: registrationsByChainId = Dict.make()
|
|
597
|
+
config.chainMap
|
|
598
|
+
->ChainMap.values
|
|
599
|
+
->Array.forEach(chainConfig => {
|
|
600
|
+
let key = chainConfig.id->Int.toString
|
|
601
|
+
let pending = r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key)
|
|
602
|
+
|
|
603
|
+
// We don't need the router itself, but only validation logic,
|
|
604
|
+
// since now event router is created for selection of events
|
|
605
|
+
// and validation doesn't work correctly in routers.
|
|
606
|
+
// Ideally to split it into two different parts.
|
|
607
|
+
let eventRouter = EventRouter.empty()
|
|
608
|
+
|
|
609
|
+
let onEventRegistrations: array<Internal.onEventRegistration> = []
|
|
610
|
+
|
|
611
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
612
|
+
let contractName = contract.name
|
|
613
|
+
|
|
614
|
+
contract.events->Array.forEach(
|
|
615
|
+
eventConfig => {
|
|
616
|
+
let eventName = eventConfig.name
|
|
617
|
+
let registration =
|
|
618
|
+
pending->Option.flatMap(
|
|
619
|
+
pending =>
|
|
620
|
+
pending.onEventRegistrations->Utils.Dict.dangerouslyGetNonOption(
|
|
621
|
+
getKey(~contractName, ~eventName),
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
// Should validate the events
|
|
626
|
+
eventRouter->EventRouter.addOrThrow(
|
|
627
|
+
eventConfig.id,
|
|
628
|
+
(),
|
|
629
|
+
~contractName,
|
|
630
|
+
~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
|
|
631
|
+
~eventName,
|
|
632
|
+
~isWildcard=switch registration {
|
|
633
|
+
| Some(registration) => registration.isWildcard
|
|
634
|
+
| None => isWildcard(~contractName, ~eventName)
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
let registration = switch registration {
|
|
639
|
+
| Some(_) as registration => registration
|
|
640
|
+
| None =>
|
|
641
|
+
// No entry in the incremental store, but the persistent dict
|
|
642
|
+
// may still hold a handler: handler modules are import-cached,
|
|
643
|
+
// so a repeated registration cycle in the same process (tests
|
|
644
|
+
// restarting the indexer) never re-runs the `indexer.onEvent`
|
|
645
|
+
// calls. Rebuild from the dict in that case. Events without a
|
|
646
|
+
// handler/contractRegister aren't fetched or dispatched
|
|
647
|
+
// (unless raw events are enabled).
|
|
648
|
+
if hasRegistration(~contractName, ~eventName) || config.enableRawEvents {
|
|
649
|
+
Some(
|
|
650
|
+
buildOnEventRegistration(
|
|
651
|
+
~config,
|
|
652
|
+
~chainId=chainConfig.id,
|
|
653
|
+
~eventConfig,
|
|
654
|
+
~startBlock=?contract.startBlock,
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
} else {
|
|
658
|
+
let eventNames = switch notRegisteredEventsByContract->Utils.Dict.dangerouslyGetNonOption(
|
|
659
|
+
contractName,
|
|
660
|
+
) {
|
|
661
|
+
| Some(set) => set
|
|
662
|
+
| None => {
|
|
663
|
+
let set = Utils.Set.make()
|
|
664
|
+
notRegisteredEventsByContract->Dict.set(contractName, set)
|
|
665
|
+
set
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
eventNames->Utils.Set.add(eventName)->ignore
|
|
669
|
+
None
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
switch registration {
|
|
674
|
+
| Some(registration) =>
|
|
675
|
+
// A `where` that resolved to no topic selections (`false` for
|
|
676
|
+
// this chain) drops the chain's registration entirely — the
|
|
677
|
+
// event should never be fetched here.
|
|
678
|
+
let isDroppedByWhere =
|
|
679
|
+
config.ecosystem.name === Evm &&
|
|
680
|
+
(registration->getResolvedWhere).topicSelections->Utils.Array.isEmpty
|
|
681
|
+
if !isDroppedByWhere {
|
|
682
|
+
onEventRegistrations->Array.push(registration)
|
|
683
|
+
}
|
|
684
|
+
| None => ()
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
)
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
registrationsByChainId->Dict.set(
|
|
691
|
+
key,
|
|
692
|
+
{
|
|
693
|
+
onEventRegistrations,
|
|
694
|
+
onBlockRegistrations: switch pending {
|
|
695
|
+
| Some(pending) => pending.onBlockRegistrations
|
|
696
|
+
| None => []
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
// Reported once for the whole indexer (a shared contract on multiple
|
|
703
|
+
// chains would otherwise repeat the same message per chain).
|
|
704
|
+
let notRegisteredEntries = notRegisteredEventsByContract->Dict.toArray
|
|
705
|
+
if notRegisteredEntries->Utils.Array.notEmpty {
|
|
706
|
+
let groups =
|
|
707
|
+
notRegisteredEntries
|
|
708
|
+
->Array.map(((contractName, eventNames)) =>
|
|
709
|
+
`${contractName} (${eventNames->Utils.Set.toArray->Array.joinUnsafe(", ")})`
|
|
710
|
+
)
|
|
711
|
+
->Array.joinUnsafe(", ")
|
|
712
|
+
Logging.getLogger()->Logging.childInfo(
|
|
713
|
+
`Events without a handler, skipped for indexing: ${groups}`,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
registrationsByChainId
|
|
718
|
+
}
|
|
719
|
+
| None =>
|
|
720
|
+
JsError.throwWithMessage(
|
|
721
|
+
"The indexer has not started registering handlers, so can't finish it.",
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let isPendingRegistration = () => {
|
|
727
|
+
switch getActiveRegistration() {
|
|
728
|
+
| Some(r) => !r.finished
|
|
729
|
+
| None => false
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Early guard called from `indexer.onEvent` / `.contractRegister` / `.onBlock` /
|
|
734
|
+
// `.onSlot` so the user sees a method-specific error at the call site, instead
|
|
735
|
+
// of hitting the generic `withRegistration` throw deep inside `setHandler` etc.
|
|
736
|
+
let throwIfFinishedRegistration = (~methodName) => {
|
|
737
|
+
switch getActiveRegistration() {
|
|
738
|
+
| Some({finished: true}) =>
|
|
739
|
+
JsError.throwWithMessage(
|
|
740
|
+
`Cannot call \`indexer.${methodName}\` after the indexer has started. Make sure all handlers are registered at the top level of your handler module.`,
|
|
741
|
+
)
|
|
742
|
+
| _ => ()
|
|
743
|
+
}
|
|
744
|
+
}
|