envio 3.3.0-alpha.0 → 3.3.0-alpha.10

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.
Files changed (140) hide show
  1. package/evm.schema.json +10 -0
  2. package/index.d.ts +45 -25
  3. package/package.json +7 -7
  4. package/src/Address.res +5 -2
  5. package/src/Address.res.mjs +3 -1
  6. package/src/Api.res +1 -1
  7. package/src/Api.res.mjs +1 -1
  8. package/src/Batch.res +0 -6
  9. package/src/Batch.res.mjs +1 -12
  10. package/src/BatchProcessing.res +7 -12
  11. package/src/BatchProcessing.res.mjs +8 -7
  12. package/src/Bin.res +3 -0
  13. package/src/Bin.res.mjs +4 -0
  14. package/src/ChainFetching.res +48 -16
  15. package/src/ChainFetching.res.mjs +36 -17
  16. package/src/ChainMetadata.res +1 -11
  17. package/src/ChainMetadata.res.mjs +1 -10
  18. package/src/ChainState.res +628 -149
  19. package/src/ChainState.res.mjs +440 -102
  20. package/src/ChainState.resi +74 -4
  21. package/src/Config.res +61 -33
  22. package/src/Config.res.mjs +50 -10
  23. package/src/ContractRegisterContext.res +2 -12
  24. package/src/ContractRegisterContext.res.mjs +3 -5
  25. package/src/Core.res +14 -3
  26. package/src/CrossChainState.res +142 -93
  27. package/src/CrossChainState.res.mjs +88 -60
  28. package/src/CrossChainState.resi +2 -10
  29. package/src/Ecosystem.res +5 -6
  30. package/src/Ecosystem.res.mjs +3 -3
  31. package/src/Env.res +0 -7
  32. package/src/Env.res.mjs +0 -6
  33. package/src/Envio.res +17 -11
  34. package/src/EnvioGlobal.res +53 -0
  35. package/src/EnvioGlobal.res.mjs +31 -0
  36. package/src/EventConfigBuilder.res +177 -88
  37. package/src/EventConfigBuilder.res.mjs +99 -42
  38. package/src/EventProcessing.res +74 -28
  39. package/src/EventProcessing.res.mjs +55 -29
  40. package/src/ExitOnCaughtUp.res +10 -2
  41. package/src/ExitOnCaughtUp.res.mjs +10 -1
  42. package/src/FetchState.res +558 -300
  43. package/src/FetchState.res.mjs +437 -423
  44. package/src/HandlerLoader.res +8 -104
  45. package/src/HandlerLoader.res.mjs +2 -81
  46. package/src/HandlerRegister.res +518 -144
  47. package/src/HandlerRegister.res.mjs +285 -133
  48. package/src/HandlerRegister.resi +24 -8
  49. package/src/IndexerLoop.res +2 -3
  50. package/src/IndexerLoop.res.mjs +1 -1
  51. package/src/IndexerState.res +10 -11
  52. package/src/IndexerState.res.mjs +14 -11
  53. package/src/IndexerState.resi +2 -5
  54. package/src/IndexingAddresses.res +108 -0
  55. package/src/IndexingAddresses.res.mjs +101 -0
  56. package/src/IndexingAddresses.resi +34 -0
  57. package/src/Internal.res +155 -46
  58. package/src/Internal.res.mjs +30 -2
  59. package/src/LoadLayer.res +5 -5
  60. package/src/LoadLayer.res.mjs +6 -6
  61. package/src/LogSelection.res +102 -165
  62. package/src/LogSelection.res.mjs +101 -116
  63. package/src/Main.res +71 -236
  64. package/src/Main.res.mjs +60 -145
  65. package/src/Metrics.res +74 -0
  66. package/src/Metrics.res.mjs +75 -0
  67. package/src/PgStorage.res +4 -4
  68. package/src/PgStorage.res.mjs +5 -5
  69. package/src/Prometheus.res +10 -74
  70. package/src/Prometheus.res.mjs +124 -186
  71. package/src/PruneStaleHistory.res +2 -2
  72. package/src/PruneStaleHistory.res.mjs +3 -3
  73. package/src/RawEvent.res +7 -2
  74. package/src/RawEvent.res.mjs +4 -4
  75. package/src/Rollback.res +3 -3
  76. package/src/Rollback.res.mjs +4 -4
  77. package/src/RollbackCommit.res +4 -1
  78. package/src/RollbackCommit.res.mjs +3 -2
  79. package/src/SimulateDeadInputTracker.res +85 -0
  80. package/src/SimulateDeadInputTracker.res.mjs +73 -0
  81. package/src/SimulateDeadInputTracker.resi +12 -0
  82. package/src/SimulateItems.res +91 -23
  83. package/src/SimulateItems.res.mjs +55 -32
  84. package/src/TestIndexer.res +54 -40
  85. package/src/TestIndexer.res.mjs +37 -30
  86. package/src/bindings/Performance.res +7 -0
  87. package/src/bindings/Performance.res.mjs +21 -0
  88. package/src/bindings/Performance.resi +7 -0
  89. package/src/bindings/Viem.res +0 -41
  90. package/src/bindings/Viem.res.mjs +1 -43
  91. package/src/sources/BlockStore.res +46 -0
  92. package/src/sources/BlockStore.res.mjs +24 -0
  93. package/src/sources/EventRouter.res +36 -33
  94. package/src/sources/EventRouter.res.mjs +13 -14
  95. package/src/sources/Evm.res +83 -72
  96. package/src/sources/Evm.res.mjs +53 -69
  97. package/src/sources/EvmChain.res +18 -20
  98. package/src/sources/EvmChain.res.mjs +15 -15
  99. package/src/sources/EvmRpcClient.res +74 -5
  100. package/src/sources/EvmRpcClient.res.mjs +13 -4
  101. package/src/sources/FieldMask.res +39 -0
  102. package/src/sources/FieldMask.res.mjs +42 -0
  103. package/src/sources/Fuel.res +9 -6
  104. package/src/sources/Fuel.res.mjs +5 -10
  105. package/src/sources/HyperFuelSource.res +46 -50
  106. package/src/sources/HyperFuelSource.res.mjs +64 -54
  107. package/src/sources/HyperSync.res +39 -7
  108. package/src/sources/HyperSync.res.mjs +57 -37
  109. package/src/sources/HyperSync.resi +12 -2
  110. package/src/sources/HyperSyncClient.res +22 -111
  111. package/src/sources/HyperSyncClient.res.mjs +2 -30
  112. package/src/sources/HyperSyncHeightStream.res +1 -8
  113. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  114. package/src/sources/HyperSyncSource.res +94 -107
  115. package/src/sources/HyperSyncSource.res.mjs +82 -86
  116. package/src/sources/Rpc.res +15 -47
  117. package/src/sources/Rpc.res.mjs +25 -56
  118. package/src/sources/RpcSource.res +320 -467
  119. package/src/sources/RpcSource.res.mjs +268 -385
  120. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  121. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  122. package/src/sources/SimulateSource.res +10 -4
  123. package/src/sources/SimulateSource.res.mjs +16 -6
  124. package/src/sources/Source.res +34 -6
  125. package/src/sources/SourceManager.res +86 -27
  126. package/src/sources/SourceManager.res.mjs +84 -47
  127. package/src/sources/SourceManager.resi +12 -3
  128. package/src/sources/Svm.res +31 -16
  129. package/src/sources/Svm.res.mjs +36 -13
  130. package/src/sources/SvmHyperSyncClient.res +5 -32
  131. package/src/sources/SvmHyperSyncSource.res +159 -134
  132. package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
  133. package/src/sources/TransactionStore.res +49 -0
  134. package/src/sources/TransactionStore.res.mjs +30 -0
  135. package/src/tui/Tui.res +13 -16
  136. package/src/tui/Tui.res.mjs +12 -14
  137. package/svm.schema.json +49 -37
  138. package/src/bindings/Hrtime.res +0 -58
  139. package/src/bindings/Hrtime.res.mjs +0 -90
  140. package/src/bindings/Hrtime.resi +0 -30
