envio 2.28.0 → 2.29.0-alpha.1

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.
@@ -0,0 +1,569 @@
1
+ open Source
2
+ open Belt
3
+
4
+ type selectionConfig = {
5
+ getLogSelectionOrThrow: (
6
+ ~addressesByContractName: dict<array<Address.t>>,
7
+ ) => array<LogSelection.t>,
8
+ fieldSelection: HyperSyncClient.QueryTypes.fieldSelection,
9
+ nonOptionalBlockFieldNames: array<string>,
10
+ nonOptionalTransactionFieldNames: array<string>,
11
+ }
12
+
13
+ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
14
+ let nonOptionalBlockFieldNames = Utils.Set.make()
15
+ let nonOptionalTransactionFieldNames = Utils.Set.make()
16
+ let capitalizedBlockFields = Utils.Set.make()
17
+ let capitalizedTransactionFields = Utils.Set.make()
18
+
19
+ let staticTopicSelectionsByContract = Js.Dict.empty()
20
+ let dynamicEventFiltersByContract = Js.Dict.empty()
21
+ let dynamicWildcardEventFiltersByContract = Js.Dict.empty()
22
+ let noAddressesTopicSelections = []
23
+ let contractNames = Utils.Set.make()
24
+
25
+ selection.eventConfigs
26
+ ->(Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>)
27
+ ->Array.forEach(({
28
+ dependsOnAddresses,
29
+ contractName,
30
+ getEventFiltersOrThrow,
31
+ blockSchema,
32
+ transactionSchema,
33
+ isWildcard,
34
+ }) => {
35
+ nonOptionalBlockFieldNames->Utils.Set.addMany(
36
+ blockSchema->Utils.Schema.getNonOptionalFieldNames,
37
+ )
38
+ nonOptionalTransactionFieldNames->Utils.Set.addMany(
39
+ transactionSchema->Utils.Schema.getNonOptionalFieldNames,
40
+ )
41
+ capitalizedBlockFields->Utils.Set.addMany(blockSchema->Utils.Schema.getCapitalizedFieldNames)
42
+ capitalizedTransactionFields->Utils.Set.addMany(
43
+ transactionSchema->Utils.Schema.getCapitalizedFieldNames,
44
+ )
45
+
46
+ let eventFilters = getEventFiltersOrThrow(chain)
47
+ if dependsOnAddresses {
48
+ let _ = contractNames->Utils.Set.add(contractName)
49
+ switch eventFilters {
50
+ | Static(topicSelections) =>
51
+ staticTopicSelectionsByContract->Utils.Dict.pushMany(contractName, topicSelections)
52
+ | Dynamic(fn) =>
53
+ (
54
+ isWildcard ? dynamicWildcardEventFiltersByContract : dynamicEventFiltersByContract
55
+ )->Utils.Dict.push(contractName, fn)
56
+ }
57
+ } else {
58
+ noAddressesTopicSelections
59
+ ->Js.Array2.pushMany(
60
+ switch eventFilters {
61
+ | Static(s) => s
62
+ | Dynamic(fn) => fn([])
63
+ },
64
+ )
65
+ ->ignore
66
+ }
67
+ })
68
+
69
+ let fieldSelection: HyperSyncClient.QueryTypes.fieldSelection = {
70
+ log: [Address, Data, LogIndex, Topic0, Topic1, Topic2, Topic3],
71
+ block: capitalizedBlockFields
72
+ ->Utils.Set.toArray
73
+ ->(Utils.magic: array<string> => array<HyperSyncClient.QueryTypes.blockField>),
74
+ transaction: capitalizedTransactionFields
75
+ ->Utils.Set.toArray
76
+ ->(Utils.magic: array<string> => array<HyperSyncClient.QueryTypes.transactionField>),
77
+ }
78
+
79
+ let noAddressesLogSelection = LogSelection.make(
80
+ ~addresses=[],
81
+ ~topicSelections=noAddressesTopicSelections,
82
+ )
83
+
84
+ let getLogSelectionOrThrow = (~addressesByContractName): array<LogSelection.t> => {
85
+ let logSelections = []
86
+ if noAddressesLogSelection.topicSelections->Utils.Array.isEmpty->not {
87
+ logSelections->Array.push(noAddressesLogSelection)
88
+ }
89
+ contractNames->Utils.Set.forEach(contractName => {
90
+ switch addressesByContractName->Utils.Dict.dangerouslyGetNonOption(contractName) {
91
+ | None
92
+ | Some([]) => ()
93
+ | Some(addresses) =>
94
+ switch staticTopicSelectionsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
95
+ | None => ()
96
+ | Some(topicSelections) =>
97
+ logSelections->Array.push(LogSelection.make(~addresses, ~topicSelections))
98
+ }
99
+ switch dynamicEventFiltersByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
100
+ | None => ()
101
+ | Some(fns) =>
102
+ logSelections->Array.push(
103
+ LogSelection.make(
104
+ ~addresses,
105
+ ~topicSelections=fns->Array.flatMapU(fn => fn(addresses)),
106
+ ),
107
+ )
108
+ }
109
+ switch dynamicWildcardEventFiltersByContract->Utils.Dict.dangerouslyGetNonOption(
110
+ contractName,
111
+ ) {
112
+ | None => ()
113
+ | Some(fns) =>
114
+ logSelections->Array.push(
115
+ LogSelection.make(
116
+ ~addresses=[],
117
+ ~topicSelections=fns->Array.flatMapU(fn => fn(addresses)),
118
+ ),
119
+ )
120
+ }
121
+ }
122
+ })
123
+ logSelections
124
+ }
125
+
126
+ {
127
+ getLogSelectionOrThrow,
128
+ fieldSelection,
129
+ nonOptionalBlockFieldNames: nonOptionalBlockFieldNames->Utils.Set.toArray,
130
+ nonOptionalTransactionFieldNames: nonOptionalTransactionFieldNames->Utils.Set.toArray,
131
+ }
132
+ }
133
+
134
+ let memoGetSelectionConfig = (~chain) => {
135
+ let cache = Utils.WeakMap.make()
136
+ selection =>
137
+ switch cache->Utils.WeakMap.get(selection) {
138
+ | Some(c) => c
139
+ | None => {
140
+ let c = selection->getSelectionConfig(~chain)
141
+ let _ = cache->Utils.WeakMap.set(selection, c)
142
+ c
143
+ }
144
+ }
145
+ }
146
+
147
+ type options = {
148
+ contracts: array<Internal.evmContractConfig>,
149
+ chain: ChainMap.Chain.t,
150
+ endpointUrl: string,
151
+ allEventSignatures: array<string>,
152
+ shouldUseHypersyncClientDecoder: bool,
153
+ eventRouter: EventRouter.t<Internal.evmEventConfig>,
154
+ apiToken: option<string>,
155
+ clientMaxRetries: int,
156
+ clientTimeoutMillis: int,
157
+ }
158
+
159
+ let make = (
160
+ {
161
+ contracts,
162
+ chain,
163
+ endpointUrl,
164
+ allEventSignatures,
165
+ shouldUseHypersyncClientDecoder,
166
+ eventRouter,
167
+ apiToken,
168
+ clientMaxRetries,
169
+ clientTimeoutMillis,
170
+ }: options,
171
+ ): t => {
172
+ let name = "HyperSync"
173
+
174
+ let getSelectionConfig = memoGetSelectionConfig(~chain)
175
+
176
+ let apiToken = apiToken->Belt.Option.getWithDefault("3dc856dd-b0ea-494f-b27e-017b8b6b7e07")
177
+
178
+ let client = HyperSyncClient.make(
179
+ ~url=endpointUrl,
180
+ ~apiToken,
181
+ ~maxNumRetries=clientMaxRetries,
182
+ ~httpReqTimeoutMillis=clientTimeoutMillis,
183
+ )
184
+
185
+ let hscDecoder: ref<option<HyperSyncClient.Decoder.t>> = ref(None)
186
+ let getHscDecoder = () => {
187
+ switch hscDecoder.contents {
188
+ | Some(decoder) => decoder
189
+ | None =>
190
+ switch HyperSyncClient.Decoder.fromSignatures(allEventSignatures) {
191
+ | exception exn =>
192
+ exn->ErrorHandling.mkLogAndRaise(
193
+ ~msg="Failed to instantiate a decoder from hypersync client, please double check your ABI or try using 'event_decoder: viem' config option",
194
+ )
195
+ | decoder =>
196
+ decoder.enableChecksummedAddresses()
197
+ decoder
198
+ }
199
+ }
200
+ }
201
+
202
+ exception UndefinedValue
203
+
204
+ let makeEventBatchQueueItem = (
205
+ item: HyperSync.logsQueryPageItem,
206
+ ~params: Internal.eventParams,
207
+ ~eventConfig: Internal.evmEventConfig,
208
+ ): Internal.item => {
209
+ let {block, log, transaction} = item
210
+ let chainId = chain->ChainMap.Chain.toChainId
211
+
212
+ Internal.Event({
213
+ eventConfig: (eventConfig :> Internal.eventConfig),
214
+ timestamp: block.timestamp->Belt.Option.getUnsafe,
215
+ chain,
216
+ blockNumber: block.number->Belt.Option.getUnsafe,
217
+ logIndex: log.logIndex,
218
+ event: {
219
+ chainId,
220
+ params,
221
+ transaction,
222
+ block: block->(Utils.magic: HyperSyncClient.ResponseTypes.block => Internal.eventBlock),
223
+ srcAddress: log.address,
224
+ logIndex: log.logIndex,
225
+ }->Internal.fromGenericEvent,
226
+ })
227
+ }
228
+
229
+ let contractNameAbiMapping = Js.Dict.empty()
230
+ contracts->Belt.Array.forEach(contract => {
231
+ contractNameAbiMapping->Js.Dict.set(contract.name, contract.abi)
232
+ })
233
+
234
+ let getItemsOrThrow = async (
235
+ ~fromBlock,
236
+ ~toBlock,
237
+ ~addressesByContractName,
238
+ ~indexingContracts,
239
+ ~currentBlockHeight,
240
+ ~partitionId as _,
241
+ ~selection,
242
+ ~retry,
243
+ ~logger,
244
+ ) => {
245
+ let mkLogAndRaise = ErrorHandling.mkLogAndRaise(~logger, ...)
246
+ let totalTimeRef = Hrtime.makeTimer()
247
+
248
+ let selectionConfig = selection->getSelectionConfig
249
+
250
+ let logSelections = try selectionConfig.getLogSelectionOrThrow(~addressesByContractName) catch {
251
+ | exn =>
252
+ exn->ErrorHandling.mkLogAndRaise(~logger, ~msg="Failed getting log selection for the query")
253
+ }
254
+
255
+ let startFetchingBatchTimeRef = Hrtime.makeTimer()
256
+
257
+ //fetch batch
258
+ let pageUnsafe = try await HyperSync.GetLogs.query(
259
+ ~client,
260
+ ~fromBlock,
261
+ ~toBlock,
262
+ ~logSelections,
263
+ ~fieldSelection=selectionConfig.fieldSelection,
264
+ ~nonOptionalBlockFieldNames=selectionConfig.nonOptionalBlockFieldNames,
265
+ ~nonOptionalTransactionFieldNames=selectionConfig.nonOptionalTransactionFieldNames,
266
+ ) catch {
267
+ | HyperSync.GetLogs.Error(error) =>
268
+ raise(
269
+ Source.GetItemsError(
270
+ Source.FailedGettingItems({
271
+ exn: %raw(`null`),
272
+ attemptedToBlock: toBlock->Option.getWithDefault(currentBlockHeight),
273
+ retry: switch error {
274
+ | WrongInstance =>
275
+ let backoffMillis = switch retry {
276
+ | 0 => 100
277
+ | _ => 500 * retry
278
+ }
279
+ WithBackoff({
280
+ message: `Block #${fromBlock->Int.toString} not found in HyperSync. HyperSync has multiple instances and it's possible that they drift independently slightly from the head. Indexing should continue correctly after retrying the query in ${backoffMillis->Int.toString}ms.`,
281
+ backoffMillis,
282
+ })
283
+ | UnexpectedMissingParams({missingParams}) =>
284
+ WithBackoff({
285
+ message: `Received page response with invalid data. Attempt a retry. Missing params: ${missingParams->Js.Array2.joinWith(
286
+ ",",
287
+ )}`,
288
+ backoffMillis: switch retry {
289
+ | 0 => 1000
290
+ | _ => 4000 * retry
291
+ },
292
+ })
293
+ },
294
+ }),
295
+ ),
296
+ )
297
+ | exn =>
298
+ raise(
299
+ Source.GetItemsError(
300
+ Source.FailedGettingItems({
301
+ exn,
302
+ attemptedToBlock: toBlock->Option.getWithDefault(currentBlockHeight),
303
+ retry: WithBackoff({
304
+ message: `Unexpected issue while fetching events from HyperSync client. Attempt a retry.`,
305
+ backoffMillis: switch retry {
306
+ | 0 => 500
307
+ | _ => 1000 * retry
308
+ },
309
+ }),
310
+ }),
311
+ ),
312
+ )
313
+ }
314
+
315
+ let pageFetchTime =
316
+ startFetchingBatchTimeRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis
317
+
318
+ //set height and next from block
319
+ let currentBlockHeight = pageUnsafe.archiveHeight
320
+
321
+ //The heighest (biggest) blocknumber that was accounted for in
322
+ //Our query. Not necessarily the blocknumber of the last log returned
323
+ //In the query
324
+ let heighestBlockQueried = pageUnsafe.nextBlock - 1
325
+
326
+ let lastBlockQueriedPromise = switch pageUnsafe.rollbackGuard {
327
+ //In the case a rollbackGuard is returned (this only happens at the head for unconfirmed blocks)
328
+ //use these values
329
+ | Some({blockNumber, timestamp, hash}) =>
330
+ (
331
+ {
332
+ blockNumber,
333
+ blockTimestamp: timestamp,
334
+ blockHash: hash,
335
+ }: ReorgDetection.blockDataWithTimestamp
336
+ )->Promise.resolve
337
+ | None =>
338
+ //The optional block and timestamp of the last item returned by the query
339
+ //(Optional in the case that there are no logs returned in the query)
340
+ switch pageUnsafe.items->Belt.Array.get(pageUnsafe.items->Belt.Array.length - 1) {
341
+ | Some({block}) if block.number->Belt.Option.getUnsafe == heighestBlockQueried =>
342
+ //If the last log item in the current page is equal to the
343
+ //heighest block acounted for in the query. Simply return this
344
+ //value without making an extra query
345
+
346
+ (
347
+ {
348
+ blockNumber: block.number->Belt.Option.getUnsafe,
349
+ blockTimestamp: block.timestamp->Belt.Option.getUnsafe,
350
+ blockHash: block.hash->Belt.Option.getUnsafe,
351
+ }: ReorgDetection.blockDataWithTimestamp
352
+ )->Promise.resolve
353
+ //If it does not match it means that there were no matching logs in the last
354
+ //block so we should fetch the block data
355
+ | Some(_)
356
+ | None =>
357
+ //If there were no logs at all in the current page query then fetch the
358
+ //timestamp of the heighest block accounted for
359
+ HyperSync.queryBlockData(
360
+ ~serverUrl=endpointUrl,
361
+ ~apiToken,
362
+ ~blockNumber=heighestBlockQueried,
363
+ ~logger,
364
+ )->Promise.thenResolve(res =>
365
+ switch res {
366
+ | Ok(Some(blockData)) => blockData
367
+ | Ok(None) =>
368
+ mkLogAndRaise(
369
+ Not_found,
370
+ ~msg=`Failure, blockData for block ${heighestBlockQueried->Int.toString} unexpectedly returned None`,
371
+ )
372
+ | Error(e) =>
373
+ HyperSync.queryErrorToMsq(e)
374
+ ->Obj.magic
375
+ ->mkLogAndRaise(
376
+ ~msg=`Failed to query blockData for block ${heighestBlockQueried->Int.toString}`,
377
+ )
378
+ }
379
+ )
380
+ }
381
+ }
382
+
383
+ let parsingTimeRef = Hrtime.makeTimer()
384
+
385
+ //Parse page items into queue items
386
+ let parsedQueueItems = []
387
+
388
+ let handleDecodeFailure = (
389
+ ~eventConfig: Internal.evmEventConfig,
390
+ ~decoder,
391
+ ~logIndex,
392
+ ~blockNumber,
393
+ ~chainId,
394
+ ~exn,
395
+ ) => {
396
+ if !eventConfig.isWildcard {
397
+ //Wildcard events can be parsed as undefined if the number of topics
398
+ //don't match the event with the given topic0
399
+ //Non wildcard events should be expected to be parsed
400
+ let msg = `Event ${eventConfig.name} was unexpectedly parsed as undefined`
401
+ let logger = Logging.createChildFrom(
402
+ ~logger,
403
+ ~params={
404
+ "chainId": chainId,
405
+ "blockNumber": blockNumber,
406
+ "logIndex": logIndex,
407
+ "decoder": decoder,
408
+ },
409
+ )
410
+ exn->ErrorHandling.mkLogAndRaise(~msg, ~logger)
411
+ }
412
+ }
413
+ if shouldUseHypersyncClientDecoder {
414
+ //Currently there are still issues with decoder for some cases so
415
+ //this can only be activated with a flag
416
+
417
+ //Parse page items into queue items
418
+ let parsedEvents = switch await getHscDecoder().decodeEvents(pageUnsafe.events) {
419
+ | exception exn =>
420
+ exn->mkLogAndRaise(
421
+ ~msg="Failed to parse events using hypersync client, please double check your ABI.",
422
+ )
423
+ | parsedEvents => parsedEvents
424
+ }
425
+
426
+ pageUnsafe.items->Belt.Array.forEachWithIndex((index, item) => {
427
+ let {block, log} = item
428
+ let chainId = chain->ChainMap.Chain.toChainId
429
+ let topic0 = log.topics->Js.Array2.unsafe_get(0)
430
+ let maybeEventConfig =
431
+ eventRouter->EventRouter.get(
432
+ ~tag=EventRouter.getEvmEventId(
433
+ ~sighash=topic0->EvmTypes.Hex.toString,
434
+ ~topicCount=log.topics->Array.length,
435
+ ),
436
+ ~indexingContracts,
437
+ ~contractAddress=log.address,
438
+ ~blockNumber=block.number->Belt.Option.getUnsafe,
439
+ )
440
+ let maybeDecodedEvent = parsedEvents->Js.Array2.unsafe_get(index)
441
+
442
+ switch (maybeEventConfig, maybeDecodedEvent) {
443
+ | (Some(eventConfig), Value(decoded)) =>
444
+ parsedQueueItems
445
+ ->Js.Array2.push(
446
+ makeEventBatchQueueItem(
447
+ item,
448
+ ~params=decoded->eventConfig.convertHyperSyncEventArgs,
449
+ ~eventConfig,
450
+ ),
451
+ )
452
+ ->ignore
453
+ | (Some(eventConfig), Null | Undefined) =>
454
+ handleDecodeFailure(
455
+ ~eventConfig,
456
+ ~decoder="hypersync-client",
457
+ ~logIndex=log.logIndex,
458
+ ~blockNumber=block.number->Belt.Option.getUnsafe,
459
+ ~chainId,
460
+ ~exn=UndefinedValue,
461
+ )
462
+ | (None, _) => () //ignore events that aren't registered
463
+ }
464
+ })
465
+ } else {
466
+ //Parse with viem -> slower than the HyperSyncClient
467
+ pageUnsafe.items->Array.forEach(item => {
468
+ let {block, log} = item
469
+ let chainId = chain->ChainMap.Chain.toChainId
470
+ let topic0 = log.topics->Js.Array2.unsafe_get(0)
471
+
472
+ switch eventRouter->EventRouter.get(
473
+ ~tag=EventRouter.getEvmEventId(
474
+ ~sighash=topic0->EvmTypes.Hex.toString,
475
+ ~topicCount=log.topics->Array.length,
476
+ ),
477
+ ~indexingContracts,
478
+ ~contractAddress=log.address,
479
+ ~blockNumber=block.number->Belt.Option.getUnsafe,
480
+ ) {
481
+ | Some(eventConfig) =>
482
+ switch contractNameAbiMapping->Viem.parseLogOrThrow(
483
+ ~contractName=eventConfig.contractName,
484
+ ~topics=log.topics,
485
+ ~data=log.data,
486
+ ) {
487
+ | exception exn =>
488
+ handleDecodeFailure(
489
+ ~eventConfig,
490
+ ~decoder="viem",
491
+ ~logIndex=log.logIndex,
492
+ ~blockNumber=block.number->Belt.Option.getUnsafe,
493
+ ~chainId,
494
+ ~exn,
495
+ )
496
+ | decodedEvent =>
497
+ parsedQueueItems
498
+ ->Js.Array2.push(makeEventBatchQueueItem(item, ~params=decodedEvent.args, ~eventConfig))
499
+ ->ignore
500
+ }
501
+ | None => () //Ignore events that aren't registered
502
+ }
503
+ })
504
+ }
505
+
506
+ let parsingTimeElapsed = parsingTimeRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis
507
+
508
+ let rangeLastBlock = await lastBlockQueriedPromise
509
+
510
+ let reorgGuard: ReorgDetection.reorgGuard = {
511
+ rangeLastBlock: rangeLastBlock->ReorgDetection.generalizeBlockDataWithTimestamp,
512
+ prevRangeLastBlock: pageUnsafe.rollbackGuard->Option.map(v => {
513
+ ReorgDetection.blockHash: v.firstParentHash,
514
+ blockNumber: v.firstBlockNumber - 1,
515
+ }),
516
+ }
517
+
518
+ let totalTimeElapsed = totalTimeRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis
519
+
520
+ let stats = {
521
+ totalTimeElapsed,
522
+ parsingTimeElapsed,
523
+ pageFetchTime,
524
+ }
525
+
526
+ {
527
+ latestFetchedBlockTimestamp: rangeLastBlock.blockTimestamp,
528
+ parsedQueueItems,
529
+ latestFetchedBlockNumber: rangeLastBlock.blockNumber,
530
+ stats,
531
+ currentBlockHeight,
532
+ reorgGuard,
533
+ fromBlockQueried: fromBlock,
534
+ }
535
+ }
536
+
537
+ let getBlockHashes = (~blockNumbers, ~logger) =>
538
+ HyperSync.queryBlockDataMulti(
539
+ ~serverUrl=endpointUrl,
540
+ ~apiToken,
541
+ ~blockNumbers,
542
+ ~logger,
543
+ )->Promise.thenResolve(HyperSync.mapExn)
544
+
545
+ let jsonApiClient = Rest.client(endpointUrl)
546
+
547
+ let malformedTokenMessage = `Your token is malformed. For more info: https://docs.envio.dev/docs/HyperSync/api-tokens.`
548
+
549
+ {
550
+ name,
551
+ sourceFor: Sync,
552
+ chain,
553
+ pollingInterval: 100,
554
+ poweredByHyperSync: true,
555
+ getBlockHashes,
556
+ getHeightOrThrow: async () =>
557
+ switch await HyperSyncJsonApi.heightRoute->Rest.fetch(apiToken, ~client=jsonApiClient) {
558
+ | Value(height) => height
559
+ | ErrorMessage(m) if m === malformedTokenMessage =>
560
+ Logging.error(`Your ENVIO_API_TOKEN is malformed. The indexer will not be able to fetch events. Update the token and restart the indexer using 'pnpm envio start'. For more info: https://docs.envio.dev/docs/HyperSync/api-tokens`)
561
+ // Don't want to retry if the token is malformed
562
+ // So just block forever
563
+ let _ = await Promise.make((_, _) => ())
564
+ 0
565
+ | ErrorMessage(m) => Js.Exn.raiseError(m)
566
+ },
567
+ getItemsOrThrow,
568
+ }
569
+ }