envio 3.6.1 → 3.7.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/index.d.ts +197 -9
- package/package.json +6 -6
- package/src/Batch.res +64 -25
- package/src/Batch.res.mjs +69 -46
- package/src/ChainFetching.res +13 -17
- package/src/ChainFetching.res.mjs +9 -14
- package/src/ChainState.res +160 -119
- package/src/ChainState.res.mjs +75 -46
- package/src/ChainState.resi +15 -12
- package/src/Config.res +9 -2
- package/src/Config.res.mjs +4 -4
- package/src/Core.res +5 -0
- package/src/Ecosystem.res +0 -1
- package/src/Envio.res +16 -3
- package/src/EventConfigBuilder.res +155 -40
- package/src/EventConfigBuilder.res.mjs +76 -24
- package/src/EventProcessing.res +4 -8
- package/src/EventProcessing.res.mjs +4 -4
- package/src/EventUtils.res +3 -47
- package/src/FetchState.res +124 -104
- package/src/FetchState.res.mjs +116 -100
- package/src/HandlerRegister.res +108 -2
- package/src/HandlerRegister.res.mjs +59 -10
- package/src/HandlerRegister.resi +4 -0
- package/src/Internal.res +84 -24
- package/src/Internal.res.mjs +33 -2
- package/src/LazyLoader.res +12 -11
- package/src/LazyLoader.res.mjs +13 -7
- package/src/Main.res +22 -9
- package/src/Main.res.mjs +18 -5
- package/src/MemoryStorage.res +724 -0
- package/src/MemoryStorage.res.mjs +601 -0
- package/src/PgStorage.res +18 -7
- package/src/PgStorage.res.mjs +14 -11
- package/src/ReorgDetection.res +0 -222
- package/src/ReorgDetection.res.mjs +0 -163
- package/src/Rollback.res +14 -15
- package/src/Rollback.res.mjs +4 -5
- package/src/SimulateItems.res +2 -2
- package/src/Utils.res +3 -0
- package/src/bindings/ClickHouse.res +16 -11
- package/src/bindings/ClickHouse.res.mjs +10 -10
- package/src/bindings/Vitest.res +1 -1
- package/src/sources/BlockStore.res +115 -5
- package/src/sources/BlockStore.res.mjs +25 -0
- package/src/sources/Evm.res +0 -1
- package/src/sources/Evm.res.mjs +0 -1
- package/src/sources/EvmHyperSyncSource.res +19 -84
- package/src/sources/EvmHyperSyncSource.res.mjs +24 -60
- package/src/sources/Fuel.res +24 -4
- package/src/sources/Fuel.res.mjs +23 -3
- package/src/sources/FuelHyperSync.res +8 -3
- package/src/sources/FuelHyperSync.res.mjs +5 -4
- package/src/sources/FuelHyperSync.resi +3 -1
- package/src/sources/FuelHyperSyncClient.res +7 -10
- package/src/sources/FuelHyperSyncSource.res +18 -45
- package/src/sources/FuelHyperSyncSource.res.mjs +18 -35
- package/src/sources/HyperSync.res +49 -229
- package/src/sources/HyperSync.res.mjs +74 -191
- package/src/sources/HyperSync.resi +11 -35
- package/src/sources/HyperSyncClient.res +9 -79
- package/src/sources/HyperSyncClient.res.mjs +2 -6
- package/src/sources/RequestStat.res +5 -0
- package/src/sources/RequestStat.res.mjs +2 -0
- package/src/sources/RpcSource.res +149 -43
- package/src/sources/RpcSource.res.mjs +104 -41
- package/src/sources/SimulateSource.res +8 -5
- package/src/sources/SimulateSource.res.mjs +6 -6
- package/src/sources/Source.res +89 -16
- package/src/sources/Source.res.mjs +50 -1
- package/src/sources/SourceManager.res +255 -50
- package/src/sources/SourceManager.res.mjs +149 -55
- package/src/sources/SourceManager.resi +2 -6
- package/src/sources/Svm.res +0 -7
- package/src/sources/Svm.res.mjs +0 -8
- package/src/sources/SvmHyperSyncClient.res +21 -16
- package/src/sources/SvmHyperSyncClient.res.mjs +3 -4
- package/src/sources/SvmHyperSyncSource.res +25 -117
- package/src/sources/SvmHyperSyncSource.res.mjs +8 -121
- package/svm.schema.json +3 -2
|
@@ -14,8 +14,13 @@ type execParams = {query: string}
|
|
|
14
14
|
@module("@clickhouse/client")
|
|
15
15
|
external createClient: clientConfig => client = "createClient"
|
|
16
16
|
|
|
17
|
+
// `command`, not `exec`: exec hands its response stream to the caller and holds
|
|
18
|
+
// the socket until it is consumed, which nothing here does. The pool is 10
|
|
19
|
+
// sockets wide, so a schema whose DDL needs more than that — initialize issues
|
|
20
|
+
// 2N+3 statements for N entities — stalled every statement past the tenth until
|
|
21
|
+
// the 30s request timeout freed one. command destroys the stream for us.
|
|
17
22
|
@send
|
|
18
|
-
external
|
|
23
|
+
external command: (client, execParams) => promise<unit> = "command"
|
|
19
24
|
|
|
20
25
|
@send
|
|
21
26
|
external close: client => promise<unit> = "close"
|
|
@@ -628,21 +633,21 @@ let initialize = async (
|
|
|
628
633
|
// CLUSTER removes the database from every node — the engine's own log
|
|
629
634
|
// can't replicate the drop of the database it lives in — and SYNC waits
|
|
630
635
|
// for the drop to finish before the CREATE below.
|
|
631
|
-
await client->
|
|
636
|
+
await client->command({
|
|
632
637
|
query: `DROP DATABASE IF EXISTS ${database} ON CLUSTER '{cluster}' SYNC`,
|
|
633
638
|
})
|
|
634
639
|
} else {
|
|
635
|
-
await client->
|
|
640
|
+
await client->command({
|
|
636
641
|
query: `TRUNCATE DATABASE IF EXISTS ${database}${onClusterClause(~onCluster=ddlOnCluster)}`,
|
|
637
642
|
})
|
|
638
643
|
}
|
|
639
|
-
await client->
|
|
644
|
+
await client->command({
|
|
640
645
|
query: `CREATE DATABASE IF NOT EXISTS ${database}${databaseOnClusterClause}${databaseEngineClause}`,
|
|
641
646
|
})
|
|
642
647
|
|
|
643
648
|
await Promise.all(
|
|
644
649
|
entities->Array.map(entityConfig =>
|
|
645
|
-
client->
|
|
650
|
+
client->command({
|
|
646
651
|
query: makeCreateHistoryTableQuery(
|
|
647
652
|
~entityConfig,
|
|
648
653
|
~database,
|
|
@@ -653,7 +658,7 @@ let initialize = async (
|
|
|
653
658
|
})
|
|
654
659
|
),
|
|
655
660
|
)->Utils.Promise.ignoreValue
|
|
656
|
-
await client->
|
|
661
|
+
await client->command({
|
|
657
662
|
query: makeCreateCheckpointsTableQuery(
|
|
658
663
|
~database,
|
|
659
664
|
~replicated,
|
|
@@ -670,14 +675,14 @@ let initialize = async (
|
|
|
670
675
|
// caught up before creating the views. ON CLUSTER must precede the
|
|
671
676
|
// database name in this command's grammar.
|
|
672
677
|
if hasReplicatedDatabaseEngine {
|
|
673
|
-
await client->
|
|
678
|
+
await client->command({
|
|
674
679
|
query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ${database}`,
|
|
675
680
|
})
|
|
676
681
|
}
|
|
677
682
|
|
|
678
683
|
await Promise.all(
|
|
679
684
|
entities->Array.map(entityConfig =>
|
|
680
|
-
client->
|
|
685
|
+
client->command({
|
|
681
686
|
query: makeCreateViewQuery(~entityConfig, ~database, ~onCluster=ddlOnCluster),
|
|
682
687
|
})
|
|
683
688
|
),
|
|
@@ -697,7 +702,7 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
|
|
|
697
702
|
try {
|
|
698
703
|
// Try to use the database - will throw if it doesn't exist
|
|
699
704
|
try {
|
|
700
|
-
await client->
|
|
705
|
+
await client->command({query: `USE ${database}`})
|
|
701
706
|
} catch {
|
|
702
707
|
| exn =>
|
|
703
708
|
Logging.errorWithExn(
|
|
@@ -717,14 +722,14 @@ let resume = async (client, ~database: string, ~checkpointId: Internal.checkpoin
|
|
|
717
722
|
await Promise.all(
|
|
718
723
|
tables->Array.map(table => {
|
|
719
724
|
let tableName = table["name"]
|
|
720
|
-
client->
|
|
725
|
+
client->command({
|
|
721
726
|
query: `ALTER TABLE ${database}.\`${tableName}\` DELETE WHERE \`${EntityHistory.checkpointIdFieldName}\` > ${checkpointId->BigInt.toString}`,
|
|
722
727
|
})
|
|
723
728
|
}),
|
|
724
729
|
)->Utils.Promise.ignoreValue
|
|
725
730
|
|
|
726
731
|
// Delete stale checkpoints
|
|
727
|
-
await client->
|
|
732
|
+
await client->command({
|
|
728
733
|
query: `DELETE FROM ${database}.\`${InternalTable.Checkpoints.table.tableName}\` WHERE \`${Table.idFieldName}\` > ${checkpointId->BigInt.toString}`,
|
|
729
734
|
})
|
|
730
735
|
} catch {
|
|
@@ -428,31 +428,31 @@ async function initialize(client, database, entities, param, chainIdModeOpt) {
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
if (hasReplicatedDatabaseEngine) {
|
|
431
|
-
await client.
|
|
431
|
+
await client.command({
|
|
432
432
|
query: `DROP DATABASE IF EXISTS ` + database + ` ON CLUSTER '{cluster}' SYNC`
|
|
433
433
|
});
|
|
434
434
|
} else {
|
|
435
|
-
await client.
|
|
435
|
+
await client.command({
|
|
436
436
|
query: `TRUNCATE DATABASE IF EXISTS ` + database + (
|
|
437
437
|
ddlOnCluster ? ` ON CLUSTER '{cluster}'` : ""
|
|
438
438
|
)
|
|
439
439
|
});
|
|
440
440
|
}
|
|
441
|
-
await client.
|
|
441
|
+
await client.command({
|
|
442
442
|
query: `CREATE DATABASE IF NOT EXISTS ` + database + databaseOnClusterClause + databaseEngineClause
|
|
443
443
|
});
|
|
444
|
-
await Promise.all(entities.map(entityConfig => client.
|
|
444
|
+
await Promise.all(entities.map(entityConfig => client.command({
|
|
445
445
|
query: makeCreateHistoryTableQuery(entityConfig, database, replicated, ddlOnCluster, chainIdMode)
|
|
446
446
|
})));
|
|
447
|
-
await client.
|
|
447
|
+
await client.command({
|
|
448
448
|
query: makeCreateCheckpointsTableQuery(database, replicated, ddlOnCluster, chainIdMode)
|
|
449
449
|
});
|
|
450
450
|
if (hasReplicatedDatabaseEngine) {
|
|
451
|
-
await client.
|
|
451
|
+
await client.command({
|
|
452
452
|
query: `SYSTEM SYNC DATABASE REPLICA ON CLUSTER '{cluster}' ` + database
|
|
453
453
|
});
|
|
454
454
|
}
|
|
455
|
-
await Promise.all(entities.map(entityConfig => client.
|
|
455
|
+
await Promise.all(entities.map(entityConfig => client.command({
|
|
456
456
|
query: makeCreateViewQuery(entityConfig, database, ddlOnCluster)
|
|
457
457
|
})));
|
|
458
458
|
return Logging.trace("ClickHouse storage initialization completed successfully");
|
|
@@ -466,7 +466,7 @@ async function initialize(client, database, entities, param, chainIdModeOpt) {
|
|
|
466
466
|
async function resume(client, database, checkpointId) {
|
|
467
467
|
try {
|
|
468
468
|
try {
|
|
469
|
-
await client.
|
|
469
|
+
await client.command({
|
|
470
470
|
query: `USE ` + database
|
|
471
471
|
});
|
|
472
472
|
} catch (raw_exn) {
|
|
@@ -480,11 +480,11 @@ async function resume(client, database, checkpointId) {
|
|
|
480
480
|
let tables = (await tablesResult.json()).data;
|
|
481
481
|
await Promise.all(tables.map(table => {
|
|
482
482
|
let tableName = table.name;
|
|
483
|
-
return client.
|
|
483
|
+
return client.command({
|
|
484
484
|
query: `ALTER TABLE ` + database + `.\`` + tableName + `\` DELETE WHERE \`` + EntityHistory.checkpointIdFieldName + `\` > ` + checkpointId.toString()
|
|
485
485
|
});
|
|
486
486
|
}));
|
|
487
|
-
return await client.
|
|
487
|
+
return await client.command({
|
|
488
488
|
query: `DELETE FROM ` + database + `.\`` + InternalTable.Checkpoints.table.tableName + `\` WHERE \`` + Table.idFieldName + `\` > ` + checkpointId.toString()
|
|
489
489
|
});
|
|
490
490
|
} catch (raw_exn$1) {
|
package/src/bindings/Vitest.res
CHANGED
|
@@ -98,7 +98,7 @@ external afterEach: (unit => unit) => unit = "afterEach"
|
|
|
98
98
|
// Async Module
|
|
99
99
|
// ============================================================================
|
|
100
100
|
|
|
101
|
-
type options = {retry?: int}
|
|
101
|
+
type options = {retry?: int, timeout?: int}
|
|
102
102
|
|
|
103
103
|
module Async = {
|
|
104
104
|
@module("vitest")
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// page that is merged in. At batch preparation the selected fields are
|
|
5
5
|
// materialised in bulk, off the JS thread, in columnar form and zipped into
|
|
6
6
|
// plain JS objects on the main thread.
|
|
7
|
+
//
|
|
8
|
+
// The store also owns response validation and reorg detection. A response page
|
|
9
|
+
// records conflicts found while it is built; SourceManager rejects those pages
|
|
10
|
+
// before the persistent store is touched. Merging a validated page compares
|
|
11
|
+
// only persistent-vs-response hashes, and pruning keeps the hash of processed
|
|
12
|
+
// blocks still inside the reorg threshold.
|
|
7
13
|
type t
|
|
8
14
|
|
|
9
15
|
@send external newEvm: (Core.blockStoreCtor, ~shouldChecksum: bool) => t = "newEvm"
|
|
@@ -26,8 +32,93 @@ let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool): t => {
|
|
|
26
32
|
// with the Rust store, `EvmBlockField`).
|
|
27
33
|
let makeMaskFn = FieldMask.makeMaskFn
|
|
28
34
|
|
|
29
|
-
//
|
|
30
|
-
|
|
35
|
+
// Sparse JS blocks accepted by the `fromJs*` page constructors. Every field is
|
|
36
|
+
// optional except the key, so a page can carry anything from a full block to a
|
|
37
|
+
// hash-only reorg observation. The Rust side re-encodes them through the same
|
|
38
|
+
// column fill as fetched blocks.
|
|
39
|
+
type evmBlockInput = {number: int, hash?: string, timestamp?: int}
|
|
40
|
+
type svmBlockInput = {slot: int, hash?: string, time?: int}
|
|
41
|
+
type fuelBlockInput = {height: int, id?: string, time?: int}
|
|
42
|
+
|
|
43
|
+
@send
|
|
44
|
+
external fromJsEvm: (Core.blockStoreCtor, array<evmBlockInput>, bool) => t = "fromJsEvm"
|
|
45
|
+
@send
|
|
46
|
+
external fromJsSvm: (Core.blockStoreCtor, array<svmBlockInput>) => t = "fromJsSvm"
|
|
47
|
+
@send
|
|
48
|
+
external fromJsFuel: (Core.blockStoreCtor, array<fuelBlockInput>) => t = "fromJsFuel"
|
|
49
|
+
|
|
50
|
+
// An ecosystem-agnostic (number, hash, timestamp) observation, mapped onto the
|
|
51
|
+
// ecosystem's own field names when the page is built.
|
|
52
|
+
type inputBlock = {blockNumber: int, blockHash?: string, blockTimestamp?: int}
|
|
53
|
+
|
|
54
|
+
// Build a page from JS-observed blocks (RPC responses, stored reorg
|
|
55
|
+
// checkpoints) for merging into the per-chain store.
|
|
56
|
+
let fromJs = (blocks: array<inputBlock>, ~ecosystem: Ecosystem.name, ~shouldChecksum): t => {
|
|
57
|
+
let ctor = Core.getAddon().blockStore
|
|
58
|
+
switch ecosystem {
|
|
59
|
+
| Evm =>
|
|
60
|
+
ctor->fromJsEvm(
|
|
61
|
+
blocks->Array.map(b => {
|
|
62
|
+
number: b.blockNumber,
|
|
63
|
+
hash: ?b.blockHash,
|
|
64
|
+
timestamp: ?b.blockTimestamp,
|
|
65
|
+
}),
|
|
66
|
+
shouldChecksum,
|
|
67
|
+
)
|
|
68
|
+
| Svm =>
|
|
69
|
+
ctor->fromJsSvm(
|
|
70
|
+
blocks->Array.map(b => {
|
|
71
|
+
slot: b.blockNumber,
|
|
72
|
+
hash: ?b.blockHash,
|
|
73
|
+
time: ?b.blockTimestamp,
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
| Fuel =>
|
|
77
|
+
ctor->fromJsFuel(
|
|
78
|
+
blocks->Array.map(b => {
|
|
79
|
+
height: b.blockNumber,
|
|
80
|
+
id: ?b.blockHash,
|
|
81
|
+
time: ?b.blockTimestamp,
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The lowest merged block at or above the reorg threshold whose received hash
|
|
88
|
+
// differed from the stored one.
|
|
89
|
+
type hashMismatch = {
|
|
90
|
+
blockNumber: int,
|
|
91
|
+
storedHash: string,
|
|
92
|
+
receivedHash: string,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Drain another store (a fetch-response page) into this one, comparing hashes
|
|
96
|
+
// on the way. Blocks below `fromBlock` (outside the reorg threshold) or without
|
|
97
|
+
// a hash on either side are merged without comparison. On a mismatch the page
|
|
98
|
+
// is discarded - the stored hashes stay for the rollback comparison - unless
|
|
99
|
+
// `reportOnly` is set (detect-only mode), which merges anyway so the same
|
|
100
|
+
// mismatch doesn't re-report on every response.
|
|
101
|
+
@send
|
|
102
|
+
external merge: (t, t, ~fromBlock: int, ~reportOnly: bool) => Null.t<hashMismatch> = "merge"
|
|
103
|
+
|
|
104
|
+
// Append a backend page to a logical response store. This always appends rows;
|
|
105
|
+
// any conflict is retained as response metadata for SourceManager to validate.
|
|
106
|
+
@send
|
|
107
|
+
external appendPage: (t, t) => unit = "appendPage"
|
|
108
|
+
|
|
109
|
+
// A conflict observed within the response itself. Such a response is
|
|
110
|
+
// discarded and retried, rather than treated as a chain reorg.
|
|
111
|
+
@send external responseConflict: t => Null.t<hashMismatch> = "responseConflict"
|
|
112
|
+
|
|
113
|
+
// Requested block numbers not covered by this response. SVM gaps count as
|
|
114
|
+
// covered when HyperSync's cursor has fully processed their half-open range;
|
|
115
|
+
// parent slot/hash links are validated separately as response consistency.
|
|
116
|
+
@send external missingHashes: (t, array<int>) => array<int> = "missingHashes"
|
|
117
|
+
|
|
118
|
+
// Compare a validated response store against the persistent store in ascending
|
|
119
|
+
// block order and stop at the first mismatch.
|
|
120
|
+
@send
|
|
121
|
+
external latestValidBlockFromStore: (t, t, array<int>) => Null.t<int> = "latestValidBlockFromStore"
|
|
31
122
|
|
|
32
123
|
// Bulk-materialise blocks off the JS thread, one row per `blockNumbers[i]` key,
|
|
33
124
|
// decoding only the fields set in that row's own `masks[i]`. Result is aligned
|
|
@@ -39,8 +130,27 @@ external materialize: (
|
|
|
39
130
|
~masks: array<float>,
|
|
40
131
|
) => promise<array<Internal.eventBlock>> = "materialize"
|
|
41
132
|
|
|
42
|
-
// Drop blocks at or below the given block (already processed)
|
|
43
|
-
|
|
133
|
+
// Drop blocks at or below the given block (already processed), keeping the
|
|
134
|
+
// hashes of blocks at or above `keepHashesFrom` for reorg detection.
|
|
135
|
+
@send external prune: (t, int, ~keepHashesFrom: int) => unit = "prune"
|
|
44
136
|
|
|
45
|
-
// Drop blocks above the given block (rolled back).
|
|
137
|
+
// Drop blocks above the given block (rolled back), hashes included. The
|
|
138
|
+
// rolled-back range is refetched, so keeping any of its hashes would compare
|
|
139
|
+
// the refetch against a fork the rollback already disproved.
|
|
46
140
|
@send external rollback: (t, int) => unit = "rollback"
|
|
141
|
+
|
|
142
|
+
// Hash of a stored block, if the store still holds it.
|
|
143
|
+
@send external getHash: (t, int) => Null.t<string> = "getHash"
|
|
144
|
+
|
|
145
|
+
// Block numbers in `[fromBlock, belowBlock)` with a stored hash, ascending.
|
|
146
|
+
@send
|
|
147
|
+
external getHashedBlockNumbers: (t, ~fromBlock: int, ~belowBlock: int) => array<int> =
|
|
148
|
+
"getHashedBlockNumbers"
|
|
149
|
+
|
|
150
|
+
// Every stored hash in `[fromBlock, belowBlock)` as two aligned columns,
|
|
151
|
+
// ascending by block number - one crossing for what `getHash` would charge per
|
|
152
|
+
// block.
|
|
153
|
+
type hashedBlocks = {blockNumbers: array<int>, hashes: array<string>}
|
|
154
|
+
|
|
155
|
+
@send
|
|
156
|
+
external getHashes: (t, ~fromBlock: int, ~belowBlock: int) => hashedBlocks = "getHashes"
|
|
@@ -15,10 +15,35 @@ function make(ecosystem, shouldChecksum) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function fromJs(blocks, ecosystem, shouldChecksum) {
|
|
19
|
+
let ctor = Core.getAddon().BlockStore;
|
|
20
|
+
switch (ecosystem) {
|
|
21
|
+
case "evm" :
|
|
22
|
+
return ctor.fromJsEvm(blocks.map(b => ({
|
|
23
|
+
number: b.blockNumber,
|
|
24
|
+
hash: b.blockHash,
|
|
25
|
+
timestamp: b.blockTimestamp
|
|
26
|
+
})), shouldChecksum);
|
|
27
|
+
case "fuel" :
|
|
28
|
+
return ctor.fromJsFuel(blocks.map(b => ({
|
|
29
|
+
height: b.blockNumber,
|
|
30
|
+
id: b.blockHash,
|
|
31
|
+
time: b.blockTimestamp
|
|
32
|
+
})));
|
|
33
|
+
case "svm" :
|
|
34
|
+
return ctor.fromJsSvm(blocks.map(b => ({
|
|
35
|
+
slot: b.blockNumber,
|
|
36
|
+
hash: b.blockHash,
|
|
37
|
+
time: b.blockTimestamp
|
|
38
|
+
})));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
let makeMaskFn = FieldMask.makeMaskFn;
|
|
19
43
|
|
|
20
44
|
export {
|
|
21
45
|
make,
|
|
22
46
|
makeMaskFn,
|
|
47
|
+
fromJs,
|
|
23
48
|
}
|
|
24
49
|
/* Core Not a pure module */
|
package/src/sources/Evm.res
CHANGED
|
@@ -76,7 +76,6 @@ let make = (~logger: Pino.t): Ecosystem.t => {
|
|
|
76
76
|
blockNumberName: "number",
|
|
77
77
|
blockTimestampName: "timestamp",
|
|
78
78
|
blockHashName: "hash",
|
|
79
|
-
cleanUpRawEventFieldsInPlace,
|
|
80
79
|
onBlockMethodName: "onBlock",
|
|
81
80
|
// EVM filter shape: `{block: {number: {_gte?, _lte?, _every?}}}`.
|
|
82
81
|
// The inner range chunk is returned as raw `S.unknown` and parsed a
|
package/src/sources/Evm.res.mjs
CHANGED
|
@@ -55,7 +55,6 @@ function make(logger) {
|
|
|
55
55
|
blockNumberName: "number",
|
|
56
56
|
blockTimestampName: "timestamp",
|
|
57
57
|
blockHashName: "hash",
|
|
58
|
-
cleanUpRawEventFieldsInPlace: cleanUpRawEventFieldsInPlace,
|
|
59
58
|
onBlockMethodName: "onBlock",
|
|
60
59
|
onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
|
|
61
60
|
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
|
|
@@ -79,7 +79,7 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
79
79
|
payload: {
|
|
80
80
|
contractName: onEventRegistration.eventConfig.contractName,
|
|
81
81
|
eventName: onEventRegistration.eventConfig.name,
|
|
82
|
-
chainId
|
|
82
|
+
chainId,
|
|
83
83
|
params: item.params,
|
|
84
84
|
srcAddress,
|
|
85
85
|
logIndex,
|
|
@@ -112,33 +112,23 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
112
112
|
~addressSet,
|
|
113
113
|
~clientFilteredContracts=selection.clientFilteredContracts,
|
|
114
114
|
) catch {
|
|
115
|
-
| HyperSync.GetLogs.Error(
|
|
115
|
+
| HyperSync.GetLogs.Error(WrongInstance) =>
|
|
116
|
+
throw(Source.SourceBehindHead({blockNumber: fromBlock, requestStats: []}))
|
|
117
|
+
| HyperSync.GetLogs.Error(UnexpectedMissingParams({missingParams})) =>
|
|
116
118
|
throw(
|
|
117
119
|
Source.GetItemsError(
|
|
118
120
|
Source.FailedGettingItems({
|
|
119
121
|
exn: %raw(`null`),
|
|
120
122
|
attemptedToBlock: toBlock->Option.getOr(knownHeight),
|
|
121
|
-
retry:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
WithBackoff({
|
|
128
|
-
message: `Block #${fromBlock->Int.toString} not found in HyperSync. HyperSync 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.`,
|
|
129
|
-
backoffMillis,
|
|
130
|
-
})
|
|
131
|
-
| UnexpectedMissingParams({missingParams}) =>
|
|
132
|
-
ImpossibleForTheQuery({
|
|
133
|
-
message: `Source returned invalid data with missing required fields: ${missingParams->Array.joinUnsafe(
|
|
134
|
-
", ",
|
|
135
|
-
)}`,
|
|
136
|
-
})
|
|
137
|
-
},
|
|
123
|
+
retry: ImpossibleForTheQuery({
|
|
124
|
+
message: `Source returned invalid data with missing required fields: ${missingParams->Array.joinUnsafe(
|
|
125
|
+
", ",
|
|
126
|
+
)}`,
|
|
127
|
+
}),
|
|
138
128
|
}),
|
|
139
129
|
),
|
|
140
130
|
)
|
|
141
|
-
| Source.RateLimited(_) as exn => throw(exn)
|
|
131
|
+
| (Source.RateLimited(_) | Source.SourceBehindHead(_)) as exn => throw(exn)
|
|
142
132
|
| exn =>
|
|
143
133
|
throw(
|
|
144
134
|
Source.GetItemsError(
|
|
@@ -173,13 +163,6 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
173
163
|
//Parse page items into queue items
|
|
174
164
|
let parsedQueueItems = []
|
|
175
165
|
|
|
176
|
-
// Block headers are returned once per number; items reference them by blockNumber.
|
|
177
|
-
let blocksByNumber = Utils.Map.make()
|
|
178
|
-
pageUnsafe.blocks->Array.forEach(block => {
|
|
179
|
-
blocksByNumber->Utils.Map.set(block.number, block)->ignore
|
|
180
|
-
})
|
|
181
|
-
let getBlock = blockNumber => blocksByNumber->Utils.Map.unsafeGet(blockNumber)
|
|
182
|
-
|
|
183
166
|
pageUnsafe.items->Array.forEach(item => {
|
|
184
167
|
let onEventRegistration = onEventRegistrations->Array.getUnsafe(item.onEventRegistrationIndex)
|
|
185
168
|
parsedQueueItems
|
|
@@ -189,43 +172,6 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
189
172
|
|
|
190
173
|
let parsingTimeElapsed = parsingTimeRef->Performance.secondsSince
|
|
191
174
|
|
|
192
|
-
// Collect (blockNumber, blockHash) pairs we already have from the response —
|
|
193
|
-
// one per returned block plus, when present, the rollbackGuard's head block
|
|
194
|
-
// and the parent of the range's first block. Duplicates are allowed; reorg
|
|
195
|
-
// detection notices same-block-number-different-hash collisions itself.
|
|
196
|
-
let blockHashes = []
|
|
197
|
-
pageUnsafe.blocks->Array.forEach(block => {
|
|
198
|
-
blockHashes
|
|
199
|
-
->Array.push({ReorgDetection.blockNumber: block.number, blockHash: block.hash})
|
|
200
|
-
->ignore
|
|
201
|
-
})
|
|
202
|
-
switch pageUnsafe.rollbackGuard {
|
|
203
|
-
| None => ()
|
|
204
|
-
| Some({blockNumber, hash, firstBlockNumber, firstParentHash}) => {
|
|
205
|
-
blockHashes->Array.push({ReorgDetection.blockNumber, blockHash: hash})->ignore
|
|
206
|
-
blockHashes
|
|
207
|
-
->Array.push({
|
|
208
|
-
ReorgDetection.blockNumber: firstBlockNumber - 1,
|
|
209
|
-
blockHash: firstParentHash,
|
|
210
|
-
})
|
|
211
|
-
->ignore
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Best-effort timestamp for the queried-range head: prefer the rollbackGuard
|
|
216
|
-
// (set at the head for unconfirmed blocks), otherwise the last item if it
|
|
217
|
-
// happens to be in the range's last block. 0 is a tolerated placeholder
|
|
218
|
-
// when neither is available (FetchState already uses 0 in several spots).
|
|
219
|
-
let latestFetchedBlockTimestamp = switch pageUnsafe.rollbackGuard {
|
|
220
|
-
| Some({timestamp}) => timestamp
|
|
221
|
-
| None =>
|
|
222
|
-
switch pageUnsafe.items->Array.get(pageUnsafe.items->Array.length - 1) {
|
|
223
|
-
| Some(item) if item.blockNumber == heighestBlockQueried =>
|
|
224
|
-
getBlock(item.blockNumber).timestamp
|
|
225
|
-
| _ => 0
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
175
|
let totalTimeElapsed = totalTimeRef->Performance.secondsSince
|
|
230
176
|
|
|
231
177
|
let stats = {
|
|
@@ -235,30 +181,24 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
235
181
|
}
|
|
236
182
|
|
|
237
183
|
{
|
|
238
|
-
latestFetchedBlockTimestamp,
|
|
239
184
|
parsedQueueItems,
|
|
240
185
|
transactionStore: Some(pageUnsafe.transactionStore),
|
|
241
|
-
|
|
186
|
+
// The page store also carries the rollbackGuard's blocks (head block and
|
|
187
|
+
// parent of the range's first block), inserted on the Rust side.
|
|
188
|
+
blockStore: pageUnsafe.blockStore,
|
|
242
189
|
latestFetchedBlockNumber: heighestBlockQueried,
|
|
243
190
|
stats,
|
|
244
191
|
knownHeight,
|
|
245
|
-
blockHashes,
|
|
246
192
|
fromBlockQueried: fromBlock,
|
|
247
193
|
requestStats,
|
|
248
194
|
}
|
|
249
195
|
}
|
|
250
196
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
~chainId=chainId,
|
|
257
|
-
~logger,
|
|
258
|
-
)->Promise.thenResolve(((queryRes, requestStats)) => {
|
|
259
|
-
Source.result: queryRes->HyperSync.mapExn,
|
|
260
|
-
requestStats,
|
|
261
|
-
})
|
|
197
|
+
// Called through the client rather than passed as a value: the client is a
|
|
198
|
+
// napi class, so a detached method reference loses the instance it belongs to.
|
|
199
|
+
let getBlockHashes = HyperSync.makeGetBlockHashes(
|
|
200
|
+
~query=(~blockNumbers) => client.getBlockHashes(~blockNumbers),
|
|
201
|
+
)
|
|
262
202
|
|
|
263
203
|
{
|
|
264
204
|
name,
|
|
@@ -287,11 +227,6 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`)
|
|
|
287
227
|
},
|
|
288
228
|
getItemsOrThrow,
|
|
289
229
|
createHeightSubscription: (~onHeight) =>
|
|
290
|
-
HyperSyncHeightStream.subscribe(
|
|
291
|
-
~hyperSyncUrl=endpointUrl,
|
|
292
|
-
~apiToken,
|
|
293
|
-
~chainId=chainId,
|
|
294
|
-
~onHeight,
|
|
295
|
-
),
|
|
230
|
+
HyperSyncHeightStream.subscribe(~hyperSyncUrl=endpointUrl, ~apiToken, ~chainId, ~onHeight),
|
|
296
231
|
}
|
|
297
232
|
}
|
|
@@ -22,7 +22,6 @@ function make(param) {
|
|
|
22
22
|
let onEventRegistrations = param.onEventRegistrations;
|
|
23
23
|
let endpointUrl = param.endpointUrl;
|
|
24
24
|
let chainId = param.chainId;
|
|
25
|
-
let name = "HyperSync";
|
|
26
25
|
let apiToken$1 = apiToken !== undefined ? apiToken : Stdlib_JsError.throwWithMessage(`An Envio API token is required for using HyperSync as a data-source.
|
|
27
26
|
Set the ENVIO_API_TOKEN environment variable in your .env file.
|
|
28
27
|
Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
@@ -58,22 +57,16 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
|
58
57
|
let pageUnsafe;
|
|
59
58
|
try {
|
|
60
59
|
pageUnsafe = await HyperSync.GetLogs.query(client, fromBlock, toBlock, itemsTarget, selection.onEventRegistrations.map(reg => reg.index), addressSet, selection.clientFilteredContracts);
|
|
61
|
-
} catch (
|
|
62
|
-
let
|
|
63
|
-
if (
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
backoffMillis: backoffMillis
|
|
72
|
-
};
|
|
73
|
-
} else {
|
|
74
|
-
tmp = {
|
|
75
|
-
TAG: "ImpossibleForTheQuery",
|
|
76
|
-
message: `Source returned invalid data with missing required fields: ` + error$1.missingParams.join(", ")
|
|
60
|
+
} catch (raw_exn) {
|
|
61
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
62
|
+
if (exn.RE_EXN_ID === HyperSync.GetLogs.$$Error) {
|
|
63
|
+
let match = exn._1;
|
|
64
|
+
if (typeof match !== "object") {
|
|
65
|
+
throw {
|
|
66
|
+
RE_EXN_ID: Source.SourceBehindHead,
|
|
67
|
+
blockNumber: fromBlock,
|
|
68
|
+
requestStats: [],
|
|
69
|
+
Error: new Error()
|
|
77
70
|
};
|
|
78
71
|
}
|
|
79
72
|
throw {
|
|
@@ -82,19 +75,25 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
|
82
75
|
TAG: "FailedGettingItems",
|
|
83
76
|
exn: null,
|
|
84
77
|
attemptedToBlock: Stdlib_Option.getOr(toBlock, knownHeight),
|
|
85
|
-
retry:
|
|
78
|
+
retry: {
|
|
79
|
+
TAG: "ImpossibleForTheQuery",
|
|
80
|
+
message: `Source returned invalid data with missing required fields: ` + match.missingParams.join(", ")
|
|
81
|
+
}
|
|
86
82
|
},
|
|
87
83
|
Error: new Error()
|
|
88
84
|
};
|
|
89
85
|
}
|
|
90
|
-
if (
|
|
91
|
-
throw
|
|
86
|
+
if (exn.RE_EXN_ID === Source.RateLimited) {
|
|
87
|
+
throw exn;
|
|
88
|
+
}
|
|
89
|
+
if (exn.RE_EXN_ID === Source.SourceBehindHead) {
|
|
90
|
+
throw exn;
|
|
92
91
|
}
|
|
93
92
|
throw {
|
|
94
93
|
RE_EXN_ID: Source.GetItemsError,
|
|
95
94
|
_1: {
|
|
96
95
|
TAG: "FailedGettingItems",
|
|
97
|
-
exn:
|
|
96
|
+
exn: exn,
|
|
98
97
|
attemptedToBlock: Stdlib_Option.getOr(toBlock, knownHeight),
|
|
99
98
|
retry: {
|
|
100
99
|
TAG: "WithBackoff",
|
|
@@ -114,41 +113,11 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
|
114
113
|
let heighestBlockQueried = pageUnsafe.nextBlock - 1 | 0;
|
|
115
114
|
let parsingTimeRef = Performance.now();
|
|
116
115
|
let parsedQueueItems = [];
|
|
117
|
-
let blocksByNumber = new Map();
|
|
118
|
-
pageUnsafe.blocks.forEach(block => {
|
|
119
|
-
blocksByNumber.set(block.number, block);
|
|
120
|
-
});
|
|
121
116
|
pageUnsafe.items.forEach(item => {
|
|
122
117
|
let onEventRegistration = onEventRegistrations[item.onEventRegistrationIndex];
|
|
123
118
|
parsedQueueItems.push(makeEventBatchQueueItem(item, onEventRegistration));
|
|
124
119
|
});
|
|
125
120
|
let parsingTimeElapsed = Performance.secondsSince(parsingTimeRef);
|
|
126
|
-
let blockHashes = [];
|
|
127
|
-
pageUnsafe.blocks.forEach(block => {
|
|
128
|
-
blockHashes.push({
|
|
129
|
-
blockHash: block.hash,
|
|
130
|
-
blockNumber: block.number
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
let match = pageUnsafe.rollbackGuard;
|
|
134
|
-
if (match !== undefined) {
|
|
135
|
-
blockHashes.push({
|
|
136
|
-
blockHash: match.hash,
|
|
137
|
-
blockNumber: match.blockNumber
|
|
138
|
-
});
|
|
139
|
-
blockHashes.push({
|
|
140
|
-
blockHash: match.firstParentHash,
|
|
141
|
-
blockNumber: match.firstBlockNumber - 1 | 0
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
let match$1 = pageUnsafe.rollbackGuard;
|
|
145
|
-
let latestFetchedBlockTimestamp;
|
|
146
|
-
if (match$1 !== undefined) {
|
|
147
|
-
latestFetchedBlockTimestamp = match$1.timestamp;
|
|
148
|
-
} else {
|
|
149
|
-
let item = pageUnsafe.items[pageUnsafe.items.length - 1 | 0];
|
|
150
|
-
latestFetchedBlockTimestamp = item !== undefined && item.blockNumber === heighestBlockQueried ? blocksByNumber.get(item.blockNumber).timestamp : 0;
|
|
151
|
-
}
|
|
152
121
|
let totalTimeElapsed = Performance.secondsSince(totalTimeRef);
|
|
153
122
|
let stats_parsing$unknowntime$unknown$lpars$rpar = parsingTimeElapsed;
|
|
154
123
|
let stats_page$unknownfetch$unknowntime$unknown$lpars$rpar = pageFetchTime;
|
|
@@ -159,23 +128,18 @@ Learn more or get a free Envio API token at: https://envio.dev/app/api-tokens`);
|
|
|
159
128
|
};
|
|
160
129
|
return {
|
|
161
130
|
knownHeight: knownHeight$1,
|
|
162
|
-
blockHashes: blockHashes,
|
|
163
131
|
parsedQueueItems: parsedQueueItems,
|
|
164
132
|
transactionStore: Primitive_option.some(pageUnsafe.transactionStore),
|
|
165
|
-
blockStore:
|
|
133
|
+
blockStore: pageUnsafe.blockStore,
|
|
166
134
|
fromBlockQueried: fromBlock,
|
|
167
135
|
latestFetchedBlockNumber: heighestBlockQueried,
|
|
168
|
-
latestFetchedBlockTimestamp: latestFetchedBlockTimestamp,
|
|
169
136
|
stats: stats,
|
|
170
137
|
requestStats: requestStats
|
|
171
138
|
};
|
|
172
139
|
};
|
|
173
|
-
let getBlockHashes = (blockNumbers
|
|
174
|
-
result: HyperSync.mapExn(param[0]),
|
|
175
|
-
requestStats: param[1]
|
|
176
|
-
}));
|
|
140
|
+
let getBlockHashes = HyperSync.makeGetBlockHashes(blockNumbers => client.getBlockHashes(blockNumbers));
|
|
177
141
|
return {
|
|
178
|
-
name:
|
|
142
|
+
name: "HyperSync",
|
|
179
143
|
sourceFor: "Sync",
|
|
180
144
|
chainId: chainId,
|
|
181
145
|
poweredByHyperSync: true,
|
|
@@ -232,4 +196,4 @@ export {
|
|
|
232
196
|
isUnauthorizedError,
|
|
233
197
|
make,
|
|
234
198
|
}
|
|
235
|
-
/*
|
|
199
|
+
/* Source Not a pure module */
|