envio 3.3.0-alpha.1 → 3.3.0-alpha.3

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 (84) hide show
  1. package/index.d.ts +23 -21
  2. package/package.json +6 -6
  3. package/src/Batch.res +0 -6
  4. package/src/Batch.res.mjs +0 -11
  5. package/src/BatchProcessing.res +2 -12
  6. package/src/BatchProcessing.res.mjs +2 -7
  7. package/src/ChainFetching.res +42 -6
  8. package/src/ChainFetching.res.mjs +30 -11
  9. package/src/ChainMetadata.res +1 -11
  10. package/src/ChainMetadata.res.mjs +1 -10
  11. package/src/ChainState.res +150 -1
  12. package/src/ChainState.res.mjs +138 -9
  13. package/src/ChainState.resi +51 -1
  14. package/src/Config.res +3 -6
  15. package/src/Config.res.mjs +2 -3
  16. package/src/Core.res +7 -0
  17. package/src/CrossChainState.res +24 -26
  18. package/src/CrossChainState.res.mjs +19 -16
  19. package/src/Ecosystem.res +6 -3
  20. package/src/Envio.res +3 -2
  21. package/src/EventConfigBuilder.res +61 -23
  22. package/src/EventConfigBuilder.res.mjs +27 -13
  23. package/src/EventProcessing.res +63 -21
  24. package/src/EventProcessing.res.mjs +47 -22
  25. package/src/FetchState.res +53 -43
  26. package/src/FetchState.res.mjs +71 -54
  27. package/src/HandlerLoader.res +10 -1
  28. package/src/HandlerLoader.res.mjs +15 -8
  29. package/src/Internal.res +46 -7
  30. package/src/Internal.res.mjs +19 -1
  31. package/src/LoadLayer.res +5 -5
  32. package/src/LoadLayer.res.mjs +6 -6
  33. package/src/Main.res +3 -50
  34. package/src/Main.res.mjs +2 -23
  35. package/src/PgStorage.res +4 -4
  36. package/src/PgStorage.res.mjs +5 -5
  37. package/src/Prometheus.res +10 -10
  38. package/src/Prometheus.res.mjs +9 -9
  39. package/src/PruneStaleHistory.res +2 -2
  40. package/src/PruneStaleHistory.res.mjs +3 -3
  41. package/src/Rollback.res +3 -3
  42. package/src/Rollback.res.mjs +4 -4
  43. package/src/SimulateItems.res +80 -15
  44. package/src/SimulateItems.res.mjs +65 -21
  45. package/src/TestIndexer.res +48 -36
  46. package/src/TestIndexer.res.mjs +35 -28
  47. package/src/bindings/Performance.res +7 -0
  48. package/src/bindings/Performance.res.mjs +21 -0
  49. package/src/bindings/Performance.resi +7 -0
  50. package/src/sources/EventRouter.res +17 -21
  51. package/src/sources/EventRouter.res.mjs +6 -9
  52. package/src/sources/Evm.res +47 -67
  53. package/src/sources/Evm.res.mjs +42 -65
  54. package/src/sources/Fuel.res +3 -3
  55. package/src/sources/Fuel.res.mjs +1 -6
  56. package/src/sources/HyperFuelSource.res +15 -11
  57. package/src/sources/HyperFuelSource.res.mjs +13 -11
  58. package/src/sources/HyperSync.res +7 -1
  59. package/src/sources/HyperSync.res.mjs +6 -3
  60. package/src/sources/HyperSync.resi +2 -0
  61. package/src/sources/HyperSyncClient.res +14 -79
  62. package/src/sources/HyperSyncClient.res.mjs +1 -22
  63. package/src/sources/HyperSyncSource.res +44 -27
  64. package/src/sources/HyperSyncSource.res.mjs +32 -27
  65. package/src/sources/RpcSource.res +14 -10
  66. package/src/sources/RpcSource.res.mjs +11 -9
  67. package/src/sources/SimulateSource.res +4 -2
  68. package/src/sources/SimulateSource.res.mjs +1 -0
  69. package/src/sources/Source.res +5 -1
  70. package/src/sources/SourceManager.res +17 -16
  71. package/src/sources/SourceManager.res.mjs +10 -15
  72. package/src/sources/Svm.res +23 -6
  73. package/src/sources/Svm.res.mjs +24 -6
  74. package/src/sources/SvmHyperSyncClient.res +3 -29
  75. package/src/sources/SvmHyperSyncSource.res +78 -96
  76. package/src/sources/SvmHyperSyncSource.res.mjs +79 -91
  77. package/src/sources/TransactionStore.res +128 -0
  78. package/src/sources/TransactionStore.res.mjs +97 -0
  79. package/src/tui/Tui.res +13 -16
  80. package/src/tui/Tui.res.mjs +12 -14
  81. package/svm.schema.json +30 -37
  82. package/src/bindings/Hrtime.res +0 -58
  83. package/src/bindings/Hrtime.res.mjs +0 -90
  84. package/src/bindings/Hrtime.resi +0 -30
