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.
Files changed (129) hide show
  1. package/package.json +6 -6
  2. package/src/Batch.res +8 -8
  3. package/src/Batch.res.mjs +4 -3
  4. package/src/BatchProcessing.res +24 -0
  5. package/src/BatchProcessing.res.mjs +12 -0
  6. package/src/ChainFetching.res +20 -19
  7. package/src/ChainFetching.res.mjs +14 -14
  8. package/src/ChainId.res +58 -0
  9. package/src/ChainId.res.mjs +49 -0
  10. package/src/ChainId.resi +27 -0
  11. package/src/ChainMap.res +12 -26
  12. package/src/ChainMap.res.mjs +5 -41
  13. package/src/ChainMap.resi +5 -20
  14. package/src/ChainMetadata.res +1 -1
  15. package/src/ChainMetadata.res.mjs +3 -2
  16. package/src/ChainState.res +45 -24
  17. package/src/ChainState.res.mjs +40 -16
  18. package/src/ChainState.resi +3 -1
  19. package/src/Config.res +43 -26
  20. package/src/Config.res.mjs +19 -16
  21. package/src/ContractRegisterContext.res +1 -1
  22. package/src/ContractRegisterContext.res.mjs +1 -1
  23. package/src/Core.res +1 -0
  24. package/src/CrossChainState.res +90 -22
  25. package/src/CrossChainState.res.mjs +55 -16
  26. package/src/CrossChainState.resi +5 -1
  27. package/src/Env.res +0 -3
  28. package/src/Env.res.mjs +0 -3
  29. package/src/EventConfigBuilder.res +1 -1
  30. package/src/EventProcessing.res +4 -4
  31. package/src/EventProcessing.res.mjs +5 -5
  32. package/src/FetchState.res +489 -153
  33. package/src/FetchState.res.mjs +399 -129
  34. package/src/FinalizeBackfill.res +61 -0
  35. package/src/FinalizeBackfill.res.mjs +44 -0
  36. package/src/HandlerRegister.res +8 -8
  37. package/src/HandlerRegister.res.mjs +5 -4
  38. package/src/HandlerRegister.resi +1 -1
  39. package/src/InMemoryStore.res +4 -0
  40. package/src/InMemoryStore.res.mjs +1 -1
  41. package/src/InMemoryTable.res +15 -15
  42. package/src/InMemoryTable.res.mjs +14 -14
  43. package/src/IndexerLoop.res +9 -2
  44. package/src/IndexerLoop.res.mjs +8 -1
  45. package/src/IndexerState.res +52 -23
  46. package/src/IndexerState.res.mjs +52 -14
  47. package/src/IndexerState.resi +11 -6
  48. package/src/Internal.res +19 -17
  49. package/src/Internal.res.mjs +18 -9
  50. package/src/LoadLayer.res +10 -2
  51. package/src/LoadLayer.res.mjs +3 -1
  52. package/src/LogSelection.res +2 -2
  53. package/src/Main.res +7 -11
  54. package/src/Main.res.mjs +4 -6
  55. package/src/Metrics.res +6 -6
  56. package/src/Metrics.res.mjs +4 -3
  57. package/src/Persistence.res +23 -4
  58. package/src/PgStorage.res +447 -100
  59. package/src/PgStorage.res.mjs +285 -99
  60. package/src/RawEvent.res +1 -2
  61. package/src/RawEvent.res.mjs +1 -1
  62. package/src/Rollback.res +18 -20
  63. package/src/Rollback.res.mjs +9 -9
  64. package/src/RollbackCommit.res +2 -2
  65. package/src/RollbackCommit.res.mjs +3 -3
  66. package/src/SafeCheckpointTracking.res +2 -2
  67. package/src/SimulateDeadInputTracker.res +17 -21
  68. package/src/SimulateDeadInputTracker.res.mjs +9 -8
  69. package/src/SimulateItems.res +8 -10
  70. package/src/SimulateItems.res.mjs +9 -10
  71. package/src/Sink.res +2 -2
  72. package/src/Sink.res.mjs +3 -2
  73. package/src/TestIndexer.res +14 -14
  74. package/src/TestIndexer.res.mjs +16 -10
  75. package/src/UserContext.res +2 -2
  76. package/src/UserContext.res.mjs +2 -1
  77. package/src/Utils.res +0 -4
  78. package/src/Utils.res.mjs +1 -6
  79. package/src/bindings/ClickHouse.res +20 -2
  80. package/src/bindings/ClickHouse.res.mjs +25 -14
  81. package/src/db/EntityFilter.res +0 -4
  82. package/src/db/EntityFilter.res.mjs +1 -8
  83. package/src/db/EntityHistory.res +1 -1
  84. package/src/db/IndexCatalog.res +248 -0
  85. package/src/db/IndexCatalog.res.mjs +222 -0
  86. package/src/db/IndexDefinition.res +131 -0
  87. package/src/db/IndexDefinition.res.mjs +138 -0
  88. package/src/db/IndexManager.res +145 -0
  89. package/src/db/IndexManager.res.mjs +130 -0
  90. package/src/db/InternalTable.res +54 -23
  91. package/src/db/InternalTable.res.mjs +38 -23
  92. package/src/db/Table.res +24 -14
  93. package/src/db/Table.res.mjs +21 -16
  94. package/src/sources/AddressSet.res +5 -4
  95. package/src/sources/AddressStore.res +25 -14
  96. package/src/sources/AddressStore.res.mjs +10 -12
  97. package/src/sources/Evm.res +2 -2
  98. package/src/sources/Evm.res.mjs +1 -1
  99. package/src/sources/EvmChain.res +3 -3
  100. package/src/sources/EvmChain.res.mjs +3 -3
  101. package/src/sources/EvmHyperSyncSource.res +7 -7
  102. package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
  103. package/src/sources/Fuel.res +2 -2
  104. package/src/sources/Fuel.res.mjs +1 -1
  105. package/src/sources/FuelHyperSyncClient.res +3 -0
  106. package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
  107. package/src/sources/FuelHyperSyncSource.res +5 -7
  108. package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
  109. package/src/sources/HyperSync.resi +2 -2
  110. package/src/sources/HyperSyncClient.res +5 -0
  111. package/src/sources/HyperSyncClient.res.mjs +1 -0
  112. package/src/sources/RpcSource.res +13 -14
  113. package/src/sources/RpcSource.res.mjs +13 -12
  114. package/src/sources/SimulateSource.res +42 -15
  115. package/src/sources/SimulateSource.res.mjs +27 -9
  116. package/src/sources/Source.res +1 -5
  117. package/src/sources/SourceManager.res +12 -12
  118. package/src/sources/SourceManager.res.mjs +11 -10
  119. package/src/sources/SourceManager.resi +2 -2
  120. package/src/sources/Svm.res +4 -5
  121. package/src/sources/Svm.res.mjs +5 -4
  122. package/src/sources/SvmHyperSyncClient.res +3 -0
  123. package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
  124. package/src/sources/SvmHyperSyncSource.res +4 -4
  125. package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
  126. package/src/tui/Tui.res +1 -1
  127. package/src/tui/Tui.res.mjs +2 -1
  128. package/src/tui/components/CustomHooks.res +5 -4
  129. 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: int,
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->Int.toString ++ "-" ++ address->Address.toString
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: int,
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(S.int),
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", Int32, ~fieldSchema=S.int),
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(S.int),
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 compositeIndices =
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
- ~compositeIndices,
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: int,
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->Int.toString} is missing a program_id`,
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->Int.toString} has multiple addresses; a program is uniquely identified by a single program_id`,
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->Int.toString}. Please remove the duplicate from your config.`
882
- : `Address ${addressString} on chain ${chainId->Int.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.`,
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
- (ChainMap.Chain.makeUnsafe(~chainId=chain.id), chain)
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<int>=?) => {
1105
+ let getEventConfig = (config: t, ~contractName, ~eventName, ~chainId: option<ChainId.t>=?) => {
1096
1106
  let chains = switch chainId {
1097
1107
  | Some(chainId) =>
1098
- let chain = ChainMap.Chain.makeUnsafe(~chainId)
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->Int.toString} is not configured. Add it to config.yaml or pass a configured chain.`,
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
- let chain = ChainMap.Chain.makeUnsafe(~chainId)
1127
- config.chainMap->ChainMap.has(chain)
1128
- ? chain
1134
+ let getChain = (config, ~chainId) =>
1135
+ config.chainMap->ChainMap.has(chainId)
1136
+ ? chainId
1129
1137
  : JsError.throwWithMessage(
1130
- "No chain with id " ++ chain->ChainMap.Chain.toString ++ " found in config.yaml",
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
- let tiers = [["version"], ["name"], ["storage"], ["evm", "fuel", "svm"], ["entities"]]
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
@@ -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 chainId.toString() + "-" + address;
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(S$RescriptSchema.int),
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", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
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(S$RescriptSchema.int),
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 compositeIndices = Stdlib_Option.getOr(entityJson.compositeIndices, []).map(ci => ci.map(f => ({
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, compositeIndices, fields.concat(derivedFields), entityJson.description);
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 ` + chainId.toString() + ` has multiple addresses; a program is uniquely identified by a single program_id`) : Stdlib_JsError.throwWithMessage(`SVM program ` + contractName + ` on chain ` + chainId.toString() + ` is missing a program_id`)
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 ` + chainId.toString() + `. Please remove the duplicate from your config.` : `Address ` + address + ` on chain ` + 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.`);
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
- ChainMap.Chain.makeUnsafe(chain.id),
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 chain = ChainMap.Chain.makeUnsafe(chainId);
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, chain);
775
+ chainConfig = ChainMap.get(config.chainMap, chainId$1);
773
776
  exit = 1;
774
777
  } catch (exn) {
775
- chains = Stdlib_JsError.throwWithMessage(`Chain ` + chainId.toString() + ` is not configured. Add it to config.yaml or pass a configured 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
- let chain = ChainMap.Chain.makeUnsafe(chainId);
812
- if (ChainMap.has(config.chainMap, chain)) {
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 " + ChainMap.Chain.toString(chain) + " found in config.yaml");
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.chain->ChainMap.Chain.toChainId->(Utils.magic: int => unknown)
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.chain;
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
@@ -21,6 +21,7 @@ type fromUserApiOptions = {
21
21
  type fromUserApiResult = {
22
22
  config: string,
23
23
  indexerTypes: Null.t<string>,
24
+ indexerCode: Null.t<string>,
24
25
  }
25
26
 
26
27
  type addon = {
@@ -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<int>,
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->Utils.Dict.dangerouslyGetByIntNonOption(chainId)->Option.getUnsafe
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 decide readiness for the
133
- // whole indexer. A chain is marked caught up only once EVERY chain is caught up
134
- // (reached endblock or fetched/processed to head) with no processable events
135
- // left so no chain flips to ready while another is still backfilling.
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
- let indexerCaughtUp = crossChainState->nextItemIsNone && everyChainCaughtUp.contents
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
- let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
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: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
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->Utils.Dict.setByInt(chainId, FetchState.WaitingForNewBlock)
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->Utils.Dict.setByInt(chainId, FetchState.NothingToQuery)
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->Utils.Dict.setByInt(chainId, action)
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->Utils.Dict.setByInt(chainId, idleOrWaitAction(cs))
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->Utils.Dict.setByInt(chainId, FetchState.Ready(queries))
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->Utils.Dict.dangerouslyGetByIntNonOption(chainId) {
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