envio 3.3.0-alpha.8 → 3.3.0-rc.0

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 (107) hide show
  1. package/index.d.ts +19 -0
  2. package/licenses/CLA.md +35 -0
  3. package/licenses/EULA.md +67 -0
  4. package/licenses/LICENSE.md +45 -0
  5. package/licenses/README.md +35 -0
  6. package/package.json +9 -8
  7. package/src/Batch.res.mjs +1 -1
  8. package/src/BatchProcessing.res +0 -7
  9. package/src/BatchProcessing.res.mjs +1 -8
  10. package/src/ChainFetching.res +30 -18
  11. package/src/ChainFetching.res.mjs +23 -13
  12. package/src/ChainState.res +205 -59
  13. package/src/ChainState.res.mjs +130 -27
  14. package/src/ChainState.resi +16 -1
  15. package/src/Config.res +9 -5
  16. package/src/Config.res.mjs +2 -2
  17. package/src/Core.res +20 -0
  18. package/src/Core.res.mjs +12 -0
  19. package/src/CrossChainState.res +147 -39
  20. package/src/CrossChainState.res.mjs +42 -10
  21. package/src/EffectState.res +100 -0
  22. package/src/EffectState.res.mjs +74 -0
  23. package/src/EffectState.resi +32 -0
  24. package/src/Envio.res +25 -7
  25. package/src/Envio.res.mjs +15 -19
  26. package/src/EnvioGlobal.res +53 -0
  27. package/src/EnvioGlobal.res.mjs +31 -0
  28. package/src/EventConfigBuilder.res +30 -56
  29. package/src/EventConfigBuilder.res.mjs +24 -30
  30. package/src/EventProcessing.res +8 -5
  31. package/src/EventProcessing.res.mjs +2 -1
  32. package/src/FetchState.res +842 -474
  33. package/src/FetchState.res.mjs +545 -356
  34. package/src/HandlerLoader.res +1 -1
  35. package/src/HandlerLoader.res.mjs +1 -1
  36. package/src/HandlerRegister.res +478 -305
  37. package/src/HandlerRegister.res.mjs +277 -209
  38. package/src/HandlerRegister.resi +11 -6
  39. package/src/InMemoryStore.res +14 -26
  40. package/src/InMemoryStore.res.mjs +6 -19
  41. package/src/IndexerState.res +15 -39
  42. package/src/IndexerState.res.mjs +18 -29
  43. package/src/IndexerState.resi +2 -10
  44. package/src/Internal.res +121 -18
  45. package/src/Internal.res.mjs +96 -2
  46. package/src/LoadLayer.res +44 -24
  47. package/src/LoadLayer.res.mjs +43 -20
  48. package/src/LoadLayer.resi +1 -0
  49. package/src/LogSelection.res +92 -217
  50. package/src/LogSelection.res.mjs +95 -184
  51. package/src/Main.res +29 -143
  52. package/src/Main.res.mjs +25 -88
  53. package/src/Metrics.res +1 -1
  54. package/src/Metrics.res.mjs +1 -1
  55. package/src/Persistence.res +12 -3
  56. package/src/PgStorage.res +155 -82
  57. package/src/PgStorage.res.mjs +130 -77
  58. package/src/Prometheus.res +20 -10
  59. package/src/Prometheus.res.mjs +22 -10
  60. package/src/PruneStaleHistory.res +146 -35
  61. package/src/PruneStaleHistory.res.mjs +114 -20
  62. package/src/RawEvent.res +2 -2
  63. package/src/Rollback.res +32 -13
  64. package/src/Rollback.res.mjs +24 -11
  65. package/src/RollbackCommit.res +4 -1
  66. package/src/RollbackCommit.res.mjs +3 -2
  67. package/src/SimulateDeadInputTracker.res +1 -1
  68. package/src/SimulateItems.res +39 -7
  69. package/src/SimulateItems.res.mjs +28 -9
  70. package/src/TestIndexer.res +2 -2
  71. package/src/TestIndexer.res.mjs +2 -2
  72. package/src/TopicFilter.res +1 -25
  73. package/src/TopicFilter.res.mjs +0 -74
  74. package/src/UserContext.res +62 -23
  75. package/src/UserContext.res.mjs +26 -6
  76. package/src/Writing.res +60 -21
  77. package/src/Writing.res.mjs +27 -9
  78. package/src/bindings/NodeJs.res +5 -0
  79. package/src/bindings/Viem.res +0 -5
  80. package/src/sources/EventRouter.res +0 -21
  81. package/src/sources/EventRouter.res.mjs +0 -12
  82. package/src/sources/Evm.res +4 -1
  83. package/src/sources/Evm.res.mjs +1 -1
  84. package/src/sources/EvmChain.res +2 -27
  85. package/src/sources/EvmChain.res.mjs +2 -23
  86. package/src/sources/EvmRpcClient.res +58 -23
  87. package/src/sources/EvmRpcClient.res.mjs +11 -5
  88. package/src/sources/Fuel.res +3 -1
  89. package/src/sources/Fuel.res.mjs +1 -1
  90. package/src/sources/HyperSync.res +9 -38
  91. package/src/sources/HyperSync.res.mjs +16 -28
  92. package/src/sources/HyperSync.resi +2 -2
  93. package/src/sources/HyperSyncClient.res +88 -11
  94. package/src/sources/HyperSyncClient.res.mjs +39 -6
  95. package/src/sources/HyperSyncSource.res +18 -210
  96. package/src/sources/HyperSyncSource.res.mjs +9 -137
  97. package/src/sources/Rpc.res +0 -32
  98. package/src/sources/Rpc.res.mjs +1 -46
  99. package/src/sources/RpcSource.res +129 -533
  100. package/src/sources/RpcSource.res.mjs +161 -379
  101. package/src/sources/SimulateSource.res +37 -19
  102. package/src/sources/SimulateSource.res.mjs +27 -10
  103. package/src/sources/SourceManager.res +4 -14
  104. package/src/sources/SourceManager.res.mjs +3 -9
  105. package/src/sources/SourceManager.resi +0 -2
  106. package/src/sources/SvmHyperSyncSource.res +13 -3
  107. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
