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 +6 -6
- package/src/EventConfigBuilder.res +35 -0
- package/src/EventConfigBuilder.res.mjs +21 -0
- package/src/FetchState.res +40 -53
- package/src/FetchState.res.mjs +23 -27
- package/src/HandlerLoader.res +6 -1
- package/src/HandlerLoader.res.mjs +13 -36
- package/src/Internal.res +16 -0
- package/src/LogSelection.res +86 -17
- package/src/LogSelection.res.mjs +67 -14
- package/src/TestIndexer.res +34 -27
- package/src/TestIndexer.res.mjs +4 -1
- package/src/sources/HyperSyncSource.res +2 -2
- package/src/sources/HyperSyncSource.res.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envio",
|
|
3
|
-
"version": "3.2.
|
|
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.
|
|
74
|
-
"envio-linux-x64-musl": "3.2.
|
|
75
|
-
"envio-linux-arm64": "3.2.
|
|
76
|
-
"envio-darwin-x64": "3.2.
|
|
77
|
-
"envio-darwin-arm64": "3.2.
|
|
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,
|
package/src/FetchState.res
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
type contractConfig = {
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
918
|
-
// Too far
|
|
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
|
|
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({
|
|
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({
|
|
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,
|
package/src/FetchState.res.mjs
CHANGED
|
@@ -571,7 +571,7 @@ function addressesByContractNameGetAll(addressesByContractName) {
|
|
|
571
571
|
return all;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
function createPartitionsFromIndexingAddresses(registeringContractsByContract,
|
|
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 (
|
|
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
|
|
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
|
-
|
|
769
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
package/src/HandlerLoader.res
CHANGED
|
@@ -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 {
|
|
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.
|
|
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>,
|
package/src/LogSelection.res
CHANGED
|
@@ -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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
}
|
package/src/LogSelection.res.mjs
CHANGED
|
@@ -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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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/TestIndexer.res
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
81
|
+
results->JSON.Encode.array
|
|
82
|
+
}
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
let handleWriteBatch = (
|
package/src/TestIndexer.res.mjs
CHANGED
|
@@ -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);
|