envio 3.3.0-alpha.0 → 3.3.0-alpha.10

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 (140) hide show
  1. package/evm.schema.json +10 -0
  2. package/index.d.ts +45 -25
  3. package/package.json +7 -7
  4. package/src/Address.res +5 -2
  5. package/src/Address.res.mjs +3 -1
  6. package/src/Api.res +1 -1
  7. package/src/Api.res.mjs +1 -1
  8. package/src/Batch.res +0 -6
  9. package/src/Batch.res.mjs +1 -12
  10. package/src/BatchProcessing.res +7 -12
  11. package/src/BatchProcessing.res.mjs +8 -7
  12. package/src/Bin.res +3 -0
  13. package/src/Bin.res.mjs +4 -0
  14. package/src/ChainFetching.res +48 -16
  15. package/src/ChainFetching.res.mjs +36 -17
  16. package/src/ChainMetadata.res +1 -11
  17. package/src/ChainMetadata.res.mjs +1 -10
  18. package/src/ChainState.res +628 -149
  19. package/src/ChainState.res.mjs +440 -102
  20. package/src/ChainState.resi +74 -4
  21. package/src/Config.res +61 -33
  22. package/src/Config.res.mjs +50 -10
  23. package/src/ContractRegisterContext.res +2 -12
  24. package/src/ContractRegisterContext.res.mjs +3 -5
  25. package/src/Core.res +14 -3
  26. package/src/CrossChainState.res +142 -93
  27. package/src/CrossChainState.res.mjs +88 -60
  28. package/src/CrossChainState.resi +2 -10
  29. package/src/Ecosystem.res +5 -6
  30. package/src/Ecosystem.res.mjs +3 -3
  31. package/src/Env.res +0 -7
  32. package/src/Env.res.mjs +0 -6
  33. package/src/Envio.res +17 -11
  34. package/src/EnvioGlobal.res +53 -0
  35. package/src/EnvioGlobal.res.mjs +31 -0
  36. package/src/EventConfigBuilder.res +177 -88
  37. package/src/EventConfigBuilder.res.mjs +99 -42
  38. package/src/EventProcessing.res +74 -28
  39. package/src/EventProcessing.res.mjs +55 -29
  40. package/src/ExitOnCaughtUp.res +10 -2
  41. package/src/ExitOnCaughtUp.res.mjs +10 -1
  42. package/src/FetchState.res +558 -300
  43. package/src/FetchState.res.mjs +437 -423
  44. package/src/HandlerLoader.res +8 -104
  45. package/src/HandlerLoader.res.mjs +2 -81
  46. package/src/HandlerRegister.res +518 -144
  47. package/src/HandlerRegister.res.mjs +285 -133
  48. package/src/HandlerRegister.resi +24 -8
  49. package/src/IndexerLoop.res +2 -3
  50. package/src/IndexerLoop.res.mjs +1 -1
  51. package/src/IndexerState.res +10 -11
  52. package/src/IndexerState.res.mjs +14 -11
  53. package/src/IndexerState.resi +2 -5
  54. package/src/IndexingAddresses.res +108 -0
  55. package/src/IndexingAddresses.res.mjs +101 -0
  56. package/src/IndexingAddresses.resi +34 -0
  57. package/src/Internal.res +155 -46
  58. package/src/Internal.res.mjs +30 -2
  59. package/src/LoadLayer.res +5 -5
  60. package/src/LoadLayer.res.mjs +6 -6
  61. package/src/LogSelection.res +102 -165
  62. package/src/LogSelection.res.mjs +101 -116
  63. package/src/Main.res +71 -236
  64. package/src/Main.res.mjs +60 -145
  65. package/src/Metrics.res +74 -0
  66. package/src/Metrics.res.mjs +75 -0
  67. package/src/PgStorage.res +4 -4
  68. package/src/PgStorage.res.mjs +5 -5
  69. package/src/Prometheus.res +10 -74
  70. package/src/Prometheus.res.mjs +124 -186
  71. package/src/PruneStaleHistory.res +2 -2
  72. package/src/PruneStaleHistory.res.mjs +3 -3
  73. package/src/RawEvent.res +7 -2
  74. package/src/RawEvent.res.mjs +4 -4
  75. package/src/Rollback.res +3 -3
  76. package/src/Rollback.res.mjs +4 -4
  77. package/src/RollbackCommit.res +4 -1
  78. package/src/RollbackCommit.res.mjs +3 -2
  79. package/src/SimulateDeadInputTracker.res +85 -0
  80. package/src/SimulateDeadInputTracker.res.mjs +73 -0
  81. package/src/SimulateDeadInputTracker.resi +12 -0
  82. package/src/SimulateItems.res +91 -23
  83. package/src/SimulateItems.res.mjs +55 -32
  84. package/src/TestIndexer.res +54 -40
  85. package/src/TestIndexer.res.mjs +37 -30
  86. package/src/bindings/Performance.res +7 -0
  87. package/src/bindings/Performance.res.mjs +21 -0
  88. package/src/bindings/Performance.resi +7 -0
  89. package/src/bindings/Viem.res +0 -41
  90. package/src/bindings/Viem.res.mjs +1 -43
  91. package/src/sources/BlockStore.res +46 -0
  92. package/src/sources/BlockStore.res.mjs +24 -0
  93. package/src/sources/EventRouter.res +36 -33
  94. package/src/sources/EventRouter.res.mjs +13 -14
  95. package/src/sources/Evm.res +83 -72
  96. package/src/sources/Evm.res.mjs +53 -69
  97. package/src/sources/EvmChain.res +18 -20
  98. package/src/sources/EvmChain.res.mjs +15 -15
  99. package/src/sources/EvmRpcClient.res +74 -5
  100. package/src/sources/EvmRpcClient.res.mjs +13 -4
  101. package/src/sources/FieldMask.res +39 -0
  102. package/src/sources/FieldMask.res.mjs +42 -0
  103. package/src/sources/Fuel.res +9 -6
  104. package/src/sources/Fuel.res.mjs +5 -10
  105. package/src/sources/HyperFuelSource.res +46 -50
  106. package/src/sources/HyperFuelSource.res.mjs +64 -54
  107. package/src/sources/HyperSync.res +39 -7
  108. package/src/sources/HyperSync.res.mjs +57 -37
  109. package/src/sources/HyperSync.resi +12 -2
  110. package/src/sources/HyperSyncClient.res +22 -111
  111. package/src/sources/HyperSyncClient.res.mjs +2 -30
  112. package/src/sources/HyperSyncHeightStream.res +1 -8
  113. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  114. package/src/sources/HyperSyncSource.res +94 -107
  115. package/src/sources/HyperSyncSource.res.mjs +82 -86
  116. package/src/sources/Rpc.res +15 -47
  117. package/src/sources/Rpc.res.mjs +25 -56
  118. package/src/sources/RpcSource.res +320 -467
  119. package/src/sources/RpcSource.res.mjs +268 -385
  120. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  121. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  122. package/src/sources/SimulateSource.res +10 -4
  123. package/src/sources/SimulateSource.res.mjs +16 -6
  124. package/src/sources/Source.res +34 -6
  125. package/src/sources/SourceManager.res +86 -27
  126. package/src/sources/SourceManager.res.mjs +84 -47
  127. package/src/sources/SourceManager.resi +12 -3
  128. package/src/sources/Svm.res +31 -16
  129. package/src/sources/Svm.res.mjs +36 -13
  130. package/src/sources/SvmHyperSyncClient.res +5 -32
  131. package/src/sources/SvmHyperSyncSource.res +159 -134
  132. package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
  133. package/src/sources/TransactionStore.res +49 -0
  134. package/src/sources/TransactionStore.res.mjs +30 -0
  135. package/src/tui/Tui.res +13 -16
  136. package/src/tui/Tui.res.mjs +12 -14
  137. package/svm.schema.json +49 -37
  138. package/src/bindings/Hrtime.res +0 -58
  139. package/src/bindings/Hrtime.res.mjs +0 -90
  140. package/src/bindings/Hrtime.resi +0 -30
