envio 3.0.0-alpha.21 → 3.0.0-alpha.22

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 (219) hide show
  1. package/bin.mjs +2 -48
  2. package/evm.schema.json +67 -0
  3. package/fuel.schema.json +67 -0
  4. package/index.d.ts +822 -38
  5. package/index.js +5 -3
  6. package/package.json +10 -8
  7. package/rescript.json +5 -9
  8. package/src/Address.res +4 -5
  9. package/src/Address.res.mjs +9 -12
  10. package/src/Api.res +15 -0
  11. package/src/Api.res.mjs +20 -0
  12. package/src/Batch.res +32 -34
  13. package/src/Batch.res.mjs +172 -187
  14. package/src/Bin.res +89 -0
  15. package/src/Bin.res.mjs +97 -0
  16. package/src/ChainFetcher.res +33 -57
  17. package/src/ChainFetcher.res.mjs +197 -227
  18. package/src/ChainManager.res +6 -14
  19. package/src/ChainManager.res.mjs +74 -85
  20. package/src/ChainMap.res +14 -16
  21. package/src/ChainMap.res.mjs +38 -38
  22. package/src/Config.res +193 -135
  23. package/src/Config.res.mjs +566 -592
  24. package/src/Core.res +182 -0
  25. package/src/Core.res.mjs +207 -0
  26. package/src/Ecosystem.res +25 -4
  27. package/src/Ecosystem.res.mjs +12 -13
  28. package/src/Env.res +20 -13
  29. package/src/Env.res.mjs +124 -113
  30. package/src/EnvSafe.res +269 -0
  31. package/src/EnvSafe.res.mjs +296 -0
  32. package/src/EnvSafe.resi +18 -0
  33. package/src/Envio.res +37 -26
  34. package/src/Envio.res.mjs +59 -60
  35. package/src/ErrorHandling.res +2 -2
  36. package/src/ErrorHandling.res.mjs +15 -15
  37. package/src/EventConfigBuilder.res +219 -81
  38. package/src/EventConfigBuilder.res.mjs +259 -202
  39. package/src/EventProcessing.res +27 -38
  40. package/src/EventProcessing.res.mjs +165 -183
  41. package/src/EventUtils.res +11 -11
  42. package/src/EventUtils.res.mjs +21 -22
  43. package/src/EvmTypes.res +0 -1
  44. package/src/EvmTypes.res.mjs +5 -5
  45. package/src/FetchState.res +360 -256
  46. package/src/FetchState.res.mjs +958 -914
  47. package/src/GlobalState.res +365 -351
  48. package/src/GlobalState.res.mjs +958 -992
  49. package/src/GlobalStateManager.res +1 -2
  50. package/src/GlobalStateManager.res.mjs +36 -44
  51. package/src/HandlerLoader.res +107 -23
  52. package/src/HandlerLoader.res.mjs +128 -38
  53. package/src/HandlerRegister.res +127 -103
  54. package/src/HandlerRegister.res.mjs +164 -164
  55. package/src/HandlerRegister.resi +12 -4
  56. package/src/Hasura.res +35 -22
  57. package/src/Hasura.res.mjs +158 -167
  58. package/src/InMemoryStore.res +20 -27
  59. package/src/InMemoryStore.res.mjs +64 -80
  60. package/src/InMemoryTable.res +34 -39
  61. package/src/InMemoryTable.res.mjs +165 -170
  62. package/src/Internal.res +52 -33
  63. package/src/Internal.res.mjs +84 -81
  64. package/src/LazyLoader.res.mjs +55 -61
  65. package/src/LoadLayer.res +77 -78
  66. package/src/LoadLayer.res.mjs +160 -189
  67. package/src/LoadManager.res +16 -21
  68. package/src/LoadManager.res.mjs +79 -84
  69. package/src/LogSelection.res +236 -68
  70. package/src/LogSelection.res.mjs +211 -141
  71. package/src/Logging.res +13 -9
  72. package/src/Logging.res.mjs +130 -143
  73. package/src/Main.res +428 -51
  74. package/src/Main.res.mjs +528 -271
  75. package/src/Persistence.res +77 -84
  76. package/src/Persistence.res.mjs +131 -132
  77. package/src/PgStorage.res +291 -167
  78. package/src/PgStorage.res.mjs +797 -817
  79. package/src/Prometheus.res +50 -58
  80. package/src/Prometheus.res.mjs +345 -373
  81. package/src/ReorgDetection.res +22 -24
  82. package/src/ReorgDetection.res.mjs +100 -106
  83. package/src/SafeCheckpointTracking.res +7 -7
  84. package/src/SafeCheckpointTracking.res.mjs +40 -43
  85. package/src/SimulateItems.res +41 -49
  86. package/src/SimulateItems.res.mjs +257 -272
  87. package/src/Sink.res +2 -2
  88. package/src/Sink.res.mjs +22 -26
  89. package/src/TableIndices.res +1 -2
  90. package/src/TableIndices.res.mjs +42 -48
  91. package/src/TestIndexer.res +196 -189
  92. package/src/TestIndexer.res.mjs +536 -536
  93. package/src/TestIndexerProxyStorage.res +15 -16
  94. package/src/TestIndexerProxyStorage.res.mjs +98 -122
  95. package/src/TestIndexerWorker.res +4 -0
  96. package/src/TestIndexerWorker.res.mjs +7 -0
  97. package/src/Throttler.res +3 -3
  98. package/src/Throttler.res.mjs +23 -24
  99. package/src/Time.res +1 -1
  100. package/src/Time.res.mjs +18 -21
  101. package/src/TopicFilter.res +3 -3
  102. package/src/TopicFilter.res.mjs +29 -30
  103. package/src/UserContext.res +93 -54
  104. package/src/UserContext.res.mjs +197 -182
  105. package/src/Utils.res +141 -86
  106. package/src/Utils.res.mjs +334 -295
  107. package/src/bindings/BigDecimal.res +0 -2
  108. package/src/bindings/BigDecimal.res.mjs +19 -23
  109. package/src/bindings/ClickHouse.res +28 -27
  110. package/src/bindings/ClickHouse.res.mjs +243 -240
  111. package/src/bindings/DateFns.res +11 -11
  112. package/src/bindings/DateFns.res.mjs +7 -7
  113. package/src/bindings/EventSource.res.mjs +2 -2
  114. package/src/bindings/Express.res +2 -5
  115. package/src/bindings/Hrtime.res +2 -2
  116. package/src/bindings/Hrtime.res.mjs +30 -32
  117. package/src/bindings/Lodash.res.mjs +1 -1
  118. package/src/bindings/NodeJs.res +14 -9
  119. package/src/bindings/NodeJs.res.mjs +20 -20
  120. package/src/bindings/Pino.res +8 -10
  121. package/src/bindings/Pino.res.mjs +40 -43
  122. package/src/bindings/Postgres.res +2 -5
  123. package/src/bindings/Postgres.res.mjs +9 -9
  124. package/src/bindings/PromClient.res +17 -2
  125. package/src/bindings/PromClient.res.mjs +30 -7
  126. package/src/bindings/SDSL.res.mjs +2 -2
  127. package/src/bindings/Viem.res +4 -4
  128. package/src/bindings/Viem.res.mjs +20 -22
  129. package/src/bindings/Vitest.res +1 -1
  130. package/src/bindings/Vitest.res.mjs +2 -2
  131. package/src/bindings/WebSocket.res +1 -1
  132. package/src/db/EntityHistory.res +9 -3
  133. package/src/db/EntityHistory.res.mjs +84 -59
  134. package/src/db/InternalTable.res +62 -60
  135. package/src/db/InternalTable.res.mjs +271 -203
  136. package/src/db/Schema.res +1 -2
  137. package/src/db/Schema.res.mjs +28 -32
  138. package/src/db/Table.res +28 -27
  139. package/src/db/Table.res.mjs +276 -292
  140. package/src/sources/EventRouter.res +21 -16
  141. package/src/sources/EventRouter.res.mjs +55 -57
  142. package/src/sources/Evm.res +17 -1
  143. package/src/sources/Evm.res.mjs +16 -8
  144. package/src/sources/EvmChain.res +15 -17
  145. package/src/sources/EvmChain.res.mjs +40 -42
  146. package/src/sources/Fuel.res +14 -1
  147. package/src/sources/Fuel.res.mjs +16 -8
  148. package/src/sources/FuelSDK.res +1 -1
  149. package/src/sources/FuelSDK.res.mjs +6 -8
  150. package/src/sources/HyperFuel.res +8 -10
  151. package/src/sources/HyperFuel.res.mjs +113 -123
  152. package/src/sources/HyperFuelClient.res.mjs +6 -7
  153. package/src/sources/HyperFuelSource.res +19 -20
  154. package/src/sources/HyperFuelSource.res.mjs +339 -356
  155. package/src/sources/HyperSync.res +11 -13
  156. package/src/sources/HyperSync.res.mjs +206 -220
  157. package/src/sources/HyperSyncClient.res +5 -7
  158. package/src/sources/HyperSyncClient.res.mjs +70 -75
  159. package/src/sources/HyperSyncHeightStream.res +8 -9
  160. package/src/sources/HyperSyncHeightStream.res.mjs +78 -86
  161. package/src/sources/HyperSyncJsonApi.res +18 -15
  162. package/src/sources/HyperSyncJsonApi.res.mjs +201 -231
  163. package/src/sources/HyperSyncSource.res +17 -21
  164. package/src/sources/HyperSyncSource.res.mjs +268 -290
  165. package/src/sources/Rpc.res +5 -5
  166. package/src/sources/Rpc.res.mjs +168 -192
  167. package/src/sources/RpcSource.res +166 -167
  168. package/src/sources/RpcSource.res.mjs +972 -1046
  169. package/src/sources/RpcWebSocketHeightStream.res +10 -11
  170. package/src/sources/RpcWebSocketHeightStream.res.mjs +131 -145
  171. package/src/sources/SimulateSource.res +1 -1
  172. package/src/sources/SimulateSource.res.mjs +35 -38
  173. package/src/sources/Source.res +1 -1
  174. package/src/sources/Source.res.mjs +3 -3
  175. package/src/sources/SourceManager.res +39 -20
  176. package/src/sources/SourceManager.res.mjs +340 -371
  177. package/src/sources/SourceManager.resi +2 -1
  178. package/src/sources/Svm.res +12 -5
  179. package/src/sources/Svm.res.mjs +44 -41
  180. package/src/tui/Tui.res +23 -12
  181. package/src/tui/Tui.res.mjs +292 -290
  182. package/src/tui/bindings/Ink.res +2 -4
  183. package/src/tui/bindings/Ink.res.mjs +35 -41
  184. package/src/tui/components/BufferedProgressBar.res +7 -7
  185. package/src/tui/components/BufferedProgressBar.res.mjs +46 -46
  186. package/src/tui/components/CustomHooks.res +1 -2
  187. package/src/tui/components/CustomHooks.res.mjs +102 -122
  188. package/src/tui/components/Messages.res +1 -2
  189. package/src/tui/components/Messages.res.mjs +38 -42
  190. package/src/tui/components/SyncETA.res +10 -11
  191. package/src/tui/components/SyncETA.res.mjs +178 -196
  192. package/src/tui/components/TuiData.res +1 -1
  193. package/src/tui/components/TuiData.res.mjs +7 -6
  194. package/src/vendored/Rest.res +52 -66
  195. package/src/vendored/Rest.res.mjs +324 -364
  196. package/svm.schema.json +67 -0
  197. package/src/Address.gen.ts +0 -8
  198. package/src/Config.gen.ts +0 -19
  199. package/src/Envio.gen.ts +0 -55
  200. package/src/EvmTypes.gen.ts +0 -6
  201. package/src/InMemoryStore.gen.ts +0 -6
  202. package/src/Internal.gen.ts +0 -64
  203. package/src/PgStorage.gen.ts +0 -10
  204. package/src/PgStorage.res.d.mts +0 -5
  205. package/src/Types.ts +0 -56
  206. package/src/bindings/BigDecimal.gen.ts +0 -14
  207. package/src/bindings/BigDecimal.res.d.mts +0 -5
  208. package/src/bindings/BigInt.gen.ts +0 -10
  209. package/src/bindings/BigInt.res +0 -70
  210. package/src/bindings/BigInt.res.d.mts +0 -5
  211. package/src/bindings/BigInt.res.mjs +0 -154
  212. package/src/bindings/Ethers.res.d.mts +0 -5
  213. package/src/bindings/Pino.gen.ts +0 -17
  214. package/src/bindings/Postgres.gen.ts +0 -8
  215. package/src/bindings/Postgres.res.d.mts +0 -5
  216. package/src/bindings/Promise.res +0 -67
  217. package/src/bindings/Promise.res.mjs +0 -26
  218. package/src/db/InternalTable.gen.ts +0 -36
  219. package/src/sources/HyperSyncClient.gen.ts +0 -19
