envio 3.3.0-alpha.1 → 3.3.0-alpha.11

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 (143) hide show
  1. package/evm.schema.json +10 -0
  2. package/index.d.ts +45 -25
  3. package/licenses/CLA.md +35 -0
  4. package/licenses/EULA.md +67 -0
  5. package/licenses/LICENSE.md +45 -0
  6. package/licenses/README.md +35 -0
  7. package/package.json +10 -9
  8. package/src/Address.res +5 -2
  9. package/src/Address.res.mjs +3 -1
  10. package/src/Api.res +1 -1
  11. package/src/Api.res.mjs +1 -1
  12. package/src/Batch.res +0 -6
  13. package/src/Batch.res.mjs +1 -12
  14. package/src/BatchProcessing.res +7 -12
  15. package/src/BatchProcessing.res.mjs +8 -7
  16. package/src/Bin.res +3 -0
  17. package/src/Bin.res.mjs +4 -0
  18. package/src/ChainFetching.res +53 -16
  19. package/src/ChainFetching.res.mjs +37 -19
  20. package/src/ChainMetadata.res +1 -11
  21. package/src/ChainMetadata.res.mjs +1 -10
  22. package/src/ChainState.res +630 -161
  23. package/src/ChainState.res.mjs +438 -101
  24. package/src/ChainState.resi +77 -4
  25. package/src/Config.res +70 -38
  26. package/src/Config.res.mjs +52 -12
  27. package/src/ContractRegisterContext.res +2 -12
  28. package/src/ContractRegisterContext.res.mjs +3 -5
  29. package/src/Core.res +34 -3
  30. package/src/Core.res.mjs +12 -0
  31. package/src/CrossChainState.res +130 -89
  32. package/src/CrossChainState.res.mjs +66 -61
  33. package/src/CrossChainState.resi +1 -1
  34. package/src/Ecosystem.res +5 -6
  35. package/src/Ecosystem.res.mjs +3 -3
  36. package/src/Envio.res +17 -11
  37. package/src/EnvioGlobal.res +53 -0
  38. package/src/EnvioGlobal.res.mjs +31 -0
  39. package/src/EventConfigBuilder.res +191 -127
  40. package/src/EventConfigBuilder.res.mjs +116 -66
  41. package/src/EventProcessing.res +77 -28
  42. package/src/EventProcessing.res.mjs +56 -29
  43. package/src/ExitOnCaughtUp.res +10 -2
  44. package/src/ExitOnCaughtUp.res.mjs +10 -1
  45. package/src/FetchState.res +684 -415
  46. package/src/FetchState.res.mjs +505 -486
  47. package/src/HandlerLoader.res +8 -104
  48. package/src/HandlerLoader.res.mjs +2 -81
  49. package/src/HandlerRegister.res +520 -144
  50. package/src/HandlerRegister.res.mjs +286 -133
  51. package/src/HandlerRegister.resi +24 -8
  52. package/src/IndexerState.res +10 -3
  53. package/src/IndexerState.res.mjs +11 -4
  54. package/src/IndexerState.resi +2 -1
  55. package/src/IndexingAddresses.res +108 -0
  56. package/src/IndexingAddresses.res.mjs +101 -0
  57. package/src/IndexingAddresses.resi +34 -0
  58. package/src/Internal.res +166 -48
  59. package/src/Internal.res.mjs +30 -2
  60. package/src/LoadLayer.res +5 -5
  61. package/src/LoadLayer.res.mjs +6 -6
  62. package/src/LogSelection.res +92 -217
  63. package/src/LogSelection.res.mjs +95 -184
  64. package/src/Main.res +71 -236
  65. package/src/Main.res.mjs +60 -145
  66. package/src/Metrics.res +74 -0
  67. package/src/Metrics.res.mjs +75 -0
  68. package/src/PgStorage.res +4 -4
  69. package/src/PgStorage.res.mjs +5 -5
  70. package/src/Prometheus.res +10 -63
  71. package/src/Prometheus.res.mjs +124 -171
  72. package/src/PruneStaleHistory.res +2 -2
  73. package/src/PruneStaleHistory.res.mjs +3 -3
  74. package/src/RawEvent.res +7 -2
  75. package/src/RawEvent.res.mjs +4 -4
  76. package/src/Rollback.res +3 -3
  77. package/src/Rollback.res.mjs +4 -4
  78. package/src/RollbackCommit.res +4 -1
  79. package/src/RollbackCommit.res.mjs +3 -2
  80. package/src/SimulateDeadInputTracker.res +85 -0
  81. package/src/SimulateDeadInputTracker.res.mjs +73 -0
  82. package/src/SimulateDeadInputTracker.resi +12 -0
  83. package/src/SimulateItems.res +128 -28
  84. package/src/SimulateItems.res.mjs +82 -40
  85. package/src/TestIndexer.res +55 -41
  86. package/src/TestIndexer.res.mjs +39 -32
  87. package/src/TopicFilter.res +1 -25
  88. package/src/TopicFilter.res.mjs +0 -74
  89. package/src/bindings/Performance.res +7 -0
  90. package/src/bindings/Performance.res.mjs +21 -0
  91. package/src/bindings/Performance.resi +7 -0
  92. package/src/bindings/Viem.res +0 -46
  93. package/src/bindings/Viem.res.mjs +1 -43
  94. package/src/sources/BlockStore.res +46 -0
  95. package/src/sources/BlockStore.res.mjs +24 -0
  96. package/src/sources/EventRouter.res +31 -49
  97. package/src/sources/EventRouter.res.mjs +12 -25
  98. package/src/sources/Evm.res +83 -72
  99. package/src/sources/Evm.res.mjs +53 -69
  100. package/src/sources/EvmChain.res +7 -34
  101. package/src/sources/EvmChain.res.mjs +6 -27
  102. package/src/sources/EvmRpcClient.res +71 -5
  103. package/src/sources/EvmRpcClient.res.mjs +13 -4
  104. package/src/sources/FieldMask.res +39 -0
  105. package/src/sources/FieldMask.res.mjs +42 -0
  106. package/src/sources/Fuel.res +9 -6
  107. package/src/sources/Fuel.res.mjs +5 -10
  108. package/src/sources/HyperFuelSource.res +46 -50
  109. package/src/sources/HyperFuelSource.res.mjs +64 -54
  110. package/src/sources/HyperSync.res +45 -42
  111. package/src/sources/HyperSync.res.mjs +68 -60
  112. package/src/sources/HyperSync.resi +14 -4
  113. package/src/sources/HyperSyncClient.res +107 -119
  114. package/src/sources/HyperSyncClient.res.mjs +36 -31
  115. package/src/sources/HyperSyncHeightStream.res +1 -8
  116. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  117. package/src/sources/HyperSyncSource.res +60 -254
  118. package/src/sources/HyperSyncSource.res.mjs +57 -180
  119. package/src/sources/Rpc.res +15 -79
  120. package/src/sources/Rpc.res.mjs +25 -101
  121. package/src/sources/RpcSource.res +221 -514
  122. package/src/sources/RpcSource.res.mjs +216 -411
  123. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  124. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  125. package/src/sources/SimulateSource.res +44 -20
  126. package/src/sources/SimulateSource.res.mjs +41 -14
  127. package/src/sources/Source.res +34 -6
  128. package/src/sources/SourceManager.res +77 -19
  129. package/src/sources/SourceManager.res.mjs +63 -21
  130. package/src/sources/SourceManager.resi +10 -0
  131. package/src/sources/Svm.res +31 -16
  132. package/src/sources/Svm.res.mjs +36 -13
  133. package/src/sources/SvmHyperSyncClient.res +5 -32
  134. package/src/sources/SvmHyperSyncSource.res +159 -134
  135. package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
  136. package/src/sources/TransactionStore.res +49 -0
  137. package/src/sources/TransactionStore.res.mjs +30 -0
  138. package/src/tui/Tui.res +13 -16
  139. package/src/tui/Tui.res.mjs +12 -14
  140. package/svm.schema.json +49 -37
  141. package/src/bindings/Hrtime.res +0 -58
  142. package/src/bindings/Hrtime.res.mjs +0 -90
  143. package/src/bindings/Hrtime.resi +0 -30