@@ -4,7 +4,7 @@ type options = {
4
4
  chain: ChainMap.Chain.t,
5
5
  endpointUrl: string,
6
6
  apiToken: option<string>,
7
- eventConfigs: array<Internal.svmInstructionEventConfig>,
7
+ onEventRegistrations: array<Internal.svmOnEventRegistration>,
8
8
  clientTimeoutMillis: int,
9
9
  }
10
10
 
@@ -184,13 +184,12 @@ let parseDecoded = (
184
184
  }
185
185
  }
186
186
 
187
+ // `block` is omitted; it's materialised from the block store at batch prep.
187
188
  let toSvmInstruction = (
188
189
  instr: SvmHyperSyncClient.ResponseTypes.instruction,
189
190
  ~programName,
190
191
  ~instructionName,
191
- ~transaction,
192
192
  ~logs,
193
- ~block,
194
193
  ): Envio.svmInstruction => {
195
194
  programName,
196
195
  instructionName,
@@ -204,49 +203,23 @@ 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
- block,
210
- }
211
-
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
207
  }
235
208
 
236
209
  // Probe the discriminator byte-length ordering longest-first. Stops at the
237
210
  // first router hit. Falls back to the `_none` key (program-wide handler) when
238
211
  // no discriminator-keyed handler matches.
