envio 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -70,10 +70,10 @@
70
70
  "tsx": "4.21.0"
71
71
  },
72
72
  "optionalDependencies": {
73
- "envio-linux-x64": "3.2.0",
74
- "envio-linux-x64-musl": "3.2.0",
75
- "envio-linux-arm64": "3.2.0",
76
- "envio-darwin-x64": "3.2.0",
77
- "envio-darwin-arm64": "3.2.0"
73
+ "envio-linux-x64": "3.2.1",
74
+ "envio-linux-x64-musl": "3.2.1",
75
+ "envio-linux-arm64": "3.2.1",
76
+ "envio-darwin-x64": "3.2.1",
77
+ "envio-darwin-arm64": "3.2.1"
78
78
  }
79
79
  }
@@ -309,6 +309,39 @@ let resolveFieldSelection = (
309
309
  (selectedBlockFields, selectedTransactionFields)
310
310
  }
311
311
 
312
+ // ============== Client-side address filter ==============
313
+
314
+ let compileAddressFilter: string => (
315
+ Internal.event,
316
+ int,
317
+ dict<Internal.indexingContract>,
318
+ ) => bool = %raw(`function (body) {
319
+ return new Function("event", "blockNumber", "indexingAddresses", body);
320
+ }`)
321
+
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 {
329
+ | [] => None
330
+ | _ =>
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(" || ") ++ ";")
338
+ }
339
+ }
340
+
341
+ let buildAddressFilter = (groups: array<array<string>>): option<
342
+ (Internal.event, int, dict<Internal.indexingContract>) => bool,
343
+ > => buildAddressFilterBody(groups)->Option.map(compileAddressFilter)
344
+
312
345
  // ============== Build complete EVM event config ==============
313
346
 
