envio 3.2.0 → 3.3.0-alpha.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 (113) hide show
  1. package/package.json +6 -7
  2. package/src/Batch.res +12 -15
  3. package/src/Batch.res.mjs +4 -5
  4. package/src/BatchProcessing.res +199 -0
  5. package/src/BatchProcessing.res.mjs +125 -0
  6. package/src/ChainFetching.res +375 -0
  7. package/src/ChainFetching.res.mjs +206 -0
  8. package/src/ChainMetadata.res +27 -0
  9. package/src/ChainMetadata.res.mjs +27 -0
  10. package/src/ChainState.res +618 -0
  11. package/src/ChainState.res.mjs +461 -0
  12. package/src/ChainState.resi +85 -0
  13. package/src/Config.res +13 -4
  14. package/src/Config.res.mjs +8 -3
  15. package/src/ContractRegisterContext.res +106 -0
  16. package/src/ContractRegisterContext.res.mjs +76 -0
  17. package/src/Core.res +3 -0
  18. package/src/CrossChainState.res +269 -0
  19. package/src/CrossChainState.res.mjs +197 -0
  20. package/src/CrossChainState.resi +47 -0
  21. package/src/Ecosystem.res +58 -0
  22. package/src/Ecosystem.res.mjs +43 -1
  23. package/src/Env.res +7 -2
  24. package/src/Env.res.mjs +5 -2
  25. package/src/EventConfigBuilder.res +35 -0
  26. package/src/EventConfigBuilder.res.mjs +21 -0
  27. package/src/EventProcessing.res +60 -46
  28. package/src/EventProcessing.res.mjs +33 -32
  29. package/src/EventUtils.res +0 -4
  30. package/src/EventUtils.res.mjs +0 -4
  31. package/src/ExitOnCaughtUp.res +10 -0
  32. package/src/ExitOnCaughtUp.res.mjs +22 -0
  33. package/src/FetchState.res +162 -113
  34. package/src/FetchState.res.mjs +181 -119
  35. package/src/HandlerLoader.res +6 -1
  36. package/src/HandlerLoader.res.mjs +13 -36
  37. package/src/InMemoryStore.res +36 -451
  38. package/src/InMemoryStore.res.mjs +20 -369
  39. package/src/IndexerLoop.res +44 -0
  40. package/src/IndexerLoop.res.mjs +46 -0
  41. package/src/IndexerState.res +544 -0
  42. package/src/IndexerState.res.mjs +547 -0
  43. package/src/IndexerState.resi +132 -0
  44. package/src/Internal.res +44 -7
  45. package/src/LoadLayer.res +20 -18
  46. package/src/LoadLayer.res.mjs +14 -12
  47. package/src/LoadLayer.resi +6 -3
  48. package/src/LogSelection.res +86 -17
  49. package/src/LogSelection.res.mjs +67 -14
  50. package/src/Logging.res +11 -44
  51. package/src/Logging.res.mjs +10 -42
  52. package/src/Main.res +54 -79
  53. package/src/Main.res.mjs +44 -56
  54. package/src/PgStorage.res +8 -71
  55. package/src/PgStorage.res.mjs +3 -47
  56. package/src/Prometheus.res +7 -8
  57. package/src/Prometheus.res.mjs +7 -4
  58. package/src/PruneStaleHistory.res +45 -0
  59. package/src/PruneStaleHistory.res.mjs +46 -0
  60. package/src/RawEvent.res +73 -0
  61. package/src/RawEvent.res.mjs +51 -0
  62. package/src/Rollback.res +204 -0
  63. package/src/Rollback.res.mjs +118 -0
  64. package/src/SimulateItems.res +12 -10
  65. package/src/SimulateItems.res.mjs +1 -1
  66. package/src/TestIndexer.res +34 -27
  67. package/src/TestIndexer.res.mjs +4 -1
  68. package/src/UserContext.res +26 -114
  69. package/src/UserContext.res.mjs +15 -78
  70. package/src/Writing.res +242 -0
  71. package/src/Writing.res.mjs +215 -0
  72. package/src/db/InternalTable.res +14 -27
  73. package/src/sources/Evm.res +40 -1
  74. package/src/sources/Evm.res.mjs +94 -84
  75. package/src/sources/Fuel.res +40 -1
  76. package/src/sources/Fuel.res.mjs +36 -26
  77. package/src/sources/HyperFuel.res +41 -120
  78. package/src/sources/HyperFuel.res.mjs +42 -80
  79. package/src/sources/HyperFuel.resi +1 -24
  80. package/src/sources/HyperFuelClient.res +16 -297
  81. package/src/sources/HyperFuelClient.res.mjs +5 -4
  82. package/src/sources/HyperFuelSource.res +33 -8
  83. package/src/sources/HyperFuelSource.res.mjs +56 -10
  84. package/src/sources/HyperSyncSource.res +7 -5
  85. package/src/sources/HyperSyncSource.res.mjs +3 -3
  86. package/src/sources/RpcSource.res +2 -2
  87. package/src/sources/RpcSource.res.mjs +1 -1
  88. package/src/sources/SourceManager.res +8 -18
  89. package/src/sources/SourceManager.res.mjs +8 -5
  90. package/src/sources/SourceManager.resi +4 -1
  91. package/src/sources/Svm.res +18 -1
  92. package/src/sources/Svm.res.mjs +30 -22
  93. package/src/sources/SvmHyperSyncSource.res +1 -1
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  95. package/src/tui/Tui.res +38 -36
  96. package/src/tui/Tui.res.mjs +51 -51
  97. package/src/ChainFetcher.res +0 -519
  98. package/src/ChainFetcher.res.mjs +0 -314
  99. package/src/ChainManager.res +0 -358
  100. package/src/ChainManager.res.mjs +0 -291
  101. package/src/Ctx.res +0 -6
  102. package/src/Ctx.res.mjs +0 -2
  103. package/src/GlobalState.res +0 -1056
  104. package/src/GlobalState.res.mjs +0 -1086
  105. package/src/GlobalStateManager.res +0 -57
  106. package/src/GlobalStateManager.res.mjs +0 -68
  107. package/src/GlobalStateManager.resi +0 -7
  108. package/src/Ports.res +0 -5
  109. package/src/Ports.res.mjs +0 -9
  110. package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
  111. package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
  112. package/src/sources/EnvioApiClient.res +0 -15
  113. package/src/sources/EnvioApiClient.res.mjs +0 -24
