envio 3.3.0-alpha.1 → 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 (136) 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 +43 -9
  15. package/src/ChainFetching.res.mjs +34 -15
  16. package/src/ChainMetadata.res +1 -11
  17. package/src/ChainMetadata.res.mjs +1 -10
  18. package/src/ChainState.res +605 -151
  19. package/src/ChainState.res.mjs +427 -104
  20. package/src/ChainState.resi +72 -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 +113 -89
  27. package/src/CrossChainState.res.mjs +64 -60
  28. package/src/CrossChainState.resi +1 -1
  29. package/src/Ecosystem.res +5 -6
  30. package/src/Ecosystem.res.mjs +3 -3
  31. package/src/Envio.res +17 -11
  32. package/src/EnvioGlobal.res +53 -0
  33. package/src/EnvioGlobal.res.mjs +31 -0
  34. package/src/EventConfigBuilder.res +177 -88
  35. package/src/EventConfigBuilder.res.mjs +99 -42
  36. package/src/EventProcessing.res +74 -28
  37. package/src/EventProcessing.res.mjs +55 -29
  38. package/src/ExitOnCaughtUp.res +10 -2
  39. package/src/ExitOnCaughtUp.res.mjs +10 -1
  40. package/src/FetchState.res +544 -356
  41. package/src/FetchState.res.mjs +412 -471
  42. package/src/HandlerLoader.res +8 -104
  43. package/src/HandlerLoader.res.mjs +2 -81
  44. package/src/HandlerRegister.res +518 -144
  45. package/src/HandlerRegister.res.mjs +285 -133
  46. package/src/HandlerRegister.resi +24 -8
  47. package/src/IndexerState.res +10 -3
  48. package/src/IndexerState.res.mjs +11 -4
  49. package/src/IndexerState.resi +2 -1
  50. package/src/IndexingAddresses.res +108 -0
  51. package/src/IndexingAddresses.res.mjs +101 -0
  52. package/src/IndexingAddresses.resi +34 -0
  53. package/src/Internal.res +155 -46
  54. package/src/Internal.res.mjs +30 -2
  55. package/src/LoadLayer.res +5 -5
  56. package/src/LoadLayer.res.mjs +6 -6
  57. package/src/LogSelection.res +102 -165
  58. package/src/LogSelection.res.mjs +101 -116
  59. package/src/Main.res +71 -236
  60. package/src/Main.res.mjs +60 -145
  61. package/src/Metrics.res +74 -0
  62. package/src/Metrics.res.mjs +75 -0
  63. package/src/PgStorage.res +4 -4
  64. package/src/PgStorage.res.mjs +5 -5
  65. package/src/Prometheus.res +10 -63
  66. package/src/Prometheus.res.mjs +124 -171
  67. package/src/PruneStaleHistory.res +2 -2
  68. package/src/PruneStaleHistory.res.mjs +3 -3
  69. package/src/RawEvent.res +7 -2
  70. package/src/RawEvent.res.mjs +4 -4
  71. package/src/Rollback.res +3 -3
  72. package/src/Rollback.res.mjs +4 -4
  73. package/src/RollbackCommit.res +4 -1
  74. package/src/RollbackCommit.res.mjs +3 -2
  75. package/src/SimulateDeadInputTracker.res +85 -0
  76. package/src/SimulateDeadInputTracker.res.mjs +73 -0
  77. package/src/SimulateDeadInputTracker.resi +12 -0
  78. package/src/SimulateItems.res +91 -23
  79. package/src/SimulateItems.res.mjs +55 -32
  80. package/src/TestIndexer.res +54 -40
  81. package/src/TestIndexer.res.mjs +37 -30
  82. package/src/bindings/Performance.res +7 -0
  83. package/src/bindings/Performance.res.mjs +21 -0
  84. package/src/bindings/Performance.resi +7 -0
  85. package/src/bindings/Viem.res +0 -41
  86. package/src/bindings/Viem.res.mjs +1 -43
  87. package/src/sources/BlockStore.res +46 -0
  88. package/src/sources/BlockStore.res.mjs +24 -0
  89. package/src/sources/EventRouter.res +36 -33
  90. package/src/sources/EventRouter.res.mjs +13 -14
  91. package/src/sources/Evm.res +83 -72
  92. package/src/sources/Evm.res.mjs +53 -69
  93. package/src/sources/EvmChain.res +18 -20
  94. package/src/sources/EvmChain.res.mjs +15 -15
  95. package/src/sources/EvmRpcClient.res +74 -5
  96. package/src/sources/EvmRpcClient.res.mjs +13 -4
  97. package/src/sources/FieldMask.res +39 -0
  98. package/src/sources/FieldMask.res.mjs +42 -0
  99. package/src/sources/Fuel.res +9 -6
  100. package/src/sources/Fuel.res.mjs +5 -10
  101. package/src/sources/HyperFuelSource.res +46 -50
  102. package/src/sources/HyperFuelSource.res.mjs +64 -54
  103. package/src/sources/HyperSync.res +39 -7
  104. package/src/sources/HyperSync.res.mjs +57 -37
  105. package/src/sources/HyperSync.resi +12 -2
  106. package/src/sources/HyperSyncClient.res +22 -111
  107. package/src/sources/HyperSyncClient.res.mjs +2 -30
  108. package/src/sources/HyperSyncHeightStream.res +1 -8
  109. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  110. package/src/sources/HyperSyncSource.res +94 -107
  111. package/src/sources/HyperSyncSource.res.mjs +82 -86
  112. package/src/sources/Rpc.res +15 -47
  113. package/src/sources/Rpc.res.mjs +25 -56
  114. package/src/sources/RpcSource.res +320 -467
  115. package/src/sources/RpcSource.res.mjs +268 -385
  116. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  117. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  118. package/src/sources/SimulateSource.res +10 -4
  119. package/src/sources/SimulateSource.res.mjs +16 -6
  120. package/src/sources/Source.res +34 -6
  121. package/src/sources/SourceManager.res +77 -19
  122. package/src/sources/SourceManager.res.mjs +63 -21
  123. package/src/sources/SourceManager.resi +10 -0
  124. package/src/sources/Svm.res +31 -16
  125. package/src/sources/Svm.res.mjs +36 -13
  126. package/src/sources/SvmHyperSyncClient.res +5 -32
  127. package/src/sources/SvmHyperSyncSource.res +159 -134
  128. package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
  129. package/src/sources/TransactionStore.res +49 -0
  130. package/src/sources/TransactionStore.res.mjs +30 -0
  131. package/src/tui/Tui.res +13 -16
  132. package/src/tui/Tui.res.mjs +12 -14
  133. package/svm.schema.json +49 -37
  134. package/src/bindings/Hrtime.res +0 -58
  135. package/src/bindings/Hrtime.res.mjs +0 -90
  136. package/src/bindings/Hrtime.resi +0 -30
