envio 3.13.0-alpha.0 → 3.13.0

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 (94) hide show
  1. package/package.json +6 -8
  2. package/src/BatchProcessing.res +4 -5
  3. package/src/BatchProcessing.res.mjs +4 -11
  4. package/src/ChainFetching.res +18 -38
  5. package/src/ChainFetching.res.mjs +14 -20
  6. package/src/ChainState.res +113 -77
  7. package/src/ChainState.res.mjs +90 -73
  8. package/src/ChainState.resi +14 -5
  9. package/src/Config.res +14 -17
  10. package/src/Config.res.mjs +5 -20
  11. package/src/CrossChainState.res +68 -20
  12. package/src/CrossChainState.res.mjs +48 -5
  13. package/src/CrossChainState.resi +1 -0
  14. package/src/Env.res +2 -2
  15. package/src/ErrorHandling.res +15 -4
  16. package/src/ErrorHandling.res.mjs +16 -5
  17. package/src/FinalizeBackfill.res +21 -9
  18. package/src/FinalizeBackfill.res.mjs +17 -4
  19. package/src/HandlerRegister.res +0 -37
  20. package/src/HandlerRegister.res.mjs +0 -18
  21. package/src/IndexerState.res +2 -0
  22. package/src/IndexerState.res.mjs +5 -0
  23. package/src/IndexerState.resi +1 -0
  24. package/src/Logging.res +6 -38
  25. package/src/Logging.res.mjs +5 -32
  26. package/src/Main.res +6 -9
  27. package/src/Main.res.mjs +3 -5
  28. package/src/Metrics.res +9 -4
  29. package/src/Metrics.res.mjs +3 -3
  30. package/src/Persistence.res +6 -3
  31. package/src/Persistence.res.mjs +5 -3
  32. package/src/PgStorage.res +2 -2
  33. package/src/PgStorage.res.mjs +2 -2
  34. package/src/Rollback.res +3 -3
  35. package/src/Server.res +0 -3
  36. package/src/SimulateItems.res +1 -1
  37. package/src/SimulateItems.res.mjs +1 -1
  38. package/src/Supervisor.res +236 -109
  39. package/src/Supervisor.res.mjs +185 -55
  40. package/src/TestIndexer.res +6 -3
  41. package/src/TestIndexer.res.mjs +3 -3
  42. package/src/Worker.res +6 -0
  43. package/src/Worker.res.mjs +2 -1
  44. package/src/bindings/NodeJs.res +25 -16
  45. package/src/bindings/NodeJs.res.mjs +8 -1
  46. package/src/sources/AddressSet.res +5 -3
  47. package/src/sources/AddressStore.res +19 -11
  48. package/src/sources/AddressStore.res.mjs +2 -7
  49. package/src/sources/BlockStore.res +10 -9
  50. package/src/sources/BlockStore.res.mjs +4 -4
  51. package/src/sources/ChainSources.res +4 -0
  52. package/src/sources/ChainSources.res.mjs +2 -2
  53. package/src/sources/EvmChain.res +4 -0
  54. package/src/sources/EvmChain.res.mjs +3 -1
  55. package/src/sources/EvmEventItem.res +56 -0
  56. package/src/sources/EvmEventItem.res.mjs +32 -0
  57. package/src/sources/EvmHyperSyncSource.res +7 -37
  58. package/src/sources/EvmHyperSyncSource.res.mjs +8 -29
  59. package/src/sources/EvmRpcClient.res +41 -59
  60. package/src/sources/EvmRpcClient.res.mjs +4 -54
  61. package/src/sources/EvmRpcWs.res +4 -1
  62. package/src/sources/EvmRpcWs.res.mjs +6 -2
  63. package/src/sources/FuelHyperSyncSource.res +6 -4
  64. package/src/sources/FuelHyperSyncSource.res.mjs +7 -9
  65. package/src/sources/HeightFeed.res +7 -1
  66. package/src/sources/HeightFeed.res.mjs +9 -7
  67. package/src/sources/HyperSyncClient.res +1 -14
  68. package/src/sources/RequestStat.res +7 -0
  69. package/src/sources/RequestStat.res.mjs +14 -1
  70. package/src/sources/RpcSource.res +100 -1043
  71. package/src/sources/RpcSource.res.mjs +81 -1056
  72. package/src/sources/SimulateSource.res +2 -5
  73. package/src/sources/SimulateSource.res.mjs +2 -3
  74. package/src/sources/Source.res +34 -12
  75. package/src/sources/Source.res.mjs +11 -0
  76. package/src/sources/SourceManager.res +111 -31
  77. package/src/sources/SourceManager.res.mjs +65 -31
  78. package/src/sources/SourceManager.resi +4 -0
  79. package/src/sources/StartBlockResolver.res +8 -5
  80. package/src/sources/StartBlockResolver.res.mjs +4 -2
  81. package/src/sources/SvmHyperSyncSource.res +4 -2
  82. package/src/sources/SvmHyperSyncSource.res.mjs +4 -6
  83. package/src/sources/TransactionStore.res +9 -6
  84. package/src/sources/TransactionStore.res.mjs +2 -2
  85. package/src/tui/Tui.res +4 -18
  86. package/src/tui/Tui.res.mjs +3 -6
  87. package/src/LazyLoader.res +0 -135
  88. package/src/LazyLoader.res.mjs +0 -118
  89. package/src/bindings/SDSL.res +0 -12
  90. package/src/bindings/SDSL.res.mjs +0 -9
  91. package/src/bindings/Yargs.res +0 -8
  92. package/src/bindings/Yargs.res.mjs +0 -2
  93. package/src/sources/Rpc.res +0 -185
  94. package/src/sources/Rpc.res.mjs +0 -183
@@ -1,685 +1,5 @@
1
1
  open Source
2
2
 
