envio 2.30.2 → 2.31.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.
@@ -5,6 +5,72 @@ let isPrimaryKey = true
5
5
  let isNullable = true
6
6
  let isIndex = true
7
7
 
8
+ module DynamicContractRegistry = {
9
+ let name = "dynamic_contract_registry"
10
+
11
+ let makeId = (~chainId, ~contractAddress) => {
12
+ chainId->Belt.Int.toString ++ "-" ++ contractAddress->Address.toString
13
+ }
14
+
15
+ // @genType Used for Test DB
16
+ @genType
17
+ type t = {
18
+ id: string,
19
+ @as("chain_id") chainId: int,
20
+ @as("registering_event_block_number") registeringEventBlockNumber: int,
21
+ @as("registering_event_log_index") registeringEventLogIndex: int,
22
+ @as("registering_event_block_timestamp") registeringEventBlockTimestamp: int,
23
+ @as("registering_event_contract_name") registeringEventContractName: string,
24
+ @as("registering_event_name") registeringEventName: string,
25
+ @as("registering_event_src_address") registeringEventSrcAddress: Address.t,
26
+ @as("contract_address") contractAddress: Address.t,
27
+ @as("contract_name") contractName: string,
28
+ }
29
+
30
+ let schema = S.schema(s => {
31
+ id: s.matches(S.string),
32
+ chainId: s.matches(S.int),
33
+ registeringEventBlockNumber: s.matches(S.int),
34
+ registeringEventLogIndex: s.matches(S.int),
35
+ registeringEventContractName: s.matches(S.string),
36
+ registeringEventName: s.matches(S.string),
37
+ registeringEventSrcAddress: s.matches(Address.schema),
38
+ registeringEventBlockTimestamp: s.matches(S.int),
39
+ contractAddress: s.matches(Address.schema),
40
+ contractName: s.matches(S.string),
41
+ })
42
+
43
+ let rowsSchema = S.array(schema)
44
+
45
+ let table = mkTable(
46
+ name,
47
+ ~fields=[
48
+ mkField("id", Text, ~isPrimaryKey, ~fieldSchema=S.string),
49
+ mkField("chain_id", Integer, ~fieldSchema=S.int),
50
+ mkField("registering_event_block_number", Integer, ~fieldSchema=S.int),
51
+ mkField("registering_event_log_index", Integer, ~fieldSchema=S.int),
52
+ mkField("registering_event_block_timestamp", Integer, ~fieldSchema=S.int),
53
+ mkField("registering_event_contract_name", Text, ~fieldSchema=S.string),
54
+ mkField("registering_event_name", Text, ~fieldSchema=S.string),
55
+ mkField("registering_event_src_address", Text, ~fieldSchema=Address.schema),
56
+ mkField("contract_address", Text, ~fieldSchema=Address.schema),
57
+ mkField("contract_name", Text, ~fieldSchema=S.string),
58
+ ],
59
+ )
60
+
61
+ let entityHistory = table->EntityHistory.fromTable(~schema)
62
+
63
+ external castToInternal: t => Internal.entity = "%identity"
64
+
65
+ let config = {
66
+ name,
67
+ schema,
68
+ rowsSchema,
69
+ table,
70
+ entityHistory,
71
+ }->Internal.fromGenericEntityConfig
72
+ }
73
+
8
74
  module Chains = {
9
75
  type progressFields = [
10
76
  | #progress_block
@@ -16,6 +82,7 @@ module Chains = {
16
82
  | #id
17
83
  | #start_block
18
84
  | #end_block
85
+ | #max_reorg_depth
19
86
  | #source_block
20
87
  | #first_event_block
21
88
  | #buffer_block
@@ -28,6 +95,7 @@ module Chains = {
28
95
  #id,
29
96
  #start_block,
30
97
  #end_block,
98
+ #max_reorg_depth,
31
99
  #source_block,
32
100
  #first_event_block,
33
101
  #buffer_block,
@@ -52,6 +120,7 @@ module Chains = {
52
120
  @as("id") id: int,
53
121
  @as("start_block") startBlock: int,
54
122
  @as("end_block") endBlock: Js.null<int>,
123
+ @as("max_reorg_depth") maxReorgDepth: int,
55
124
  @as("progress_block") progressBlockNumber: int,
56
125
  @as("events_processed") numEventsProcessed: int,
57
126
  ...metaFields,
@@ -64,6 +133,7 @@ module Chains = {
64
133
  // Values populated from config
65
134
  mkField((#start_block: field :> string), Integer, ~fieldSchema=S.int),
66
135
  mkField((#end_block: field :> string), Integer, ~fieldSchema=S.null(S.int), ~isNullable),
136
+ mkField((#max_reorg_depth: field :> string), Integer, ~fieldSchema=S.int),
67
137
  // Block number of the latest block that was fetched from the source
68
138
  mkField((#buffer_block: field :> string), Integer, ~fieldSchema=S.int),
69
139
  // Block number of the currently active source
@@ -98,6 +168,7 @@ module Chains = {
98
168
  id: chainConfig.id,
99
169
  startBlock: chainConfig.startBlock,
100
170
  endBlock: chainConfig.endBlock->Js.Null.fromOption,
171
+ maxReorgDepth: chainConfig.maxReorgDepth,
101
172
  blockHeight: 0,
102
173
  firstEventBlockNumber: Js.Null.empty,
103
174
  latestFetchedBlockNumber: -1,
@@ -160,7 +231,51 @@ VALUES ${valuesRows->Js.Array2.joinWith(",\n ")};`,
160
231
 
161
232
  `UPDATE "${pgSchema}"."${table.tableName}"
162
233
  SET ${setClauses->Js.Array2.joinWith(",\n ")}
163
- WHERE "id" = $1;`
234
+ WHERE "${(#id: field :> string)}" = $1;`
235
+ }
236
+
237
+ type rawInitialState = {
238
+ id: int,
239
+ startBlock: int,
240
+ endBlock: Js.Null.t<int>,
241
+ maxReorgDepth: int,
242
+ firstEventBlockNumber: Js.Null.t<int>,
243
+ timestampCaughtUpToHeadOrEndblock: Js.Null.t<Js.Date.t>,
244
+ numEventsProcessed: int,
245
+ progressBlockNumber: int,
246
+ dynamicContracts: array<Internal.indexingContract>,
247
+ }
248
+
249
+ // FIXME: Using registering_event_block_number for startBlock
250
+ // seems incorrect, since there might be a custom start block
251
+ // for the contract.
252
+ // TODO: Write a repro test where it might break something and fix
253
+ let makeGetInitialStateQuery = (~pgSchema) => {
254
+ `SELECT "${(#id: field :> string)}" as "id",
255
+ "${(#start_block: field :> string)}" as "startBlock",
256
+ "${(#end_block: field :> string)}" as "endBlock",
257
+ "${(#max_reorg_depth: field :> string)}" as "maxReorgDepth",
258
+ "${(#first_event_block: field :> string)}" as "firstEventBlockNumber",
259
+ "${(#ready_at: field :> string)}" as "timestampCaughtUpToHeadOrEndblock",
260
+ "${(#events_processed: field :> string)}" as "numEventsProcessed",
261
+ "${(#progress_block: field :> string)}" as "progressBlockNumber",
262
+ (
263
+ SELECT COALESCE(json_agg(json_build_object(
264
+ 'address', "contract_address",
265
+ 'contractName', "contract_name",
266
+ 'startBlock', "registering_event_block_number",
267
+ 'registrationBlock', "registering_event_block_number"
268
+ )), '[]'::json)
269
+ FROM "${pgSchema}"."${DynamicContractRegistry.table.tableName}"
270
+ WHERE "chain_id" = chains."${(#id: field :> string)}"
271
+ ) as "dynamicContracts"
272
+ FROM "${pgSchema}"."${table.tableName}" as chains;`
273
+ }
274
+
275
+ let getInitialState = (sql, ~pgSchema) => {
276
+ sql
277
+ ->Postgres.unsafe(makeGetInitialStateQuery(~pgSchema))
278
+ ->(Utils.magic: promise<array<unknown>> => promise<array<rawInitialState>>)
164
279
  }
165
280
 
166
281
  let progressFields: array<progressFields> = [#progress_block, #events_processed]
@@ -182,7 +297,7 @@ WHERE "id" = $1;`
182
297
 
183
298
  let promises = []
184
299
 
185
- chainsData->Utils.Dict.forEachWithKey((chainId, data) => {
300
+ chainsData->Utils.Dict.forEachWithKey((data, chainId) => {
186
301
  let params = []
187
302
 
188
303
  // Push id first (for WHERE clause)
@@ -201,7 +316,13 @@ WHERE "id" = $1;`
201
316
  Promise.all(promises)
202
317
  }
203
318
 
204
- let setProgressedChains = (sql, ~pgSchema, ~progressedChains: array<Batch.progressedChain>) => {
319
+ type progressedChain = {
320
+ chainId: int,
321
+ progressBlockNumber: int,
322
+ totalEventsProcessed: int,
323
+ }
324
+
325
+ let setProgressedChains = (sql, ~pgSchema, ~progressedChains: array<progressedChain>) => {
205
326
  let query = makeProgressFieldsUpdateQuery(~pgSchema)
206
327
 
207
328
  let promises = []
@@ -254,21 +375,175 @@ module PersistedState = {
254
375
  )
255
376
  }
256
377
 
257
- module EndOfBlockRangeScannedData = {
378
+ module Checkpoints = {
379
+ type field = [
380
+ | #id
381
+ | #chain_id
382
+ | #block_number
383
+ | #block_hash
384
+ | #events_processed
385
+ ]
386
+
258
387
  type t = {
259
- chain_id: int,
260
- block_number: int,
261
- block_hash: string,
388
+ id: int,
389
+ @as("chain_id")
390
+ chainId: int,
391
+ @as("block_number")
392
+ blockNumber: int,
393
+ @as("block_hash")
394
+ blockHash: Js.null<string>,
395
+ @as("events_processed")
396
+ eventsProcessed: int,
262
397
  }
263
398
 
399
+ let initialCheckpointId = 0
400
+
264
401
  let table = mkTable(
265
- "end_of_block_range_scanned_data",
402
+ "envio_checkpoints",
266
403
  ~fields=[
267
- mkField("chain_id", Integer, ~fieldSchema=S.int, ~isPrimaryKey),
268
- mkField("block_number", Integer, ~fieldSchema=S.int, ~isPrimaryKey),
269
- mkField("block_hash", Text, ~fieldSchema=S.string),
404
+ mkField((#id: field :> string), Integer, ~fieldSchema=S.int, ~isPrimaryKey),
405
+ mkField((#chain_id: field :> string), Integer, ~fieldSchema=S.int),
406
+ mkField((#block_number: field :> string), Integer, ~fieldSchema=S.int),
407
+ mkField((#block_hash: field :> string), Text, ~fieldSchema=S.null(S.string), ~isNullable),
408
+ mkField((#events_processed: field :> string), Integer, ~fieldSchema=S.int),
270
409
  ],
271
410
  )
411
+
412
+ let makeGetReorgCheckpointsQuery = (~pgSchema): string => {
413
+ // Use CTE to pre-filter chains and compute safe_block once per chain
414
+ // This is faster because:
415
+ // 1. Chains table is small, so filtering it first is cheap
416
+ // 2. safe_block is computed once per chain, not per checkpoint
417
+ // 3. Query planner can materialize the small CTE result before joining
418
+ `WITH reorg_chains AS (
419
+ SELECT
420
+ "${(#id: Chains.field :> string)}" as id,
421
+ "${(#source_block: Chains.field :> string)}" - "${(#max_reorg_depth: Chains.field :> string)}" AS safe_block
422
+ FROM "${pgSchema}"."${Chains.table.tableName}"
423
+ WHERE "${(#max_reorg_depth: Chains.field :> string)}" > 0
424
+ AND "${(#progress_block: Chains.field :> string)}" > "${(#source_block: Chains.field :> string)}" - "${(#max_reorg_depth: Chains.field :> string)}"
425
+ )
426
+ SELECT
427
+ cp."${(#id: field :> string)}",
428
+ cp."${(#chain_id: field :> string)}",
429
+ cp."${(#block_number: field :> string)}",
430
+ cp."${(#block_hash: field :> string)}"
431
+ FROM "${pgSchema}"."${table.tableName}" cp
432
+ INNER JOIN reorg_chains rc
433
+ ON cp."${(#chain_id: field :> string)}" = rc.id
434
+ WHERE cp."${(#block_hash: field :> string)}" IS NOT NULL
435
+ AND cp."${(#block_number: field :> string)}" >= rc.safe_block;` // Include safe_block checkpoint to use it for safe checkpoint tracking
436
+ }
437
+
438
+ let makeCommitedCheckpointIdQuery = (~pgSchema) => {
439
+ `SELECT COALESCE(MAX(${(#id: field :> string)}), ${initialCheckpointId->Belt.Int.toString}) AS id FROM "${pgSchema}"."${table.tableName}";`
440
+ }
441
+
442
+ let makeInsertCheckpointQuery = (~pgSchema) => {
443
+ `INSERT INTO "${pgSchema}"."${table.tableName}" ("${(#id: field :> string)}", "${(#chain_id: field :> string)}", "${(#block_number: field :> string)}", "${(#block_hash: field :> string)}", "${(#events_processed: field :> string)}")
444
+ SELECT * FROM unnest($1::${(Integer :> string)}[],$2::${(Integer :> string)}[],$3::${(Integer :> string)}[],$4::${(Text :> string)}[],$5::${(Integer :> string)}[]);`
445
+ }
446
+
447
+ let insert = (
448
+ sql,
449
+ ~pgSchema,
450
+ ~checkpointIds,
451
+ ~checkpointChainIds,
452
+ ~checkpointBlockNumbers,
453
+ ~checkpointBlockHashes,
454
+ ~checkpointEventsProcessed,
455
+ ) => {
456
+ let query = makeInsertCheckpointQuery(~pgSchema)
457
+
458
+ sql
459
+ ->Postgres.preparedUnsafe(
460
+ query,
461
+ (
462
+ checkpointIds,
463
+ checkpointChainIds,
464
+ checkpointBlockNumbers,
465
+ checkpointBlockHashes,
466
+ checkpointEventsProcessed,
467
+ )->(
468
+ Utils.magic: (
469
+ (array<int>, array<int>, array<int>, array<Js.Null.t<string>>, array<int>)
470
+ ) => unknown
471
+ ),
472
+ )
473
+ ->Promise.ignoreValue
474
+ }
475
+
476
+ let rollback = (sql, ~pgSchema, ~rollbackTargetCheckpointId: int) => {
477
+ sql
478
+ ->Postgres.preparedUnsafe(
479
+ `DELETE FROM "${pgSchema}"."${table.tableName}" WHERE "${(#id: field :> string)}" > $1;`,
480
+ [rollbackTargetCheckpointId]->Utils.magic,
481
+ )
482
+ ->Promise.ignoreValue
483
+ }
484
+
485
+ let makePruneStaleCheckpointsQuery = (~pgSchema) => {
486
+ `DELETE FROM "${pgSchema}"."${table.tableName}" WHERE "${(#id: field :> string)}" < $1;`
487
+ }
488
+
489
+ let pruneStaleCheckpoints = (sql, ~pgSchema, ~safeCheckpointId: int) => {
490
+ sql
491
+ ->Postgres.preparedUnsafe(
492
+ makePruneStaleCheckpointsQuery(~pgSchema),
493
+ [safeCheckpointId]->Obj.magic,
494
+ )
495
+ ->Promise.ignoreValue
496
+ }
497
+
498
+ let makeGetRollbackTargetCheckpointQuery = (~pgSchema) => {
499
+ `SELECT "${(#id: field :> string)}" FROM "${pgSchema}"."${table.tableName}"
500
+ WHERE
501
+ "${(#chain_id: field :> string)}" = $1 AND
502
+ "${(#block_number: field :> string)}" <= $2
503
+ ORDER BY "${(#id: field :> string)}" DESC
504
+ LIMIT 1;`
505
+ }
506
+
507
+ let getRollbackTargetCheckpoint = (
508
+ sql,
509
+ ~pgSchema,
510
+ ~reorgChainId: int,
511
+ ~lastKnownValidBlockNumber: int,
512
+ ) => {
513
+ sql
514
+ ->Postgres.preparedUnsafe(
515
+ makeGetRollbackTargetCheckpointQuery(~pgSchema),
516
+ (reorgChainId, lastKnownValidBlockNumber)->Obj.magic,
517
+ )
518
+ ->(Utils.magic: promise<unknown> => promise<array<{"id": int}>>)
519
+ }
520
+
521
+ let makeGetRollbackProgressDiffQuery = (~pgSchema) => {
522
+ `SELECT
523
+ "${(#chain_id: field :> string)}",
524
+ SUM("${(#events_processed: field :> string)}") as events_processed_diff,
525
+ MIN("${(#block_number: field :> string)}") - 1 as new_progress_block_number
526
+ FROM "${pgSchema}"."${table.tableName}"
527
+ WHERE "${(#id: field :> string)}" > $1
528
+ GROUP BY "${(#chain_id: field :> string)}";`
529
+ }
530
+
531
+ let getRollbackProgressDiff = (sql, ~pgSchema, ~rollbackTargetCheckpointId: int) => {
532
+ sql
533
+ ->Postgres.preparedUnsafe(
534
+ makeGetRollbackProgressDiffQuery(~pgSchema),
535
+ [rollbackTargetCheckpointId]->Obj.magic,
536
+ )
537
+ ->(
538
+ Utils.magic: promise<unknown> => promise<
539
+ array<{
540
+ "chain_id": int,
541
+ "events_processed_diff": string,
542
+ "new_progress_block_number": int,
543
+ }>,
544
+ >
545
+ )
546
+ }
272
547
  }
273
548
 
274
549
  module RawEvents = {
@@ -363,69 +638,3 @@ module Views = {
363
638
  FROM "${pgSchema}"."${Chains.table.tableName}";`
364
639
  }
365
640
  }
366
-
367
- module DynamicContractRegistry = {
368
- let name = "dynamic_contract_registry"
369
-
370
- let makeId = (~chainId, ~contractAddress) => {
371
- chainId->Belt.Int.toString ++ "-" ++ contractAddress->Address.toString
372
- }
373
-
374
- // @genType Used for Test DB
375
- @genType
376
- type t = {
377
- id: string,
378
- @as("chain_id") chainId: int,
379
- @as("registering_event_block_number") registeringEventBlockNumber: int,
380
- @as("registering_event_log_index") registeringEventLogIndex: int,
381
- @as("registering_event_block_timestamp") registeringEventBlockTimestamp: int,
382
- @as("registering_event_contract_name") registeringEventContractName: string,
383
- @as("registering_event_name") registeringEventName: string,
384
- @as("registering_event_src_address") registeringEventSrcAddress: Address.t,
385
- @as("contract_address") contractAddress: Address.t,
386
- @as("contract_name") contractName: string,
387
- }
388
-
389
- let schema = S.schema(s => {
390
- id: s.matches(S.string),
391
- chainId: s.matches(S.int),
392
- registeringEventBlockNumber: s.matches(S.int),
393
- registeringEventLogIndex: s.matches(S.int),
394
- registeringEventContractName: s.matches(S.string),
395
- registeringEventName: s.matches(S.string),
396
- registeringEventSrcAddress: s.matches(Address.schema),
397
- registeringEventBlockTimestamp: s.matches(S.int),
398
- contractAddress: s.matches(Address.schema),
399
- contractName: s.matches(S.string),
400
- })
401
-
402
- let rowsSchema = S.array(schema)
403
-
404
- let table = mkTable(
405
- name,
406
- ~fields=[
407
- mkField("id", Text, ~isPrimaryKey, ~fieldSchema=S.string),
408
- mkField("chain_id", Integer, ~fieldSchema=S.int),
409
- mkField("registering_event_block_number", Integer, ~fieldSchema=S.int),
410
- mkField("registering_event_log_index", Integer, ~fieldSchema=S.int),
411
- mkField("registering_event_block_timestamp", Integer, ~fieldSchema=S.int),
412
- mkField("registering_event_contract_name", Text, ~fieldSchema=S.string),
413
- mkField("registering_event_name", Text, ~fieldSchema=S.string),
414
- mkField("registering_event_src_address", Text, ~fieldSchema=Address.schema),
415
- mkField("contract_address", Text, ~fieldSchema=Address.schema),
416
- mkField("contract_name", Text, ~fieldSchema=S.string),
417
- ],
418
- )
419
-
420
- let entityHistory = table->EntityHistory.fromTable(~schema)
421
-
422
- external castToInternal: t => Internal.entity = "%identity"
423
-
424
- let config = {
425
- name,
426
- schema,
427
- rowsSchema,
428
- table,
429
- entityHistory,
430
- }->Internal.fromGenericEntityConfig
431
- }