@@ -1,7 +1,5 @@
1
1
  open Source
2
2
 
3
- exception QueryTimout(string)
4
-
5
3
  // eth_getTransactionByHash/eth_getTransactionReceipt returning null is usually
6
4
  // transient: a load-balanced provider can route the lookup to a node that
7
5
  // hasn't caught up with the one that served eth_getLogs. Must stay retryable,
@@ -48,18 +46,16 @@ let getKnownRawBlockWithBackoff = async (
48
46
  ~chain,
49
47
  ~blockNumber,
50
48
  ~backoffMsOnFailure,
49
+ ~recordRequest: (~method: string, ~seconds: float) => unit,
51
50
  ) => {
52
51
  let currentBackoff = ref(backoffMsOnFailure)
53
52
  let result = ref(None)
54
53
 
55
54
  while result.contents->Option.isNone {
56
- Prometheus.SourceRequestCount.increment(
57
- ~sourceName,
58
- ~chainId=chain->ChainMap.Chain.toChainId,
59
- ~method="eth_getBlockByNumber",
60
- )
55
+ let timerRef = Performance.now()
61
56
  switch await getKnownRawBlock(~client, ~blockNumber) {
62
57
  | exception err =>
58
+ recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
63
59
  Logging.warn({
64
60
  "err": err->Utils.prettifyExn,
65
61
  "msg": `Issue while running fetching batch of events from the RPC. Will wait ${currentBackoff.contents->Int.toString}ms and try again.`,
@@ -69,327 +65,217 @@ let getKnownRawBlockWithBackoff = async (
69
65
  })
70
66
  await Time.resolvePromiseAfterDelay(~delayMilliseconds=currentBackoff.contents)
71
67
  currentBackoff := currentBackoff.contents * 2
72
- | json => result := Some(json)
68
+ | json =>
69
+ recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
70
+ result := Some(json)
73
71
  }
74
72
  }
75
73
  result.contents->Option.getOrThrow
76
74
  }
77
- let getSuggestedBlockIntervalFromExn = {
78
- // Unknown provider: "retry with the range 123-456"
79
- let suggestedRangeRegExp = /retry with the range (\d+)-(\d+)/
80
-
81
- // QuickNode, 1RPC, Blast: "limited to a 1000 blocks range"
82
- let blockRangeLimitRegExp = /limited to a (\d+) blocks range/
83
-
84
- // Alchemy: "up to a 500 block range"
85
- let alchemyRangeRegExp = /up to a (\d+) block range/
86
-
87
- // Cloudflare: "Max range: 3500"
88
- let cloudflareRangeRegExp = /Max range: (\d+)/
89
-
90
- // Thirdweb: "Maximum allowed number of requested blocks is 3500"
91
- let thirdwebRangeRegExp = /Maximum allowed number of requested blocks is (\d+)/
92
-
93
- // BlockPI: "limited to 2000 block"
94
- let blockpiRangeRegExp = /limited to (\d+) block/
95
-
96
- // Base: "block range too large" - fixed 2000 block limit
97
- let baseRangeRegExp = /block range too large/
98
-
99
- // evm-rpc.sei-apis.com: "block range too large (2000), maximum allowed is 1000 blocks"
100
- let maxAllowedBlocksRegExp = /maximum allowed is (\d+) blocks/
101
-
102
- // Blast (paid): "exceeds the range allowed for your plan (5000 > 3000)"
103
- let blastPaidRegExp = /exceeds the range allowed for your plan \(\d+ > (\d+)\)/
104
-
105
- // Chainstack: "Block range limit exceeded" - 10000 block limit
106
- let chainstackRegExp = /Block range limit exceeded./
107
-
108
- // Coinbase: "please limit the query to at most 1000 blocks"
109
- let coinbaseRegExp = /please limit the query to at most (\d+) blocks/
110
-
111
- // PublicNode: "maximum block range: 2000"
112
- let publicNodeRegExp = /maximum block range: (\d+)/
113
-
114
- // Hyperliquid: "query exceeds max block range 1000"
115
- let hyperliquidRegExp = /query exceeds max block range (\d+)/
116
-
117
- // TODO: Reproduce how the error message looks like
118
- // when we send request with numeric block range instead of hex
119
- // Infura, ZkSync: "Try with this block range [0x123,0x456]"
120
-
121
- // Future handling needed for these providers that don't suggest ranges:
122
- // - Ankr: "block range is too wide"
123
- // - 1RPC: "response size should not greater than 10000000 bytes"
124
- // - ZkEVM: "query returned more than 10000 results"
125
- // - LlamaRPC: "query exceeds max results"
126
- // - Optimism: "backend response too large" or "Block range is too large"
127
- // - Arbitrum: "logs matched by query exceeds limit of 10000"
128
-
129
- let parseMessageForBlockRange = (message: string) => {
130
- // Helper to extract block range from regex match
131
- let extractBlockRange = (execResult, ~isMaxRange) =>
132
- switch execResult->RegExp.Result.matches {
133
- | [Some(blockRangeLimit)] =>
134
- switch blockRangeLimit->Int.fromString {
135
- | Some(blockRangeLimit) if blockRangeLimit > 0 => Some(blockRangeLimit, isMaxRange)
75
+ // Pulls the underlying provider error message back out of a caught exn, for
76
+ // logging/debugging. Provider JSON-RPC errors surface as `Rpc.JsonRpcError`;
77
+ // the paging retry decision (see `parseGetNextPageRetryError` below) surfaces
78
+ // as a napi `JsExn` whose message is the JSON payload `EvmRpcClient.getNextPage`
79
+ // throws, carrying the classified message (if any) under `errorMessage`.
80
+ let getErrorMessage = (exn: exn): option<string> =>
81
+ switch exn {
82
+ | Rpc.JsonRpcError({message}) => Some(message)
83
+ | JsExn(e) =>
84
+ switch e->JsExn.message {
85
+ | Some(msg) =>
86
+ switch msg->JSON.parseOrThrow->JSON.Decode.object {
87
+ | exception _ => None
88
+ | None => None
89
+ | Some(obj) =>
90
+ switch obj->Dict.get("errorMessage") {
91
+ | Some(String(message)) => Some(message)
136
92
  | _ => None
137
93
  }
138
- | _ => None
139
94
  }
95
+ | None => None
96
+ }
97
+ | _ => None
98
+ }
140
99
 
141
- // Try each regex pattern in order
142
- switch suggestedRangeRegExp->RegExp.exec(message) {
143
- | Some(execResult) =>
144
- switch execResult->RegExp.Result.matches {
145
- | [Some(fromBlock), Some(toBlock)] =>
146
- switch (fromBlock->Int.fromString, toBlock->Int.fromString) {
147
- | (Some(fromBlock), Some(toBlock)) if toBlock >= fromBlock =>
148
- Some(toBlock - fromBlock + 1, false)
149
- | _ => None
150
- }
151
- | _ => None
152
- }
153
- | None =>
154
- // Try each provider's specific error pattern
155
- switch blockRangeLimitRegExp->RegExp.exec(message) {
156
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
157
- | None =>
158
- switch alchemyRangeRegExp->RegExp.exec(message) {
159
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
160
- | None =>
161
- switch cloudflareRangeRegExp->RegExp.exec(message) {
162
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
163
- | None =>
164
- switch thirdwebRangeRegExp->RegExp.exec(message) {
165
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
166
- | None =>
167
- switch blockpiRangeRegExp->RegExp.exec(message) {
168
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
169
- | None =>
170
- switch maxAllowedBlocksRegExp->RegExp.exec(message) {
171
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
172
- | None =>
173
- switch baseRangeRegExp->RegExp.exec(message) {
174
- | Some(_) => Some(2000, true)
175
- | None =>
176
- switch blastPaidRegExp->RegExp.exec(message) {
177
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
178
- | None =>
179
- switch chainstackRegExp->RegExp.exec(message) {
180
- | Some(_) => Some(10000, true)
181
- | None =>
182
- switch coinbaseRegExp->RegExp.exec(message) {
183
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
184
- | None =>
185
- switch publicNodeRegExp->RegExp.exec(message) {
186
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
187
- | None =>
188
- switch hyperliquidRegExp->RegExp.exec(message) {
189
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
190
- | None => None
191
- }
192
- }
193
- }
194
- }
195
- }
196
- }
100
+ type logSelection = {
101
+ addresses: option<array<Address.t>>,
102
+ topicQuery: Rpc.GetLogs.topicQuery,
103
+ }
104
+
105
+ // `EvmRpcClient.getNextPage` throws a napi error whose message is a JSON
106
+ // payload describing the retry decision:
107
+ // `{"kind":"Retry","attemptedToBlock":int,"errorMessage":string|null,
108
+ // "requestStats":[{"method":string,"seconds":float}],"retry":
109
+ // {"tag":"WithSuggestedToBlock","toBlock":int} |
110
+ // {"tag":"WithBackoff","message":string,"backoffMillis":int}}`.
111
+ let parseGetNextPageRetryError = (exn: exn): option<(
112
+ int,
113
+ Source.getItemsRetry,
114
+ array<Source.requestStat>,
115
+ )> =>
116
+ switch exn {
117
+ | JsExn(e) =>
118
+ switch e->JsExn.message {
119
+ | Some(msg) =>
120
+ switch msg->JSON.parseOrThrow->JSON.Decode.object {
121
+ | exception _ => None
122
+ | None => None
123
+ | Some(obj) =>
124
+ switch (obj->Dict.get("kind"), obj->Dict.get("attemptedToBlock"), obj->Dict.get("retry")) {
125
+ | (Some(String("Retry")), Some(Number(attemptedToBlock)), Some(Object(retryObj))) =>
126
+ let requestStats = switch obj->Dict.get("requestStats") {
127
+ | Some(Array(stats)) =>
128
+ stats->Array.filterMap(s =>
129
+ switch s->JSON.Decode.object {
130
+ | Some(o) =>
131
+ switch (o->Dict.get("method"), o->Dict.get("seconds")) {
132
+ | (Some(String(method)), Some(Number(seconds))) => Some({Source.method, seconds})
133
+ | _ => None
197
134
  }
135
+ | None => None
198
136
  }
137
+ )
138
+ | _ => []
139
+ }
140
+ let retry = switch retryObj->Dict.get("tag") {
141
+ | Some(String("WithSuggestedToBlock")) =>
142
+ switch retryObj->Dict.get("toBlock") {
143
+ | Some(Number(toBlock)) =>
144
+ Some(Source.WithSuggestedToBlock({toBlock: toBlock->Float.toInt}))
145
+ | _ => None
146
+ }
147
+ | Some(String("WithBackoff")) =>
148
+ switch (retryObj->Dict.get("message"), retryObj->Dict.get("backoffMillis")) {
149
+ | (Some(String(message)), Some(Number(backoffMillis))) =>
150
+ Some(Source.WithBackoff({message, backoffMillis: backoffMillis->Float.toInt}))
151
+ | _ => None
199
152
  }
153
+ | _ => None
200
154
  }
155
+ retry->Option.map(retry => (attemptedToBlock->Float.toInt, retry, requestStats))
156
+ | _ => None
201
157
  }
202
158
  }
159
+ | None => None
203
160
  }
161
+ | _ => None
204
162
  }
205
163
 
206
- (exn): option<(
207
- // The suggested block range
208
- int,
209
- // Whether it's the max range that the provider allows
210
- bool,
211
- )> =>
212
- switch exn {
213
- | Rpc.JsonRpcError({message}) => parseMessageForBlockRange(message)
214
- | JsExn(error) =>
215
- try {
216
- let message: string = (error->Obj.magic)["error"]["message"]
217
- message->S.assertOrThrow(S.string)
218
- parseMessageForBlockRange(message)
219
- } catch {
220
- | _ => None
221
- }
222
- | _ => None
223
- }
224
- }
225
-
226
- type eventBatchQuery = {
227
- logs: array<Rpc.GetLogs.log>,
228
- latestFetchedBlockInfo: blockInfo,
164
+ type selectionConfig = {
165
+ getLogSelectionsOrThrow: (
166
+ ~addressesByContractName: dict<array<Address.t>>,
167
+ ) => array<logSelection>,
229
168
  }
230
169
 
231
- let maxSuggestedBlockIntervalKey = "max"
232
-
233
- let getNextPage = (
234
- ~fromBlock,
235
- ~toBlock,
236
- ~addresses,
237
- ~topicQuery,
238
- ~loadBlock,
239
- ~syncConfig as sc: Config.sourceSync,
240
- ~client,
241
- ~mutSuggestedBlockIntervals,
242
- ~partitionId,
243
- ~sourceName,
244
- ~chainId,
245
- ): promise<eventBatchQuery> => {
246
- //If the query hangs for longer than this, reject this promise to reduce the block interval
247
- let queryTimoutPromise =
248
- Time.resolvePromiseAfterDelay(~delayMilliseconds=sc.queryTimeoutMillis)->Promise.then(() =>
249
- Promise.reject(
250
- QueryTimout(`Query took longer than ${Int.toString(sc.queryTimeoutMillis / 1000)} seconds`),
251
- )
170
+ let getSelectionConfig = (selection: FetchState.selection) => {
171
+ let evmOnEventRegistrations =
172
+ selection.onEventRegistrations->(
173
+ Utils.magic: array<Internal.onEventRegistration> => array<Internal.evmOnEventRegistration>
252
174
  )
253
175
 
254
- let latestFetchedBlockPromise = loadBlock(toBlock)
255
- Prometheus.SourceRequestCount.increment(~sourceName, ~chainId, ~method="eth_getLogs")
256
- let logsPromise = Rpc.getLogs(
257
- ~client,
258
- ~param={
259
- address: ?addresses,
260
- topics: topicQuery,
261
- fromBlock,
262
- toBlock,
263
- },
264
- )->Promise.then(async logs => {
265
- {
266
- logs,
267
- latestFetchedBlockInfo: await latestFetchedBlockPromise,
268
- }
269
- })
176
+ if evmOnEventRegistrations->Utils.Array.isEmpty {
177
+ throw(
178
+ Source.GetItemsError(
179
+ UnsupportedSelection({
180
+ message: "Invalid events configuration for the partition. Nothing to fetch. Please, report to the Envio team.",
181
+ }),
182
+ ),
183
+ )
184
+ }
270
185
 
271
- [queryTimoutPromise, logsPromise]
272
- ->Promise.race
273
- ->Promise.catch(err => {
274
- switch getSuggestedBlockIntervalFromExn(err) {
275
- | Some((nextBlockIntervalTry, isMaxRange)) =>
276
- mutSuggestedBlockIntervals->Dict.set(
277
- isMaxRange ? maxSuggestedBlockIntervalKey : partitionId,
278
- nextBlockIntervalTry,
186
+ // eth_getLogs takes one address list and one topic selection per request, so
187
+ // fan out to one request per selection. Each address-bound event is grouped by
188
+ // its contract and later scoped to that contract's own addresses — pooling all
189
+ // contracts' addresses would let one contract's query fetch a sibling's logs,
190
+ // which route back by address and bypass the sibling's filter (routing never
191
+ // re-applies it). Pure-wildcard events carry no address constraint, so they're
192
+ // pooled and resolved once.
193
+ let noAddressTopicSelections = []
194
+ let byContract = Dict.make()
195
+ let wildcardByContract = Dict.make()
196
+ let contractNames = Utils.Set.make()
197
+
198
+ evmOnEventRegistrations->Array.forEach(reg => {
199
+ let contractName = reg.eventConfig.contractName
200
+ let {isWildcard, dependsOnAddresses, resolvedWhere} = reg
201
+ if dependsOnAddresses {
202
+ contractNames->Utils.Set.add(contractName)->ignore
203
+ (isWildcard ? wildcardByContract : byContract)->Utils.Dict.pushMany(
204
+ contractName,
205
+ resolvedWhere.topicSelections,
279
206
  )
280
- throw(
281
- Source.GetItemsError(
282
- FailedGettingItems({
283
- exn: err,
284
- attemptedToBlock: toBlock,
285
- retry: WithSuggestedToBlock({
286
- toBlock: fromBlock + nextBlockIntervalTry - 1,
287
- }),
288
- }),
289
- ),
290
- )
291
- | None =>
292
- let executedBlockInterval = toBlock - fromBlock + 1
293
- let nextBlockIntervalTry =
294
- (executedBlockInterval->Int.toFloat *. sc.backoffMultiplicative)->Float.toInt
295
- mutSuggestedBlockIntervals->Dict.set(partitionId, nextBlockIntervalTry)
296
- throw(
297
- Source.GetItemsError(
298
- Source.FailedGettingItems({
299
- exn: err,
300
- attemptedToBlock: toBlock,
301
- retry: WithBackoff({
302
- message: `Failed getting data for the block range. Will try smaller block range for the next attempt.`,
303
- backoffMillis: sc.backoffMillis,
304
- }),
305
- }),
306
- ),
207
+ } else {
208
+ noAddressTopicSelections
209
+ ->Array.pushMany(
210
+ resolvedWhere.topicSelections->LogSelection.materializeTopicSelections(~addresses=[]),
307
211
  )
212
+ ->ignore
308
213
  }
309
214
  })
310
- }
311
215
 
312
- type logSelection = {
313
- addresses: option<array<Address.t>>,
314
- topicQuery: Rpc.GetLogs.topicQuery,
315
- }
316
-
317
- type selectionConfig = {
318
- getLogSelectionOrThrow: (~addressesByContractName: dict<array<Address.t>>) => logSelection,
319
- }
216
+ // `compressTopicSelections` folds the filter-less events into a single topic0
217
+ // OR-set, keeping the common case at one request.
218
+ let toLogSelections = (~addresses, topicSelections): array<logSelection> =>
219
+ topicSelections
220
+ ->LogSelection.compressTopicSelections
221
+ ->Array.map(topicSelection => {
222
+ addresses,
223
+ topicQuery: topicSelection->Rpc.GetLogs.mapTopicQuery,
224
+ })
320
225
 
321
- let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
322
- let staticTopicSelections = []
323
- let dynamicEventFilters = []
226
+ // Address-independent, so resolve once (the wildcard partition reuses this).
227
+ let noAddressLogSelections = toLogSelections(~addresses=None, noAddressTopicSelections)
324
228
 
325
- selection.eventConfigs
326
- ->(Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>)
327
- ->Array.forEach(({getEventFiltersOrThrow}) => {
328
- switch getEventFiltersOrThrow(chain) {
329
- | Static(s) => staticTopicSelections->Array.pushMany(s)->ignore
330
- | Dynamic(fn) => dynamicEventFilters->Array.push(fn)->ignore
331
- }
332
- })
229
+ let getLogSelectionsOrThrow = if contractNames->Utils.Set.size === 0 {
230
+ (~addressesByContractName as _) => noAddressLogSelections
231
+ } else {
232
+ (~addressesByContractName): array<logSelection> => {
233
+ let logSelections = noAddressLogSelections->Array.copy
234
+ contractNames->Utils.Set.forEach(contractName => {
235
+ switch addressesByContractName->Utils.Dict.dangerouslyGetNonOption(contractName) {
236
+ | None
237
+ | Some([]) => ()
238
+ | Some(addresses) =>
239
+ // Non-wildcard filters, scoped to this contract's addresses.
240
+ switch byContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
241
+ | Some(topicSelections) =>
242
+ logSelections
243
+ ->Array.pushMany(
244
+ toLogSelections(
245
+ ~addresses=Some(addresses),
246
+ topicSelections->LogSelection.materializeTopicSelections(~addresses),
247
+ ),
248
+ )
249
+ ->ignore
250
+ | None => ()
251
+ }
333
252
 
334
- let getLogSelectionOrThrow = switch (
335
- staticTopicSelections->LogSelection.compressTopicSelections,
336
- dynamicEventFilters,
337
- ) {
338
- | ([], []) =>
339
- throw(
340
- Source.GetItemsError(
341
- UnsupportedSelection({
342
- message: "Invalid events configuration for the partition. Nothing to fetch. Please, report to the Envio team.",
343
- }),
344
- ),
345
- )
346
- | ([topicSelection], []) => {
347
- let topicQuery = topicSelection->Rpc.GetLogs.mapTopicQuery
348
- (~addressesByContractName) => {
349
- addresses: switch addressesByContractName->FetchState.addressesByContractNameGetAll {
350
- | [] => None
351
- | addresses => Some(addresses)
352
- },
353
- topicQuery,
354
- }
355
- }
356
- | ([], [dynamicEventFilter]) if selection.eventConfigs->Array.length === 1 =>
357
- let eventConfig = selection.eventConfigs->Utils.Array.firstUnsafe
358
-
359
- (~addressesByContractName) => {
360
- let addresses = addressesByContractName->FetchState.addressesByContractNameGetAll
361
- {
362
- addresses: eventConfig.isWildcard ? None : Some(addresses),
363
- topicQuery: switch dynamicEventFilter(addresses) {
364
- | [topicSelection] => topicSelection->Rpc.GetLogs.mapTopicQuery
365
- | _ =>
366
- throw(
367
- Source.GetItemsError(
368
- UnsupportedSelection({
369
- message: "RPC data-source currently doesn't support an array of event filters. Please, create a GitHub issue if it's a blocker for you.",
370
- }),
371
- ),
372
- )
373
- },
374
- }
253
+ // Wildcard-by-address filters fold the address into the topics,
254
+ // so they still match any address.
255
+ switch wildcardByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
256
+ | Some(topicSelections) =>
257
+ logSelections
258
+ ->Array.pushMany(
259
+ toLogSelections(
260
+ ~addresses=None,
261
+ topicSelections->LogSelection.materializeTopicSelections(~addresses),
262
+ ),
263
+ )
264
+ ->ignore
265
+ | None => ()
266
+ }
267
+ }
268
+ })
269
+ logSelections
375
270
  }
376
- | _ =>
377
- throw(
378
- Source.GetItemsError(
379
- UnsupportedSelection({
380
- message: "RPC data-source currently supports event filters only when there's a single wildcard event. Please, create a GitHub issue if it's a blocker for you.",
381
- }),
382
- ),
383
- )
384
271
  }
385
272
 
386
273
  {
387
- getLogSelectionOrThrow: getLogSelectionOrThrow,
274
+ getLogSelectionsOrThrow: getLogSelectionsOrThrow,
388
275
  }
389
276
  }
390
277
 
391
- let memoGetSelectionConfig = (~chain) =>
392
- Utils.WeakMap.memoize(selection => selection->getSelectionConfig(~chain))
278
+ let memoGetSelectionConfig = () => Utils.WeakMap.memoize(getSelectionConfig)
393
279
 
394
280
  // Type-erase a schema for storage in the field registry
395
281
  external toFieldSchema: S.t<'a> => S.t<JSON.t> = "%identity"
@@ -531,11 +417,9 @@ let makeThrowingGetEventBlock = (
531
417
  }
532
418
  }
533
419
 
534
- // `number`, `timestamp` and `hash` are always part of the selected block
535
- // fields, so they can be read from the assembled block at item construction.
420
+ // `number` is always part of the selected block fields, so it can be read
421
+ // from the assembled block for the item's own `blockNumber`.
536
422
  @get external getBlockNumber: Internal.eventBlock => int = "number"
537
- @get external getBlockTimestamp: Internal.eventBlock => int = "timestamp"
538
- @get external getBlockHash: Internal.eventBlock => string = "hash"
539
423
 
540
424
  // Field source classification for RPC calls
541
425
  type fieldSource = TransactionOnly | ReceiptOnly | Both
@@ -851,10 +735,11 @@ type options = {
851
735
  syncConfig: Config.sourceSync,
852
736
  url: string,
853
737
  chain: ChainMap.Chain.t,
854
- eventRouter: EventRouter.t<Internal.evmEventConfig>,
738
+ eventRouter: EventRouter.t<Internal.evmOnEventRegistration>,
855
739
  allEventParams: array<HyperSyncClient.Decoder.eventParamsInput>,
856
740
  lowercaseAddresses: bool,
857
741
  ws?: string,
742
+ headers?: dict<string>,
858
743
  }
859
744
 
860
745
  let make = (
@@ -867,6 +752,7 @@ let make = (
867
752
  allEventParams,
868
753
  lowercaseAddresses,
869
754
  ?ws,
755
+ ?headers,
870
756
  }: options,
871
757
  ): t => {
872
758
  let chainId = chain->ChainMap.Chain.toChainId
@@ -879,22 +765,48 @@ let make = (
879
765
  }
880
766
  let name = `RPC (${urlHost})`
881
767
 
882
- let getSelectionConfig = memoGetSelectionConfig(~chain)
768
+ let getSelectionConfig = memoGetSelectionConfig()
883
769
 
884
- let mutSuggestedBlockIntervals = Dict.make()
770
+ let client = Rpc.makeClient(url, ~headers?)
771
+ let rpcClient = EvmRpcClient.make(
772
+ ~url,
773
+ ~allEventParams,
774
+ ~checksumAddresses=!lowercaseAddresses,
775
+ ~syncConfig,
776
+ ~headers?,
777
+ )
885
778
 
886
- let client = Rpc.makeClient(url)
887
- let rpcClient = EvmRpcClient.make(~url)
779
+ // Requests are made from shared, memoized loaders, so they can't be
780
+ // attributed to a single getItemsOrThrow/getHeightOrThrow/getBlockHashes
781
+ // call at its call site. Every actual request (cache/dedup hits never reach
782
+ // recordRequest) pushes here; each method drains whatever is pending when it
783
+ // returns. Since a push always lands in exactly one drain, per-source totals
784
+ // stay exact even with concurrent in-flight calls — which call happens to
785
+ // drain a given entry doesn't matter, since SourceManager aggregates by
786
+ // (source, method) regardless of which call returned it.
787
+ let pendingRequestStats: array<Source.requestStat> = []
788
+ let recordRequest = (~method, ~seconds) => {
789
+ pendingRequestStats->Array.push({Source.method, seconds})->ignore
790
+ }
791
+ let drainRequestStats = () => {
792
+ let stats = pendingRequestStats->Utils.Array.copy
793
+ pendingRequestStats->Utils.Array.clearInPlace
794
+ stats
795
+ }
888
796
 
889
797
  let makeTransactionLoader = () =>
890
798
  LazyLoader.make(
891
799
  ~loaderFn=transactionHash => {
892
- Prometheus.SourceRequestCount.increment(
893
- ~sourceName=name,
894
- ~chainId=chain->ChainMap.Chain.toChainId,
895
- ~method="eth_getTransactionByHash",
896
- )
897
- Rpc.GetTransactionByHash.rawRoute->Rest.fetch(transactionHash, ~client)
800
+ let timerRef = Performance.now()
801
+ Rpc.GetTransactionByHash.rawRoute
802
+ ->Rest.fetch(transactionHash, ~client)
803
+ ->Promise.thenResolve(res => {
804
+ recordRequest(
805
+ ~method="eth_getTransactionByHash",
806
+ ~seconds=timerRef->Performance.secondsSince,
807
+ )
808
+ res
809
+ })
898
810
  },
899
811
  ~onError=(am, ~exn) => {
900
812
  Logging.error({
@@ -922,6 +834,7 @@ let make = (
922
834
  ~chain,
923
835
  ~backoffMsOnFailure=1000,
924
836
  ~blockNumber,
837
+ ~recordRequest,
925
838
  )
926
839
  },
927
840
  ~onError=(am, ~exn) => {
@@ -944,12 +857,16 @@ let make = (
944
857
  let makeReceiptLoader = () =>
945
858
  LazyLoader.make(
946
859
  ~loaderFn=transactionHash => {
947
- Prometheus.SourceRequestCount.increment(
948
- ~sourceName=name,
949
- ~chainId=chain->ChainMap.Chain.toChainId,
950
- ~method="eth_getTransactionReceipt",
951
- )
952
- Rpc.GetTransactionReceipt.rawRoute->Rest.fetch(transactionHash, ~client)
860
+ let timerRef = Performance.now()
861
+ Rpc.GetTransactionReceipt.rawRoute
862
+ ->Rest.fetch(transactionHash, ~client)
863
+ ->Promise.thenResolve(res => {
864
+ recordRequest(
865
+ ~method="eth_getTransactionReceipt",
866
+ ~seconds=timerRef->Performance.secondsSince,
867
+ )
868
+ res
869
+ })
953
870
  },
954
871
  ~onError=(am, ~exn) => {
955
872
  Logging.error({
@@ -1000,58 +917,19 @@ let make = (
1000
917
  ~lowercaseAddresses,
1001
918
  )
1002
919
 
1003
- let convertLogToHyperSyncEvent = (log: Rpc.GetLogs.log): HyperSyncClient.ResponseTypes.event => {
1004
- let hyperSyncLog: HyperSyncClient.ResponseTypes.log = {
1005
- removed: log.removed,
1006
- index: log.logIndex,
1007
- transactionIndex: log.transactionIndex,
1008
- transactionHash: log.transactionHash,
1009
- blockHash: log.blockHash,
1010
- blockNumber: log.blockNumber,
1011
- address: log.address,
1012
- data: log.data,
1013
- topics: log.topics->(Utils.magic: array<string> => array<Nullable.t<EvmTypes.Hex.t>>),
1014
- }
1015
- {log: hyperSyncLog}
1016
- }
1017
-
1018
- let hscDecoder: ref<option<HyperSyncClient.Decoder.tWithParams>> = ref(None)
1019
- let getHscDecoder = () => {
1020
- switch hscDecoder.contents {
1021
- | Some(decoder) => decoder
1022
- | None => {
1023
- let decoder = HyperSyncClient.Decoder.fromParams(
1024
- allEventParams,
1025
- ~checksumAddresses=!lowercaseAddresses,
1026
- )
1027
- hscDecoder := Some(decoder)
1028
- decoder
1029
- }
1030
- }
1031
- }
1032
-
1033
920
  let getItemsOrThrow = async (
1034
921
  ~fromBlock,
1035
922
  ~toBlock,
1036
923
  ~addressesByContractName,
1037
- ~indexingAddresses,
924
+ ~contractNameByAddress,
1038
925
  ~knownHeight,
1039
926
  ~partitionId,
1040
927
  ~selection: FetchState.selection,
928
+ ~itemsTarget as _,
1041
929
  ~retry,
1042
930
  ~logger as _,
1043
931
  ) => {
1044
- let startFetchingBatchTimeRef = Hrtime.makeTimer()
1045
-
1046
- let suggestedBlockInterval = switch mutSuggestedBlockIntervals->Utils.Dict.dangerouslyGetNonOption(
1047
- maxSuggestedBlockIntervalKey,
1048
- ) {
1049
- | Some(maxSuggestedBlockInterval) => maxSuggestedBlockInterval
1050
- | None =>
1051
- mutSuggestedBlockIntervals
1052
- ->Utils.Dict.dangerouslyGetNonOption(partitionId)
1053
- ->Option.getOr(syncConfig.initialBlockInterval)
1054
- }
932
+ let startFetchingBatchTimeRef = Performance.now()
1055
933
 
1056
934
  // Always have a toBlock for an RPC worker
1057
935
  let toBlock = switch toBlock {
@@ -1059,10 +937,6 @@ let make = (
1059
937
  | None => knownHeight
1060
938
  }
1061
939
 
1062
- let suggestedToBlock = Pervasives.min(fromBlock + suggestedBlockInterval - 1, toBlock)
1063
- //Defensively ensure we never query a target block below fromBlock
1064
- ->Pervasives.max(fromBlock)
1065
-
1066
940
  let firstBlockParentPromise =
1067
941
  fromBlock > 0
1068
942
  ? blockLoader.contents
@@ -1070,71 +944,60 @@ let make = (
1070
944
  ->Promise.thenResolve(json => Some(parseBlockInfo(json)))
1071
945
  : Promise.resolve(None)
1072
946
 
1073
- let {getLogSelectionOrThrow} = getSelectionConfig(selection)
1074
- let {addresses, topicQuery} = getLogSelectionOrThrow(~addressesByContractName)
1075
-
1076
- let {logs, latestFetchedBlockInfo} = await getNextPage(
1077
- ~fromBlock,
1078
- ~toBlock=suggestedToBlock,
1079
- ~addresses,
1080
- ~topicQuery,
1081
- ~loadBlock=blockNumber =>
1082
- blockLoader.contents
1083
- ->LazyLoader.get(blockNumber)
1084
- ->Promise.thenResolve(parseBlockInfo),
1085
- ~syncConfig,
1086
- ~client,
1087
- ~mutSuggestedBlockIntervals,
1088
- ~partitionId,
1089
- ~sourceName=name,
1090
- ~chainId=chain->ChainMap.Chain.toChainId,
1091
- )
947
+ let {getLogSelectionsOrThrow} = getSelectionConfig(selection)
948
+ let logSelections = getLogSelectionsOrThrow(~addressesByContractName)
1092
949
 
1093
- let executedBlockInterval = suggestedToBlock - fromBlock + 1
1094
-
1095
- // Increase the suggested block interval only when it was actually applied
1096
- // and we didn't query to a hard toBlock
1097
- // We also don't care about it when we have a hard max block interval
1098
- if (
1099
- executedBlockInterval >= suggestedBlockInterval &&
1100
- !(mutSuggestedBlockIntervals->Dict.has(maxSuggestedBlockIntervalKey))
1101
- ) {
1102
- // Increase batch size going forward, but do not increase past a configured maximum
1103
- // See: https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease
1104
- mutSuggestedBlockIntervals->Dict.set(
1105
- partitionId,
1106
- Pervasives.min(
1107
- executedBlockInterval + syncConfig.accelerationAdditive,
1108
- syncConfig.intervalCeiling,
950
+ let {items, toBlock: queriedToBlock, requestStats} = try await rpcClient.getNextPage({
951
+ fromBlock,
952
+ toBlockCeiling: toBlock,
953
+ logSelections: logSelections->Array.map(({
954
+ addresses,
955
+ topicQuery,
956
+ }): EvmRpcClient.logSelectionInput => {
957
+ ?addresses,
958
+ topics: topicQuery->Array.map(filter =>
959
+ switch filter {
960
+ | Rpc.GetLogs.Null => Nullable.null
961
+ | Single(topic) => Nullable.make([topic])
962
+ | Multiple(topics) => Nullable.make(topics)
963
+ }
1109
964
  ),
1110
- )
1111
- }
1112
-
1113
- // Convert RPC logs to HyperSync events
1114
- let hyperSyncEvents = logs->Array.map(convertLogToHyperSyncEvent)
1115
-
1116
- // Decode using HyperSyncClient decoder
1117
- let parsedEvents = try await getHscDecoder().decodeLogs(hyperSyncEvents) catch {
965
+ }),
966
+ partitionId,
967
+ }) catch {
1118
968
  | exn =>
1119
- throw(
1120
- Source.GetItemsError(
1121
- FailedGettingItems({
1122
- exn,
1123
- attemptedToBlock: toBlock,
1124
- retry: ImpossibleForTheQuery({
1125
- message: "Failed to parse events using hypersync client decoder. Please double-check your ABI.",
969
+ switch exn->parseGetNextPageRetryError {
970
+ | Some((attemptedToBlock, retry, requestStats)) =>
971
+ requestStats->Array.forEach(stat =>
972
+ recordRequest(~method=stat.method, ~seconds=stat.seconds)
973
+ )
974
+ throw(Source.GetItemsError(FailedGettingItems({exn, attemptedToBlock, retry})))
975
+ | None =>
976
+ throw(
977
+ Source.GetItemsError(
978
+ FailedGettingItems({
979
+ exn,
980
+ attemptedToBlock: toBlock,
981
+ retry: WithBackoff({
982
+ message: "Unexpected issue while fetching events from the RPC client. Attempt a retry.",
983
+ backoffMillis: switch retry {
984
+ | 0 => 500
985
+ | _ => 1000 * retry
986
+ },
987
+ }),
1126
988
  }),
1127
- }),
1128
- ),
1129
- )
989
+ ),
990
+ )
991
+ }
1130
992
  }
993
+ requestStats->Array.forEach(stat => recordRequest(~method=stat.method, ~seconds=stat.seconds))
994
+
995
+ let latestFetchedBlockInfo = await blockLoader.contents
996
+ ->LazyLoader.get(queriedToBlock)
997
+ ->Promise.thenResolve(parseBlockInfo)
1131
998
 
1132
- let parsedQueueItems = await logs
1133
- ->Array.zip(parsedEvents)
1134
- ->Array.filterMap(((
1135
- log: Rpc.GetLogs.log,
1136
- maybeDecodedEvent: Nullable.t<dict<Internal.eventParams>>,
1137
- )) => {
999
+ let parsedQueueItems = await items
1000
+ ->Array.filterMap(({log, params: maybeDecodedEvent}: EvmRpcClient.rpcEventItem) => {
1138
1001
  let topic0 = log.topics[0]->Option.getOr("0x0")
1139
1002
  let routedAddress = if lowercaseAddresses {
1140
1003
  log.address->Address.Evm.fromAddressLowercaseOrThrow
@@ -1144,12 +1007,15 @@ let make = (
1144
1007
 
1145
1008
  switch eventRouter->EventRouter.get(
1146
1009
  ~tag=EventRouter.getEvmEventId(~sighash=topic0, ~topicCount=log.topics->Array.length),
1147
- ~indexingAddresses,
1010
+ ~contractNameByAddress,
1148
1011
  ~contractAddress=routedAddress,
1149
- ~blockNumber=log.blockNumber,
1150
1012
  ) {
1151
1013
  | None => None
1152
- | Some(eventConfig) =>
1014
+ | Some(onEventRegistration) =>
1015
+ let eventConfig =
1016
+ onEventRegistration.eventConfig->(
1017
+ Utils.magic: Internal.eventConfig => Internal.evmEventConfig
1018
+ )
1153
1019
  switch maybeDecodedEvent
1154
1020
  ->Nullable.toOption
1155
1021
  ->Option.flatMap(Dict.get(_, eventConfig.contractName)) {
@@ -1160,7 +1026,9 @@ let make = (
1160
1026
  let (block, transaction) = try await Promise.all2((
1161
1027
  log->getEventBlockOrThrow(~selectedBlockFields=eventConfig.selectedBlockFields),
1162
1028
  log->getEventTransactionOrThrow(
1163
- ~selectedTransactionFields=eventConfig.selectedTransactionFields,
1029
+ ~selectedTransactionFields=eventConfig.selectedTransactionFields->(
1030
+ Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.evmTransactionField>
1031
+ ),
1164
1032
  ),
1165
1033
  )) catch {
1166
1034
  | TransactionDataNotFound({message}) =>
@@ -1194,19 +1062,18 @@ let make = (
1194
1062
  }
1195
1063
 
1196
1064
  Internal.Event({
1197
- eventConfig: (eventConfig :> Internal.eventConfig),
1198
- timestamp: block->getBlockTimestamp,
1065
+ onEventRegistration: (onEventRegistration :> Internal.onEventRegistration),
1199
1066
  blockNumber: block->getBlockNumber,
1200
- blockHash: block->getBlockHash,
1201
1067
  chain,
1202
1068
  logIndex: log.logIndex,
1069
+ transactionIndex: log.transactionIndex,
1203
1070
  payload: {
1204
1071
  contractName: eventConfig.contractName,
1205
1072
  eventName: eventConfig.name,
1206
1073
  chainId: chain->ChainMap.Chain.toChainId,
1207
1074
  params: decoded,
1208
- transaction,
1209
1075
  block,
1076
+ transaction,
1210
1077
  srcAddress: routedAddress,
1211
1078
  logIndex: log.logIndex,
1212
1079
  }->Evm.fromPayload,
@@ -1222,7 +1089,7 @@ let make = (
1222
1089
 
1223
1090
  let optFirstBlockParent = await firstBlockParentPromise
1224
1091
 
1225
- let totalTimeElapsed = startFetchingBatchTimeRef->Hrtime.timeSince->Hrtime.toSecondsFloat
1092
+ let totalTimeElapsed = startFetchingBatchTimeRef->Performance.secondsSince
1226
1093
 
1227
1094
  // Every fetched block carries `hash` and `parentHash`, so each one yields
1228
1095
  // two confirmed (number, hash) pairs for reorg detection at no extra cost.
@@ -1240,7 +1107,7 @@ let make = (
1240
1107
  | Some(b) => pushBlockInfo(b)
1241
1108
  | None => ()
1242
1109
  }
1243
- logs->Array.forEach(log =>
1110
+ items->Array.forEach(({log}) =>
1244
1111
  blockHashes
1245
1112
  ->Array.push({ReorgDetection.blockNumber: log.blockNumber, blockHash: log.blockHash})
1246
1113
  ->ignore
@@ -1250,12 +1117,16 @@ let make = (
1250
1117
  latestFetchedBlockTimestamp: latestFetchedBlockInfo.timestamp,
1251
1118
  latestFetchedBlockNumber: latestFetchedBlockInfo.number,
1252
1119
  parsedQueueItems,
1120
+ // RPC keeps the transaction and block inline on the payload; no store pages.
1121
+ transactionStore: None,
1122
+ blockStore: None,
1253
1123
  stats: {
1254
1124
  totalTimeElapsed: totalTimeElapsed,
1255
1125
  },
1256
1126
  knownHeight,
1257
1127
  blockHashes,
1258
1128
  fromBlockQueried: fromBlock,
1129
+ requestStats: drainRequestStats(),
1259
1130
  }
1260
1131
  }
1261
1132
 
@@ -1272,21 +1143,25 @@ let make = (
1272
1143
  ->Array.map(blockNum => blockLoader.contents->LazyLoader.get(blockNum))
1273
1144
  ->Promise.all
1274
1145
  ->Promise.thenResolve(rawBlocks => {
1275
- rawBlocks
1276
- ->Array.map(json => {
1277
- let b = parseBlockInfo(json)
1278
-
1279
- (
1280
- {
1281
- blockNumber: b.number,
1282
- blockHash: b.hash,
1283
- blockTimestamp: b.timestamp,
1284
- }: ReorgDetection.blockDataWithTimestamp
1285
- )
1286
- })
1287
- ->Ok
1146
+ let result =
1147
+ rawBlocks
1148
+ ->Array.map(json => {
1149
+ let b = parseBlockInfo(json)
1150
+
1151
+ (
1152
+ {
1153
+ blockNumber: b.number,
1154
+ blockHash: b.hash,
1155
+ blockTimestamp: b.timestamp,
1156
+ }: ReorgDetection.blockDataWithTimestamp
1157
+ )
1158
+ })
1159
+ ->Ok
1160
+ {Source.result, requestStats: drainRequestStats()}
1288
1161
  })
1289
- ->Promise.catch(exn => exn->Error->Promise.resolve)
1162
+ ->Promise.catch(exn =>
1163
+ {Source.result: Error(exn), requestStats: drainRequestStats()}->Promise.resolve
1164
+ )
1290
1165
  }
1291
1166
 
1292
1167
  let createHeightSubscription =
@@ -1303,38 +1178,16 @@ let make = (
1303
1178
  getBlockHashes,
1304
1179
  onReorg,
1305
1180
  getHeightOrThrow: async () => {
1306
- let timerRef = Hrtime.makeTimer()
1181
+ let timerRef = Performance.now()
1307
1182
  let height = try {
1308
1183
  await rpcClient.getHeight()
1309
1184
  } catch {
1310
1185
  | exn =>
1311
- let seconds = timerRef->Hrtime.timeSince->Hrtime.toSecondsFloat
1312
- Prometheus.SourceRequestCount.increment(
1313
- ~sourceName=name,
1314
- ~chainId=chain->ChainMap.Chain.toChainId,
1315
- ~method="eth_blockNumber",
1316
- )
1317
- Prometheus.SourceRequestCount.addSeconds(
1318
- ~sourceName=name,
1319
- ~chainId=chain->ChainMap.Chain.toChainId,
1320
- ~method="eth_blockNumber",
1321
- ~seconds,
1322
- )
1186
+ recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
1323
1187
  exn->throw
1324
1188
  }
1325
- let seconds = timerRef->Hrtime.timeSince->Hrtime.toSecondsFloat
1326
- Prometheus.SourceRequestCount.increment(
1327
- ~sourceName=name,
1328
- ~chainId=chain->ChainMap.Chain.toChainId,
1329
- ~method="eth_blockNumber",
1330
- )
1331
- Prometheus.SourceRequestCount.addSeconds(
1332
- ~sourceName=name,
1333
- ~chainId=chain->ChainMap.Chain.toChainId,
1334
- ~method="eth_blockNumber",
1335
- ~seconds,
1336
- )
1337
- height
1189
+ recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
1190
+ {height, requestStats: drainRequestStats()}
1338
1191
  },
1339
1192
  getItemsOrThrow,
1340
1193
  ?createHeightSubscription,