239
212
  let probeRouter = (
240
- router: EventRouter.t<Internal.svmInstructionEventConfig>,
213
+ router: EventRouter.t<Internal.svmOnEventRegistration>,
241
214
  programId: SvmTypes.Pubkey.t,
242
215
  instr: SvmHyperSyncClient.ResponseTypes.instruction,
243
216
  byteLengthsDesc: array<int>,
244
217
  ~contractAddress,
245
- ~indexingAddresses,
218
+ ~contractNameByAddress,
246
219
  ) => {
247
220
  let probe = (dN: option<string>) => {
248
221
  let tag = EventRouter.getSvmEventId(~programId, ~discriminator=dN)
249
- router->EventRouter.get(~tag, ~contractAddress, ~blockNumber=instr.slot, ~indexingAddresses)
222
+ router->EventRouter.get(~tag, ~contractAddress, ~contractNameByAddress)
250
223
  }
251
224
 
252
225
  let result = byteLengthsDesc->Array.reduce(None, (acc, len) =>
@@ -273,9 +246,51 @@ let probeRouter = (
273
246
  }
274
247
  }
275
248
 
276
- let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: options): t => {
249
+ // Map a selected transaction field to the extra query-side column it needs.
250
+ // `transactionIndex` is always fetched as the store key, and `tokenBalances`
251
+ // lives in a separate table (requested via `needsTokenBalances`), so neither
252
+ // adds a transaction column here.
253
+ let toQueryTxField = (field: Internal.svmTransactionField): option<
254
+ SvmHyperSyncClient.QueryTypes.transactionField,
255
+ > =>
256
+ switch field {
257
+ | TransactionIndex => None
258
+ | Signatures => Some(Signatures)
259
+ | FeePayer => Some(FeePayer)
260
+ | Success => Some(Success)
261
+ | Err => Some(Err)
262
+ | Fee => Some(Fee)
263
+ | ComputeUnitsConsumed => Some(ComputeUnitsConsumed)
264
+ | AccountKeys => Some(AccountKeys)
265
+ | RecentBlockhash => Some(RecentBlockhash)
266
+ | Version => Some(Version)
267
+ | TokenBalances => None
268
+ }
269
+
270
+ // Map a selected block field to its HyperSync query column. Slot/Blockhash/
271
+ // BlockTime are requested unconditionally (needed for reorg detection and the
272
+ // item's slot/timestamp), so selecting `slot`/`time`/`hash` adds no extra column.
273
+ let toQueryBlockField = (field: Internal.svmBlockField): option<
274
+ SvmHyperSyncClient.QueryTypes.blockField,
275
+ > =>
276
+ switch field {
277
+ | Slot | Time | Hash => None
278
+ | Height => Some(BlockHeight)
279
+ | ParentSlot => Some(ParentSlot)
280
+ | ParentHash => Some(ParentBlockhash)
281
+ }
282
+
283
+ let make = (
284
+ {chain, endpointUrl, apiToken, onEventRegistrations, clientTimeoutMillis}: options,
285
+ ): t => {
277
286
  let name = "SvmHyperSync"
278
- let chainId = chain->ChainMap.Chain.toChainId
287
+
288
+ // Definitions drive query/decode building; the registrations drive routing
289
+ // (they carry `isWildcard` and become each decoded item's `onEventRegistration`).
290
+ let eventConfigs =
291
+ onEventRegistrations->Array.map(reg =>
292
+ reg.eventConfig->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
293
+ )
279
294
 
280
295
  // Built once at startup and handed to the client so `get` decodes matching
281
296
  // instructions in Rust rather than per-instruction over the napi boundary.
@@ -290,7 +305,10 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
290
305
  },
291
306
  )