@@ -15,7 +15,7 @@ type sourceState = {
15
15
  // with a mutable state for easier reasoning and testing.
16
16
  type t = {
17
17
  sourcesState: array<sourceState>,
18
- mutable statusStart: Hrtime.timeRef,
18
+ mutable statusStart: Performance.timeRef,
19
19
  mutable status: sourceManagerStatus,
20
20
  newBlockStallTimeout: int,
21
21
  newBlockStallTimeoutRealtime: int,
@@ -24,6 +24,10 @@ type t = {
24
24
  getHeightRetryInterval: (~retry: int) => int,
25
25
  mutable activeSource: Source.t,
26
26
  mutable waitingForNewBlockStateId: option<int>,
27
+ // Dedupes the "waiting for new blocks" trace so it fires once per contiguous
28
+ // wait period instead of on every epoch that re-enters the wait before any
29
+ // new block is found. Reset when blocks are found.
30
+ mutable waitingLogged: bool,
27
31
  // Should take into consideration partitions fetching for previous states (before rollback)
28
32
  mutable fetchingPartitionsCount: int,
29
33
  recoveryTimeout: float,
@@ -186,6 +190,7 @@ let make = (
186
190
  }),
187
191
  activeSource: initialActiveSource,
188
192
  waitingForNewBlockStateId: None,
193
+ waitingLogged: false,
189
194
  fetchingPartitionsCount: 0,
190
195
  newBlockStallTimeout,
191
196
  newBlockStallTimeoutRealtime,
@@ -193,7 +198,7 @@ let make = (
193
198
  reducedPollingInterval,
194
199
  getHeightRetryInterval,
195
200
  recoveryTimeout,
196
- statusStart: Hrtime.makeTimer(),
201
+ statusStart: Performance.now(),
197
202
  status: Idle,
198
203
  hasRealtime,
199
204
  committedRateLimitTimeMs: 0.0,
@@ -211,9 +216,9 @@ let trackNewStatus = (sourceManager: t, ~newStatus) => {
211
216
  }
212
217
  promCounter->Prometheus.SafeCounter.handleFloat(
213
218
  ~labels=sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
214
- ~value=sourceManager.statusStart->Hrtime.timeSince->Hrtime.toSecondsFloat,
219
+ ~value=sourceManager.statusStart->Performance.secondsSince,
215
220
  )
216
- sourceManager.statusStart = Hrtime.makeTimer()
221
+ sourceManager.statusStart = Performance.now()
217
222
  sourceManager.status = newStatus
218
223
  }
219
224
 
@@ -496,13 +501,14 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight, ~isRealtime, ~reduc
496
501
  "knownHeight": knownHeight,
497
502
  },
498
503
  )
499
- if reducedPolling {
504
+ if !sourceManager.waitingLogged {
500
505
  logger->Logging.childTrace(
501
- `Waiting for new blocks with reduced polling (${(sourceManager.reducedPollingInterval / 1000)
502
- ->Int.toString}s). Chain is caught up, waiting for other chains to backfill.`,
506
+ reducedPolling
507
+ ? `Waiting for new blocks with reduced polling (${(sourceManager.reducedPollingInterval / 1000)
508
+ ->Int.toString}s). Chain is caught up, waiting for other chains to backfill.`
509
+ : "Initiating check for new blocks.",
503
510
  )
504
- } else {
505
- logger->Logging.childTrace("Initiating check for new blocks.")
511
+ sourceManager.waitingLogged = true
506
512
  }
507
513
 
508
514
  let mainSources = sourceManager->getNextSources(~isRealtime)
@@ -600,6 +606,7 @@ let waitForNewBlock = async (sourceManager: t, ~knownHeight, ~isRealtime, ~reduc
600
606
  "source": source.name,
601
607
  "newBlockHeight": newBlockHeight,
602
608
  })
609
+ sourceManager.waitingLogged = false
603
610
 
604
611
  status := Done
605
612
 
@@ -669,19 +676,13 @@ let executeQuery = async (
669
676
  ~fromBlock=query.fromBlock,
670
677
  ~toBlock,
671
678
  ~addressesByContractName=query.addressesByContractName,
672
- ~indexingAddresses=query.indexingAddresses,
679
+ ~contractNameByAddress=query.contractNameByAddress,
673
680
  ~partitionId=query.partitionId,
674
681
  ~knownHeight,
675
682
  ~selection=query.selection,
676
683
  ~retry,
677
684
  ~logger,
678
685
  )
