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/src/Main.res CHANGED
@@ -39,40 +39,24 @@ let stateSchema = S.union([
39
39
  })),
40
40
  ])
41
41
 
42
- // Shape of the user-returned `{_gte?, _lte?, _every?}` filter chunk after
43
- // the ecosystem-specific wrapper is stripped. Shared across all ecosystems
44
- // the outer `block.number` / `block.height` / `slot` unwrap lives on each
45
- // ecosystem's `onBlockFilterSchema`, and the inner range fields are the
46
- // same everywhere.
47
- type blockRange = {
48
- _gte: option<int>,
49
- _lte: option<int>,
50
- _every: int,
51
- }
52
-
53
- // `S.strict` rejects unknown fields so typos like `_gt` / `_evry` surface
54
- // with a readable schema error pointing at the offending key, instead of
55
- // silently registering a broken filter. `_every` defaults to 1 inside the
56
- // schema so the caller always sees a plain `int`, and `intMin(1)` rejects
57
- // zero/negative strides — `(blockNumber - startBlock) % 0` would crash and
58
- // any negative stride would never match.
59
- let blockRangeSchema: S.t<blockRange> = S.object(s => {
60
- _gte: s.field("_gte", S.option(S.int)),
61
- _lte: s.field("_lte", S.option(S.int)),
62
- _every: s.field("_every", S.option(S.int->S.intMin(1))->S.Option.getOr(1)),
63
- })->S.strict
64
-
65
- let defaultBlockRange: blockRange = {_gte: None, _lte: None, _every: 1}
66
-
67
- let indexerStateRef: ref<option<IndexerState.t>> = ref(None)
42
+ // Runtime state lives in the process-wide `EnvioGlobal` record (shared
43
+ // across duplicate envio module instances); the slots are opaque there, so
44
+ // cast them to the real types here.
45
+ let getIndexerState = () =>
46
+ EnvioGlobal.value.indexerState->(Utils.magic: option<unknown> => option<IndexerState.t>)
47
+ let setIndexerState = (state: IndexerState.t) =>
48
+ EnvioGlobal.value.indexerState = Some(state->(Utils.magic: IndexerState.t => unknown))
68
49
 
69
50
  // Persistence is set by Main.start before handler modules load, so that
70
51
  // the exported indexer value can lazily expose DB state (startBlock,
71
52
  // endBlock, isRealtime, dynamic contract addresses) once it's ready.
72
- let globalPersistenceRef: ref<option<Persistence.t>> = ref(None)
53
+ let getGlobalPersistence = () =>
54
+ EnvioGlobal.value.persistence->(Utils.magic: option<unknown> => option<Persistence.t>)
55
+ let setGlobalPersistence = (persistence: Persistence.t) =>
56
+ EnvioGlobal.value.persistence = Some(persistence->(Utils.magic: Persistence.t => unknown))
73
57
 