292
307
 
293
- let (eventRouter, programOrderings) = EventRouter.fromSvmEventConfigsOrThrow(eventConfigs, ~chain)
308
+ let (eventRouter, programOrderings) = EventRouter.fromSvmEventConfigsOrThrow(
309
+ onEventRegistrations,
310
+ ~chain,
311
+ )
294
312
 
295
313
  // programId.toString -> sorted-desc byte lengths
296
314
  let orderingByProgram = Dict.make()
@@ -298,23 +316,77 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
298
316
  orderingByProgram->Dict.set(o.programId->SvmTypes.Pubkey.toString, o.byteLengthsDesc)
299
317
  )
300
318
 
301
- let needsTransactions = eventConfigs->Array.some(cfg => cfg.includeTransaction)
302
319
  let needsLogs = eventConfigs->Array.some(cfg => cfg.includeLogs)
303
- let needsTokenBalances = eventConfigs->Array.some(cfg => cfg.includeTokenBalances)
320
+
321
+ // Union of selected transaction fields across the chain's events. Drives both
322
+ // the query column selection (fetch only what's used) and the materialisation
323
+ // mask. `slot`/`transactionIndex` are always fetched as the store key.
324
+ let selectedTxFields = Utils.Set.make()
325
+ eventConfigs->Array.forEach(cfg =>
326
+ cfg.selectedTransactionFields
327
+ ->(Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.svmTransactionField>)
328
+ ->Utils.Set.forEach(field => selectedTxFields->Utils.Set.add(field)->ignore)
329
+ )
330
+ let needsTokenBalances = selectedTxFields->Utils.Set.has(Internal.TokenBalances)
331
+ let txQueryFields = {
332
+ // Slot + TransactionIndex are always fetched so the store can be keyed by
333
+ // (slot, transactionIndex).
334
+ let fields: array<SvmHyperSyncClient.QueryTypes.transactionField> = [Slot, TransactionIndex]
335
+ selectedTxFields
336
+ ->Utils.Set.toArray
337
+ ->Array.forEach(field =>
338
+ switch toQueryTxField(field) {
339
+ | Some(queryField) => fields->Array.push(queryField)
340
+ | None => ()
341
+ }
342
+ )
343
+ fields
344
+ }
345
+ // The transaction table is fetched only when a selected field is actually read
346
+ // off a stored transaction record. `transactionIndex` materialises from the
347
+ // store key and `tokenBalances` lives in its own table, so neither requires it.
348
+ let needsTransactions =
349
+ selectedTxFields
350
+ ->Utils.Set.toArray
351
+ ->Array.some(field =>
352
+ switch field {
353
+ | Internal.TransactionIndex | Internal.TokenBalances => false
354
+ | _ => true
355
+ }
356
+ )
357
+
358
+ // Union of selected block fields across the chain's events. `slot`/`time`/
359
+ // `hash` are always fetched (as Slot/BlockTime/Blockhash); the rest are added
360
+ // only when an instruction selected them.
361
+ let blockQueryFields = {
362
+ let fields: array<SvmHyperSyncClient.QueryTypes.blockField> = [Slot, Blockhash, BlockTime]
363
+ let selected = Utils.Set.make()
364
+ eventConfigs->Array.forEach(cfg =>
365
+ cfg.selectedBlockFields->Utils.Set.forEach(field => selected->Utils.Set.add(field)->ignore)
366
+ )
367
+ selected->Utils.Set.forEach(field =>
368
+ switch field->toQueryBlockField {
369
+ | Some(queryField) => fields->Array.push(queryField)->ignore
370
+ | None => ()
371
+ }
372
+ )
373
+ fields
374
+ }
304
375
 