package/src/Env.res.mjs CHANGED
@@ -19,10 +19,6 @@ let targetBufferSize = EnvSafe.get(envSafe, "ENVIO_INDEXING_MAX_BUFFER_SIZE", S$
19
19
 
20
20
  let maxAddrInPartition = EnvSafe.get(envSafe, "MAX_PARTITION_SIZE", S$RescriptSchema.int, undefined, 5000, undefined, undefined);
21
21
 
22
- let maxBackfillConcurrency = EnvSafe.get(envSafe, "ENVIO_MAX_BACKFILL_CONCURRENCY", S$RescriptSchema.int, undefined, 30, undefined, undefined);
23
-
24
- let maxRealtimeConcurrency = EnvSafe.get(envSafe, "ENVIO_MAX_REALTIME_CONCURRENCY", S$RescriptSchema.int, undefined, 200, undefined, undefined);
25
-
26
22
  let inMemoryObjectsTarget = EnvSafe.get(envSafe, "ENVIO_IN_MEMORY_OBJECTS_TARGET", S$RescriptSchema.int, undefined, 100000, undefined, undefined);
27
23
 
28
24
  let serverPort = EnvSafe.get(envSafe, "ENVIO_INDEXER_PORT", S$RescriptSchema.port(S$RescriptSchema.int, undefined), undefined, EnvSafe.get(envSafe, "METRICS_PORT", S$RescriptSchema.port(S$RescriptSchema.int, undefined), undefined, 9898, undefined, undefined), undefined, undefined);
@@ -224,8 +220,6 @@ export {
224
220
  updateSyncTimeOnRestart,
225
221
  targetBufferSize,
226
222
  maxAddrInPartition,
227
- maxBackfillConcurrency,
228
- maxRealtimeConcurrency,
229
223
  inMemoryObjectsTarget,
230
224
  serverPort,
231
225
  tuiEnvVar,
package/src/Envio.res CHANGED
@@ -48,6 +48,7 @@ type svmTokenBalance = {
48
48
  }
49
49
 
50
50
  type svmTransaction = {
51
+ transactionIndex?: int,
51
52
  signatures: array<string>,
52
53
  feePayer?: SvmTypes.Pubkey.t,
53
54
  success?: bool,
@@ -65,18 +66,21 @@ type svmLog = {
65
66
  message: string,
66
67
  }
67
68
 
68
- /** Block context for a matched instruction. `time`/`hash` follow the
69
- EVM/Fuel field names so the shared `Ecosystem.t` getters in `Svm.res` read
70
- them uniformly. */
69
+ /** Block context for a matched instruction. `slot`/`hash` are always present;
70
+ `time` can still be absent HyperSync/Solana may not report a block time for
71
+ every slot. Every other field is opt-in via `field_selection.block_fields`,
72
+ materialised from the per-chain block store at batch prep. */
71
73
  type svmInstructionBlock = {
72
74
  /** Slot this instruction's block was matched in. */
73
75
  slot: int,
74
- /** Unix block time (seconds). `0` when HyperSync didn't return a block
75
- for this instruction's slot. */
76
- time: int,
77
- /** Block hash. Currently always empty — populated by the future
78
- reorg-guard `queryBlockHash(slot)` route. */
76
+ /** Unix block time (seconds). */
77
+ time?: int,
79
78
  hash: string,
79
+ /** Block height (distinct from slot). Absent when not selected via
80
+ `field_selection.block_fields`. */
81
+ height?: int,
82
+ parentSlot?: int,
83
+ parentHash?: string,
80
84
  }
81
85
 
82
86
  /** The per-instruction payload handlers receive as their `instruction`
@@ -104,13 +108,15 @@ type svmInstruction = {
104
108
  d8?: string,
105
109
  /** Borsh-decoded params view. See [[svmInstructionParams]]. */
106
110
  params?: svmInstructionParams,
107
- /** Parent transaction. Absent when the per-instruction
108
- `include_transaction` flag is `false`. */
111
+ /** Parent transaction. Carries only the fields selected via
112
+ `field_selection.transaction_fields`; absent when none are selected. */
109
113
  transaction?: svmTransaction,
110
114
  /** Program log entries scoped to this instruction. Absent when the
111
115
  per-instruction `include_logs` flag is `false`. */
112
116
  logs?: array<svmLog>,
113
- block: svmInstructionBlock,
117
+ // Omitted on construction; materialised onto the payload at batch prep,
118
+ // before a handler ever reads it.
119
+ block?: svmInstructionBlock,
114
120
  }
115
121
 
116
122
  /** Arguments passed to handlers registered via `indexer.onInstruction`. */
@@ -0,0 +1,53 @@
1
+ // The single process-wide mutable state record, stashed on `globalThis` so a
2
+ // duplicate envio module instance — e.g. when the CLI's `bin.mjs` resolves
3
+ // envio from one path but the user's handlers resolve it from
4
+ // `node_modules/envio` — shares one state. Without this, each copy keeps its
5
+ // own module-level state: registration would read an empty registry and the
6
+ // `indexer.chains` getters would silently fall back to static config values.
7
+ //
8
+ // Slots are opaque (`unknown`) so this module stays at the bottom of the
9
+ // dependency graph; each owning module casts its slot to the real type once
10
+ // (`HandlerRegister` for the registration slots, `Main` for the runtime
11
+ // slots, `RollbackCommit` for its callbacks).
12
+ //
13
+ // Version-gated: the slot shapes can evolve between envio versions, so the
14
+ // guard uses strict full-version equality. On mismatch we throw with a
15
+ // deduplication hint instead of silently mixing shapes across builds.
16
+ type t = {
17
+ version: string,
18
+ eventRegistrations: dict<unknown>,
19
+ mutable activeRegistration: option<unknown>,
20
+ preRegistered: array<unknown>,
21
+ rollbackCommitCallbacks: array<unknown>,
22
+ mutable indexerState: option<unknown>,
23
+ mutable persistence: option<unknown>,
24
+ }
25
+
26
+ // Record type with `mutable` so assignment typechecks; ReScript keeps the
27
+ // field name verbatim in the generated JS so the globalThis slot is
28
+ // `__envioGlobal`.
29
+ type globalThis = {mutable __envioGlobal: Nullable.t<t>}
30
+ @val external globalThis: globalThis = "globalThis"
31
+
32
+ let value: t = {
33
+ let version = Utils.EnvioPackage.value.version
34
+ switch globalThis.__envioGlobal->Nullable.toOption {
35
+ | Some(existing) if existing.version === version => existing
36
+ | Some(existing) =>
37
+ JsError.throwWithMessage(
38
+ `Multiple incompatible envio versions loaded in the same process: ${existing.version} and ${version}. Deduplicate the 'envio' dependency in your project.`,
39
+ )
40
+ | None =>
41
+ let fresh = {
42
+ version,
43
+ eventRegistrations: Dict.make(),
44
+ activeRegistration: None,
45
+ preRegistered: [],
46
+ rollbackCommitCallbacks: [],
47
+ indexerState: None,
48
+ persistence: None,
49
+ }
50
+ globalThis.__envioGlobal = Nullable.make(fresh)
51
+ fresh
52
+ }
53
+ }
@@ -0,0 +1,31 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Utils from "./Utils.res.mjs";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+
6
+ let version = Utils.EnvioPackage.value.version;
7
+
8
+ let existing = globalThis.__envioGlobal;
9
+
10
+ let value;
11
+
12
+ if (existing == null) {
13
+ let fresh = {
14
+ version: version,
15
+ eventRegistrations: {},
16
+ activeRegistration: undefined,
17
+ preRegistered: [],
18
+ rollbackCommitCallbacks: [],
19
+ indexerState: undefined,
20
+ persistence: undefined
21
+ };
22
+ globalThis.__envioGlobal = fresh;
23
+ value = fresh;
24
+ } else {
25
+ value = existing.version === version ? existing : Stdlib_JsError.throwWithMessage(`Multiple incompatible envio versions loaded in the same process: ` + existing.version + ` and ` + version + `. Deduplicate the 'envio' dependency in your project.`);
26
+ }
27
+
28
+ export {
29
+ value,
30
+ }
31
+ /* existing Not a pure module */
@@ -243,9 +243,13 @@ let getTopicEncoder = (abiType: string): (unknown => EvmTypes.Hex.t) => {
243
243
  } else {
244
244
  switch abiType {
245
245
  | "address" =>
246
- TopicFilter.fromAddress->(
247
- Utils.magic: (Address.t => EvmTypes.Hex.t) => unknown => EvmTypes.Hex.t
248
- )
246
+ // Lowercase before encoding so mixed-case (checksummed) user input
247
+ // still matches the lowercase hex topics returned by sources.
248
+
249
+ (
250
+ (value: string) =>
251
+ value->String.toLowerCase->Address.unsafeFromString->TopicFilter.fromAddress
252
+ )->(Utils.magic: (string => EvmTypes.Hex.t) => unknown => EvmTypes.Hex.t)
249
253
 
250
254
  | "bool" =>
251
255
  TopicFilter.fromBool->(Utils.magic: (bool => EvmTypes.Hex.t) => unknown => EvmTypes.Hex.t)
@@ -306,7 +310,14 @@ let resolveFieldSelection = (
306
310
  | Some(fields) => Utils.Set.fromArray(fields)
307
311
  | None => globalTransactionFieldsSet
308
312
  }
309
- (selectedBlockFields, selectedTransactionFields)
313
+ // The base eventConfig stores these as a string set (field names match the
314
+ // typed variants at runtime).
315
+ (
316
+ selectedBlockFields,
317
+ selectedTransactionFields->(
318
+ Utils.magic: Utils.Set.t<Internal.evmTransactionField> => Utils.Set.t<string>
319
+ ),
320
+ )
310
321
  }
311
322
 
312
323
  // ============== Client-side address filter ==============
@@ -319,28 +330,50 @@ let compileAddressFilter: string => (
319
330
  return new Function("event", "blockNumber", "indexingAddresses", body);
320
331
  }`)
321
332
 
322
- // Body of the client-side address filter for a DNF of address-filtered param
323
- // names (OR of AND-groups): keep the event only if some group's params are all
324
- // registered at or before the log's block. The DNF is fixed here, so it's
325
- // unrolled into one boolean expression no per-event closure, loop, or array.
326
- // `None` when there's no address-param filter. Exposed for snapshotting.
327
- let buildAddressFilterBody = (groups: array<array<string>>): option<string> => {
328
- switch groups {
333
+ // Body of the client-side address filter. Two analogous registered-at-or-before
334
+ // checks, ANDed: (1) for non-wildcard events, the log's srcAddress must itself be
335
+ // registered (ownership is resolved structurally by partition, but the temporal
336
+ // `effectiveStartBlock` gate lives here now); (2) a DNF of address-filtered param
337
+ // names (OR of AND-groups) for events that filter an indexed address param. The
338
+ // DNF is fixed here, so it's unrolled into one boolean expression — no per-event
339
+ // closure, loop, or array. `None` only for wildcard events without a param
340
+ // filter. Exposed for snapshotting.
341
+ // `srcAddressExpr` is the JS expression for the event's owning address: EVM and
342
+ // Fuel events expose `event.srcAddress`; SVM instructions expose `event.programId`.
343
+ let buildAddressFilterBody = (
344
+ groups: array<array<string>>,
345
+ ~isWildcard: bool,
346
+ ~srcAddressExpr: string="event.srcAddress",
347
+ ): option<string> => {
348
+ let paramLeaf = name =>
349
+ `(ic = indexingAddresses[p[${JSON.stringify(
350
+ JSON.String(name),
351
+ )}]]) !== undefined && ic.effectiveStartBlock <= blockNumber`
352
+ let paramDnf = switch groups {
329
353
  | [] => None
330
354
  | _ =>
331
- let leaf = name =>
332
- `(ic = indexingAddresses[p[${JSON.stringify(
333
- JSON.String(name),
334
- )}]]) !== undefined && ic.effectiveStartBlock <= blockNumber`
335
- let groupExprs =
336
- groups->Array.map(group => "(" ++ group->Array.map(leaf)->Array.join(" && ") ++ ")")
337
- Some("var p = event.params, ic; return " ++ groupExprs->Array.join(" || ") ++ ";")
355
+ Some(
356
+ groups
357
+ ->Array.map(group => "(" ++ group->Array.map(paramLeaf)->Array.join(" && ") ++ ")")
358
+ ->Array.join(" || "),
359
+ )
360
+ }
361
+ let srcLeaf = `(ic = indexingAddresses[${srcAddressExpr}]) !== undefined && ic.effectiveStartBlock <= blockNumber`
362
+ switch (isWildcard, paramDnf) {
363
+ | (true, None) => None
364
+ | (true, Some(dnf)) => Some("var p = event.params, ic; return " ++ dnf ++ ";")
365
+ | (false, None) => Some("var ic; return " ++ srcLeaf ++ ";")
366
+ | (false, Some(dnf)) =>
367
+ Some("var p = event.params, ic; return " ++ srcLeaf ++ " && (" ++ dnf ++ ");")
338
368
  }
339
369
  }
340
370
 
341
- let buildAddressFilter = (groups: array<array<string>>): option<
342
- (Internal.eventPayload, int, dict<Internal.indexingContract>) => bool,
343
- > => buildAddressFilterBody(groups)->Option.map(compileAddressFilter)
371
+ let buildAddressFilter = (
372
+ groups: array<array<string>>,
373
+ ~isWildcard: bool,
374
+ ~srcAddressExpr: string="event.srcAddress",
375
+ ): option<(Internal.eventPayload, int, dict<Internal.indexingContract>) => bool> =>
376
+ buildAddressFilterBody(groups, ~isWildcard, ~srcAddressExpr)->Option.map(compileAddressFilter)
344
377
 
345
378
  // ============== Build complete EVM event config ==============
346
379
 
@@ -349,46 +382,12 @@ let buildEvmEventConfig = (
349
382
  ~eventName: string,
350
383
  ~sighash: string,
351
384
  ~params: array<paramMeta>,
352
- ~isWildcard: bool,
353
- ~handler: option<Internal.handler>,
354
- ~contractRegister: option<Internal.contractRegister>,
355
- ~eventFilters: option<JSON.t>,
356
- ~probeChainId: int,
357
- ~onEventBlockFilterSchema: S.t<option<unknown>>,
358
385
  ~blockFields: option<array<Internal.evmBlockField>>=?,
359
386
  ~transactionFields: option<array<Internal.evmTransactionField>>=?,
360
- ~startBlock: option<int>=?,
361
387
  ~globalBlockFieldsSet: Utils.Set.t<Internal.evmBlockField>=Utils.Set.make(),
362
388
  ~globalTransactionFieldsSet: Utils.Set.t<Internal.evmTransactionField>=Utils.Set.make(),
363
389
  ): Internal.evmEventConfig => {
364
390
  let topicCount = params->Array.reduce(1, (acc, p) => p.indexed ? acc + 1 : acc)
365
- let indexedParams = params->Array.filter(p => p.indexed)
366
-
367
- let {
368
- getEventFiltersOrThrow,
369
- filterByAddresses,
370
- addressFilterParamGroups,
371
- startBlock: whereStartBlock,
372
- } = LogSelection.parseEventFiltersOrThrow(
373
- ~eventFilters,
374
- ~sighash,
375
- ~params=indexedParams->Array.map(p => p.name),
376
- ~contractName,
377
- ~probeChainId,
378
- ~onEventBlockFilterSchema,
379
- ~topic1=?indexedParams->Array.get(0)->Option.map(buildTopicGetter),
380
- ~topic2=?indexedParams->Array.get(1)->Option.map(buildTopicGetter),
381
- ~topic3=?indexedParams->Array.get(2)->Option.map(buildTopicGetter),
382
- )
383
-
384
- // `where.block.number._gte` overrides the contract-level startBlock
385
- // when present. The user opts into this explicitly in handler code so
386
- // it should win over the `config.yaml` contract `start_block`; absent
387
- // `where.block`, the caller's contract-level value passes through.
388
- let resolvedStartBlock = switch whereStartBlock {
389
- | Some(_) as sb => sb
390
- | None => startBlock
391
- }
392
391
 
393
392
  let (selectedBlockFields, selectedTransactionFields) = resolveFieldSelection(
394
393
  ~blockFields,
@@ -401,25 +400,75 @@ let buildEvmEventConfig = (
401
400
  id: sighash ++ "_" ++ topicCount->Int.toString,
402
401
  name: eventName,
403
402
  contractName,
404
- isWildcard,
405
- handler,
406
- contractRegister,
407
403
  paramsRawEventSchema: buildParamsSchema(params),
408
404
  simulateParamsSchema: buildSimulateParamsSchema(params),
409
- getEventFiltersOrThrow,
410
- filterByAddresses,
411
- clientAddressFilter: ?buildAddressFilter(addressFilterParamGroups),
412
- dependsOnAddresses: !isWildcard || filterByAddresses,
413
- startBlock: resolvedStartBlock,
414
405
  selectedBlockFields,
415
406
  selectedTransactionFields,
407
+ transactionFieldMask: Evm.eventTransactionFieldMask(selectedTransactionFields),
408
+ blockFieldMask: Evm.eventBlockFieldMask(
409
+ selectedBlockFields->(
410
+ Utils.magic: Utils.Set.t<Internal.evmBlockField> => Utils.Set.t<string>
411
+ ),
412
+ ),
416
413
  sighash,
417
414
  topicCount,
418
415
  paramsMetadata: params,
419
416
  }
420
417
  }
421
418
 
422
- // ============== Build Fuel event config ==============
419
+ // Enrich an EVM definition into a per-(event,chain) registration: resolve the
420
+ // registered `where` for this chain into `resolvedWhere` + address filters,
421
+ // and override `startBlock` with `where.block._gte`.
422
+ let buildEvmOnEventRegistration = (
423
+ ~eventConfig: Internal.evmEventConfig,
424
+ ~isWildcard: bool,
425
+ ~handler: option<Internal.handler>,
426
+ ~contractRegister: option<Internal.contractRegister>,
427
+ ~where: option<JSON.t>,
428
+ ~chainId: int,
429
+ ~onEventBlockFilterSchema: S.t<option<unknown>>,
430
+ ~startBlock: option<int>=?,
431
+ ): Internal.evmOnEventRegistration => {
432
+ let indexedParams = eventConfig.paramsMetadata->Array.filter(p => p.indexed)
433
+
434
+ let {resolvedWhere, filterByAddresses, addressFilterParamGroups} = LogSelection.parseWhereOrThrow(
435
+ ~where,
436
+ ~sighash=eventConfig.sighash,
437
+ ~params=indexedParams->Array.map(p => p.name),
438
+ ~contractName=eventConfig.contractName,
439
+ ~chainId,
440
+ ~onEventBlockFilterSchema,
441
+ ~topic1=?indexedParams->Array.get(0)->Option.map(buildTopicGetter),
442
+ ~topic2=?indexedParams->Array.get(1)->Option.map(buildTopicGetter),
443
+ ~topic3=?indexedParams->Array.get(2)->Option.map(buildTopicGetter),
444
+ )
445
+
446
+ // `where.block.number._gte` overrides the contract-level startBlock when
447
+ // present (an explicit per-event opt-in that wins over `config.yaml`);
448
+ // otherwise the contract/chain value passes through.
449
+ let resolvedStartBlock = switch resolvedWhere.startBlock {
450
+ | Some(_) as sb => sb
451
+ | None => startBlock
452
+ }
453
+
454
+ {
455
+ eventConfig: (eventConfig :> Internal.eventConfig),
456
+ isWildcard,
457
+ handler,
458
+ contractRegister,
459
+ resolvedWhere,
460
+ filterByAddresses,
461
+ clientAddressFilter: ?buildAddressFilter(addressFilterParamGroups, ~isWildcard),
462
+ dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses),
463
+ startBlock: resolvedStartBlock,
464
+ }
465
+ }
466
+
467
+ // ============== Build SVM instruction event config ==============
468
+
469
+ // Always-included block fields (slot, time, hash) are prepended at runtime so
470
+ // they're always present regardless of config.
471
+ let alwaysIncludedSvmBlockFields: array<Internal.svmBlockField> = [Slot, Time, Hash]
423
472
 
424
473
  let buildSvmInstructionEventConfig = (
425
474
  ~contractName: string,
@@ -427,23 +476,32 @@ let buildSvmInstructionEventConfig = (
427
476
  ~programId: SvmTypes.Pubkey.t,
428
477
  ~discriminator: option<string>,
429
478
  ~discriminatorByteLen: int,
430
- ~includeTransaction: bool,
431
479
  ~includeLogs: bool,
432
- ~includeTokenBalances: bool,
480
+ ~transactionFields: array<Internal.svmTransactionField>=[],
481
+ ~blockFields: array<Internal.svmBlockField>=[],
433
482
  ~accountFilters: array<Internal.svmAccountFilterGroup>,
434
483
  ~isInner: option<bool>,
435
- ~isWildcard: bool,
436
- ~handler: option<Internal.handler>,
437
- ~contractRegister: option<Internal.contractRegister>,
438
484
  ~accounts: array<string>=[],
439
485
  ~args: JSON.t=JSON.Null,
440
486
  ~definedTypes: JSON.t=JSON.Null,
441
- ~startBlock: option<int>=?,
442
487
  ): Internal.svmInstructionEventConfig => {
443
488
  let paramsSchema =
444
489
  S.json(~validate=false)
445
490
  ->Utils.Schema.coerceToJsonPgType
446
491
  ->(Utils.magic: S.t<JSON.t> => S.t<Internal.eventParams>)
492
+
493
+ // The base eventConfig stores these as a string set (field names match the
494
+ // typed variants at runtime).
495
+ let selectedTransactionFields =
496
+ Utils.Set.fromArray(transactionFields)->(
497
+ Utils.magic: Utils.Set.t<Internal.svmTransactionField> => Utils.Set.t<string>
498
+ )
499
+ let selectedBlockFields = Utils.Set.fromArray(
500
+ Array.concat(alwaysIncludedSvmBlockFields, blockFields),
501
+ )
502
+ let blockFieldMask = Svm.eventBlockFieldMask(
503
+ selectedBlockFields->(Utils.magic: Utils.Set.t<Internal.svmBlockField> => Utils.Set.t<string>),
504
+ )
447
505
  {
448
506
  id: switch discriminator {
449
507
  | Some(d) => d
@@ -451,20 +509,16 @@ let buildSvmInstructionEventConfig = (
451
509
  },
452
510
  name: instructionName,
453
511
  contractName,
454
- isWildcard,
455
- handler,
456
- contractRegister,
457
512
  paramsRawEventSchema: paramsSchema,
458
513
  simulateParamsSchema: paramsSchema,
459
- filterByAddresses: false,
460
- dependsOnAddresses: !isWildcard,
461
- startBlock,
462
514
  programId,
463
515
  discriminator,
464
516
  discriminatorByteLen,
465
- includeTransaction,
466
517
  includeLogs,
467
- includeTokenBalances,
518
+ selectedTransactionFields,
519
+ transactionFieldMask: Svm.eventTransactionFieldMask(selectedTransactionFields),
520
+ selectedBlockFields,
521
+ blockFieldMask,
468
522
  accountFilters,
469
523
  isInner,
470
524
  accounts,
@@ -473,16 +527,33 @@ let buildSvmInstructionEventConfig = (
473
527
  }
474
528
  }
475
529
 
530
+ // Enrich an SVM definition into a registration. SVM has no `where`; only the
531
+ // handler binding + wildcard-derived address gate are registration state.
532
+ let buildSvmOnEventRegistration = (
533
+ ~eventConfig: Internal.svmInstructionEventConfig,
534
+ ~isWildcard: bool,
535
+ ~handler: option<Internal.handler>,
536
+ ~contractRegister: option<Internal.contractRegister>,
537
+ ~startBlock: option<int>=?,
538
+ ): Internal.svmOnEventRegistration => {
539
+ eventConfig: (eventConfig :> Internal.eventConfig),
540
+ handler,
541
+ contractRegister,
542
+ isWildcard,
543
+ filterByAddresses: false,
544
+ dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
545
+ clientAddressFilter: ?buildAddressFilter([], ~isWildcard, ~srcAddressExpr="event.programId"),
546
+ startBlock,
547
+ }
548
+
549
+ // ============== Build Fuel event config ==============
550
+
476
551
  let buildFuelEventConfig = (
477
552
  ~contractName: string,
478
553
  ~eventName: string,
479
554
  ~kind: string,
480
555
  ~sighash: string,
481
556
  ~rawAbi: JSON.t,
482
- ~isWildcard: bool,
483
- ~handler: option<Internal.handler>,
484
- ~contractRegister: option<Internal.contractRegister>,
485
- ~startBlock: option<int>=?,
486
557
  ): Internal.fuelEventConfig => {
487
558
  let fuelKind = switch kind {
488
559
  | "logData" =>
@@ -522,14 +593,32 @@ let buildFuelEventConfig = (
522
593
  },
523
594
  name: eventName,
524
595
  contractName,
525
- isWildcard,
526
- handler,
527
- contractRegister,
528
596
  paramsRawEventSchema: paramsSchema,
529
597
  simulateParamsSchema: paramsSchema,
530
- filterByAddresses: false,
531
- dependsOnAddresses: !isWildcard,
532
- startBlock,
598
+ // Fuel keeps the transaction and block inline on the payload; nothing to
599
+ // materialise.
600
+ selectedTransactionFields: Utils.Set.make(),
601
+ transactionFieldMask: 0.,
602
+ blockFieldMask: 0.,
533
603
  kind: fuelKind,
534
604
  }
535
605
  }
606
+
607
+ // Enrich a Fuel definition into a registration (handler binding +
608
+ // wildcard-derived address gate; Fuel never filters by addresses).
609
+ let buildFuelOnEventRegistration = (
610
+ ~eventConfig: Internal.fuelEventConfig,
611
+ ~isWildcard: bool,
612
+ ~handler: option<Internal.handler>,
613
+ ~contractRegister: option<Internal.contractRegister>,
614
+ ~startBlock: option<int>=?,
615
+ ): Internal.fuelOnEventRegistration => {
616
+ eventConfig: (eventConfig :> Internal.eventConfig),
617
+ handler,
618
+ contractRegister,
619
+ isWildcard,
620
+ filterByAddresses: false,
621
+ dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
622
+ clientAddressFilter: ?buildAddressFilter([], ~isWildcard),
623
+ startBlock,
624
+ }