envio 3.3.0-alpha.6 → 3.3.0-alpha.7

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.3.0-alpha.6",
3
+ "version": "3.3.0-alpha.7",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -69,10 +69,10 @@
69
69
  "tsx": "4.21.0"
70
70
  },
71
71
  "optionalDependencies": {
72
- "envio-linux-x64": "3.3.0-alpha.6",
73
- "envio-linux-x64-musl": "3.3.0-alpha.6",
74
- "envio-linux-arm64": "3.3.0-alpha.6",
75
- "envio-darwin-x64": "3.3.0-alpha.6",
76
- "envio-darwin-arm64": "3.3.0-alpha.6"
72
+ "envio-linux-x64": "3.3.0-alpha.7",
73
+ "envio-linux-x64-musl": "3.3.0-alpha.7",
74
+ "envio-linux-arm64": "3.3.0-alpha.7",
75
+ "envio-darwin-x64": "3.3.0-alpha.7",
76
+ "envio-darwin-arm64": "3.3.0-alpha.7"
77
77
  }
78
78
  }
package/src/Address.res CHANGED
@@ -11,8 +11,11 @@ module Evm = {
11
11
  external fromStringOrThrow: string => t = "getAddress"
12
12
 
13
13
  // NOTE: the function is named to be overshadowed by the one below, so that we don't have to import viem in the handler code
14
+ // strict:false checks only the 20-byte hex shape, not EIP-55 checksum casing —
15
+ // since we're about to lowercase it anyway, an all-uppercase or wrongly-cased
16
+ // input address is just as valid as one that's already lowercase.
14
17
  @module("viem")
15
- external fromStringLowercaseOrThrow: string => bool = "isAddress"
18
+ external fromStringLowercaseOrThrow: (string, {"strict": bool}) => bool = "isAddress"
16
19
 
17
20
  // Reassign since the function might be used in the handler code
18
21
  // and we don't want to have a "viem" import there. It's needed to keep "viem" a dependency
@@ -20,7 +23,7 @@ module Evm = {
20
23
  // Also, we want a custom error message, which is searchable in our codebase.
21
24
  // Validate that the string is a proper address but return a lowercased value
22
25
  let fromStringLowercaseOrThrow = string => {
23
- if fromStringLowercaseOrThrow(string) {
26
+ if fromStringLowercaseOrThrow(string, {"strict": false}) {
24
27
  unsafeFromString(string->String.toLowerCase)
25
28
  } else {
26
29
  JsError.throwWithMessage(
@@ -7,7 +7,9 @@ import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
7
7
  let schema = S$RescriptSchema.setName(S$RescriptSchema.string, "Address");
8
8
 
9
9
  function fromStringLowercaseOrThrow(string) {
10
- if (Viem.isAddress(string)) {
10
+ if (Viem.isAddress(string, {
11
+ strict: false
12
+ })) {
11
13
  return string.toLowerCase();
12
14
  } else {
13
15
  return Stdlib_JsError.throwWithMessage(`Address "` + string + `" is invalid. Expected a 20-byte hex string starting with 0x.`);
@@ -140,6 +140,20 @@ let rec onQueryResponse = async (
140
140
  ~blockRangeSize=latestFetchedBlockNumber - fromBlockQueried + 1,
141
141
  )
142
142
 
143
+ let numContractRegisterEvents = parsedQueueItems->Array.reduce(0, (count, item) => {
144
+ let eventItem = item->Internal.castUnsafeEventItem
145
+ eventItem.eventConfig.contractRegister !== None ? count + 1 : count
146
+ })
147
+ Logging.trace({
148
+ "msg": "Finished querying",
149
+ "chainId": chain->ChainMap.Chain.toChainId,
150
+ "partitionId": query.partitionId,
151
+ "fromBlock": fromBlockQueried,
152
+ "toBlock": latestFetchedBlockNumber,
153
+ "numEvents": parsedQueueItems->Array.length,
154
+ "numContractRegisterEvents": numContractRegisterEvents,
155
+ })
156
+
143
157
  let reorgResult = chainState->ChainState.registerReorgGuard(~blockHashes, ~knownHeight)
144
158
 
145
159
  let rollbackWithReorgDetectedBlockNumber = switch reorgResult {
@@ -334,15 +348,6 @@ let fetchChain = async (
334
348
  // there's nothing to fetch. During backfill any such chain is idle.
335
349
  let reducedPolling = !isRealtime
336
350
 
337
- // Accumulated across all queries dispatched in this tick so their per-query
338
- // results collapse into a single completion log instead of one per query.
339
- let dispatchedCount = switch action {
340
- | FetchState.Ready(queries) => queries->Array.length
341
- | WaitingForNewBlock | NothingToQuery => 0
342
- }
343
- let fetchedByPartition = Dict.make()
344
- let fetchedCount = ref(0)
345
-
346
351
  // Owns its error boundary: launch doesn't catch, so any failure here (the
347
352
  // query, response handling, or dispatch itself) must stop the indexer.
348
353
  try {
@@ -368,22 +373,6 @@ let fetchChain = async (
368
373
  ~knownHeight=chainState->ChainState.knownHeight,
369
374
  ~isRealtime,
370
375
  )
371
- fetchedCount := fetchedCount.contents + 1
372
- fetchedByPartition->Dict.set(
373
- query.partitionId,
374
- {
375
- "fromBlock": response.fromBlockQueried,
376
- "toBlock": response.latestFetchedBlockNumber,
377
- "numEvents": response.parsedQueueItems->Array.length,
378
- },
379
- )
380
- if dispatchedCount > 0 && fetchedCount.contents === dispatchedCount {
381
- Logging.trace({
382
- "msg": "Finished querying",
383
- "chainId": chain->ChainMap.Chain.toChainId,
384
- "partitions": fetchedByPartition,
385
- })
386
- }
387
376
  await onQueryResponse(
388
377
  state,
389
378
  {chain, response, query},
@@ -6,6 +6,7 @@ import * as Ecosystem from "./Ecosystem.res.mjs";
6
6
  import * as ChainState from "./ChainState.res.mjs";
7
7
  import * as Prometheus from "./Prometheus.res.mjs";
8
8
  import * as IndexerState from "./IndexerState.res.mjs";
9
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
10
  import * as ChainMetadata from "./ChainMetadata.res.mjs";
10
11
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
11
12
  import * as SourceManager from "./sources/SourceManager.res.mjs";
@@ -94,13 +95,30 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
94
95
  let stats = response.stats;
95
96
  let latestFetchedBlockTimestamp = response.latestFetchedBlockTimestamp;
96
97
  let latestFetchedBlockNumber = response.latestFetchedBlockNumber;
98
+ let fromBlockQueried = response.fromBlockQueried;
97
99
  let transactionStore = response.transactionStore;
98
100
  let parsedQueueItems = response.parsedQueueItems;
99
101
  let knownHeight = response.knownHeight;
100
102
  if (knownHeight > ChainState.knownHeight(chainState)) {
101
103
  Prometheus.SourceHeight.set(SourceManager.getActiveSource(ChainState.sourceManager(chainState)).name, ChainState.chainConfig(chainState).id, knownHeight);
102
104
  }
103
- Prometheus.FetchingBlockRange.increment(chain, stats["total time elapsed (s)"], Stdlib_Option.getOr(stats["parsing time (s)"], 0), parsedQueueItems.length, (latestFetchedBlockNumber - response.fromBlockQueried | 0) + 1 | 0);
105
+ Prometheus.FetchingBlockRange.increment(chain, stats["total time elapsed (s)"], Stdlib_Option.getOr(stats["parsing time (s)"], 0), parsedQueueItems.length, (latestFetchedBlockNumber - fromBlockQueried | 0) + 1 | 0);
106
+ let numContractRegisterEvents = Stdlib_Array.reduce(parsedQueueItems, 0, (count, item) => {
107
+ if (item.eventConfig.contractRegister !== undefined) {
108
+ return count + 1 | 0;
109
+ } else {
110
+ return count;
111
+ }
112
+ });
113
+ Logging.trace({
114
+ msg: "Finished querying",
115
+ chainId: chain,
116
+ partitionId: query.partitionId,
117
+ fromBlock: fromBlockQueried,
118
+ toBlock: latestFetchedBlockNumber,
119
+ numEvents: parsedQueueItems.length,
120
+ numContractRegisterEvents: numContractRegisterEvents
121
+ });
104
122
  let reorgResult = ChainState.registerReorgGuard(chainState, response.blockHashes, knownHeight);
105
123
  let rollbackWithReorgDetectedBlockNumber;
106
124
  if (typeof reorgResult !== "object") {
@@ -176,29 +194,10 @@ async function fetchChain(state, chain, action, stateId, scheduleFetch, schedule
176
194
  let isRealtime = IndexerState.isRealtime(state);
177
195
  let sourceManager = ChainState.sourceManager(chainState);
178
196
  let reducedPolling = !isRealtime;
179
- let dispatchedCount;
180
- dispatchedCount = typeof action !== "object" ? 0 : action._0.length;
181
- let fetchedByPartition = {};
182
- let fetchedCount = {
183
- contents: 0
184
- };
185
197
  try {
186
198
  return await ChainState.dispatch(chainState, async query => {
187
199
  try {
188
200
  let response = await SourceManager.executeQuery(sourceManager, query, ChainState.knownHeight(chainState), isRealtime);
189
- fetchedCount.contents = fetchedCount.contents + 1 | 0;
190
- fetchedByPartition[query.partitionId] = {
191
- fromBlock: response.fromBlockQueried,
192
- toBlock: response.latestFetchedBlockNumber,
193
- numEvents: response.parsedQueueItems.length
194
- };
195
- if (dispatchedCount > 0 && fetchedCount.contents === dispatchedCount) {
196
- Logging.trace({
197
- msg: "Finished querying",
198
- chainId: chain,
199
- partitions: fetchedByPartition
200
- });
201
- }
202
201
  return await onQueryResponse(state, {
203
202
  chain: chain,
204
203
  response: response,
@@ -457,10 +457,10 @@ let resetPendingQueries = (cs: t) => {
457
457
  cs.pendingBudget = 0.
458
458
  }
459
459
 
460
- // Propose the chain's candidate queries against the shared buffer budget,
461
- // accounting for this chain's own in-flight reservations.
462
- let getNextQuery = (cs: t, ~budget) =>
463
- cs.fetchState->FetchState.getNextQuery(~budget, ~chainPendingBudget=cs.pendingBudget)
460
+ // Propose the chain's candidate queries against their natural ceiling
461
+ // (head/endBlock/mergeBlock). CrossChainState admits them against the shared
462
+ // buffer budget afterwards.
463
+ let getNextQuery = (cs: t) => cs.fetchState->FetchState.getNextQuery
464
464
 
465
465
  // Run a fetch tick for this chain against its sources, feeding the owned fetch
466
466
  // frontier to the source manager.
@@ -282,8 +282,8 @@ function startFetchingQueries(cs, queries) {
282
282
  cs.pendingBudget = cs.pendingBudget + Stdlib_Array.reduce(queries, 0, (acc, query) => acc + query.estResponseSize);
283
283
  }
284
284
 
285
- function getNextQuery(cs, budget) {
286
- return FetchState.getNextQuery(cs.fetchState, budget, cs.pendingBudget);
285
+ function getNextQuery(cs) {
286
+ return FetchState.getNextQuery(cs.fetchState);
287
287
  }
288
288
 
289
289
  function dispatch(cs, executeQuery, waitForNewBlock, onNewBlock, action, stateId) {
@@ -62,7 +62,7 @@ let hasReadyItem: t => bool
62
62
  let isReadyToEnterReorgThreshold: t => bool
63
63
 
64
64
  // Fetch control.
65
- let getNextQuery: (t, ~budget: int) => FetchState.nextQuery
65
+ let getNextQuery: t => FetchState.nextQuery
66
66
  let dispatch: (
67
67
  t,
68
68
  ~executeQuery: FetchState.query => promise<unit>,
package/src/Config.res CHANGED
@@ -1018,6 +1018,50 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1018
1018
  }
1019
1019
  }
1020
1020
 
1021
+ // Canonicalize a user-provided address to the configured casing so it matches
1022
+ // addresses parsed from config.yaml during routing. HyperSync/RPC data arrives
1023
+ // already canonical; only user-land input (simulate srcAddress, contractRegister
1024
+ // add) can carry arbitrary casing and needs this before comparison.
1025
+ let normalizeUserAddress = (config: t, address: Address.t): Address.t =>
1026
+ switch config.ecosystem.name {
1027
+ | Ecosystem.Evm =>
1028
+ if config.lowercaseAddresses {
1029
+ address->Address.Evm.fromAddressLowercaseOrThrow
1030
+ } else {
1031
+ address->Address.Evm.fromAddressOrThrow
1032
+ }
1033
+ // TODO: Ideally we should do the same for other ecosystems
1034
+ | Ecosystem.Fuel | Ecosystem.Svm => address
1035
+ }
1036
+
1037
+ // Relaxed counterpart to normalizeUserAddress, used only for simulate srcAddress.
1038
+ // Tests commonly use short placeholder strings like "0xfoo" as a stand-in
1039
+ // identity (e.g. for a wildcard event whose srcAddress doesn't need to resolve
1040
+ // to any real contract), so only the "0x" prefix is enforced here. Under
1041
+ // address_format: checksum, a real address is checksummed even if the input
1042
+ // casing doesn't match (getAddress doesn't require the input to already be
1043
+ // checksummed) — a placeholder that isn't valid hex falls back unchanged.
1044
+ let normalizeSimulateAddress = (config: t, address: Address.t): Address.t =>
1045
+ switch config.ecosystem.name {
1046
+ | Ecosystem.Evm =>
1047
+ let str = address->Address.toString
1048
+ if !(str->String.startsWith("0x")) {
1049
+ JsError.throwWithMessage(
1050
+ `simulate: srcAddress "${str}" is invalid. Expected a string starting with "0x".`,
1051
+ )
1052
+ }
1053
+ if config.lowercaseAddresses {
1054
+ address->Address.Evm.fromAddressLowercaseOrThrow
1055
+ } else {
1056
+ try {
1057
+ address->Address.Evm.fromAddressOrThrow
1058
+ } catch {
1059
+ | _ => address
1060
+ }
1061
+ }
1062
+ | Ecosystem.Fuel | Ecosystem.Svm => address
1063
+ }
1064
+
1021
1065
  // Look up an event config by (contract, event) name. When `chainId` is given,
1022
1066
  // returns that chain's per-chain event config (matters for where-callback
1023
1067
  // probe detection, which runs with the chain's real id). Without `chainId`,
@@ -706,6 +706,42 @@ function fromPublic(publicConfigJson) {
706
706
  };
707
707
  }
708
708
 
709
+ function normalizeUserAddress(config, address) {
710
+ let match = config.ecosystem.name;
711
+ switch (match) {
712
+ case "evm" :
713
+ if (config.lowercaseAddresses) {
714
+ return Address.Evm.fromAddressLowercaseOrThrow(address);
715
+ } else {
716
+ return Address.Evm.fromAddressOrThrow(address);
717
+ }
718
+ case "fuel" :
719
+ case "svm" :
720
+ return address;
721
+ }
722
+ }
723
+
724
+ function normalizeSimulateAddress(config, address) {
725
+ let match = config.ecosystem.name;
726
+ switch (match) {
727
+ case "evm" :
728
+ if (!address.startsWith("0x")) {
729
+ Stdlib_JsError.throwWithMessage(`simulate: srcAddress "` + address + `" is invalid. Expected a string starting with "0x".`);
730
+ }
731
+ if (config.lowercaseAddresses) {
732
+ return Address.Evm.fromAddressLowercaseOrThrow(address);
733
+ }
734
+ try {
735
+ return Address.Evm.fromAddressOrThrow(address);
736
+ } catch (exn) {
737
+ return address;
738
+ }
739
+ case "fuel" :
740
+ case "svm" :
741
+ return address;
742
+ }
743
+ }
744
+
709
745
  function getEventConfig(config, contractName, eventName, chainId) {
710
746
  let chains;
711
747
  if (chainId !== undefined) {
@@ -1012,6 +1048,8 @@ export {
1012
1048
  publicConfigStorageSchema,
1013
1049
  publicConfigSchema,
1014
1050
  fromPublic,
1051
+ normalizeUserAddress,
1052
+ normalizeSimulateAddress,
1015
1053
  getEventConfig,
1016
1054
  shouldSaveHistory,
1017
1055
  shouldPruneHistory,
@@ -22,18 +22,8 @@ let makeAddFunction = (~params: contractRegisterParams, ~contractName: string):
22
22
  ~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
23
23
  )
24
24
  }
25
- let validatedAddress = if params.config.ecosystem.name === Evm {
26
- // The value is passed from the user-land,
27
- // so we need to validate and checksum/lowercase the address.
28
- if params.config.lowercaseAddresses {
29
- contractAddress->Address.Evm.fromAddressLowercaseOrThrow
30
- } else {
31
- contractAddress->Address.Evm.fromAddressOrThrow
32
- }
33
- } else {
34
- // TODO: Ideally we should do the same for other ecosystems
35
- contractAddress
36
- }
25
+ // The value is passed from user-land, so validate and checksum/lowercase it.
26
+ let validatedAddress = params.config->Config.normalizeUserAddress(contractAddress)
37
27
 
38
28
  params.onRegister(~item=params.item, ~contractAddress=validatedAddress, ~contractName)
39
29
  }
@@ -1,6 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Address from "./Address.res.mjs";
3
+ import * as Config from "./Config.res.mjs";
4
4
  import * as ChainMap from "./ChainMap.res.mjs";
5
5
  import * as Ecosystem from "./Ecosystem.res.mjs";
6
6
  import * as EntityFilter from "./db/EntityFilter.res.mjs";
@@ -12,9 +12,7 @@ function makeAddFunction(params, contractName) {
12
12
  if (params.isResolved) {
13
13
  ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access context.chain after the contract register is resolved. Make sure you didn't miss an await in the handler.`));
14
14
  }
15
- let validatedAddress = params.config.ecosystem.name === "evm" ? (
16
- params.config.lowercaseAddresses ? Address.Evm.fromAddressLowercaseOrThrow(contractAddress) : Address.Evm.fromAddressOrThrow(contractAddress)
17
- ) : contractAddress;
15
+ let validatedAddress = Config.normalizeUserAddress(params.config, contractAddress);
18
16
  params.onRegister(params.item, validatedAddress, contractName);
19
17
  };
20
18
  }
@@ -73,4 +71,4 @@ export {
73
71
  getContractRegisterContext,
74
72
  getContractRegisterArgs,
75
73
  }
76
- /* Address Not a pure module */
74
+ /* Config Not a pure module */
@@ -19,7 +19,7 @@ type t = {
19
19
  let calculateTargetBufferSize = () =>
20
20
  switch Env.targetBufferSize {
21
21
  | Some(size) => size
22
- | None => 100_000
22
+ | None => 50_000
23
23
  }
24
24
 
25
25
  let make = (
@@ -198,7 +198,8 @@ let compareByProgress = (a: FetchState.query, b: FetchState.query) =>
198
198
 
199
199
  // Dispatch a fetch tick across the whole indexer from one shared pool of
200
200
  // ~targetBufferSize ready events. Every chain proposes its candidate queries
201
- // (each carrying an estimated response size) against the full free budget; the
201
+ // (each carrying an estimated response size) up to its own natural ceiling
202
+ // (head/endBlock/mergeBlock), unconstrained by the shared budget; the
202
203
  // candidates are then pooled, ordered by chain progress (furthest-behind first),
203
204
  // and admitted until the budget is consumed. So the budget is split per query
204
205
  // across chains rather than per chain — a chain that can only use a little
@@ -223,7 +224,7 @@ let checkAndFetch = async (
223
224
  for i in 0 to chainIds->Array.length - 1 {
224
225
  let chainId = chainIds->Array.getUnsafe(i)
225
226
  let cs = crossChainState->getChainState(chainId)
226
- switch cs->ChainState.getNextQuery(~budget=remaining) {
227
+ switch cs->ChainState.getNextQuery {
227
228
  | (WaitingForNewBlock | NothingToQuery) as action =>
228
229
  actionByChain->Utils.Dict.setByInt(chainId, action)
229
230
  | Ready(queries) =>
@@ -261,6 +262,7 @@ let checkAndFetch = async (
261
262
  {
262
263
  "fromBlock": query.fromBlock,
263
264
  "targetBlock": query.toBlock,
265
+ "targetEvents": query.estResponseSize->Math.round->Float.toInt,
264
266
  },
265
267
  )
266
268
  )
@@ -17,13 +17,13 @@ function calculateTargetBufferSize() {
17
17
  if (Env.targetBufferSize !== undefined) {
18
18
  return Env.targetBufferSize;
19
19
  } else {
20
- return 100000;
20
+ return 50000;
21
21
  }
22
22
  }
23
23
 
24
24
  function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt) {
25
25
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
26
- Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
26
+ Env.targetBufferSize !== undefined ? Env.targetBufferSize : 50000
27
27
  );
28
28
  let crossChainState = {
29
29
  chainStates: chainStates,
@@ -147,7 +147,7 @@ async function checkAndFetch(crossChainState, dispatchChain) {
147
147
  for (let i = 0, i_finish = chainIds.length; i < i_finish; ++i) {
148
148
  let chainId = chainIds[i];
149
149
  let cs = crossChainState.chainStates[chainId];
150
- let action = ChainState.getNextQuery(cs, remaining);
150
+ let action = ChainState.getNextQuery(cs);
151
151
  if (typeof action !== "object") {
152
152
  actionByChain[chainId] = action;
153
153
  } else {
@@ -174,7 +174,8 @@ async function checkAndFetch(crossChainState, dispatchChain) {
174
174
  queries.forEach(query => {
175
175
  partitions[query.partitionId] = {
176
176
  fromBlock: query.fromBlock,
177
- targetBlock: query.toBlock
177
+ targetBlock: query.toBlock,
178
+ targetEvents: Math.round(query.estResponseSize) | 0
178
179
  };
179
180
  });
180
181
  Logging.trace({