envio 3.9.0 → 3.10.0
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.
- package/evm.schema.json +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +35 -3
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +52 -44
- package/src/Config.res.mjs +51 -17
- package/src/Core.res +4 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/svm.schema.json +1 -75
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
|
@@ -9,6 +9,7 @@ import * as AddressRows from "../AddressRows.res.mjs";
|
|
|
9
9
|
import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
|
|
10
10
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
11
11
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
12
|
+
import * as CheckpointBounds from "./CheckpointBounds.res.mjs";
|
|
12
13
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
13
14
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
15
|
|
|
@@ -389,13 +390,35 @@ let dbSchema = S$RescriptSchema.object(s => ({
|
|
|
389
390
|
events_processed: s.f("events_processed", S$RescriptSchema.int)
|
|
390
391
|
}));
|
|
391
392
|
|
|
392
|
-
let
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
393
|
+
let columns = [
|
|
394
|
+
{
|
|
395
|
+
field: Table.mkField("id", "UInt64", S$RescriptSchema.bigint, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
396
|
+
clickHouseFieldType: "UInt64",
|
|
397
|
+
valuesOf: batch => batch.checkpointIds
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
field: Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
401
|
+
clickHouseFieldType: "ChainId",
|
|
402
|
+
valuesOf: batch => batch.checkpointChainIds
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
field: Table.mkField("block_number", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
406
|
+
clickHouseFieldType: "Int32",
|
|
407
|
+
valuesOf: batch => batch.checkpointBlockNumbers
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
field: Table.mkField("block_hash", "String", S$RescriptSchema.$$null(S$RescriptSchema.string), undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
411
|
+
clickHouseFieldType: "String",
|
|
412
|
+
valuesOf: batch => batch.checkpointBlockHashes
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
field: Table.mkField("events_processed", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
416
|
+
clickHouseFieldType: "UInt64",
|
|
417
|
+
valuesOf: batch => batch.checkpointEventsProcessed
|
|
418
|
+
}
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
let table$4 = Table.mkTable("envio_checkpoints", undefined, columns.map(param => param.field), undefined);
|
|
399
422
|
|
|
400
423
|
function makeGetReorgCheckpointsQuery(pgSchema) {
|
|
401
424
|
return `WITH reorg_chains AS (
|
|
@@ -415,7 +438,8 @@ FROM "` + pgSchema + `"."` + table$4.tableName + `" cp
|
|
|
415
438
|
INNER JOIN reorg_chains rc
|
|
416
439
|
ON cp."` + "chain_id" + `" = rc.id
|
|
417
440
|
WHERE cp."` + "block_hash" + `" IS NOT NULL
|
|
418
|
-
AND cp."` + "block_number" + `" >= rc.safe_block
|
|
441
|
+
AND cp."` + "block_number" + `" >= rc.safe_block
|
|
442
|
+
ORDER BY cp."` + "id" + `";`;
|
|
419
443
|
}
|
|
420
444
|
|
|
421
445
|
function makeCommitedCheckpointIdQuery(pgSchema) {
|
|
@@ -442,16 +466,22 @@ function insert$2(sql, pgSchema, checkpointIds, checkpointChainIds, checkpointBl
|
|
|
442
466
|
], {prepare: true});
|
|
443
467
|
}
|
|
444
468
|
|
|
445
|
-
|
|
446
|
-
|
|
469
|
+
let chainIdColumn = "chain_id";
|
|
470
|
+
|
|
471
|
+
function rollback(sql, pgSchema, floors) {
|
|
472
|
+
let tableRef = `"` + pgSchema + `"."` + table$4.tableName + `"`;
|
|
473
|
+
let bounds = CheckpointBounds.sql(floors.floors, chainIdColumn, tableRef);
|
|
474
|
+
return sql.unsafe(`DELETE FROM ` + tableRef + bounds.using + ` WHERE "` + "id" + `" > ` + bounds.checkpointId + bounds.usingMatch + `;`, CheckpointBounds.params(floors.floors), {prepare: true});
|
|
447
475
|
}
|
|
448
476
|
|
|
449
|
-
function makePruneStaleCheckpointsQuery(pgSchema) {
|
|
450
|
-
|
|
477
|
+
function makePruneStaleCheckpointsQuery(pgSchema, safeCheckpoints) {
|
|
478
|
+
let tableRef = `"` + pgSchema + `"."` + table$4.tableName + `"`;
|
|
479
|
+
let bounds = CheckpointBounds.sql(safeCheckpoints, chainIdColumn, tableRef);
|
|
480
|
+
return `DELETE FROM ` + tableRef + bounds.using + ` WHERE "` + "id" + `" < ` + bounds.checkpointId + bounds.usingMatch + `;`;
|
|
451
481
|
}
|
|
452
482
|
|
|
453
|
-
function pruneStaleCheckpoints(sql, pgSchema,
|
|
454
|
-
return sql.unsafe(makePruneStaleCheckpointsQuery(pgSchema),
|
|
483
|
+
function pruneStaleCheckpoints(sql, pgSchema, safeCheckpoints) {
|
|
484
|
+
return sql.unsafe(makePruneStaleCheckpointsQuery(pgSchema, safeCheckpoints), CheckpointBounds.params(safeCheckpoints), {prepare: true});
|
|
455
485
|
}
|
|
456
486
|
|
|
457
487
|
function makeGetRollbackTargetCheckpointQuery(pgSchema) {
|
|
@@ -471,28 +501,31 @@ function getRollbackTargetCheckpoint(sql, pgSchema, reorgChainId, lastKnownValid
|
|
|
471
501
|
return rawResult.then(rows => Stdlib_Option.map(rows[0], row => BigInt(row.id)));
|
|
472
502
|
}
|
|
473
503
|
|
|
474
|
-
function makeGetRollbackProgressDiffQuery(pgSchema) {
|
|
504
|
+
function makeGetRollbackProgressDiffQuery(pgSchema, floors) {
|
|
505
|
+
let bounds = CheckpointBounds.sql(floors.floors, chainIdColumn, "t");
|
|
475
506
|
return `SELECT
|
|
476
|
-
"` + "chain_id" + `"::float8 as "` + "chain_id" + `",
|
|
477
|
-
SUM("` + "events_processed" + `") as events_processed_diff,
|
|
478
|
-
MIN("` + "block_number" + `") - 1 as new_progress_block_number
|
|
479
|
-
FROM "` + pgSchema + `"."` + table$4.tableName + `"
|
|
480
|
-
WHERE "` + "id" + `" >
|
|
481
|
-
GROUP BY "` + "chain_id" + `";`;
|
|
507
|
+
t."` + "chain_id" + `"::float8 as "` + "chain_id" + `",
|
|
508
|
+
SUM(t."` + "events_processed" + `") as events_processed_diff,
|
|
509
|
+
MIN(t."` + "block_number" + `") - 1 as new_progress_block_number
|
|
510
|
+
FROM "` + pgSchema + `"."` + table$4.tableName + `" t` + bounds.join + `
|
|
511
|
+
WHERE t."` + "id" + `" > ` + bounds.checkpointId + `
|
|
512
|
+
GROUP BY t."` + "chain_id" + `";`;
|
|
482
513
|
}
|
|
483
514
|
|
|
484
|
-
function getRollbackProgressDiff(sql, pgSchema,
|
|
485
|
-
return sql.unsafe(makeGetRollbackProgressDiffQuery(pgSchema),
|
|
515
|
+
function getRollbackProgressDiff(sql, pgSchema, floors) {
|
|
516
|
+
return sql.unsafe(makeGetRollbackProgressDiffQuery(pgSchema, floors), CheckpointBounds.params(floors.floors), {prepare: true});
|
|
486
517
|
}
|
|
487
518
|
|
|
488
519
|
let Checkpoints = {
|
|
489
520
|
dbSchema: dbSchema,
|
|
490
521
|
initialCheckpointId: 0n,
|
|
522
|
+
columns: columns,
|
|
491
523
|
table: table$4,
|
|
492
524
|
makeGetReorgCheckpointsQuery: makeGetReorgCheckpointsQuery,
|
|
493
525
|
makeCommitedCheckpointIdQuery: makeCommitedCheckpointIdQuery,
|
|
494
526
|
makeInsertCheckpointQuery: makeInsertCheckpointQuery,
|
|
495
527
|
insert: insert$2,
|
|
528
|
+
chainIdColumn: chainIdColumn,
|
|
496
529
|
rollback: rollback,
|
|
497
530
|
makePruneStaleCheckpointsQuery: makePruneStaleCheckpointsQuery,
|
|
498
531
|
pruneStaleCheckpoints: pruneStaleCheckpoints,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// How far back a reorg rollback takes each chain: the last checkpoint each keeps,
|
|
2
|
+
// and for a chain whose own reorg set its floor, the last block that reorg left
|
|
3
|
+
// valid. Superseding an unwritten rollback with a new one is a pointwise
|
|
4
|
+
// minimum, so a merge can neither lose a chain's floor nor raise it.
|
|
5
|
+
type t = {
|
|
6
|
+
floors: CheckpointBounds.t,
|
|
7
|
+
// Progress recomputed from the surviving checkpoints alone can land above the
|
|
8
|
+
// fork: the blocks between it and the chain's next checkpoint carried no
|
|
9
|
+
// events on the orphaned chain, but the chain replacing it can have its own.
|
|
10
|
+
// Absent for a chain dragged along by another chain's reorg.
|
|
11
|
+
forkBlockNumberByChain: array<(ChainId.t, int)>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The rollback of a schema whose chains can't have written each other's rows.
|
|
15
|
+
let isolated = (~chainId, ~floorCheckpointId, ~forkBlockNumber) => {
|
|
16
|
+
floors: PerChain([(chainId, floorCheckpointId)]),
|
|
17
|
+
forkBlockNumberByChain: [(chainId, forkBlockNumber)],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The rollback of a schema with a cross-chain entity: every chain goes back to
|
|
21
|
+
// the same checkpoint.
|
|
22
|
+
let global = (~floorCheckpointId, ~reorgChainId, ~forkBlockNumber) => {
|
|
23
|
+
floors: EveryChain(floorCheckpointId),
|
|
24
|
+
forkBlockNumberByChain: [(reorgChainId, forkBlockNumber)],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let mergeByChain = (pending: array<(ChainId.t, 'a)>, next: array<(ChainId.t, 'a)>) => {
|
|
28
|
+
let merged = pending->Array.copy
|
|
29
|
+
next->Array.forEach(((chainId, value)) =>
|
|
30
|
+
switch merged->Array.findIndexOpt(((mergedChainId, _)) => mergedChainId === chainId) {
|
|
31
|
+
| Some(idx) =>
|
|
32
|
+
let (_, pendingValue) = merged->Array.getUnsafe(idx)
|
|
33
|
+
merged->Array.setUnsafe(idx, (chainId, Pervasives.min(pendingValue, value)))
|
|
34
|
+
| None => merged->Array.push((chainId, value))
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
merged
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let merge = (pending: t, next: t) => {
|
|
41
|
+
floors: switch (pending.floors, next.floors) {
|
|
42
|
+
| (EveryChain(pending), EveryChain(next)) => EveryChain(Pervasives.min(pending, next))
|
|
43
|
+
| (PerChain(pending), PerChain(next)) => PerChain(mergeByChain(pending, next))
|
|
44
|
+
| (EveryChain(_), PerChain(_)) | (PerChain(_), EveryChain(_)) =>
|
|
45
|
+
JsError.throwWithMessage(
|
|
46
|
+
"Internal error: a rollback reaching every chain can't be merged with one isolated to a chain. The schema decides which a run builds, and it doesn't change.",
|
|
47
|
+
)
|
|
48
|
+
},
|
|
49
|
+
forkBlockNumberByChain: mergeByChain(pending.forkBlockNumberByChain, next.forkBlockNumberByChain),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let forkBlockNumber = (floors: t, chainId) =>
|
|
53
|
+
floors.forkBlockNumberByChain
|
|
54
|
+
->Array.find(((forkChainId, _)) => forkChainId === chainId)
|
|
55
|
+
->Option.map(((_, forkBlockNumber)) => forkBlockNumber)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
|
+
import * as Primitive_bigint from "@rescript/runtime/lib/es6/Primitive_bigint.js";
|
|
7
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
|
|
9
|
+
function isolated(chainId, floorCheckpointId, forkBlockNumber) {
|
|
10
|
+
return {
|
|
11
|
+
floors: {
|
|
12
|
+
TAG: "PerChain",
|
|
13
|
+
_0: [[
|
|
14
|
+
chainId,
|
|
15
|
+
floorCheckpointId
|
|
16
|
+
]]
|
|
17
|
+
},
|
|
18
|
+
forkBlockNumberByChain: [[
|
|
19
|
+
chainId,
|
|
20
|
+
forkBlockNumber
|
|
21
|
+
]]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function global(floorCheckpointId, reorgChainId, forkBlockNumber) {
|
|
26
|
+
return {
|
|
27
|
+
floors: {
|
|
28
|
+
TAG: "EveryChain",
|
|
29
|
+
_0: floorCheckpointId
|
|
30
|
+
},
|
|
31
|
+
forkBlockNumberByChain: [[
|
|
32
|
+
reorgChainId,
|
|
33
|
+
forkBlockNumber
|
|
34
|
+
]]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mergeByChain(pending, next) {
|
|
39
|
+
let merged = pending.slice();
|
|
40
|
+
next.forEach(param => {
|
|
41
|
+
let value = param[1];
|
|
42
|
+
let chainId = param[0];
|
|
43
|
+
let idx = Stdlib_Array.findIndexOpt(merged, param => param[0] === chainId);
|
|
44
|
+
if (idx !== undefined) {
|
|
45
|
+
let match = merged[idx];
|
|
46
|
+
merged[idx] = [
|
|
47
|
+
chainId,
|
|
48
|
+
Primitive_object.min(match[1], value)
|
|
49
|
+
];
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
merged.push([
|
|
53
|
+
chainId,
|
|
54
|
+
value
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
return merged;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function merge(pending, next) {
|
|
61
|
+
let match = pending.floors;
|
|
62
|
+
let match$1 = next.floors;
|
|
63
|
+
let tmp;
|
|
64
|
+
tmp = match.TAG === "EveryChain" ? (
|
|
65
|
+
match$1.TAG === "EveryChain" ? ({
|
|
66
|
+
TAG: "EveryChain",
|
|
67
|
+
_0: Primitive_bigint.min(match._0, match$1._0)
|
|
68
|
+
}) : Stdlib_JsError.throwWithMessage("Internal error: a rollback reaching every chain can't be merged with one isolated to a chain. The schema decides which a run builds, and it doesn't change.")
|
|
69
|
+
) : (
|
|
70
|
+
match$1.TAG === "EveryChain" ? Stdlib_JsError.throwWithMessage("Internal error: a rollback reaching every chain can't be merged with one isolated to a chain. The schema decides which a run builds, and it doesn't change.") : ({
|
|
71
|
+
TAG: "PerChain",
|
|
72
|
+
_0: mergeByChain(match._0, match$1._0)
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
floors: tmp,
|
|
77
|
+
forkBlockNumberByChain: mergeByChain(pending.forkBlockNumberByChain, next.forkBlockNumberByChain)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function forkBlockNumber(floors, chainId) {
|
|
82
|
+
return Stdlib_Option.map(floors.forkBlockNumberByChain.find(param => param[0] === chainId), param => param[1]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
isolated,
|
|
87
|
+
global,
|
|
88
|
+
mergeByChain,
|
|
89
|
+
merge,
|
|
90
|
+
forkBlockNumber,
|
|
91
|
+
}
|
|
92
|
+
/* No side effect */
|
package/src/db/Table.res
CHANGED
|
@@ -22,8 +22,9 @@ type fieldType =
|
|
|
22
22
|
| Uint32
|
|
23
23
|
| UInt52
|
|
24
24
|
| SmallInt
|
|
25
|
-
// Raw bytes
|
|
26
|
-
// address in the binary form the Rust address
|
|
25
|
+
// Raw bytes: the `Bytes` scalar under `bytes_type: uint8array`, and the
|
|
26
|
+
// internal column that stores an address in the binary form the Rust address
|
|
27
|
+
// store keys on.
|
|
27
28
|
| Bytea
|
|
28
29
|
| UInt64
|
|
29
30
|
| Int32
|
|
@@ -288,6 +289,9 @@ let getChainIdField = (table): option<field> =>
|
|
|
288
289
|
}
|
|
289
290
|
)
|
|
290
291
|
|
|
292
|
+
// None for a cross-chain entity, whose rows no single chain owns.
|
|
293
|
+
let getPgChainIdColumn = table => table->getChainIdField->Option.map(getPgDbFieldName)
|
|
294
|
+
|
|
291
295
|
let getFieldByName = (table, fieldName) =>
|
|
292
296
|
table.fields->Array.find(field => field->getUserDefinedFieldName === fieldName)
|
|
293
297
|
|
|
@@ -360,7 +364,10 @@ let queryFields: table => dict<queryField> = Utils.WeakMap.memoize(table => {
|
|
|
360
364
|
field->getApiFieldName,
|
|
361
365
|
{
|
|
362
366
|
fieldSchema: field.fieldSchema,
|
|
363
|
-
arrayFieldSchema:
|
|
367
|
+
arrayFieldSchema: switch field.fieldType {
|
|
368
|
+
| Bytea => Utils.Schema.bytesArray->S.toUnknown
|
|
369
|
+
| _ => S.array(field.fieldSchema)->S.toUnknown
|
|
370
|
+
},
|
|
364
371
|
pgDbFieldName: field->getPgDbFieldName,
|
|
365
372
|
isChainId: field.isChainId,
|
|
366
373
|
},
|
|
@@ -431,6 +438,7 @@ type sqlParams<'entity> = {
|
|
|
431
438
|
quotedFieldNames: array<string>,
|
|
432
439
|
quotedNonPrimaryFieldNames: array<string>,
|
|
433
440
|
arrayFieldTypes: array<string>,
|
|
441
|
+
byteaColumnIndexes: array<int>,
|
|
434
442
|
hasArrayField: bool,
|
|
435
443
|
}
|
|
436
444
|
|
|
@@ -438,6 +446,9 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
|
|
|
438
446
|
let quotedFieldNames = []
|
|
439
447
|
let quotedNonPrimaryFieldNames = []
|
|
440
448
|
let arrayFieldTypes = []
|
|
449
|
+
// Positions of the bytea columns among the unnest parameters, which the
|
|
450
|
+
// caller binds as array literals (see `Utils.Bytes.toPgArrayLiteral`).
|
|
451
|
+
let byteaColumnIndexes = []
|
|
441
452
|
let hasArrayField = ref(false)
|
|
442
453
|
|
|
443
454
|
let dbSchema: S.t<dict<unknown>> = S.schema(s =>
|
|
@@ -450,7 +461,7 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
|
|
|
450
461
|
| BigInt => Utils.BigInt.schema->S.toUnknown
|
|
451
462
|
| Option(child)
|
|
452
463
|
| Null(child) =>
|
|
453
|
-
|
|
464
|
+
Utils.Schema.nullTolerant(child->coerceSchema)->S.toUnknown
|
|
454
465
|
| Array(child) => {
|
|
455
466
|
hasArrayField := true
|
|
456
467
|
S.array(child->coerceSchema)->S.toUnknown
|
|
@@ -472,6 +483,12 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
|
|
|
472
483
|
| Some(field) => field
|
|
473
484
|
| None => throw(NonExistingTableField(location))
|
|
474
485
|
}
|
|
486
|
+
switch field {
|
|
487
|
+
| Field({isArray: true}) => hasArrayField := true
|
|
488
|
+
| Field({fieldType: Bytea}) =>
|
|
489
|
+
byteaColumnIndexes->Array.push(arrayFieldTypes->Array.length)->ignore
|
|
490
|
+
| _ => ()
|
|
491
|
+
}
|
|
475
492
|
|
|
476
493
|
// Schema locations use API field names, while the SQL references
|
|
477
494
|
// columns by their possibly renamed db names.
|
|
@@ -521,6 +538,7 @@ let toSqlParams = (table: table, ~schema, ~pgSchema, ~chainIdMode: ChainId.mode=
|
|
|
521
538
|
quotedFieldNames,
|
|
522
539
|
quotedNonPrimaryFieldNames,
|
|
523
540
|
arrayFieldTypes,
|
|
541
|
+
byteaColumnIndexes,
|
|
524
542
|
hasArrayField: hasArrayField.contents,
|
|
525
543
|
}
|
|
526
544
|
}
|
package/src/db/Table.res.mjs
CHANGED
|
@@ -253,6 +253,10 @@ function getChainIdField(table) {
|
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
function getPgChainIdColumn(table) {
|
|
257
|
+
return Stdlib_Option.map(getChainIdField(table), getPgDbFieldName);
|
|
258
|
+
}
|
|
259
|
+
|
|
256
260
|
function getFieldByName(table, fieldName) {
|
|
257
261
|
return table.fields.find(field => getUserDefinedFieldName(field) === fieldName);
|
|
258
262
|
}
|
|
@@ -307,9 +311,12 @@ let queryFields = Utils.$$WeakMap.memoize(table => {
|
|
|
307
311
|
return;
|
|
308
312
|
}
|
|
309
313
|
let field$1 = field._0;
|
|
314
|
+
let match = field$1.fieldType;
|
|
315
|
+
let tmp;
|
|
316
|
+
tmp = typeof match !== "object" && match === "Bytea" ? Utils.Schema.bytesArray : S$RescriptSchema.array(field$1.fieldSchema);
|
|
310
317
|
dict[getApiFieldName(field$1)] = {
|
|
311
318
|
fieldSchema: field$1.fieldSchema,
|
|
312
|
-
arrayFieldSchema:
|
|
319
|
+
arrayFieldSchema: tmp,
|
|
313
320
|
pgDbFieldName: getPgDbFieldName(field$1),
|
|
314
321
|
isChainId: field$1.isChainId
|
|
315
322
|
};
|
|
@@ -368,6 +375,7 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
|
|
|
368
375
|
let quotedFieldNames = [];
|
|
369
376
|
let quotedNonPrimaryFieldNames = [];
|
|
370
377
|
let arrayFieldTypes = [];
|
|
378
|
+
let byteaColumnIndexes = [];
|
|
371
379
|
let hasArrayField = {
|
|
372
380
|
contents: false
|
|
373
381
|
};
|
|
@@ -400,7 +408,7 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
|
|
|
400
408
|
switch (child.TAG) {
|
|
401
409
|
case "option" :
|
|
402
410
|
case "null" :
|
|
403
|
-
return
|
|
411
|
+
return Utils.Schema.nullTolerant(coerceSchema(child._0));
|
|
404
412
|
case "array" :
|
|
405
413
|
hasArrayField.contents = true;
|
|
406
414
|
return S$RescriptSchema.array(coerceSchema(child._0));
|
|
@@ -423,6 +431,17 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
|
|
|
423
431
|
Error: new Error()
|
|
424
432
|
};
|
|
425
433
|
}
|
|
434
|
+
if (field$1.TAG === "Field") {
|
|
435
|
+
let match = field$1._0;
|
|
436
|
+
if (match.isArray) {
|
|
437
|
+
hasArrayField.contents = true;
|
|
438
|
+
} else {
|
|
439
|
+
let tmp = match.fieldType;
|
|
440
|
+
if (typeof tmp !== "object" && tmp === "Bytea") {
|
|
441
|
+
byteaColumnIndexes.push(arrayFieldTypes.length);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
426
445
|
let quotedDbName = `"` + getPgFieldName(field$1) + `"`;
|
|
427
446
|
quotedFieldNames.push(quotedDbName);
|
|
428
447
|
if (field$1.TAG === "Field") {
|
|
@@ -432,20 +451,20 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
|
|
|
432
451
|
quotedNonPrimaryFieldNames.push(quotedDbName);
|
|
433
452
|
}
|
|
434
453
|
}
|
|
435
|
-
let tmp;
|
|
454
|
+
let tmp$1;
|
|
436
455
|
if (field$1.TAG === "Field") {
|
|
437
456
|
let f = field$1._0;
|
|
438
457
|
let pgFieldType = getPgFieldType(f.fieldType, pgSchema, true, false, f.isNullable, chainIdMode);
|
|
439
|
-
let match = f.fieldType;
|
|
440
|
-
tmp = typeof match !== "object" ? (
|
|
441
|
-
match === "Boolean" ? "INTEGER" + `[]::` + pgFieldType : pgFieldType
|
|
458
|
+
let match$1 = f.fieldType;
|
|
459
|
+
tmp$1 = typeof match$1 !== "object" ? (
|
|
460
|
+
match$1 === "Boolean" ? "INTEGER" + `[]::` + pgFieldType : pgFieldType
|
|
442
461
|
) : (
|
|
443
|
-
match.type === "Enum" ? "TEXT" + `[]::` + pgFieldType : pgFieldType
|
|
462
|
+
match$1.type === "Enum" ? "TEXT" + `[]::` + pgFieldType : pgFieldType
|
|
444
463
|
);
|
|
445
464
|
} else {
|
|
446
|
-
tmp = "TEXT[]";
|
|
465
|
+
tmp$1 = "TEXT[]";
|
|
447
466
|
}
|
|
448
|
-
arrayFieldTypes.push(tmp);
|
|
467
|
+
arrayFieldTypes.push(tmp$1);
|
|
449
468
|
dict[location] = s.m(coerceSchema(param.schema));
|
|
450
469
|
});
|
|
451
470
|
return dict;
|
|
@@ -455,6 +474,7 @@ function toSqlParams(table, schema, pgSchema, chainIdModeOpt) {
|
|
|
455
474
|
quotedFieldNames: quotedFieldNames,
|
|
456
475
|
quotedNonPrimaryFieldNames: quotedNonPrimaryFieldNames,
|
|
457
476
|
arrayFieldTypes: arrayFieldTypes,
|
|
477
|
+
byteaColumnIndexes: byteaColumnIndexes,
|
|
458
478
|
hasArrayField: hasArrayField.contents
|
|
459
479
|
};
|
|
460
480
|
}
|
|
@@ -505,6 +525,7 @@ export {
|
|
|
505
525
|
getLinkedEntityFields,
|
|
506
526
|
getDerivedFromFields,
|
|
507
527
|
getChainIdField,
|
|
528
|
+
getPgChainIdColumn,
|
|
508
529
|
getFieldByName,
|
|
509
530
|
NoIdField,
|
|
510
531
|
getIdFieldOrThrow,
|
package/src/sources/Evm.res
CHANGED
|
@@ -77,6 +77,8 @@ let make = (~logger: Pino.t): Ecosystem.t => {
|
|
|
77
77
|
blockTimestampName: "timestamp",
|
|
78
78
|
blockHashName: "hash",
|
|
79
79
|
onBlockMethodName: "onBlock",
|
|
80
|
+
contractNoun: "contract",
|
|
81
|
+
eventNoun: "event",
|
|
80
82
|
// EVM filter shape: `{block: {number: {_gte?, _lte?, _every?}}}`.
|
|
81
83
|
// The inner range chunk is returned as raw `S.unknown` and parsed a
|
|
82
84
|
// second time in `Main.res` by the shared `blockRangeSchema`.
|
package/src/sources/Evm.res.mjs
CHANGED
|
@@ -56,6 +56,8 @@ function make(logger) {
|
|
|
56
56
|
blockTimestampName: "timestamp",
|
|
57
57
|
blockHashName: "hash",
|
|
58
58
|
onBlockMethodName: "onBlock",
|
|
59
|
+
contractNoun: "contract",
|
|
60
|
+
eventNoun: "event",
|
|
59
61
|
onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
|
|
60
62
|
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
|
|
61
63
|
logger: logger,
|
|
@@ -205,7 +205,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
205
205
|
name,
|
|
206
206
|
sourceFor: Sync,
|
|
207
207
|
chainId,
|
|
208
|
-
pollingInterval:
|
|
208
|
+
pollingInterval: HyperSync.pollingInterval,
|
|
209
209
|
poweredByHyperSync: true,
|
|
210
210
|
getBlockHashes,
|
|
211
211
|
getHeightOrThrow: async () => {
|
|
@@ -227,7 +227,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
227
227
|
{height, requestStats: [{method: "getHeight", seconds}]}
|
|
228
228
|
},
|
|
229
229
|
getItemsOrThrow,
|
|
230
|
-
createHeightSubscription: (~onHeight) =>
|
|
231
|
-
|
|
230
|
+
createHeightSubscription: (~onHeight, ~onStatus) =>
|
|
231
|
+
HyperSyncSSE.subscribe(~hyperSyncUrl=endpointUrl, ~apiToken, ~onHeight, ~onStatus),
|
|
232
232
|
}
|
|
233
233
|
}
|
|
@@ -4,6 +4,7 @@ import * as Source from "./Source.res.mjs";
|
|
|
4
4
|
import * as Logging from "../Logging.res.mjs";
|
|
5
5
|
import * as HyperSync from "./HyperSync.res.mjs";
|
|
6
6
|
import * as Performance from "../bindings/Performance.res.mjs";
|
|
7
|
+
import * as HyperSyncSSE from "./HyperSyncSSE.res.mjs";
|
|
7
8
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
8
9
|
import * as ErrorHandling from "../ErrorHandling.res.mjs";
|
|
9
10
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
@@ -11,7 +12,6 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
|
11
12
|
import * as HyperSyncClient from "./HyperSyncClient.res.mjs";
|
|
12
13
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
13
14
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
|
-
import * as HyperSyncHeightStream from "./HyperSyncHeightStream.res.mjs";
|
|
15
15
|
|
|
16
16
|
function isUnauthorizedError(message) {
|
|
17
17
|
return message.includes("401 Unauthorized");
|
|
@@ -143,7 +143,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
|
143
143
|
sourceFor: "Sync",
|
|
144
144
|
chainId: chainId,
|
|
145
145
|
poweredByHyperSync: true,
|
|
146
|
-
pollingInterval:
|
|
146
|
+
pollingInterval: HyperSync.pollingInterval,
|
|
147
147
|
getBlockHashes: getBlockHashes,
|
|
148
148
|
getHeightOrThrow: async () => {
|
|
149
149
|
let timerRef = Performance.now();
|
|
@@ -188,7 +188,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
|
188
188
|
};
|
|
189
189
|
},
|
|
190
190
|
getItemsOrThrow: getItemsOrThrow,
|
|
191
|
-
createHeightSubscription: onHeight =>
|
|
191
|
+
createHeightSubscription: (onHeight, onStatus) => HyperSyncSSE.subscribe(endpointUrl, apiToken$1, onHeight, onStatus)
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// newHeads carries no keep-alive, so this has to tolerate slow chains rather
|
|
2
|
+
// than the seconds an SSE ping allows.
|
|
3
|
+
let staleTimeout = 60_000
|
|
4
|
+
|
|
5
|
+
// The id of the one request this stream ever sends, which is what tells a
|
|
6
|
+
// response meant for it apart from one about anything else sharing the socket.
|
|
7
|
+
let subscribeRequestId = 1
|
|
8
|
+
let subscribeRequestIdText = subscribeRequestId->Int.toString
|
|
9
|
+
|
|
10
|
+
type wsMessage =
|
|
11
|
+
| NewHead(int)
|
|
12
|
+
// A response to some request, carrying the id it answers when it names one.
|
|
13
|
+
| Response({id: option<string>, isError: bool})
|
|
14
|
+
|
|
15
|
+
let subscribeRequestJson =
|
|
16
|
+
{"jsonrpc": "2.0", "id": subscribeRequestId, "method": "eth_subscribe", "params": ["newHeads"]}
|
|
17
|
+
->(Utils.magic: {
|
|
18
|
+
"jsonrpc": string,
|
|
19
|
+
"id": int,
|
|
20
|
+
"method": string,
|
|
21
|
+
"params": array<string>,
|
|
22
|
+
} => JSON.t)
|
|
23
|
+
->JSON.stringify
|
|
24
|
+
|
|
25
|
+
// A JSON-RPC id is any scalar, and gateways do echo an int id back as a string.
|
|
26
|
+
// Both spellings name the same request, so both are read as the one text.
|
|
27
|
+
// Nullable, not merely optional: JSON-RPC answers a request whose id it could
|
|
28
|
+
// not read with an explicit null.
|
|
29
|
+
let responseIdSchema = S.nullable(
|
|
30
|
+
S.union([S.string, S.int->S.transform(_ => {parser: id => id->Int.toString})]),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// The subscribe is the only request this socket ever sends, so an error naming
|
|
34
|
+
// its id is that subscription being refused. So is one naming no id at all —
|
|
35
|
+
// which is how JSON-RPC answers a request it could not parse — but only until
|
|
36
|
+
// the node has confirmed the subscription: after that the request has had its
|
|
37
|
+
// answer, and a null id belongs to whatever else the provider wants to say,
|
|
38
|
+
// throttling most of all. Tearing a delivering subscription down over that
|
|
39
|
+
// would reconnect against an endpoint already asking for less.
|
|
40
|
+
let refusesSubscribe = (id, ~subscribed) =>
|
|
41
|
+
switch id {
|
|
42
|
+
| None => !subscribed
|
|
43
|
+
| Some(id) => id === subscribeRequestIdText
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let wsMessageSchema = S.union([
|
|
47
|
+
S.object(s => {
|
|
48
|
+
let _ = s.field("method", S.literal("eth_subscription"))
|
|
49
|
+
NewHead(
|
|
50
|
+
s.field(
|
|
51
|
+
"params",
|
|
52
|
+
S.object(s => {
|
|
53
|
+
s.field(
|
|
54
|
+
"result",
|
|
55
|
+
S.object(
|
|
56
|
+
s => {
|
|
57
|
+
s.field("number", Rpc.hexIntSchema)
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
}),
|
|
65
|
+
S.object(s => {
|
|
66
|
+
let id = s.field("id", responseIdSchema)
|
|
67
|
+
let _ = s.field("result", S.string)
|
|
68
|
+
Response({id, isError: false})
|
|
69
|
+
}),
|
|
70
|
+
S.object(s => {
|
|
71
|
+
let id = s.field("id", responseIdSchema)
|
|
72
|
+
// The error member as JSON-RPC defines it, rather than `S.unknown`: a
|
|
73
|
+
// missing field parses as `undefined`, which `S.unknown` accepts, and this
|
|
74
|
+
// arm would then match every frame the ones above turned down — including
|
|
75
|
+
// one nobody can read.
|
|
76
|
+
let _ = s.field("error", S.object(s => s.field("code", S.int)))
|
|
77
|
+
Response({id, isError: true})
|
|
78
|
+
}),
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
let subscribe = (~wsUrl, ~onHeight, ~onStatus) =>
|
|
82
|
+
HeightStream.subscribe(~staleTimeout, ~onHeight, ~onStatus, ~connect=driver => {
|
|
83
|
+
let ws = WebSocket.create(wsUrl)
|
|
84
|
+
let subscribed = ref(false)
|
|
85
|
+
|
|
86
|
+
ws->WebSocket.onopen(() => ws->WebSocket.send(subscribeRequestJson))
|
|
87
|
+
|
|
88
|
+
ws->WebSocket.onmessage(event =>
|
|
89
|
+
if event.data->typeof === #string {
|
|
90
|
+
let text = event.data->(Utils.magic: unknown => string)
|
|
91
|
+
let message = Utils.Option.catchToNone(
|
|
92
|
+
() => text->JSON.parseOrThrow->S.parseOrThrow(wsMessageSchema),
|
|
93
|
+
)
|
|
94
|
+
switch message {
|
|
95
|
+
| Some(NewHead(blockNumber)) => driver.onHeight(blockNumber)
|
|
96
|
+
// An open socket isn't usable until the node accepts the subscription,
|
|
97
|
+
// and only the answer to the subscribe says that it has. A success frame
|
|
98
|
+
// answering anything else would otherwise report a stream as live
|
|
99
|
+
// without one ever having been subscribed.
|
|
100
|
+
| Some(Response({id: Some(id), isError: false})) if id === subscribeRequestIdText =>
|
|
101
|
+
subscribed := true
|
|
102
|
+
driver.onConnected()
|
|
103
|
+
| Some(Response({id, isError: true})) if id->refusesSubscribe(~subscribed=subscribed.contents) =>
|
|
104
|
+
driver.onFailure(~reason=SubscribeRejected, ~detail=text)
|
|
105
|
+
// A provider talking about something other than the subscription this
|
|
106
|
+
// socket asked for — a rate limit, a request nobody here made. Read
|
|
107
|
+
// fine, so not unreadable, and not this stream's to tear itself down
|
|
108
|
+
// over.
|
|
109
|
+
| Some(Response(_)) => ()
|
|
110
|
+
| None => driver.onUnreadable(~detail=text)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// A binary frame from a peer that should only ever be sending JSON text.
|
|
114
|
+
driver.onUnreadable(~detail="binary frame")
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
ws->WebSocket.onerror(error =>
|
|
119
|
+
driver.onFailure(~reason=TransportError, ~detail=?error->JsExn.message)
|
|
120
|
+
)
|
|
121
|
+
ws->WebSocket.onclose(() => driver.onFailure(~reason=Closed))
|
|
122
|
+
|
|
123
|
+
() => ws->WebSocket.close
|
|
124
|
+
})
|