314
347
  let buildEvmEventConfig = (
@@ -334,6 +367,7 @@ let buildEvmEventConfig = (
334
367
  let {
335
368
  getEventFiltersOrThrow,
336
369
  filterByAddresses,
370
+ addressFilterParamGroups,
337
371
  startBlock: whereStartBlock,
338
372
  } = LogSelection.parseEventFiltersOrThrow(
339
373
  ~eventFilters,
@@ -374,6 +408,7 @@ let buildEvmEventConfig = (
374
408
  simulateParamsSchema: buildSimulateParamsSchema(params),
375
409
  getEventFiltersOrThrow,
376
410
  filterByAddresses,
411
+ clientAddressFilter: ?buildAddressFilter(addressFilterParamGroups),
377
412
  dependsOnAddresses: !isWildcard || filterByAddresses,
378
413
  startBlock: resolvedStartBlock,
379
414
  selectedBlockFields,
@@ -251,6 +251,23 @@ function resolveFieldSelection(blockFields, transactionFields, globalBlockFields
251
251
  ];
252
252
  }
253
253
 
254
+ let compileAddressFilter = (function (body) {
255
+ return new Function("event", "blockNumber", "indexingAddresses", body);
256
+ });
257
+
258
+ function buildAddressFilterBody(groups) {
259
+ if (groups.length === 0) {
260
+ return;
261
+ }
262
+ let leaf = name => `(ic = indexingAddresses[p[` + JSON.stringify(name) + `]]) !== undefined && ic.effectiveStartBlock <= blockNumber`;
263
+ let groupExprs = groups.map(group => "(" + group.map(leaf).join(" && ") + ")");
264
+ return "var p = event.params, ic; return " + groupExprs.join(" || ") + ";";
265
+ }
266
+
267
+ function buildAddressFilter(groups) {
268
+ return Stdlib_Option.map(buildAddressFilterBody(groups), compileAddressFilter);
269
+ }
270
+
254
271
  function buildEvmEventConfig(contractName, eventName, sighash, params, isWildcard, handler, contractRegister, eventFilters, probeChainId, onEventBlockFilterSchema, blockFields, transactionFields, startBlock, globalBlockFieldsSetOpt, globalTransactionFieldsSetOpt) {
255
272
  let globalBlockFieldsSet = globalBlockFieldsSetOpt !== undefined ? Primitive_option.valFromOption(globalBlockFieldsSetOpt) : new Set();
256
273
  let globalTransactionFieldsSet = globalTransactionFieldsSetOpt !== undefined ? Primitive_option.valFromOption(globalTransactionFieldsSetOpt) : new Set();
@@ -274,6 +291,7 @@ function buildEvmEventConfig(contractName, eventName, sighash, params, isWildcar
274
291
  isWildcard: isWildcard,
275
292
  filterByAddresses: filterByAddresses,
276
293
  dependsOnAddresses: !isWildcard || filterByAddresses,
294
+ clientAddressFilter: Stdlib_Option.map(buildAddressFilterBody(match.addressFilterParamGroups), compileAddressFilter),
277
295
  handler: handler,
278
296
  contractRegister: contractRegister,
279
297
  paramsRawEventSchema: buildParamsSchema(params),
@@ -393,6 +411,9 @@ export {
393
411
  buildTopicGetter,
394
412
  alwaysIncludedBlockFields,
395
413
  resolveFieldSelection,
414
+ compileAddressFilter,
415
+ buildAddressFilterBody,
416
+ buildAddressFilter,
396
417
  buildEvmEventConfig,
397
418
  buildSvmInstructionEventConfig,
398
419
  buildFuelEventConfig,
@@ -1,9 +1,6 @@
1
- type contractConfig = {filterByAddresses: bool, startBlock: option<int>}
1
+ type contractConfig = {startBlock: option<int>}
2
2
 
3
- type indexingAddress = {
4
- ...Internal.indexingAddress,
5
- effectiveStartBlock: int,
6
- }
3
+ type indexingAddress = Internal.indexingContract
7
4
 
8
5
  let deriveEffectiveStartBlock = (~registrationBlock: int, ~contractStartBlock: option<int>) => {
9
6
  Pervasives.max(Pervasives.max(registrationBlock, 0), contractStartBlock->Option.getOr(0))
@@ -756,7 +753,6 @@ Returns OptimizedPartitions.t directly.
756
753
  */
757
754
  let createPartitionsFromIndexingAddresses = (
758
755
  ~registeringContractsByContract: dict<dict<indexingAddress>>,
759
- ~contractConfigs: dict<contractConfig>,
760
756
  ~dynamicContracts: Utils.Set.t<string>,
761
757
  ~normalSelection: selection,
762
758
  ~maxAddrInPartition: int,
@@ -778,9 +774,6 @@ OptimizedPartitions.t => {
778
774
  let addresses =
779
775
  registeringContracts->Dict.keysToArray->(Utils.magic: array<string> => array<Address.t>)
780
776
 
781
- // Can unsafely get it, because we already filtered out the contracts
782
- // that don't have any events to fetch
783
- let contractConfig = contractConfigs->Dict.getUnsafe(contractName)
784
777
  let isDynamic = dynamicContracts->Utils.Set.has(contractName)
785
778
  let partitions = isDynamic ? dynamicPartitions : nonDynamicPartitions
786
779
 
@@ -802,30 +795,23 @@ OptimizedPartitions.t => {
802
795
  let maybeNextStartBlockKey =
803
796
  ascKeys->Array.getUnsafe(idx + 1)->(Utils.magic: string => option<string>)
804
797
 
805
- // For this case we can't filter out events earlier than contract registration
806
- // on the client side, so we need to keep the old logic of creating
807
- // a partition for every block range, so there are no irrelevant events
808
- let shouldAllocateNewPartition = if contractConfig.filterByAddresses {
809
- true
810
- } else {
811
- switch maybeNextStartBlockKey {
812
- | None => true
813
- | Some(nextStartBlockKey) => {
814
- let nextStartBlock = nextStartBlockKey->Int.fromString->Option.getUnsafe
815
- let shouldJoinCurrentStartBlock =
816
- nextStartBlock - startBlockRef.contents < OptimizedPartitions.tooFarBlockRange
817
-
818
- // If dynamic contract registration are close to eachother
819
- // and it's possible to use dc.effectiveStartBlock to filter out events on client side
820
- // then we can optimize the number of partitions,
821
- // by putting dcs with different startBlocks in the same partition
822
- if shouldJoinCurrentStartBlock {
823
- addressesRef :=
824
- addressesRef.contents->Array.concat(byStartBlock->Dict.getUnsafe(nextStartBlockKey))
825
- false
826
- } else {
827
- true
828
- }
798
+ let shouldAllocateNewPartition = switch maybeNextStartBlockKey {
799
+ | None => true
800
+ | Some(nextStartBlockKey) => {
801
+ let nextStartBlock = nextStartBlockKey->Int.fromString->Option.getUnsafe
802
+ let shouldJoinCurrentStartBlock =
803
+ nextStartBlock - startBlockRef.contents < OptimizedPartitions.tooFarBlockRange
804
+
805
+ // Addresses with different start blocks within range share a partition;
806
+ // events before each address's effectiveStartBlock are dropped on the
807
+ // client side (EventRouter for the srcAddress, the event's
808
+ // clientAddressFilter for address-valued params).
809
+ if shouldJoinCurrentStartBlock {
810
+ addressesRef :=
811
+ addressesRef.contents->Array.concat(byStartBlock->Dict.getUnsafe(nextStartBlockKey))
812
+ false
813
+ } else {
814
+ true
829
815
  }
830
816
  }
831
817
  }
@@ -907,15 +893,10 @@ OptimizedPartitions.t => {
907
893
  }
908
894
  }
909
895
 
910
- let nextContractName =
911
- nextP.addressesByContractName->Dict.keysToArray->Utils.Array.firstUnsafe
912
- let hasFilterByAddresses = (
913
- contractConfigs->Dict.getUnsafe(nextContractName)
914
- ).filterByAddresses
915
896
  let isTooFar = currentPBlock + OptimizedPartitions.tooFarBlockRange < nextPBlock
916
897
 
917
- if isTooFar || hasFilterByAddresses {
918
- // Too far or address-filtered: mergeBlock on current, merge addresses into next
898
+ if isTooFar {
899
+ // Too far: mergeBlock on current, merge addresses into next
919
900
  mergedNonDynamic
920
901
  ->Array.push({
921
902
  ...currentP,
@@ -927,7 +908,7 @@ OptimizedPartitions.t => {
927
908
  addressesByContractName: mergedAddresses,
928
909
  }
929
910
  } else {
930
- // Close and not address-filtered: push next's addresses into current
911
+ // Close: push next's addresses into current
931
912
  currentPRef := {
932
913
  ...currentP,
933
914
  addressesByContractName: mergedAddresses,
@@ -968,7 +949,6 @@ let registerDynamicContracts = (
968
949
  let indexingAddresses = fetchState.indexingAddresses
969
950
  let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
970
951
  let earliestRegisteringEventBlockNumber = ref(%raw(`Infinity`))
971
- let hasDCWithFilterByAddresses = ref(false)
972
952
  // Addresses registered for contracts without matching events. These are not
973
953
  // added to partitions, but they are tracked on fetchState.indexingAddresses
974
954
  // so that later conflicting registrations are detected, and are persisted
@@ -991,7 +971,7 @@ let registerDynamicContracts = (
991
971
  let shouldRemove = ref(false)
992
972
 
993
973
  switch fetchState.contractConfigs->Utils.Dict.dangerouslyGetNonOption(dc.contractName) {
994
- | Some({filterByAddresses, startBlock: contractStartBlock}) =>
974
+ | Some({startBlock: contractStartBlock}) =>
995
975
  let dcWithStartBlock: indexingAddress = {
996
976
  address: dc.address,
997
977
  contractName: dc.contractName,
@@ -1038,9 +1018,7 @@ let registerDynamicContracts = (
1038
1018
  // FIXME: This unsafely relies on the asc order of the items
1039
1019
  // which is 99% true, but there were cases when the source ordering was wrong
1040
1020
  false
1041
- | None =>
1042
- hasDCWithFilterByAddresses := hasDCWithFilterByAddresses.contents || filterByAddresses
1043
- true
1021
+ | None => true
1044
1022
  }
1045
1023
  if shouldUpdate {
1046
1024
  earliestRegisteringEventBlockNumber :=
@@ -1218,7 +1196,6 @@ let registerDynamicContracts = (
1218
1196
 
1219
1197
  let optimizedPartitions = createPartitionsFromIndexingAddresses(
1220
1198
  ~registeringContractsByContract,
1221
- ~contractConfigs=fetchState.contractConfigs,
1222
1199
  ~dynamicContracts=dynamicContractsRef.contents,
1223
1200
  ~normalSelection=fetchState.normalSelection,
1224
1201
  ~maxAddrInPartition=fetchState.optimizedPartitions.maxAddrInPartition,
@@ -1245,6 +1222,20 @@ let handleQueryResult = (
1245
1222
  ~latestFetchedBlock: blockNumberAndTimestamp,
1246
1223
  ~newItems,
1247
1224
  ): t => {
1225
+ // Drop events an address-param filter rejects. A merged partition may
1226
+ // over-fetch a wildcard event whose indexed address param references an
1227
+ // address registered after the log's block; `clientAddressFilter` is the
1228
+ // param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
1229
+ let newItems = newItems->Array.filter(item =>
1230
+ switch item {
1231
+ | Internal.Event({eventConfig, event, blockNumber}) =>
1232
+ switch eventConfig.clientAddressFilter {
1233
+ | Some(filter) => filter(event, blockNumber, fetchState.indexingAddresses)
1234
+ | None => true
1235
+ }
1236
+ | _ => true
1237
+ }
1238
+ )
1248
1239
  fetchState->updateInternal(
1249
1240
  ~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
1250
1241
  ~query,
@@ -1572,15 +1563,14 @@ let make = (
1572
1563
  let normalEventConfigs = []
1573
1564
  let contractNamesWithNormalEvents = Utils.Set.make()
1574
1565
  let indexingAddresses = Dict.make()
1575
- let contractConfigs = Dict.make()
1566
+ let contractConfigs: dict<contractConfig> = Dict.make()
1576
1567
 
1577
1568
  eventConfigs->Array.forEach(ec => {
1578
1569
  switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(ec.contractName) {
1579
- | Some({filterByAddresses, startBlock}) =>
1570
+ | Some({startBlock}) =>
1580
1571
  contractConfigs->Dict.set(
1581
1572
  ec.contractName,
1582
1573
  {
1583
- filterByAddresses: filterByAddresses || ec.filterByAddresses,
1584
1574
  startBlock: switch (startBlock, ec.startBlock) {
1585
1575
  | (Some(a), Some(b)) => Some(Pervasives.min(a, b))
1586
1576
  | (Some(_) as s, None) | (None, Some(_) as s) => s
@@ -1592,7 +1582,6 @@ let make = (
1592
1582
  contractConfigs->Dict.set(
1593
1583
  ec.contractName,
1594
1584
  {
1595
- filterByAddresses: ec.filterByAddresses,
1596
1585
  startBlock: ec.startBlock,
1597
1586
  },
1598
1587
  )
@@ -1672,7 +1661,6 @@ let make = (
1672
1661
 
1673
1662
  let optimizedPartitions = createPartitionsFromIndexingAddresses(
1674
1663
  ~registeringContractsByContract,
1675
- ~contractConfigs,
1676
1664
  ~dynamicContracts,
1677
1665
  ~normalSelection,
1678
1666
  ~maxAddrInPartition,
@@ -1875,7 +1863,6 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
1875
1863
  // Step 3: Recreate partitions from deleted partition addresses
1876
1864
  let optimizedPartitions = createPartitionsFromIndexingAddresses(
1877
1865
  ~registeringContractsByContract,
1878
- ~contractConfigs=fetchState.contractConfigs,
1879
1866
  ~dynamicContracts=fetchState.optimizedPartitions.dynamicContracts,
1880
1867
  ~normalSelection=fetchState.normalSelection,
1881
1868
  ~maxAddrInPartition=fetchState.optimizedPartitions.maxAddrInPartition,
@@ -571,7 +571,7 @@ function addressesByContractNameGetAll(addressesByContractName) {
571
571
  return all;
572
572
  }
573
573
 
574
- function createPartitionsFromIndexingAddresses(registeringContractsByContract, contractConfigs, dynamicContracts, normalSelection, maxAddrInPartition, nextPartitionIndex, existingPartitions, progressBlockNumber) {
574
+ function createPartitionsFromIndexingAddresses(registeringContractsByContract, dynamicContracts, normalSelection, maxAddrInPartition, nextPartitionIndex, existingPartitions, progressBlockNumber) {
575
575
  let nextPartitionIndexRef = nextPartitionIndex;
576
576
  let dynamicPartitions = [];
577
577
  let nonDynamicPartitions = [];
@@ -580,7 +580,6 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, c
580
580
  let contractName = contractNames[cIdx];
581
581
  let registeringContracts = registeringContractsByContract[contractName];
582
582
  let addresses = Object.keys(registeringContracts);
583
- let contractConfig = contractConfigs[contractName];
584
583
  let isDynamic = dynamicContracts.has(contractName);
585
584
  let partitions = isDynamic ? dynamicPartitions : nonDynamicPartitions;
586
585
  let byStartBlock = {};
@@ -596,9 +595,7 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, c
596
595
  for (let idx = 0, idx_finish = ascKeys.length; idx < idx_finish; ++idx) {
597
596
  let maybeNextStartBlockKey = ascKeys[idx + 1 | 0];
598
597
  let shouldAllocateNewPartition;
599
- if (contractConfig.filterByAddresses || maybeNextStartBlockKey === undefined) {
600
- shouldAllocateNewPartition = true;
601
- } else {
598
+ if (maybeNextStartBlockKey !== undefined) {
602
599
  let nextStartBlock = Stdlib_Int.fromString(maybeNextStartBlockKey, undefined);
603
600
  let shouldJoinCurrentStartBlock = (nextStartBlock - startBlockRef | 0) < 20000;
604
601
  if (shouldJoinCurrentStartBlock) {
@@ -607,6 +604,8 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, c
607
604
  } else {
608
605
  shouldAllocateNewPartition = true;
609
606
  }
607
+ } else {
608
+ shouldAllocateNewPartition = true;
610
609
  }
611
610
  if (shouldAllocateNewPartition) {
612
611
  let latestFetchedBlock_blockNumber = Primitive_int.max(startBlockRef - 1 | 0, progressBlockNumber);
@@ -667,10 +666,8 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, c
667
666
  mergedAddresses[cn] = currentAddrs;
668
667
  }
669
668
  }
670
- let nextContractName = Utils.$$Array.firstUnsafe(Object.keys(nextP.addressesByContractName));
671
- let hasFilterByAddresses = contractConfigs[nextContractName].filterByAddresses;
672
669
  let isTooFar = (currentPBlock + 20000 | 0) < nextPBlock;
673
- if (isTooFar || hasFilterByAddresses) {
670
+ if (isTooFar) {
674
671
  mergedNonDynamic.push({
675
672
  id: currentP.id,
676
673
  latestFetchedBlock: currentP.latestFetchedBlock,
@@ -725,7 +722,6 @@ function registerDynamicContracts(fetchState, items) {
725
722
  let indexingAddresses = fetchState.indexingAddresses;
726
723
  let registeringContractsByContract = {};
727
724
  let earliestRegisteringEventBlockNumber = Infinity;
728
- let hasDCWithFilterByAddresses = false;
729
725
  let noEventsAddresses = {};
730
726
  let registeringAddresses = {};
731
727
  for (let itemIdx = 0, itemIdx_finish = items.length; itemIdx < itemIdx_finish; ++itemIdx) {
@@ -764,18 +760,9 @@ function registerDynamicContracts(fetchState, items) {
764
760
  shouldRemove = true;
765
761
  } else {
766
762
  let registeringContract = registeringAddresses[dc.address];
767
- let shouldUpdate;
768
- if (registeringContract !== undefined) {
769
- if (registeringContract.contractName !== dc.contractName) {
770
- warnDifferentContractType(fetchState, registeringContract, dcWithStartBlock);
771
- shouldUpdate = false;
772
- } else {
773
- shouldUpdate = false;
774
- }
775
- } else {
776
- hasDCWithFilterByAddresses = hasDCWithFilterByAddresses || match.filterByAddresses;
777
- shouldUpdate = true;
778
- }
763
+ let shouldUpdate = registeringContract !== undefined ? (
764
+ registeringContract.contractName !== dc.contractName ? (warnDifferentContractType(fetchState, registeringContract, dcWithStartBlock), false) : false
765
+ ) : true;
779
766
  if (shouldUpdate) {
780
767
  earliestRegisteringEventBlockNumber = Primitive_int.min(earliestRegisteringEventBlockNumber, dcWithStartBlock_effectiveStartBlock);
781
768
  Utils.Dict.getOrInsertEmptyDict(registeringContractsByContract, dc.contractName)[dc.address] = dcWithStartBlock;
@@ -898,7 +885,7 @@ function registerDynamicContracts(fetchState, items) {
898
885
  Object.assign(newIndexingAddresses, registeringContracts);
899
886
  }
900
887
  Object.assign(newIndexingAddresses, noEventsAddresses);
901
- let optimizedPartitions = createPartitionsFromIndexingAddresses(registeringContractsByContract, fetchState.contractConfigs, dynamicContractsRef, fetchState.normalSelection, fetchState.optimizedPartitions.maxAddrInPartition, fetchState.optimizedPartitions.nextPartitionIndex + newPartitions.length | 0, mutExistingPartitions.concat(newPartitions), 0);
888
+ let optimizedPartitions = createPartitionsFromIndexingAddresses(registeringContractsByContract, dynamicContractsRef, fetchState.normalSelection, fetchState.optimizedPartitions.maxAddrInPartition, fetchState.optimizedPartitions.nextPartitionIndex + newPartitions.length | 0, mutExistingPartitions.concat(newPartitions), 0);
902
889
  return updateInternal(fetchState, optimizedPartitions, newIndexingAddresses, undefined, undefined, undefined);
903
890
  }
904
891
  if (!hasNoEventsUpdates) {
@@ -910,7 +897,18 @@ function registerDynamicContracts(fetchState, items) {
910
897
  }
911
898
 
912
899
  function handleQueryResult(fetchState, query, latestFetchedBlock, newItems) {
913
- return updateInternal(fetchState, handleQueryResponse(fetchState.optimizedPartitions, query, fetchState.knownHeight, latestFetchedBlock), undefined, newItems.length !== 0 ? fetchState.buffer.concat(newItems) : undefined, undefined, undefined);
900
+ let newItems$1 = newItems.filter(item => {
901
+ if (item.kind !== 0) {
902
+ return true;
903
+ }
904
+ let filter = item.eventConfig.clientAddressFilter;
905
+ if (filter !== undefined) {
906
+ return filter(item.event, item.blockNumber, fetchState.indexingAddresses);
907
+ } else {
908
+ return true;
909
+ }
910
+ });
911
+ return updateInternal(fetchState, handleQueryResponse(fetchState.optimizedPartitions, query, fetchState.knownHeight, latestFetchedBlock), undefined, newItems$1.length !== 0 ? fetchState.buffer.concat(newItems$1) : undefined, undefined, undefined);
914
912
  }
915
913
 
916
914
  function startFetchingQueries(param, queries) {
@@ -1236,7 +1234,6 @@ function make$1(startBlock, endBlock, eventConfigs, addresses, maxAddrInPartitio
1236
1234
  let startBlock = match.startBlock;
1237
1235
  let match$1 = ec.startBlock;
1238
1236
  contractConfigs[ec.contractName] = {
1239
- filterByAddresses: match.filterByAddresses || ec.filterByAddresses,
1240
1237
  startBlock: startBlock !== undefined ? (
1241
1238
  match$1 !== undefined ? Primitive_int.min(startBlock, match$1) : startBlock
1242
1239
  ) : (
@@ -1245,7 +1242,6 @@ function make$1(startBlock, endBlock, eventConfigs, addresses, maxAddrInPartitio
1245
1242
  };
1246
1243
  } else {
1247
1244
  contractConfigs[ec.contractName] = {
1248
- filterByAddresses: ec.filterByAddresses,
1249
1245
  startBlock: ec.startBlock
1250
1246
  };
1251
1247
  }
@@ -1305,7 +1301,7 @@ function make$1(startBlock, endBlock, eventConfigs, addresses, maxAddrInPartitio
1305
1301
  return;
1306
1302
  }
1307
1303
  });
1308
- let optimizedPartitions = createPartitionsFromIndexingAddresses(registeringContractsByContract, contractConfigs, dynamicContracts, normalSelection, maxAddrInPartition, partitions.length, partitions, progressBlockNumber);
1304
+ let optimizedPartitions = createPartitionsFromIndexingAddresses(registeringContractsByContract, dynamicContracts, normalSelection, maxAddrInPartition, partitions.length, partitions, progressBlockNumber);
1309
1305
  if (optimizedPartitions.idsInAscOrder.length === 0 && Utils.$$Array.isEmpty(onBlockConfigs)) {
1310
1306
  Stdlib_JsError.throwWithMessage(`Invalid configuration: Nothing to fetch on chain ` + chainId.toString() + `. ` + (`addresses=` + addresses.length.toString() + `, `) + (`eventConfigs=` + eventConfigs.length.toString() + `, `) + (`normalEventConfigs=` + normalEventConfigs.length.toString() + `. `) + `Make sure that you provided at least one contract address to index, or have events with Wildcard mode enabled, or have onBlock handlers.`);
1311
1307
  }
@@ -1453,7 +1449,7 @@ function rollback(fetchState, targetBlockNumber) {
1453
1449
  });
1454
1450
  }
1455
1451
  }
1456
- let optimizedPartitions = createPartitionsFromIndexingAddresses(registeringContractsByContract, fetchState.contractConfigs, fetchState.optimizedPartitions.dynamicContracts, fetchState.normalSelection, fetchState.optimizedPartitions.maxAddrInPartition, nextKeptIdRef, keptPartitions, targetBlockNumber);
1452
+ let optimizedPartitions = createPartitionsFromIndexingAddresses(registeringContractsByContract, fetchState.optimizedPartitions.dynamicContracts, fetchState.normalSelection, fetchState.optimizedPartitions.maxAddrInPartition, nextKeptIdRef, keptPartitions, targetBlockNumber);
1457
1453
  return updateInternal({
1458
1454
  optimizedPartitions: fetchState.optimizedPartitions,
1459
1455
  startBlock: fetchState.startBlock,
@@ -115,7 +115,11 @@ let applyRegistrations = (~config: Config.t): Config.t => {
115
115
  ~eventName=ev.name,
116
116
  )
117
117
  let indexedParams = evmEv.paramsMetadata->Array.filter(p => p.indexed)
118
- let {getEventFiltersOrThrow, filterByAddresses} = LogSelection.parseEventFiltersOrThrow(
118
+ let {
119
+ getEventFiltersOrThrow,
120
+ filterByAddresses,
121
+ addressFilterParamGroups,
122
+ } = LogSelection.parseEventFiltersOrThrow(
119
123
  ~eventFilters,
120
124
  ~sighash=evmEv.sighash,
121
125
  ~params=indexedParams->Array.map(p => p.name),
@@ -140,6 +144,7 @@ let applyRegistrations = (~config: Config.t): Config.t => {
140
144
  contractRegister,
141
145
  getEventFiltersOrThrow,
142
146
  filterByAddresses,
147
+ clientAddressFilter: ?EventConfigBuilder.buildAddressFilter(addressFilterParamGroups),
143
148
  dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses),
144
149
  } :> Internal.eventConfig)
145
150
  | Svm =>
@@ -75,48 +75,25 @@ function applyRegistrations(config) {
75
75
  let indexedParams = ev.paramsMetadata.filter(p => p.indexed);
76
76
  let match$1 = LogSelection.parseEventFiltersOrThrow(eventFilters, ev.sighash, indexedParams.map(p => p.name), ev.contractName, chain.id, config.ecosystem.onEventBlockFilterSchema, Stdlib_Option.map(indexedParams[0], EventConfigBuilder.buildTopicGetter), Stdlib_Option.map(indexedParams[1], EventConfigBuilder.buildTopicGetter), Stdlib_Option.map(indexedParams[2], EventConfigBuilder.buildTopicGetter));
77
77
  let filterByAddresses = match$1.filterByAddresses;
78
- return {
79
- id: ev.id,
80
- name: ev.name,
81
- contractName: ev.contractName,
82
- isWildcard: isWildcard,
83
- filterByAddresses: filterByAddresses,
84
- dependsOnAddresses: Internal.dependsOnAddresses(isWildcard, filterByAddresses),
85
- handler: handler,
86
- contractRegister: contractRegister,
87
- paramsRawEventSchema: ev.paramsRawEventSchema,
88
- simulateParamsSchema: ev.simulateParamsSchema,
89
- startBlock: ev.startBlock,
90
- getEventFiltersOrThrow: match$1.getEventFiltersOrThrow,
91
- selectedBlockFields: ev.selectedBlockFields,
92
- selectedTransactionFields: ev.selectedTransactionFields,
93
- sighash: ev.sighash,
94
- topicCount: ev.topicCount,
95
- paramsMetadata: ev.paramsMetadata
96
- };
97
- case "fuel" :
98
- return {
99
- id: ev.id,
100
- name: ev.name,
101
- contractName: ev.contractName,
102
- isWildcard: isWildcard,
103
- filterByAddresses: ev.filterByAddresses,
104
- dependsOnAddresses: Internal.dependsOnAddresses(isWildcard, false),
105
- handler: handler,
106
- contractRegister: contractRegister,
107
- paramsRawEventSchema: ev.paramsRawEventSchema,
108
- simulateParamsSchema: ev.simulateParamsSchema,
109
- startBlock: ev.startBlock,
110
- kind: ev.kind
111
- };
112
- case "svm" :
113
78
  let newrecord = {...ev};
79
+ newrecord.getEventFiltersOrThrow = match$1.getEventFiltersOrThrow;
114
80
  newrecord.contractRegister = contractRegister;
115
81
  newrecord.handler = handler;
116
- newrecord.dependsOnAddresses = Internal.dependsOnAddresses(isWildcard, false);
82
+ newrecord.clientAddressFilter = EventConfigBuilder.buildAddressFilter(match$1.addressFilterParamGroups);
83
+ newrecord.dependsOnAddresses = Internal.dependsOnAddresses(isWildcard, filterByAddresses);
84
+ newrecord.filterByAddresses = filterByAddresses;
117
85
  newrecord.isWildcard = isWildcard;
118
86
  return newrecord;
87
+ case "fuel" :
88
+ case "svm" :
89
+ break;
119
90
  }
91
+ let newrecord$1 = {...ev};
92
+ newrecord$1.contractRegister = contractRegister;
93
+ newrecord$1.handler = handler;
94
+ newrecord$1.dependsOnAddresses = Internal.dependsOnAddresses(isWildcard, false);
95
+ newrecord$1.isWildcard = isWildcard;
96
+ return newrecord$1;
120
97
  });
121
98
  return {
122
99
  name: contract.name,
package/src/Internal.res CHANGED
@@ -344,6 +344,16 @@ type rec paramMeta = {
344
344
  components?: array<paramMeta>,
345
345
  }
346
346
 
347
+ // Fetch-state registry value for an indexed contract address.
348
+ // `effectiveStartBlock` is derived from the registration block and the
349
+ // contract's configured start block (see `FetchState.deriveEffectiveStartBlock`).
350
+ type indexingContract = {
351
+ address: Address.t,
352
+ contractName: string,
353
+ registrationBlock: int,
354
+ effectiveStartBlock: int,
355
+ }
356
+
347
357
  // This is private so it's not manually constructed internally
348
358
  // The idea is that it can only be coerced from fuel/evmEventConfig
349
359
  // and it can include their fields. We prevent manual creation,
@@ -358,6 +368,12 @@ type eventConfig = private {
358
368
  // Usually always false for wildcard events
359
369
  // But might be true for wildcard event with dynamic event filter by addresses
360
370
  dependsOnAddresses: bool,
371
+ // Precompiled predicate (EVM only) for events that filter an indexed address
372
+ // param by registered addresses. Given the decoded event and the log's block
373
+ // number, drops an event whose param-address isn't registered at/before that
374
+ // block — the param-level analogue of EventRouter's srcAddress
375
+ // `effectiveStartBlock` check. Absent otherwise.
376
+ clientAddressFilter?: (event, int, dict<indexingContract>) => bool,
361
377
  handler: option<handler>,
362
378
  contractRegister: option<contractRegister>,
363
379
  paramsRawEventSchema: S.schema<eventParams>,
@@ -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 */
@@ -44,35 +44,42 @@ let toIndexingAddress = (dc: InternalTable.EnvioAddresses.t): Internal.indexingA
44
44
  }
45
45
 
46
46
  let handleLoad = (state: testIndexerState, ~tableName: string, ~filter: EntityFilter.t): JSON.t => {
47
- let entityDict = state.entities->Dict.get(tableName)->Option.getOr(Dict.make())
48
- let entityConfig = state.entityConfigs->Dict.getUnsafe(tableName)
49
- let results = []
50
-
51
- // Field values arrive as JSON from the worker boundary, so parse them
52
- // with the field's schema before comparing. This properly handles
53
- // bigint and BigDecimal comparisons
54
- let parseLeaf = (~fieldName, ~fieldValue: unknown, ~isArray): unknown => {
55
- let queryField = switch entityConfig.table->Table.queryFields->Dict.get(fieldName) {
56
- | Some(queryField) => queryField
57
- | None => JsError.throwWithMessage(`Field ${fieldName} not found in entity ${tableName}`)
58
- }
59
- fieldValue->S.convertOrThrow(isArray ? queryField.arrayFieldSchema : queryField.fieldSchema)
60
- }
61
- let filter = filter->EntityFilter.mapValues(~mapValue=parseLeaf)
62
-
63
- entityDict
64
- ->Dict.valuesToArray
65
- ->Array.forEach(entity => {
66
- // Cast entity to dict of field values (same approach as InMemoryTable)
67
- let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
68
- if filter->EntityFilter.matches(~entity=entityAsDict) {
69
- // Serialize entity back to JSON for worker thread
70
- let jsonEntity = entity->S.reverseConvertToJsonOrThrow(entityConfig.schema)
71
- results->Array.push(jsonEntity)->ignore
47
+ // Loads for non-entity tables (e.g. effect caches `envio_effect_<name>`) reach
48
+ // here too. TestIndexer never persists those, so there's nothing to return —
49
+ // an empty result makes the effect recompute instead of crashing on a missing
50
+ // entityConfig.
51
+ switch state.entityConfigs->Dict.get(tableName) {
52
+ | None => []->JSON.Encode.array
53
+ | Some(entityConfig) =>
54
+ let entityDict = state.entities->Dict.get(tableName)->Option.getOr(Dict.make())
55
+ let results = []
56
+
57
+ // Field values arrive as JSON from the worker boundary, so parse them
58
+ // with the field's schema before comparing. This properly handles
59
+ // bigint and BigDecimal comparisons
60
+ let parseLeaf = (~fieldName, ~fieldValue: unknown, ~isArray): unknown => {
61
+ let queryField = switch entityConfig.table->Table.queryFields->Dict.get(fieldName) {
62
+ | Some(queryField) => queryField
63
+ | None => JsError.throwWithMessage(`Field ${fieldName} not found in entity ${tableName}`)
64
+ }
65
+ fieldValue->S.convertOrThrow(isArray ? queryField.arrayFieldSchema : queryField.fieldSchema)
72
66
  }
73
- })
67
+ let filter = filter->EntityFilter.mapValues(~mapValue=parseLeaf)
68
+
69
+ entityDict
70
+ ->Dict.valuesToArray
71
+ ->Array.forEach(entity => {
72
+ // Cast entity to dict of field values (same approach as InMemoryTable)
73
+ let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
74
+ if filter->EntityFilter.matches(~entity=entityAsDict) {
75
+ // Serialize entity back to JSON for worker thread
76
+ let jsonEntity = entity->S.reverseConvertToJsonOrThrow(entityConfig.schema)
77
+ results->Array.push(jsonEntity)->ignore
78
+ }
79
+ })
74
80
 
75
- results->JSON.Encode.array
81
+ results->JSON.Encode.array
82
+ }
76
83
  }
77
84
 
78
85
  let handleWriteBatch = (
@@ -33,8 +33,11 @@ function toIndexingAddress(dc) {
33
33
  }
34
34
 
35
35
  function handleLoad(state, tableName, filter) {
36
- let entityDict = Stdlib_Option.getOr(state.entities[tableName], {});
37
36
  let entityConfig = state.entityConfigs[tableName];
37
+ if (entityConfig === undefined) {
38
+ return [];
39
+ }
40
+ let entityDict = Stdlib_Option.getOr(state.entities[tableName], {});
38
41
  let results = [];
39
42
  let parseLeaf = (fieldName, fieldValue, isArray) => {
40
43
  let queryField = Table.queryFields(entityConfig.table)[fieldName];
@@ -167,9 +167,9 @@ let make = (
167
167
  let apiToken = switch apiToken {
168
168
  | Some(token) => token
169
169
  | None =>
170
- JsError.throwWithMessage(`An API token is required for using HyperSync as a data-source.
170
+ JsError.throwWithMessage(`An Envio API token is required for using HyperSync as a data-source.
171
171
  Set the ENVIO_API_TOKEN environment variable in your .env file.
172
- Learn more or get a free API token at: https://envio.dev/app/api-tokens`)
172
+ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
173
173
  }
174
174
 
175
175
  let client = switch HyperSyncClient.make(
@@ -113,9 +113,9 @@ function make(param) {
113
113
  let chain = param.chain;
114
114
  let name = "HyperSync";
115
115
  let getSelectionConfig = memoGetSelectionConfig(chain);
116
- let apiToken$1 = apiToken !== undefined ? apiToken : Stdlib_JsError.throwWithMessage(`An API token is required for using HyperSync as a data-source.
116
+ let apiToken$1 = apiToken !== undefined ? apiToken : Stdlib_JsError.throwWithMessage(`An Envio API token is required for using HyperSync as a data-source.
117
117
  Set the ENVIO_API_TOKEN environment variable in your .env file.
118
- Learn more or get a free API token at: https://envio.dev/app/api-tokens`);
118
+ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
119
119
  let client;
120
120
  try {
121
121
  client = HyperSyncClient.make(endpointUrl, apiToken$1, param.clientTimeoutMillis, param.allEventParams, !param.lowercaseAddresses, param.serializationFormat, param.enableQueryCaching, undefined, undefined, undefined, param.logLevel);