679
- logger->Logging.childTrace({
680
- "msg": "Fetched block range from server",
681
- "toBlock": response.latestFetchedBlockNumber,
682
- "numEvents": response.parsedQueueItems->Array.length,
683
- "stats": response.stats,
684
- })
685
686
  sourceState.lastFailedAt = None
686
687
  responseRef := Some(response)
687
688
  } catch {
@@ -1,11 +1,11 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Utils from "../Utils.res.mjs";
4
- import * as Hrtime from "../bindings/Hrtime.res.mjs";
5
4
  import * as Source from "./Source.res.mjs";
6
5
  import * as Logging from "../Logging.res.mjs";
7
6
  import * as FetchState from "../FetchState.res.mjs";
8
7
  import * as Prometheus from "../Prometheus.res.mjs";
8
+ import * as Performance from "../bindings/Performance.res.mjs";
9
9
  import * as ErrorHandling from "../ErrorHandling.res.mjs";
10
10
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
11
11
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
@@ -139,7 +139,7 @@ function make(sources, isRealtime, newBlockStallTimeoutOpt, newBlockStallTimeout
139
139
  disabled: false,
140
140
  lastFailedAt: undefined
141
141
  })),
142
- statusStart: Hrtime.makeTimer(),
142
+ statusStart: Performance.now(),
143
143
  status: "Idle",
144
144
  newBlockStallTimeout: newBlockStallTimeout,
145
145
  newBlockStallTimeoutRealtime: newBlockStallTimeoutRealtime,
@@ -148,6 +148,7 @@ function make(sources, isRealtime, newBlockStallTimeoutOpt, newBlockStallTimeout
148
148
  getHeightRetryInterval: getHeightRetryInterval,
149
149
  activeSource: initialActiveSource,
150
150
  waitingForNewBlockStateId: undefined,
151
+ waitingLogged: false,
151
152
  fetchingPartitionsCount: 0,
152
153
  recoveryTimeout: recoveryTimeout,
153
154
  hasRealtime: hasRealtime,
@@ -172,8 +173,8 @@ function trackNewStatus(sourceManager, newStatus) {
172
173
  promCounter = Prometheus.IndexingQueryTime.counter;
173
174
  break;
174
175
  }
175
- Prometheus.SafeCounter.handleFloat(promCounter, sourceManager.activeSource.chain, Hrtime.toSecondsFloat(Hrtime.timeSince(sourceManager.statusStart)));
176
- sourceManager.statusStart = Hrtime.makeTimer();
176
+ Prometheus.SafeCounter.handleFloat(promCounter, sourceManager.activeSource.chain, Performance.secondsSince(sourceManager.statusStart));
177
+ sourceManager.statusStart = Performance.now();
177
178
  sourceManager.status = newStatus;
178
179
  }
179
180
 
@@ -408,10 +409,9 @@ async function waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPo
408
409
  chainId: sourceManager.activeSource.chain,
409
410
  knownHeight: knownHeight
410
411
  });
411
- if (reducedPolling) {
412
- Logging.childTrace(logger, `Waiting for new blocks with reduced polling (` + (sourceManager.reducedPollingInterval / 1000 | 0).toString() + `s). Chain is caught up, waiting for other chains to backfill.`);
413
- } else {
414
- Logging.childTrace(logger, "Initiating check for new blocks.");
412
+ if (!sourceManager.waitingLogged) {
413
+ Logging.childTrace(logger, reducedPolling ? `Waiting for new blocks with reduced polling (` + (sourceManager.reducedPollingInterval / 1000 | 0).toString() + `s). Chain is caught up, waiting for other chains to backfill.` : "Initiating check for new blocks.");
414
+ sourceManager.waitingLogged = true;
415
415
  }
416
416
  let mainSources = getNextSources(sourceManager, isRealtime, undefined);
417
417
  let status = {
@@ -453,6 +453,7 @@ async function waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPo
453
453
  source: source.name,
454
454
  newBlockHeight: newBlockHeight
455
455
  });
456
+ sourceManager.waitingLogged = false;
456
457
  status.contents = "Done";
457
458
  return newBlockHeight;
458
459
  }
@@ -499,13 +500,7 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
499
500
  retry: retry
500
501
  });