305
376
  let getItemsOrThrow = async (
306
377
  ~fromBlock,
307
378
  ~toBlock,
308
379
  ~addressesByContractName as _,
309
- ~indexingAddresses,
380
+ ~contractNameByAddress,
310
381
  ~knownHeight,
311
382
  ~partitionId as _,
312
383
  ~selection as _,
384
+ ~itemsTarget,
313
385
  ~retry,
314
386
  ~logger,
315
387
  ) => {
316
- let totalTimeRef = Hrtime.makeTimer()
317
- let pageFetchRef = Hrtime.makeTimer()
388
+ let totalTimeRef = Performance.now()
389
+ let pageFetchRef = Performance.now()
318
390
 
319
391
  let instructionSelections = buildInstructionSelections(eventConfigs)
320
392
  // Under the server's default merge mode, requesting a table's columns is
@@ -322,24 +394,8 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
322
394
  // field list returns no rows (instructions and blocks are exempt), so each
323
395
  // opted-into table needs its columns spelled out here.
324
396
  let fields: SvmHyperSyncClient.QueryTypes.fieldSelection = {
325
- 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
- ),
397
+ block: blockQueryFields,
398
+ transaction: ?(needsTransactions ? Some(txQueryFields) : None),
343
399
  log: ?(needsLogs ? Some([Slot, TransactionIndex, InstructionAddress, Kind, Message]) : None),
344
400
  tokenBalance: ?(
345
401
  needsTokenBalances
@@ -354,11 +410,10 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
354
410
  toSlot: ?(toBlock->Option.map(toBlock => toBlock + 1)),
355
411
  instructions: instructionSelections,
356
412
  fields,
413
+ maxNumInstructions: itemsTarget,
357
414
  }
358
415
 
359
- Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getInstructions")
360
-
361
- let resp = try await client.get(~query) catch {
416
+ let (resp, transactionStore, blockStore) = try await client.get(~query) catch {
362
417
  | exn =>
363
418
  throw(
364
419
  Source.GetItemsError(
@@ -376,12 +431,14 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
376
431
  ),
377
432
  )
378
433
  }
379
- let pageFetchTime = pageFetchRef->Hrtime.timeSince->Hrtime.toSecondsFloat
434
+ let pageFetchTime = pageFetchRef->Performance.secondsSince
435
+ let requestStats = [{Source.method: "getInstructions", seconds: pageFetchTime}]
380
436
 
381
- let parsingRef = Hrtime.makeTimer()
437
+ let parsingRef = Performance.now()
382
438
 
383
- // Per-slot unix timestamp lookup from the response's `blocks` table. Slots
384
- // without a block row (rare; usually skipped slots) fall back to `None`.
439
+ // Per-slot blockTime lookup from the response's `blocks` table, for the
440
+ // batch's `latestFetchedBlockTimestamp`. Slots without a block row (rare;
441
+ // usually skipped slots) fall back to `None`.
385
442
  let blockTimeBySlot = Dict.make()
386
443
  resp.data.blocks->Array.forEach(b => {
387
444
  switch b.blockTime {
@@ -390,13 +447,6 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
390
447
  }
391
448
  })
392
449
 
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
450
  // Per (slot, transaction_index, instruction_address) lookup for logs
401
451
  // scoped to a single instruction. `instructionAddress: None` logs are
402
452
  // attached to no instruction (rare; usually only system messages).
@@ -418,21 +468,6 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
418
468
  }
419
469
  })