@@ -18,10 +18,10 @@ function convertFieldsToJson(fields) {
18
18
  }
19
19
  }
20
20
 
21
- function make(eventItem, block, transaction, params, srcAddress, cleanUpRawEventFieldsInPlace) {
21
+ function make(eventItem, block, transaction, params, srcAddress, blockHash, blockTimestamp, cleanUpRawEventFieldsInPlace) {
22
22
  let logIndex = eventItem.logIndex;
23
23
  let blockNumber = eventItem.blockNumber;
24
- let eventConfig = eventItem.eventConfig;
24
+ let eventConfig = eventItem.onEventRegistration.eventConfig;
25
25
  let eventId = EventUtils.packEventIndex(blockNumber, logIndex);
26
26
  let blockFields = convertFieldsToJson(block);
27
27
  let transactionFields = convertFieldsToJson(transaction);
@@ -36,8 +36,8 @@ function make(eventItem, block, transaction, params, srcAddress, cleanUpRawEvent
36
36
  block_number: blockNumber,
37
37
  log_index: logIndex,
38
38
  src_address: srcAddress,
39
- block_hash: eventItem.blockHash,
40
- block_timestamp: eventItem.timestamp,
39
+ block_hash: blockHash,
40
+ block_timestamp: blockTimestamp,
41
41
  block_fields: blockFields,
42
42
  transaction_fields: transactionFields,
43
43
  params: params$2
package/src/Rollback.res CHANGED
@@ -18,7 +18,7 @@ let getLastKnownValidBlock = async (
18
18
  ->ChainState.reorgDetection
19
19
  ->ReorgDetection.getThresholdBlockNumbersBelowBlock(
20
20
  ~blockNumber=reorgBlockNumber,
21
- ~knownHeight=(chainState->ChainState.fetchState).knownHeight,
21
+ ~knownHeight=chainState->ChainState.knownHeight,
22
22
  )
23
23
 
24
24
  switch scannedBlockNumbers {
@@ -94,7 +94,7 @@ and executeRollback = async (
94
94
  ~scheduleFetch,
95
95
  ~scheduleProcessing,
96
96
  ) => {
97
- let startTime = Hrtime.makeTimer()
97
+ let startTime = Performance.now()
98
98
 
99
99
  let chainState = state->IndexerState.getChainState(~chain=reorgChain)
100
100
 
@@ -194,7 +194,7 @@ and executeRollback = async (
194
194
  "targetCheckpointId": rollbackTargetCheckpointId,
195
195
  })
196
196
  Prometheus.RollbackSuccess.increment(
197
- ~timeSeconds=Hrtime.timeSince(startTime)->Hrtime.toSecondsFloat,
197
+ ~timeSeconds=Performance.secondsSince(startTime),
198
198
  ~rollbackedProcessedEvents=rollbackedProcessedEvents.contents,
199
199
  )
200
200
 
@@ -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 Logging from "./Logging.res.mjs";
6
5
  import * as Writing from "./Writing.res.mjs";
7
6
  import * as ChainState from "./ChainState.res.mjs";
8
7
  import * as Prometheus from "./Prometheus.res.mjs";
8
+ import * as Performance from "./bindings/Performance.res.mjs";
9
9
  import * as IndexerState from "./IndexerState.res.mjs";
10
10
  import * as Stdlib_Float from "@rescript/runtime/lib/es6/Stdlib_Float.js";
11
11
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
@@ -18,7 +18,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
18
18
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
19
19
 
20
20
  async function getLastKnownValidBlock(chainState, reorgBlockNumber, isRealtime) {
21
- let scannedBlockNumbers = ReorgDetection.getThresholdBlockNumbersBelowBlock(ChainState.reorgDetection(chainState), reorgBlockNumber, ChainState.fetchState(chainState).knownHeight);
21
+ let scannedBlockNumbers = ReorgDetection.getThresholdBlockNumbersBelowBlock(ChainState.reorgDetection(chainState), reorgBlockNumber, ChainState.knownHeight(chainState));
22
22
  if (scannedBlockNumbers.length === 0) {
23
23
  return ChainState.getHighestBlockBelowThreshold(chainState);
24
24
  }
@@ -32,7 +32,7 @@ async function getLastKnownValidBlock(chainState, reorgBlockNumber, isRealtime)
32
32
  }
33
33
 
34
34
  async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, scheduleFetch, scheduleProcessing) {
35
- let startTime = Hrtime.makeTimer();
35
+ let startTime = Performance.now();
36
36
  let chainState = IndexerState.getChainState(state, reorgChain);
37
37
  let logger = Logging.createChildFrom(ChainState.logger(chainState), {
38
38
  action: "Rollback",
@@ -70,7 +70,7 @@ async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, sch
70
70
  beforeCheckpointId: IndexerState.committedCheckpointId(state),
71
71
  targetCheckpointId: rollbackTargetCheckpointId
72
72
  });
73
- Prometheus.RollbackSuccess.increment(Hrtime.toSecondsFloat(Hrtime.timeSince(startTime)), rollbackedProcessedEvents);
73
+ Prometheus.RollbackSuccess.increment(Performance.secondsSince(startTime), rollbackedProcessedEvents);
74
74
  IndexerState.completeRollback(state, eventsProcessedDiffByChain);
75
75
  scheduleFetch();
76
76
  return scheduleProcessing();
@@ -6,7 +6,10 @@
6
6
  type args = {chainId: int, rollbackToBlock: int}
7
7
  type callback = args => promise<unit>
8
8
 
9
- let callbacks: array<callback> = []
9
+ // Lives in the process-wide `EnvioGlobal` record so callbacks registered
10
+ // through a duplicate envio module instance still fire.
11
+ let callbacks =
12
+ EnvioGlobal.value.rollbackCommitCallbacks->(Utils.magic: array<unknown> => array<callback>)
10
13
 
11
14
  let register = (callback: callback) => {
12
15
  callbacks->Array.push(callback)
@@ -1,8 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
4
5
 
5
- let callbacks = [];
6
+ let callbacks = EnvioGlobal.value.rollbackCommitCallbacks;
6
7
 
7
8
  function register(callback) {
8
9
  callbacks.push(callback);
@@ -32,4 +33,4 @@ export {
32
33
  register,
33
34
  fire,
34
35
  }
35
- /* No side effect */
36
+ /* EnvioGlobal Not a pure module */
@@ -0,0 +1,85 @@
1
+ // Lives on IndexerState and is fed each processed batch, so ChainState and the
2
+ // fetch loop carry no simulate-specific state.
3
+
4
+ // Match a provided item to a processed one by its (chain, block, logIndex)
5
+ // coordinate rather than object identity, so matching survives any copy or
6
+ // transform of the item between the source and the batch.
7
+ let itemKey = (item: Internal.item): string =>
8
+ switch item {
9
+ | Internal.Event({chain, blockNumber, logIndex}) =>
10
+ `${chain
11
+ ->ChainMap.Chain.toChainId
12
+ ->Int.toString}:${blockNumber->Int.toString}:${logIndex->Int.toString}`
13
+ | Internal.Block(_) => ""
14
+ }
15
+
16
+ // `index` is the item's position in its chain's `simulate` array, reported back
17
+ // so a user finds it without echoing its fields.
18
+ type entry = {chainId: int, index: int, key: string}
19
+
20
+ type t = {mutable unprocessed: array<entry>}
21
+
22
+ let makeFromConfig = (config: Config.t): option<t> => {
23
+ let entries =
24
+ config.chainMap
25
+ ->ChainMap.values
26
+ ->Array.flatMap(chainConfig =>
27
+ switch chainConfig.sourceConfig {
28
+ | Config.CustomSources(sources) =>
29
+ sources->Array.flatMap(source =>
30
+ source.simulateItems
31
+ ->Option.getOr([])
32
+ ->Array.mapWithIndex(
33
+ (item, index) => {
34
+ chainId: chainConfig.id,
35
+ index,
36
+ key: itemKey(item),
37
+ },
38
+ )
39
+ )
40
+ | _ => []
41
+ }
42
+ )
43
+ switch entries {
44
+ | [] => None
45
+ | _ => Some({unprocessed: entries})
46
+ }
47
+ }
48
+
49
+ let recordProcessed = (t: t, ~batch: Batch.t) => {
50
+ let processedKeys = batch.items->Array.map(itemKey)->Utils.Set.fromArray
51
+ t.unprocessed = t.unprocessed->Array.filter(entry => !(processedKeys->Utils.Set.has(entry.key)))
52
+ }
53
+
54
+ // Unrouted item indices grouped by chain, in the order chains were first seen.
55
+ let unroutedByChain = (t: t): array<(int, array<int>)> => {
56
+ let indicesByChain = Dict.make()
57
+ let chainOrder = []
58
+ t.unprocessed->Array.forEach(entry => {
59
+ let key = entry.chainId->Int.toString
60
+ if indicesByChain->Dict.get(key)->Option.isNone {
61
+ chainOrder->Array.push(entry.chainId)->ignore
62
+ }
63
+ indicesByChain->Utils.Dict.push(key, entry.index)
64
+ })
65
+ chainOrder->Array.map(chainId => (chainId, indicesByChain->Dict.getUnsafe(chainId->Int.toString)))
66
+ }
67
+
68
+ let failureMessage = (t: t): option<string> =>
69
+ switch t->unroutedByChain {
70
+ | [] => None
71
+ | byChain =>
72
+ let count = byChain->Array.reduce(0, (acc, (_chainId, indices)) => acc + indices->Array.length)
73
+ let itemWord = count === 1 ? "item" : "items"
74
+ let lines =
75
+ byChain
76
+ ->Array.map(((chainId, indices)) =>
77
+ ` - chain ${chainId->Int.toString}: ${indices
78
+ ->Array.map(index => index->Int.toString)
79
+ ->Array.join(", ")}`
80
+ )
81
+ ->Array.join("\n")
82
+ Some(
83
+ `simulate: ${count->Int.toString} ${itemWord} you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n${lines}`,
84
+ )
85
+ }
@@ -0,0 +1,73 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Utils from "./Utils.res.mjs";
4
+ import * as ChainMap from "./ChainMap.res.mjs";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+
8
+ function itemKey(item) {
9
+ if (item.kind === 0) {
10
+ return item.chain.toString() + `:` + item.blockNumber.toString() + `:` + item.logIndex.toString();
11
+ } else {
12
+ return "";
13
+ }
14
+ }
15
+
16
+ function makeFromConfig(config) {
17
+ let entries = ChainMap.values(config.chainMap).flatMap(chainConfig => {
18
+ let sources = chainConfig.sourceConfig;
19
+ if (sources.TAG === "CustomSources") {
20
+ return sources._0.flatMap(source => Stdlib_Option.getOr(source.simulateItems, []).map((item, index) => ({
21
+ chainId: chainConfig.id,
22
+ index: index,
23
+ key: itemKey(item)
24
+ })));
25
+ } else {
26
+ return [];
27
+ }
28
+ });
29
+ if (entries.length !== 0) {
30
+ return {
31
+ unprocessed: entries
32
+ };
33
+ }
34
+ }
35
+
36
+ function recordProcessed(t, batch) {
37
+ let processedKeys = new Set(batch.items.map(itemKey));
38
+ t.unprocessed = t.unprocessed.filter(entry => !processedKeys.has(entry.key));
39
+ }
40
+
41
+ function unroutedByChain(t) {
42
+ let indicesByChain = {};
43
+ let chainOrder = [];
44
+ t.unprocessed.forEach(entry => {
45
+ let key = entry.chainId.toString();
46
+ if (Stdlib_Option.isNone(indicesByChain[key])) {
47
+ chainOrder.push(entry.chainId);
48
+ }
49
+ Utils.Dict.push(indicesByChain, key, entry.index);
50
+ });
51
+ return chainOrder.map(chainId => [
52
+ chainId,
53
+ indicesByChain[chainId.toString()]
54
+ ]);
55
+ }
56
+
57
+ function failureMessage(t) {
58
+ let byChain = unroutedByChain(t);
59
+ if (byChain.length === 0) {
60
+ return;
61
+ }
62
+ let count = Stdlib_Array.reduce(byChain, 0, (acc, param) => acc + param[1].length | 0);
63
+ let itemWord = count === 1 ? "item" : "items";
64
+ let lines = byChain.map(param => ` - chain ` + param[0].toString() + `: ` + param[1].map(index => index.toString()).join(", ")).join("\n");
65
+ return `simulate: ` + count.toString() + ` ` + itemWord + ` you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n` + lines;
66
+ }
67
+
68
+ export {
69
+ makeFromConfig,
70
+ recordProcessed,
71
+ failureMessage,
72
+ }
73
+ /* Utils Not a pure module */
@@ -0,0 +1,12 @@
1
+ type t
2
+
3
+ // Builds a tracker from the run's config, seeded with every item each simulate
4
+ // source provided. None when no chain has a simulate source.
5
+ let makeFromConfig: Config.t => option<t>
6
+
7
+ // Drop the batch's items from the not-yet-routed set, matched by event coordinate.
8
+ let recordProcessed: (t, ~batch: Batch.t) => unit
9
+
10
+ // The error to fail the run with when items never reached a handler, listing them
11
+ // by chain and index within that chain's `simulate` array. None when all routed.
12
+ let failureMessage: t => option<string>
@@ -195,9 +195,57 @@ let findEventConfig = (~config: Config.t, ~contractName: string, ~eventName: str
195
195
  found.contents
196
196
  }
197
197
 
198
- let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Config.chain): array<
199
- Internal.item,
198
+ let dummySrcAddress = Address.unsafeFromString("0x0000000000000000000000000000000000000000")
199
+
200
+ // First address configured for `contractName` on the simulated chain. Only the
201
+ // simulated chain is consulted — a contract's address on another chain has no
202
+ // meaning here.
203
+ let firstContractAddress = (~chainConfig: Config.chain, ~contractName: string): option<
204
+ Address.t,
200
205
  > => {
206
+ let found = ref(None)
207
+ chainConfig.contracts->Array.forEach(contract => {
208
+ if found.contents->Option.isNone && contract.name === contractName {
209
+ found := contract.addresses->Array.get(0)
210
+ }
211
+ })
212
+ found.contents
213
+ }
214
+
215
+ let deriveSrcAddress = (
216
+ ~providedSrcAddress: option<Address.t>,
217
+ ~eventConfig: Internal.eventConfig,
218
+ ~chainConfig: Config.chain,
219
+ ~config: Config.t,
220
+ ): Address.t => {
221
+ switch providedSrcAddress {
222
+ // Canonicalize to the configured casing; the fallback addresses below already
223
+ // come from the parsed config and need no normalization. Relaxed (not
224
+ // Config.normalizeUserAddress) so test placeholders like "0xfoo" are allowed.
225
+ | Some(addr) => config->Config.normalizeSimulateAddress(addr)
226
+ | None =>
227
+ if (
228
+ HandlerRegister.isWildcard(
229
+ ~contractName=eventConfig.contractName,
230
+ ~eventName=eventConfig.name,
231
+ )
232
+ ) {
233
+ dummySrcAddress
234
+ } else {
235
+ switch firstContractAddress(~chainConfig, ~contractName=eventConfig.contractName) {
236
+ | Some(addr) => addr
237
+ | None => dummySrcAddress
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ let parse = (
244
+ ~simulateItems: array<JSON.t>,
245
+ ~config: Config.t,
246
+ ~chainConfig: Config.chain,
247
+ ~onEventRegistrations: array<Internal.onEventRegistration>,
248
+ ): array<Internal.item> => {
201
249
  let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
202
250
  let chainId = chainConfig.id
203
251
  let startBlock = chainConfig.startBlock
@@ -205,8 +253,11 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
205
253
  let currentLogIndex = ref(0)
206
254
 
207
255
  let items = []
256
+ // Coordinate "block:logIndex" -> the index of the first item that claimed it,
257
+ // used to reject two items resolving to the same (block, logIndex).
258
+ let seenCoordinates = Dict.make()
208
259
 
209
- simulateItems->Array.forEach(rawJson => {
260
+ simulateItems->Array.forEachWithIndex((rawJson, itemIndex) => {
210
261
  let raw = rawJson->(Utils.magic: JSON.t => rawSimulateItem)
211
262
 
212
263
  switch (raw->getContract, raw->getEvent) {
@@ -230,44 +281,41 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
230
281
  }
231
282
  let params = paramsJson->S.convertOrThrow(eventConfig.simulateParamsSchema)
232
283
 
284
+ // An explicit logIndex advances the auto-increment counter past itself, so a
285
+ // later item that omits logIndex picks up after it instead of colliding.
233
286
  let logIndex = switch item.logIndex {
234
- | Some(li) => li
287
+ | Some(li) =>
288
+ if li >= currentLogIndex.contents {
289
+ currentLogIndex := li + 1
290
+ }
291
+ li
235
292
  | None =>
236
293
  let li = currentLogIndex.contents
237
294
  currentLogIndex := li + 1
238
295
  li
239
296
  }
240
297
 
241
- let srcAddress = switch item.srcAddress {
242
- | Some(addr) => addr
243
- | None =>
244
- // Use first address from contract config
245
- let addr = ref(Address.unsafeFromString("0x0000000000000000000000000000000000000000"))
246
- chainConfig.contracts->Array.forEach(contract => {
247
- if contract.name === contractName {
248
- switch contract.addresses->Array.get(0) {
249
- | Some(a) => addr := a
250
- | None => ()
251
- }
252
- }
253
- })
254
- addr.contents
255
- }
298
+ let srcAddress = deriveSrcAddress(
299
+ ~providedSrcAddress=item.srcAddress,
300
+ ~eventConfig,
301
+ ~chainConfig,
302
+ ~config,
303
+ )
256
304
 
257
305
  let rawItem = rawJson->(Utils.magic: JSON.t => {..})
258
306
  let blockJson: option<JSON.t> =
259
307
  rawItem["block"]->(Utils.magic: 'a => Nullable.t<JSON.t>)->Nullable.toOption
260
308
  let transactionJson: option<JSON.t> =
261
309
  rawItem["transaction"]->(Utils.magic: 'a => Nullable.t<JSON.t>)->Nullable.toOption
262
- let (block, blockNumber, timestamp, blockHash) = switch config.ecosystem.name {
310
+ let (block, blockNumber) = switch config.ecosystem.name {
263
311
  | Fuel =>
264
312
  let block = parseFuelSimulateBlock(~defaultBlockNumber=currentBlock.contents, ~blockJson)
265
313
  let blockFields = block->(Utils.magic: Internal.eventBlock => fuelSimulateBlock)
266
- (block, blockFields.height, blockFields.time, blockFields.id)
314
+ (block, blockFields.height)
267
315
  | Evm =>
268
316
  let block = parseEvmSimulateBlock(~defaultBlockNumber=currentBlock.contents, ~blockJson)
269
317
  let blockFields = block->(Utils.magic: Internal.eventBlock => evmSimulateBlock)
270
- (block, blockFields.number, blockFields.timestamp, blockFields.hash)
318
+ (block, blockFields.number)
271
319
  | Svm => JsError.throwWithMessage("simulate is not supported for SVM ecosystem")
272
320
  }
273
321
  let transaction = switch config.ecosystem.name {
@@ -279,15 +327,43 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
279
327
  // Update currentBlock for subsequent items
280
328
  currentBlock := blockNumber
281
329
 
330
+ // A simulate item must land on a distinct (block, logIndex): event ordering
331
+ // and the dead-input tracker both key on it. Catch explicit duplicates and
332
+ // explicit-vs-auto-increment collisions here, naming both offending indices.
333
+ let coordinate = `${blockNumber->Int.toString}:${logIndex->Int.toString}`
334
+ switch seenCoordinates->Dict.get(coordinate) {
335
+ | Some(firstIndex) =>
336
+ JsError.throwWithMessage(
337
+ `simulate: items at index ${firstIndex->Int.toString} and ${itemIndex->Int.toString} on chain ${chainId->Int.toString} both resolve to block ${blockNumber->Int.toString}, logIndex ${logIndex->Int.toString}. Give each item a distinct logIndex (or omit logIndex so they auto-increment).`,
338
+ )
339
+ | None => seenCoordinates->Dict.set(coordinate, itemIndex)
340
+ }
341
+
342
+ // Build a real registration the same way `HandlerRegister.finishRegistration`
343
+ // does at startup (not a stub), so the address filter and `where`
344
+ // behave identically to real indexing — the dead-input tracker relies
345
+ // on `clientAddressFilter` actually gating unrouted items.
346
+ let onEventRegistration = HandlerRegister.buildOnEventRegistration(
347
+ ~config,
348
+ ~chainId,
349
+ ~eventConfig,
350
+ )
351
+ // Append into the registration array that the chain state will own and
352
+ // put that same registration object directly on the simulated item.
353
+ let onEventRegistrationIndex = onEventRegistrations->Array.length
354
+ let onEventRegistration = {...onEventRegistration, index: onEventRegistrationIndex}
355
+ onEventRegistrations->Array.push(onEventRegistration)->ignore
356
+
282
357
  items
283
358
  ->Array.push(
284
359
  Internal.Event({
285
- eventConfig,
286
- timestamp,
360
+ onEventRegistration,
287
361
  chain,
288
362
  blockNumber,
289
- blockHash,
290
363
  logIndex,
364
+ // Simulate keeps the transaction inline on the payload, so the store
365
+ // key is unused.
366
+ transactionIndex: 0,
291
367
  payload: (
292
368
  {
293
369
  contractName: eventConfig.contractName,
@@ -314,7 +390,11 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
314
390
 
315
391
  // Apply simulate source config from processConfig JSON to a Config.t
316
392
  // This patches chainMap entries that have simulate items with CustomSources
317
- let patchConfig = (~config: Config.t, ~processConfig: JSON.t): Config.t => {
393
+ let patchConfig = (
394
+ ~config: Config.t,
395
+ ~processConfig: JSON.t,
396
+ ~registrationsByChainId: HandlerRegister.registrationsByChainId,
397
+ ): Config.t => {
318
398
  let processChains: option<dict<JSON.t>> =
319
399
  (processConfig->(Utils.magic: JSON.t => {..}))["chains"]->Nullable.toOption
320
400
  switch processChains {
@@ -327,11 +407,31 @@ let patchConfig = (~config: Config.t, ~processConfig: JSON.t): Config.t => {
327
407
  let simulateRaw: option<array<JSON.t>> = raw["simulate"]->Nullable.toOption
328
408
  switch simulateRaw {
329
409
  | Some(simulateItems) =>
330
- let items = parse(~simulateItems, ~config, ~chainConfig)
410
+ let chainRegistrations = switch registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(
411
+ chainIdStr,
412
+ ) {
413
+ | Some(registrations) => registrations
414
+ | None =>
415
+ let registrations: HandlerRegister.chainRegistrations = {
416
+ onEventRegistrations: [],
417
+ onBlockRegistrations: [],
418
+ }
419
+ registrationsByChainId->Dict.set(chainIdStr, registrations)
420
+ registrations
421
+ }
331
422
  let startBlock: int = raw["startBlock"]->(Utils.magic: 'a => int)
332
423
  let endBlock: int = raw["endBlock"]->(Utils.magic: 'a => int)
424
+ // Parse with the process's startBlock so items default into the range
425
+ // the source will be queried over; the source now filters by range.
426
+ let chainConfig = {...chainConfig, startBlock, endBlock}
427
+ let items = parse(
428
+ ~simulateItems,
429
+ ~config,
430
+ ~chainConfig,
431
+ ~onEventRegistrations=chainRegistrations.onEventRegistrations,
432
+ )
333
433
  let source = SimulateSource.make(~items, ~endBlock, ~chain)
334
- {...chainConfig, startBlock, endBlock, sourceConfig: Config.CustomSources([source])}
434
+ {...chainConfig, sourceConfig: Config.CustomSources([source])}
335
435
  | None => chainConfig
336
436
  }
337
437
  | None => chainConfig