envio 3.3.0-alpha.6 → 3.3.0-alpha.8

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 (93) hide show
  1. package/index.d.ts +23 -5
  2. package/package.json +6 -6
  3. package/src/Address.res +5 -2
  4. package/src/Address.res.mjs +3 -1
  5. package/src/Api.res +1 -1
  6. package/src/Api.res.mjs +1 -1
  7. package/src/ChainFetching.res +34 -34
  8. package/src/ChainFetching.res.mjs +29 -29
  9. package/src/ChainState.res +328 -165
  10. package/src/ChainState.res.mjs +205 -100
  11. package/src/ChainState.resi +16 -7
  12. package/src/Config.res +55 -27
  13. package/src/Config.res.mjs +46 -7
  14. package/src/ContractRegisterContext.res +2 -12
  15. package/src/ContractRegisterContext.res.mjs +3 -5
  16. package/src/Core.res +7 -0
  17. package/src/CrossChainState.res +54 -79
  18. package/src/CrossChainState.res.mjs +45 -60
  19. package/src/CrossChainState.resi +1 -1
  20. package/src/Ecosystem.res +3 -3
  21. package/src/Ecosystem.res.mjs +3 -3
  22. package/src/Envio.res +14 -9
  23. package/src/EventConfigBuilder.res +105 -56
  24. package/src/EventConfigBuilder.res.mjs +69 -33
  25. package/src/EventProcessing.res +19 -15
  26. package/src/EventProcessing.res.mjs +13 -12
  27. package/src/FetchState.res +383 -182
  28. package/src/FetchState.res.mjs +260 -264
  29. package/src/HandlerLoader.res +7 -112
  30. package/src/HandlerLoader.res.mjs +1 -87
  31. package/src/HandlerRegister.res +209 -6
  32. package/src/HandlerRegister.res.mjs +90 -5
  33. package/src/HandlerRegister.resi +13 -2
  34. package/src/IndexerState.res +6 -3
  35. package/src/IndexerState.res.mjs +3 -3
  36. package/src/IndexerState.resi +1 -1
  37. package/src/IndexingAddresses.res +10 -7
  38. package/src/IndexingAddresses.res.mjs +8 -7
  39. package/src/IndexingAddresses.resi +3 -1
  40. package/src/Internal.res +82 -36
  41. package/src/Internal.res.mjs +11 -1
  42. package/src/Main.res +22 -23
  43. package/src/Main.res.mjs +15 -17
  44. package/src/Metrics.res +43 -2
  45. package/src/Metrics.res.mjs +39 -3
  46. package/src/Prometheus.res +0 -35
  47. package/src/Prometheus.res.mjs +33 -68
  48. package/src/RawEvent.res +7 -2
  49. package/src/RawEvent.res.mjs +4 -4
  50. package/src/SimulateItems.res +26 -8
  51. package/src/SimulateItems.res.mjs +11 -14
  52. package/src/TestIndexer.res +1 -1
  53. package/src/TestIndexer.res.mjs +1 -1
  54. package/src/sources/BlockStore.res +46 -0
  55. package/src/sources/BlockStore.res.mjs +24 -0
  56. package/src/sources/EventRouter.res +19 -12
  57. package/src/sources/EventRouter.res.mjs +7 -5
  58. package/src/sources/Evm.res +54 -4
  59. package/src/sources/Evm.res.mjs +43 -4
  60. package/src/sources/EvmChain.res +14 -18
  61. package/src/sources/EvmChain.res.mjs +12 -13
  62. package/src/sources/FieldMask.res +39 -0
  63. package/src/sources/FieldMask.res.mjs +42 -0
  64. package/src/sources/Fuel.res +5 -2
  65. package/src/sources/Fuel.res.mjs +4 -3
  66. package/src/sources/HyperFuelSource.res +32 -40
  67. package/src/sources/HyperFuelSource.res.mjs +53 -45
  68. package/src/sources/HyperSync.res +35 -9
  69. package/src/sources/HyperSync.res.mjs +52 -35
  70. package/src/sources/HyperSync.resi +11 -3
  71. package/src/sources/HyperSyncClient.res +15 -4
  72. package/src/sources/HyperSyncHeightStream.res +1 -8
  73. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  74. package/src/sources/HyperSyncSource.res +39 -58
  75. package/src/sources/HyperSyncSource.res.mjs +44 -44
  76. package/src/sources/RpcSource.res +114 -92
  77. package/src/sources/RpcSource.res.mjs +77 -46
  78. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  79. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  80. package/src/sources/SimulateSource.res +6 -3
  81. package/src/sources/SimulateSource.res.mjs +13 -5
  82. package/src/sources/Source.res +25 -5
  83. package/src/sources/SourceManager.res +66 -3
  84. package/src/sources/SourceManager.res.mjs +55 -7
  85. package/src/sources/SourceManager.resi +10 -0
  86. package/src/sources/Svm.res +15 -10
  87. package/src/sources/Svm.res.mjs +24 -7
  88. package/src/sources/SvmHyperSyncClient.res +5 -6
  89. package/src/sources/SvmHyperSyncSource.res +84 -41
  90. package/src/sources/SvmHyperSyncSource.res.mjs +90 -43
  91. package/src/sources/TransactionStore.res +6 -107
  92. package/src/sources/TransactionStore.res.mjs +5 -86
  93. package/svm.schema.json +19 -0