420
470
 
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
471
  let parsedQueueItems = []
437
472
  resp.data.instructions->Array.forEach(instr => {
438
473
  let programId = instr.programId->SvmTypes.Pubkey.fromStringUnsafe
@@ -448,26 +483,16 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
448
483
  instr,
449
484
  byteLengths,
450
485
  ~contractAddress,
451
- ~indexingAddresses,
486
+ ~contractNameByAddress,
452
487
  )
453
488
 
454
489
  switch maybeConfig {
455
490
  | None => ()
456
- | 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
- }
491
+ | Some(onEventRegistration) =>
492
+ let eventConfig =
493
+ onEventRegistration.eventConfig->(
494
+ Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig
495
+ )
471
496
  let logKey =
472
497
  instr.slot->Int.toString ++
473
498
  ":" ++
@@ -486,30 +511,22 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
486
511
  )
487
512
  )
488
513
 
489
- let slotKey = instr.slot->Int.toString
490
- let blockTime = blockTimeBySlot->Utils.Dict.dangerouslyGetNonOption(slotKey)
491
514
  let payload = toSvmInstruction(
492
515
  instr,
493
516
  ~programName=eventConfig.contractName,
494
517
  ~instructionName=eventConfig.name,
495
- ~transaction=eventConfig.includeTransaction ? maybeTx : None,
496
518
  ~logs=eventConfig.includeLogs ? maybeLogs : None,
497
- ~block={
498
- slot: instr.slot,
499
- time: blockTime->Option.getOr(0),
500
- hash: "",
501
- },
502
519
  )
503
520
 
504
521
  parsedQueueItems
505
522
  ->Array.push(
506
523
  Internal.Event({
507
- eventConfig: (eventConfig :> Internal.eventConfig),
508
- timestamp: blockTime->Option.getOr(0),
524
+ onEventRegistration,
509
525
  chain,
510
526
  blockNumber: instr.slot,
511
- blockHash: "",
512
527
  logIndex: synthLogIndex(instr),
528
+ // The parent transaction is materialised from the store at batch prep.
529
+ transactionIndex: instr.transactionIndex,
513
530
  payload: payload->(Utils.magic: Envio.svmInstruction => Internal.eventPayload),
514
531
  }),
515
532
  )
@@ -519,7 +536,7 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
519
536
  let _ = logger
520
537
  })
521
538
 
522
- let parsingTimeElapsed = parsingRef->Hrtime.timeSince->Hrtime.toSecondsFloat
539
+ let parsingTimeElapsed = parsingRef->Performance.secondsSince
523
540
  let highestSlot = resp.nextSlot - 1
524
541
  let latestBlockTime =
525
542
  blockTimeBySlot
@@ -534,16 +551,21 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
534
551
  blockHash: b.blockhash,
535
552
  })
536
553
 
537
- let totalTimeElapsed = totalTimeRef->Hrtime.timeSince->Hrtime.toSecondsFloat
554
+ let totalTimeElapsed = totalTimeRef->Performance.secondsSince
538
555
 
