envio 3.0.0-alpha.2 → 3.0.0-alpha.3

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 (60) hide show
  1. package/evm.schema.json +44 -33
  2. package/fuel.schema.json +32 -21
  3. package/index.d.ts +1 -0
  4. package/package.json +7 -6
  5. package/src/Batch.res.mjs +1 -1
  6. package/src/Benchmark.res +394 -0
  7. package/src/Benchmark.res.mjs +398 -0
  8. package/src/ChainFetcher.res +459 -0
  9. package/src/ChainFetcher.res.mjs +281 -0
  10. package/src/ChainManager.res +179 -0
  11. package/src/ChainManager.res.mjs +139 -0
  12. package/src/Config.res +15 -1
  13. package/src/Config.res.mjs +27 -4
  14. package/src/Ecosystem.res +9 -124
  15. package/src/Ecosystem.res.mjs +19 -160
  16. package/src/Env.res +0 -1
  17. package/src/Env.res.mjs +0 -3
  18. package/src/Envio.gen.ts +9 -1
  19. package/src/Envio.res +12 -9
  20. package/src/EventProcessing.res +476 -0
  21. package/src/EventProcessing.res.mjs +341 -0
  22. package/src/FetchState.res +54 -29
  23. package/src/FetchState.res.mjs +62 -35
  24. package/src/GlobalState.res +1169 -0
  25. package/src/GlobalState.res.mjs +1196 -0
  26. package/src/Internal.res +2 -1
  27. package/src/LoadLayer.res +444 -0
  28. package/src/LoadLayer.res.mjs +296 -0
  29. package/src/LoadLayer.resi +32 -0
  30. package/src/Prometheus.res +8 -8
  31. package/src/Prometheus.res.mjs +10 -10
  32. package/src/ReorgDetection.res +6 -10
  33. package/src/ReorgDetection.res.mjs +6 -6
  34. package/src/UserContext.res +356 -0
  35. package/src/UserContext.res.mjs +238 -0
  36. package/src/bindings/DateFns.res +71 -0
  37. package/src/bindings/DateFns.res.mjs +22 -0
  38. package/src/sources/Evm.res +87 -0
  39. package/src/sources/Evm.res.mjs +105 -0
  40. package/src/sources/EvmChain.res +95 -0
  41. package/src/sources/EvmChain.res.mjs +61 -0
  42. package/src/sources/Fuel.res +19 -34
  43. package/src/sources/Fuel.res.mjs +34 -16
  44. package/src/sources/FuelSDK.res +37 -0
  45. package/src/sources/FuelSDK.res.mjs +29 -0
  46. package/src/sources/HyperFuel.res +2 -2
  47. package/src/sources/HyperFuel.resi +1 -1
  48. package/src/sources/HyperFuelClient.res +2 -2
  49. package/src/sources/HyperFuelSource.res +8 -8
  50. package/src/sources/HyperFuelSource.res.mjs +5 -5
  51. package/src/sources/HyperSyncSource.res +5 -5
  52. package/src/sources/HyperSyncSource.res.mjs +5 -5
  53. package/src/sources/RpcSource.res +4 -4
  54. package/src/sources/RpcSource.res.mjs +3 -3
  55. package/src/sources/Solana.res +59 -0
  56. package/src/sources/Solana.res.mjs +79 -0
  57. package/src/sources/Source.res +2 -2
  58. package/src/sources/SourceManager.res +24 -32
  59. package/src/sources/SourceManager.res.mjs +20 -20
  60. package/src/sources/SourceManager.resi +4 -5