@@ -97,7 +97,7 @@ function make$2(config, persistence, chainStates, isInReorgThreshold, isRealtime
97
97
  };
98
98
  }
99
99
 
100
- function makeFromDbState(config, persistence, initialState, registrations, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, onError) {
100
+ function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, onError) {
101
101
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
102
102
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
103
103
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
@@ -123,7 +123,7 @@ function makeFromDbState(config, persistence, initialState, registrations, isDev
123
123
  initialState.chains.forEach(resumedChainState => {
124
124
  let chain = Config.getChain(config, resumedChainState.id);
125
125
  let chainConfig = ChainMap.get(config.chainMap, chain);
126
- chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrations, reducedPollingInterval);
126
+ chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrationsByChainId, reducedPollingInterval);
127
127
  });
128
128
  let allChainsReady = {
129
129
  contents: initialState.chains.length !== 0
@@ -231,7 +231,7 @@ function invalidateInflight(state) {
231
231
  }
232
232
 
233
233
  function applyBatchProgress(state, batch) {
234
- CrossChainState.applyBatchProgress(state.crossChainState, batch);
234
+ CrossChainState.applyBatchProgress(state.crossChainState, batch, state.config.ecosystem.blockTimestampName);
235
235
  }
236
236
 
237
237
  function isProcessing(state) {
@@ -44,7 +44,7 @@ let makeFromDbState: (
44
44
  ~config: Config.t,
45
45
  ~persistence: Persistence.t,
46
46
  ~initialState: Persistence.initialState,
47
- ~registrations: HandlerRegister.registrations,
47
+ ~registrationsByChainId: HandlerRegister.registrationsByChainId,
48
48
  ~isDevelopmentMode: bool=?,
49
49
  ~shouldUseTui: bool=?,
50
50
  ~exitAfterFirstEventBlock: bool=?,
@@ -8,15 +8,18 @@ let deriveEffectiveStartBlock = (~registrationBlock: int, ~contractStartBlock: o
8
8
  Pervasives.max(Pervasives.max(registrationBlock, 0), contractStartBlock->Option.getOr(0))
9
9
  }
10
10
 
11
- let makeContractConfigs = (~eventConfigs: array<Internal.eventConfig>): dict<contractConfig> => {
11
+ let makeContractConfigs = (~onEventRegistrations: array<Internal.onEventRegistration>): dict<
12
+ contractConfig,
13
+ > => {
12
14
  let contractConfigs: dict<contractConfig> = Dict.make()
13
- eventConfigs->Array.forEach(ec => {
14
- switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(ec.contractName) {
15
+ onEventRegistrations->Array.forEach(reg => {
16
+ let contractName = reg.eventConfig.contractName
17
+ switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(contractName) {
15
18
  | Some({startBlock}) =>
16
19
  contractConfigs->Dict.set(
17
- ec.contractName,
20
+ contractName,
18
21
  {
19
- startBlock: switch (startBlock, ec.startBlock) {
22
+ startBlock: switch (startBlock, reg.startBlock) {
20
23
  | (Some(a), Some(b)) => Some(Pervasives.min(a, b))
21
24
  | (Some(_) as s, None) | (None, Some(_) as s) => s
22
25
  | (None, None) => None
@@ -25,9 +28,9 @@ let makeContractConfigs = (~eventConfigs: array<Internal.eventConfig>): dict<con
25
28
  )
26
29
  | None =>
27
30
  contractConfigs->Dict.set(
28
- ec.contractName,
31
+ contractName,
29
32
  {
30
- startBlock: ec.startBlock,
33
+ startBlock: reg.startBlock,
31
34
  },
32
35
  )
33
36
  }
@@ -8,14 +8,15 @@ function deriveEffectiveStartBlock(registrationBlock, contractStartBlock) {
8
8
  return Primitive_int.max(Primitive_int.max(registrationBlock, 0), Stdlib_Option.getOr(contractStartBlock, 0));
9
9
  }
10
10
 
11
- function makeContractConfigs(eventConfigs) {
11
+ function makeContractConfigs(onEventRegistrations) {
12
12
  let contractConfigs = {};
13
- eventConfigs.forEach(ec => {
14
- let match = contractConfigs[ec.contractName];
13
+ onEventRegistrations.forEach(reg => {
14
+ let contractName = reg.eventConfig.contractName;
15
+ let match = contractConfigs[contractName];
15
16
  if (match !== undefined) {
16
17
  let startBlock = match.startBlock;
17
- let match$1 = ec.startBlock;
18
- contractConfigs[ec.contractName] = {
18
+ let match$1 = reg.startBlock;
19
+ contractConfigs[contractName] = {
19
20
  startBlock: startBlock !== undefined ? (
20
21
  match$1 !== undefined ? Primitive_int.min(startBlock, match$1) : startBlock
21
22
  ) : (
@@ -24,8 +25,8 @@ function makeContractConfigs(eventConfigs) {
24
25
  };
25
26
  return;
26
27
  }
27
- contractConfigs[ec.contractName] = {
28
- startBlock: ec.startBlock
28
+ contractConfigs[contractName] = {
29
+ startBlock: reg.startBlock
29
30
  };
30
31
  });
31
32
  return contractConfigs;
@@ -6,7 +6,9 @@ type t
6
6
 
7
7
  let deriveEffectiveStartBlock: (~registrationBlock: int, ~contractStartBlock: option<int>) => int
8
8
 
9
- let makeContractConfigs: (~eventConfigs: array<Internal.eventConfig>) => dict<contractConfig>
9
+ let makeContractConfigs: (
10
+ ~onEventRegistrations: array<Internal.onEventRegistration>,
11
+ ) => dict<contractConfig>
10
12
 
11
13
  let makeIndexingAddress: (
12
14
  ~contract: Internal.indexingAddress,
package/src/Internal.res CHANGED
@@ -166,6 +166,19 @@ let allSvmTransactionFields: array<svmTransactionField> = [
166
166
  ]
167
167
  let svmTransactionFieldSchema = S.enum(allSvmTransactionFields)
168
168
 
169
+ // All SVM block fields. `slot`/`time`/`hash` are always included; the rest are
170
+ // selectable via `field_selection.block_fields` (see `allSvmBlockFields`).
171
+ type svmBlockField =
172
+ | @as("slot") Slot
173
+ | @as("time") Time
174
+ | @as("hash") Hash
175
+ | @as("height") Height
176
+ | @as("parentSlot") ParentSlot
177
+ | @as("parentHash") ParentHash
178
+
179
+ let allSvmBlockFields: array<svmBlockField> = [Height, ParentSlot, ParentHash]
180
+ let svmBlockFieldSchema = S.enum(allSvmBlockFields)
181
+
169
182
  // Static sets of nullable field names — used by RpcSource and HyperSyncSource to wrap schemas with S.nullable
170
183
  let evmNullableBlockFields = Utils.Set.fromArray(
171
184
  (
@@ -295,8 +308,8 @@ type genericEvent<'params, 'block, 'transaction> = {
295
308
  block: 'block,
296
309
  }
297
310
 
298
- // Opaque internally — block-level values needed by the runtime
299
- // (blockNumber, timestamp, blockHash) live on the item instead.
311
+ // Opaque internally — the block number needed by the runtime lives on the
312
+ // item instead.
300
313
  type event
301
314
 
302
315
  // Opaque payload an item carries. A source builds an ecosystem-specific
@@ -312,6 +325,11 @@ type eventPayload
312
325
  @get external getPayloadTransaction: eventPayload => Nullable.t<eventTransaction> = "transaction"
313
326
  @set external setPayloadTransaction: (eventPayload, eventTransaction) => unit = "transaction"
314
327
 
328
+ // Generic access to the payload's `block`: written/enriched at batch prep for
329
+ // store-backed ecosystems (EVM/SVM HyperSync) and present inline otherwise.
330
+ @get external getPayloadBlock: eventPayload => Nullable.t<eventBlock> = "block"
331
+ @set external setPayloadBlock: (eventPayload, eventBlock) => unit = "block"
332
+
315
333
  type genericLoaderArgs<'event, 'context> = {
316
334
  event: 'event,
317
335
  context: 'context,
@@ -390,31 +408,17 @@ type indexingContract = {
390
408
  effectiveStartBlock: int,
391
409
  }
392
410
 
393
- // This is private so it's not manually constructed internally
394
- // The idea is that it can only be coerced from fuel/evmEventConfig
395
- // and it can include their fields. We prevent manual creation,
396
- // so the fields are not overwritten and we can safely cast the type back to fuel/evmEventConfig
411
+ // Definition of an event/instruction we know how to decode: identity + decode
412
+ // schemas + chain-independent field selection. A pure function of the ABI +
413
+ // config, shared across chains. `private` so it can only be coerced from an
414
+ // ecosystem variant (fields never overwritten), which lets sources cast the
415
+ // base back down to evm/fuel/svm safely.
397
416
  type eventConfig = private {
398
417
  id: string,
399
418
  name: string,
400
419
  contractName: string,
401
- isWildcard: bool,
402
- // Whether the event has an event filter which uses addresses
403
- filterByAddresses: bool,
404
- // Usually always false for wildcard events
405
- // But might be true for wildcard event with dynamic event filter by addresses
406
- dependsOnAddresses: bool,
407
- // Precompiled predicate (EVM only) for events that filter an indexed address
408
- // param by registered addresses. Given the decoded event and the log's block
409
- // number, drops an event whose param-address isn't registered at/before that
410
- // block — the param-level analogue of EventRouter's srcAddress
411
- // `effectiveStartBlock` check. Absent otherwise.
412
- clientAddressFilter?: (eventPayload, int, dict<indexingContract>) => bool,
413
- handler: option<handler>,
414
- contractRegister: option<contractRegister>,
415
420
  paramsRawEventSchema: S.schema<eventParams>,
416
421
  simulateParamsSchema: S.schema<eventParams>,
417
- startBlock: option<int>,
418
422
  // Field names selected for the chain's transaction-store materialisation
419
423
  // (camelCase, matching the ecosystem's `transactionFields`). Stored as a
420
424
  // string set so the shared mask logic is ecosystem-agnostic; sources recover
@@ -425,6 +429,12 @@ type eventConfig = private {
425
429
  // so each transaction decodes only the fields its event selected. `0.` when
426
430
  // nothing is selected or the ecosystem carries the transaction inline (Fuel).
427
431
  transactionFieldMask: float,
432
+ // Selected block fields precompiled to the block-store selection bitmask (bit
433
+ // per ecosystem field code). `0.` for ecosystems that carry the block fully
434
+ // inline (RPC/Fuel). The EVM selection always includes number/timestamp/hash,
435
+ // so an EVM mask always has their bits set; SVM stamps slot/time/hash inline
436
+ // from the response and its mask is the user's selection alone.
437
+ blockFieldMask: float,
428
438
  }
429
439
 
430
440
  type fuelEventKind =
@@ -460,18 +470,16 @@ type eventFilters =
460
470
 
461
471
  type evmEventConfig = {
462
472
  ...eventConfig,
463
- getEventFiltersOrThrow: ChainMap.Chain.t => eventFilters,
464
473
  selectedBlockFields: Utils.Set.t<evmBlockField>,
465
474
  sighash: string,
466
475
  topicCount: int,
467
476
  paramsMetadata: array<paramMeta>,
468
477
  }
469
478
 
470
- // Shared formula for `eventConfig.dependsOnAddresses`. Kept here so
471
- // `EventConfigBuilder.build{Evm,Fuel}EventConfig` and
472
- // `HandlerLoader.applyRegistrations` stay in sync when handler state flips
473
- // the value. Fuel events always have `filterByAddresses=false`, so callers
474
- // there simply pass it through as `false`.
479
+ // Shared formula for a registration's `dependsOnAddresses`. Kept here so the
480
+ // `EventConfigBuilder.build*OnEventRegistration` builders stay in sync. Fuel
481
+ // and SVM events always have `filterByAddresses=false`, so callers there pass
482
+ // it through as `false`.
475
483
  let dependsOnAddresses = (~isWildcard, ~filterByAddresses) => !isWildcard || filterByAddresses
476
484
 
477
485
  type evmContractConfig = {
@@ -490,6 +498,10 @@ type svmAccountFilterGroup = array<svmAccountFilter>
490
498
 
491
499
  type svmInstructionEventConfig = {
492
500
  ...eventConfig,
501
+ /** Block fields selected via `field_selection.block_fields` (`slot` is always
502
+ included and excluded from this set). Drives the block query columns;
503
+ precompiled to `blockFieldMask` for store materialisation. */
504
+ selectedBlockFields: Utils.Set.t<svmBlockField>,
493
505
  /** Base58 Solana program id this instruction belongs to. */
494
506
  programId: SvmTypes.Pubkey.t,
495
507
  /** Hex-encoded discriminator. `None` matches every instruction in the program. */
@@ -516,6 +528,44 @@ type svmInstructionEventConfig = {
516
528
  definedTypes: JSON.t,
517
529
  }
518
530
 
531
+ // Per-(event, chain) registration produced when user handler code registers an
532
+ // event (`onEvent`) or a dynamic contract registers. References its definition
533
+ // by value as `.eventConfig` and adds the handler binding plus the
534
+ // registration/`where`-derived fetch state. Not `private`: Fuel/SVM
535
+ // registrations add no ecosystem-specific fields (so they're bare aliases that
536
+ // must stay directly constructable), and the evm→base cast in sources is sound
537
+ // by ecosystem homogeneity — an EVM chain only ever holds `evmOnEventRegistration`s.
538
+ type onEventRegistration = {
539
+ eventConfig: eventConfig,
540
+ handler: option<handler>,
541
+ contractRegister: option<contractRegister>,
542
+ isWildcard: bool,
543
+ // Whether the event has an event filter which uses addresses.
544
+ filterByAddresses: bool,
545
+ // Usually always false for wildcard events, but might be true for a wildcard
546
+ // event with a dynamic event filter by addresses.
547
+ dependsOnAddresses: bool,
548
+ // Precompiled predicate for events that filter an indexed address param by
549
+ // registered addresses (see `EventConfigBuilder.buildAddressFilter`); drops a
550
+ // decoded event whose param-address isn't registered at/before the log's
551
+ // block. Absent otherwise.
552
+ clientAddressFilter?: (eventPayload, int, dict<indexingContract>) => bool,
553
+ // Final start block: the contract/chain config value, overridden by a
554
+ // `where.block.number._gte` when the registered `where` supplies one.
555
+ startBlock: option<int>,
556
+ }
557
+
558
+ type evmOnEventRegistration = {
559
+ ...onEventRegistration,
560
+ getEventFiltersOrThrow: ChainMap.Chain.t => eventFilters,
561
+ }
562
+
563
+ // Fuel and SVM registrations add no ecosystem-specific fetch state (their
564
+ // filters are config-derived and live on the definition), so they're bare
565
+ // aliases of the base registration.
566
+ type fuelOnEventRegistration = onEventRegistration
567
+ type svmOnEventRegistration = onEventRegistration
568
+
519
569
  type svmProgramConfig = {
520
570
  name: string,
521
571
  programId: SvmTypes.Pubkey.t,
@@ -536,11 +586,9 @@ type dcs = array<indexingAddress>
536
586
  // to make item properly unboxed
537
587
  type eventItem = private {
538
588
  kind: [#0],
539
- eventConfig: eventConfig,
540
- timestamp: int,
589
+ onEventRegistration: onEventRegistration,
541
590
  chain: ChainMap.Chain.t,
542
591
  blockNumber: int,
543
- blockHash: string,
544
592
  logIndex: int,
545
593
  // Within-block transaction index — the key into the per-chain transaction
546
594
  // store. Unused (0) for ecosystems that carry the transaction inline (Fuel).
@@ -575,7 +623,7 @@ type onBlockArgs = {
575
623
  context: handlerContext,
576
624
  }
577
625
 
578
- type onBlockConfig = {
626
+ type onBlockRegistration = {
579
627
  // When there are multiple onBlock handlers per chain,
580
628
  // we want to use the order they are defined for sorting
581
629
  index: int,
@@ -591,16 +639,14 @@ type onBlockConfig = {
591
639
  type item =
592
640
  | @as(0)
593
641
  Event({
594
- eventConfig: eventConfig,
595
- timestamp: int,
642
+ onEventRegistration: onEventRegistration,
596
643
  chain: ChainMap.Chain.t,
597
644
  blockNumber: int,
598
- blockHash: string,
599
645
  logIndex: int,
600
646
  transactionIndex: int,
601
647
  payload: eventPayload,
602
648
  })
603
- | @as(1) Block({onBlockConfig: onBlockConfig, blockNumber: int, logIndex: int})
649
+ | @as(1) Block({onBlockRegistration: onBlockRegistration, blockNumber: int, logIndex: int})
604
650
 
605
651
  external castUnsafeEventItem: item => eventItem = "%identity"
606
652
 
@@ -612,7 +658,7 @@ external getItemLogIndex: item => int = "logIndex"
612
658
  let getItemChainId = item =>
613
659
  switch item {
614
660
  | Event({chain}) => chain->ChainMap.Chain.toChainId
615
- | Block({onBlockConfig: {chainId}}) => chainId
661
+ | Block({onBlockRegistration: {chainId}}) => chainId
616
662
  }
617
663
 
618
664
  @get
@@ -90,6 +90,14 @@ let allSvmTransactionFields = [
90
90
 
91
91
  let svmTransactionFieldSchema = S$RescriptSchema.$$enum(allSvmTransactionFields);
92
92
 
93
+ let allSvmBlockFields = [
94
+ "height",
95
+ "parentSlot",
96
+ "parentHash"
97
+ ];
98
+
99
+ let svmBlockFieldSchema = S$RescriptSchema.$$enum(allSvmBlockFields);
100
+
93
101
  let evmNullableBlockFields = new Set([
94
102
  "nonce",
95
103
  "difficulty",
@@ -141,7 +149,7 @@ function getItemChainId(item) {
141
149
  if (item.kind === 0) {
142
150
  return item.chain;
143
151
  } else {
144
- return item.onBlockConfig.chainId;
152
+ return item.onBlockRegistration.chainId;
145
153
  }
146
154
  }
147
155
 
@@ -178,6 +186,8 @@ export {
178
186
  evmTransactionFieldSchema,
179
187
  allSvmTransactionFields,
180
188
  svmTransactionFieldSchema,
189
+ allSvmBlockFields,
190
+ svmBlockFieldSchema,
181
191
  evmNullableBlockFields,
182
192
  evmNullableTransactionFields,
183
193
  dependsOnAddresses,
package/src/Main.res CHANGED
@@ -82,7 +82,7 @@ let getInitialChainState = (~chainId: int): option<Persistence.initialChainState
82
82
  }
83
83
  }
84
84
 
85
- // Importing `generated` must not trigger `Config.loadWithoutRegistrations()`,
85
+ // Importing `generated` must not trigger `Config.load()`,
86
86
  // so the exported indexer calls this lazily on first `indexer.chains` access.
87
87
  let buildChainsObject = (~config: Config.t) => {
88
88
  let chainIds = []
@@ -372,7 +372,7 @@ let getGlobalIndexer = (): 'indexer => {
372
372
  // math at `FetchState.res:619` stays untouched.
373
373
  let onBlockFn = (rawOptions: 'a, handler: 'b) => {
374
374
  HandlerRegister.throwIfFinishedRegistration(~methodName="onBlock")
375
- let config = Config.loadWithoutRegistrations()
375
+ let config = Config.load()
376
376
  let ecosystem = config.ecosystem
377
377
  let raw =
378
378
  rawOptions->(
@@ -471,13 +471,13 @@ let getGlobalIndexer = (): 'indexer => {
471
471
  // `Api.res` calls `getGlobalIndexer()` at envio-package load, so the keys
472
472
  // array is memoized lazily: an early `createEffect` / `S` import that
473
473
  // never touches the indexer must not trigger a config parse. The memo is
474
- // safe because `Config.loadWithoutRegistrations` is itself pure.
474
+ // safe because `Config.load` is itself pure.
475
475
  let keysMemo: ref<option<array<string>>> = ref(None)
476
476
  let getKeys = () =>
477
477
  switch keysMemo.contents {
478
478
  | Some(k) => k
479
479
  | None => {
480
- let keys = switch Config.loadWithoutRegistrations().ecosystem.name {
480
+ let keys = switch Config.load().ecosystem.name {
481
481
  | Evm | Fuel => [
482
482
  "name",
483
483
  "description",
@@ -505,15 +505,15 @@ let getGlobalIndexer = (): 'indexer => {
505
505
 
506
506
  let get = (~prop: string) =>
507
507
  switch prop {
508
- | "name" => Config.loadWithoutRegistrations().name->(Utils.magic: string => unknown)
508
+ | "name" => Config.load().name->(Utils.magic: string => unknown)
509
509
  | "description" =>
510
- Config.loadWithoutRegistrations().description->(Utils.magic: option<string> => unknown)
510
+ Config.load().description->(Utils.magic: option<string> => unknown)
511
511
  | "chainIds" => {
512
- let (_, chainIds) = buildChainsObject(~config=Config.loadWithoutRegistrations())
512
+ let (_, chainIds) = buildChainsObject(~config=Config.load())
513
513
  chainIds->(Utils.magic: array<int> => unknown)
514
514
  }
515
515
  | "chains" => {
516
- let (chains, _) = buildChainsObject(~config=Config.loadWithoutRegistrations())
516
+ let (chains, _) = buildChainsObject(~config=Config.load())
517
517
  chains->(Utils.magic: {..} => unknown)
518
518
  }
519
519
  | "onEvent" => onEventFn->Utils.magic
@@ -657,7 +657,7 @@ type mainArgs = Yargs.parsedArgs<args>
657
657
  let getEnvioInfo = () => Config.getPublicConfigJson()->Config.stripSensitiveData
658
658
 
659
659
  let migrate = async (~reset) => {
660
- let config = Config.loadWithoutRegistrations()
660
+ let config = Config.load()
661
661
  let persistence = PgStorage.makePersistenceFromConfig(~config)
662
662
  await persistence->Persistence.init(
663
663
  ~reset,
@@ -670,7 +670,7 @@ let migrate = async (~reset) => {
670
670
  }
671
671
 
672
672
  let dropSchema = async () => {
673
- let config = Config.loadWithoutRegistrations()
673
+ let config = Config.load()
674
674
  let persistence = PgStorage.makePersistenceFromConfig(~config)
675
675
  await persistence.storage.reset()
676
676
  await persistence.storage.close()
@@ -685,7 +685,7 @@ let start = async (
685
685
  ~reset=false,
686
686
  ~isTest=false,
687
687
  ~exitAfterFirstEventBlock=false,
688
- ~patchConfig: option<(Config.t, HandlerRegister.registrations) => Config.t>=?,
688
+ ~patchConfig: option<(Config.t, HandlerRegister.registrationsByChainId) => Config.t>=?,
689
689
  ) => {
690
690
  let mainArgs: mainArgs = process->argv->Yargs.hideBin->Yargs.yargs->Yargs.argv
691
691
  let explicitTui = switch mainArgs.tuiOff {
@@ -699,31 +699,30 @@ let start = async (
699
699
  }
700
700
  // Initialize persistence first so the exported indexer value contains state from the database
701
701
  // when handler files are loaded (they may access the indexer at module top level).
702
- let configWithoutRegistrations = Config.loadWithoutRegistrations()
702
+ let config = Config.load()
703
703
  // isDevelopmentMode controls whether the indexer stays alive after all
704
704
  // chains finish (keepProcessAlive) and whether the console API is exposed.
705
705
  // Set by `envio dev` via the public config's `isDev` field; `envio start`
706
706
  // leaves it false so the process exits cleanly when indexing completes.
707
- let isDevelopmentMode = !isTest && configWithoutRegistrations.isDev
707
+ let isDevelopmentMode = !isTest && config.isDev
708
708
  let persistence = switch persistence {
709
709
  | Some(p) => p
710
- | None => PgStorage.makePersistenceFromConfig(~config=configWithoutRegistrations)
710
+ | None => PgStorage.makePersistenceFromConfig(~config)
711
711
  }
712
712
  globalPersistenceRef := Some(persistence)
713
713
  await persistence->Persistence.init(
714
714
  ~reset,
715
- ~chainConfigs=configWithoutRegistrations.chainMap->ChainMap.values,
715
+ ~chainConfigs=config.chainMap->ChainMap.values,
716
716
  ~envioInfo=getEnvioInfo(),
717
717
  ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
718
718
  ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
719
719
  )
720
720
 
721
- // `Config.loadWithoutRegistrations` never sees registration state; handler,
722
- // contractRegister, and eventFilters are baked into each event config only
723
- // by the returned value here.
724
- let (config, registrations) = await HandlerLoader.registerAllHandlers(
725
- ~config=configWithoutRegistrations,
726
- )
721
+ // Loads user handler files, which register handler/contractRegister/where
722
+ // state into the global `HandlerRegister` registry as a side effect; this
723
+ // returns that state resolved into per-chain registrations. `config` itself
724
+ // is never mutated by registration — it holds only event definitions.
725
+ let registrationsByChainId = await HandlerLoader.registerAllHandlers(~config)
727
726
  let config = if isTest {
728
727
  {...config, shouldRollbackOnReorg: false}
729
728
  } else {
@@ -731,7 +730,7 @@ let start = async (
731
730
  }
732
731
 
733
732
  let config = switch patchConfig {
734
- | Some(patchConfig) => patchConfig(config, registrations)
733
+ | Some(patchConfig) => patchConfig(config, registrationsByChainId)
735
734
  | None => config
736
735
  }
737
736
  // The single fatal-error handler, invoked once via IndexerState.errorExit.
@@ -778,7 +777,7 @@ let start = async (
778
777
  ~config,
779
778
  ~persistence,
780
779
  ~initialState=persistence->Persistence.getInitializedState,
781
- ~registrations,
780
+ ~registrationsByChainId,
782
781
  ~isDevelopmentMode,
783
782
  ~shouldUseTui,
784
783
  ~exitAfterFirstEventBlock,
package/src/Main.res.mjs CHANGED
@@ -296,7 +296,7 @@ function getGlobalIndexer() {
296
296
  };
297
297
  let onBlockFn = (rawOptions, handler) => {
298
298
  HandlerRegister.throwIfFinishedRegistration("onBlock");
299
- let config = Config.loadWithoutRegistrations();
299
+ let config = Config.load();
300
300
  let ecosystem = config.ecosystem;
301
301
  let match = buildChainsObject(config);
302
302
  let chains = match[0];
@@ -350,7 +350,7 @@ function getGlobalIndexer() {
350
350
  if (k !== undefined) {
351
351
  return k;
352
352
  }
353
- let match = Config.loadWithoutRegistrations().ecosystem.name;
353
+ let match = Config.load().ecosystem.name;
354
354
  let keys;
355
355
  let exit = 0;
356
356
  switch (match) {
@@ -388,15 +388,15 @@ function getGlobalIndexer() {
388
388
  let get = prop => {
389
389
  switch (prop) {
390
390
  case "chainIds" :
391
- return buildChainsObject(Config.loadWithoutRegistrations())[1];
391
+ return buildChainsObject(Config.load())[1];
392
392
  case "chains" :
393
- return buildChainsObject(Config.loadWithoutRegistrations())[0];
393
+ return buildChainsObject(Config.load())[0];
394
394
  case "contractRegister" :
395
395
  return contractRegisterFn;
396
396
  case "description" :
397
- return Config.loadWithoutRegistrations().description;
397
+ return Config.load().description;
398
398
  case "name" :
399
- return Config.loadWithoutRegistrations().name;
399
+ return Config.load().name;
400
400
  case "onEvent" :
401
401
  return onEventFn;
402
402
  case "onInstruction" :
@@ -501,14 +501,14 @@ function getEnvioInfo() {
501
501
  }
502
502
 
503
503
  async function migrate(reset) {
504
- let config = Config.loadWithoutRegistrations();
504
+ let config = Config.load();
505
505
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
506
506
  await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset);
507
507
  return await persistence.storage.close();
508
508
  }
509
509
 
510
510
  async function dropSchema() {
511
- let config = Config.loadWithoutRegistrations();
511
+ let config = Config.load();
512
512
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
513
513
  await persistence.storage.reset();
514
514
  return await persistence.storage.close();
@@ -526,14 +526,12 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
526
526
  let shouldUseTui = isTest ? false : (
527
527
  explicitTui !== undefined ? explicitTui : !Envio.isNonInteractive()
528
528
  );
529
- let configWithoutRegistrations = Config.loadWithoutRegistrations();
530
- let isDevelopmentMode = !isTest && configWithoutRegistrations.isDev;
531
- let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(configWithoutRegistrations, undefined);
529
+ let config = Config.load();
530
+ let isDevelopmentMode = !isTest && config.isDev;
531
+ let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
532
532
  globalPersistenceRef.contents = persistence$1;
533
- await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
534
- let match = await HandlerLoader.registerAllHandlers(configWithoutRegistrations);
535
- let registrations = match[1];
536
- let config = match[0];
533
+ await Persistence.init(persistence$1, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
534
+ let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
537
535
  let config$1 = isTest ? ({
538
536
  name: config.name,
539
537
  description: config.description,
@@ -555,7 +553,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
555
553
  allEntities: config.allEntities,
556
554
  allEnums: config.allEnums
557
555
  }) : config;
558
- let config$2 = patchConfig !== undefined ? patchConfig(config$1, registrations) : config$1;
556
+ let config$2 = patchConfig !== undefined ? patchConfig(config$1, registrationsByChainId) : config$1;
559
557
  let onErrorReject = {
560
558
  contents: undefined
561
559
  };
@@ -593,7 +591,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
593
591
  };
594
592
  }, persistence$1, isDevelopmentMode);
595
593
  }
596
- let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrations, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError);
594
+ let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError);
597
595
  if (shouldUseTui) {
598
596
  Tui.start(() => state);
599
597
  }
package/src/Metrics.res CHANGED
@@ -18,16 +18,57 @@ let renderGauge = (~name, ~help, ~chains: dict<'a>, ~value: 'a => int) => {
18
18
  out.contents
19
19
  }
20
20
 
21
+ let sourceRequestTotalName = "envio_source_request_total"
22
+ let sourceRequestTotalHelp = "The number of requests made to data sources."
23
+ let sourceRequestSecondsTotalName = "envio_source_request_seconds_total"
24
+ let sourceRequestSecondsTotalHelp = "Cumulative time spent on data source requests."
25
+
26
+ // Hand-rolls both HELP/TYPE blocks and their samples straight from
27
+ // SourceManager's aggregates — fully our own state, no prom-client registry
28
+ // involved. A leading "\n" on each block since, unlike prom-client's own
29
+ // metrics() output, renderGauge's output right before this doesn't end in one.
30
+ // Skips a method's seconds line when it has no timing (e.g. heightSubscription,
31
+ // which only ever records a count), and skips both blocks entirely when there's
32
+ // nothing to report at all.
33
+ let renderSourceRequests = (~samples: array<SourceManager.requestStatSample>) => {
34
+ if samples->Utils.Array.isEmpty {
35
+ ""
36
+ } else {
37
+ let countOut = ref(
38
+ `\n# HELP ${sourceRequestTotalName} ${sourceRequestTotalHelp}\n# TYPE ${sourceRequestTotalName} counter`,
39
+ )
40
+ let secondsOut = ref(
41
+ `\n# HELP ${sourceRequestSecondsTotalName} ${sourceRequestSecondsTotalHelp}\n# TYPE ${sourceRequestSecondsTotalName} counter`,
42
+ )
43
+ samples->Array.forEach(sample => {
44
+ let labels = `{source="${sample.sourceName}",chainId="${sample.chainId->Int.toString}",method="${sample.method}"}`
45
+ countOut :=
46
+ countOut.contents ++ `\n${sourceRequestTotalName}${labels} ${sample.count->Int.toString}`
47
+ if sample.seconds !== 0. {
48
+ secondsOut :=
49
+ secondsOut.contents ++
50
+ `\n${sourceRequestSecondsTotalName}${labels} ${sample.seconds->Float.toString}`
51
+ }
52
+ })
53
+ countOut.contents ++ secondsOut.contents
54
+ }
55
+ }
56
+
21
57
  let collect = async (~state: option<IndexerState.t>) => {
22
58
  let base = await PromClient.defaultRegister->PromClient.metrics
23
59
  switch state {
24
60
  | None => base
25
61
  | Some(state) =>
62
+ let chainStates = state->IndexerState.chainStates
63
+ let sourceRequestSamples =
64
+ chainStates
65
+ ->Dict.valuesToArray
66
+ ->Array.flatMap(cs => cs->ChainState.sourceManager->SourceManager.getRequestStatSamples)
26
67
  `${base}${renderGauge(
27
68
  ~name=indexingAddressesName,
28
69
  ~help=indexingAddressesHelp,
29
- ~chains=state->IndexerState.chainStates,
70
+ ~chains=chainStates,
30
71
  ~value=cs => (cs->ChainState.toChainData).numAddresses,
31
- )}\n`
72
+ )}${renderSourceRequests(~samples=sourceRequestSamples)}\n`
32
73
  }
33
74
  }