501
502
  try {
502
- let response = await source.getItemsOrThrow(query.fromBlock, toBlock, query.addressesByContractName, query.indexingAddresses, knownHeight, query.partitionId, query.selection, retry, logger$2);
503
- Logging.childTrace(logger$2, {
504
- msg: "Fetched block range from server",
505
- toBlock: response.latestFetchedBlockNumber,
506
- numEvents: response.parsedQueueItems.length,
507
- stats: response.stats
508
- });
503
+ let response = await source.getItemsOrThrow(query.fromBlock, toBlock, query.addressesByContractName, query.contractNameByAddress, knownHeight, query.partitionId, query.selection, retry, logger$2);
509
504
  sourceState.lastFailedAt = undefined;
510
505
  responseRef = response;
511
506
  } catch (raw_error) {
@@ -4,10 +4,26 @@ let cleanUpRawEventFieldsInPlace: JSON.t => unit = %raw(`fields => {
4
4
  delete fields.time
5
5
  }`)
6
6
 
7
+ // Ordered transaction field names. The index of each is the field code shared
8
+ // with the Rust store (`SvmTxField`) — keep this order in sync.
9
+ let transactionFields = [
10
+ "transactionIndex",
11
+ "signatures",
12
+ "feePayer",
13
+ "success",
14
+ "err",
15
+ "fee",
16
+ "computeUnitsConsumed",
17
+ "accountKeys",
18
+ "recentBlockhash",
19
+ "version",
20
+ "tokenBalances",
21
+ ]
22
+
23
+ let transactionFieldMask = TransactionStore.makeMaskFn(transactionFields)
24
+
7
25
  let make = (~logger: Pino.t): Ecosystem.t => {
8
26
  name: Svm,
9
- blockFields: ["slot"],
10
- transactionFields: [],
11
27
  blockNumberName: "height",
12
28
  blockTimestampName: "time",
13
29
  blockHashName: "hash",
@@ -20,7 +36,8 @@ let make = (~logger: Pino.t): Ecosystem.t => {
20
36
  // parse. The schema is a no-op object that always surfaces `None`.
21
37
  onEventBlockFilterSchema: S.object(_ => None),
22
38
  logger,
23
- toEvent: eventItem => eventItem.payload->Internal.payloadToEvent,
39
+ transactionFieldMask,
40
+ toEvent: eventItem => eventItem.payload->(Utils.magic: Internal.eventPayload => Internal.event),
24
41
  toEventLogger: eventItem => {
25
42
  let instruction =
26
43
  eventItem.payload->(Utils.magic: Internal.eventPayload => Envio.svmInstruction)
@@ -71,9 +88,9 @@ let makeRPCSource = (~chain, ~rpc: string, ~sourceFor: Source.sourceFor=Sync): S
71
88
  getBlockHashes: (~blockNumbers as _, ~logger as _) =>
72
89
  JsError.throwWithMessage("Svm does not support getting block hashes"),
73
90
  getHeightOrThrow: async () => {
74
- let timerRef = Hrtime.makeTimer()
91
+ let timerRef = Performance.now()
75
92
  let height = await GetFinalizedSlot.route->Rest.fetch((), ~client)
76
- let seconds = timerRef->Hrtime.timeSince->Hrtime.toSecondsFloat
93
+ let seconds = timerRef->Performance.secondsSince
77
94
  Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getSlot")
78
95
  Prometheus.SourceRequestCount.addSeconds(
79
96
  ~sourceName=name,
@@ -87,7 +104,7 @@ let makeRPCSource = (~chain, ~rpc: string, ~sourceFor: Source.sourceFor=Sync): S
87
104
  ~fromBlock as _,
88
105
  ~toBlock as _,
89
106
  ~addressesByContractName as _,
90
- ~indexingAddresses as _,
107
+ ~contractNameByAddress as _,
91
108
  ~knownHeight as _,
92
109
  ~partitionId as _,
93
110
  ~selection as _,
@@ -3,11 +3,12 @@
3
3
  import * as Rpc from "./Rpc.res.mjs";
4
4
  import * as Rest from "../vendored/Rest.res.mjs";
5
5
  import * as Utils from "../Utils.res.mjs";
6
- import * as Hrtime from "../bindings/Hrtime.res.mjs";
7
6
  import * as Logging from "../Logging.res.mjs";
8
7
  import * as Prometheus from "../Prometheus.res.mjs";
8
+ import * as Performance from "../bindings/Performance.res.mjs";
9
9
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
10
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
11
+ import * as TransactionStore from "./TransactionStore.res.mjs";
11
12
 
12
13
  let cleanUpRawEventFieldsInPlace = (fields => {
13
14
  delete fields.hash
@@ -15,11 +16,25 @@ let cleanUpRawEventFieldsInPlace = (fields => {
15
16
  delete fields.time
16
17
  });
17
18
 
19
+ let transactionFields = [
20
+ "transactionIndex",
21
+ "signatures",
22
+ "feePayer",
23
+ "success",
24
+ "err",
25
+ "fee",
26
+ "computeUnitsConsumed",
27
+ "accountKeys",
28
+ "recentBlockhash",
29
+ "version",
30
+ "tokenBalances"
31
+ ];
32
+
33
+ let transactionFieldMask = TransactionStore.makeMaskFn(transactionFields);
34
+
18
35
  function make(logger) {
19
36
  return {
20
37
  name: "svm",
21
- blockFields: ["slot"],
22
- transactionFields: [],
23
38
  blockNumberName: "height",
24
39
  blockTimestampName: "time",
25
40
  blockHashName: "hash",
@@ -29,6 +44,7 @@ function make(logger) {
29
44
  onEventBlockFilterSchema: S$RescriptSchema.object(param => {}),
30
45
  logger: logger,
31
46
  toEvent: eventItem => eventItem.payload,
47
+ transactionFieldMask: transactionFieldMask,
32
48
  toEventLogger: eventItem => {
33
49
  let instruction = eventItem.payload;
34
50
  return Logging.createChildFrom(logger, {
@@ -67,9 +83,9 @@ function makeRPCSource(chain, rpc, sourceForOpt) {
67
83
  pollingInterval: 10000,
68
84
  getBlockHashes: (param, param$1) => Stdlib_JsError.throwWithMessage("Svm does not support getting block hashes"),
69
85
  getHeightOrThrow: async () => {
70
- let timerRef = Hrtime.makeTimer();
86
+ let timerRef = Performance.now();
71
87
  let height = await Rest.fetch(route, undefined, client);
72
- let seconds = Hrtime.toSecondsFloat(Hrtime.timeSince(timerRef));
88
+ let seconds = Performance.secondsSince(timerRef);
73
89
  Prometheus.SourceRequestCount.increment(name, chain, "getSlot");
74
90
  Prometheus.SourceRequestCount.addSeconds(name, chain, "getSlot", seconds);
75
91
  return height;
@@ -80,8 +96,10 @@ function makeRPCSource(chain, rpc, sourceForOpt) {
80
96
 
81
97
  export {
82
98
  cleanUpRawEventFieldsInPlace,
99
+ transactionFields,
100
+ transactionFieldMask,
83
101
  make,
84
102
  GetFinalizedSlot,
85
103
  makeRPCSource,
86
104
  }
87
- /* route Not a pure module */
105
+ /* transactionFieldMask Not a pure module */
@@ -147,22 +147,6 @@ module ResponseTypes = {
147
147
  blockHeight?: int,
148
148
  }
149
149
 
150
- type transaction = {
151
- slot: int,
152
- transactionIndex: int,
153
- signatures: array<string>,
154
- feePayer?: string,
155
- success?: bool,
156
- err?: string,
157
- fee?: int,
158
- computeUnitsConsumed?: int,
159
- accountKeys: array<string>,
160
- recentBlockhash?: string,
161
- version?: string,
162
- loadedAddressesWritable: array<string>,
163
- loadedAddressesReadonly: array<string>,
164
- }
165
-
166
150
  /// Borsh-decoded view attached by the Rust client. `argsJson`/`accountsJson`
167
151
  /// are stringified to side-step napi-rs's lack of native JSON passthrough.
168
152
  /** Solana instruction record.
@@ -204,22 +188,10 @@ module ResponseTypes = {
204
188
  message?: string,
205
189
  }
206
190
 
207
- type tokenBalance = {
208
- slot: int,
209
- transactionIndex?: int,
210
- account?: string,
211
- mint?: string,
212
- owner?: string,
213
- preAmount?: string,
214
- postAmount?: string,
215
- }
216
-
217
191
  type queryResponseData = {
218
192
  blocks: array<block>,
219
- transactions: array<transaction>,
220
193
  instructions: array<instruction>,
221
194
  logs: array<log>,
222
- tokenBalances: array<tokenBalance>,
223
195
  }
224
196
 
225
197
  type queryResponse = {
@@ -234,7 +206,9 @@ type queryResponse = ResponseTypes.queryResponse
234
206
 
235
207
  type t = {
236
208
  getHeight: unit => promise<int>,
237
- get: (~query: query) => promise<queryResponse>,
209
+ // Returns the response plus a page of raw transactions (kept in Rust),
210
+ // keyed by (slot, transactionIndex), materialised at batch prep.
211
+ get: (~query: query) => promise<(queryResponse, TransactionStore.t)>,
238
212
  }
239
213
 
240
214
  @send
@@ -188,7 +188,6 @@ let toSvmInstruction = (
188
188
  instr: SvmHyperSyncClient.ResponseTypes.instruction,
189
189
  ~programName,
190
190
  ~instructionName,
191
- ~transaction,
192
191
  ~logs,
193
192
  ~block,
194
193
  ): Envio.svmInstruction => {
@@ -204,35 +203,10 @@ let toSvmInstruction = (
204
203
  d4: ?instr.d4,
205
204
  d8: ?instr.d8,
206
205
  params: ?(instr.decoded->Option.map(parseDecoded)),
207
- ?transaction,
208
206
  ?logs,
209
207
  block,
210
208
  }
211
209
 
212
- let toSvmTransaction = (tx: SvmHyperSyncClient.ResponseTypes.transaction): Envio.svmTransaction => {
213
- signatures: tx.signatures,
214
- accountKeys: tx.accountKeys->SvmTypes.Pubkey.fromStringsUnsafe,
215
- feePayer: ?(tx.feePayer->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
216
- success: ?tx.success,
217
- err: ?tx.err,
218
- // u64 lamports / compute units arrive as `int` over napi. Convert to
219
- // `bigint` so the public type stays defensible even for pathological values.
220
- fee: ?(tx.fee->Option.map(BigInt.fromInt)),
221
- computeUnitsConsumed: ?(tx.computeUnitsConsumed->Option.map(BigInt.fromInt)),
222
- recentBlockhash: ?tx.recentBlockhash,
223
- version: ?tx.version,
224
- }
225
-
226
- let toSvmTokenBalance = (
227
- tb: SvmHyperSyncClient.ResponseTypes.tokenBalance,
228
- ): Envio.svmTokenBalance => {
229
- account: ?(tb.account->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
230
- mint: ?(tb.mint->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
231
- owner: ?(tb.owner->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
232
- preAmount: ?tb.preAmount,
233
- postAmount: ?tb.postAmount,
234
- }
235
-
236
210
  // Probe the discriminator byte-length ordering longest-first. Stops at the
237
211
  // first router hit. Falls back to the `_none` key (program-wide handler) when
238
212
  // no discriminator-keyed handler matches.
@@ -242,11 +216,11 @@ let probeRouter = (
242
216
  instr: SvmHyperSyncClient.ResponseTypes.instruction,
243
217
  byteLengthsDesc: array<int>,
244
218
  ~contractAddress,
245
- ~indexingAddresses,
219
+ ~contractNameByAddress,
246
220
  ) => {
247
221
  let probe = (dN: option<string>) => {
248
222
  let tag = EventRouter.getSvmEventId(~programId, ~discriminator=dN)
249
- router->EventRouter.get(~tag, ~contractAddress, ~blockNumber=instr.slot, ~indexingAddresses)
223
+ router->EventRouter.get(~tag, ~contractAddress, ~contractNameByAddress)
250
224
  }
251
225
 
252
226
  let result = byteLengthsDesc->Array.reduce(None, (acc, len) =>
@@ -273,6 +247,27 @@ let probeRouter = (
273
247
  }
274
248
  }
275
249
 
250
+ // Map a selected transaction field to the extra query-side column it needs.
251
+ // `transactionIndex` is always fetched as the store key, and `tokenBalances`
252
+ // lives in a separate table (requested via `needsTokenBalances`), so neither
253
+ // adds a transaction column here.
254
+ let toQueryTxField = (field: Internal.svmTransactionField): option<
255
+ SvmHyperSyncClient.QueryTypes.transactionField,
256
+ > =>
257
+ switch field {
258
+ | TransactionIndex => None
259
+ | Signatures => Some(Signatures)
260
+ | FeePayer => Some(FeePayer)
261
+ | Success => Some(Success)
262
+ | Err => Some(Err)
263
+ | Fee => Some(Fee)
264
+ | ComputeUnitsConsumed => Some(ComputeUnitsConsumed)
265
+ | AccountKeys => Some(AccountKeys)
266
+ | RecentBlockhash => Some(RecentBlockhash)
267
+ | Version => Some(Version)
268
+ | TokenBalances => None
269
+ }
270
+
276
271
  let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: options): t => {
277
272
  let name = "SvmHyperSync"
278
273
  let chainId = chain->ChainMap.Chain.toChainId
@@ -298,23 +293,58 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
298
293
  orderingByProgram->Dict.set(o.programId->SvmTypes.Pubkey.toString, o.byteLengthsDesc)
299
294
  )
300
295
 
301
- let needsTransactions = eventConfigs->Array.some(cfg => cfg.includeTransaction)
302
296
  let needsLogs = eventConfigs->Array.some(cfg => cfg.includeLogs)
303
- let needsTokenBalances = eventConfigs->Array.some(cfg => cfg.includeTokenBalances)
297
+
298
+ // Union of selected transaction fields across the chain's events. Drives both
299
+ // the query column selection (fetch only what's used) and the materialisation
300
+ // mask. `slot`/`transactionIndex` are always fetched as the store key.
301
+ let selectedTxFields = Utils.Set.make()
302
+ eventConfigs->Array.forEach(cfg =>
303
+ cfg.selectedTransactionFields
304
+ ->(Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.svmTransactionField>)
305
+ ->Utils.Set.forEach(field => selectedTxFields->Utils.Set.add(field)->ignore)
306
+ )
307
+ let needsTokenBalances = selectedTxFields->Utils.Set.has(Internal.TokenBalances)
308
+ let txQueryFields = {
309
+ // Slot + TransactionIndex are always fetched so the store can be keyed by
310
+ // (slot, transactionIndex).
311
+ let fields: array<SvmHyperSyncClient.QueryTypes.transactionField> = [Slot, TransactionIndex]
312
+ selectedTxFields
313
+ ->Utils.Set.toArray
314
+ ->Array.forEach(field =>
315
+ switch toQueryTxField(field) {
316
+ | Some(queryField) => fields->Array.push(queryField)
317
+ | None => ()
318
+ }
319
+ )
320
+ fields
321
+ }
322
+ // The transaction table is fetched only when a selected field is actually read
323
+ // off a stored transaction record. `transactionIndex` materialises from the
324
+ // store key and `tokenBalances` lives in its own table, so neither requires it.
325
+ let needsTransactions =
326
+ selectedTxFields
327
+ ->Utils.Set.toArray
328
+ ->Array.some(field =>
329
+ switch field {
330
+ | Internal.TransactionIndex | Internal.TokenBalances => false
331
+ | _ => true
332
+ }
333
+ )
304
334
 
305
335
  let getItemsOrThrow = async (
306
336
  ~fromBlock,
307
337
  ~toBlock,
308
338
  ~addressesByContractName as _,
309
- ~indexingAddresses,
339
+ ~contractNameByAddress,
310
340
  ~knownHeight,
311
341
  ~partitionId as _,
312
342
  ~selection as _,
313
343
  ~retry,
314
344
  ~logger,
315
345
  ) => {
316
- let totalTimeRef = Hrtime.makeTimer()
317
- let pageFetchRef = Hrtime.makeTimer()
346
+ let totalTimeRef = Performance.now()
347
+ let pageFetchRef = Performance.now()
318
348
 
319
349
  let instructionSelections = buildInstructionSelections(eventConfigs)
320
350
  // Under the server's default merge mode, requesting a table's columns is
@@ -323,23 +353,7 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
323
353
  // opted-into table needs its columns spelled out here.
324
354
  let fields: SvmHyperSyncClient.QueryTypes.fieldSelection = {
325
355
  block: [Slot, Blockhash, BlockTime],
326
- transaction: ?(
327
- needsTransactions
328
- ? Some([
329
- Slot,
330
- TransactionIndex,
331
- Signatures,
332
- FeePayer,
333
- Success,
334
- Err,
335
- Fee,
336
- ComputeUnitsConsumed,
337
- AccountKeys,
338
- RecentBlockhash,
339
- Version,
340
- ])
341
- : None
342
- ),
356
+ transaction: ?(needsTransactions ? Some(txQueryFields) : None),
343
357
  log: ?(needsLogs ? Some([Slot, TransactionIndex, InstructionAddress, Kind, Message]) : None),
344
358
  tokenBalance: ?(
345
359
  needsTokenBalances
@@ -358,7 +372,7 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
358
372
 
359
373
  Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getInstructions")
360
374
 
361
- let resp = try await client.get(~query) catch {
375
+ let (resp, transactionStore) = try await client.get(~query) catch {
362
376
  | exn =>
363
377
  throw(
364
378
  Source.GetItemsError(
@@ -376,9 +390,9 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
376
390
  ),
377
391
  )
378
392
  }
379
- let pageFetchTime = pageFetchRef->Hrtime.timeSince->Hrtime.toSecondsFloat
393
+ let pageFetchTime = pageFetchRef->Performance.secondsSince
380
394
 
381
- let parsingRef = Hrtime.makeTimer()
395
+ let parsingRef = Performance.now()
382
396
 
383
397
  // Per-slot unix timestamp lookup from the response's `blocks` table. Slots
384
398
  // without a block row (rare; usually skipped slots) fall back to `None`.
@@ -390,13 +404,6 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
390
404
  }
391
405
  })
392
406
 
393
- // Per (slot, transaction_index) lookup for parent transactions.
394
- let txByKey = Dict.make()
395
- resp.data.transactions->Array.forEach(tx => {
396
- let key = tx.slot->Int.toString ++ ":" ++ tx.transactionIndex->Int.toString
397
- txByKey->Dict.set(key, tx)
398
- })
399
-
400
407
  // Per (slot, transaction_index, instruction_address) lookup for logs
401
408
  // scoped to a single instruction. `instructionAddress: None` logs are
402
409
  // attached to no instruction (rare; usually only system messages).
@@ -418,21 +425,6 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
418
425
  }
419
426
  })
420
427
 
421
- let tokenBalancesByTx = Dict.make()
422
- if needsTokenBalances {
423
- resp.data.tokenBalances->Array.forEach(tb => {
424
- switch tb.transactionIndex {
425
- | Some(txIdx) =>
426
- let key = tb.slot->Int.toString ++ ":" ++ txIdx->Int.toString
427
- switch tokenBalancesByTx->Dict.get(key) {
428
- | Some(existing) => existing->Array.push(tb)
429
- | None => tokenBalancesByTx->Dict.set(key, [tb])
430
- }
431
- | None => ()
432
- }
433
- })
434
- }
435
-
436
428
  let parsedQueueItems = []
437
429
  resp.data.instructions->Array.forEach(instr => {
438
430
  let programId = instr.programId->SvmTypes.Pubkey.fromStringUnsafe
@@ -448,26 +440,12 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
448
440
  instr,
449
441
  byteLengths,
450
442
  ~contractAddress,
451
- ~indexingAddresses,
443
+ ~contractNameByAddress,
452
444
  )
453
445
 
454
446
  switch maybeConfig {
455
447
  | None => ()
456
448
  | Some(eventConfig) =>
457
- let txKey = instr.slot->Int.toString ++ ":" ++ instr.transactionIndex->Int.toString
458
- let maybeTx =
459
- txByKey->Utils.Dict.dangerouslyGetNonOption(txKey)->Option.map(toSvmTransaction)
460
- let maybeTx = if eventConfig.includeTokenBalances {
461
- maybeTx->Option.map(tx => {
462
- let maybeBalances =
463
- tokenBalancesByTx
464
- ->Utils.Dict.dangerouslyGetNonOption(txKey)
465
- ->Option.map(bals => bals->Array.map(toSvmTokenBalance))
466
- {...tx, tokenBalances: ?maybeBalances}
467
- })
468
- } else {
469
- maybeTx
470
- }
471
449
  let logKey =
472
450
  instr.slot->Int.toString ++
473
451
  ":" ++
@@ -492,7 +470,6 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
492
470
  instr,
493
471
  ~programName=eventConfig.contractName,
494
472
  ~instructionName=eventConfig.name,
495
- ~transaction=eventConfig.includeTransaction ? maybeTx : None,
496
473
  ~logs=eventConfig.includeLogs ? maybeLogs : None,
497
474
  ~block={
498
475
  slot: instr.slot,
@@ -510,6 +487,8 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
510
487
  blockNumber: instr.slot,
511
488
  blockHash: "",
512
489
  logIndex: synthLogIndex(instr),
490
+ // The parent transaction is materialised from the store at batch prep.
491
+ transactionIndex: instr.transactionIndex,
513
492
  payload: payload->(Utils.magic: Envio.svmInstruction => Internal.eventPayload),
514
493
  }),
515
494
  )
@@ -519,7 +498,7 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
519
498
  let _ = logger
520
499
  })
521
500
 
522
- let parsingTimeElapsed = parsingRef->Hrtime.timeSince->Hrtime.toSecondsFloat
501
+ let parsingTimeElapsed = parsingRef->Performance.secondsSince
523
502
  let highestSlot = resp.nextSlot - 1
524
503
  let latestBlockTime =
525
504
  blockTimeBySlot
@@ -534,11 +513,13 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
534
513
  blockHash: b.blockhash,
535
514
  })
536
515
 
537
- let totalTimeElapsed = totalTimeRef->Hrtime.timeSince->Hrtime.toSecondsFloat
516
+ let totalTimeElapsed = totalTimeRef->Performance.secondsSince
538
517
 
539
518
  {
540
519
  latestFetchedBlockTimestamp: latestBlockTime,
541
520
  parsedQueueItems,
521
+ // Raw transactions kept in Rust; materialised (selected fields) at batch prep.
522
+ transactionStore: Some(transactionStore),
542
523
  latestFetchedBlockNumber: highestSlot,
543
524
  stats: {totalTimeElapsed, parsingTimeElapsed, pageFetchTime},
544
525
  knownHeight,
@@ -563,7 +544,8 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
563
544
  maxNumBlocks: 1000,
564
545
  }
565
546
  Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getBlockHashes")
566
- let resp = await client.get(~query)
547
+ // Block-only query; the store page is empty.
548
+ let (resp, _) = await client.get(~query)
567
549
  resp.data.blocks->Array.forEach(b =>
568
550
  blockDatas
569
551
  ->Array.push({
@@ -621,9 +603,9 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
621
603
  poweredByHyperSync: true,
622
604
  getBlockHashes,
623
605
  getHeightOrThrow: async () => {
624
- let timer = Hrtime.makeTimer()
606
+ let timer = Performance.now()
625
607
  let h = await client.getHeight()
626
- let seconds = timer->Hrtime.timeSince->Hrtime.toSecondsFloat
608
+ let seconds = timer->Performance.secondsSince
627
609
  Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getHeight")
628
610
  Prometheus.SourceRequestCount.addSeconds(
629
611
  ~sourceName=name,