package/src/Internal.res CHANGED
@@ -269,12 +269,15 @@ type genericEvent<'params, 'block, 'transaction> = {
269
269
  // (blockNumber, timestamp, blockHash) live on the item instead.
270
270
  type event
271
271
 
272
- external fromGenericEvent: genericEvent<'a, 'b, 'c> => event = "%identity"
272
+ // Opaque payload an item carries. A source builds an ecosystem-specific
273
+ // concrete payload (see `Evm.payload` / `Fuel.payload`) and erases it to this
274
+ // type; consumers never read it directly — the ecosystem converts it back to
275
+ // its own payload to produce the user-facing `event`, a logger, or a raw
276
+ // event. The concrete payload types deliberately live in the ecosystem
277
+ // modules, not here, and are distinct per ecosystem.
278
+ type eventPayload
273
279
 
274
- // Escape hatch for serialization boundaries (raw events, logging)
275
- // which genuinely need the runtime shape.
276
- external toGenericEvent: event => genericEvent<eventParams, eventBlock, eventTransaction> =
277
- "%identity"
280
+ external payloadToEvent: eventPayload => event = "%identity"
278
281
 
279
282
  type genericLoaderArgs<'event, 'context> = {
280
283
  event: 'event,
@@ -344,6 +347,16 @@ type rec paramMeta = {
344
347
  components?: array<paramMeta>,
345
348
  }
346
349
 
350
+ // Fetch-state registry value for an indexed contract address.
351
+ // `effectiveStartBlock` is derived from the registration block and the
352
+ // contract's configured start block (see `FetchState.deriveEffectiveStartBlock`).
353
+ type indexingContract = {
354
+ address: Address.t,
355
+ contractName: string,
356
+ registrationBlock: int,
357
+ effectiveStartBlock: int,
358
+ }
359
+
347
360
  // This is private so it's not manually constructed internally
348
361
  // The idea is that it can only be coerced from fuel/evmEventConfig
349
362
  // and it can include their fields. We prevent manual creation,
@@ -358,6 +371,12 @@ type eventConfig = private {
358
371
  // Usually always false for wildcard events
359
372
  // But might be true for wildcard event with dynamic event filter by addresses
360
373
  dependsOnAddresses: bool,
374
+ // Precompiled predicate (EVM only) for events that filter an indexed address
375
+ // param by registered addresses. Given the decoded event and the log's block
376
+ // number, drops an event whose param-address isn't registered at/before that
377
+ // block — the param-level analogue of EventRouter's srcAddress
378
+ // `effectiveStartBlock` check. Absent otherwise.
379
+ clientAddressFilter?: (eventPayload, int, dict<indexingContract>) => bool,
361
380
  handler: option<handler>,
362
381
  contractRegister: option<contractRegister>,
363
382
  paramsRawEventSchema: S.schema<eventParams>,
@@ -483,7 +502,25 @@ type eventItem = private {
483
502
  blockNumber: int,
484
503
  blockHash: string,
485
504
  logIndex: int,
486
- event: event,
505
+ payload: eventPayload,
506
+ }
507
+
508
+ // Row shape for the `raw_events` table. Defined here (rather than in
509
+ // `InternalTable`) so the ecosystem's `toRawEvent` can reference it without
510
+ // pulling in `InternalTable`'s dependency on `Config`.
511
+ type rawEvent = {
512
+ chain_id: int,
513
+ event_id: bigint,
514
+ event_name: string,
515
+ contract_name: string,
516
+ block_number: int,
517
+ log_index: int,
518
+ src_address: Address.t,
519
+ block_hash: string,
520
+ block_timestamp: int,
521
+ block_fields: JSON.t,
522
+ transaction_fields: JSON.t,
523
+ params: JSON.t,
487
524
  }
488
525
 
489
526
  // Opaque type to support both EVM and other ecosystems
@@ -517,7 +554,7 @@ type item =
517
554
  blockNumber: int,
518
555
  blockHash: string,
519
556
  logIndex: int,
520
- event: event,
557
+ payload: eventPayload,
521
558
  })