@@ -1,79 +1,74 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Utils from "./Utils.res.mjs";
4
- import * as Belt_Array from "rescript/lib/es6/belt_Array.js";
5
- import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js";
4
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
5
 
7
- var Call = {};
6
+ let Call = {};
8
7
 
9
- var Group = {};
8
+ let Group = {};
10
9
 
11
10
  function make() {
12
11
  return {
13
- groups: {},
14
- isCollecting: false
15
- };
12
+ groups: {},
13
+ isCollecting: false
14
+ };
16
15
  }
17
16
 
18
17
  async function schedule(loadManager) {
19
18
  loadManager.isCollecting = true;
20
19
  await Promise.resolve();
21
20
  loadManager.isCollecting = false;
22
- var groups = loadManager.groups;
23
- Object.keys(groups).forEach(async function (key) {
24
- var group = groups[key];
25
- var calls = group.calls;
26
- var inputsToLoad = [];
27
- var currentInputKeys = [];
28
- Object.keys(calls).forEach(function (inputKey) {
29
- var call = calls[inputKey];
30
- if (!call.isLoading) {
31
- call.isLoading = true;
32
- currentInputKeys.push(inputKey);
33
- if (!group.hasInMemory(inputKey)) {
34
- inputsToLoad.push(call.input);
35
- return ;
36
- } else {
37
- return ;
38
- }
39
- }
40
-
41
- });
42
- if (!Utils.$$Array.isEmpty(inputsToLoad)) {
43
- try {
44
- await group.load(inputsToLoad, (function (inputKey, exn) {
45
- var call = calls[inputKey];
46
- call.exn = exn;
47
- }));
48
- }
49
- catch (raw_exn){
50
- var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
51
- var exn$1 = Utils.prettifyExn(exn);
52
- Belt_Array.forEach(currentInputKeys, (function (inputKey) {
53
- var call = calls[inputKey];
54
- call.exn = exn$1;
55
- }));
56
- }
21
+ let groups = loadManager.groups;
22
+ Object.keys(groups).forEach(async key => {
23
+ let group = groups[key];
24
+ let calls = group.calls;
25
+ let inputsToLoad = [];
26
+ let currentInputKeys = [];
27
+ Utils.Dict.forEachWithKey(calls, (call, inputKey) => {
28
+ if (!call.isLoading) {
29
+ call.isLoading = true;
30
+ currentInputKeys.push(inputKey);
31
+ if (!group.hasInMemory(inputKey)) {
32
+ inputsToLoad.push(call.input);
33
+ return;
34
+ } else {
35
+ return;
57
36
  }
58
- if (Utils.$$Array.isEmpty(currentInputKeys)) {
59
- return ;
60
- }
61
- currentInputKeys.forEach(function (inputKey) {
62
- var call = calls[inputKey];
63
- Utils.Dict.deleteInPlace(calls, inputKey);
64
- var exn = call.exn;
65
- if (exn !== undefined) {
66
- return call.reject(Utils.prettifyExn(exn));
67
- } else {
68
- return call.resolve(group.getUnsafeInMemory(inputKey));
69
- }
70
- });
71
- var latestGroup = groups[key];
72
- if (Utils.$$Array.isEmpty(Object.keys(latestGroup.calls))) {
73
- return Utils.Dict.deleteInPlace(groups, key);
74
- }
75
-
76
- });
37
+ }
38
+ });
39
+ if (!Utils.$$Array.isEmpty(inputsToLoad)) {
40
+ try {
41
+ await group.load(inputsToLoad, (inputKey, exn) => {
42
+ let call = calls[inputKey];
43
+ call.exn = exn;
44
+ });
45
+ } catch (raw_exn) {
46
+ let exn = Primitive_exceptions.internalToException(raw_exn);
47
+ let exn$1 = Utils.prettifyExn(exn);
48
+ currentInputKeys.forEach(inputKey => {
49
+ let call = calls[inputKey];
50
+ call.exn = exn$1;
51
+ });
52
+ }
53
+ }
54
+ if (Utils.$$Array.isEmpty(currentInputKeys)) {
55
+ return;
56
+ }
57
+ currentInputKeys.forEach(inputKey => {
58
+ let call = calls[inputKey];
59
+ Utils.Dict.deleteInPlace(calls, inputKey);
60
+ let exn = call.exn;
61
+ if (exn !== undefined) {
62
+ return call.reject(Utils.prettifyExn(exn));
63
+ } else {
64
+ return call.resolve(group.getUnsafeInMemory(inputKey));
65
+ }
66
+ });
67
+ let latestGroup = groups[key];
68
+ if (Utils.Dict.isEmpty(latestGroup.calls)) {
69
+ return Utils.Dict.deleteInPlace(groups, key);
70
+ }
71
+ });
77
72
  }
78
73
 
79
74
  function noopHasher(input) {
@@ -81,17 +76,17 @@ function noopHasher(input) {
81
76
  }
82
77
 
83
78
  function call(loadManager, input, key, load, hasher, shouldGroup, hasInMemory, getUnsafeInMemory) {
84
- var inputKey = hasher === noopHasher ? input : hasher(input);
79
+ let inputKey = hasher === noopHasher ? input : hasher(input);
85
80
  if (!shouldGroup && hasInMemory(inputKey)) {
86
81
  return Promise.resolve(getUnsafeInMemory(inputKey));
87
82
  }
88
- var group = loadManager.groups[key];
89
- var group$1;
83
+ let group = loadManager.groups[key];
84
+ let group$1;
90
85
  if (group !== undefined) {
91
86
  group$1 = group;
92
87
  } else {
93
- var g_calls = {};
94
- var g = {
88
+ let g_calls = {};
89
+ let g = {
95
90
  calls: g_calls,
96
91
  load: load,
97
92
  getUnsafeInMemory: getUnsafeInMemory,
@@ -100,21 +95,21 @@ function call(loadManager, input, key, load, hasher, shouldGroup, hasInMemory, g
100
95
  loadManager.groups[key] = g;
101
96
  group$1 = g;
102
97
  }
103
- var c = group$1.calls[inputKey];
98
+ let c = group$1.calls[inputKey];
104
99
  if (c !== undefined) {
105
100
  return c.promise;
106
101
  }
107
- var promise = new Promise((function (resolve, reject) {
108
- var call = {
109
- input: input,
110
- resolve: resolve,
111
- reject: reject,
112
- exn: undefined,
113
- promise: null,
114
- isLoading: false
115
- };
116
- group$1.calls[inputKey] = call;
117
- }));
102
+ let promise = new Promise((resolve, reject) => {
103
+ let call = {
104
+ input: input,
105
+ resolve: resolve,
106
+ reject: reject,
107
+ exn: undefined,
108
+ promise: null,
109
+ isLoading: false
110
+ };
111
+ group$1.calls[inputKey] = call;
112
+ });
118
113
  group$1.calls[inputKey].promise = promise;
119
114
  if (!loadManager.isCollecting) {
120
115
  schedule(loadManager);
@@ -123,11 +118,11 @@ function call(loadManager, input, key, load, hasher, shouldGroup, hasInMemory, g
123
118
  }
124
119
 
125
120
  export {
126
- Call ,
127
- Group ,
128
- make ,
129
- schedule ,
130
- noopHasher ,
131
- call ,
121
+ Call,
122
+ Group,
123
+ make,
124
+ schedule,
125
+ noopHasher,
126
+ call,
132
127
  }
133
128
  /* Utils Not a pure module */
@@ -12,7 +12,7 @@ let makeTopicSelection = (~topic0, ~topic1=[], ~topic2=[], ~topic3=[]) =>
12
12
  }
13
13
 
14
14
  let hasFilters = ({topic1, topic2, topic3}: Internal.topicSelection) => {
15
- [topic1, topic2, topic3]->Js.Array2.find(topic => !Utils.Array.isEmpty(topic))->Belt.Option.isSome
15
+ [topic1, topic2, topic3]->Array.find(topic => !Utils.Array.isEmpty(topic))->Belt.Option.isSome
16
16
  }
17
17
 
18
18
  /**
@@ -26,10 +26,10 @@ let compressTopicSelections = (topicSelections: array<Internal.topicSelection>)
26
26
 
27
27
  topicSelections->Belt.Array.forEach(selection => {
28
28
  if selection->hasFilters {
29
- selectionsWithFilters->Js.Array2.push(selection)->ignore
29
+ selectionsWithFilters->Array.push(selection)->ignore
30
30
  } else {
31
31
  selection.topic0->Belt.Array.forEach(topic0 => {
32
- topic0sOfSelectionsWithoutFilters->Js.Array2.push(topic0)->ignore
32
+ topic0sOfSelectionsWithoutFilters->Array.push(topic0)->ignore
33
33
  })
34
34
  }
35
35
  })
@@ -60,6 +60,95 @@ let make = (~addresses, ~topicSelections) => {
60
60
  type parsedEventFilters = {
61
61
  getEventFiltersOrThrow: ChainMap.Chain.t => Internal.eventFilters,
62
62
  filterByAddresses: bool,
63
+ // `_gte` from the top-level `block` filter of the user's `where`,
64
+ // resolved at build time (per-chain via the `probeChainId`). The
65
+ // caller uses this to override the per-event `startBlock` — a
66
+ // `where`-derived startBlock always wins over contract-level
67
+ // config, so users can widen or narrow individual event ranges
68
+ // without touching `config.yaml`.
69
+ startBlock: option<int>,
70
+ }
71
+
72
+ // Inner schema for the event `block` filter chunk: `{_gte?}`.
73
+ // `S.strict` rejects `_lte` / `_every` — those are stride/range concepts
74
+ // that only make sense for `onBlock` handlers, not event filters. Typos
75
+ // like `_gt` surface through the same strict-schema error path.
76
+ type eventBlockRange = {_gte: option<int>}
77
+ let eventBlockRangeSchema: S.t<eventBlockRange> = S.object(s => {
78
+ _gte: s.field("_gte", S.option(S.int)),
79
+ })->S.strict
80
+
81
+ // Extract the per-event `startBlock` from a `where` result (or static
82
+ // value). Two-stage parse mirroring `onBlock`: the ecosystem schema
83
+ // strips the outer `block.number` / `block.height` wrapper, then
84
+ // `eventBlockRangeSchema` validates `{_gte?}` strictly — `_lte` and
85
+ // `_every` are rejected with a user-friendly message pointing users
86
+ // at `onBlock` for stride/endBlock semantics.
87
+ //
88
+ // Returns `None` for boolean `where` results, missing `block`, and
89
+ // `block.number: {}` (no `_gte`). Wraps schema errors with the
90
+ // contract/event context so the call-site is obvious in the log.
91
+ let extractStartBlock = (
92
+ where: JSON.t,
93
+ ~onEventBlockFilterSchema: S.t<option<unknown>>,
94
+ ~contractName: string,
95
+ ): option<int> => {
96
+ // `where` may be a bool at runtime even though the static type is
97
+ // `JSON.t` — the user callbacks can return `true`/`false` to keep/skip
98
+ // a chain, and the value reaches here unwrapped. Detect with
99
+ // `typeof` instead of an identity-equal check so ReScript doesn't
100
+ // constant-fold the comparison away for the `JSON.t` nominal type.
101
+ if typeof(where) === #boolean {
102
+ None
103
+ } else {
104
+ try {
105
+ switch where->S.parseOrThrow(onEventBlockFilterSchema) {
106
+ | None => None
107
+ | Some(inner) => (inner->S.parseOrThrow(eventBlockRangeSchema))._gte
108
+ }
109
+ } catch {
110
+ | S.Raised(exn) =>
111
+ JsError.throwWithMessage(
112
+ `Invalid where configuration for ${contractName}. \`block\` filter is invalid: ${exn
113
+ ->Utils.prettifyExn
114
+ ->(
115
+ Utils.magic: exn => string
116
+ )}. Only \`_gte\` is supported on event filters — use \`indexer.onBlock\` for \`_lte\` or \`_every\`.`,
117
+ )
118
+ }
119
+ }
120
+ }
121
+
122
+ // Build the runtime `chain` argument passed into a `where` callback.
123
+ // Exposes `chain.id` and `chain.<ContractName>.addresses` as plain values
124
+ // on a normal (Object.prototype) JS object. `Dict` is used so the
125
+ // contract name can be a dynamic property key without defineProperty
126
+ // ceremony.
127
+ let makeChainArg = (~contractName: string, ~chainId: int, ~addresses: array<Address.t>) => {
128
+ let chainObj = Dict.make()
129
+ chainObj->Dict.set("id", chainId->Obj.magic)
130
+ chainObj->Dict.set(contractName, {"addresses": addresses}->Obj.magic)
131
+ chainObj
132
+ }
133
+
134
+ // Build the detection-time `chain` argument. `chain.<ContractName>.addresses`
135
+ // is a getter so the runtime can tell whether the callback actually reads
136
+ // it; the contract sub-object itself is built via `defineProperty` only
137
+ // because its `addresses` field needs the getter — the enclosing chainObj
138
+ // is a plain JS object.
139
+ let makeDetectionChainArg = (
140
+ ~contractName: string,
141
+ ~chainId: int,
142
+ ~getAddresses: unit => array<Address.t>,
143
+ ) => {
144
+ let contractObj = Utils.Object.createNullObject()
145
+ contractObj
146
+ ->Utils.Object.defineProperty("addresses", {enumerable: true, get: getAddresses})
147
+ ->ignore
148
+ let chainObj = Dict.make()
149
+ chainObj->Dict.set("id", chainId->Obj.magic)
150
+ chainObj->Dict.set(contractName, contractObj->Obj.magic)
151
+ chainObj
63
152
  }
64
153
 
65
154
  let parseEventFiltersOrThrow = {
@@ -67,14 +156,18 @@ let parseEventFiltersOrThrow = {
67
156
  let noopGetter = _ => emptyTopics
68
157
 
69
158
  (
70
- ~eventFilters: option<Js.Json.t>,
159
+ ~eventFilters: option<JSON.t>,
71
160
  ~sighash,
72
161
  ~params,
162
+ ~contractName: string,
163
+ ~probeChainId: int,
164
+ ~onEventBlockFilterSchema: S.t<option<unknown>>,
73
165
  ~topic1=noopGetter,
74
166
  ~topic2=noopGetter,
75
167
  ~topic3=noopGetter,
76
168
  ): parsedEventFilters => {
77
169
  let filterByAddresses = ref(false)
170
+ let startBlock = ref(None)
78
171
  let topic0 = [sighash->EvmTypes.Hex.fromStringUnsafe]
79
172
  let default = {
80
173
  Internal.topic0,
@@ -83,44 +176,97 @@ let parseEventFiltersOrThrow = {
83
176
  topic3: emptyTopics,
84
177
  }
85
178
 
86
- let parse = (eventFilters: Js.Json.t): array<Internal.topicSelection> => {
87
- if eventFilters === Obj.magic(true) {
179
+ // Build a single topic selection from one indexed-param record (the
180
+ // inside of `params`). Validates that the keys are actual indexed
181
+ // parameters of the event — TS type checking doesn't catch this when
182
+ // `where` is a callback.
183
+ let paramsRecordToTopicSelection = (paramsFilter: dict<JSON.t>) => {
184
+ let filterKeys = paramsFilter->Dict.keysToArray
185
+ switch filterKeys {
186
+ | [] => default
187
+ | _ => {
188
+ filterKeys->Array.forEach(key => {
189
+ if params->Array.includes(key)->not {
190
+ JsError.throwWithMessage(
191
+ `Invalid where configuration. The event doesn't have an indexed parameter "${key}" and can't use it for filtering`,
192
+ )
193
+ }
194
+ })
195
+ {
196
+ Internal.topic0,
197
+ topic1: topic1(paramsFilter),
198
+ topic2: topic2(paramsFilter),
199
+ topic3: topic3(paramsFilter),
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // Known top-level `where` keys. `block` is a sibling of `params` — its
206
+ // `_gte` promotes to the event's `startBlock` (extracted separately
207
+ // by `extractStartBlock`). Unknown keys are rejected to catch typos
208
+ // like `parmas` or `blocks` at registration time.
209
+ let acceptedWhereKeys = ["params", "block"]
210
+
211
+ // Parse a `where` value (or the result of calling the dynamic callback)
212
+ // into a list of topic selections.
213
+ //
214
+ // Accepted shapes:
215
+ // - `true` → KeepAll → match the event signature with no narrowing
216
+ // - `false` → SkipAll → no events
217
+ // - `{}` (or `{params: undefined}`) → no narrowing
218
+ // - `{params: {...}}` → single AND-conjunction
219
+ // - `{params: [{...}, {...}]}` → OR of multiple AND-conjunctions
220
+ // - `{block: {number: {_gte: N}}}` → no topic narrowing; startBlock only
221
+ // - `{params: ..., block: ...}` → combined
222
+ //
223
+ // The runtime accepts both the function form (the only form ReScript
224
+ // exposes) and a top-level static object form (TypeScript convenience).
225
+ let parse = (where: JSON.t): array<Internal.topicSelection> => {
226
+ if where === Obj.magic(true) {
88
227
  [default]
89
- } else if eventFilters === Obj.magic(false) {
228
+ } else if where === Obj.magic(false) {
90
229
  []
91
230
  } else {
92
- switch eventFilters {
93
- | Array([]) => [%raw(`{}`)]
94
- | Array(a) => a
95
- | _ => [eventFilters]
96
- }->Js.Array2.map(eventFilter => {
97
- switch eventFilter {
98
- | Object(eventFilter) => {
99
- let filterKeys = eventFilter->Js.Dict.keys
100
- switch filterKeys {
101
- | [] => default
102
- | _ => {
103
- filterKeys->Js.Array2.forEach(key => {
104
- if params->Js.Array2.includes(key)->not {
105
- // In TS type validation doesn't catch this
106
- // when we have eventFilters as a callback
107
- Js.Exn.raiseError(
108
- `Invalid event filters configuration. The event doesn't have an indexed parameter "${key}" and can't use it for filtering`,
109
- )
110
- }
111
- })
112
- {
113
- Internal.topic0,
114
- topic1: topic1(eventFilter),
115
- topic2: topic2(eventFilter),
116
- topic3: topic3(eventFilter),
117
- }
118
- }
231
+ // A `where` condition is shaped as `{params?: ..., block?: ...}`.
232
+ // `params` carries the indexed-parameter filter record; `block`
233
+ // carries the per-event block-range filter handled by
234
+ // `extractStartBlock`.
235
+ switch where {
236
+ | Object(obj) => {
237
+ // Catch typos (e.g. `parmas:`) and the legacy flat-filter
238
+ // shape (`{from: ...}`) by rejecting any unknown sibling.
239
+ obj
240
+ ->Dict.keysToArray
241
+ ->Array.forEach(key => {
242
+ if acceptedWhereKeys->Array.includes(key)->not {
243
+ JsError.throwWithMessage(
244
+ `Invalid where configuration. Unknown field "${key}". Indexed parameter filters must be nested under \`params\` and block-range filters under \`block\``,
245
+ )
119
246
  }
247
+ })
248
+ switch obj->Dict.get("params") {
249
+ | None => [default]
250
+ | Some(Object(p)) => [paramsRecordToTopicSelection(p)]
251
+ | Some(Array([])) => [default]
252
+ | Some(Array(arr)) =>
253
+ arr->Array.map(item =>
254
+ switch item {
255
+ | Object(p) => paramsRecordToTopicSelection(p)
256
+ | _ =>
257
+ JsError.throwWithMessage(
258
+ "Invalid where configuration. Each entry in `params` must be an object",
259
+ )
260
+ }
261
+ )
262
+ | Some(_) =>
263
+ JsError.throwWithMessage(
264
+ "Invalid where configuration. Expected `params` to be an object or an array of objects",
265
+ )
120
266
  }
121
- | _ => Js.Exn.raiseError("Invalid event filters configuration. Expected an object")
122
267
  }
123
- })
268
+ | _ => JsError.throwWithMessage("Invalid where configuration. Expected an object")
269
+ }
124
270
  }
125
271
  }
126
272
 
@@ -130,48 +276,69 @@ let parseEventFiltersOrThrow = {
130
276
  _ => static
131
277
  }
132
278
  | Some(eventFilters) =>
133
- if Js.typeof(eventFilters) === "function" {
134
- let fn = eventFilters->(Utils.magic: Js.Json.t => Internal.eventFiltersArgs => Js.Json.t)
135
- // When user passess a function to event filters we need to
136
- // first determine whether it uses addresses or not
137
- // Because the fetching logic will be different for wildcard events
138
- // 1. If wildcard event doesn't use addresses,
139
- // it should start fetching even without static addresses in the config
140
- // 2. If wildcard event uses addresses in event filters,
141
- // it should first wait for dynamic contract registration
142
- // So to deterimine which case we run the function with dummy args
143
- // and check if it uses addresses by using the getter.
144
- try {
145
- let args = (
146
- {
147
- chainId: 0,
148
- addresses: [],
149
- }: Internal.eventFiltersArgs
150
- )->Utils.Object.defineProperty(
151
- "addresses",
152
- {
153
- get: () => {
154
- filterByAddresses := true
155
- []
156
- },
279
+ if typeof(eventFilters) === #function {
280
+ let fn = eventFilters->(Utils.magic: JSON.t => Internal.onEventWhereArgs<_> => JSON.t)
281
+ // Determine whether the callback uses addresses by probing it with
282
+ // a detection chain arg whose `chain.<ContractName>.addresses` getter
283
+ // flips a flag. The probe uses this chain's real configured id, so
284
+ // handlers that branch on `chain.id` are exercised along the path
285
+ // they take for this chain. Event configs are built per-chain, so
286
+ // each chain gets a `filterByAddresses` verdict that matches its
287
+ // own callback behaviour.
288
+ //
289
+ // The probe result is also reused to extract the per-event
290
+ // `startBlock` (from `where.block`) for this chain — a second
291
+ // invocation would risk observing different state for callbacks
292
+ // that close over mutable references.
293
+ let probedResult = try {
294
+ let chain = makeDetectionChainArg(
295
+ ~contractName,
296
+ ~chainId=probeChainId,
297
+ ~getAddresses=() => {
298
+ filterByAddresses := true
299
+ []
157
300
  },
158
301
  )
159
- let _ = fn(args)
302
+ Some(fn({chain: chain->Obj.magic}))
160
303
  } catch {
161
- | _ => ()
304
+ | _ => None
305
+ }
306
+ switch probedResult {
307
+ | Some(result) =>
308
+ startBlock := extractStartBlock(~onEventBlockFilterSchema, ~contractName, result)
309
+ | None => ()
162
310
  }
163
311
  if filterByAddresses.contents {
164
312
  chain => Internal.Dynamic(
165
- addresses => fn({chainId: chain->ChainMap.Chain.toChainId, addresses})->parse,
313
+ addresses => {
314
+ let chainArg = makeChainArg(
315
+ ~contractName,
316
+ ~chainId=chain->ChainMap.Chain.toChainId,
317
+ ~addresses,
318
+ )
319
+ fn({chain: chainArg->Obj.magic})->parse
320
+ },
166
321
  )
167
322
  } else {
168
- // When we don't depend on addresses, can mark the event filter
169
- // as static and avoid recalculating on every batch
170
- chain => Internal.Static(
171
- fn({chainId: chain->ChainMap.Chain.toChainId, addresses: []})->parse,
172
- )
323
+ // No probed chain referenced the contract cache as Static
324
+ // per chain to avoid recomputing topic selections each batch.
325
+ // The addresses getter throws: if a code path the probe didn't
326
+ // exercise reads `chain.<Contract>.addresses` at runtime, silent
327
+ // [] would produce wrong topics — throw a user-friendly error
328
+ // instead so the user rewrites the callback to surface the
329
+ // dependency up-front.
330
+ chain => {
331
+ let chainId = chain->ChainMap.Chain.toChainId
332
+ let chainArg = makeDetectionChainArg(~contractName, ~chainId, ~getAddresses=() =>
333
+ JsError.throwWithMessage(
334
+ `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.`,
335
+ )
336
+ )
337
+ Internal.Static(fn({chain: chainArg->Obj.magic})->parse)
338
+ }
173
339
  }
174
340
  } else {
341
+ startBlock := extractStartBlock(~onEventBlockFilterSchema, ~contractName, eventFilters)
175
342
  let static: Internal.eventFilters = Static(eventFilters->parse)
176
343
  _ => static
177
344
  }
@@ -180,6 +347,7 @@ let parseEventFiltersOrThrow = {
180
347
  {
181
348
  getEventFiltersOrThrow,
182
349
  filterByAddresses: filterByAddresses.contents,
350
+ startBlock: startBlock.contents,
183
351
  }
184
352
  }
185
353
  }