envio 3.5.0-alpha.2 → 3.5.0-rc.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/package.json +6 -6
- package/src/Batch.res +8 -8
- package/src/Batch.res.mjs +4 -3
- package/src/BatchProcessing.res +24 -0
- package/src/BatchProcessing.res.mjs +12 -0
- package/src/ChainFetching.res +20 -19
- package/src/ChainFetching.res.mjs +14 -14
- package/src/ChainId.res +58 -0
- package/src/ChainId.res.mjs +49 -0
- package/src/ChainId.resi +27 -0
- package/src/ChainMap.res +12 -26
- package/src/ChainMap.res.mjs +5 -41
- package/src/ChainMap.resi +5 -20
- package/src/ChainMetadata.res +1 -1
- package/src/ChainMetadata.res.mjs +3 -2
- package/src/ChainState.res +45 -24
- package/src/ChainState.res.mjs +40 -16
- package/src/ChainState.resi +3 -1
- package/src/Config.res +43 -26
- package/src/Config.res.mjs +19 -16
- package/src/ContractRegisterContext.res +1 -1
- package/src/ContractRegisterContext.res.mjs +1 -1
- package/src/Core.res +1 -0
- package/src/CrossChainState.res +90 -22
- package/src/CrossChainState.res.mjs +55 -16
- package/src/CrossChainState.resi +5 -1
- package/src/Env.res +0 -3
- package/src/Env.res.mjs +0 -3
- package/src/EventConfigBuilder.res +1 -1
- package/src/EventProcessing.res +4 -4
- package/src/EventProcessing.res.mjs +5 -5
- package/src/FetchState.res +489 -153
- package/src/FetchState.res.mjs +399 -129
- package/src/FinalizeBackfill.res +61 -0
- package/src/FinalizeBackfill.res.mjs +44 -0
- package/src/HandlerRegister.res +8 -8
- package/src/HandlerRegister.res.mjs +5 -4
- package/src/HandlerRegister.resi +1 -1
- package/src/InMemoryStore.res +4 -0
- package/src/InMemoryStore.res.mjs +1 -1
- package/src/InMemoryTable.res +15 -15
- package/src/InMemoryTable.res.mjs +14 -14
- package/src/IndexerLoop.res +9 -2
- package/src/IndexerLoop.res.mjs +8 -1
- package/src/IndexerState.res +52 -23
- package/src/IndexerState.res.mjs +52 -14
- package/src/IndexerState.resi +11 -6
- package/src/Internal.res +19 -17
- package/src/Internal.res.mjs +18 -9
- package/src/LoadLayer.res +10 -2
- package/src/LoadLayer.res.mjs +3 -1
- package/src/LogSelection.res +2 -2
- package/src/Main.res +7 -11
- package/src/Main.res.mjs +4 -6
- package/src/Metrics.res +6 -6
- package/src/Metrics.res.mjs +4 -3
- package/src/Persistence.res +23 -4
- package/src/PgStorage.res +447 -100
- package/src/PgStorage.res.mjs +285 -99
- package/src/RawEvent.res +1 -2
- package/src/RawEvent.res.mjs +1 -1
- package/src/Rollback.res +18 -20
- package/src/Rollback.res.mjs +9 -9
- package/src/RollbackCommit.res +2 -2
- package/src/RollbackCommit.res.mjs +3 -3
- package/src/SafeCheckpointTracking.res +2 -2
- package/src/SimulateDeadInputTracker.res +17 -21
- package/src/SimulateDeadInputTracker.res.mjs +9 -8
- package/src/SimulateItems.res +8 -10
- package/src/SimulateItems.res.mjs +9 -10
- package/src/Sink.res +2 -2
- package/src/Sink.res.mjs +3 -2
- package/src/TestIndexer.res +14 -14
- package/src/TestIndexer.res.mjs +16 -10
- package/src/UserContext.res +2 -2
- package/src/UserContext.res.mjs +2 -1
- package/src/Utils.res +0 -4
- package/src/Utils.res.mjs +1 -6
- package/src/bindings/ClickHouse.res +20 -2
- package/src/bindings/ClickHouse.res.mjs +25 -14
- package/src/db/EntityFilter.res +0 -4
- package/src/db/EntityFilter.res.mjs +1 -8
- package/src/db/EntityHistory.res +1 -1
- package/src/db/IndexCatalog.res +248 -0
- package/src/db/IndexCatalog.res.mjs +222 -0
- package/src/db/IndexDefinition.res +131 -0
- package/src/db/IndexDefinition.res.mjs +138 -0
- package/src/db/IndexManager.res +145 -0
- package/src/db/IndexManager.res.mjs +130 -0
- package/src/db/InternalTable.res +54 -23
- package/src/db/InternalTable.res.mjs +38 -23
- package/src/db/Table.res +24 -14
- package/src/db/Table.res.mjs +21 -16
- package/src/sources/AddressSet.res +5 -4
- package/src/sources/AddressStore.res +25 -14
- package/src/sources/AddressStore.res.mjs +10 -12
- package/src/sources/Evm.res +2 -2
- package/src/sources/Evm.res.mjs +1 -1
- package/src/sources/EvmChain.res +3 -3
- package/src/sources/EvmChain.res.mjs +3 -3
- package/src/sources/EvmHyperSyncSource.res +7 -7
- package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
- package/src/sources/Fuel.res +2 -2
- package/src/sources/Fuel.res.mjs +1 -1
- package/src/sources/FuelHyperSyncClient.res +3 -0
- package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
- package/src/sources/FuelHyperSyncSource.res +5 -7
- package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +5 -0
- package/src/sources/HyperSyncClient.res.mjs +1 -0
- package/src/sources/RpcSource.res +13 -14
- package/src/sources/RpcSource.res.mjs +13 -12
- package/src/sources/SimulateSource.res +42 -15
- package/src/sources/SimulateSource.res.mjs +27 -9
- package/src/sources/Source.res +1 -5
- package/src/sources/SourceManager.res +12 -12
- package/src/sources/SourceManager.res.mjs +11 -10
- package/src/sources/SourceManager.resi +2 -2
- package/src/sources/Svm.res +4 -5
- package/src/sources/Svm.res.mjs +5 -4
- package/src/sources/SvmHyperSyncClient.res +3 -0
- package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
- package/src/sources/SvmHyperSyncSource.res +4 -4
- package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +2 -1
- package/src/tui/components/CustomHooks.res +5 -4
- package/src/tui/components/CustomHooks.res.mjs +4 -2
package/src/Config.res
CHANGED
|
@@ -30,12 +30,16 @@ type sourceConfig =
|
|
|
30
30
|
| EvmSourceConfig({hypersync: option<string>, rpcs: array<evmRpcConfig>})
|
|
31
31
|
| FuelSourceConfig({hypersync: string})
|
|
32
32
|
| SvmSourceConfig({hypersync: option<string>, rpc: option<string>})
|
|
33
|
+
// A `simulate` run: the items the test fed in, parsed against the chain's
|
|
34
|
+
// registrations. The source itself is built with the chain's address store,
|
|
35
|
+
// like every other source, so it can apply the same gates.
|
|
36
|
+
| SimulateSourceConfig({items: array<Internal.item>, endBlock: int})
|
|
33
37
|
// For tests: pass custom sources directly
|
|
34
38
|
| CustomSources(array<Source.t>)
|
|
35
39
|
|
|
36
40
|
type chain = {
|
|
37
41
|
name: string,
|
|
38
|
-
id:
|
|
42
|
+
id: ChainId.t,
|
|
39
43
|
startBlock: int,
|
|
40
44
|
endBlock?: int,
|
|
41
45
|
maxReorgDepth: int,
|
|
@@ -73,6 +77,10 @@ type t = {
|
|
|
73
77
|
shouldRollbackOnReorg: bool,
|
|
74
78
|
shouldSaveFullHistory: bool,
|
|
75
79
|
storage: storage,
|
|
80
|
+
// Widest scalar the internal chain-id columns need, resolved by the CLI from
|
|
81
|
+
// the maximum active chain id. Older configs predate the field, and every id
|
|
82
|
+
// they can express fits an INTEGER.
|
|
83
|
+
chainIdMode: ChainId.mode,
|
|
76
84
|
chainMap: ChainMap.t<chain>,
|
|
77
85
|
defaultChain: option<chain>,
|
|
78
86
|
ecosystem: Ecosystem.t,
|
|
@@ -98,13 +106,13 @@ module EnvioAddresses = {
|
|
|
98
106
|
let name = "envio_addresses"
|
|
99
107
|
let index = -1
|
|
100
108
|
|
|
101
|
-
let makeId = (~chainId, ~address) => {
|
|
102
|
-
chainId->
|
|
109
|
+
let makeId = (~chainId: ChainId.t, ~address) => {
|
|
110
|
+
chainId->ChainId.toString ++ "-" ++ address->Address.toString
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
type t = {
|
|
106
114
|
id: string,
|
|
107
|
-
@as("chain_id") chainId:
|
|
115
|
+
@as("chain_id") chainId: ChainId.t,
|
|
108
116
|
@as("registration_block") registrationBlock: int,
|
|
109
117
|
// -1 when the address was registered from a block handler (no log index)
|
|
110
118
|
@as("registration_log_index") registrationLogIndex: int,
|
|
@@ -123,7 +131,7 @@ module EnvioAddresses = {
|
|
|
123
131
|
|
|
124
132
|
let schema = S.schema(s => {
|
|
125
133
|
id: s.matches(S.string),
|
|
126
|
-
chainId: s.matches(
|
|
134
|
+
chainId: s.matches(ChainId.schema),
|
|
127
135
|
registrationBlock: s.matches(S.int),
|
|
128
136
|
registrationLogIndex: s.matches(S.int),
|
|
129
137
|
contractName: s.matches(S.string),
|
|
@@ -133,7 +141,7 @@ module EnvioAddresses = {
|
|
|
133
141
|
name,
|
|
134
142
|
~fields=[
|
|
135
143
|
Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string),
|
|
136
|
-
Table.mkField("chain_id",
|
|
144
|
+
Table.mkField("chain_id", ChainId, ~fieldSchema=ChainId.schema),
|
|
137
145
|
Table.mkField("registration_block", Int32, ~fieldSchema=S.int),
|
|
138
146
|
// -1 sentinel when registered from a block handler (no log index)
|
|
139
147
|
Table.mkField("registration_log_index", Int32, ~fieldSchema=S.int),
|
|
@@ -185,7 +193,7 @@ let chainContractSchema = S.schema(s =>
|
|
|
185
193
|
|
|
186
194
|
let publicConfigChainSchema = S.schema(s =>
|
|
187
195
|
{
|
|
188
|
-
"id": s.matches(
|
|
196
|
+
"id": s.matches(ChainId.schema),
|
|
189
197
|
"startBlock": s.matches(S.int),
|
|
190
198
|
"endBlock": s.matches(S.option(S.int)),
|
|
191
199
|
"maxReorgDepth": s.matches(S.option(S.int)),
|
|
@@ -455,7 +463,7 @@ let parseEntitiesFromJson = (
|
|
|
455
463
|
)
|
|
456
464
|
)
|
|
457
465
|
|
|
458
|
-
let
|
|
466
|
+
let compositeIndexes =
|
|
459
467
|
entityJson["compositeIndices"]
|
|
460
468
|
->Option.getOr([])
|
|
461
469
|
->Array.map(ci =>
|
|
@@ -470,7 +478,7 @@ let parseEntitiesFromJson = (
|
|
|
470
478
|
let table = Table.mkTable(
|
|
471
479
|
entityName,
|
|
472
480
|
~fields=Array.concat(fields, derivedFields),
|
|
473
|
-
~
|
|
481
|
+
~compositeIndexes,
|
|
474
482
|
~description=?entityJson["description"],
|
|
475
483
|
)
|
|
476
484
|
|
|
@@ -548,6 +556,7 @@ let publicConfigSchema = S.schema(s =>
|
|
|
548
556
|
"rollbackOnReorg": s.matches(S.option(S.bool)),
|
|
549
557
|
"saveFullHistory": s.matches(S.option(S.bool)),
|
|
550
558
|
"rawEvents": s.matches(S.option(S.bool)),
|
|
559
|
+
"chainIdMode": s.matches(S.option(ChainId.modeSchema)),
|
|
551
560
|
"storage": s.matches(publicConfigStorageSchema),
|
|
552
561
|
"evm": s.matches(S.option(publicConfigEvmSchema)),
|
|
553
562
|
"fuel": s.matches(S.option(publicConfigEcosystemSchema)),
|
|
@@ -690,7 +699,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
690
699
|
~contractName,
|
|
691
700
|
~events: option<array<_>>,
|
|
692
701
|
~abi,
|
|
693
|
-
~chainId:
|
|
702
|
+
~chainId: ChainId.t,
|
|
694
703
|
~addresses: array<string>,
|
|
695
704
|
~svmDefinedTypes: JSON.t=JSON.Null,
|
|
696
705
|
) => {
|
|
@@ -724,11 +733,11 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
724
733
|
| [pid] => pid->SvmTypes.Pubkey.fromStringUnsafe
|
|
725
734
|
| [] =>
|
|
726
735
|
JsError.throwWithMessage(
|
|
727
|
-
`SVM program ${contractName} on chain ${chainId->
|
|
736
|
+
`SVM program ${contractName} on chain ${chainId->ChainId.toString} is missing a program_id`,
|
|
728
737
|
)
|
|
729
738
|
| _ =>
|
|
730
739
|
JsError.throwWithMessage(
|
|
731
|
-
`SVM program ${contractName} on chain ${chainId->
|
|
740
|
+
`SVM program ${contractName} on chain ${chainId->ChainId.toString} has multiple addresses; a program is uniquely identified by a single program_id`,
|
|
732
741
|
)
|
|
733
742
|
}
|
|
734
743
|
let widenedEventItem =
|
|
@@ -878,8 +887,8 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
878
887
|
| Some(existingContractName) =>
|
|
879
888
|
JsError.throwWithMessage(
|
|
880
889
|
existingContractName === contract.name
|
|
881
|
-
? `Address ${addressString} is listed multiple times for the contract ${contract.name} on chain ${chainId->
|
|
882
|
-
: `Address ${addressString} on chain ${chainId->
|
|
890
|
+
? `Address ${addressString} is listed multiple times for the contract ${contract.name} on chain ${chainId->ChainId.toString}. Please remove the duplicate from your config.`
|
|
891
|
+
: `Address ${addressString} on chain ${chainId->ChainId.toString} is configured for multiple contracts: ${existingContractName} and ${contract.name}. Indexing the same address with multiple contract definitions is not supported. Please define the events on a single contract definition instead.`,
|
|
883
892
|
)
|
|
884
893
|
| None => contractNameByAddress->Dict.set(addressString, contract.name)
|
|
885
894
|
}
|
|
@@ -969,7 +978,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
969
978
|
let chainMap =
|
|
970
979
|
chains
|
|
971
980
|
->Array.map(chain => {
|
|
972
|
-
(
|
|
981
|
+
(chain.id, chain)
|
|
973
982
|
})
|
|
974
983
|
->ChainMap.fromArrayUnsafe
|
|
975
984
|
|
|
@@ -1027,6 +1036,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
1027
1036
|
shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
|
|
1028
1037
|
shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
|
|
1029
1038
|
storage: globalStorage,
|
|
1039
|
+
chainIdMode: publicConfig["chainIdMode"]->Option.getOr(Int32),
|
|
1030
1040
|
chainMap,
|
|
1031
1041
|
defaultChain: chains->Array.get(0),
|
|
1032
1042
|
enableRawEvents: publicConfig["rawEvents"]->Option.getOr(false),
|
|
@@ -1092,15 +1102,14 @@ let normalizeSimulateAddress = (config: t, address: Address.t): Address.t =>
|
|
|
1092
1102
|
// returns that chain's per-chain event config (matters for where-callback
|
|
1093
1103
|
// probe detection, which runs with the chain's real id). Without `chainId`,
|
|
1094
1104
|
// falls back to the first chain that declares this event.
|
|
1095
|
-
let getEventConfig = (config: t, ~contractName, ~eventName, ~chainId: option<
|
|
1105
|
+
let getEventConfig = (config: t, ~contractName, ~eventName, ~chainId: option<ChainId.t>=?) => {
|
|
1096
1106
|
let chains = switch chainId {
|
|
1097
1107
|
| Some(chainId) =>
|
|
1098
|
-
|
|
1099
|
-
switch config.chainMap->ChainMap.get(chain) {
|
|
1108
|
+
switch config.chainMap->ChainMap.get(chainId) {
|
|
1100
1109
|
| chainConfig => [chainConfig]
|
|
1101
1110
|
| exception _ =>
|
|
1102
1111
|
JsError.throwWithMessage(
|
|
1103
|
-
`Chain ${chainId->
|
|
1112
|
+
`Chain ${chainId->ChainId.toString} is not configured. Add it to config.yaml or pass a configured chain.`,
|
|
1104
1113
|
)
|
|
1105
1114
|
}
|
|
1106
1115
|
| None => config.chainMap->ChainMap.values
|
|
@@ -1122,14 +1131,12 @@ let shouldSaveHistory = (config, ~isInReorgThreshold) =>
|
|
|
1122
1131
|
let shouldPruneHistory = (config, ~isInReorgThreshold) =>
|
|
1123
1132
|
!config.shouldSaveFullHistory && (config.shouldRollbackOnReorg && isInReorgThreshold)
|
|
1124
1133
|
|
|
1125
|
-
let getChain = (config, ~chainId) =>
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
? chain
|
|
1134
|
+
let getChain = (config, ~chainId) =>
|
|
1135
|
+
config.chainMap->ChainMap.has(chainId)
|
|
1136
|
+
? chainId
|
|
1129
1137
|
: JsError.throwWithMessage(
|
|
1130
|
-
"No chain with id " ++
|
|
1138
|
+
"No chain with id " ++ chainId->ChainId.toString ++ " found in config.yaml",
|
|
1131
1139
|
)
|
|
1132
|
-
}
|
|
1133
1140
|
|
|
1134
1141
|
// A CLI command payload already contains the resolved JSON; priming lets
|
|
1135
1142
|
// downstream callers skip the NAPI `getConfigJson` round-trip. Calling
|
|
@@ -1267,7 +1274,17 @@ let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
|
|
|
1267
1274
|
|
|
1268
1275
|
switch (stored, current) {
|
|
1269
1276
|
| (Object(sObj), Object(cObj)) =>
|
|
1270
|
-
|
|
1277
|
+
// chainIdMode sits right after version: it decides the physical type of
|
|
1278
|
+
// every chain-id column, so a change to it is reported on its own rather
|
|
1279
|
+
// than buried under the chain diffs that always accompany it.
|
|
1280
|
+
let tiers = [
|
|
1281
|
+
["version"],
|
|
1282
|
+
["chainIdMode"],
|
|
1283
|
+
["name"],
|
|
1284
|
+
["storage"],
|
|
1285
|
+
["evm", "fuel", "svm"],
|
|
1286
|
+
["entities"],
|
|
1287
|
+
]
|
|
1271
1288
|
let firstHit = tiers->Array.reduce(None, (acc, tier) =>
|
|
1272
1289
|
switch acc {
|
|
1273
1290
|
| Some(_) => acc
|
package/src/Config.res.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import * as Fuel from "./sources/Fuel.res.mjs";
|
|
|
8
8
|
import * as Table from "./db/Table.res.mjs";
|
|
9
9
|
import * as Utils from "./Utils.res.mjs";
|
|
10
10
|
import * as Address from "./Address.res.mjs";
|
|
11
|
+
import * as ChainId from "./ChainId.res.mjs";
|
|
11
12
|
import * as Logging from "./Logging.res.mjs";
|
|
12
13
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
13
14
|
import * as Internal from "./Internal.res.mjs";
|
|
@@ -24,7 +25,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
|
|
|
24
25
|
let name = "envio_addresses";
|
|
25
26
|
|
|
26
27
|
function makeId(chainId, address) {
|
|
27
|
-
return
|
|
28
|
+
return ChainId.toString(chainId) + "-" + address;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function getAddress(entity) {
|
|
@@ -34,7 +35,7 @@ function getAddress(entity) {
|
|
|
34
35
|
|
|
35
36
|
let schema = S$RescriptSchema.schema(s => ({
|
|
36
37
|
id: s.m(S$RescriptSchema.string),
|
|
37
|
-
chain_id: s.m(
|
|
38
|
+
chain_id: s.m(ChainId.schema),
|
|
38
39
|
registration_block: s.m(S$RescriptSchema.int),
|
|
39
40
|
registration_log_index: s.m(S$RescriptSchema.int),
|
|
40
41
|
contract_name: s.m(S$RescriptSchema.string)
|
|
@@ -42,7 +43,7 @@ let schema = S$RescriptSchema.schema(s => ({
|
|
|
42
43
|
|
|
43
44
|
let table = Table.mkTable(name, undefined, [
|
|
44
45
|
Table.mkField("id", "String", S$RescriptSchema.string, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined),
|
|
45
|
-
Table.mkField("chain_id", "
|
|
46
|
+
Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
46
47
|
Table.mkField("registration_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
47
48
|
Table.mkField("registration_log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
|
|
48
49
|
Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
@@ -98,7 +99,7 @@ let chainContractSchema = S$RescriptSchema.schema(s => ({
|
|
|
98
99
|
}));
|
|
99
100
|
|
|
100
101
|
let publicConfigChainSchema = S$RescriptSchema.schema(s => ({
|
|
101
|
-
id: s.m(
|
|
102
|
+
id: s.m(ChainId.schema),
|
|
102
103
|
startBlock: s.m(S$RescriptSchema.int),
|
|
103
104
|
endBlock: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
|
|
104
105
|
maxReorgDepth: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
|
|
@@ -331,11 +332,11 @@ function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage) {
|
|
|
331
332
|
return Table.mkField(prop.name, match[0], match[1], undefined, match[3], match[2], prop.name === "id", match[4], prop.linkedEntity, prop.description, prop.postgresDbName, prop.clickhouseDbName);
|
|
332
333
|
});
|
|
333
334
|
let derivedFields = Stdlib_Option.getOr(entityJson.derivedFields, []).map(df => Table.mkDerivedFromField(df.fieldName, df.derivedFromEntity, df.derivedFromField, df.description));
|
|
334
|
-
let
|
|
335
|
+
let compositeIndexes = Stdlib_Option.getOr(entityJson.compositeIndices, []).map(ci => ci.map(f => ({
|
|
335
336
|
fieldName: f.fieldName,
|
|
336
337
|
direction: f.direction === "Asc" ? "Asc" : "Desc"
|
|
337
338
|
})));
|
|
338
|
-
let table = Table.mkTable(entityName,
|
|
339
|
+
let table = Table.mkTable(entityName, compositeIndexes, fields.concat(derivedFields), entityJson.description);
|
|
339
340
|
let getApiFieldName = prop => {
|
|
340
341
|
let match = prop.linkedEntity;
|
|
341
342
|
if (match !== undefined) {
|
|
@@ -400,6 +401,7 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
|
|
|
400
401
|
rollbackOnReorg: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
|
|
401
402
|
saveFullHistory: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
|
|
402
403
|
rawEvents: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
|
|
404
|
+
chainIdMode: s.m(S$RescriptSchema.option(ChainId.modeSchema)),
|
|
403
405
|
storage: s.m(publicConfigStorageSchema),
|
|
404
406
|
evm: s.m(S$RescriptSchema.option(publicConfigEvmSchema)),
|
|
405
407
|
fuel: s.m(S$RescriptSchema.option(publicConfigEcosystemSchema)),
|
|
@@ -528,7 +530,7 @@ function fromPublic(publicConfigJson) {
|
|
|
528
530
|
case "svm" :
|
|
529
531
|
let len = addresses.length;
|
|
530
532
|
let programId = len !== 1 ? (
|
|
531
|
-
len !== 0 ? Stdlib_JsError.throwWithMessage(`SVM program ` + contractName + ` on chain ` +
|
|
533
|
+
len !== 0 ? Stdlib_JsError.throwWithMessage(`SVM program ` + contractName + ` on chain ` + ChainId.toString(chainId) + ` has multiple addresses; a program is uniquely identified by a single program_id`) : Stdlib_JsError.throwWithMessage(`SVM program ` + contractName + ` on chain ` + ChainId.toString(chainId) + ` is missing a program_id`)
|
|
532
534
|
) : addresses[0];
|
|
533
535
|
let s = eventItem.svm;
|
|
534
536
|
let svm = s !== undefined ? Primitive_option.valFromOption(s) : Stdlib_JsError.throwWithMessage(`SVM instruction ` + contractName + `.` + eventName + ` is missing the "svm" descriptor in internal config`);
|
|
@@ -591,7 +593,7 @@ function fromPublic(publicConfigJson) {
|
|
|
591
593
|
contract.addresses.forEach(address => {
|
|
592
594
|
let existingContractName = contractNameByAddress[address];
|
|
593
595
|
if (existingContractName !== undefined) {
|
|
594
|
-
return Stdlib_JsError.throwWithMessage(existingContractName === contract.name ? `Address ` + address + ` is listed multiple times for the contract ` + contract.name + ` on chain ` +
|
|
596
|
+
return Stdlib_JsError.throwWithMessage(existingContractName === contract.name ? `Address ` + address + ` is listed multiple times for the contract ` + contract.name + ` on chain ` + ChainId.toString(chainId) + `. Please remove the duplicate from your config.` : `Address ` + address + ` on chain ` + ChainId.toString(chainId) + ` is configured for multiple contracts: ` + existingContractName + ` and ` + contract.name + `. Indexing the same address with multiple contract definitions is not supported. Please define the events on a single contract definition instead.`);
|
|
595
597
|
} else {
|
|
596
598
|
contractNameByAddress[address] = contract.name;
|
|
597
599
|
return;
|
|
@@ -677,7 +679,7 @@ function fromPublic(publicConfigJson) {
|
|
|
677
679
|
};
|
|
678
680
|
});
|
|
679
681
|
let chainMap = ChainMap.fromArrayUnsafe(chains.map(chain => [
|
|
680
|
-
|
|
682
|
+
chain.id,
|
|
681
683
|
chain
|
|
682
684
|
]));
|
|
683
685
|
let allEnums = parseEnumsFromJson(Stdlib_Option.getOr(publicConfig.enums, {}));
|
|
@@ -709,6 +711,7 @@ function fromPublic(publicConfigJson) {
|
|
|
709
711
|
shouldRollbackOnReorg: Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true),
|
|
710
712
|
shouldSaveFullHistory: Stdlib_Option.getOr(publicConfig.saveFullHistory, false),
|
|
711
713
|
storage: globalStorage,
|
|
714
|
+
chainIdMode: Stdlib_Option.getOr(publicConfig.chainIdMode, "int32"),
|
|
712
715
|
chainMap: chainMap,
|
|
713
716
|
defaultChain: chains[0],
|
|
714
717
|
ecosystem: ecosystem,
|
|
@@ -765,14 +768,14 @@ function normalizeSimulateAddress(config, address) {
|
|
|
765
768
|
function getEventConfig(config, contractName, eventName, chainId) {
|
|
766
769
|
let chains;
|
|
767
770
|
if (chainId !== undefined) {
|
|
768
|
-
let
|
|
771
|
+
let chainId$1 = Primitive_option.valFromOption(chainId);
|
|
769
772
|
let exit = 0;
|
|
770
773
|
let chainConfig;
|
|
771
774
|
try {
|
|
772
|
-
chainConfig = ChainMap.get(config.chainMap,
|
|
775
|
+
chainConfig = ChainMap.get(config.chainMap, chainId$1);
|
|
773
776
|
exit = 1;
|
|
774
777
|
} catch (exn) {
|
|
775
|
-
chains = Stdlib_JsError.throwWithMessage(`Chain ` +
|
|
778
|
+
chains = Stdlib_JsError.throwWithMessage(`Chain ` + ChainId.toString(chainId$1) + ` is not configured. Add it to config.yaml or pass a configured chain.`);
|
|
776
779
|
}
|
|
777
780
|
if (exit === 1) {
|
|
778
781
|
chains = [chainConfig];
|
|
@@ -808,11 +811,10 @@ function shouldPruneHistory(config, isInReorgThreshold) {
|
|
|
808
811
|
}
|
|
809
812
|
|
|
810
813
|
function getChain(config, chainId) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
return chain;
|
|
814
|
+
if (ChainMap.has(config.chainMap, chainId)) {
|
|
815
|
+
return chainId;
|
|
814
816
|
} else {
|
|
815
|
-
return Stdlib_JsError.throwWithMessage("No chain with id " +
|
|
817
|
+
return Stdlib_JsError.throwWithMessage("No chain with id " + ChainId.toString(chainId) + " found in config.yaml");
|
|
816
818
|
}
|
|
817
819
|
}
|
|
818
820
|
|
|
@@ -983,6 +985,7 @@ function diffPaths(stored, current) {
|
|
|
983
985
|
if (typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof current === "object" && current !== null && !Array.isArray(current)) {
|
|
984
986
|
let tiers = [
|
|
985
987
|
["version"],
|
|
988
|
+
["chainIdMode"],
|
|
986
989
|
["name"],
|
|
987
990
|
["storage"],
|
|
988
991
|
[
|
|
@@ -36,7 +36,7 @@ let contractRegisterChainTraps: Utils.Proxy.traps<contractRegisterParams> = {
|
|
|
36
36
|
switch prop {
|
|
37
37
|
| "id" =>
|
|
38
38
|
let eventItem = params.item->Internal.castUnsafeEventItem
|
|
39
|
-
eventItem.
|
|
39
|
+
eventItem.chainId->(Utils.magic: ChainId.t => unknown)
|
|
40
40
|
| _ =>
|
|
41
41
|
// Look up the contract name directly in config contracts across all chains.
|
|
42
42
|
let contractName = prop
|
|
@@ -19,7 +19,7 @@ function makeAddFunction(params, contractName) {
|
|
|
19
19
|
|
|
20
20
|
let contractRegisterChainTraps_get = (params, prop) => {
|
|
21
21
|
if (prop === "id") {
|
|
22
|
-
return params.item.
|
|
22
|
+
return params.item.chainId;
|
|
23
23
|
}
|
|
24
24
|
let isValidContract = ChainMap.values(params.config.chainMap).some(chain => chain.contracts.some(c => c.name === prop));
|
|
25
25
|
if (isValidContract) {
|
package/src/Core.res
CHANGED
package/src/CrossChainState.res
CHANGED
|
@@ -7,9 +7,13 @@ type t = {
|
|
|
7
7
|
chainStates: dict<ChainState.t>,
|
|
8
8
|
// Chain ids in a stable order, so the cross-chain loops iterate the chains
|
|
9
9
|
// without allocating a values array on every tick.
|
|
10
|
-
chainIds: array<
|
|
10
|
+
chainIds: array<ChainId.t>,
|
|
11
11
|
// True once every chain has caught up to head/endBlock. Monotonic during a run.
|
|
12
12
|
mutable isRealtime: bool,
|
|
13
|
+
// True once every chain has caught up and there's nothing left to process,
|
|
14
|
+
// but before the deferred schema indexes and `ready_at` are committed. The
|
|
15
|
+
// gap between this and `isRealtime` is the FinalizingIndexes phase.
|
|
16
|
+
mutable isCaughtUp: bool,
|
|
13
17
|
mutable isInReorgThreshold: bool,
|
|
14
18
|
// Indexer-wide fetch buffer pool (item count), shared across all chains.
|
|
15
19
|
targetBufferSize: int,
|
|
@@ -32,6 +36,7 @@ let make = (
|
|
|
32
36
|
chainStates,
|
|
33
37
|
chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
|
|
34
38
|
isRealtime,
|
|
39
|
+
isCaughtUp: isRealtime,
|
|
35
40
|
isInReorgThreshold,
|
|
36
41
|
targetBufferSize,
|
|
37
42
|
}
|
|
@@ -40,12 +45,13 @@ let make = (
|
|
|
40
45
|
// Resolve a chain's state by id. The id always comes from `chainIds`, which is
|
|
41
46
|
// derived from `chainStates`, so the entry is guaranteed present.
|
|
42
47
|
let getChainState = (crossChainState: t, chainId) =>
|
|
43
|
-
crossChainState.chainStates->
|
|
48
|
+
crossChainState.chainStates->ChainId.Dict.dangerouslyGetNonOption(chainId)->Option.getUnsafe
|
|
44
49
|
|
|
45
50
|
// --- Accessors. ---
|
|
46
51
|
|
|
47
52
|
let chainStates = (crossChainState: t) => crossChainState.chainStates
|
|
48
53
|
let isRealtime = (crossChainState: t) => crossChainState.isRealtime
|
|
54
|
+
let isCaughtUp = (crossChainState: t) => crossChainState.isCaughtUp
|
|
49
55
|
let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
|
|
50
56
|
let targetBufferSize = (crossChainState: t) => crossChainState.targetBufferSize
|
|
51
57
|
|
|
@@ -129,10 +135,11 @@ let enterReorgThreshold = (crossChainState: t) => {
|
|
|
129
135
|
crossChainState.isInReorgThreshold = true
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
// Commit each progressed chain's batch progress, then
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
138
|
+
// Commit each progressed chain's batch progress, then record whether the whole
|
|
139
|
+
// indexer has caught up (every chain reached endblock or processed to head,
|
|
140
|
+
// with no processable events left). Catching up doesn't make the indexer ready:
|
|
141
|
+
// the deferred schema indexes still have to be created, which `markReady`
|
|
142
|
+
// below concludes once they're committed.
|
|
136
143
|
let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampName: string) => {
|
|
137
144
|
let chainIds = crossChainState.chainIds
|
|
138
145
|
|
|
@@ -145,15 +152,14 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
|
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
crossChainState.isCaughtUp =
|
|
156
|
+
crossChainState.isCaughtUp || (crossChainState->nextItemIsNone && everyChainCaughtUp.contents)
|
|
149
157
|
|
|
158
|
+
// A run resumed with every chain already stamped ready needs no finalize —
|
|
159
|
+
// the indexes were committed together with those stamps.
|
|
150
160
|
let allChainsReady = ref(true)
|
|
151
161
|
for i in 0 to chainIds->Array.length - 1 {
|
|
152
|
-
|
|
153
|
-
if indexerCaughtUp {
|
|
154
|
-
cs->ChainState.markReady
|
|
155
|
-
}
|
|
156
|
-
if !(cs->ChainState.isReady) {
|
|
162
|
+
if !(crossChainState->getChainState(chainIds->Array.getUnsafe(i))->ChainState.isReady) {
|
|
157
163
|
allChainsReady := false
|
|
158
164
|
}
|
|
159
165
|
}
|
|
@@ -161,6 +167,70 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
|
|
|
161
167
|
crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
// Every chain has buffered up to its head (or endblock) with nothing
|
|
171
|
+
// processable left. Derived from current state rather than from the flag a
|
|
172
|
+
// progressed batch sets, because a run that reached the head and died before
|
|
173
|
+
// finalizing resumes with no batch to process — nothing would ever set it.
|
|
174
|
+
let isSettledAtHead = (crossChainState: t) => {
|
|
175
|
+
let settled = ref(crossChainState->nextItemIsNone)
|
|
176
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
177
|
+
if (
|
|
178
|
+
!(
|
|
179
|
+
crossChainState
|
|
180
|
+
->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
181
|
+
->ChainState.isFetchingAtHead
|
|
182
|
+
)
|
|
183
|
+
) {
|
|
184
|
+
settled := false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
settled.contents
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Enter the FinalizingIndexes phase without a batch, for the resume above.
|
|
191
|
+
let markCaughtUpIfSettled = (crossChainState: t) =>
|
|
192
|
+
if crossChainState->isSettledAtHead {
|
|
193
|
+
crossChainState.isCaughtUp = true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The same resume, decided from persisted values at construction instead of from
|
|
197
|
+
// the fetch frontier. A run that reached the head and died before finalizing has
|
|
198
|
+
// no batch to process on resume, and by the time the first height lands the head
|
|
199
|
+
// may have moved on — at which point no chain looks at head any more and the
|
|
200
|
+
// indexes it still owes would wait out another whole backfill. Deciding here,
|
|
201
|
+
// before any source request, keeps that debt tied to the progress that was
|
|
202
|
+
// actually committed. Skipped once every chain carries `ready_at`: those indexes
|
|
203
|
+
// were committed together with the stamps.
|
|
204
|
+
let markCaughtUpOnResume = (crossChainState: t) => {
|
|
205
|
+
let everyChainCaughtUp = ref(crossChainState.chainIds->Array.length > 0)
|
|
206
|
+
let everyChainReady = ref(true)
|
|
207
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
208
|
+
let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
209
|
+
if !(cs->ChainState.isDurablyCaughtUp) {
|
|
210
|
+
everyChainCaughtUp := false
|
|
211
|
+
}
|
|
212
|
+
if !(cs->ChainState.isReady) {
|
|
213
|
+
everyChainReady := false
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if everyChainCaughtUp.contents && !everyChainReady.contents && crossChainState->nextItemIsNone {
|
|
218
|
+
crossChainState.isCaughtUp = true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Concludes the FinalizingIndexes phase: stamps every chain with the `ready_at`
|
|
223
|
+
// already committed alongside the deferred schema indexes and switches the
|
|
224
|
+
// indexer to realtime.
|
|
225
|
+
let markReady = (crossChainState: t, ~readyAt) => {
|
|
226
|
+
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
227
|
+
crossChainState
|
|
228
|
+
->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
|
|
229
|
+
->ChainState.markReady(~readyAt)
|
|
230
|
+
}
|
|
231
|
+
crossChainState.isRealtime = true
|
|
232
|
+
}
|
|
233
|
+
|
|
164
234
|
// --- Fetch control. ---
|
|
165
235
|
|
|
166
236
|
// Chains ordered furthest-behind first by fetch-frontier progress, so the
|
|
@@ -226,7 +296,7 @@ let idleOrWaitAction = (cs: ChainState.t) =>
|
|
|
226
296
|
// dropped — chains at head only trail each other by real-time block production.
|
|
227
297
|
let checkAndFetch = async (
|
|
228
298
|
crossChainState: t,
|
|
229
|
-
~dispatchChain: (~
|
|
299
|
+
~dispatchChain: (~chainId: ChainId.t, ~action: FetchState.nextQuery) => promise<unit>,
|
|
230
300
|
) => {
|
|
231
301
|
let targetBudget = crossChainState.targetBufferSize->Int.toFloat
|
|
232
302
|
let remaining = ref(
|
|
@@ -274,14 +344,14 @@ let checkAndFetch = async (
|
|
|
274
344
|
// hold the whole pool. (The general can't-fetch-yet rule, including
|
|
275
345
|
// blockLag, lives in FetchState.getNextQuery — this branch only
|
|
276
346
|
// short-circuits the unambiguous no-height case.)
|
|
277
|
-
actionByChain->
|
|
347
|
+
actionByChain->ChainId.Dict.set(chainId, FetchState.WaitingForNewBlock)
|
|
278
348
|
} else if remaining.contents < minimumAdmissionBudget {
|
|
279
349
|
// More than 90% of the target pool is ready or reserved. Don't admit new
|
|
280
350
|
// queries until a full admission unit becomes free. No wake-up poll is
|
|
281
351
|
// needed: a saturated pool means some chain holds ready items or
|
|
282
352
|
// in-flight reservations, so a batch completion or landing response is
|
|
283
353
|
// guaranteed to schedule another tick that revisits this chain.
|
|
284
|
-
actionByChain->
|
|
354
|
+
actionByChain->ChainId.Dict.set(chainId, FetchState.NothingToQuery)
|
|
285
355
|
} else {
|
|
286
356
|
let isCold = cs->ChainState.effectiveDensity === None
|
|
287
357
|
let chainTargetItems =
|
|
@@ -296,12 +366,12 @@ let checkAndFetch = async (
|
|
|
296
366
|
| _ => None
|
|
297
367
|
}
|
|
298
368
|
switch cs->ChainState.getNextQuery(~chainTargetItems, ~maxTargetBlock?) {
|
|
299
|
-
| WaitingForNewBlock as action => actionByChain->
|
|
369
|
+
| WaitingForNewBlock as action => actionByChain->ChainId.Dict.set(chainId, action)
|
|
300
370
|
| NothingToQuery =>
|
|
301
371
|
// A chain below its head can emit no query when its budget went to
|
|
302
372
|
// more-behind chains or the cross-chain alignment clamped its range to
|
|
303
373
|
// nothing — idleOrWaitAction keeps it polling for new blocks.
|
|
304
|
-
actionByChain->
|
|
374
|
+
actionByChain->ChainId.Dict.set(chainId, idleOrWaitAction(cs))
|
|
305
375
|
| Ready(queries) => {
|
|
306
376
|
let consumed =
|
|
307
377
|
queries->Array.reduce(0., (acc, query: FetchState.query) =>
|
|
@@ -325,7 +395,7 @@ let checkAndFetch = async (
|
|
|
325
395
|
"partitions": partitions,
|
|
326
396
|
})
|
|
327
397
|
|
|
328
|
-
actionByChain->
|
|
398
|
+
actionByChain->ChainId.Dict.set(chainId, FetchState.Ready(queries))
|
|
329
399
|
// Mark the queries in flight and reserve their size against the
|
|
330
400
|
// shared budget; released as each response lands in
|
|
331
401
|
// handleQueryResult.
|
|
@@ -339,12 +409,10 @@ let checkAndFetch = async (
|
|
|
339
409
|
let promises = []
|
|
340
410
|
for i in 0 to crossChainState.chainIds->Array.length - 1 {
|
|
341
411
|
let chainId = crossChainState.chainIds->Array.getUnsafe(i)
|
|
342
|
-
switch actionByChain->
|
|
412
|
+
switch actionByChain->ChainId.Dict.dangerouslyGetNonOption(chainId) {
|
|
343
413
|
| Some(NothingToQuery)
|
|
344
414
|
| None => ()
|
|
345
|
-
| Some(action) =>
|
|
346
|
-
let chain = ChainMap.Chain.makeUnsafe(~chainId)
|
|
347
|
-
promises->Array.push(dispatchChain(~chain, ~action))
|
|
415
|
+
| Some(action) => promises->Array.push(dispatchChain(~chainId=chainId, ~action))
|
|
348
416
|
}
|
|
349
417
|
}
|
|
350
418
|
let _ = await promises->Promise.all
|