522
559
  | @as(1) Block({onBlockConfig: onBlockConfig, blockNumber: int, logIndex: int})
523
560
 
package/src/LoadLayer.res CHANGED
@@ -2,19 +2,20 @@ let loadById = (
2
2
  ~loadManager,
3
3
  ~persistence: Persistence.t,
4
4
  ~entityConfig: Internal.entityConfig,
5
- ~inMemoryStore,
5
+ ~indexerState,
6
6
  ~shouldGroup,
7
7
  ~item,
8
+ ~ecosystem,
8
9
  ~entityId,
9
10
  ) => {
10
11
  let key = `${entityConfig.name}.get`
11
- let inMemTable = inMemoryStore->InMemoryStore.getInMemTable(~entityConfig)
12
+ let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig)
12
13
 
13
14
  let load = async (idsToLoad, ~onError as _) => {
14
15
  let storage = persistence->Persistence.getInitializedStorageOrThrow
15
16
  let timerRef = Prometheus.StorageLoad.startOperation(~storage=storage.name, ~operation=key)
16
17
 
17
- // Since LoadManager.call prevents registerign entities already existing in the inMemoryStore,
18
+ // Since LoadManager.call prevents registering entities already in the in-memory store,
18
19
  // we can be sure that we load only the new ones.
19
20
  let dbEntities = try {
20
21
  (
@@ -28,7 +29,10 @@ let loadById = (
28
29
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
29
30
  } catch {
30
31
  | Persistence.StorageError({message, reason}) =>
31
- reason->ErrorHandling.mkLogAndRaise(~logger=item->Logging.getItemLogger, ~msg=message)
32
+ reason->ErrorHandling.mkLogAndRaise(
33
+ ~logger=Ecosystem.getItemLogger(item, ~ecosystem),
34
+ ~msg=message,
35
+ )
32
36
  }
33
37
 
34
38
  let entitiesMap = Dict.make()
@@ -40,7 +44,7 @@ let loadById = (
40
44
  }
41
45
  idsToLoad->Array.forEach(entityId => {
42
46
  inMemTable->InMemoryTable.Entity.initValue(
43
- ~committedCheckpointId=inMemoryStore.committedCheckpointId,
47
+ ~committedCheckpointId=indexerState->IndexerState.committedCheckpointId,
44
48
  ~key=entityId,
45
49
  ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
46
50
  )
@@ -68,7 +72,7 @@ let loadById = (
68
72
  let callEffect = (
69
73
  ~effect: Internal.effect,
70
74
  ~arg: Internal.effectArgs,
71
- ~inMemTable: InMemoryStore.effectCacheInMemTable,
75
+ ~inMemTable: IndexerState.effectCacheInMemTable,
72
76
  ~timerRef,
73
77
  ~onError,
74
78
  ) => {
@@ -245,13 +249,14 @@ let loadEffect = (
245
249
  ~persistence: Persistence.t,
246
250
  ~effect: Internal.effect,
247
251
  ~effectArgs,
248
- ~inMemoryStore,
252
+ ~indexerState,
249
253
  ~shouldGroup,
250
254
  ~item,
255
+ ~ecosystem,
251
256
  ) => {
252
257
  let effectName = effect.name
253
258
  let key = `${effectName}.effect`
254
- let inMemTable = inMemoryStore->InMemoryStore.getEffectInMemTable(~effect)
259
+ let inMemTable = indexerState->InMemoryStore.getEffectInMemTable(~effect)
255
260
 
256
261
  let load = async (args, ~onError) => {
257
262
  let idsToLoad = args->Array.map((arg: Internal.effectArgs) => arg.cacheKey)
@@ -279,9 +284,7 @@ let loadEffect = (
279
284
  )->(Utils.magic: array<unknown> => array<Internal.effectCacheItem>)
280
285
  } catch {
281
286
  | exn =>
282
- item
283
- ->Logging.getItemLogger
284
- ->Logging.childWarn({
287
+ Ecosystem.getItemLogger(item, ~ecosystem)->Logging.childWarn({
285
288
  "msg": `Failed to load cache effect cache. The indexer will continue working, but the effect will not be able to use the cache.`,
286
289
  "err": exn->Utils.prettifyExn,
287
290
  "effect": effectName,
@@ -298,9 +301,7 @@ let loadEffect = (
298
301
  | S.Raised(error) =>
299
302
  inMemTable.invalidationsCount = inMemTable.invalidationsCount + 1
300
303
  Prometheus.EffectCacheInvalidationsCount.increment(~effectName)
301
- item
302
- ->Logging.getItemLogger
303
- ->Logging.childTrace({
304
+ Ecosystem.getItemLogger(item, ~ecosystem)->Logging.childTrace({
304
305
  "msg": "Invalidated effect cache",
305
306
  "input": dbEntity.id,
306
307
  "effect": effectName,
@@ -354,13 +355,14 @@ let loadByFilter = (
354
355
  ~loadManager,
355
356
  ~persistence: Persistence.t,
356
357
  ~entityConfig: Internal.entityConfig,
357
- ~inMemoryStore,
358
+ ~indexerState,
358
359
  ~shouldGroup,
359
360
  ~item,
361
+ ~ecosystem,
360
362
  ~filter: EntityFilter.t,
361
363
  ) => {
362
364
  let key = filter->EntityFilter.toOperationKey(~entityName=entityConfig.name)
363
- let inMemTable = inMemoryStore->InMemoryStore.getInMemTable(~entityConfig)
365
+ let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig)
364
366
 
365
367
  let load = async (filters: array<EntityFilter.t>, ~onError as _) => {
366
368
  let storage = persistence->Persistence.getInitializedStorageOrThrow
@@ -385,7 +387,7 @@ let loadByFilter = (
385
387
 
386
388
  entities->Array.forEach(entity => {
387
389
  inMemTable->InMemoryTable.Entity.initValue(
388
- ~committedCheckpointId=inMemoryStore.committedCheckpointId,
390
+ ~committedCheckpointId=indexerState->IndexerState.committedCheckpointId,
389
391
  ~key=entity.id,
390
392
  ~entity=Some(entity),
391
393
  )
@@ -396,7 +398,7 @@ let loadByFilter = (
396
398
  | Persistence.StorageError({message, reason}) =>
397
399
  reason->ErrorHandling.mkLogAndRaise(
398
400
  ~logger=Logging.createChildFrom(
399
- ~logger=item->Logging.getItemLogger,
401
+ ~logger=Ecosystem.getItemLogger(item, ~ecosystem),
400
402
  // The executed query might be merged from multiple getWhere
401
403
  // calls, so report it as the operation users write with the
402
404
  // values bound to its placeholders, instead of an internal
@@ -4,10 +4,12 @@ import * as Table from "./db/Table.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
5
  import * as Hrtime from "./bindings/Hrtime.res.mjs";
6
6
  import * as Logging from "./Logging.res.mjs";
7
+ import * as Ecosystem from "./Ecosystem.res.mjs";
7
8
  import * as Prometheus from "./Prometheus.res.mjs";
8
9
  import * as LoadManager from "./LoadManager.res.mjs";
9
10
  import * as Persistence from "./Persistence.res.mjs";
10
11
  import * as EntityFilter from "./db/EntityFilter.res.mjs";
12
+ import * as IndexerState from "./IndexerState.res.mjs";
11
13
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
12
14
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
13
15
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
@@ -15,9 +17,9 @@ import * as InMemoryTable from "./InMemoryTable.res.mjs";
15
17
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
16
18
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
17
19
 
18
- function loadById(loadManager, persistence, entityConfig, inMemoryStore, shouldGroup, item, entityId) {
20
+ function loadById(loadManager, persistence, entityConfig, indexerState, shouldGroup, item, ecosystem, entityId) {
19
21
  let key = entityConfig.name + `.get`;
20
- let inMemTable = InMemoryStore.getInMemTable(inMemoryStore, entityConfig);
22
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig);
21
23
  let load = async (idsToLoad, param) => {
22
24
  let storage = Persistence.getInitializedStorageOrThrow(persistence);
23
25
  let timerRef = Prometheus.StorageLoad.startOperation(storage.name, key);
@@ -31,7 +33,7 @@ function loadById(loadManager, persistence, entityConfig, inMemoryStore, shouldG
31
33
  } catch (raw_exn) {
32
34
  let exn = Primitive_exceptions.internalToException(raw_exn);
33
35
  if (exn.RE_EXN_ID === Persistence.StorageError) {
34
- dbEntities = ErrorHandling.mkLogAndRaise(Logging.getItemLogger(item), exn.message, exn.reason);
36
+ dbEntities = ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(item, ecosystem), exn.message, exn.reason);
35
37
  } else {
36
38
  throw exn;
37
39
  }
@@ -41,7 +43,7 @@ function loadById(loadManager, persistence, entityConfig, inMemoryStore, shouldG
41
43
  let entity = dbEntities[idx];
42
44
  entitiesMap[entity.id] = entity;
43
45
  }
44
- idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, inMemoryStore.committedCheckpointId, entityId, entitiesMap[entityId]));
46
+ idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, IndexerState.committedCheckpointId(indexerState), entityId, entitiesMap[entityId]));
45
47
  return Prometheus.StorageLoad.endOperation(timerRef, storage.name, key, idsToLoad.length, dbEntities.length);
46
48
  };
47
49
  return LoadManager.call(loadManager, entityId, key, load, LoadManager.noopHasher, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
@@ -126,10 +128,10 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
126
128
  return Promise.all(promises);
127
129
  }
128
130
 
129
- function loadEffect(loadManager, persistence, effect, effectArgs, inMemoryStore, shouldGroup, item) {
131
+ function loadEffect(loadManager, persistence, effect, effectArgs, indexerState, shouldGroup, item, ecosystem) {
130
132
  let effectName = effect.name;
131
133
  let key = effectName + `.effect`;
132
- let inMemTable = InMemoryStore.getEffectInMemTable(inMemoryStore, effect);
134
+ let inMemTable = InMemoryStore.getEffectInMemTable(indexerState, effect);
133
135
  let load = async (args, onError) => {
134
136
  let idsToLoad = args.map(arg => arg.cacheKey);
135
137
  let idsFromCache = new Set();
@@ -150,7 +152,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, inMemoryStore,
150
152
  }, match$1.table);
151
153
  } catch (raw_exn) {
152
154
  let exn = Primitive_exceptions.internalToException(raw_exn);
153
- Logging.childWarn(Logging.getItemLogger(item), {
155
+ Logging.childWarn(Ecosystem.getItemLogger(item, ecosystem), {
154
156
  msg: `Failed to load cache effect cache. The indexer will continue working, but the effect will not be able to use the cache.`,
155
157
  err: Utils.prettifyExn(exn),
156
158
  effect: effectName
@@ -167,7 +169,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, inMemoryStore,
167
169
  if (error.RE_EXN_ID === S$RescriptSchema.Raised) {
168
170
  inMemTable.invalidationsCount = inMemTable.invalidationsCount + 1 | 0;
169
171
  Prometheus.EffectCacheInvalidationsCount.increment(effectName);
170
- return Logging.childTrace(Logging.getItemLogger(item), {
172
+ return Logging.childTrace(Ecosystem.getItemLogger(item, ecosystem), {
171
173
  msg: "Invalidated effect cache",
172
174
  input: dbEntity.id,
173
175
  effect: effectName,
@@ -197,9 +199,9 @@ function loadEffect(loadManager, persistence, effect, effectArgs, inMemoryStore,
197
199
  return LoadManager.call(loadManager, effectArgs, key, load, args => args.cacheKey, shouldGroup, hash => InMemoryStore.hasEffectOutput(inMemTable, hash), hash => InMemoryStore.getEffectOutputUnsafe(inMemTable, hash));
198
200
  }
199
201
 
200
- function loadByFilter(loadManager, persistence, entityConfig, inMemoryStore, shouldGroup, item, filter) {
202
+ function loadByFilter(loadManager, persistence, entityConfig, indexerState, shouldGroup, item, ecosystem, filter) {
201
203
  let key = EntityFilter.toOperationKey(filter, entityConfig.name);
202
- let inMemTable = InMemoryStore.getInMemTable(inMemoryStore, entityConfig);
204
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig);
203
205
  let load = async (filters, param) => {
204
206
  let storage = Persistence.getInitializedStorageOrThrow(persistence);
205
207
  let timerRef = Prometheus.StorageLoad.startOperation(storage.name, key);
@@ -211,13 +213,13 @@ function loadByFilter(loadManager, persistence, entityConfig, inMemoryStore, sho
211
213
  await Promise.all(queries.map(async filter => {
212
214
  try {
213
215
  let entities = await storage.loadOrThrow(filter, entityConfig.table);
214
- entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, inMemoryStore.committedCheckpointId, entity.id, entity));
216
+ entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, IndexerState.committedCheckpointId(indexerState), entity.id, entity));
215
217
  size.contents = size.contents + entities.length | 0;
216
218
  return;
217
219
  } catch (raw_exn) {
218
220
  let exn = Primitive_exceptions.internalToException(raw_exn);
219
221
  if (exn.RE_EXN_ID === Persistence.StorageError) {
220
- return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Logging.getItemLogger(item), {
222
+ return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
221
223
  operation: key,
222
224
  params: EntityFilter.getParams(filter)
223
225
  }), exn.message, exn.reason);
@@ -2,9 +2,10 @@ let loadById: (
2
2
  ~loadManager: LoadManager.t,
3
3
  ~persistence: Persistence.t,
4
4
  ~entityConfig: Internal.entityConfig,
5
- ~inMemoryStore: InMemoryStore.t,
5
+ ~indexerState: IndexerState.t,
6
6
  ~shouldGroup: bool,
7
7
  ~item: Internal.item,
8
+ ~ecosystem: Ecosystem.t,
8
9
  ~entityId: string,
9
10
  ) => promise<option<Internal.entity>>
10
11
 
@@ -12,9 +13,10 @@ let loadByFilter: (
12
13
  ~loadManager: LoadManager.t,
13
14
  ~persistence: Persistence.t,
14
15
  ~entityConfig: Internal.entityConfig,
15
- ~inMemoryStore: InMemoryStore.t,
16
+ ~indexerState: IndexerState.t,
16
17
  ~shouldGroup: bool,
17
18
  ~item: Internal.item,
19
+ ~ecosystem: Ecosystem.t,
18
20
  ~filter: EntityFilter.t,
19
21
  ) => promise<array<Internal.entity>>
20
22
 
@@ -23,7 +25,8 @@ let loadEffect: (
23
25
  ~persistence: Persistence.t,
24
26
  ~effect: Internal.effect,
25
27
  ~effectArgs: Internal.effectArgs,
26
- ~inMemoryStore: InMemoryStore.t,
28
+ ~indexerState: IndexerState.t,
27
29
  ~shouldGroup: bool,
28
30
  ~item: Internal.item,
31
+ ~ecosystem: Ecosystem.t,
29
32
  ) => promise<Internal.effectOutput>
@@ -60,6 +60,10 @@ let make = (~addresses, ~topicSelections) => {
60
60
  type parsedEventFilters = {
61
61
  getEventFiltersOrThrow: ChainMap.Chain.t => Internal.eventFilters,
62
62
  filterByAddresses: bool,
63
+ // Indexed params filtered by `chain.<Contract>.addresses`, in disjunctive
64
+ // normal form (outer array OR of AND-groups). Empty unless `filterByAddresses`.
65
+ // Consumed by the codegen of the event's `clientAddressFilter`.
66
+ addressFilterParamGroups: array<array<string>>,
63
67
  // `_gte` from the top-level `block` filter of the user's `where`,
64
68
  // resolved at build time (per-chain via the `probeChainId`). The
65
69
  // caller uses this to override the per-event `startBlock` — a
@@ -151,6 +155,70 @@ let makeDetectionChainArg = (
151
155
  chainObj
152
156
  }
153
157
 
158
+ // Sentinel returned by `chain.<Contract>.addresses` during detection. A Proxy
159
+ // whose traps throw on any access, so the only non-throwing use is passing it
160
+ // straight through as a param filter value — which `extractAddressFilterGroups`
161
+ // then finds by identity. Misuse (spread/map/index/...) fails loud at the site.
162
+ let makeAddressesProbe: (
163
+ ~contractName: string,
164
+ ) => array<Address.t> = %raw(`function (contractName) {
165
+ var trap = function () {
166
+ throw new Error(
167
+ 'Invalid where configuration for "' + contractName +
168
+ '": chain.' + contractName + '.addresses must be passed directly as an indexed-param ' +
169
+ 'filter value (e.g. { params: { to: chain.' + contractName + '.addresses } }). ' +
170
+ 'It cannot be spread, mapped, indexed, or otherwise transformed.'
171
+ );
172
+ };
173
+ return new Proxy([], {get: trap});
174
+ }`)
175
+
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
+
154
222
  let parseEventFiltersOrThrow = {
155
223
  let emptyTopics = []
156
224
  let noopGetter = _ => emptyTopics
@@ -167,6 +235,7 @@ let parseEventFiltersOrThrow = {
167
235
  ~topic3=noopGetter,
168
236
  ): parsedEventFilters => {
169
237
  let filterByAddresses = ref(false)
238
+ let addressFilterParamGroups = ref([])
170
239
  let startBlock = ref(None)
171
240
  let topic0 = [sighash->EvmTypes.Hex.fromStringUnsafe]
172
241
  let default = {
@@ -286,24 +355,23 @@ let parseEventFiltersOrThrow = {
286
355
  // `startBlock` (from `where.block`) for this chain — a second
287
356
  // invocation would risk observing different state for callbacks
288
357
  // that close over mutable references.
289
- let probedResult = try {
290
- let chain = makeDetectionChainArg(
291
- ~contractName,
292
- ~chainId=probeChainId,
293
- ~getAddresses=() => {
294
- filterByAddresses := true
295
- []
296
- },
297
- )
298
- Some(fn({chain: chain->Obj.magic}))
299
- } catch {
300
- | _ => None
301
- }
302
- switch probedResult {
303
- | Some(result) =>
304
- startBlock := extractStartBlock(~onEventBlockFilterSchema, ~contractName, result)
305
- | None => ()
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
+ },
368
+ )
369
+ let probedResult = fn({chain: chain->Obj.magic})
370
+ if filterByAddresses.contents {
371
+ addressFilterParamGroups :=
372
+ extractAddressFilterGroupsOrThrow(probedResult, ~probe=addressesProbe, ~contractName)
306
373
  }
374
+ startBlock := extractStartBlock(~onEventBlockFilterSchema, ~contractName, probedResult)
307
375
  if filterByAddresses.contents {
308
376
  chain => Internal.Dynamic(
309
377
  addresses => {
@@ -343,6 +411,7 @@ let parseEventFiltersOrThrow = {
343
411
  {
344
412
  getEventFiltersOrThrow,
345
413
  filterByAddresses: filterByAddresses.contents,
414
+ addressFilterParamGroups: addressFilterParamGroups.contents,
346
415
  startBlock: startBlock.contents,
347
416
  }
348
417
  }
@@ -121,6 +121,58 @@ function makeDetectionChainArg(contractName, chainId, getAddresses) {
121
121
  return chainObj;
122
122
  }
123
123
 
124
+ let makeAddressesProbe = (function (contractName) {
125
+ var trap = function () {
126
+ throw new Error(
127
+ 'Invalid where configuration for "' + contractName +
128
+ '": chain.' + contractName + '.addresses must be passed directly as an indexed-param ' +
129
+ 'filter value (e.g. { params: { to: chain.' + contractName + '.addresses } }). ' +
130
+ 'It cannot be spread, mapped, indexed, or otherwise transformed.'
131
+ );
132
+ };
133
+ return new Proxy([], {get: trap});
134
+ });
135
+
136
+ function extractAddressFilterGroupsOrThrow(result, probe, contractName) {
137
+ let groups = [];
138
+ let scanGroup = paramsObj => {
139
+ let names = [];
140
+ Utils.Dict.forEachWithKey(paramsObj, (value, key) => {
141
+ if (value === probe) {
142
+ names.push(key);
143
+ paramsObj[key] = [];
144
+ return;
145
+ }
146
+ });
147
+ if (!Utils.$$Array.isEmpty(names)) {
148
+ groups.push(names);
149
+ return;
150
+ }
151
+ };
152
+ if (typeof result === "object" && result !== null && !Array.isArray(result)) {
153
+ let match = result["params"];
154
+ if (match !== undefined) {
155
+ if (Array.isArray(match)) {
156
+ match.forEach(item => {
157
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
158
+ return scanGroup(item);
159
+ }
160
+ });
161
+ } else {
162
+ switch (typeof match) {
163
+ case "object" :
164
+ scanGroup(match);
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ if (Utils.$$Array.isEmpty(groups)) {
171
+ Stdlib_JsError.throwWithMessage(`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 } }.`);
172
+ }
173
+ return groups;
174
+ }
175
+
124
176
  let emptyTopics = [];
125
177
 
126
178
  function noopGetter(param) {
@@ -134,6 +186,7 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
134
186
  let filterByAddresses = {
135
187
  contents: false
136
188
  };
189
+ let addressFilterParamGroups = [];
137
190
  let startBlock;
138
191
  let topic0 = [sighash];
139
192
  let $$default = {
@@ -208,21 +261,18 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
208
261
  let getEventFiltersOrThrow;
209
262
  if (eventFilters !== undefined) {
210
263
  if (typeof eventFilters === "function") {
211
- let probedResult;
212
- try {
213
- let chain = makeDetectionChainArg(contractName, probeChainId, () => {
214
- filterByAddresses.contents = true;
215
- return [];
216
- });
217
- probedResult = eventFilters({
218
- chain: chain
219
- });
220
- } catch (exn) {
221
- probedResult = undefined;
222
- }
223
- if (probedResult !== undefined) {
224
- startBlock = extractStartBlock(probedResult, onEventBlockFilterSchema, contractName);
264
+ let addressesProbe = makeAddressesProbe(contractName);
265
+ let chain = makeDetectionChainArg(contractName, probeChainId, () => {
266
+ filterByAddresses.contents = true;
267
+ return addressesProbe;
268
+ });
269
+ let probedResult = eventFilters({
270
+ chain: chain
271
+ });
272
+ if (filterByAddresses.contents) {
273
+ addressFilterParamGroups = extractAddressFilterGroupsOrThrow(probedResult, addressesProbe, contractName);
225
274
  }
275
+ startBlock = extractStartBlock(probedResult, onEventBlockFilterSchema, contractName);
226
276
  getEventFiltersOrThrow = filterByAddresses.contents ? chain => ({
227
277
  TAG: "Dynamic",
228
278
  _0: addresses => {
@@ -258,6 +308,7 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
258
308
  return {
259
309
  getEventFiltersOrThrow: getEventFiltersOrThrow,
260
310
  filterByAddresses: filterByAddresses.contents,
311
+ addressFilterParamGroups: addressFilterParamGroups,
261
312
  startBlock: startBlock
262
313
  };
263
314
  }
@@ -272,6 +323,8 @@ export {
272
323
  extractStartBlock,
273
324
  makeChainArg,
274
325
  makeDetectionChainArg,
326
+ makeAddressesProbe,
327
+ extractAddressFilterGroupsOrThrow,
275
328
  parseEventFiltersOrThrow,
276
329
  }
277
330
  /* eventBlockRangeSchema Not a pure module */
package/src/Logging.res CHANGED
@@ -155,46 +155,11 @@ let createChildFrom = (~logger: t, ~params: 'a) => {
155
155
  logger->child(params->createChildParams)
156
156
  }
157
157
 
158
- let getItemLogger = {
159
- let cacheKey = "_logger"
160
- (item: Internal.item) => {
161
- switch item
162
- ->(Utils.magic: Internal.item => dict<Pino.t>)
163
- ->Utils.Dict.dangerouslyGetNonOption(cacheKey) {
164
- | Some(l) => l
165
- | None => {
166
- let l = getLogger()->child(
167
- switch item {
168
- | Event({eventConfig, chain, blockNumber, logIndex, event}) =>
169
- {
170
- "contractName": eventConfig.contractName,
171
- "eventName": eventConfig.name,
172
- "chainId": chain->ChainMap.Chain.toChainId,
173
- "block": blockNumber,
174
- "logIndex": logIndex,
175
- "address": (event->Internal.toGenericEvent).srcAddress,
176
- }->createChildParams
177
- | Block({blockNumber, onBlockConfig}) =>
178
- {
179
- "onBlock": onBlockConfig.name,
180
- "chainId": onBlockConfig.chainId,
181
- "block": blockNumber,
182
- }->createChildParams
183
- },
184
- )
185
- item->(Utils.magic: Internal.item => dict<Pino.t>)->Dict.set(cacheKey, l)
186
- l
187
- }
188
- }
189
- }
190
- }
191
-
192
158
  @inline
193
- let logForItem = (item, level: Pino.logLevel, message: string, ~params=?) => {
159
+ let logAtLevel = (logger: t, level: Pino.logLevel, message: string, ~params=?) => {
194
160
  (
195
- item
196
- ->getItemLogger
197
- ->(Utils.magic: Pino.t => dict<(option<'a>, string) => unit>)
161
+ logger
162
+ ->(Utils.magic: t => dict<(option<'a>, string) => unit>)
198
163
  ->Dict.getUnsafe((level :> string))
199
164
  )(params, message)
200
165
  }
@@ -207,11 +172,13 @@ let noopLogger: Envio.logger = {
207
172
  errorWithExn: (_message: string, _exn) => (),
208
173
  }
209
174
 
210
- let getUserLogger = (item): Envio.logger => {
211
- info: (message: string, ~params=?) => item->logForItem(#uinfo, message, ~params?),
212
- debug: (message: string, ~params=?) => item->logForItem(#udebug, message, ~params?),
213
- warn: (message: string, ~params=?) => item->logForItem(#uwarn, message, ~params?),
214
- error: (message: string, ~params=?) => item->logForItem(#uerror, message, ~params?),
175
+ // Wrap a (child) logger as the user-facing `context.log`, routing through the
176
+ // custom `u*` levels. The caller builds the per-item logger via the ecosystem.
177
+ let userLogger = (logger: t): Envio.logger => {
178
+ info: (message: string, ~params=?) => logger->logAtLevel(#uinfo, message, ~params?),
179
+ debug: (message: string, ~params=?) => logger->logAtLevel(#udebug, message, ~params?),
180
+ warn: (message: string, ~params=?) => logger->logAtLevel(#uwarn, message, ~params?),
181
+ error: (message: string, ~params=?) => logger->logAtLevel(#uerror, message, ~params?),
215
182
  errorWithExn: (message: string, exn) =>
216
- item->logForItem(#uerror, message, ~params={"err": exn->Utils.prettifyExn}),
183
+ logger->logAtLevel(#uerror, message, ~params={"err": exn->Utils.prettifyExn}),
217
184
  }