envio 2.27.6 → 2.28.0-alpha.2

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,430 @@
1
+ open Table
2
+
3
+ //shorthand for punning
4
+ let isPrimaryKey = true
5
+ let isNullable = true
6
+ let isIndex = true
7
+
8
+ module EventSyncState = {
9
+ //Used unsafely in DbFunctions.res so just enforcing the naming here
10
+ let blockTimestampFieldName = "block_timestamp"
11
+ let blockNumberFieldName = "block_number"
12
+ let logIndexFieldName = "log_index"
13
+ let isPreRegisteringDynamicContractsFieldName = "is_pre_registering_dynamic_contracts"
14
+
15
+ // @genType Used for Test DB
16
+ @genType
17
+ type t = {
18
+ @as("chain_id") chainId: int,
19
+ @as("block_number") blockNumber: int,
20
+ @as("log_index") logIndex: int,
21
+ @as("block_timestamp") blockTimestamp: int,
22
+ }
23
+
24
+ let table = mkTable(
25
+ "event_sync_state",
26
+ ~fields=[
27
+ mkField("chain_id", Integer, ~fieldSchema=S.int, ~isPrimaryKey),
28
+ mkField(blockNumberFieldName, Integer, ~fieldSchema=S.int),
29
+ mkField(logIndexFieldName, Integer, ~fieldSchema=S.int),
30
+ mkField(blockTimestampFieldName, Integer, ~fieldSchema=S.int),
31
+ // Keep it in case Hosted Service relies on it to prevent a breaking changes
32
+ mkField(
33
+ isPreRegisteringDynamicContractsFieldName,
34
+ Boolean,
35
+ ~default="false",
36
+ ~fieldSchema=S.bool,
37
+ ),
38
+ ],
39
+ )
40
+
41
+ //We need to update values here not delet the rows, since restarting without a row
42
+ //has a different behaviour to restarting with an initialised row with zero values
43
+ let resetCurrentCurrentSyncStateQuery = (~pgSchema) =>
44
+ `UPDATE "${pgSchema}"."${table.tableName}"
45
+ SET ${blockNumberFieldName} = 0,
46
+ ${logIndexFieldName} = 0,
47
+ ${blockTimestampFieldName} = 0,
48
+ ${isPreRegisteringDynamicContractsFieldName} = false;`
49
+ }
50
+
51
+ module Chains = {
52
+ type field = [
53
+ | #id
54
+ | #start_block
55
+ | #end_block
56
+ | #source_block
57
+ | #first_event_block
58
+ | #buffer_block
59
+ | #ready_at
60
+ | #events_processed
61
+ | #_is_hyper_sync
62
+ | #_latest_processed_block
63
+ | #_num_batches_fetched
64
+ ]
65
+
66
+ let fields: array<field> = [
67
+ #id,
68
+ #start_block,
69
+ #end_block,
70
+ #source_block,
71
+ #first_event_block,
72
+ #buffer_block,
73
+ #ready_at,
74
+ #events_processed,
75
+ #_is_hyper_sync,
76
+ #_latest_processed_block,
77
+ #_num_batches_fetched,
78
+ ]
79
+
80
+ type t = {
81
+ @as("id") id: int,
82
+ @as("start_block") startBlock: int,
83
+ @as("end_block") endBlock: Js.null<int>,
84
+ @as("source_block") blockHeight: int,
85
+ @as("first_event_block") firstEventBlockNumber: Js.null<int>,
86
+ @as("buffer_block") latestFetchedBlockNumber: int,
87
+ @as("ready_at")
88
+ timestampCaughtUpToHeadOrEndblock: Js.null<Js.Date.t>,
89
+ @as("events_processed") numEventsProcessed: int,
90
+ @as("_latest_processed_block") latestProcessedBlock: Js.null<int>,
91
+ @as("_is_hyper_sync") isHyperSync: bool,
92
+ @as("_num_batches_fetched") numBatchesFetched: int,
93
+ }
94
+
95
+ let table = mkTable(
96
+ "envio_chains",
97
+ ~fields=[
98
+ mkField((#id: field :> string), Integer, ~fieldSchema=S.int, ~isPrimaryKey),
99
+ // Values populated from config
100
+ mkField((#start_block: field :> string), Integer, ~fieldSchema=S.int),
101
+ mkField((#end_block: field :> string), Integer, ~fieldSchema=S.null(S.int), ~isNullable),
102
+ // Block number of the latest block that was fetched from the source
103
+ mkField((#buffer_block: field :> string), Integer, ~fieldSchema=S.int),
104
+ // Block number of the currently active source
105
+ mkField((#source_block: field :> string), Integer, ~fieldSchema=S.int),
106
+ // Block number of the first event that was processed for this chain
107
+ mkField(
108
+ (#first_event_block: field :> string),
109
+ Integer,
110
+ ~fieldSchema=S.null(S.int),
111
+ ~isNullable,
112
+ ),
113
+ // Used to show how much time historical sync has taken, so we need a timezone here (TUI and Hosted Service)
114
+ // null during historical sync, set to current time when sync is complete
115
+ mkField(
116
+ (#ready_at: field :> string),
117
+ TimestampWithNullTimezone,
118
+ ~fieldSchema=S.null(Utils.Schema.dbDate),
119
+ ~isNullable,
120
+ ),
121
+ mkField((#events_processed: field :> string), Integer, ~fieldSchema=S.int), // TODO: In the future it should reference a table with sources
122
+ mkField((#_is_hyper_sync: field :> string), Boolean, ~fieldSchema=S.bool),
123
+ // TODO: Make the data more public facing
124
+ mkField(
125
+ (#_latest_processed_block: field :> string),
126
+ Integer,
127
+ ~fieldSchema=S.null(S.int),
128
+ ~isNullable,
129
+ ),
130
+ mkField((#_num_batches_fetched: field :> string), Integer, ~fieldSchema=S.int),
131
+ ],
132
+ )
133
+
134
+ let initialFromConfig = (chainConfig: InternalConfig.chain) => {
135
+ {
136
+ id: chainConfig.id,
137
+ startBlock: chainConfig.startBlock,
138
+ endBlock: chainConfig.endBlock->Js.Null.fromOption,
139
+ blockHeight: 0,
140
+ firstEventBlockNumber: Js.Null.empty,
141
+ latestFetchedBlockNumber: -1,
142
+ timestampCaughtUpToHeadOrEndblock: Js.Null.empty,
143
+ latestProcessedBlock: Js.Null.empty,
144
+ isHyperSync: false,
145
+ numEventsProcessed: 0,
146
+ numBatchesFetched: 0,
147
+ }
148
+ }
149
+
150
+ let makeInitialValuesQuery = (~pgSchema, ~chainConfigs: array<InternalConfig.chain>) => {
151
+ if chainConfigs->Array.length === 0 {
152
+ None
153
+ } else {
154
+ // Create column names list
155
+ let columnNames = fields->Belt.Array.map(field => `"${(field :> string)}"`)
156
+
157
+ // Create VALUES rows for each chain config
158
+ let valuesRows = chainConfigs->Belt.Array.map(chainConfig => {
159
+ let initialValues = initialFromConfig(chainConfig)
160
+ let values = fields->Belt.Array.map((field: field) => {
161
+ let value =
162
+ initialValues->(Utils.magic: t => dict<unknown>)->Js.Dict.get((field :> string))
163
+ switch Js.typeof(value) {
164
+ | "object" => "NULL"
165
+ | "number" => value->Utils.magic->Belt.Int.toString
166
+ | "boolean" => value->Utils.magic ? "true" : "false"
167
+ | _ => Js.Exn.raiseError("Invalid envio_chains value type")
168
+ }
169
+ })
170
+
171
+ `(${values->Js.Array2.joinWith(", ")})`
172
+ })
173
+
174
+ Some(
175
+ `INSERT INTO "${pgSchema}"."${table.tableName}" (${columnNames->Js.Array2.joinWith(", ")})
176
+ VALUES ${valuesRows->Js.Array2.joinWith(",\n ")};`,
177
+ )
178
+ }
179
+ }
180
+
181
+ // Fields that should be updated on conflict (excluding static config fields)
182
+ let updateFields: array<field> = [
183
+ #source_block,
184
+ #first_event_block,
185
+ #buffer_block,
186
+ #ready_at,
187
+ #events_processed,
188
+ #_is_hyper_sync,
189
+ #_latest_processed_block,
190
+ #_num_batches_fetched,
191
+ ]
192
+
193
+ let makeSingleUpdateQuery = (~pgSchema) => {
194
+ // Generate SET clauses with parameter placeholders
195
+ let setClauses = Belt.Array.mapWithIndex(updateFields, (index, field) => {
196
+ let fieldName = (field :> string)
197
+ let paramIndex = index + 2 // +2 because $1 is for id in WHERE clause
198
+ `"${fieldName}" = $${Belt.Int.toString(paramIndex)}`
199
+ })
200
+
201
+ `UPDATE "${pgSchema}"."${table.tableName}"
202
+ SET ${setClauses->Js.Array2.joinWith(",\n ")}
203
+ WHERE "id" = $1;`
204
+ }
205
+
206
+ let setValues = (sql, ~pgSchema, ~chainsData: array<t>) => {
207
+ let query = makeSingleUpdateQuery(~pgSchema)
208
+
209
+ let promises = chainsData->Belt.Array.map(chain => {
210
+ let params = []
211
+
212
+ // Push id first (for WHERE clause)
213
+ let idValue = chain->(Utils.magic: t => dict<unknown>)->Js.Dict.get("id")
214
+ params->Js.Array2.push(idValue)->ignore
215
+
216
+ // Then push all updateable field values (for SET clause)
217
+ updateFields->Js.Array2.forEach(field => {
218
+ let value = chain->(Utils.magic: t => dict<unknown>)->Js.Dict.get((field :> string))
219
+ params->Js.Array2.push(value)->ignore
220
+ })
221
+
222
+ sql->Postgres.preparedUnsafe(query, params->Obj.magic)
223
+ })
224
+
225
+ Promise.all(promises)
226
+ }
227
+ }
228
+
229
+ module PersistedState = {
230
+ type t = {
231
+ id: int,
232
+ envio_version: string,
233
+ config_hash: string,
234
+ schema_hash: string,
235
+ handler_files_hash: string,
236
+ abi_files_hash: string,
237
+ }
238
+
239
+ let table = mkTable(
240
+ "persisted_state",
241
+ ~fields=[
242
+ mkField("id", Serial, ~fieldSchema=S.int, ~isPrimaryKey),
243
+ mkField("envio_version", Text, ~fieldSchema=S.string),
244
+ mkField("config_hash", Text, ~fieldSchema=S.string),
245
+ mkField("schema_hash", Text, ~fieldSchema=S.string),
246
+ mkField("handler_files_hash", Text, ~fieldSchema=S.string),
247
+ mkField("abi_files_hash", Text, ~fieldSchema=S.string),
248
+ ],
249
+ )
250
+ }
251
+
252
+ module EndOfBlockRangeScannedData = {
253
+ type t = {
254
+ chain_id: int,
255
+ block_number: int,
256
+ block_hash: string,
257
+ }
258
+
259
+ let table = mkTable(
260
+ "end_of_block_range_scanned_data",
261
+ ~fields=[
262
+ mkField("chain_id", Integer, ~fieldSchema=S.int, ~isPrimaryKey),
263
+ mkField("block_number", Integer, ~fieldSchema=S.int, ~isPrimaryKey),
264
+ mkField("block_hash", Text, ~fieldSchema=S.string),
265
+ ],
266
+ )
267
+ }
268
+
269
+ module RawEvents = {
270
+ // @genType Used for Test DB and internal tests
271
+ @genType
272
+ type t = {
273
+ @as("chain_id") chainId: int,
274
+ @as("event_id") eventId: bigint,
275
+ @as("event_name") eventName: string,
276
+ @as("contract_name") contractName: string,
277
+ @as("block_number") blockNumber: int,
278
+ @as("log_index") logIndex: int,
279
+ @as("src_address") srcAddress: Address.t,
280
+ @as("block_hash") blockHash: string,
281
+ @as("block_timestamp") blockTimestamp: int,
282
+ @as("block_fields") blockFields: Js.Json.t,
283
+ @as("transaction_fields") transactionFields: Js.Json.t,
284
+ params: Js.Json.t,
285
+ }
286
+
287
+ let schema = S.schema(s => {
288
+ chainId: s.matches(S.int),
289
+ eventId: s.matches(S.bigint),
290
+ eventName: s.matches(S.string),
291
+ contractName: s.matches(S.string),
292
+ blockNumber: s.matches(S.int),
293
+ logIndex: s.matches(S.int),
294
+ srcAddress: s.matches(Address.schema),
295
+ blockHash: s.matches(S.string),
296
+ blockTimestamp: s.matches(S.int),
297
+ blockFields: s.matches(S.json(~validate=false)),
298
+ transactionFields: s.matches(S.json(~validate=false)),
299
+ params: s.matches(S.json(~validate=false)),
300
+ })
301
+
302
+ let table = mkTable(
303
+ "raw_events",
304
+ ~fields=[
305
+ mkField("chain_id", Integer, ~fieldSchema=S.int),
306
+ mkField("event_id", Numeric, ~fieldSchema=S.bigint),
307
+ mkField("event_name", Text, ~fieldSchema=S.string),
308
+ mkField("contract_name", Text, ~fieldSchema=S.string),
309
+ mkField("block_number", Integer, ~fieldSchema=S.int),
310
+ mkField("log_index", Integer, ~fieldSchema=S.int),
311
+ mkField("src_address", Text, ~fieldSchema=Address.schema),
312
+ mkField("block_hash", Text, ~fieldSchema=S.string),
313
+ mkField("block_timestamp", Integer, ~fieldSchema=S.int),
314
+ mkField("block_fields", JsonB, ~fieldSchema=S.json(~validate=false)),
315
+ mkField("transaction_fields", JsonB, ~fieldSchema=S.json(~validate=false)),
316
+ mkField("params", JsonB, ~fieldSchema=S.json(~validate=false)),
317
+ mkField(
318
+ "db_write_timestamp",
319
+ TimestampWithoutTimezone,
320
+ ~default="CURRENT_TIMESTAMP",
321
+ ~fieldSchema=S.int,
322
+ ),
323
+ mkField("serial", Serial, ~isNullable, ~isPrimaryKey, ~fieldSchema=S.null(S.int)),
324
+ ],
325
+ )
326
+ }
327
+
328
+ // View names for Hasura integration
329
+ module Views = {
330
+ let metaViewName = "_meta"
331
+ let chainMetadataViewName = "chain_metadata"
332
+
333
+ let makeMetaViewQuery = (~pgSchema) => {
334
+ `CREATE VIEW "${pgSchema}"."${metaViewName}" AS
335
+ SELECT
336
+ "${(#id: Chains.field :> string)}" AS "chainId",
337
+ "${(#start_block: Chains.field :> string)}" AS "startBlock",
338
+ "${(#end_block: Chains.field :> string)}" AS "endBlock",
339
+ "${(#buffer_block: Chains.field :> string)}" AS "bufferBlock",
340
+ "${(#ready_at: Chains.field :> string)}" AS "readyAt",
341
+ "${(#first_event_block: Chains.field :> string)}" AS "firstEventBlock",
342
+ "${(#events_processed: Chains.field :> string)}" AS "eventsProcessed",
343
+ ("${(#ready_at: Chains.field :> string)}" IS NOT NULL) AS "isReady"
344
+ FROM "${pgSchema}"."${Chains.table.tableName}"
345
+ ORDER BY "${(#id: Chains.field :> string)}";`
346
+ }
347
+
348
+ let makeChainMetadataViewQuery = (~pgSchema) => {
349
+ `CREATE VIEW "${pgSchema}"."${chainMetadataViewName}" AS
350
+ SELECT
351
+ "${(#source_block: Chains.field :> string)}" AS "block_height",
352
+ "${(#id: Chains.field :> string)}" AS "chain_id",
353
+ "${(#end_block: Chains.field :> string)}" AS "end_block",
354
+ "${(#first_event_block: Chains.field :> string)}" AS "first_event_block_number",
355
+ "${(#_is_hyper_sync: Chains.field :> string)}" AS "is_hyper_sync",
356
+ "${(#buffer_block: Chains.field :> string)}" AS "latest_fetched_block_number",
357
+ "${(#_latest_processed_block: Chains.field :> string)}" AS "latest_processed_block",
358
+ "${(#_num_batches_fetched: Chains.field :> string)}" AS "num_batches_fetched",
359
+ "${(#events_processed: Chains.field :> string)}" AS "num_events_processed",
360
+ "${(#start_block: Chains.field :> string)}" AS "start_block",
361
+ "${(#ready_at: Chains.field :> string)}" AS "timestamp_caught_up_to_head_or_endblock"
362
+ FROM "${pgSchema}"."${Chains.table.tableName}";`
363
+ }
364
+ }
365
+
366
+ module DynamicContractRegistry = {
367
+ let name = "dynamic_contract_registry"
368
+
369
+ let makeId = (~chainId, ~contractAddress) => {
370
+ chainId->Belt.Int.toString ++ "-" ++ contractAddress->Address.toString
371
+ }
372
+
373
+ // @genType Used for Test DB
374
+ @genType
375
+ type t = {
376
+ id: string,
377
+ @as("chain_id") chainId: int,
378
+ @as("registering_event_block_number") registeringEventBlockNumber: int,
379
+ @as("registering_event_log_index") registeringEventLogIndex: int,
380
+ @as("registering_event_block_timestamp") registeringEventBlockTimestamp: int,
381
+ @as("registering_event_contract_name") registeringEventContractName: string,
382
+ @as("registering_event_name") registeringEventName: string,
383
+ @as("registering_event_src_address") registeringEventSrcAddress: Address.t,
384
+ @as("contract_address") contractAddress: Address.t,
385
+ @as("contract_name") contractName: string,
386
+ }
387
+
388
+ let schema = S.schema(s => {
389
+ id: s.matches(S.string),
390
+ chainId: s.matches(S.int),
391
+ registeringEventBlockNumber: s.matches(S.int),
392
+ registeringEventLogIndex: s.matches(S.int),
393
+ registeringEventContractName: s.matches(S.string),
394
+ registeringEventName: s.matches(S.string),
395
+ registeringEventSrcAddress: s.matches(Address.schema),
396
+ registeringEventBlockTimestamp: s.matches(S.int),
397
+ contractAddress: s.matches(Address.schema),
398
+ contractName: s.matches(S.string),
399
+ })
400
+
401
+ let rowsSchema = S.array(schema)
402
+
403
+ let table = mkTable(
404
+ name,
405
+ ~fields=[
406
+ mkField("id", Text, ~isPrimaryKey, ~fieldSchema=S.string),
407
+ mkField("chain_id", Integer, ~fieldSchema=S.int),
408
+ mkField("registering_event_block_number", Integer, ~fieldSchema=S.int),
409
+ mkField("registering_event_log_index", Integer, ~fieldSchema=S.int),
410
+ mkField("registering_event_block_timestamp", Integer, ~fieldSchema=S.int),
411
+ mkField("registering_event_contract_name", Text, ~fieldSchema=S.string),
412
+ mkField("registering_event_name", Text, ~fieldSchema=S.string),
413
+ mkField("registering_event_src_address", Text, ~fieldSchema=Address.schema),
414
+ mkField("contract_address", Text, ~fieldSchema=Address.schema),
415
+ mkField("contract_name", Text, ~fieldSchema=S.string),
416
+ ],
417
+ )
418
+
419
+ let entityHistory = table->EntityHistory.fromTable(~schema)
420
+
421
+ external castToInternal: t => Internal.entity = "%identity"
422
+
423
+ let config = {
424
+ name,
425
+ schema,
426
+ rowsSchema,
427
+ table,
428
+ entityHistory,
429
+ }->Internal.fromGenericEntityConfig
430
+ }