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,506 @@
1
+ open Source
2
+ open Belt
3
+
4
+ exception EventRoutingFailed
5
+
6
+ let mintEventTag = "mint"
7
+ let burnEventTag = "burn"
8
+ let transferEventTag = "transfer"
9
+ let callEventTag = "call"
10
+
11
+ type selectionConfig = {
12
+ getRecieptsSelection: (
13
+ ~addressesByContractName: dict<array<Address.t>>,
14
+ ) => array<HyperFuelClient.QueryTypes.receiptSelection>,
15
+ eventRouter: EventRouter.t<Internal.fuelEventConfig>,
16
+ }
17
+
18
+ let logDataReceiptTypeSelection: array<Fuel.receiptType> = [LogData]
19
+
20
+ // only transactions with status 1 (success)
21
+ let txStatusSelection = [1]
22
+
23
+ let makeGetNormalRecieptsSelection = (
24
+ ~nonWildcardLogDataRbsByContract,
25
+ ~nonLogDataReceiptTypesByContract,
26
+ ~contractNames,
27
+ ) => {
28
+ (~addressesByContractName) => {
29
+ let selection: array<HyperFuelClient.QueryTypes.receiptSelection> = []
30
+
31
+ //Instantiate each time to add new registered contract addresses
32
+ contractNames->Utils.Set.forEach(contractName => {
33
+ switch addressesByContractName->Utils.Dict.dangerouslyGetNonOption(contractName) {
34
+ | None
35
+ | Some([]) => ()
36
+ | Some(addresses) => {
37
+ switch nonLogDataReceiptTypesByContract->Utils.Dict.dangerouslyGetNonOption(
38
+ contractName,
39
+ ) {
40
+ | Some(receiptTypes) =>
41
+ selection
42
+ ->Js.Array2.push({
43
+ rootContractId: addresses,
44
+ receiptType: receiptTypes,
45
+ txStatus: txStatusSelection,
46
+ })
47
+ ->ignore
48
+ | None => ()
49
+ }
50
+ switch nonWildcardLogDataRbsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
51
+ | None
52
+ | Some([]) => ()
53
+ | Some(nonWildcardLogDataRbs) =>
54
+ selection
55
+ ->Js.Array2.push({
56
+ rootContractId: addresses,
57
+ receiptType: logDataReceiptTypeSelection,
58
+ txStatus: txStatusSelection,
59
+ rb: nonWildcardLogDataRbs,
60
+ })
61
+ ->ignore
62
+ }
63
+ }
64
+ }
65
+ })
66
+
67
+ selection
68
+ }
69
+ }
70
+
71
+ let makeWildcardRecieptsSelection = (~wildcardLogDataRbs, ~nonLogDataWildcardReceiptTypes) => {
72
+ let selection: array<HyperFuelClient.QueryTypes.receiptSelection> = []
73
+
74
+ switch nonLogDataWildcardReceiptTypes {
75
+ | [] => ()
76
+ | nonLogDataWildcardReceiptTypes =>
77
+ selection
78
+ ->Js.Array2.push(
79
+ (
80
+ {
81
+ receiptType: nonLogDataWildcardReceiptTypes,
82
+ txStatus: txStatusSelection,
83
+ }: HyperFuelClient.QueryTypes.receiptSelection
84
+ ),
85
+ )
86
+ ->ignore
87
+ }
88
+
89
+ switch wildcardLogDataRbs {
90
+ | [] => ()
91
+ | wildcardLogDataRbs =>
92
+ selection
93
+ ->Js.Array2.push(
94
+ (
95
+ {
96
+ receiptType: logDataReceiptTypeSelection,
97
+ txStatus: txStatusSelection,
98
+ rb: wildcardLogDataRbs,
99
+ }: HyperFuelClient.QueryTypes.receiptSelection
100
+ ),
101
+ )
102
+ ->ignore
103
+ }
104
+
105
+ selection
106
+ }
107
+
108
+ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
109
+ let eventRouter = EventRouter.empty()
110
+ let nonWildcardLogDataRbsByContract = Js.Dict.empty()
111
+ let wildcardLogDataRbs = []
112
+
113
+ // This is for non-LogData events, since they don't have rb filter and can be grouped
114
+ let nonLogDataReceiptTypesByContract = Js.Dict.empty()
115
+ let nonLogDataWildcardReceiptTypes = []
116
+
117
+ let addNonLogDataWildcardReceiptTypes = (receiptType: Fuel.receiptType) => {
118
+ nonLogDataWildcardReceiptTypes->Array.push(receiptType)->ignore
119
+ }
120
+ let addNonLogDataReceiptType = (contractName, receiptType: Fuel.receiptType) => {
121
+ switch nonLogDataReceiptTypesByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
122
+ | None => nonLogDataReceiptTypesByContract->Js.Dict.set(contractName, [receiptType])
123
+ | Some(receiptTypes) => receiptTypes->Array.push(receiptType)->ignore // Duplication prevented by EventRouter
124
+ }
125
+ }
126
+
127
+ let contractNames = Utils.Set.make()
128
+
129
+ selection.eventConfigs
130
+ ->(Utils.magic: array<Internal.eventConfig> => array<Internal.fuelEventConfig>)
131
+ ->Array.forEach(eventConfig => {
132
+ let contractName = eventConfig.contractName
133
+ if !eventConfig.isWildcard {
134
+ let _ = contractNames->Utils.Set.add(contractName)
135
+ }
136
+ eventRouter->EventRouter.addOrThrow(
137
+ eventConfig.id,
138
+ eventConfig,
139
+ ~contractName,
140
+ ~eventName=eventConfig.name,
141
+ ~chain,
142
+ ~isWildcard=eventConfig.isWildcard,
143
+ )
144
+
145
+ switch eventConfig {
146
+ | {kind: Mint, isWildcard: true} => addNonLogDataWildcardReceiptTypes(Mint)
147
+ | {kind: Mint} => addNonLogDataReceiptType(contractName, Mint)
148
+ | {kind: Burn, isWildcard: true} => addNonLogDataWildcardReceiptTypes(Burn)
149
+ | {kind: Burn} => addNonLogDataReceiptType(contractName, Burn)
150
+ | {kind: Transfer, isWildcard: true} => {
151
+ addNonLogDataWildcardReceiptTypes(Transfer)
152
+ addNonLogDataWildcardReceiptTypes(TransferOut)
153
+ }
154
+ | {kind: Transfer} => {
155
+ addNonLogDataReceiptType(contractName, Transfer)
156
+ addNonLogDataReceiptType(contractName, TransferOut)
157
+ }
158
+ | {kind: Call, isWildcard: true} => addNonLogDataWildcardReceiptTypes(Call)
159
+ | {kind: Call} =>
160
+ Js.Exn.raiseError("Call receipt indexing currently supported only in wildcard mode")
161
+ | {kind: LogData({logId}), isWildcard} => {
162
+ let rb = logId->BigInt.fromStringUnsafe
163
+ if isWildcard {
164
+ wildcardLogDataRbs->Array.push(rb)->ignore
165
+ } else {
166
+ switch nonWildcardLogDataRbsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
167
+ | Some(arr) => arr->Belt.Array.push(rb)
168
+ | None => nonWildcardLogDataRbsByContract->Js.Dict.set(contractName, [rb])
169
+ }
170
+ }
171
+ }
172
+ }
173
+ })
174
+
175
+ {
176
+ getRecieptsSelection: switch selection.dependsOnAddresses {
177
+ | false => {
178
+ let recieptsSelection = makeWildcardRecieptsSelection(
179
+ ~wildcardLogDataRbs,
180
+ ~nonLogDataWildcardReceiptTypes,
181
+ )
182
+ (~addressesByContractName as _) => recieptsSelection
183
+ }
184
+ | true =>
185
+ makeGetNormalRecieptsSelection(
186
+ ~nonWildcardLogDataRbsByContract,
187
+ ~nonLogDataReceiptTypesByContract,
188
+ ~contractNames,
189
+ )
190
+ },
191
+ eventRouter,
192
+ }
193
+ }
194
+
195
+ let memoGetSelectionConfig = (~chain) => {
196
+ let cache = Utils.WeakMap.make()
197
+ selection =>
198
+ switch cache->Utils.WeakMap.get(selection) {
199
+ | Some(c) => c
200
+ | None => {
201
+ let c = selection->getSelectionConfig(~chain)
202
+ let _ = cache->Utils.WeakMap.set(selection, c)
203
+ c
204
+ }
205
+ }
206
+ }
207
+
208
+ type options = {
209
+ chain: ChainMap.Chain.t,
210
+ endpointUrl: string,
211
+ }
212
+
213
+ let make = ({chain, endpointUrl}: options): t => {
214
+ let name = "HyperFuel"
215
+
216
+ let getSelectionConfig = memoGetSelectionConfig(~chain)
217
+
218
+ let getItemsOrThrow = async (
219
+ ~fromBlock,
220
+ ~toBlock,
221
+ ~addressesByContractName,
222
+ ~indexingContracts,
223
+ ~currentBlockHeight,
224
+ ~partitionId as _,
225
+ ~selection: FetchState.selection,
226
+ ~retry,
227
+ ~logger,
228
+ ) => {
229
+ let mkLogAndRaise = ErrorHandling.mkLogAndRaise(~logger, ...)
230
+ let totalTimeRef = Hrtime.makeTimer()
231
+
232
+ let selectionConfig = getSelectionConfig(selection)
233
+ let recieptsSelection = selectionConfig.getRecieptsSelection(~addressesByContractName)
234
+
235
+ let startFetchingBatchTimeRef = Hrtime.makeTimer()
236
+
237
+ //fetch batch
238
+ let pageUnsafe = try await HyperFuel.GetLogs.query(
239
+ ~serverUrl=endpointUrl,
240
+ ~fromBlock,
241
+ ~toBlock,
242
+ ~recieptsSelection,
243
+ ) catch {
244
+ | HyperSync.GetLogs.Error(error) =>
245
+ raise(
246
+ Source.GetItemsError(
247
+ Source.FailedGettingItems({
248
+ exn: %raw(`null`),
249
+ attemptedToBlock: toBlock->Option.getWithDefault(currentBlockHeight),
250
+ retry: switch error {
251
+ | WrongInstance =>
252
+ let backoffMillis = switch retry {
253
+ | 0 => 100
254
+ | _ => 500 * retry
255
+ }
256
+ WithBackoff({
257
+ message: `Block #${fromBlock->Int.toString} not found in HyperFuel. HyperFuel 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.`,
258
+ backoffMillis,
259
+ })
260
+ | UnexpectedMissingParams({missingParams}) =>
261
+ WithBackoff({
262
+ message: `Received page response with invalid data. Attempt a retry. Missing params: ${missingParams->Js.Array2.joinWith(
263
+ ",",
264
+ )}`,
265
+ backoffMillis: switch retry {
266
+ | 0 => 1000
267
+ | _ => 4000 * retry
268
+ },
269
+ })
270
+ },
271
+ }),
272
+ ),
273
+ )
274
+ | exn =>
275
+ raise(
276
+ Source.GetItemsError(
277
+ Source.FailedGettingItems({
278
+ exn,
279
+ attemptedToBlock: toBlock->Option.getWithDefault(currentBlockHeight),
280
+ retry: WithBackoff({
281
+ message: `Unexpected issue while fetching events from HyperFuel client. Attempt a retry.`,
282
+ backoffMillis: switch retry {
283
+ | 0 => 500
284
+ | _ => 1000 * retry
285
+ },
286
+ }),
287
+ }),
288
+ ),
289
+ )
290
+ }
291
+
292
+ let pageFetchTime =
293
+ startFetchingBatchTimeRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis
294
+
295
+ //set height and next from block
296
+ let currentBlockHeight = pageUnsafe.archiveHeight
297
+
298
+ //The heighest (biggest) blocknumber that was accounted for in
299
+ //Our query. Not necessarily the blocknumber of the last log returned
300
+ //In the query
301
+ let heighestBlockQueried = pageUnsafe.nextBlock - 1
302
+
303
+ let lastBlockQueriedPromise = // switch pageUnsafe.rollbackGuard {
304
+ // //In the case a rollbackGuard is returned (this only happens at the head for unconfirmed blocks)
305
+ // //use these values
306
+ // | Some({blockNumber, timestamp, hash}) =>
307
+ // {
308
+ // ReorgDetection.blockNumber,
309
+ // blockTimestamp: timestamp,
310
+ // blockHash: hash,
311
+ // }->Promise.resolve
312
+ // | None =>
313
+ //The optional block and timestamp of the last item returned by the query
314
+ //(Optional in the case that there are no logs returned in the query)
315
+ switch pageUnsafe.items->Belt.Array.get(pageUnsafe.items->Belt.Array.length - 1) {
316
+ | Some({block}) if block.height == heighestBlockQueried =>
317
+ //If the last log item in the current page is equal to the
318
+ //heighest block acounted for in the query. Simply return this
319
+ //value without making an extra query
320
+
321
+ (
322
+ {
323
+ blockNumber: block.height,
324
+ blockTimestamp: block.time,
325
+ blockHash: block.id,
326
+ }: ReorgDetection.blockDataWithTimestamp
327
+ )->Promise.resolve
328
+ //If it does not match it means that there were no matching logs in the last
329
+ //block so we should fetch the block data
330
+ | Some(_)
331
+ | None =>
332
+ //If there were no logs at all in the current page query then fetch the
333
+ //timestamp of the heighest block accounted for
334
+ HyperFuel.queryBlockData(~serverUrl=endpointUrl, ~blockNumber=heighestBlockQueried, ~logger)
335
+ ->Promise.thenResolve(res => {
336
+ switch res {
337
+ | Some(blockData) => blockData
338
+ | None =>
339
+ mkLogAndRaise(
340
+ Not_found,
341
+ ~msg=`Failure, blockData for block ${heighestBlockQueried->Int.toString} unexpectedly returned None`,
342
+ )
343
+ }
344
+ })
345
+ ->Promise.catch(exn => {
346
+ exn->mkLogAndRaise(
347
+ ~msg=`Failed to query blockData for block ${heighestBlockQueried->Int.toString}`,
348
+ )
349
+ })
350
+ }
351
+
352
+ let parsingTimeRef = Hrtime.makeTimer()
353
+
354
+ let parsedQueueItems = pageUnsafe.items->Array.map(item => {
355
+ let {contractId: contractAddress, receipt, block, receiptIndex} = item
356
+
357
+ let chainId = chain->ChainMap.Chain.toChainId
358
+ let eventId = switch receipt {
359
+ | LogData({rb}) => BigInt.toString(rb)
360
+ | Mint(_) => mintEventTag
361
+ | Burn(_) => burnEventTag
362
+ | Transfer(_)
363
+ | TransferOut(_) => transferEventTag
364
+ | Call(_) => callEventTag
365
+ }
366
+
367
+ let eventConfig = switch selectionConfig.eventRouter->EventRouter.get(
368
+ ~tag=eventId,
369
+ ~indexingContracts,
370
+ ~contractAddress,
371
+ ~blockNumber=block.height,
372
+ ) {
373
+ | None => {
374
+ let logger = Logging.createChildFrom(
375
+ ~logger,
376
+ ~params={
377
+ "chainId": chainId,
378
+ "blockNumber": block.height,
379
+ "logIndex": receiptIndex,
380
+ "contractAddress": contractAddress,
381
+ "eventId": eventId,
382
+ },
383
+ )
384
+ EventRoutingFailed->ErrorHandling.mkLogAndRaise(
385
+ ~msg="Failed to route registered event",
386
+ ~logger,
387
+ )
388
+ }
389
+ | Some(eventConfig) => eventConfig
390
+ }
391
+
392
+ let params = switch (eventConfig, receipt) {
393
+ | ({kind: LogData({decode})}, LogData({data})) =>
394
+ try decode(data) catch {
395
+ | exn => {
396
+ let params = {
397
+ "chainId": chainId,
398
+ "blockNumber": block.height,
399
+ "logIndex": receiptIndex,
400
+ }
401
+ let logger = Logging.createChildFrom(~logger, ~params)
402
+ exn->ErrorHandling.mkLogAndRaise(
403
+ ~msg="Failed to decode Fuel LogData receipt, please double check your ABI.",
404
+ ~logger,
405
+ )
406
+ }
407
+ }
408
+ | (_, Mint({val, subId}))
409
+ | (_, Burn({val, subId})) =>
410
+ (
411
+ {
412
+ subId,
413
+ amount: val,
414
+ }: Internal.fuelSupplyParams
415
+ )->Obj.magic
416
+ | (_, Transfer({amount, assetId, to})) =>
417
+ (
418
+ {
419
+ to: to->Address.unsafeFromString,
420
+ assetId,
421
+ amount,
422
+ }: Internal.fuelTransferParams
423
+ )->Obj.magic
424
+ | (_, TransferOut({amount, assetId, toAddress})) =>
425
+ (
426
+ {
427
+ to: toAddress->Address.unsafeFromString,
428
+ assetId,
429
+ amount,
430
+ }: Internal.fuelTransferParams
431
+ )->Obj.magic
432
+ | (_, Call({amount, assetId, to})) =>
433
+ (
434
+ {
435
+ to: to->Address.unsafeFromString,
436
+ assetId,
437
+ amount,
438
+ }: Internal.fuelTransferParams
439
+ )->Obj.magic
440
+ // This should never happen unless there's a bug in the routing logic
441
+ | _ => Js.Exn.raiseError("Unexpected bug in the event routing logic")
442
+ }
443
+
444
+ Internal.Event({
445
+ eventConfig: (eventConfig :> Internal.eventConfig),
446
+ timestamp: block.time,
447
+ chain,
448
+ blockNumber: block.height,
449
+ logIndex: receiptIndex,
450
+ event: {
451
+ chainId,
452
+ params,
453
+ transaction: {
454
+ "id": item.transactionId,
455
+ }->Obj.magic, // TODO: Obj.magic needed until the field selection types are not configurable for Fuel and Evm separately
456
+ block: block->Obj.magic,
457
+ srcAddress: contractAddress,
458
+ logIndex: receiptIndex,
459
+ },
460
+ })
461
+ })
462
+
463
+ let parsingTimeElapsed = parsingTimeRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis
464
+
465
+ let rangeLastBlock = await lastBlockQueriedPromise
466
+
467
+ let reorgGuard: ReorgDetection.reorgGuard = {
468
+ rangeLastBlock: rangeLastBlock->ReorgDetection.generalizeBlockDataWithTimestamp,
469
+ prevRangeLastBlock: None,
470
+ }
471
+
472
+ let totalTimeElapsed = totalTimeRef->Hrtime.timeSince->Hrtime.toMillis->Hrtime.intFromMillis
473
+
474
+ let stats = {
475
+ totalTimeElapsed,
476
+ parsingTimeElapsed,
477
+ pageFetchTime,
478
+ }
479
+
480
+ {
481
+ latestFetchedBlockTimestamp: rangeLastBlock.blockTimestamp,
482
+ parsedQueueItems,
483
+ latestFetchedBlockNumber: rangeLastBlock.blockNumber,
484
+ stats,
485
+ currentBlockHeight,
486
+ reorgGuard,
487
+ fromBlockQueried: fromBlock,
488
+ }
489
+ }
490
+
491
+ let getBlockHashes = (~blockNumbers as _, ~logger as _) =>
492
+ Js.Exn.raiseError("HyperFuel does not support getting block hashes")
493
+
494
+ let jsonApiClient = Rest.client(endpointUrl)
495
+
496
+ {
497
+ name,
498
+ sourceFor: Sync,
499
+ chain,
500
+ getBlockHashes,
501
+ pollingInterval: 100,
502
+ poweredByHyperSync: true,
503
+ getHeightOrThrow: () => HyperFuel.heightRoute->Rest.fetch((), ~client=jsonApiClient),
504
+ getItemsOrThrow,
505
+ }
506
+ }