envio 3.3.0-alpha.1 → 3.3.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +6 -6
  2. package/src/Batch.res +0 -6
  3. package/src/Batch.res.mjs +0 -11
  4. package/src/BatchProcessing.res +2 -12
  5. package/src/BatchProcessing.res.mjs +2 -7
  6. package/src/ChainFetching.res +17 -6
  7. package/src/ChainFetching.res.mjs +11 -11
  8. package/src/ChainMetadata.res +1 -11
  9. package/src/ChainMetadata.res.mjs +1 -10
  10. package/src/ChainState.res +150 -1
  11. package/src/ChainState.res.mjs +138 -9
  12. package/src/ChainState.resi +51 -1
  13. package/src/Config.res +3 -6
  14. package/src/Config.res.mjs +2 -3
  15. package/src/Core.res +7 -0
  16. package/src/CrossChainState.res +9 -26
  17. package/src/CrossChainState.res.mjs +7 -16
  18. package/src/Ecosystem.res +6 -1
  19. package/src/Envio.res +3 -2
  20. package/src/EventConfigBuilder.res +19 -5
  21. package/src/EventConfigBuilder.res.mjs +6 -4
  22. package/src/EventProcessing.res +26 -0
  23. package/src/EventProcessing.res.mjs +20 -0
  24. package/src/Internal.res +43 -4
  25. package/src/Internal.res.mjs +18 -0
  26. package/src/Main.res +3 -50
  27. package/src/Main.res.mjs +2 -23
  28. package/src/Rollback.res +1 -1
  29. package/src/Rollback.res.mjs +1 -1
  30. package/src/SimulateItems.res +3 -0
  31. package/src/SimulateItems.res.mjs +2 -1
  32. package/src/sources/Evm.res +52 -38
  33. package/src/sources/Evm.res.mjs +48 -36
  34. package/src/sources/Fuel.res +3 -1
  35. package/src/sources/Fuel.res.mjs +1 -0
  36. package/src/sources/HyperFuelSource.res +5 -0
  37. package/src/sources/HyperFuelSource.res.mjs +2 -0
  38. package/src/sources/HyperSync.res +4 -1
  39. package/src/sources/HyperSync.res.mjs +5 -3
  40. package/src/sources/HyperSync.resi +1 -0
  41. package/src/sources/HyperSyncClient.res +9 -78
  42. package/src/sources/HyperSyncClient.res.mjs +1 -22
  43. package/src/sources/HyperSyncSource.res +3 -4
  44. package/src/sources/HyperSyncSource.res.mjs +2 -1
  45. package/src/sources/RpcSource.res +7 -2
  46. package/src/sources/RpcSource.res.mjs +3 -1
  47. package/src/sources/SimulateSource.res +3 -1
  48. package/src/sources/SimulateSource.res.mjs +1 -0
  49. package/src/sources/Source.res +4 -0
  50. package/src/sources/Svm.res +25 -2
  51. package/src/sources/Svm.res.mjs +27 -2
  52. package/src/sources/SvmHyperSyncClient.res +3 -29
  53. package/src/sources/SvmHyperSyncSource.res +60 -84
  54. package/src/sources/SvmHyperSyncSource.res.mjs +59 -78
  55. package/src/sources/TransactionStore.res +111 -0
  56. package/src/sources/TransactionStore.res.mjs +77 -0
  57. package/src/tui/Tui.res +13 -16
  58. package/src/tui/Tui.res.mjs +12 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.3.0-alpha.1",
3
+ "version": "3.3.0-alpha.2",
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.1",
73
- "envio-linux-x64-musl": "3.3.0-alpha.1",
74
- "envio-linux-arm64": "3.3.0-alpha.1",
75
- "envio-darwin-x64": "3.3.0-alpha.1",
76
- "envio-darwin-arm64": "3.3.0-alpha.1"
72
+ "envio-linux-x64": "3.3.0-alpha.2",
73
+ "envio-linux-x64-musl": "3.3.0-alpha.2",
74
+ "envio-linux-arm64": "3.3.0-alpha.2",
75
+ "envio-darwin-x64": "3.3.0-alpha.2",
76
+ "envio-darwin-arm64": "3.3.0-alpha.2"
77
77
  }
78
78
  }
package/src/Batch.res CHANGED
@@ -34,12 +34,6 @@ type t = {
34
34
  checkpointEventsProcessed: array<int>,
35
35
  }
36
36
 