539
556
  {
540
557
  latestFetchedBlockTimestamp: latestBlockTime,
541
558
  parsedQueueItems,
559
+ // Raw transactions kept in Rust; materialised (selected fields) at batch prep.
560
+ transactionStore: Some(transactionStore),
561
+ // Raw blocks kept in Rust; materialised onto the payload at batch prep.
562
+ blockStore: Some(blockStore),
542
563
  latestFetchedBlockNumber: highestSlot,
543
564
  stats: {totalTimeElapsed, parsingTimeElapsed, pageFetchTime},
544
565
  knownHeight,
545
566
  blockHashes,
546
567
  fromBlockQueried: fromBlock,
568
+ requestStats,
547
569
  }
548
570
  }
549
571
 
@@ -552,6 +574,7 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
552
574
  // wire, so we request `maxSlot + 1`; the caller filters to the exact slots.
553
575
  let queryBlockDataRange = async (~fromSlot, ~toSlot) => {
554
576
  let blockDatas = []
577
+ let requestStats = []
555
578
  let fromRef = ref(fromSlot)
556
579
  let keepGoing = ref(true)
557
580
  while keepGoing.contents {
@@ -562,8 +585,12 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
562
585
  fields: {block: [Slot, Blockhash, BlockTime]},
563
586
  maxNumBlocks: 1000,
564
587
  }
565
- Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getBlockHashes")
566
- let resp = await client.get(~query)
588
+ let timerRef = Performance.now()
589
+ // Block-only query; the store pages are empty.
590
+ let (resp, _, _) = await client.get(~query)
591
+ requestStats
592
+ ->Array.push({Source.method: "getBlockHashes", seconds: timerRef->Performance.secondsSince})
593
+ ->ignore
567
594
  resp.data.blocks->Array.forEach(b =>
568
595
  blockDatas
569
596
  ->Array.push({
@@ -582,12 +609,12 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
582
609
  fromRef := resp.nextSlot
583
610
  }
584
611
  }
585
- blockDatas
612
+ (blockDatas, requestStats)
586
613
  }
587
614
 
588
615
  let getBlockHashes = async (~blockNumbers, ~logger as _) =>
589
616
  switch blockNumbers->Array.get(0) {
590
- | None => Ok([])
617
+ | None => {Source.result: Ok([]), requestStats: []}
591
618
  | Some(firstSlot) =>
592
619
  try {
593
620
  let minSlot = ref(firstSlot)
@@ -602,14 +629,19 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
602
629
  }
603
630
  requested->Utils.Set.add(slot)->ignore
604
631
  })
605
- let blockDatas = await queryBlockDataRange(
632
+ let (blockDatas, requestStats) = await queryBlockDataRange(
606
633
  ~fromSlot=minSlot.contents,
607
634
  ~toSlot=maxSlot.contents,
608
635
  )
609
636
  // Keep one entry per requested slot; drop duplicates and unrelated slots.
610
- Ok(blockDatas->Array.filter(data => requested->Utils.Set.delete(data.blockNumber)))
637
+ {
638
+ Source.result: Ok(
639
+ blockDatas->Array.filter(data => requested->Utils.Set.delete(data.blockNumber)),
640
+ ),
641
+ requestStats,
642
+ }
611
643
  } catch {
612
- | exn => Error(exn)
644
+ | exn => {Source.result: Error(exn), requestStats: []}
613
645
  }
614
646
  }
615
647
 
@@ -621,17 +653,10 @@ let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: o
621
653
  poweredByHyperSync: true,
622
654
  getBlockHashes,
623
655
  getHeightOrThrow: async () => {
624
- let timer = Hrtime.makeTimer()
625
- let h = await client.getHeight()
626
- let seconds = timer->Hrtime.timeSince->Hrtime.toSecondsFloat
627
- Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getHeight")
628
- Prometheus.SourceRequestCount.addSeconds(
629
- ~sourceName=name,
630
- ~chainId,
631
- ~method="getHeight",
632
- ~seconds,
633
- )
634
- h
656
+ let timer = Performance.now()
657
+ let height = await client.getHeight()
658
+ let seconds = timer->Performance.secondsSince
659
+ {height, requestStats: [{method: "getHeight", seconds}]}
635
660
  },
636
661
  getItemsOrThrow,
637
662
  }