@@ -0,0 +1,459 @@
1
+ open Belt
2
+
3
+ //A filter should return true if the event should be kept and isValid should return
4
+ //false when the filter should be removed/cleaned up
5
+ type processingFilter = {
6
+ filter: Internal.item => bool,
7
+ isValid: (~fetchState: FetchState.t) => bool,
8
+ }
9
+
10
+ type t = {
11
+ logger: Pino.t,
12
+ fetchState: FetchState.t,
13
+ sourceManager: SourceManager.t,
14
+ chainConfig: Config.chain,
15
+ isProgressAtHead: bool,
16
+ timestampCaughtUpToHeadOrEndblock: option<Js.Date.t>,
17
+ committedProgressBlockNumber: int,
18
+ firstEventBlockNumber: option<int>,
19
+ numEventsProcessed: int,
20
+ numBatchesFetched: int,
21
+ reorgDetection: ReorgDetection.t,
22
+ safeCheckpointTracking: option<SafeCheckpointTracking.t>,
23
+ }
24
+
25
+ //CONSTRUCTION
26
+ let make = (
27
+ ~chainConfig: Config.chain,
28
+ ~dynamicContracts: array<Internal.indexingContract>,
29
+ ~startBlock,
30
+ ~endBlock,
31
+ ~firstEventBlockNumber,
32
+ ~progressBlockNumber,
33
+ ~config: Config.t,
34
+ ~registrations: EventRegister.registrations,
35
+ ~targetBufferSize,
36
+ ~logger,
37
+ ~timestampCaughtUpToHeadOrEndblock,
38
+ ~numEventsProcessed,
39
+ ~numBatchesFetched,
40
+ ~isInReorgThreshold,
41
+ ~reorgCheckpoints: array<Internal.reorgCheckpoint>,
42
+ ~maxReorgDepth,
43
+ ): t => {
44
+ // We don't need the router itself, but only validation logic,
45
+ // since now event router is created for selection of events
46
+ // and validation doesn't work correctly in routers.
47
+ // Ideally to split it into two different parts.
48
+ let eventRouter = EventRouter.empty()
49
+
50
+ // Aggregate events we want to fetch
51
+ let contracts = []
52
+ let eventConfigs: array<Internal.eventConfig> = []
53
+
54
+ let notRegisteredEvents = []
55
+
56
+ chainConfig.contracts->Array.forEach(contract => {
57
+ let contractName = contract.name
58
+
59
+ contract.events->Array.forEach(eventConfig => {
60
+ let {isWildcard} = eventConfig
61
+ let hasContractRegister = eventConfig.contractRegister->Option.isSome
62
+
63
+ // Should validate the events
64
+ eventRouter->EventRouter.addOrThrow(
65
+ eventConfig.id,
66
+ (),
67
+ ~contractName,
68
+ ~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
69
+ ~eventName=eventConfig.name,
70
+ ~isWildcard,
71
+ )
72
+
73
+ // Filter out non-preRegistration events on preRegistration phase
74
+ // so we don't care about it in fetch state and workers anymore
75
+ let shouldBeIncluded = if config.enableRawEvents {
76
+ true
77
+ } else {
78
+ let isRegistered = hasContractRegister || eventConfig.handler->Option.isSome
79
+ if !isRegistered {
80
+ notRegisteredEvents->Array.push(eventConfig)
81
+ }
82
+ isRegistered
83
+ }
84
+
85
+ if shouldBeIncluded {
86
+ eventConfigs->Array.push(eventConfig)
87
+ }
88
+ })
89
+
90
+ switch contract.startBlock {
91
+ | Some(startBlock) if startBlock < chainConfig.startBlock =>
92
+ Js.Exn.raiseError(
93
+ `The start block for contract "${contractName}" is less than the chain start block. This is not supported yet.`,
94
+ )
95
+ | _ => ()
96
+ }
97
+
98
+ contract.addresses->Array.forEach(address => {
99
+ contracts->Array.push({
100
+ Internal.address,
101
+ contractName: contract.name,
102
+ startBlock: switch contract.startBlock {
103
+ | Some(startBlock) => startBlock
104
+ | None => chainConfig.startBlock
105
+ },
106
+ registrationBlock: None,
107
+ })
108
+ })
109
+ })
110
+
111
+ dynamicContracts->Array.forEach(dc => contracts->Array.push(dc))
112
+
113
+ if notRegisteredEvents->Utils.Array.notEmpty {
114
+ logger->Logging.childInfo(
115
+ `The event${if notRegisteredEvents->Array.length > 1 {
116
+ "s"
117
+ } else {
118
+ ""
119
+ }} ${notRegisteredEvents
120
+ ->Array.map(eventConfig => `${eventConfig.contractName}.${eventConfig.name}`)
121
+ ->Js.Array2.joinWith(", ")} don't have an event handler and skipped for indexing.`,
122
+ )
123
+ }
124
+
125
+ let onBlockConfigs =
126
+ registrations.onBlockByChainId->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString)
127
+ switch onBlockConfigs {
128
+ | Some(onBlockConfigs) =>
129
+ // TODO: Move it to the EventRegister module
130
+ // so the error is thrown with better stack trace
131
+ onBlockConfigs->Array.forEach(onBlockConfig => {
132
+ if onBlockConfig.startBlock->Option.getWithDefault(startBlock) < startBlock {
133
+ Js.Exn.raiseError(
134
+ `The start block for onBlock handler "${onBlockConfig.name}" is less than the chain start block (${startBlock->Belt.Int.toString}). This is not supported yet.`,
135
+ )
136
+ }
137
+ switch endBlock {
138
+ | Some(chainEndBlock) =>
139
+ if onBlockConfig.endBlock->Option.getWithDefault(chainEndBlock) > chainEndBlock {
140
+ Js.Exn.raiseError(
141
+ `The end block for onBlock handler "${onBlockConfig.name}" is greater than the chain end block (${chainEndBlock->Belt.Int.toString}). This is not supported yet.`,
142
+ )
143
+ }
144
+ | None => ()
145
+ }
146
+ })
147
+ | None => ()
148
+ }
149
+
150
+ let fetchState = FetchState.make(
151
+ ~maxAddrInPartition=config.maxAddrInPartition,
152
+ ~contracts,
153
+ ~progressBlockNumber,
154
+ ~startBlock,
155
+ ~endBlock,
156
+ ~eventConfigs,
157
+ ~targetBufferSize,
158
+ ~knownHeight=0, // FIXME: Get it from db or fetch before creating FetchState
159
+ ~chainId=chainConfig.id,
160
+ // FIXME: Shouldn't set with full history
161
+ ~blockLag=Pervasives.max(
162
+ !config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth,
163
+ Env.indexingBlockLag->Option.getWithDefault(0),
164
+ ),
165
+ ~onBlockConfigs?,
166
+ )
167
+
168
+ let chainReorgCheckpoints = reorgCheckpoints->Array.keepMapU(reorgCheckpoint => {
169
+ if reorgCheckpoint.chainId === chainConfig.id {
170
+ Some(reorgCheckpoint)
171
+ } else {
172
+ None
173
+ }
174
+ })
175
+
176
+ {
177
+ logger,
178
+ chainConfig,
179
+ sourceManager: SourceManager.make(
180
+ ~sources=chainConfig.sources,
181
+ ~maxPartitionConcurrency=Env.maxPartitionConcurrency,
182
+ ),
183
+ reorgDetection: ReorgDetection.make(
184
+ ~chainReorgCheckpoints,
185
+ ~maxReorgDepth,
186
+ ~shouldRollbackOnReorg=config.shouldRollbackOnReorg,
187
+ ),
188
+ safeCheckpointTracking: SafeCheckpointTracking.make(
189
+ ~maxReorgDepth,
190
+ ~shouldRollbackOnReorg=config.shouldRollbackOnReorg,
191
+ ~chainReorgCheckpoints,
192
+ ),
193
+ isProgressAtHead: false,
194
+ fetchState,
195
+ firstEventBlockNumber,
196
+ committedProgressBlockNumber: progressBlockNumber,
197
+ timestampCaughtUpToHeadOrEndblock,
198
+ numEventsProcessed,
199
+ numBatchesFetched,
200
+ }
201
+ }
202
+
203
+ let makeFromConfig = (chainConfig: Config.chain, ~config, ~registrations, ~targetBufferSize) => {
204
+ let logger = Logging.createChild(~params={"chainId": chainConfig.id})
205
+
206
+ make(
207
+ ~chainConfig,
208
+ ~config,
209
+ ~registrations,
210
+ ~startBlock=chainConfig.startBlock,
211
+ ~endBlock=chainConfig.endBlock,
212
+ ~reorgCheckpoints=[],
213
+ ~maxReorgDepth=chainConfig.maxReorgDepth,
214
+ ~firstEventBlockNumber=None,
215
+ ~progressBlockNumber=-1,
216
+ ~timestampCaughtUpToHeadOrEndblock=None,
217
+ ~numEventsProcessed=0,
218
+ ~numBatchesFetched=0,
219
+ ~targetBufferSize,
220
+ ~logger,
221
+ ~dynamicContracts=[],
222
+ ~isInReorgThreshold=false,
223
+ )
224
+ }
225
+
226
+ /**
227
+ * This function allows a chain fetcher to be created from metadata, in particular this is useful for restarting an indexer and making sure it fetches blocks from the same place.
228
+ */
229
+ let makeFromDbState = async (
230
+ chainConfig: Config.chain,
231
+ ~resumedChainState: Persistence.initialChainState,
232
+ ~reorgCheckpoints,
233
+ ~isInReorgThreshold,
234
+ ~config,
235
+ ~registrations,
236
+ ~targetBufferSize,
237
+ ) => {
238
+ let chainId = chainConfig.id
239
+ let logger = Logging.createChild(~params={"chainId": chainId})
240
+
241
+ Prometheus.ProgressEventsCount.set(~processedCount=resumedChainState.numEventsProcessed, ~chainId)
242
+
243
+ let progressBlockNumber =
244
+ // Can be -1 when not set
245
+ resumedChainState.progressBlockNumber >= 0
246
+ ? resumedChainState.progressBlockNumber
247
+ : resumedChainState.startBlock - 1
248
+
249
+ make(
250
+ ~dynamicContracts=resumedChainState.dynamicContracts,
251
+ ~chainConfig,
252
+ ~startBlock=resumedChainState.startBlock,
253
+ ~endBlock=resumedChainState.endBlock,
254
+ ~config,
255
+ ~registrations,
256
+ ~reorgCheckpoints,
257
+ ~maxReorgDepth=resumedChainState.maxReorgDepth,
258
+ ~firstEventBlockNumber=resumedChainState.firstEventBlockNumber,
259
+ ~progressBlockNumber,
260
+ ~timestampCaughtUpToHeadOrEndblock=Env.updateSyncTimeOnRestart
261
+ ? None
262
+ : resumedChainState.timestampCaughtUpToHeadOrEndblock,
263
+ ~numEventsProcessed=resumedChainState.numEventsProcessed,
264
+ ~numBatchesFetched=0,
265
+ ~logger,
266
+ ~targetBufferSize,
267
+ ~isInReorgThreshold,
268
+ )
269
+ }
270
+
271
+ /**
272
+ * Helper function to get the configured start block for a contract from config
273
+ */
274
+ let getContractStartBlock = (
275
+ config: Config.t,
276
+ ~chain: ChainMap.Chain.t,
277
+ ~contractName: string,
278
+ ): option<int> => {
279
+ let chainConfig = config.chainMap->ChainMap.get(chain)
280
+ chainConfig.contracts
281
+ ->Js.Array2.find(contract => contract.name === contractName)
282
+ ->Option.flatMap(contract => contract.startBlock)
283
+ }
284
+
285
+ let runContractRegistersOrThrow = async (
286
+ ~itemsWithContractRegister: array<Internal.item>,
287
+ ~chain: ChainMap.Chain.t,
288
+ ~config: Config.t,
289
+ ) => {
290
+ let itemsWithDcs = []
291
+
292
+ let onRegister = (~item: Internal.item, ~contractAddress, ~contractName) => {
293
+ let eventItem = item->Internal.castUnsafeEventItem
294
+ let {blockNumber} = eventItem
295
+
296
+ // Use contract-specific start block if configured, otherwise fall back to registration block
297
+ let contractStartBlock = switch getContractStartBlock(config, ~chain, ~contractName) {
298
+ | Some(configuredStartBlock) => configuredStartBlock
299
+ | None => blockNumber
300
+ }
301
+
302
+ let dc: Internal.indexingContract = {
303
+ address: contractAddress,
304
+ contractName,
305
+ startBlock: contractStartBlock,
306
+ registrationBlock: Some(blockNumber),
307
+ }
308
+
309
+ switch item->Internal.getItemDcs {
310
+ | None => {
311
+ item->Internal.setItemDcs([dc])
312
+ itemsWithDcs->Array.push(item)
313
+ }
314
+ | Some(dcs) => dcs->Array.push(dc)
315
+ }
316
+ }
317
+
318
+ let promises = []
319
+ for idx in 0 to itemsWithContractRegister->Array.length - 1 {
320
+ let item = itemsWithContractRegister->Array.getUnsafe(idx)
321
+ let eventItem = item->Internal.castUnsafeEventItem
322
+ let contractRegister = switch eventItem {
323
+ | {eventConfig: {contractRegister: Some(contractRegister)}} => contractRegister
324
+ | {eventConfig: {contractRegister: None, name: eventName}} =>
325
+ // Unexpected case, since we should pass only events with contract register to this function
326
+ Js.Exn.raiseError("Contract register is not set for event " ++ eventName)
327
+ }
328
+
329
+ let errorMessage = "Event contractRegister failed, please fix the error to keep the indexer running smoothly"
330
+
331
+ // Catch sync and async errors
332
+ try {
333
+ let params: UserContext.contractRegisterParams = {
334
+ item,
335
+ onRegister,
336
+ config,
337
+ isResolved: false,
338
+ }
339
+ let result = contractRegister(UserContext.getContractRegisterArgs(params))
340
+
341
+ // Even though `contractRegister` always returns a promise,
342
+ // in the ReScript type, but it might return a non-promise value for TS API.
343
+ if result->Promise.isCatchable {
344
+ promises->Array.push(
345
+ result
346
+ ->Promise.thenResolve(r => {
347
+ params.isResolved = true
348
+ r
349
+ })
350
+ ->Promise.catch(exn => {
351
+ params.isResolved = true
352
+ exn->ErrorHandling.mkLogAndRaise(~msg=errorMessage, ~logger=item->Logging.getItemLogger)
353
+ }),
354
+ )
355
+ } else {
356
+ params.isResolved = true
357
+ }
358
+ } catch {
359
+ | exn =>
360
+ exn->ErrorHandling.mkLogAndRaise(~msg=errorMessage, ~logger=item->Logging.getItemLogger)
361
+ }
362
+ }
363
+
364
+ if promises->Utils.Array.notEmpty {
365
+ let _ = await Promise.all(promises)
366
+ }
367
+
368
+ itemsWithDcs
369
+ }
370
+
371
+ let handleQueryResult = (
372
+ chainFetcher: t,
373
+ ~query: FetchState.query,
374
+ ~newItems,
375
+ ~newItemsWithDcs,
376
+ ~latestFetchedBlock,
377
+ ~knownHeight,
378
+ ) => {
379
+ let fs = switch newItemsWithDcs {
380
+ | [] => chainFetcher.fetchState
381
+ | _ => chainFetcher.fetchState->FetchState.registerDynamicContracts(newItemsWithDcs)
382
+ }
383
+
384
+ fs
385
+ ->FetchState.handleQueryResult(~query, ~latestFetchedBlock, ~newItems)
386
+ ->Result.map(fs => {
387
+ ...chainFetcher,
388
+ fetchState: fs->FetchState.updateKnownHeight(~knownHeight),
389
+ })
390
+ }
391
+
392
+ /**
393
+ Gets the latest item on the front of the queue and returns updated fetcher
394
+ */
395
+ let hasProcessedToEndblock = (self: t) => {
396
+ let {committedProgressBlockNumber, fetchState} = self
397
+ switch fetchState.endBlock {
398
+ | Some(endBlock) => committedProgressBlockNumber >= endBlock
399
+ | None => false
400
+ }
401
+ }
402
+
403
+ let hasNoMoreEventsToProcess = (self: t) => {
404
+ self.fetchState->FetchState.bufferSize === 0
405
+ }
406
+
407
+ let getHighestBlockBelowThreshold = (cf: t): int => {
408
+ let highestBlockBelowThreshold = cf.fetchState.knownHeight - cf.chainConfig.maxReorgDepth
409
+ highestBlockBelowThreshold < 0 ? 0 : highestBlockBelowThreshold
410
+ }
411
+
412
+ /**
413
+ Finds the last known valid block number below the reorg block
414
+ If not found, returns the highest block below threshold
415
+ */
416
+ let getLastKnownValidBlock = async (
417
+ chainFetcher: t,
418
+ ~reorgBlockNumber: int,
419
+ //Parameter used for dependency injecting in tests
420
+ ~getBlockHashes=(chainFetcher.sourceManager->SourceManager.getActiveSource).getBlockHashes,
421
+ ) => {
422
+ // Improtant: It's important to not include the reorg detection block number
423
+ // because there might be different instances of the source
424
+ // with mismatching hashes between them.
425
+ // So we MUST always rollback the block number where we detected a reorg.
426
+ let scannedBlockNumbers =
427
+ chainFetcher.reorgDetection->ReorgDetection.getThresholdBlockNumbersBelowBlock(
428
+ ~blockNumber=reorgBlockNumber,
429
+ ~knownHeight=chainFetcher.fetchState.knownHeight,
430
+ )
431
+
432
+ let getBlockHashes = blockNumbers => {
433
+ getBlockHashes(~blockNumbers, ~logger=chainFetcher.logger)->Promise.thenResolve(res =>
434
+ switch res {
435
+ | Ok(v) => v
436
+ | Error(exn) =>
437
+ exn->ErrorHandling.mkLogAndRaise(
438
+ ~msg="Failed to fetch blockHashes for given blockNumbers during rollback",
439
+ )
440
+ }
441
+ )
442
+ }
443
+
444
+ switch scannedBlockNumbers {
445
+ | [] => chainFetcher->getHighestBlockBelowThreshold
446
+ | _ => {
447
+ let blockNumbersAndHashes = await getBlockHashes(scannedBlockNumbers)
448
+
449
+ switch chainFetcher.reorgDetection->ReorgDetection.getLatestValidScannedBlock(
450
+ ~blockNumbersAndHashes,
451
+ ) {
452
+ | Some(blockNumber) => blockNumber
453
+ | None => chainFetcher->getHighestBlockBelowThreshold
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ let isActivelyIndexing = (chainFetcher: t) => chainFetcher.fetchState->FetchState.isActivelyIndexing