37
- let hasReadyItem = (fetchStates: array<FetchState.t>) => {
38
- fetchStates->Array.some(fetchState => {
39
- fetchState->FetchState.isActivelyIndexing && fetchState->FetchState.hasReadyItem
40
- })
41
- }
42
-
43
37
  let getProgressedChainsById = {
44
38
  let getChainAfterBatchIfProgressed = (
45
39
  ~chainBeforeBatch: chainBeforeBatch,
package/src/Batch.res.mjs CHANGED
@@ -4,16 +4,6 @@ import * as Utils from "./Utils.res.mjs";
4
4
  import * as FetchState from "./FetchState.res.mjs";
5
5
  import * as ReorgDetection from "./ReorgDetection.res.mjs";
6
6
 
7
- function hasReadyItem(fetchStates) {
8
- return fetchStates.some(fetchState => {
9
- if (FetchState.isActivelyIndexing(fetchState)) {
10
- return FetchState.hasReadyItem(fetchState);
11
- } else {
12
- return false;
13
- }
14
- });
15
- }
16
-
17
7
  function getChainAfterBatchIfProgressed(chainBeforeBatch, progressBlockNumberAfterBatch, fetchStateAfterBatch, batchSize) {
18
8
  if (chainBeforeBatch.progressBlockNumber < progressBlockNumberAfterBatch) {
19
9
  return {
@@ -208,7 +198,6 @@ function findLastEventItem(batch, chainId) {
208
198
  }
209
199
 
210
200
  export {
211
- hasReadyItem,
212
201
  getProgressedChainsById,
213
202
  addReorgCheckpoints,
214
203
  prepareBatch,
@@ -77,15 +77,7 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
77
77
  state
78
78
  ->IndexerState.chainStates
79
79
  ->Dict.valuesToArray
80
- ->Array.every(cs => {
81
- let fetchState = switch progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(
82
- (cs->ChainState.fetchState).chainId,
83
- ) {
84
- | Some(chainAfterBatch) => chainAfterBatch.fetchState
85
- | None => cs->ChainState.fetchState
86
- }
87
- fetchState->FetchState.isReadyToEnterReorgThreshold
88
- })
80
+ ->Array.every(cs => cs->ChainState.isReadyToEnterReorgThresholdAfterBatch(~batch))
89
81
 
90
82
  if shouldEnterReorgThreshold {
91
83
  IndexerState.enterReorgThreshold(state)
@@ -171,9 +163,7 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
171
163
  state
172
164
  ->IndexerState.chainStates
173
165
  ->Dict.valuesToArray
174
- ->Array.every(cs =>
175
- cs->ChainState.isProgressAtHead && (cs->ChainState.fetchState).endBlock->Option.isNone
176
- )
166
+ ->Array.every(ChainState.isAtHeadWithoutEndBlock)
177
167
  ) {
178
168
  IndexerState.errorExit(
179
169
  state,
@@ -4,12 +4,10 @@ import * as Utils from "./Utils.res.mjs";
4
4
  import * as Config from "./Config.res.mjs";
5
5
  import * as Logging from "./Logging.res.mjs";
6
6
  import * as ChainState from "./ChainState.res.mjs";
7
- import * as FetchState from "./FetchState.res.mjs";
8
7
  import * as IndexerState from "./IndexerState.res.mjs";
9
8
  import * as ChainMetadata from "./ChainMetadata.res.mjs";
10
9
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
11
10
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
12
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
13
11
  import * as ExitOnCaughtUp from "./ExitOnCaughtUp.res.mjs";
14
12
  import * as EventProcessing from "./EventProcessing.res.mjs";
15
13
  import * as PruneStaleHistory from "./PruneStaleHistory.res.mjs";
@@ -30,10 +28,7 @@ async function processNextBatch(state, scheduleFetch) {
30
28
  let batch = IndexerState.createBatch(state, IndexerState.processedCheckpointId(state), IndexerState.config(state).batchSize, isRollbackBatch);
31
29
  let progressedChainsById = batch.progressedChainsById;
32
30
  let isBelowReorgThreshold = !isInReorgThresholdBeforeUpdate && IndexerState.config(state).shouldRollbackOnReorg;
33
- let shouldEnterReorgThreshold = isBelowReorgThreshold && Object.values(IndexerState.chainStates(state)).every(cs => {
34
- let chainAfterBatch = progressedChainsById[ChainState.fetchState(cs).chainId];
35
- return FetchState.isReadyToEnterReorgThreshold(chainAfterBatch !== undefined ? chainAfterBatch.fetchState : ChainState.fetchState(cs));
36
- });
31
+ let shouldEnterReorgThreshold = isBelowReorgThreshold && Object.values(IndexerState.chainStates(state)).every(cs => ChainState.isReadyToEnterReorgThresholdAfterBatch(cs, batch));
37
32
  if (shouldEnterReorgThreshold) {
38
33
  IndexerState.enterReorgThreshold(state);
39
34
  }
@@ -75,7 +70,7 @@ async function processNextBatch(state, scheduleFetch) {
75
70
  }
76
71
  if (allCaughtUp && !IndexerState.keepProcessAlive(state)) {
77
72
  return await ExitOnCaughtUp.run(state);
78
- } else if (!allCaughtUp && IndexerState.exitAfterFirstEventBlock(state) && Object.values(IndexerState.chainStates(state)).every(cs => ChainState.isProgressAtHead(cs) ? Stdlib_Option.isNone(ChainState.fetchState(cs).endBlock) : false)) {
73
+ } else if (!allCaughtUp && IndexerState.exitAfterFirstEventBlock(state) && Object.values(IndexerState.chainStates(state)).every(ChainState.isAtHeadWithoutEndBlock)) {
79
74
  return IndexerState.errorExit(state, ErrorHandling.make(new Error("No events found between startBlock and chain head. Cannot auto-detect endBlock."), undefined, undefined));
80
75
  } else {
81
76
  ChainMetadata.stage(state);
@@ -11,7 +11,14 @@ type partitionQueryResponse = {
11
11
  let runContractRegistersOrThrow = async (
12
12
  ~itemsWithContractRegister: array<Internal.item>,
13
13
  ~config: Config.t,
14
+ ~chainState: ChainState.t,
15
+ ~page: option<TransactionStore.t>,
14
16
  ) => {
17
+ // contractRegister handlers can read event.transaction, so materialise the
18
+ // selected fields onto the payloads before running them. All items belong to
19
+ // the chain being fetched, hence its single page store and mask.
20
+ await chainState->ChainState.materializePageItems(~items=itemsWithContractRegister, ~page)
21
+
15
22
  let itemsWithDcs = []
16
23
 
17
24
  let onRegister = (~item: Internal.item, ~contractAddress, ~contractName) => {
@@ -106,6 +113,7 @@ let rec onQueryResponse = async (
106
113
  let chainState = state->IndexerState.getChainState(~chain)
107
114
  let {
108
115
  parsedQueueItems,
116
+ transactionStore,
109
117
  latestFetchedBlockNumber,
110
118
  latestFetchedBlockTimestamp,
111
119
  stats,
@@ -114,7 +122,7 @@ let rec onQueryResponse = async (
114
122
  fromBlockQueried,
115
123
  } = response
116
124
 
117
- if knownHeight > (chainState->ChainState.fetchState).knownHeight {
125
+ if knownHeight > chainState->ChainState.knownHeight {
118
126
  Prometheus.SourceHeight.set(
119
127
  ~blockNumber=knownHeight,
120
128
  ~chainId=(chainState->ChainState.chainConfig).id,
@@ -212,6 +220,7 @@ let rec onQueryResponse = async (
212
220
  blockTimestamp: latestFetchedBlockTimestamp,
213
221
  },
214
222
  ~query,
223
+ ~transactionStore,
215
224
  )
216
225
  ChainMetadata.stage(state)
217
226
  scheduleFetch()
@@ -224,6 +233,8 @@ let rec onQueryResponse = async (
224
233
  switch await runContractRegistersOrThrow(
225
234
  ~itemsWithContractRegister,
226
235
  ~config=state->IndexerState.config,
236
+ ~chainState,
237
+ ~page=transactionStore,
227
238
  ) {
228
239
  | exception exn => IndexerState.errorExit(state, exn->ErrorHandling.make)
229
240
  | newItemsWithDcs => proceed(~newItemsWithDcs)
@@ -240,6 +251,7 @@ and applyQueryResponse = (
240
251
  ~knownHeight,
241
252
  ~latestFetchedBlock,
242
253
  ~query,
254
+ ~transactionStore,
243
255
  ) => {
244
256
  let chainState = state->IndexerState.getChainState(~chain)
245
257
  let wasFetchingAtHead = chainState->ChainState.isFetchingAtHead
@@ -250,6 +262,7 @@ and applyQueryResponse = (
250
262
  ~newItems,
251
263
  ~newItemsWithDcs,
252
264
  ~knownHeight,
265
+ ~transactionStore,
253
266
  )
254
267
 
255
268
  // In auto-exit mode, set endBlock to the first event's block when events arrive.
@@ -294,7 +307,7 @@ let finishWaitingForNewBlock = (
294
307
  ->IndexerState.chainStates
295
308
  ->Dict.valuesToArray
296
309
  ->Array.every(cs => {
297
- cs->ChainState.fetchState->FetchState.isReadyToEnterReorgThreshold
310
+ cs->ChainState.isReadyToEnterReorgThreshold
298
311
  })
299
312
 
300
313
  // Kick processing in case there are block handlers to run.
@@ -316,7 +329,6 @@ let fetchChain = async (
316
329
  ) => {
317
330
  let chainState = state->IndexerState.getChainState(~chain)
318
331
  if !(state->IndexerState.isResolvingReorg) && !(state->IndexerState.isStopped) {
319
- let fetchState = chainState->ChainState.fetchState
320
332
  let isRealtime = state->IndexerState.isRealtime
321
333
  let sourceManager = chainState->ChainState.sourceManager
322
334
 
@@ -327,8 +339,7 @@ let fetchChain = async (
327
339
  // Owns its error boundary: launch doesn't catch, so any failure here (the
328
340
  // query, response handling, or dispatch itself) must stop the indexer.
329
341
  try {
330
- await sourceManager->SourceManager.dispatch(
331
- ~fetchState,
342
+ await chainState->ChainState.dispatch(
332
343
  ~waitForNewBlock=(~knownHeight) =>
333
344
  sourceManager->SourceManager.waitForNewBlock(~knownHeight, ~isRealtime, ~reducedPolling),
334
345
  ~onNewBlock=(~knownHeight) =>
@@ -347,7 +358,7 @@ let fetchChain = async (
347
358
  try {
348
359
  let response = await sourceManager->SourceManager.executeQuery(
349
360
  ~query,
350
- ~knownHeight=fetchState.knownHeight,
361
+ ~knownHeight=chainState->ChainState.knownHeight,
351
362
  ~isRealtime,
352
363
  )
353
364
  await onQueryResponse(
@@ -4,7 +4,6 @@ import * as Utils from "./Utils.res.mjs";
4
4
  import * as Logging from "./Logging.res.mjs";
5
5
  import * as Ecosystem from "./Ecosystem.res.mjs";
6
6
  import * as ChainState from "./ChainState.res.mjs";
7
- import * as FetchState from "./FetchState.res.mjs";
8
7
  import * as Prometheus from "./Prometheus.res.mjs";
9
8
  import * as IndexerState from "./IndexerState.res.mjs";
10
9
  import * as ChainMetadata from "./ChainMetadata.res.mjs";
@@ -17,7 +16,8 @@ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
17
16
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
18
17
  import * as ContractRegisterContext from "./ContractRegisterContext.res.mjs";
19
18
 
20
- async function runContractRegistersOrThrow(itemsWithContractRegister, config) {
19
+ async function runContractRegistersOrThrow(itemsWithContractRegister, config, chainState, page) {
20
+ await ChainState.materializePageItems(chainState, itemsWithContractRegister, page);
21
21
  let itemsWithDcs = [];
22
22
  let onRegister = (item, contractAddress, contractName) => {
23
23
  let dc_registrationBlock = item.blockNumber;
@@ -71,10 +71,10 @@ async function runContractRegistersOrThrow(itemsWithContractRegister, config) {
71
71
  return itemsWithDcs;
72
72
  }
73
73
 
74
- function applyQueryResponse(state, chain, newItems, newItemsWithDcs, knownHeight, latestFetchedBlock, query) {
74
+ function applyQueryResponse(state, chain, newItems, newItemsWithDcs, knownHeight, latestFetchedBlock, query, transactionStore) {
75
75
  let chainState = IndexerState.getChainState(state, chain);
76
76
  let wasFetchingAtHead = ChainState.isFetchingAtHead(chainState);
77
- ChainState.handleQueryResult(chainState, query, newItems, newItemsWithDcs, latestFetchedBlock, knownHeight);
77
+ ChainState.handleQueryResult(chainState, query, newItems, newItemsWithDcs, latestFetchedBlock, knownHeight, transactionStore);
78
78
  if (IndexerState.exitAfterFirstEventBlock(state) && newItems.length !== 0) {
79
79
  ChainState.setEndBlockToFirstEvent(chainState, newItems[0].blockNumber);
80
80
  }
@@ -94,9 +94,10 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
94
94
  let stats = response.stats;
95
95
  let latestFetchedBlockTimestamp = response.latestFetchedBlockTimestamp;
96
96
  let latestFetchedBlockNumber = response.latestFetchedBlockNumber;
97
+ let transactionStore = response.transactionStore;
97
98
  let parsedQueueItems = response.parsedQueueItems;
98
99
  let knownHeight = response.knownHeight;
99
- if (knownHeight > ChainState.fetchState(chainState).knownHeight) {
100
+ if (knownHeight > ChainState.knownHeight(chainState)) {
100
101
  Prometheus.SourceHeight.set(SourceManager.getActiveSource(ChainState.sourceManager(chainState)).name, ChainState.chainConfig(chainState).id, knownHeight);
101
102
  }
102
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);
@@ -133,7 +134,7 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
133
134
  applyQueryResponse(state, chain, newItems, newItemsWithDcs, knownHeight, {
134
135
  blockNumber: latestFetchedBlockNumber,
135
136
  blockTimestamp: latestFetchedBlockTimestamp
136
- }, query);
137
+ }, query, transactionStore);
137
138
  ChainMetadata.stage(state);
138
139
  scheduleFetch();
139
140
  return scheduleProcessing();
@@ -144,7 +145,7 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
144
145
  }
145
146
  let newItemsWithDcs;
146
147
  try {
147
- newItemsWithDcs = await runContractRegistersOrThrow(itemsWithContractRegister, IndexerState.config(state));
148
+ newItemsWithDcs = await runContractRegistersOrThrow(itemsWithContractRegister, IndexerState.config(state), chainState, transactionStore);
148
149
  } catch (raw_exn) {
149
150
  let exn = Primitive_exceptions.internalToException(raw_exn);
150
151
  return IndexerState.errorExit(state, ErrorHandling.make(exn, undefined, undefined));
@@ -159,7 +160,7 @@ function finishWaitingForNewBlock(state, chain, knownHeight, stateId, scheduleFe
159
160
  let chainState = IndexerState.getChainState(state, chain);
160
161
  ChainState.updateKnownHeight(chainState, knownHeight);
161
162
  let isBelowReorgThreshold = !IndexerState.isInReorgThreshold(state) && IndexerState.config(state).shouldRollbackOnReorg;
162
- let shouldEnterReorgThreshold = isBelowReorgThreshold && Object.values(IndexerState.chainStates(state)).every(cs => FetchState.isReadyToEnterReorgThreshold(ChainState.fetchState(cs)));
163
+ let shouldEnterReorgThreshold = isBelowReorgThreshold && Object.values(IndexerState.chainStates(state)).every(ChainState.isReadyToEnterReorgThreshold);
163
164
  if (shouldEnterReorgThreshold) {
164
165
  IndexerState.enterReorgThreshold(state);
165
166
  }
@@ -172,14 +173,13 @@ async function fetchChain(state, chain, action, stateId, scheduleFetch, schedule
172
173
  if (IndexerState.isResolvingReorg(state) || IndexerState.isStopped(state)) {
173
174
  return;
174
175
  }
175
- let fetchState = ChainState.fetchState(chainState);
176
176
  let isRealtime = IndexerState.isRealtime(state);
177
177
  let sourceManager = ChainState.sourceManager(chainState);
178
178
  let reducedPolling = !isRealtime;
179
179
  try {
180
- return await SourceManager.dispatch(sourceManager, fetchState, async query => {
180
+ return await ChainState.dispatch(chainState, async query => {
181
181
  try {
182
- let response = await SourceManager.executeQuery(sourceManager, query, fetchState.knownHeight, isRealtime);
182
+ let response = await SourceManager.executeQuery(sourceManager, query, ChainState.knownHeight(chainState), isRealtime);
183
183
  return await onQueryResponse(state, {
184
184
  chain: chain,
185
185
  response: response,
@@ -7,19 +7,9 @@ let stage = (state: IndexerState.t) => {
7
7
  ->IndexerState.chainStates
8
8
  ->Dict.valuesToArray
9
9
  ->Array.forEach(cs => {
10
- let fetchState = cs->ChainState.fetchState
11
10
  chainsData->Dict.set(
12
11
  (cs->ChainState.chainConfig).id->Int.toString,
13
- {
14
- firstEventBlockNumber: fetchState.firstEventBlock->Null.fromOption,
15
- isHyperSync: (
16
- cs->ChainState.sourceManager->SourceManager.getActiveSource
17
- ).poweredByHyperSync,
18
- latestFetchedBlockNumber: fetchState->FetchState.bufferBlockNumber,
19
- timestampCaughtUpToHeadOrEndblock: cs
20
- ->ChainState.timestampCaughtUpToHeadOrEndblock
21
- ->Null.fromOption,
22
- },
12
+ cs->ChainState.toChainMetadata,
23
13
  )
24
14
  })
25
15
 
@@ -2,21 +2,12 @@
2
2
 
3
3
  import * as Writing from "./Writing.res.mjs";
4
4
  import * as ChainState from "./ChainState.res.mjs";
5
- import * as FetchState from "./FetchState.res.mjs";
6
- import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
7
5
  import * as IndexerState from "./IndexerState.res.mjs";
8
- import * as SourceManager from "./sources/SourceManager.res.mjs";
9
6
 
10
7
  function stage(state) {
11
8
  let chainsData = {};
12
9
  Object.values(IndexerState.chainStates(state)).forEach(cs => {
13
- let fetchState = ChainState.fetchState(cs);
14
- chainsData[ChainState.chainConfig(cs).id.toString()] = {
15
- first_event_block: Stdlib_Null.fromOption(fetchState.firstEventBlock),
16
- buffer_block: FetchState.bufferBlockNumber(fetchState),
17
- ready_at: Stdlib_Null.fromOption(ChainState.timestampCaughtUpToHeadOrEndblock(cs)),
18
- _is_hyper_sync: SourceManager.getActiveSource(ChainState.sourceManager(cs)).poweredByHyperSync
19
- };
10
+ chainsData[ChainState.chainConfig(cs).id.toString()] = ChainState.toChainMetadata(cs);
20
11
  });
21
12
  Writing.setChainMeta(state, chainsData);
22
13
  }
@@ -17,6 +17,30 @@ type t = {
17
17
  mutable pendingBudget: float,
18
18
  mutable reorgDetection: ReorgDetection.t,
19
19
  mutable safeCheckpointTracking: option<SafeCheckpointTracking.t>,
20
+ // Holds this chain's transactions (kept in Rust) keyed by (blockNumber,
21
+ // transactionIndex). Fetch responses merge their page in; entries are pruned
22
+ // as the chain progresses and dropped above the target on rollback.
23
+ transactionStore: TransactionStore.t,
24
+ // Bitmask of the transaction fields the store materialises at batch prep.
25
+ transactionFieldMask: float,
26
+ }
27
+
28
+ // Per-chain shape returned by the status API.
29
+ type chainData = {
30
+ chainId: float,
31
+ poweredByHyperSync: bool,
32
+ firstEventBlockNumber: option<int>,
33
+ latestProcessedBlock: option<int>,
34
+ timestampCaughtUpToHeadOrEndblock: option<Date.t>,
35
+ numEventsProcessed: float,
36
+ latestFetchedBlockNumber: int,
37
+ // Need this for API backwards compatibility
38
+ @as("currentBlockHeight")
39
+ knownHeight: int,
40
+ numBatchesFetched: int,
41
+ startBlock: int,
42
+ endBlock: option<int>,
43
+ numAddresses: int,
20
44
  }
21
45
 
22
46
  let configAddresses = (chainConfig: Config.chain): array<Internal.indexingAddress> => {
@@ -43,6 +67,7 @@ let make = (
43
67
  ~numEventsProcessed=0.,
44
68
  ~timestampCaughtUpToHeadOrEndblock=None,
45
69
  ~isProgressAtHead=false,
70
+ ~transactionFieldMask=0.,
46
71
  ~logger: Pino.t,
47
72
  ): t => {
48
73
  logger,
@@ -56,6 +81,8 @@ let make = (
56
81
  pendingBudget: 0.,
57
82
  reorgDetection,
58
83
  safeCheckpointTracking,
84
+ transactionStore: TransactionStore.make(),
85
+ transactionFieldMask,
59
86
  }
60
87
 
61
88
  let makeInternal = (
@@ -301,6 +328,7 @@ let makeInternal = (
301
328
  ~committedProgressBlockNumber=progressBlockNumber,
302
329
  ~timestampCaughtUpToHeadOrEndblock,
303
330
  ~numEventsProcessed,
331
+ ~transactionFieldMask=config.ecosystem.transactionFieldMask(eventConfigs),
304
332
  ~logger,
305
333
  )
306
334
  }
@@ -377,7 +405,6 @@ let makeFromDbState = (
377
405
  // --- Read accessors. ---
378
406
 
379
407
  let logger = (cs: t) => cs.logger
380
- let fetchState = (cs: t) => cs.fetchState
381
408
  let sourceManager = (cs: t) => cs.sourceManager
382
409
  let chainConfig = (cs: t) => cs.chainConfig
383
410
  let reorgDetection = (cs: t) => cs.reorgDetection
@@ -388,6 +415,19 @@ let numEventsProcessed = (cs: t) => cs.numEventsProcessed
388
415
  let pendingBudget = (cs: t) => cs.pendingBudget
389
416
  let timestampCaughtUpToHeadOrEndblock = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock
390
417
 
418
+ // Fetch-frontier reads. The FetchState is owned here; callers go through these
419
+ // rather than reaching into it.
420
+ let knownHeight = (cs: t) => cs.fetchState.knownHeight
421
+ let indexingAddresses = (cs: t) => cs.fetchState.indexingAddresses
422
+ let bufferSize = (cs: t) => cs.fetchState->FetchState.bufferSize
423
+ let bufferReadyCount = (cs: t) => cs.fetchState->FetchState.bufferReadyCount
424
+ let getProgressPercentage = (cs: t) => cs.fetchState->FetchState.getProgressPercentage
425
+ let getProgressPercentageAt = (cs: t, ~blockNumber) =>
426
+ cs.fetchState->FetchState.getProgressPercentageAt(~blockNumber)
427
+ let hasReadyItem = (cs: t) =>
428
+ cs.fetchState->FetchState.isActivelyIndexing && cs.fetchState->FetchState.hasReadyItem
429
+ let isReadyToEnterReorgThreshold = (cs: t) => cs.fetchState->FetchState.isReadyToEnterReorgThreshold
430
+
391
431
  // Mark queries as in flight and reserve their estimated size against the shared
392
432
  // buffer budget in one step, so the counter stays in sync with the pending
393
433
  // queries it tracks.
@@ -404,6 +444,30 @@ let resetPendingQueries = (cs: t) => {
404
444
  cs.pendingBudget = 0.
405
445
  }
406
446
 
447
+ // Propose the chain's candidate queries against the shared buffer budget,
448
+ // accounting for this chain's own in-flight reservations.
449
+ let getNextQuery = (cs: t, ~budget) =>
450
+ cs.fetchState->FetchState.getNextQuery(~budget, ~chainPendingBudget=cs.pendingBudget)
451
+
452
+ // Run a fetch tick for this chain against its sources, feeding the owned fetch
453
+ // frontier to the source manager.
454
+ let dispatch = (
455
+ cs: t,
456
+ ~executeQuery,
457
+ ~waitForNewBlock,
458
+ ~onNewBlock,
459
+ ~action: FetchState.nextQuery,
460
+ ~stateId,
461
+ ) =>
462
+ cs.sourceManager->SourceManager.dispatch(
463
+ ~fetchState=cs.fetchState,
464
+ ~executeQuery,
465
+ ~waitForNewBlock,
466
+ ~onNewBlock,
467
+ ~action,
468
+ ~stateId,
469
+ )
470
+
407
471
  // --- Derived (pure). ---
408
472
 
409
473
  let hasProcessedToEndblock = (cs: t) => {
@@ -426,11 +490,34 @@ let isReady = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock !== None
426
490
  // True once the fetch frontier has reached the head/endBlock for this chain.
427
491
  let isFetchingAtHead = (cs: t) => cs.fetchState->FetchState.isFetchingAtHead
428
492
 
493
+ // Reached head on a chain with no configured endBlock — used by auto-exit to
494
+ // detect that no events were found in the start..head range.
495
+ let isAtHeadWithoutEndBlock = (cs: t) =>
496
+ cs.isProgressAtHead && cs.fetchState.endBlock->Option.isNone
497
+
429
498
  // --- State transitions. The chain state is mutated only through these; each
430
499
  // owns a cohesive update so callers don't juggle individual fields. ---
431
500
 
432
501
  // Apply a fetch response: register any new dynamic contracts, append the items
433
502
  // to the buffer and advance the known head.
503
+ // Materialise the chain store's selected transaction fields onto a batch's
504
+ // items at batch prep (the persistent-store path).
505
+ let materializeBatchItems = (cs: t, ~items: array<Internal.item>) =>
506
+ cs.transactionStore->TransactionStore.materializeItems(~items, ~mask=cs.transactionFieldMask)
507
+
508
+ // Materialise a fetch-response page's transactions onto its items before
509
+ // contract-register handlers read them. `None` pages (RPC/Fuel/Simulate keep the
510
+ // transaction inline) are a no-op.
511
+ let materializePageItems = (
512
+ cs: t,
513
+ ~items: array<Internal.item>,
514
+ ~page: option<TransactionStore.t>,
515
+ ) =>
516
+ switch page {
517
+ | Some(store) => store->TransactionStore.materializeItems(~items, ~mask=cs.transactionFieldMask)
518
+ | None => Promise.resolve()
519
+ }
520
+
434
521
  let handleQueryResult = (
435
522
  cs: t,
436
523
  ~query: FetchState.query,
@@ -438,7 +525,15 @@ let handleQueryResult = (
438
525
  ~newItemsWithDcs,
439
526
  ~latestFetchedBlock,
440
527
  ~knownHeight,
528
+ ~transactionStore as page: option<TransactionStore.t>,
441
529
  ) => {
530
+ // Merge this response's page into the chain store in lockstep with appending
531
+ // its items to the buffer. Inline-transaction sources contribute no page.
532
+ switch page {
533
+ | Some(page) => cs.transactionStore->TransactionStore.merge(page)
534
+ | None => ()
535
+ }
536
+
442
537
  let fs = switch newItemsWithDcs {
443
538
  | [] => cs.fetchState
444
539
  | _ => cs.fetchState->FetchState.registerDynamicContracts(newItemsWithDcs)
@@ -491,6 +586,56 @@ let setEndBlockToFirstEvent = (cs: t, ~blockNumber) =>
491
586
  let enterReorgThreshold = (cs: t) =>
492
587
  cs.fetchState = cs.fetchState->FetchState.updateInternal(~blockLag=cs.chainConfig.blockLag)
493
588
 
589
+ // Snapshot the chain's metadata fields for staging into the chains table.
590
+ let toChainMetadata = (cs: t): InternalTable.Chains.metaFields => {
591
+ firstEventBlockNumber: cs.fetchState.firstEventBlock->Null.fromOption,
592
+ isHyperSync: (cs.sourceManager->SourceManager.getActiveSource).poweredByHyperSync,
593
+ latestFetchedBlockNumber: cs.fetchState->FetchState.bufferBlockNumber,
594
+ timestampCaughtUpToHeadOrEndblock: cs.timestampCaughtUpToHeadOrEndblock->Null.fromOption,
595
+ }
596
+
597
+ // Snapshot the chain's view for the status API.
598
+ let toChainData = (cs: t): chainData => {
599
+ chainId: cs.chainConfig.id->Int.toFloat,
600
+ poweredByHyperSync: (cs.sourceManager->SourceManager.getActiveSource).poweredByHyperSync,
601
+ firstEventBlockNumber: cs.fetchState.firstEventBlock,
602
+ latestProcessedBlock: cs.committedProgressBlockNumber === -1
603
+ ? None
604
+ : Some(cs.committedProgressBlockNumber),
605
+ timestampCaughtUpToHeadOrEndblock: cs.timestampCaughtUpToHeadOrEndblock,
606
+ numEventsProcessed: cs.numEventsProcessed,
607
+ latestFetchedBlockNumber: Pervasives.max(cs.fetchState->FetchState.bufferBlockNumber, 0),
608
+ knownHeight: cs->hasProcessedToEndblock
609
+ ? cs.fetchState.endBlock->Option.getOr(cs.fetchState.knownHeight)
610
+ : cs.fetchState.knownHeight,
611
+ numBatchesFetched: 0,
612
+ startBlock: cs.fetchState.startBlock,
613
+ endBlock: cs.fetchState.endBlock,
614
+ numAddresses: cs.fetchState->FetchState.numAddresses,
615
+ }
616
+
617
+ // Snapshot the inputs a batch build needs from this chain.
618
+ let toChainBeforeBatch = (cs: t): Batch.chainBeforeBatch => {
619
+ fetchState: cs.fetchState,
620
+ progressBlockNumber: cs.committedProgressBlockNumber,
621
+ totalEventsProcessed: cs.numEventsProcessed,
622
+ sourceBlockNumber: cs.fetchState.knownHeight,
623
+ reorgDetection: cs.reorgDetection,
624
+ chainConfig: cs.chainConfig,
625
+ }
626
+
627
+ // Whether the chain's post-batch fetch frontier is ready to cross into the reorg
628
+ // threshold, using the batch's progressed frontier when this chain advanced.
629
+ let isReadyToEnterReorgThresholdAfterBatch = (cs: t, ~batch: Batch.t) => {
630
+ let fetchState = switch batch.progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(
631
+ cs.fetchState.chainId,
632
+ ) {
633
+ | Some(chainAfterBatch) => chainAfterBatch.fetchState
634
+ | None => cs.fetchState
635
+ }
636
+ fetchState->FetchState.isReadyToEnterReorgThreshold
637
+ }
638
+
494
639
  // Commit the post-batch fetch frontier for a chain that progressed in the batch,
495
640
  // applying blockLag when this batch also crosses into the reorg threshold.
496
641
  let advanceAfterBatch = (cs: t, ~batch: Batch.t, ~enteringReorgThreshold) =>
@@ -551,6 +696,8 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t) => {
551
696
 
552
697
  cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber
553
698
  cs.numEventsProcessed = chainAfterBatch.totalEventsProcessed
699
+ // Processed blocks' transactions are no longer needed.
700
+ cs.transactionStore->TransactionStore.prune(chainAfterBatch.progressBlockNumber)
554
701
  cs.isProgressAtHead = cs.isProgressAtHead || chainAfterBatch.isProgressAtHeadWhenBatchCreated
555
702
  switch cs.safeCheckpointTracking {
556
703
  | Some(safeCheckpointTracking) =>
@@ -624,6 +771,7 @@ let rollback = (
624
771
  | None => ()
625
772
  }
626
773
  cs.fetchState = cs.fetchState->FetchState.rollback(~targetBlockNumber=newProgressBlockNumber)
774
+ cs.transactionStore->TransactionStore.rollback(newProgressBlockNumber)
627
775
  cs.committedProgressBlockNumber = newProgressBlockNumber
628
776
  cs.numEventsProcessed = newTotalEventsProcessed
629
777
  | None =>
@@ -634,6 +782,7 @@ let rollback = (
634
782
  )
635
783
  cs.fetchState =
636
784
  cs.fetchState->FetchState.rollback(~targetBlockNumber=rollbackTargetBlockNumber)
785
+ cs.transactionStore->TransactionStore.rollback(rollbackTargetBlockNumber)
637
786
  cs.committedProgressBlockNumber = Pervasives.min(
638
787
  cs.committedProgressBlockNumber,
639
788
  rollbackTargetBlockNumber,