3
- // eth_getTransactionByHash/eth_getTransactionReceipt returning null is usually
4
- // transient: a load-balanced provider can route the lookup to a node that
5
- // hasn't caught up with the one that served eth_getLogs. Must stay retryable,
6
- // unlike other field-selection failures which disable the source.
7
- exception TransactionDataNotFound({message: string})
8
-
9
- // Minimal block data needed for infrastructure (reorg guard, timestamps, etc.)
10
- type blockInfo = {
11
- number: int,
12
- timestamp: int,
13
- hash: string,
14
- parentHash: string,
15
- }
16
-
17
- let getKnownRawBlock = async (~client, ~blockNumber) =>
18
- switch await Rpc.getRawBlock(~client, ~blockNumber) {
19
- | Some(json) => json
20
- | None =>
21
- JsError.throwWithMessage(`RPC returned null for blockNumber ${blockNumber->Int.toString}`)
22
- }
23
-
24
- // Extract infrastructure fields (number, timestamp, hash) from raw block JSON
25
- let parseBlockInfo = (json: JSON.t): blockInfo => {
26
- let jsonDict = json->(Utils.magic: JSON.t => dict<JSON.t>)
27
- {
28
- number: jsonDict
29
- ->Dict.getUnsafe("number")
30
- ->S.parseOrThrow(Rpc.hexIntSchema),
31
- timestamp: jsonDict
32
- ->Dict.getUnsafe("timestamp")
33
- ->S.parseOrThrow(Rpc.hexIntSchema),
34
- hash: jsonDict
35
- ->Dict.getUnsafe("hash")
36
- ->S.parseOrThrow(S.string),
37
- parentHash: jsonDict
38
- ->Dict.getUnsafe("parentHash")
39
- ->S.parseOrThrow(S.string),
40
- }
41
- }
42
-
43
- let getKnownRawBlockWithBackoff = async (
44
- ~client,
45
- ~sourceName,
46
- ~chainId,
47
- ~blockNumber,
48
- ~backoffMsOnFailure,
49
- ~recordRequest: (~method: string, ~seconds: float) => unit,
50
- ) => {
51
- let currentBackoff = ref(backoffMsOnFailure)
52
- let result = ref(None)
53
-
54
- while result.contents->Option.isNone {
55
- let timerRef = Performance.now()
56
- switch await getKnownRawBlock(~client, ~blockNumber) {
57
- | exception err =>
58
- recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
59
- Logging.warn({
60
- "err": err->Utils.prettifyExn,
61
- "msg": `Issue while running fetching batch of events from the RPC. Will wait ${currentBackoff.contents->Int.toString}ms and try again.`,
62
- "source": sourceName,
63
- "chainId": chainId,
64
- "type": "EXPONENTIAL_BACKOFF",
65
- })
66
- await Time.resolvePromiseAfterDelay(~delayMilliseconds=currentBackoff.contents)
67
- currentBackoff := currentBackoff.contents * 2
68
- | json =>
69
- recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
70
- result := Some(json)
71
- }
72
- }
73
- result.contents->Option.getOrThrow
74
- }
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)
92
- | _ => None
93
- }
94
- }
95
- | None => None
96
- }
97
- | _ => None
98
- }
99
-
100
- // `EvmRpcClient.getNextPage` throws a napi error whose message is a JSON
101
- // payload describing the retry decision:
102
- // `{"kind":"Retry","attemptedToBlock":int,"errorMessage":string|null,
103
- // "requestStats":[{"method":string,"seconds":float}],"retry":
104
- // {"tag":"WithSuggestedToBlock","toBlock":int} |
105
- // {"tag":"WithBackoff","message":string,"backoffMillis":int}}`.
106
- let parseGetNextPageRetryError = (exn: exn): option<(
107
- int,
108
- Source.getItemsRetry,
109
- array<Source.requestStat>,
110
- )> =>
111
- switch exn {
112
- | JsExn(e) =>
113
- switch e->JsExn.message {
114
- | Some(msg) =>
115
- switch msg->JSON.parseOrThrow->JSON.Decode.object {
116
- | exception _ => None
117
- | None => None
118
- | Some(obj) =>
119
- switch (obj->Dict.get("kind"), obj->Dict.get("attemptedToBlock"), obj->Dict.get("retry")) {
120
- | (Some(String("Retry")), Some(Number(attemptedToBlock)), Some(Object(retryObj))) =>
121
- let requestStats = switch obj->Dict.get("requestStats") {
122
- | Some(Array(stats)) =>
123
- stats->Array.filterMap(s =>
124
- switch s->JSON.Decode.object {
125
- | Some(o) =>
126
- switch (o->Dict.get("method"), o->Dict.get("seconds")) {
127
- | (Some(String(method)), Some(Number(seconds))) => Some({Source.method, seconds})
128
- | _ => None
129
- }
130
- | None => None
131
- }
132
- )
133
- | _ => []
134
- }
135
- let retry = switch retryObj->Dict.get("tag") {
136
- | Some(String("WithSuggestedToBlock")) =>
137
- switch retryObj->Dict.get("toBlock") {
138
- | Some(Number(toBlock)) =>
139
- Some(Source.WithSuggestedToBlock({toBlock: toBlock->Float.toInt}))
140
- | _ => None
141
- }
142
- | Some(String("WithBackoff")) =>
143
- switch (retryObj->Dict.get("message"), retryObj->Dict.get("backoffMillis")) {
144
- | (Some(String(message)), Some(Number(backoffMillis))) =>
145
- Some(Source.WithBackoff({message, backoffMillis: backoffMillis->Float.toInt}))
146
- | _ => None
147
- }
148
- | _ => None
149
- }
150
- retry->Option.map(retry => (attemptedToBlock->Float.toInt, retry, requestStats))
151
- | _ => None
152
- }
153
- }
154
- | None => None
155
- }
156
- | _ => None
157
- }
158
-
159
- // Type-erase a schema for storage in the field registry
160
- external toFieldSchema: S.t<'a> => S.t<JSON.t> = "%identity"
161
-
162
- let lowercaseAddressSchema: S.t<JSON.t> =
163
- S.string
164
- ->S.transform(_ => {
165
- parser: str => str->String.toLowerCase->Address.unsafeFromString,
166
- })
167
- ->toFieldSchema
168
-
169
- let checksumAddressSchema: S.t<JSON.t> =
170
- S.string
171
- ->S.transform(_ => {
172
- parser: str => str->Address.Evm.fromStringOrThrow,
173
- })
174
- ->toFieldSchema
175
-
176
- // Block field definition for per-field parsing
177
- type blockFieldDef = {
178
- location: Internal.evmBlockField,
179
- jsonKey: string,
180
- schema: S.t<JSON.t>, // Type-erased schema
181
- }
182
-
183
- // Block field registry: maps field location (= JS property name) to parsing info.
184
- let makeBlockFieldRegistry = (addressSchema: S.t<JSON.t>): Utils.Record.t<
185
- Internal.evmBlockField,
186
- blockFieldDef,
187
- > =>
188
- [
189
- {location: Number, jsonKey: "number", schema: Rpc.hexIntSchema->toFieldSchema},
190
- {location: Timestamp, jsonKey: "timestamp", schema: Rpc.hexIntSchema->toFieldSchema},
191
- {location: Hash, jsonKey: "hash", schema: S.string->toFieldSchema},
192
- {location: ParentHash, jsonKey: "parentHash", schema: S.string->toFieldSchema},
193
- {location: Nonce, jsonKey: "nonce", schema: Rpc.hexBigintSchema->toFieldSchema},
194
- {location: Sha3Uncles, jsonKey: "sha3Uncles", schema: S.string->toFieldSchema},
195
- {location: LogsBloom, jsonKey: "logsBloom", schema: S.string->toFieldSchema},
196
- {location: TransactionsRoot, jsonKey: "transactionsRoot", schema: S.string->toFieldSchema},
197
- {location: StateRoot, jsonKey: "stateRoot", schema: S.string->toFieldSchema},
198
- {location: ReceiptsRoot, jsonKey: "receiptsRoot", schema: S.string->toFieldSchema},
199
- {location: Miner, jsonKey: "miner", schema: addressSchema},
200
- {location: Difficulty, jsonKey: "difficulty", schema: Rpc.hexBigintSchema->toFieldSchema},
201
- {
202
- location: TotalDifficulty,
203
- jsonKey: "totalDifficulty",
204
- schema: Rpc.hexBigintSchema->toFieldSchema,
205
- },
206
- {location: ExtraData, jsonKey: "extraData", schema: S.string->toFieldSchema},
207
- {location: Size, jsonKey: "size", schema: Rpc.hexBigintSchema->toFieldSchema},
208
- {location: GasLimit, jsonKey: "gasLimit", schema: Rpc.hexBigintSchema->toFieldSchema},
209
- {location: GasUsed, jsonKey: "gasUsed", schema: Rpc.hexBigintSchema->toFieldSchema},
210
- {location: Uncles, jsonKey: "uncles", schema: S.array(S.string)->toFieldSchema},
211
- {location: BaseFeePerGas, jsonKey: "baseFeePerGas", schema: Rpc.hexBigintSchema->toFieldSchema},
212
- {location: BlobGasUsed, jsonKey: "blobGasUsed", schema: Rpc.hexBigintSchema->toFieldSchema},
213
- {location: ExcessBlobGas, jsonKey: "excessBlobGas", schema: Rpc.hexBigintSchema->toFieldSchema},
214
- {
215
- location: ParentBeaconBlockRoot,
216
- jsonKey: "parentBeaconBlockRoot",
217
- schema: S.string->toFieldSchema,
218
- },
219
- {location: WithdrawalsRoot, jsonKey: "withdrawalsRoot", schema: S.string->toFieldSchema},
220
- {location: L1BlockNumber, jsonKey: "l1BlockNumber", schema: Rpc.hexIntSchema->toFieldSchema},
221
- {location: SendCount, jsonKey: "sendCount", schema: S.string->toFieldSchema},
222
- {location: SendRoot, jsonKey: "sendRoot", schema: S.string->toFieldSchema},
223
- {location: MixHash, jsonKey: "mixHash", schema: S.string->toFieldSchema},
224
- ]
225
- ->Array.map(def => (
226
- def.location,
227
- if Internal.evmNullableBlockFields->Utils.Set.has(def.location) {
228
- {...def, schema: S.nullable(def.schema)->toFieldSchema}
229
- } else {
230
- def
231
- },
232
- ))
233
- ->Utils.Record.fromArray
234
-
235
- let blockFieldRegistryLowercase = makeBlockFieldRegistry(lowercaseAddressSchema)
236
- let blockFieldRegistryChecksum = makeBlockFieldRegistry(checksumAddressSchema)
237
-
238
- // Parse block fields from raw JSON, similar to parseFieldsFromJson for transactions
239
- let parseBlockFieldsFromJson = (
240
- mutBlockAcc: dict<JSON.t>,
241
- fields: array<blockFieldDef>,
242
- json: JSON.t,
243
- ) => {
244
- let jsonDict = json->(Utils.magic: JSON.t => dict<JSON.t>)
245
- fields->Array.forEach(def => {
246
- let raw = jsonDict->Dict.getUnsafe(def.jsonKey)
247
- try {
248
- let parsed = raw->S.parseOrThrow(def.schema)
249
- mutBlockAcc->Dict.set((def.location :> string), parsed)
250
- } catch {
251
- | S.Raised(error) =>
252
- JsError.throwWithMessage(
253
- `Invalid block field "${(def.location :> string)}" found in the RPC response. Error: ${error->S.Error.reason}`,
254
- )
255
- }
256
- })
257
- }
258
-
259
- let makeThrowingGetEventBlock = (
260
- ~getBlockJson: int => promise<JSON.t>,
261
- ~lowercaseAddresses: bool,
262
- ) => {
263
- let blockFieldRegistry = if lowercaseAddresses {
264
- blockFieldRegistryLowercase
265
- } else {
266
- blockFieldRegistryChecksum
267
- }
268
- let fnsCache = Utils.WeakMap.make()
269
- (log: Rpc.GetLogs.log, ~selectedBlockFields: Utils.Set.t<Internal.evmBlockField>) => {
270
- (
271
- switch fnsCache->Utils.WeakMap.get(selectedBlockFields) {
272
- | Some(fn) => fn
273
- // Build per-field parser on first call, then cache in WeakMap
274
- | None => {
275
- let fields: array<blockFieldDef> = []
276
- selectedBlockFields->Utils.Set.forEach(fieldName => {
277
- fields->Array.push(blockFieldRegistry->Utils.Record.getUnsafe(fieldName))->ignore
278
- })
279
-
280
- let fn = if selectedBlockFields->Utils.Set.size == 0 {
281
- _ => %raw(`{}`)->(Utils.magic: 'a => Internal.eventBlock)->Promise.resolve
282
- } else {
283
- (log: Rpc.GetLogs.log) => {
284
- getBlockJson(log.blockNumber)->Promise.thenResolve(json => {
285
- let mutBlockAcc = Dict.make()
286
- parseBlockFieldsFromJson(mutBlockAcc, fields, json)
287
- mutBlockAcc->(Utils.magic: dict<JSON.t> => Internal.eventBlock)
288
- })
289
- }
290
- }
291
- let _ = fnsCache->Utils.WeakMap.set(selectedBlockFields, fn)
292
- fn
293
- }
294
- }
295
- )(log)
296
- }
297
- }
298
-
299
- // `number` is always part of the selected block fields, so it can be read
300
- // from the assembled block for the item's own `blockNumber`.
301
- @get external getBlockNumber: Internal.eventBlock => int = "number"
302
-
303
- // Field source classification for RPC calls
304
- type fieldSource = TransactionOnly | ReceiptOnly | Both
305
-
306
- type fieldDef = {
307
- location: Internal.evmTransactionField,
308
- jsonKey: string,
309
- schema: S.t<JSON.t>, // Type-erased schema (S.nullable for optional fields)
310
- source: fieldSource,
311
- }
312
-
313
- // Field registry: maps field location (= JS property name) to parsing info.
314
- // Only includes fields that require an RPC call. Log-derived fields (hash, transactionIndex) are special-cased.
315
- // Nullable fields are wrapped with S.nullable during registry construction based on Internal.evmNullableTransactionFields
316
- let makeFieldRegistry = (addressSchema: S.t<JSON.t>): Utils.Record.t<
317
- Internal.evmTransactionField,
318
- fieldDef,
319
- > =>
320
- [
321
- // TransactionOnly fields (only in eth_getTransactionByHash)
322
- {
323
- location: Gas,
324
- jsonKey: "gas",
325
- schema: Rpc.hexBigintSchema->toFieldSchema,
326
- source: TransactionOnly,
327
- },
328
- {
329
- location: GasPrice,
330
- jsonKey: "gasPrice",
331
- schema: Rpc.hexBigintSchema->toFieldSchema,
332
- source: TransactionOnly,
333
- },
334
- {location: Input, jsonKey: "input", schema: S.string->toFieldSchema, source: TransactionOnly},
335
- {
336
- location: Nonce,
337
- jsonKey: "nonce",
338
- schema: Rpc.hexBigintSchema->toFieldSchema,
339
- source: TransactionOnly,
340
- },
341
- {
342
- location: Value,
343
- jsonKey: "value",
344
- schema: Rpc.hexBigintSchema->toFieldSchema,
345
- source: TransactionOnly,
346
- },
347
- {location: V, jsonKey: "v", schema: S.string->toFieldSchema, source: TransactionOnly},
348
- {location: R, jsonKey: "r", schema: S.string->toFieldSchema, source: TransactionOnly},
349
- {location: S, jsonKey: "s", schema: S.string->toFieldSchema, source: TransactionOnly},
350
- {
351
- location: YParity,
352
- jsonKey: "yParity",
353
- schema: S.string->toFieldSchema,
354
- source: TransactionOnly,
355
- },
356
- {
357
- location: MaxPriorityFeePerGas,
358
- jsonKey: "maxPriorityFeePerGas",
359
- schema: Rpc.hexBigintSchema->toFieldSchema,
360
- source: TransactionOnly,
361
- },
362
- {
363
- location: MaxFeePerGas,
364
- jsonKey: "maxFeePerGas",
365
- schema: Rpc.hexBigintSchema->toFieldSchema,
366
- source: TransactionOnly,
367
- },
368
- {
369
- location: MaxFeePerBlobGas,
370
- jsonKey: "maxFeePerBlobGas",
371
- schema: Rpc.hexBigintSchema->toFieldSchema,
372
- source: TransactionOnly,
373
- },
374
- {
375
- location: BlobVersionedHashes,
376
- jsonKey: "blobVersionedHashes",
377
- schema: S.array(S.string)->toFieldSchema,
378
- source: TransactionOnly,
379
- },
380
- // ReceiptOnly fields (only in eth_getTransactionReceipt)
381
- {
382
- location: GasUsed,
383
- jsonKey: "gasUsed",
384
- schema: Rpc.hexBigintSchema->toFieldSchema,
385
- source: ReceiptOnly,
386
- },
387
- {
388
- location: CumulativeGasUsed,
389
- jsonKey: "cumulativeGasUsed",
390
- schema: Rpc.hexBigintSchema->toFieldSchema,
391
- source: ReceiptOnly,
392
- },
393
- {
394
- location: EffectiveGasPrice,
395
- jsonKey: "effectiveGasPrice",
396
- schema: Rpc.hexBigintSchema->toFieldSchema,
397
- source: ReceiptOnly,
398
- },
399
- {
400
- location: ContractAddress,
401
- jsonKey: "contractAddress",
402
- schema: addressSchema,
403
- source: ReceiptOnly,
404
- },
405
- {
406
- location: LogsBloom,
407
- jsonKey: "logsBloom",
408
- schema: S.string->toFieldSchema,
409
- source: ReceiptOnly,
410
- },
411
- {location: Root, jsonKey: "root", schema: S.string->toFieldSchema, source: ReceiptOnly},
412
- {
413
- location: Status,
414
- jsonKey: "status",
415
- schema: Rpc.hexIntSchema->toFieldSchema,
416
- source: ReceiptOnly,
417
- },
418
- {
419
- location: L1Fee,
420
- jsonKey: "l1Fee",
421
- schema: Rpc.hexBigintSchema->toFieldSchema,
422
- source: ReceiptOnly,
423
- },
424
- {
425
- location: L1GasPrice,
426
- jsonKey: "l1GasPrice",
427
- schema: Rpc.hexBigintSchema->toFieldSchema,
428
- source: ReceiptOnly,
429
- },
430
- {
431
- location: L1GasUsed,
432
- jsonKey: "l1GasUsed",
433
- schema: Rpc.hexBigintSchema->toFieldSchema,
434
- source: ReceiptOnly,
435
- },
436
- {
437
- location: L1FeeScalar,
438
- jsonKey: "l1FeeScalar",
439
- schema: Rpc.decimalFloatSchema->toFieldSchema,
440
- source: ReceiptOnly,
441
- },
442
- {
443
- location: GasUsedForL1,
444
- jsonKey: "gasUsedForL1",
445
- schema: Rpc.hexBigintSchema->toFieldSchema,
446
- source: ReceiptOnly,
447
- },
448
- // Both fields (available in both eth_getTransactionByHash and eth_getTransactionReceipt)
449
- {location: From, jsonKey: "from", schema: addressSchema, source: Both},
450
- {location: To, jsonKey: "to", schema: addressSchema, source: Both},
451
- {location: Type, jsonKey: "type", schema: Rpc.hexIntSchema->toFieldSchema, source: Both},
452
- ]
453
- ->Array.map(def => (
454
- def.location,
455
- if Internal.evmNullableTransactionFields->Utils.Set.has(def.location) {
456
- {...def, schema: S.nullable(def.schema)->toFieldSchema}
457
- } else {
458
- def
459
- },
460
- ))
461
- ->Utils.Record.fromArray
462
-
463
- let fieldRegistryLowercase = makeFieldRegistry(lowercaseAddressSchema)
464
- let fieldRegistryChecksum = makeFieldRegistry(checksumAddressSchema)
465
-
466
- // Whether an RPC source can populate a transaction field. The getter skips a
467
- // field it can't parse, so this is what separates "absent because the chain has
468
- // none" from "absent because RPC never provides it". `hash` and
469
- // `transactionIndex` come off the log itself, so they have no registry entry.
470
- // Blocks need no equivalent: `eth_getBlockByNumber` carries every block field,
471
- // which `RpcFieldSelection_test.res` holds the registry to.
472
- let isRpcTransactionField = (name: string) =>
473
- switch name {
474
- | "transactionIndex" | "hash" => true
475
- | _ =>
476
- fieldRegistryChecksum
477
- ->Utils.Record.get(name->(Utils.magic: string => Internal.evmTransactionField))
478
- ->Option.isSome
479
- }
480
-
481
- type fetchStrategy = NoRpc | TransactionOnly | ReceiptOnly | TransactionAndReceipt
482
-
483
- // Parse fields from a raw JSON object into a result dict.
484
- // Uses unsafeGet so nullable schemas (S.nullable) handle both null and undefined.
485
- let parseFieldsFromJson = (
486
- mutTransactionAcc: dict<JSON.t>,
487
- fields: array<fieldDef>,
488
- json: JSON.t,
489
- ) => {
490
- let jsonDict = json->(Utils.magic: JSON.t => dict<JSON.t>)
491
- fields->Array.forEach(def => {
492
- let raw = jsonDict->Dict.getUnsafe(def.jsonKey)
493
- try {
494
- let parsed = raw->S.parseOrThrow(def.schema)
495
- mutTransactionAcc->Dict.set((def.location :> string), parsed)
496
- } catch {
497
- | S.Raised(error) =>
498
- JsError.throwWithMessage(
499
- `Invalid transaction field "${(def.location :> string)}" found in the RPC response. Error: ${error->S.Error.reason}`,
500
- )
501
- }
502
- })
503
- }
504
-
505
- let effectiveGasPriceKey = (Internal.EffectiveGasPrice: Internal.evmTransactionField :> string)
506
-
507
- // Pre-EIP-1559 receipts carry no `effectiveGasPrice` — every Optimism block
508
- // below the Bedrock migration at 105235063, for one. Those chains price every
509
- // transaction the legacy way, so the transaction's `gasPrice` is the effective
510
- // price, which is the same substitution HyperSync serves for those blocks.
511
- // Reads `gasPrice` but stores under `effectiveGasPrice`.
512
- let effectiveGasPriceFallbackDef = {
513
- location: EffectiveGasPrice,
514
- jsonKey: "gasPrice",
515
- schema: Rpc.hexBigintSchema->toFieldSchema,
516
- source: TransactionOnly,
517
- }
518
-
519
- let fillEffectiveGasPriceOrThrow = (mutTransactionAcc: dict<JSON.t>, txJson: JSON.t) => {
520
- let gasPrice =
521
- txJson->(Utils.magic: JSON.t => dict<JSON.t>)->Utils.Dict.dangerouslyGetNonOption("gasPrice")
522
- switch gasPrice {
523
- | None =>
524
- JsError.throwWithMessage(`Neither "effectiveGasPrice" nor "gasPrice" is present in the RPC response for the transaction. Remove "effectiveGasPrice" from the field selection, or index this chain via HyperSync.`)
525
- | Some(_) => parseFieldsFromJson(mutTransactionAcc, [effectiveGasPriceFallbackDef], txJson)
526
- }
527
- }
528
-
529
- let makeThrowingGetEventTransaction = (
530
- ~getTransactionJson: string => promise<JSON.t>,
531
- ~getReceiptJson: string => promise<JSON.t>,
532
- ~lowercaseAddresses: bool,
533
- ) => {
534
- let fieldRegistry = if lowercaseAddresses {
535
- fieldRegistryLowercase
536
- } else {
537
- fieldRegistryChecksum
538
- }
539
- let fnsCache = Utils.WeakMap.make()
540
- (log, ~selectedTransactionFields: Utils.Set.t<Internal.evmTransactionField>) => {
541
- (
542
- switch fnsCache->Utils.WeakMap.get(selectedTransactionFields) {
543
- | Some(fn) => fn
544
- // Build per-field parser on first call, then cache in WeakMap
545
- | None => {
546
- // Classify fields: log-derived vs RPC fields
547
- let hasTransactionIndex = ref(false)
548
- let hasHash = ref(false)
549
- let hasEffectiveGasPrice = ref(false)
550
- let txFields: array<fieldDef> = []
551
- let receiptFields: array<fieldDef> = []
552
- let bothFields: array<fieldDef> = []
553
-
554
- selectedTransactionFields->Utils.Set.forEach(fieldName => {
555
- switch fieldName {
556
- | TransactionIndex => hasTransactionIndex := true
557
- | Hash => hasHash := true
558
- | _ =>
559
- switch fieldRegistry->Utils.Record.get(fieldName) {
560
- | Some(def) =>
561
- // Absent from the receipt means fall back to the transaction's
562
- // `gasPrice`, so this one field parses leniently rather than
563
- // throwing on a receipt that omits it.
564
- let def = if fieldName === EffectiveGasPrice {
565
- hasEffectiveGasPrice := true
566
- {...def, schema: S.nullable(def.schema)->toFieldSchema}
567
- } else {
568
- def
569
- }
570
- switch def.source {
571
- | TransactionOnly => txFields->Array.push(def)->ignore
572
- | ReceiptOnly => receiptFields->Array.push(def)->ignore
573
- | Both => bothFields->Array.push(def)->ignore
574
- }
575
- | None => () // Unknown field — skip silently
576
- }
577
- }
578
- })
579
-
580
- // Determine fetch strategy
581
- let strategy = switch (txFields->Array.length > 0, receiptFields->Array.length > 0) {
582
- | (true, true) => TransactionAndReceipt
583
- | (true, false) => TransactionOnly
584
- | (false, true) => ReceiptOnly
585
- | (false, false) if bothFields->Array.length > 0 => TransactionOnly
586
- | (false, false) => NoRpc
587
- }
588
-
589
- // Assign Both fields to whichever source is already being fetched; default to transaction
590
- let targetForBoth = strategy == ReceiptOnly ? receiptFields : txFields
591
- bothFields->Array.forEach(f => targetForBoth->Array.push(f)->ignore)
592
-
593
- // Set log-derived fields on the mutable accumulator
594
- let setLogFields = (mutTransactionAcc: dict<JSON.t>, log: Rpc.GetLogs.log) => {
595
- if hasTransactionIndex.contents {
596
- mutTransactionAcc->Dict.set(
597
- "transactionIndex",
598
- log.transactionIndex->(Utils.magic: int => JSON.t),
599
- )
600
- }
601
- if hasHash.contents {
602
- mutTransactionAcc->Dict.set(
603
- "hash",
604
- log.transactionHash->(Utils.magic: string => JSON.t),
605
- )
606
- }
607
- }
608
-
609
- let fn = if selectedTransactionFields->Utils.Set.size == 0 {
610
- _ => %raw(`{}`)->Promise.resolve
611
- } else {
612
- switch strategy {
613
- | NoRpc =>
614
- (log: Rpc.GetLogs.log) => {
615
- let mutTransactionAcc = Dict.make()
616
- setLogFields(mutTransactionAcc, log)
617
- mutTransactionAcc->(Utils.magic: dict<JSON.t> => 'a)->Promise.resolve
618
- }
619
- | _ =>
620
- (log: Rpc.GetLogs.log) => {
621
- let txJsonPromise = switch strategy {
622
- | TransactionOnly | TransactionAndReceipt =>
623
- getTransactionJson(log.transactionHash)->Promise.thenResolve(v => Some(v))
624
- | _ => Promise.resolve(None)
625
- }
626
- let receiptJsonPromise = switch strategy {
627
- | ReceiptOnly | TransactionAndReceipt =>
628
- getReceiptJson(log.transactionHash)->Promise.thenResolve(v => Some(v))
629
- | _ => Promise.resolve(None)
630
- }
631
-
632
- Promise.all2((txJsonPromise, receiptJsonPromise))
633
- ->Promise.then(((txJson, receiptJson)) => {
634
- let mutTransactionAcc = Dict.make()
635
- setLogFields(mutTransactionAcc, log)
636
-
637
- switch txJson {
638
- | Some(json) => parseFieldsFromJson(mutTransactionAcc, txFields, json)
639
- | None => ()
640
- }
641
- switch receiptJson {
642
- | Some(json) => parseFieldsFromJson(mutTransactionAcc, receiptFields, json)
643
- | None => ()
644
- }
645
-
646
- // Only the chains that omit it pay for the extra request, and
647
- // only when the receipt has already come back without it.
648
- if (
649
- hasEffectiveGasPrice.contents &&
650
- mutTransactionAcc
651
- ->Utils.Dict.dangerouslyGetNonOption(effectiveGasPriceKey)
652
- ->Option.isNone
653
- ) {
654
- switch txJson {
655
- | Some(json) => {
656
- fillEffectiveGasPriceOrThrow(mutTransactionAcc, json)
657
- Promise.resolve(mutTransactionAcc)
658
- }
659
- | None =>
660
- getTransactionJson(log.transactionHash)->Promise.thenResolve(json => {
661
- fillEffectiveGasPriceOrThrow(mutTransactionAcc, json)
662
- mutTransactionAcc
663
- })
664
- }
665
- } else {
666
- Promise.resolve(mutTransactionAcc)
667
- }
668
- })
669
- ->Promise.thenResolve(mutTransactionAcc => {
670
- mutTransactionAcc->(Utils.magic: dict<JSON.t> => 'a)
671
- })
672
- }
673
- }
674
- }
675
- let _ = fnsCache->Utils.WeakMap.set(selectedTransactionFields, fn)
676
- fn
677
- }
678
- }
679
- )(log)
680
- }
681
- }
682
-
683
3
  type options = {
684
4
  sourceFor: Source.sourceFor,
685
5
  syncConfig: Config.sourceSync,
@@ -690,6 +10,10 @@ type options = {
690
10
  lowercaseAddresses: bool,
691
11
  // The chain's address index; the client reads it while routing.
692
12
  addressStore: AddressStore.t,
13
+ // Read by the client while planning; the pages it returns are separate, and
14
+ // the caller merges them.
15
+ blockStore: BlockStore.t,
16
+ transactionStore: TransactionStore.t,
693
17
  ws?: string,
694
18
  headers?: dict<string>,
695
19
  }
@@ -703,6 +27,8 @@ let make = (
703
27
  onEventRegistrations,
704
28
  lowercaseAddresses,
705
29
  addressStore,
30
+ blockStore,
31
+ transactionStore,
706
32
  ?ws,
707
33
  ?headers,
708
34
  }: options,
@@ -716,7 +42,6 @@ let make = (
716
42
  }
717
43
  let name = `RPC (${urlHost})`
718
44
 
719
- let client = Rpc.makeClient(url, ~headers?)
720
45
  let rpcClient = EvmRpcClient.make(
721
46
  ~url,
722
47
  ~eventRegistrations=HyperSyncClient.Registration.fromOnEventRegistrations(onEventRegistrations),
@@ -726,153 +51,6 @@ let make = (
726
51
  ~addressStore,
727
52
  )
728
53
 
729
- // Requests are made from shared, memoized loaders, so they can't be
730
- // attributed to a single getItemsOrThrow/getHeightOrThrow/getBlockHashes
731
- // call at its call site. Every actual request (cache/dedup hits never reach
732
- // recordRequest) pushes here; each method drains whatever is pending when it
733
- // returns. Since a push always lands in exactly one drain, per-source totals
734
- // stay exact even with concurrent in-flight calls — which call happens to
735
- // drain a given entry doesn't matter, since SourceManager aggregates by
736
- // (source, method) regardless of which call returned it.
737
- let pendingRequestStats: array<Source.requestStat> = []
738
- let recordRequest = (~method, ~seconds) => {
739
- pendingRequestStats->Array.push({Source.method, seconds})->ignore
740
- }
741
- let drainRequestStats = () => {
742
- let stats = pendingRequestStats->Utils.Array.copy
743
- pendingRequestStats->Utils.Array.clearInPlace
744
- stats
745
- }
746
-
747
- let makeTransactionLoader = () =>
748
- LazyLoader.make(
749
- ~loaderFn=transactionHash => {
750
- let timerRef = Performance.now()
751
- Rpc.GetTransactionByHash.rawRoute
752
- ->Rest.fetch(transactionHash, ~client)
753
- ->Promise.thenResolve(res => {
754
- recordRequest(
755
- ~method="eth_getTransactionByHash",
756
- ~seconds=timerRef->Performance.secondsSince,
757
- )
758
- res
759
- })
760
- },
761
- ~onError=(am, ~exn) => {
762
- Logging.error({
763
- "err": exn->Utils.prettifyExn,
764
- "msg": `Top level promise timeout reached. Please review other errors or warnings in the code. This function will retry in ${(am._retryDelayMillis / 1000)
765
- ->Int.toString} seconds. It is highly likely that your indexer isn't syncing on one or more chains currently. Also take a look at the "suggestedFix" in the metadata of this command`,
766
- "source": name,
767
- "chainId": chainId,
768
- "metadata": {
769
- {
770
- "asyncTaskName": "transactionLoader: fetching transaction data - `getTransaction` rpc call",
771
- "suggestedFix": "This likely means the RPC url you are using is not responding correctly. Please try another RPC endipoint.",
772
- }
773
- },
774
- })
775
- },
776
- )
777
-
778
- let makeBlockLoader = () =>
779
- LazyLoader.make(
780
- ~loaderFn=blockNumber => {
781
- getKnownRawBlockWithBackoff(
782
- ~client,
783
- ~sourceName=name,
784
- ~chainId,
785
- ~backoffMsOnFailure=1000,
786
- ~blockNumber,
787
- ~recordRequest,
788
- )
789
- },
790
- ~onError=(am, ~exn) => {
791
- Logging.error({
792
- "err": exn->Utils.prettifyExn,
793
- "msg": `Top level promise timeout reached. Please review other errors or warnings in the code. This function will retry in ${(am._retryDelayMillis / 1000)
794
- ->Int.toString} seconds. It is highly likely that your indexer isn't syncing on one or more chains currently. Also take a look at the "suggestedFix" in the metadata of this command`,
795
- "source": name,
796
- "chainId": chainId,
797
- "metadata": {
798
- {
799
- "asyncTaskName": "blockLoader: fetching block data - `getBlock` rpc call",
800
- "suggestedFix": "This likely means the RPC url you are using is not responding correctly. Please try another RPC endipoint.",
801
- }
802
- },
803
- })
804
- },
805
- )
806
-
807
- let makeReceiptLoader = () =>
808
- LazyLoader.make(
809
- ~loaderFn=transactionHash => {
810
- let timerRef = Performance.now()
811
- Rpc.GetTransactionReceipt.rawRoute
812
- ->Rest.fetch(transactionHash, ~client)
813
- ->Promise.thenResolve(res => {
814
- recordRequest(
815
- ~method="eth_getTransactionReceipt",
816
- ~seconds=timerRef->Performance.secondsSince,
817
- )
818
- res
819
- })
820
- },
821
- ~onError=(am, ~exn) => {
822
- Logging.error({
823
- "err": exn->Utils.prettifyExn,
824
- "msg": `Top level promise timeout reached. Please review other errors or warnings in the code. This function will retry in ${(am._retryDelayMillis / 1000)
825
- ->Int.toString} seconds. It is highly likely that your indexer isn't syncing on one or more chains currently. Also take a look at the "suggestedFix" in the metadata of this command`,
826
- "source": name,
827
- "chainId": chainId,
828
- "metadata": {
829
- {
830
- "asyncTaskName": "receiptLoader: fetching transaction receipt - `getTransactionReceipt` rpc call",
831
- "suggestedFix": "This likely means the RPC url you are using is not responding correctly. Please try another RPC endipoint.",
832
- }
833
- },
834
- })
835
- },
836
- )
837
-
838
- let blockLoader = ref(makeBlockLoader())
839
- let transactionLoader = ref(makeTransactionLoader())
840
- let receiptLoader = ref(makeReceiptLoader())
841
-
842
- let resetCachedLoaders = () => {
843
- blockLoader := makeBlockLoader()
844
- transactionLoader := makeTransactionLoader()
845
- receiptLoader := makeReceiptLoader()
846
- }
847
-
848
- let getEventBlockOrThrow = makeThrowingGetEventBlock(
849
- ~getBlockJson=blockNumber => blockLoader.contents->LazyLoader.get(blockNumber),
850
- ~lowercaseAddresses,
851
- )
852
- let getEventTransactionOrThrow = makeThrowingGetEventTransaction(
853
- ~getTransactionJson=async transactionHash => {
854
- switch await transactionLoader.contents->LazyLoader.get(transactionHash) {
855
- | Some(json) => json
856
- | None =>
857
- throw(
858
- TransactionDataNotFound({message: `Transaction not found for hash: ${transactionHash}`}),
859
- )
860
- }
861
- },
862
- ~getReceiptJson=async transactionHash => {
863
- switch await receiptLoader.contents->LazyLoader.get(transactionHash) {
864
- | Some(json) => json
865
- | None =>
866
- throw(
867
- TransactionDataNotFound({
868
- message: `Transaction receipt not found for hash: ${transactionHash}`,
869
- }),
870
- )
871
- }
872
- },
873
- ~lowercaseAddresses,
874
- )
875
-
876
54
  let getItemsOrThrow = async (
877
55
  ~fromBlock,
878
56
  ~toBlock,
@@ -885,7 +63,7 @@ let make = (
885
63
  ~retry,
886
64
  ~logger as _,
887
65
  ) => {
888
- let startFetchingBatchTimeRef = Performance.now()
66
+ let totalTimeRef = Performance.now()
889
67
 
890
68
  // Always have a toBlock for an RPC worker
891
69
  let toBlock = switch toBlock {
@@ -893,19 +71,6 @@ let make = (
893
71
  | None => knownHeight
894
72
  }
895
73
 
896
- // The seam block (`fromBlock - 1`) is the only block this range shares with
897
- // what the store already scanned, so it is where a reorg at the boundary
898
- // shows up. Reading it directly would answer from the block cache — it was
899
- // the previous range's `toBlock`, so it is always cached, and always on the
900
- // chain that range saw. Read `fromBlock` instead, which no earlier range
901
- // touched, and take the seam's hash from its `parentHash`.
902
- let firstBlockPromise =
903
- fromBlock > 0 && fromBlock <= toBlock
904
- ? blockLoader.contents
905
- ->LazyLoader.get(fromBlock)
906
- ->Promise.thenResolve(json => Some(parseBlockInfo(json)))
907
- : Promise.resolve(None)
908
-
909
74
  if selection.onEventRegistrations->Utils.Array.isEmpty {
910
75
  throw(
911
76
  Source.GetItemsError(
@@ -916,216 +81,116 @@ let make = (
916
81
  )
917
82
  }
918
83
 
919
- let {items, toBlock: queriedToBlock, requestStats} = try await rpcClient.getNextPage(
84
+ let pageFetchTimeRef = Performance.now()
85
+ let (result, pageBlockStore, pageTransactionStore) = await rpcClient.getNextPage(
920
86
  {
921
87
  fromBlock,
922
88
  toBlockCeiling: toBlock,
923
89
  partitionId,
924
90
  registrationIndexes: selection.onEventRegistrations->Array.map(reg => reg.index),
925
91
  clientFilteredContracts: selection.clientFilteredContracts,
92
+ retry,
926
93
  },
927
94
  addressSet,
928
- ) catch {
929
- | exn =>
930
- switch exn->parseGetNextPageRetryError {
931
- | Some((attemptedToBlock, retry, requestStats)) =>
932
- requestStats->Array.forEach(stat =>
933
- recordRequest(~method=stat.method, ~seconds=stat.seconds)
934
- )
935
- throw(Source.GetItemsError(FailedGettingItems({exn, attemptedToBlock, retry})))
936
- | None =>
937
- throw(
938
- Source.GetItemsError(
939
- FailedGettingItems({
940
- exn,
941
- attemptedToBlock: toBlock,
942
- retry: WithBackoff({
943
- message: "Unexpected issue while fetching events from the RPC client. Attempt a retry.",
944
- backoffMillis: switch retry {
945
- | 0 => 500
946
- | _ => 1000 * retry
947
- },
948
- }),
949
- }),
950
- ),
951
- )
952
- }
953
- }
954
- requestStats->Array.forEach(stat => recordRequest(~method=stat.method, ~seconds=stat.seconds))
955
-
956
- let latestFetchedBlockInfo = await blockLoader.contents
957
- ->LazyLoader.get(queriedToBlock)
958
- ->Promise.thenResolve(parseBlockInfo)
959
-
960
- let parsedQueueItems = await items
961
- ->Array.map(({log, onEventRegistrationIndex, params: decoded}: EvmRpcClient.rpcEventItem) => {
962
- // `log.address` comes back already normalized to the client's casing.
963
- let onEventRegistration = onEventRegistrations->Array.getUnsafe(onEventRegistrationIndex)
964
- let eventConfig =
965
- onEventRegistration.eventConfig->(
966
- Utils.magic: Internal.eventConfig => Internal.evmEventConfig
967
- )
968
-
969
- (
970
- async () => {
971
- let (block, transaction) = try await Promise.all2((
972
- log->getEventBlockOrThrow(
973
- ~selectedBlockFields=onEventRegistration.fieldSelection.blockFields->(
974
- Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.evmBlockField>
975
- ),
976
- ),
977
- log->getEventTransactionOrThrow(
978
- ~selectedTransactionFields=onEventRegistration.fieldSelection.transactionFields->(
979
- Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.evmTransactionField>
980
- ),
981
- ),
982
- )) catch {
983
- | TransactionDataNotFound({message}) =>
984
- let backoffMillis = switch retry {
985
- | 0 => 100
986
- | _ => 500 * retry
987
- }
988
- throw(
989
- Source.GetItemsError(
990
- FailedGettingItems({
991
- exn: %raw(`null`),
992
- attemptedToBlock: toBlock,
993
- retry: WithBackoff({
994
- message: `${message}. The RPC provider might be load-balanced between nodes that drift independently slightly from the head. Indexing should continue correctly after retrying the query in ${backoffMillis->Int.toString}ms.`,
995
- backoffMillis,
996
- }),
997
- }),
998
- ),
999
- )
1000
- | exn =>
1001
- throw(
1002
- Source.GetItemsError(
1003
- FailedGettingFieldSelection({
1004
- message: "Failed getting selected fields. Please double-check your RPC provider returns correct data.",
1005
- exn,
1006
- blockNumber: log.blockNumber,
1007
- logIndex: log.logIndex,
1008
- }),
1009
- ),
1010
- )
1011
- }
1012
-
1013
- Internal.Event({
1014
- onEventRegistration: (onEventRegistration :> Internal.onEventRegistration),
1015
- blockNumber: block->getBlockNumber,
1016
- chainId,
1017
- logIndex: log.logIndex,
1018
- transactionIndex: log.transactionIndex,
1019
- payload: {
1020
- contractName: eventConfig.contractName,
1021
- eventName: eventConfig.name,
1022
- chainId,
1023
- params: decoded,
1024
- block,
1025
- transaction,
1026
- srcAddress: log.address,
1027
- logIndex: log.logIndex,
1028
- }->Evm.fromPayload,
1029
- })
1030
- }
1031
- )()
1032
- })
1033
- ->Promise.all
1034
-
1035
- let optFirstBlock = await firstBlockPromise
95
+ blockStore,
96
+ transactionStore,
97
+ )
98
+ let pageFetchTime = pageFetchTimeRef->Performance.secondsSince
99
+
100
+ // The cause travels as a real error so it reaches the logs the way every
101
+ // other source's does. Absent where the message is the whole story.
102
+ let exn = result.providerMessage->Option.map(message => JsError.make(message)->JsExn.anyToExnInternal)
103
+
104
+ let failedGettingItems = (decision): exn => Source.GetItemsError(
105
+ FailedGettingItems({
106
+ requestStats: result.requestStats,
107
+ ?exn,
108
+ attemptedToBlock: result.toBlock,
109
+ retry: decision,
110
+ }),
111
+ )
1036
112
 
1037
- let totalTimeElapsed = startFetchingBatchTimeRef->Performance.secondsSince
113
+ let missing = field =>
114
+ JsError.throwWithMessage(
115
+ `The RPC client returned a "${(result.kind :> string)}" outcome without a ${field}. Please, report to the Envio team.`,
116
+ )
1038
117
 
1039
- // Every fetched block carries `hash` and `parentHash`, so each one yields
1040
- // two confirmed (number, hash) pairs for reorg detection at no extra cost.
1041
- // Both these blocks and the logs' own `blockHash` come from this range's
1042
- // responses, never from the block cache's older view of the chain. They go
1043
- // into a hash-only page store merged into the chain store, where hash
1044
- // comparison happens. The block data itself stays inline on the payload.
1045
- let observedBlocks: array<BlockStore.inputBlock> = []
1046
- let pushBlockInfo = (b: blockInfo) => {
1047
- observedBlocks
1048
- ->Array.push({
1049
- BlockStore.blockNumber: b.number,
1050
- blockHash: b.hash,
1051
- blockTimestamp: b.timestamp,
1052
- })
1053
- ->ignore
1054
- if b.number > 0 {
1055
- observedBlocks
1056
- ->Array.push({BlockStore.blockNumber: b.number - 1, blockHash: b.parentHash})
1057
- ->ignore
1058
- }
1059
- }
1060
- pushBlockInfo(latestFetchedBlockInfo)
1061
- switch optFirstBlock {
1062
- | Some(b) => pushBlockInfo(b)
1063
- | None => ()
118
+ switch result.kind {
119
+ | Ok => ()
120
+ | FieldSelection =>
121
+ throw(
122
+ Source.GetItemsError(
123
+ FailedGettingFieldSelection({
124
+ requestStats: result.requestStats,
125
+ message: switch result.message {
126
+ | Some(message) => message
127
+ | None => missing("message")
128
+ },
129
+ ?exn,
130
+ blockNumber: switch result.blockNumber {
131
+ | Some(blockNumber) => blockNumber
132
+ | None => missing("blockNumber")
133
+ },
134
+ }),
135
+ ),
136
+ )
137
+ | SuggestedToBlock =>
138
+ throw(
139
+ failedGettingItems(
140
+ WithSuggestedToBlock({
141
+ toBlock: switch result.retryToBlock {
142
+ | Some(toBlock) => toBlock
143
+ | None => missing("retryToBlock")
144
+ },
145
+ }),
146
+ ),
147
+ )
148
+ | Backoff =>
149
+ throw(
150
+ failedGettingItems(
151
+ WithBackoff({
152
+ message: switch result.message {
153
+ | Some(message) => message
154
+ | None => missing("message")
155
+ },
156
+ backoffMillis: switch result.backoffMillis {
157
+ | Some(backoffMillis) => backoffMillis
158
+ | None => missing("backoffMillis")
159
+ },
160
+ }),
161
+ ),
162
+ )
1064
163
  }
1065
- items->Array.forEach(({log}) =>
1066
- observedBlocks
1067
- ->Array.push({BlockStore.blockNumber: log.blockNumber, blockHash: log.blockHash})
1068
- ->ignore
1069
- )
164
+
165
+ let parsingTimeRef = Performance.now()
166
+ let parsedQueueItems =
167
+ result.items->EvmEventItem.toInternalItems(~onEventRegistrations, ~chainId)
168
+ let parsingTimeElapsed = parsingTimeRef->Performance.secondsSince
1070
169
 
1071
170
  {
1072
- latestFetchedBlockNumber: latestFetchedBlockInfo.number,
1073
171
  parsedQueueItems,
1074
- // RPC keeps the transaction and block inline on the payload; no
1075
- // transaction page, and the block page carries only observed hashes.
1076
- transactionStore: None,
1077
- blockStore: BlockStore.fromJs(
1078
- observedBlocks,
1079
- ~ecosystem=Evm,
1080
- ~shouldChecksum=!lowercaseAddresses,
1081
- ),
172
+ transactionStore: Some(pageTransactionStore),
173
+ blockStore: pageBlockStore,
174
+ latestFetchedBlockNumber: result.toBlock,
1082
175
  stats: {
1083
- totalTimeElapsed: totalTimeElapsed,
176
+ totalTimeElapsed: totalTimeRef->Performance.secondsSince,
177
+ parsingTimeElapsed,
178
+ pageFetchTime,
1084
179
  },
1085
180
  knownHeight,
1086
- fromBlockQueried: fromBlock,
1087
- requestStats: drainRequestStats(),
181
+ requestStats: result.requestStats,
1088
182
  }
1089
183
  }
1090
184
 
1091
- let onReorg = () => {
1092
- // Drop cached block/transaction/receipt data — after a reorg or an
1093
- // internally inconsistent response these may refer to orphaned-chain values.
1094
- resetCachedLoaders()
1095
- }
1096
-
1097
- let getBlockHashes = (~blockNumbers, ~logger as _currentlyUnusedLogger) => {
1098
- blockNumbers
1099
- ->Array.map(blockNum => blockLoader.contents->LazyLoader.get(blockNum))
1100
- ->Promise.all
1101
- ->Promise.thenResolve(rawBlocks => {
1102
- // Each block is fetched in its own request, so responses can mix forks.
1103
- // The parent hash becomes a minimal extra row: when consecutive blocks
1104
- // are requested, the page's own hash-collision check cross-validates the
1105
- // separately fetched responses.
1106
- let observedBlocks: array<BlockStore.inputBlock> = []
1107
- rawBlocks->Array.forEach(json => {
1108
- let b = parseBlockInfo(json)
1109
- observedBlocks
1110
- ->Array.push({
1111
- BlockStore.blockNumber: b.number,
1112
- blockHash: b.hash,
1113
- blockTimestamp: b.timestamp,
1114
- })
1115
- ->ignore
1116
- if b.number > 0 {
1117
- observedBlocks
1118
- ->Array.push({BlockStore.blockNumber: b.number - 1, blockHash: b.parentHash})
1119
- ->ignore
1120
- }
1121
- })
1122
- let blockStore =
1123
- observedBlocks->BlockStore.fromJs(~ecosystem=Evm, ~shouldChecksum=!lowercaseAddresses)
1124
- {Source.result: Ok(blockStore), requestStats: drainRequestStats()}
1125
- })
1126
- ->Promise.catch(exn =>
1127
- {Source.result: Error(exn), requestStats: drainRequestStats()}->Promise.resolve
1128
- )
185
+ let getBlockHashes = async (~blockNumbers, ~logger as _) => {
186
+ let (result, pageBlockStore) = await rpcClient.getBlockHashes(blockNumbers)
187
+ {
188
+ Source.result: switch result.message {
189
+ | None => Ok(pageBlockStore)
190
+ | Some(message) => Error(JsError.make(message)->JsExn.anyToExnInternal)
191
+ },
192
+ requestStats: result.requestStats,
193
+ }
1129
194
  }
1130
195
 
1131
196
  let createHeightSubscription =
@@ -1140,18 +205,10 @@ let make = (
1140
205
  poweredByHyperSync: false,
1141
206
  pollingInterval: syncConfig.pollingInterval,
1142
207
  getBlockHashes,
1143
- onReorg,
208
+ onReorg: () => rpcClient.onReorg(),
1144
209
  getHeightOrThrow: async () => {
1145
- let timerRef = Performance.now()
1146
- let height = try {
1147
- await rpcClient.getHeight()
1148
- } catch {
1149
- | exn =>
1150
- recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
1151
- exn->throw
1152
- }
1153
- recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
1154
- {height, requestStats: drainRequestStats()}
210
+ let (height, requestStats) = await rpcClient.getHeight()
211
+ {height, requestStats}
1155
212
  },
1156
213
  getItemsOrThrow,
1157
214
  ?createHeightSubscription,