@@ -1,76 +1,30 @@
1
- exception MissingRequiredTopic0
2
- let makeTopicSelection = (~topic0, ~topic1=[], ~topic2=[], ~topic3=[]) =>
3
- if topic0->Utils.Array.isEmpty {
4
- Error(MissingRequiredTopic0)
5
- } else {
6
- {
7
- Internal.topic0,
8
- topic1,
9
- topic2,
10
- topic3,
11
- }->Ok
1
+ // Expand a resolved topic selection into concrete topic values for a query:
2
+ // `ContractAddresses` markers become the given partition addresses encoded as
3
+ // topics; `Values` pass through.
4
+ let materializeTopicFilter = (filter: Internal.topicFilter, ~addresses: array<Address.t>) =>
5
+ switch filter {
6
+ | Values(values) => values
7
+ | ContractAddresses(_) => addresses->Array.map(TopicFilter.fromAddress)
12
8
  }
13
9
 
14
- let hasFilters = ({topic1, topic2, topic3}: Internal.topicSelection) => {
15
- [topic1, topic2, topic3]->Array.find(topic => !Utils.Array.isEmpty(topic))->Option.isSome
16
- }
17
-
18
- /**
19
- For a group of topic selections, if multiple only use topic0, then they can be compressed into one
20
- selection combining the topic0s
21
- */
22
- let compressTopicSelections = (topicSelections: array<Internal.topicSelection>) => {
23
- let topic0sOfSelectionsWithoutFilters = []
24
-
25
- let selectionsWithFilters = []
26
-
27
- topicSelections->Array.forEach(selection => {
28
- if selection->hasFilters {
29
- selectionsWithFilters->Array.push(selection)->ignore
30
- } else {
31
- selection.topic0->Array.forEach(topic0 => {
32
- topic0sOfSelectionsWithoutFilters->Array.push(topic0)->ignore
33
- })
34
- }
10
+ let materializeTopicSelections = (
11
+ topicSelections: array<Internal.resolvedTopicSelection>,
12
+ ~addresses: array<Address.t>,
13
+ ): array<Internal.topicSelection> =>
14
+ topicSelections->Array.map(({topic0, topic1, topic2, topic3}): Internal.topicSelection => {
15
+ topic0,
16
+ topic1: topic1->materializeTopicFilter(~addresses),
17
+ topic2: topic2->materializeTopicFilter(~addresses),
18
+ topic3: topic3->materializeTopicFilter(~addresses),
35
19
  })
36
20
 
37
- switch topic0sOfSelectionsWithoutFilters {
38
- | [] => selectionsWithFilters
39
- | topic0 =>
40
- let selectionWithoutFilters = {
41
- Internal.topic0,
42
- topic1: [],
43
- topic2: [],
44
- topic3: [],
45
- }
46
- Array.concat([selectionWithoutFilters], selectionsWithFilters)
47
- }
48
- }
49
-
50
- type t = {
51
- addresses: array<Address.t>,
52
- topicSelections: array<Internal.topicSelection>,
53
- }
54
-
55
- let make = (~addresses, ~topicSelections) => {
56
- let topicSelections = compressTopicSelections(topicSelections)
57
- {addresses, topicSelections}
58
- }
59
-
60
- type parsedEventFilters = {
61
- getEventFiltersOrThrow: ChainMap.Chain.t => Internal.eventFilters,
21
+ type parsedWhere = {
22
+ resolvedWhere: Internal.resolvedWhere,
62
23
  filterByAddresses: bool,
63
24
  // Indexed params filtered by `chain.<Contract>.addresses`, in disjunctive
64
25
  // normal form (outer array OR of AND-groups). Empty unless `filterByAddresses`.
65
26
  // Consumed by the codegen of the event's `clientAddressFilter`.
66
27
  addressFilterParamGroups: array<array<string>>,
67
- // `_gte` from the top-level `block` filter of the user's `where`,
68
- // resolved at build time (per-chain via the `probeChainId`). The
69
- // caller uses this to override the per-event `startBlock` — a
70
- // `where`-derived startBlock always wins over contract-level
71
- // config, so users can widen or narrow individual event ranges
72
- // without touching `config.yaml`.
73
- startBlock: option<int>,
74
28
  }
75
29
 
76
30
  // Inner schema for the event `block` filter chunk: `{_gte?}`.
@@ -123,24 +77,12 @@ let extractStartBlock = (
123
77
  }
124
78
  }
125
79
 
126
- // Build the runtime `chain` argument passed into a `where` callback.
127
- // Exposes `chain.id` and `chain.<ContractName>.addresses` as plain values
128
- // on a normal (Object.prototype) JS object. `Dict` is used so the
129
- // contract name can be a dynamic property key without defineProperty
130
- // ceremony.
131
- let makeChainArg = (~contractName: string, ~chainId: int, ~addresses: array<Address.t>) => {
132
- let chainObj = Dict.make()
133
- chainObj->Dict.set("id", chainId->Obj.magic)
134
- chainObj->Dict.set(contractName, {"addresses": addresses}->Obj.magic)
135
- chainObj
136
- }
137
-
138
- // Build the detection-time `chain` argument. `chain.<ContractName>.addresses`
139
- // is a getter so the runtime can tell whether the callback actually reads
140
- // it; the contract sub-object itself is built via `defineProperty` only
141
- // because its `addresses` field needs the getter — the enclosing chainObj
142
- // is a plain JS object.
143
- let makeDetectionChainArg = (
80
+ // Build the `chain` argument passed into a `where` callback.
81
+ // `chain.<ContractName>.addresses` is a getter so the runtime can tell
82
+ // whether the callback actually reads it; the contract sub-object itself is
83
+ // built via `defineProperty` only because its `addresses` field needs the
84
+ // getter — the enclosing chainObj is a plain JS object.
85
+ let makeChainArg = (
144
86
  ~contractName: string,
145
87
  ~chainId: int,
146
88
  ~getAddresses: unit => array<Address.t>,
@@ -155,7 +97,7 @@ let makeDetectionChainArg = (
155
97
  chainObj
156
98
  }
157
99
 
158
- // Sentinel returned by `chain.<Contract>.addresses` during detection. A Proxy
100
+ // Sentinel returned by `chain.<Contract>.addresses`. A Proxy
159
101
  // whose traps throw on any access, so the only non-throwing use is passing it
160
102
  // straight through as a param filter value — which `extractAddressFilterGroups`
161
103
  // then finds by identity. Misuse (spread/map/index/...) fails loud at the site.
@@ -173,78 +115,46 @@ let makeAddressesProbe: (
173
115
  return new Proxy([], {get: trap});
174
116
  }`)
175
117
 
176
- // Find which indexed params the probed `where` result assigned the Proxy to
177
- // (DNF: object => one AND-group, array => OR of groups), neutralizing each match
178
- // to `[]` so later passes can't touch the throwing Proxy. Throws when the
179
- // callback read the addresses but didn't use them as a param filter value, since
180
- // the caller only invokes this once it knows the addresses were read.
181
- let extractAddressFilterGroupsOrThrow = (
182
- result: JSON.t,
183
- ~probe: array<Address.t>,
184
- ~contractName: string,
185
- ): array<array<string>> => {
186
- let groups = []
187
- let scanGroup = (paramsObj: dict<JSON.t>) => {
188
- let names = []
189
- paramsObj->Utils.Dict.forEachWithKey((value, key) => {
190
- if value === probe->(Utils.magic: array<Address.t> => JSON.t) {
191
- names->Array.push(key)->ignore
192
- paramsObj->Dict.set(key, []->(Utils.magic: array<unknown> => JSON.t))
193
- }
194
- })
195
- if names->Utils.Array.isEmpty->not {
196
- groups->Array.push(names)->ignore
197
- }
198
- }
199
- switch result {
200
- | Object(obj) =>
201
- switch obj->Dict.get("params") {
202
- | Some(Object(p)) => scanGroup(p)
203
- | Some(Array(arr)) =>
204
- arr->Array.forEach(item =>
205
- switch item {
206
- | Object(p) => scanGroup(p)
207
- | _ => ()
208
- }
209
- )
210
- | _ => ()
211
- }
212
- | _ => ()
213
- }
214
- if groups->Utils.Array.isEmpty {
215
- JsError.throwWithMessage(
216
- `Invalid where configuration for ${contractName}. The callback reads \`chain.${contractName}.addresses\` but doesn't use it as an indexed-param filter value. Use it directly, e.g. { params: { to: chain.${contractName}.addresses } }.`,
217
- )
218
- }
219
- groups
220
- }
221
-
222
- let parseEventFiltersOrThrow = {
118
+ let parseWhereOrThrow = {
223
119
  let emptyTopics = []
224
120
  let noopGetter = _ => emptyTopics
225
121
 
226
122
  (
227
- ~eventFilters: option<JSON.t>,
123
+ ~where: option<JSON.t>,
228
124
  ~sighash,
229
- ~params,
125
+ ~params: array<string>,
230
126
  ~contractName: string,
231
- ~probeChainId: int,
127
+ ~chainId: int,
232
128
  ~onEventBlockFilterSchema: S.t<option<unknown>>,
233
129
  ~topic1=noopGetter,
234
130
  ~topic2=noopGetter,
235
131
  ~topic3=noopGetter,
236
- ): parsedEventFilters => {
237
- let filterByAddresses = ref(false)
238
- let addressFilterParamGroups = ref([])
239
- let startBlock = ref(None)
132
+ ): parsedWhere => {
133
+ let addressFilterParamGroups = []
134
+ let readAddresses = ref(false)
135
+ let addressesSentinel = makeAddressesProbe(~contractName)
136
+ let sentinelJson = addressesSentinel->(Utils.magic: array<Address.t> => JSON.t)
240
137
  let topic0 = [sighash->EvmTypes.Hex.fromStringUnsafe]
241
138
  let default = {
242
139
  Internal.topic0,
243
- topic1: emptyTopics,
244
- topic2: emptyTopics,
245
- topic3: emptyTopics,
140
+ topic1: Values(emptyTopics),
141
+ topic2: Values(emptyTopics),
142
+ topic3: Values(emptyTopics),
246
143
  }
247
144
 
145
+ // Topic positions map 1:1 onto the event's indexed params in declared
146
+ // order, so the sentinel check keys off `params[index]`. The sentinel must
147
+ // be detected before the topic getter runs — the getter would otherwise
148
+ // touch the throwing Proxy while encoding.
149
+ let topicFilterAt = (paramsFilter: dict<JSON.t>, ~index, ~getter) =>
150
+ switch params
151
+ ->Array.get(index)
152
+ ->Option.flatMap(name => paramsFilter->Utils.Dict.dangerouslyGetNonOption(name)) {
153
+ | Some(value) if value === sentinelJson =>
154
+ Internal.ContractAddresses({contractName: contractName})
155
+ | _ => Values(getter(paramsFilter))
156
+ }
157
+
248
158
  // Build a single topic selection from one indexed-param record (the
249
159
  // inside of `params`). Validates that the keys are actual indexed
250
160
  // parameters of the event — TS type checking doesn't catch this when
@@ -253,18 +163,25 @@ let parseEventFiltersOrThrow = {
253
163
  if paramsFilter->Utils.Dict.isEmpty {
254
164
  default
255
165
  } else {
256
- paramsFilter->Utils.Dict.forEachWithKey((_, key) => {
166
+ let sentinelParamNames = []
167
+ paramsFilter->Utils.Dict.forEachWithKey((value, key) => {
257
168
  if params->Array.includes(key)->not {
258
169
  JsError.throwWithMessage(
259
170
  `Invalid where configuration. The event doesn't have an indexed parameter "${key}" and can't use it for filtering`,
260
171
  )
261
172
  }
173
+ if value === sentinelJson {
174
+ sentinelParamNames->Array.push(key)->ignore
175
+ }
262
176
  })
177
+ if sentinelParamNames->Utils.Array.isEmpty->not {
178
+ addressFilterParamGroups->Array.push(sentinelParamNames)->ignore
179
+ }
263
180
  {
264
181
  Internal.topic0,
265
- topic1: topic1(paramsFilter),
266
- topic2: topic2(paramsFilter),
267
- topic3: topic3(paramsFilter),
182
+ topic1: paramsFilter->topicFilterAt(~index=0, ~getter=topic1),
183
+ topic2: paramsFilter->topicFilterAt(~index=1, ~getter=topic2),
184
+ topic3: paramsFilter->topicFilterAt(~index=2, ~getter=topic3),
268
185
  }
269
186
  }
270
187
  }
@@ -289,7 +206,7 @@ let parseEventFiltersOrThrow = {
289
206
  //
290
207
  // The runtime accepts both the function form (the only form ReScript
291
208
  // exposes) and a top-level static object form (TypeScript convenience).
292
- let parse = (where: JSON.t): array<Internal.topicSelection> => {
209
+ let parse = (where: JSON.t): array<Internal.resolvedTopicSelection> => {
293
210
  if where === Obj.magic(true) {
294
211
  [default]
295
212
  } else if where === Obj.magic(false) {
@@ -335,84 +252,42 @@ let parseEventFiltersOrThrow = {
335
252
  }
336
253
  }
337
254
 
338
- let getEventFiltersOrThrow = switch eventFilters {
339
- | None => {
340
- let static: Internal.eventFilters = Static([default])
341
- _ => static
255
+ // The callback is invoked exactly once per chain, at registration time,
256
+ // with the chain's real configured id. `chain.<Contract>.addresses` yields
257
+ // the throwing-Proxy sentinel, which the param parser turns into a
258
+ // `ContractAddresses` marker; addresses are expanded from the marker only
259
+ // when a source query is built.
260
+ // A misused Proxy (or any throw from the callback) propagates as-is —
261
+ // the Proxy's guidance message surfaces without wrapping.
262
+ let (topicSelections, startBlock) = switch where {
263
+ | None => ([default], None)
264
+ | Some(where) =>
265
+ let whereValue = if typeof(where) === #function {
266
+ let fn = where->(Utils.magic: JSON.t => Internal.onEventWhereArgs<_> => JSON.t)
267
+ let chain = makeChainArg(~contractName, ~chainId, ~getAddresses=() => {
268
+ readAddresses := true
269
+ addressesSentinel
270
+ })
271
+ fn({chain: chain->Obj.magic})
272
+ } else {
273
+ where
342
274
  }
343
- | Some(eventFilters) =>
344
- if typeof(eventFilters) === #function {
345
- let fn = eventFilters->(Utils.magic: JSON.t => Internal.onEventWhereArgs<_> => JSON.t)
346
- // Determine whether the callback uses addresses by probing it with
347
- // a detection chain arg whose `chain.<ContractName>.addresses` getter
348
- // flips a flag. The probe uses this chain's real configured id, so
349
- // handlers that branch on `chain.id` are exercised along the path
350
- // they take for this chain. Event configs are built per-chain, so
351
- // each chain gets a `filterByAddresses` verdict that matches its
352
- // own callback behaviour.
353
- //
354
- // The probe result is also reused to extract the per-event
355
- // `startBlock` (from `where.block`) for this chain — a second
356
- // invocation would risk observing different state for callbacks
357
- // that close over mutable references.
358
- // A misused Proxy (or any throw from the callback) propagates as-is —
359
- // the Proxy's guidance message surfaces without wrapping.
360
- let addressesProbe = makeAddressesProbe(~contractName)
361
- let chain = makeDetectionChainArg(
362
- ~contractName,
363
- ~chainId=probeChainId,
364
- ~getAddresses=() => {
365
- filterByAddresses := true
366
- addressesProbe
367
- },
275
+ let topicSelections = whereValue->parse
276
+ if readAddresses.contents && addressFilterParamGroups->Utils.Array.isEmpty {
277
+ JsError.throwWithMessage(
278
+ `Invalid where configuration for ${contractName}. The callback reads \`chain.${contractName}.addresses\` but doesn't use it as an indexed-param filter value. Use it directly, e.g. { params: { to: chain.${contractName}.addresses } }.`,
368
279
  )
369
- let probedResult = fn({chain: chain->Obj.magic})
370
- if filterByAddresses.contents {
371
- addressFilterParamGroups :=
372
- extractAddressFilterGroupsOrThrow(probedResult, ~probe=addressesProbe, ~contractName)
373
- }
374
- startBlock := extractStartBlock(~onEventBlockFilterSchema, ~contractName, probedResult)
375
- if filterByAddresses.contents {
376
- chain => Internal.Dynamic(
377
- addresses => {
378
- let chainArg = makeChainArg(
379
- ~contractName,
380
- ~chainId=chain->ChainMap.Chain.toChainId,
381
- ~addresses,
382
- )
383
- fn({chain: chainArg->Obj.magic})->parse
384
- },
385
- )
386
- } else {
387
- // No probed chain referenced the contract — cache as Static
388
- // per chain to avoid recomputing topic selections each batch.
389
- // The addresses getter throws: if a code path the probe didn't
390
- // exercise reads `chain.<Contract>.addresses` at runtime, silent
391
- // [] would produce wrong topics — throw a user-friendly error
392
- // instead so the user rewrites the callback to surface the
393
- // dependency up-front.
394
- chain => {
395
- let chainId = chain->ChainMap.Chain.toChainId
396
- let chainArg = makeDetectionChainArg(~contractName, ~chainId, ~getAddresses=() =>
397
- JsError.throwWithMessage(
398
- `Invalid where configuration. Event callback for contract "${contractName}" read \`chain.${contractName}.addresses\` at runtime but the probe didn't detect the access on chainId ${chainId->Int.toString}. Move the \`chain.${contractName}.addresses\` read above any \`chain.id\` branching so the probe picks up the dependency and switches to the dynamic fetch path.`,
399
- )
400
- )
401
- Internal.Static(fn({chain: chainArg->Obj.magic})->parse)
402
- }
403
- }
404
- } else {
405
- startBlock := extractStartBlock(~onEventBlockFilterSchema, ~contractName, eventFilters)
406
- let static: Internal.eventFilters = Static(eventFilters->parse)
407
- _ => static
408
280
  }
281
+ (topicSelections, extractStartBlock(~onEventBlockFilterSchema, ~contractName, whereValue))
409
282
  }
410
283
 
411
284
  {
412
- getEventFiltersOrThrow,
413
- filterByAddresses: filterByAddresses.contents,
414
- addressFilterParamGroups: addressFilterParamGroups.contents,
415
- startBlock: startBlock.contents,
285
+ resolvedWhere: {
286
+ topicSelections,
287
+ startBlock,
288
+ },
289
+ filterByAddresses: addressFilterParamGroups->Utils.Array.isEmpty->not,
290
+ addressFilterParamGroups,
416
291
  }
417
292
  }
418
293
  }