74
58
  let getInitialChainState = (~chainId: int): option<Persistence.initialChainState> => {
75
- switch globalPersistenceRef.contents {
59
+ switch getGlobalPersistence() {
76
60
  | Some(persistence) =>
77
61
  switch persistence.storageStatus {
78
62
  | Ready(initialState) => initialState.chains->Array.find(c => c.id === chainId)
@@ -130,7 +114,7 @@ let buildChainsObject = (~config: Config.t) => {
130
114
  {
131
115
  enumerable: true,
132
116
  get: () => {
133
- switch indexerStateRef.contents {
117
+ switch getIndexerState() {
134
118
  | Some(state) => state->IndexerState.isRealtime
135
119
  // Before the global state is available (eg during handler
136
120
  // module load after resume), derive from persistence: every chain
@@ -164,7 +148,7 @@ let buildChainsObject = (~config: Config.t) => {
164
148
  {
165
149
  enumerable: true,
166
150
  get: () => {
167
- switch indexerStateRef.contents {
151
+ switch getIndexerState() {
168
152
  | Some(state) => {
169
153
  let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
170
154
  let chainState = state->IndexerState.getChainState(~chain)
@@ -345,121 +329,24 @@ let getGlobalIndexer = (): 'indexer => {
345
329
  let _ = RollbackCommit.register(callback->(Utils.magic: 'a => RollbackCommit.callback))
346
330
  }
347
331
 
348
- // Two-stage parse: first the ecosystem-specific outer schema unwraps the
349
- // wrapper (`block.number` / `block.height` / `slot`) and surfaces the
350
- // inner chunk as raw `unknown`; then the shared `blockRangeSchema`
351
- // validates the `{_gte?, _lte?, _every?}` fields. Keeping the inner
352
- // validation in one place means typos and shape mismatches surface with
353
- // the same user-friendly error regardless of ecosystem.
354
- let extractRange = (filter: unknown, ~name, ~ecosystem: Ecosystem.t): blockRange =>
355
- try {
356
- switch filter->S.parseOrThrow(ecosystem.onBlockFilterSchema) {
357
- | None => defaultBlockRange
358
- | Some(inner) => inner->S.parseOrThrow(blockRangeSchema)
359
- }
360
- } catch {
361
- | S.Raised(exn) =>
362
- JsError.throwWithMessage(
363
- `\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` returned an invalid filter: ${exn
364
- ->Utils.prettifyExn
365
- ->(Utils.magic: exn => string)}`,
366
- )
367
- }
368
-
369
- // `where` is evaluated once per configured chain at registration time.
370
- // Decoded ranges/stride feed directly into `HandlerRegister.registerOnBlock`
371
- // so the fetcher's `(blockNumber - handlerStartBlock) % interval === 0`
372
- // math at `FetchState.res:619` stays untouched.
373
332
  let onBlockFn = (rawOptions: 'a, handler: 'b) => {
374
333
  HandlerRegister.throwIfFinishedRegistration(~methodName="onBlock")
375
- let config = Config.load()
376
- let ecosystem = config.ecosystem
377
334
  let raw =
378
335
  rawOptions->(
379
336
  Utils.magic: 'a => {
380
337
  "name": string,
381
- "where": option<Envio.onBlockWhereArgs<unknown> => unknown>,
338
+ "where": unknown,
382
339
  }
383
340
  )
384
- let typedHandler = handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>)
385
- let (chains, _) = buildChainsObject(~config)
386
- let chainsDict = chains->(Utils.magic: {..} => dict<unknown>)
387
- let name = raw["name"]
388
- let logger = Logging.createChild(~params={"onBlock": name})
389
-
390
- // `where` must be a function (unlike onEvent, which also accepts a static
391
- // value). A static value would have to be evaluated against every chain
392
- // independently, which has no useful semantic for block handlers.
393
- // Normalize undefined/null to None up front so the per-chain loop below
394
- // can't accidentally call `null` as a predicate (ReScript treats a JS
395
- // `null` value as `Some(null)` when the field is typed as option).
396
- let where = switch raw["where"]->(Utils.magic: option<'a> => unknown) {
397
- | w if w === %raw(`undefined`) || w === %raw(`null`) => None
398
- | w if typeof(w) === #function => Some(raw["where"]->Option.getUnsafe)
399
- | w =>
400
- JsError.throwWithMessage(
401
- `\`indexer.${ecosystem.onBlockMethodName}("${name}")\` expected \`where\` to be a function or omitted, but got ${(typeof(
402
- w,
403
- ) :> string)}.`,
404
- )
405
- }
406
-
407
- let matchedAny = ref(false)
408
-
409
- config.chainMap
410
- ->ChainMap.values
411
- ->Array.forEach(chainConfig => {
412
- let chainId = chainConfig.id
413
- let chainObj = chainsDict->Dict.getUnsafe(chainId->Int.toString)
414
-
415
- // Predicate returns `true` → match with no filter; `false` → skip;
416
- // any plain object → structured filter. `undefined`/`null` returns
417
- // are rejected — the TS type excludes `void`, so a missing return is
418
- // a user bug we surface early rather than silently match-all.
419
- let result = switch where {
420
- | None => %raw(`true`)
421
- | Some(predicate) => predicate({chain: chainObj})
422
- }
423
-
424
- let (shouldRegister, range) = if result === %raw(`true`) {
425
- (true, defaultBlockRange)
426
- } else if result === %raw(`false`) {
427
- (false, defaultBlockRange)
428
- } else if typeof(result) === #object && !(result->Array.isArray) && result !== %raw(`null`) {
429
- (true, extractRange(result, ~name, ~ecosystem))
430
- } else {
431
- // Reject numbers, strings, functions, arrays, undefined, null —
432
- // anything that isn't bool or a plain object would silently
433
- // misregister.
434
- JsError.throwWithMessage(
435
- `\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` predicate returned an invalid value of type ${(typeof(
436
- result,
437
- ) :> string)}. Expected boolean or a filter object.`,
438
- )
439
- }
440
-
441
- if shouldRegister {
442
- matchedAny := true
443
- HandlerRegister.registerOnBlock(
444
- ~name,
445
- ~chainId,
446
- ~interval=range._every,
447
- ~startBlock=range._gte,
448
- ~endBlock=range._lte,
449
- ~handler=typedHandler,
450
- )
451
- }
452
- })
453
-
454
- // Catches misconfigured `where` predicates that return `false` for every
455
- // configured chain — the handler would otherwise never fire with no hint.
456
- // Includes the ecosystem-specific method name so SVM users see "onSlot"
457
- // and don't get confused looking for a "Block handler" they never wrote.
458
- if !matchedAny.contents {
459
- logger->Logging.childWarn(
460
- `\`indexer.${ecosystem.onBlockMethodName}\` matched 0 chains. Check the \`where\` predicate.`,
461
- )
462
- }
341
+ HandlerRegister.registerOnBlock(
342
+ ~name=raw["name"],
343
+ ~where=raw["where"],
344
+ ~handler=handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>),
345
+ ~getChainsObject=config => {
346
+ let (chains, _) = buildChainsObject(~config)
347
+ chains->(Utils.magic: {..} => dict<unknown>)
348
+ },
349
+ )
463
350
  }
464
351
 
465
352
  // Ecosystem-specific surface: EVM/Fuel expose event + block handlers; SVM
@@ -506,8 +393,7 @@ let getGlobalIndexer = (): 'indexer => {
506
393
  let get = (~prop: string) =>
507
394
  switch prop {
508
395
  | "name" => Config.load().name->(Utils.magic: string => unknown)
509
- | "description" =>
510
- Config.load().description->(Utils.magic: option<string> => unknown)
396
+ | "description" => Config.load().description->(Utils.magic: option<string> => unknown)
511
397
  | "chainIds" => {
512
398
  let (_, chainIds) = buildChainsObject(~config=Config.load())
513
399
  chainIds->(Utils.magic: array<int> => unknown)
@@ -616,7 +502,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
616
502
  app->get("/metrics", (_req, res) => {
617
503
  res->set("Content-Type", PromClient.defaultRegister->PromClient.getContentType)
618
504
  let _ =
619
- Metrics.collect(~state=indexerStateRef.contents)->Promise.thenResolve(metrics =>
505
+ Metrics.collect(~state=getIndexerState())->Promise.thenResolve(metrics =>
620
506
  res->endWithData(metrics)
621
507
  )
622
508
  })
@@ -709,7 +595,7 @@ let start = async (
709
595
  | Some(p) => p
710
596
  | None => PgStorage.makePersistenceFromConfig(~config)
711
597
  }
712
- globalPersistenceRef := Some(persistence)
598
+ setGlobalPersistence(persistence)
713
599
  await persistence->Persistence.init(
714
600
  ~reset,
715
601
  ~chainConfigs=config.chainMap->ChainMap.values,
@@ -756,7 +642,7 @@ let start = async (
756
642
 
757
643
  if !isTest {
758
644
  startServer(~persistence, ~isDevelopmentMode, ~getState=() =>
759
- switch indexerStateRef.contents {
645
+ switch getIndexerState() {
760
646
  | None => Initializing({})
761
647
  | Some(state) => {
762
648
  let chains =
@@ -786,7 +672,7 @@ let start = async (
786
672
  if shouldUseTui {
787
673
  let _rerender = Tui.start(~getState=() => state)
788
674
  }
789
- indexerStateRef := Some(state)
675
+ setIndexerState(state)
790
676
  state->IndexerLoop.start
791
677
  await runUntilFatalError
792
678
  }
package/src/Main.res.mjs CHANGED
@@ -13,6 +13,7 @@ import * as ChainMap from "./ChainMap.res.mjs";
13
13
  import * as PgStorage from "./PgStorage.res.mjs";
14
14
  import * as ChainState from "./ChainState.res.mjs";
15
15
  import * as Prometheus from "./Prometheus.res.mjs";
16
+ import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
16
17
  import * as IndexerLoop from "./IndexerLoop.res.mjs";
17
18
  import * as Persistence from "./Persistence.res.mjs";
18
19
  import * as PromClient from "prom-client";
@@ -61,28 +62,24 @@ let stateSchema = S$RescriptSchema.union([
61
62
  }))
62
63
  ]);
63
64
 
64
- let blockRangeSchema = S$RescriptSchema.strict(S$RescriptSchema.object(s => ({
65
- _gte: s.f("_gte", S$RescriptSchema.option(S$RescriptSchema.int)),
66
- _lte: s.f("_lte", S$RescriptSchema.option(S$RescriptSchema.int)),
67
- _every: s.f("_every", S$RescriptSchema.Option.getOr(S$RescriptSchema.option(S$RescriptSchema.intMin(S$RescriptSchema.int, 1, undefined)), 1))
68
- })));
65
+ function getIndexerState() {
66
+ return EnvioGlobal.value.indexerState;
67
+ }
69
68
 
70
- let defaultBlockRange = {
71
- _gte: undefined,
72
- _lte: undefined,
73
- _every: 1
74
- };
69
+ function setIndexerState(state) {
70
+ EnvioGlobal.value.indexerState = Primitive_option.some(state);
71
+ }
75
72
 
76
- let indexerStateRef = {
77
- contents: undefined
78
- };
73
+ function getGlobalPersistence() {
74
+ return EnvioGlobal.value.persistence;
75
+ }
79
76
 
80
- let globalPersistenceRef = {
81
- contents: undefined
82
- };
77
+ function setGlobalPersistence(persistence) {
78
+ EnvioGlobal.value.persistence = Primitive_option.some(persistence);
79
+ }
83
80
 
84
81
  function getInitialChainState(chainId) {
85
- let persistence = globalPersistenceRef.contents;
82
+ let persistence = EnvioGlobal.value.persistence;
86
83
  if (persistence === undefined) {
87
84
  return;
88
85
  }
@@ -134,7 +131,7 @@ function buildChainsObject(config) {
134
131
  }), "isRealtime", {
135
132
  enumerable: true,
136
133
  get: () => {
137
- let state = indexerStateRef.contents;
134
+ let state = EnvioGlobal.value.indexerState;
138
135
  if (state !== undefined) {
139
136
  return IndexerState.isRealtime(Primitive_option.valFromOption(state));
140
137
  } else if (Env.updateSyncTimeOnRestart) {
@@ -162,7 +159,7 @@ function buildChainsObject(config) {
162
159
  }), "addresses", {
163
160
  enumerable: true,
164
161
  get: () => {
165
- let state = indexerStateRef.contents;
162
+ let state = EnvioGlobal.value.indexerState;
166
163
  if (state !== undefined) {
167
164
  let chain = ChainMap.Chain.makeUnsafe(chainConfig.id);
168
165
  let chainState = IndexerState.getChainState(Primitive_option.valFromOption(state), chain);
@@ -278,69 +275,9 @@ function getGlobalIndexer() {
278
275
  HandlerRegister.throwIfFinishedRegistration("~internalAndWillBeRemovedSoon_onRollbackCommit");
279
276
  RollbackCommit.register(callback);
280
277
  };
281
- let extractRange = (filter, name, ecosystem) => {
282
- try {
283
- let inner = S$RescriptSchema.parseOrThrow(filter, ecosystem.onBlockFilterSchema);
284
- if (inner !== undefined) {
285
- return S$RescriptSchema.parseOrThrow(Primitive_option.valFromOption(inner), blockRangeSchema);
286
- } else {
287
- return defaultBlockRange;
288
- }
289
- } catch (raw_exn) {
290
- let exn = Primitive_exceptions.internalToException(raw_exn);
291
- if (exn.RE_EXN_ID === S$RescriptSchema.Raised) {
292
- return Stdlib_JsError.throwWithMessage(`\`indexer.` + ecosystem.onBlockMethodName + `("` + name + `")\` \`where\` returned an invalid filter: ` + Utils.prettifyExn(exn._1));
293
- }
294
- throw exn;
295
- }
296
- };
297
278
  let onBlockFn = (rawOptions, handler) => {
298
279
  HandlerRegister.throwIfFinishedRegistration("onBlock");
299
- let config = Config.load();
300
- let ecosystem = config.ecosystem;
301
- let match = buildChainsObject(config);
302
- let chains = match[0];
303
- let name = rawOptions.name;
304
- let logger = Logging.createChild({
305
- onBlock: name
306
- });
307
- let w = rawOptions.where;
308
- let where = w === undefined || w === null ? undefined : (
309
- typeof w === "function" ? rawOptions.where : Stdlib_JsError.throwWithMessage(`\`indexer.` + ecosystem.onBlockMethodName + `("` + name + `")\` expected \`where\` to be a function or omitted, but got ` + typeof w + `.`)
310
- );
311
- let matchedAny = {
312
- contents: false
313
- };
314
- ChainMap.values(config.chainMap).forEach(chainConfig => {
315
- let chainId = chainConfig.id;
316
- let chainObj = chains[chainId.toString()];
317
- let result = where !== undefined ? where({
318
- chain: chainObj
319
- }) : true;
320
- let match = result === true ? [
321
- true,
322
- defaultBlockRange
323
- ] : (
324
- result === false ? [
325
- false,
326
- defaultBlockRange
327
- ] : (
328
- typeof result === "object" && !Array.isArray(result) && result !== null ? [
329
- true,
330
- extractRange(result, name, ecosystem)
331
- ] : Stdlib_JsError.throwWithMessage(`\`indexer.` + ecosystem.onBlockMethodName + `("` + name + `")\` \`where\` predicate returned an invalid value of type ` + typeof result + `. Expected boolean or a filter object.`)
332
- )
333
- );
334
- if (!match[0]) {
335
- return;
336
- }
337
- let range = match[1];
338
- matchedAny.contents = true;
339
- HandlerRegister.registerOnBlock(name, chainId, range._every, range._gte, range._lte, handler);
340
- });
341
- if (!matchedAny.contents) {
342
- return Logging.childWarn(logger, `\`indexer.` + ecosystem.onBlockMethodName + `\` matched 0 chains. Check the \`where\` predicate.`);
343
- }
280
+ HandlerRegister.registerOnBlock(rawOptions.name, rawOptions.where, handler, config => buildChainsObject(config)[0]);
344
281
  };
345
282
  let keysMemo = {
346
283
  contents: undefined
@@ -478,7 +415,7 @@ function startServer(getState, persistence, isDevelopmentMode) {
478
415
  });
479
416
  app.get("/metrics", (_req, res) => {
480
417
  res.set("Content-Type", PromClient.register.contentType);
481
- Metrics.collect(indexerStateRef.contents).then(metrics => res.end(metrics));
418
+ Metrics.collect(EnvioGlobal.value.indexerState).then(metrics => res.end(metrics));
482
419
  });
483
420
  app.get("/metrics/runtime", (_req, res) => {
484
421
  res.set("Content-Type", runtimeRegistry.contentType);
@@ -529,7 +466,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
529
466
  let config = Config.load();
530
467
  let isDevelopmentMode = !isTest && config.isDev;
531
468
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
532
- globalPersistenceRef.contents = persistence$1;
469
+ EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
533
470
  await Persistence.init(persistence$1, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
534
471
  let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
535
472
  let config$1 = isTest ? ({
@@ -573,7 +510,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
573
510
  Prometheus.RollbackEnabled.set(config$2.shouldRollbackOnReorg);
574
511
  if (!isTest) {
575
512
  startServer(() => {
576
- let state = indexerStateRef.contents;
513
+ let state = EnvioGlobal.value.indexerState;
577
514
  if (state === undefined) {
578
515
  return {
579
516
  status: "initializing"
@@ -595,7 +532,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
595
532
  if (shouldUseTui) {
596
533
  Tui.start(() => state);
597
534
  }
598
- indexerStateRef.contents = Primitive_option.some(state);
535
+ EnvioGlobal.value.indexerState = Primitive_option.some(state);
599
536
  IndexerLoop.start(state);
600
537
  return await runUntilFatalError;
601
538
  }
@@ -603,10 +540,10 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
603
540
  export {
604
541
  chainDataSchema,
605
542
  stateSchema,
606
- blockRangeSchema,
607
- defaultBlockRange,
608
- indexerStateRef,
609
- globalPersistenceRef,
543
+ getIndexerState,
544
+ setIndexerState,
545
+ getGlobalPersistence,
546
+ setGlobalPersistence,
610
547
  getInitialChainState,
611
548
  buildChainsObject,
612
549
  getGlobalIndexer,
@@ -6,7 +6,10 @@
6
6
  type args = {chainId: int, rollbackToBlock: int}
7
7
  type callback = args => promise<unit>
8
8
 
9
- let callbacks: array<callback> = []
9
+ // Lives in the process-wide `EnvioGlobal` record so callbacks registered
10
+ // through a duplicate envio module instance still fire.
11
+ let callbacks =
12
+ EnvioGlobal.value.rollbackCommitCallbacks->(Utils.magic: array<unknown> => array<callback>)
10
13
 
11
14
  let register = (callback: callback) => {
12
15
  callbacks->Array.push(callback)
@@ -1,8 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
4
5
 
5
- let callbacks = [];
6
+ let callbacks = EnvioGlobal.value.rollbackCommitCallbacks;
6
7
 
7
8
  function register(callback) {
8
9
  callbacks.push(callback);
@@ -32,4 +33,4 @@ export {
32
33
  register,
33
34
  fire,
34
35
  }
35
- /* No side effect */
36
+ /* EnvioGlobal Not a pure module */
@@ -336,7 +336,7 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
336
336
  | None => seenCoordinates->Dict.set(coordinate, itemIndex)
337
337
  }
338
338
 
339
- // Build a real registration the same way `HandlerRegister.buildOnEventRegistrations`
339
+ // Build a real registration the same way `HandlerRegister.finishRegistration`
340
340
  // does at startup (not a stub), so the address filter and `where`
341
341
  // behave identically to real indexing — the dead-input tracker relies
342
342
  // on `clientAddressFilter` actually gating unrouted items.
@@ -90,8 +90,11 @@ let make = (~logger: Pino.t): Ecosystem.t => {
90
90
  // range chunk is validated by `eventBlockRangeSchema` in
91
91
  // `LogSelection.res` which rejects `_lte`/`_every` (use `onBlock` for
92
92
  // stride- and endBlock-based block handlers).
93
+ // `S.strict` on the inner object rejects unknown `block` fields (e.g. a
94
+ // `numbre` typo or `block: {timestamp: ...}`) instead of silently
95
+ // ignoring them.
93
96
  onEventBlockFilterSchema: S.object(s =>
94
- s.field("block", S.option(S.object(s2 => s2.field("number", S.unknown))))
97
+ s.field("block", S.option(S.object(s2 => s2.field("number", S.unknown))->S.strict))
95
98
  ),
96
99
  logger,
97
100
  // The payload carries `transaction` by batch prep (HyperSync) or inline
@@ -58,7 +58,7 @@ function make(logger) {
58
58
  cleanUpRawEventFieldsInPlace: cleanUpRawEventFieldsInPlace,
59
59
  onBlockMethodName: "onBlock",
60
60
  onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
61
- onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
61
+ onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
62
62
  logger: logger,
63
63
  toEvent: eventItem => eventItem.payload,
64
64
  toEventLogger: eventItem => Logging.createChildFrom(logger, {
@@ -35,8 +35,10 @@ let make = (~logger: Pino.t): Ecosystem.t => {
35
35
  // Analogous to EVM, but keyed by `block.height` instead of
36
36
  // `block.number`. See `Evm.res` for the rationale on the two-stage
37
37
  // parse and the `_lte`/`_every` rejection.
38
+ // `S.strict` on the inner object rejects unknown `block` fields — including
39
+ // `block.number`, which is EVM-only; Fuel filters by `block.height`.
38
40
  onEventBlockFilterSchema: S.object(s =>
39
- s.field("block", S.option(S.object(s2 => s2.field("height", S.unknown))))
41
+ s.field("block", S.option(S.object(s2 => s2.field("height", S.unknown))->S.strict))
40
42
  ),
41
43
  logger,
42
44
  toEvent: eventItem => eventItem.payload->(Utils.magic: Internal.eventPayload => Internal.event),
@@ -19,7 +19,7 @@ function make(logger) {
19
19
  cleanUpRawEventFieldsInPlace: cleanUpRawEventFieldsInPlace,
20
20
  onBlockMethodName: "onBlock",
21
21
  onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("height", S$RescriptSchema.unknown))))),
22
- onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("height", S$RescriptSchema.unknown))))),
22
+ onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("height", S$RescriptSchema.unknown)))))),
23
23
  logger: logger,
24
24
  toEvent: eventItem => eventItem.payload,
25
25
  toEventLogger: eventItem => Logging.createChildFrom(logger, {
@@ -7,13 +7,12 @@ type selectionConfig = {
7
7
  fieldSelection: HyperSyncClient.QueryTypes.fieldSelection,
8
8
  }
9
9
 
10
- let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
10
+ let getSelectionConfig = (selection: FetchState.selection) => {
11
11
  let capitalizedBlockFields = Utils.Set.make()
12
12
  let capitalizedTransactionFields = Utils.Set.make()
13
13
 
14
- let staticTopicSelectionsByContract = Dict.make()
15
- let dynamicEventFiltersByContract = Dict.make()
16
- let dynamicWildcardEventFiltersByContract = Dict.make()
14
+ let topicSelectionsByContract = Dict.make()
15
+ let wildcardTopicSelectionsByContract = Dict.make()
17
16
  let noAddressesTopicSelections = []
18
17
  let contractNames = Utils.Set.make()
19
18
 
@@ -24,7 +23,7 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
24
23
  reg.eventConfig->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig)
25
24
  let contractName = eventConfig.contractName
26
25
  let {selectedBlockFields, selectedTransactionFields} = eventConfig
27
- let {dependsOnAddresses, getEventFiltersOrThrow, isWildcard} = reg
26
+ let {dependsOnAddresses, resolvedWhere, isWildcard} = reg
28
27
  selectedBlockFields
29
28
  ->Utils.Set.toArray
30
29
  ->Array.forEach(name =>
@@ -44,24 +43,16 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
44
43
  }
45
44
  })
46
45
 
47
- let eventFilters = getEventFiltersOrThrow(chain)
48
46
  if dependsOnAddresses {
49
47
  let _ = contractNames->Utils.Set.add(contractName)
50
- switch eventFilters {
51
- | Static(topicSelections) =>
52
- staticTopicSelectionsByContract->Utils.Dict.pushMany(contractName, topicSelections)
53
- | Dynamic(fn) =>
54
- (
55
- isWildcard ? dynamicWildcardEventFiltersByContract : dynamicEventFiltersByContract
56
- )->Utils.Dict.push(contractName, fn)
57
- }
48
+
49
+ (
50
+ isWildcard ? wildcardTopicSelectionsByContract : topicSelectionsByContract
51
+ )->Utils.Dict.pushMany(contractName, resolvedWhere.topicSelections)
58
52
  } else {
59
53
  noAddressesTopicSelections
60
54
  ->Array.pushMany(
61
- switch eventFilters {
62
- | Static(s) => s
63
- | Dynamic(fn) => fn([])
64
- },
55
+ resolvedWhere.topicSelections->LogSelection.materializeTopicSelections(~addresses=[]),
65
56
  )
66
57
  ->ignore
67
58
  }
@@ -92,27 +83,26 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
92
83
  | None
93
84
  | Some([]) => ()
94
85
  | Some(addresses) =>
95
- switch staticTopicSelectionsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
86
+ switch topicSelectionsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
96
87
  | None => ()
97
88
  | Some(topicSelections) =>
98
- logSelections->Array.push(LogSelection.make(~addresses, ~topicSelections))
99
- }
100
- switch dynamicEventFiltersByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
101
- | None => ()
102
- | Some(fns) =>
103
89
  logSelections->Array.push(
104
- LogSelection.make(~addresses, ~topicSelections=fns->Array.flatMap(fn => fn(addresses))),
90
+ LogSelection.make(
91
+ ~addresses,
92
+ ~topicSelections=topicSelections->LogSelection.materializeTopicSelections(~addresses),
93
+ ),
105
94
  )
106
95
  }
107
- switch dynamicWildcardEventFiltersByContract->Utils.Dict.dangerouslyGetNonOption(
108
- contractName,
109
- ) {
96
+ // Wildcard events that filter an indexed param by registered addresses:
97
+ // the addresses fold into the topics, so the query itself stays
98
+ // address-unbound.
99
+ switch wildcardTopicSelectionsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
110
100
  | None => ()
111
- | Some(fns) =>
101
+ | Some(topicSelections) =>
112
102
  logSelections->Array.push(
113
103
  LogSelection.make(
114
104
  ~addresses=[],
115
- ~topicSelections=fns->Array.flatMap(fn => fn(addresses)),
105
+ ~topicSelections=topicSelections->LogSelection.materializeTopicSelections(~addresses),
116
106
  ),
117
107
  )
118
108
  }
@@ -127,8 +117,7 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
127
117
  }
128
118
  }
129
119
 
130
- let memoGetSelectionConfig = (~chain) =>
131
- Utils.WeakMap.memoize(selection => selection->getSelectionConfig(~chain))
120
+ let memoGetSelectionConfig = () => Utils.WeakMap.memoize(getSelectionConfig)
132
121
 
133
122
  // Surfaced by HyperSyncClient.getHeight (Rust) when HyperSync rejects the API
134
123
  // token. The corrupted-token test feeds the real server error through this
@@ -164,7 +153,7 @@ let make = (
164
153
  ): t => {
165
154
  let name = "HyperSync"
166
155
 
167
- let getSelectionConfig = memoGetSelectionConfig(~chain)
156
+ let getSelectionConfig = memoGetSelectionConfig()
168
157
 
169
158
  let apiToken = switch apiToken {
170
159
  | Some(token) => token