envio 3.0.2-svm-alpha.1 → 3.0.2-svm-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +0 -1
  2. package/evm.schema.json +7 -0
  3. package/fuel.schema.json +7 -0
  4. package/index.d.ts +0 -2
  5. package/package.json +6 -6
  6. package/src/Batch.res +4 -214
  7. package/src/Batch.res.mjs +6 -165
  8. package/src/ChainFetcher.res +4 -5
  9. package/src/ChainFetcher.res.mjs +6 -7
  10. package/src/ChainManager.res +3 -5
  11. package/src/ChainManager.res.mjs +4 -6
  12. package/src/Config.res +100 -32
  13. package/src/Config.res.mjs +102 -31
  14. package/src/Core.res +4 -8
  15. package/src/Env.res +0 -1
  16. package/src/Env.res.mjs +0 -2
  17. package/src/EventConfigBuilder.res +38 -136
  18. package/src/EventConfigBuilder.res.mjs +14 -79
  19. package/src/EventUtils.res +0 -18
  20. package/src/EventUtils.res.mjs +0 -18
  21. package/src/FetchState.res +1 -11
  22. package/src/FetchState.res.mjs +2 -16
  23. package/src/GlobalState.res +3 -8
  24. package/src/GlobalState.res.mjs +1 -13
  25. package/src/HandlerLoader.res +6 -5
  26. package/src/HandlerLoader.res.mjs +27 -9
  27. package/src/HandlerRegister.res +1 -12
  28. package/src/HandlerRegister.res.mjs +1 -6
  29. package/src/HandlerRegister.resi +1 -1
  30. package/src/Hasura.res +96 -32
  31. package/src/Hasura.res.mjs +93 -38
  32. package/src/Internal.res +10 -17
  33. package/src/Internal.res.mjs +3 -3
  34. package/src/Main.res +2 -6
  35. package/src/Main.res.mjs +24 -16
  36. package/src/Persistence.res +17 -2
  37. package/src/Persistence.res.mjs +14 -2
  38. package/src/PgStorage.res +5 -1
  39. package/src/PgStorage.res.mjs +12 -6
  40. package/src/SimulateItems.res.mjs +21 -3
  41. package/src/TestIndexer.res.mjs +23 -4
  42. package/src/bindings/ClickHouse.res +6 -2
  43. package/src/bindings/ClickHouse.res.mjs +3 -2
  44. package/src/bindings/Vitest.res +3 -0
  45. package/src/db/InternalTable.res.mjs +35 -35
  46. package/src/db/Table.res +9 -2
  47. package/src/db/Table.res.mjs +10 -7
  48. package/src/sources/EvmChain.res +32 -9
  49. package/src/sources/EvmChain.res.mjs +31 -4
  50. package/src/sources/HyperSyncClient.res +35 -79
  51. package/src/sources/HyperSyncClient.res.mjs +35 -46
  52. package/src/sources/HyperSyncSolanaClient.res +2 -0
  53. package/src/sources/HyperSyncSolanaSource.res +72 -50
  54. package/src/sources/HyperSyncSolanaSource.res.mjs +71 -57
  55. package/src/sources/HyperSyncSource.res +13 -14
  56. package/src/sources/HyperSyncSource.res.mjs +8 -5
  57. package/src/sources/RpcSource.res +11 -7
  58. package/src/sources/RpcSource.res.mjs +6 -5
  59. package/svm.schema.json +112 -29
@@ -1,7 +1,6 @@
1
1
  type t = {
2
2
  committedCheckpointId: bigint,
3
3
  chainFetchers: ChainMap.t<ChainFetcher.t>,
4
- multichain: Config.multichain,
5
4
  isInReorgThreshold: bool,
6
5
  // True once every chain has caught up to head/endBlock. Monotonic during a run.
7
6
  isRealtime: bool,
@@ -35,6 +34,7 @@ let makeFromDbState = (
35
34
  ~initialState: Persistence.initialState,
36
35
  ~config: Config.t,
37
36
  ~registrations,
37
+ ~reducedPollingInterval=?,
38
38
  ): t => {
39
39
  let isInReorgThreshold = if initialState.cleanRun {
40
40
  false
@@ -81,6 +81,7 @@ let makeFromDbState = (
81
81
  ~targetBufferSize,
82
82
  ~config,
83
83
  ~registrations,
84
+ ~reducedPollingInterval?,
84
85
  ),
85
86
  )
86
87
  })
@@ -106,7 +107,6 @@ let makeFromDbState = (
106
107
 
107
108
  {
108
109
  committedCheckpointId: initialState.checkpointId,
109
- multichain: config.multichain,
110
110
  chainFetchers,
111
111
  isInReorgThreshold,
112
112
  isRealtime,
@@ -128,11 +128,10 @@ let setChainFetcher = (chainManager: t, chainFetcher: ChainFetcher.t) => {
128
128
  }
129
129
 
130
130
  let nextItemIsNone = (chainManager: t): bool => {
131
- !Batch.hasMultichainReadyItem(
131
+ !Batch.hasReadyItem(
132
132
  chainManager.chainFetchers->ChainMap.map(cf => {
133
133
  cf.fetchState
134
134
  }),
135
- ~multichain=chainManager.multichain,
136
135
  )
137
136
  }
138
137
 
@@ -152,7 +151,6 @@ let createBatch = (chainManager: t, ~batchSizeTarget: int, ~isRollback: bool): B
152
151
  reorgDetection: cf.reorgDetection,
153
152
  chainConfig: cf.chainConfig,
154
153
  }),
155
- ~multichain=chainManager.multichain,
156
154
  ~batchSizeTarget,
157
155
  )
158
156
  }
@@ -38,7 +38,7 @@ function calculateTargetBufferSize(activeChainsCount) {
38
38
  }
39
39
  }
40
40
 
41
- function makeFromDbState(initialState, config, registrations) {
41
+ function makeFromDbState(initialState, config, registrations, reducedPollingInterval) {
42
42
  let isInReorgThreshold = initialState.cleanRun ? false : initialState.chains.some(chain => isProgressInReorgThreshold(chain.progressBlockNumber, chain.sourceBlockNumber, chain.maxReorgDepth));
43
43
  let targetBufferSize = calculateTargetBufferSize(initialState.chains.length);
44
44
  Prometheus.ProcessingMaxBatchSize.set(config.batchSize);
@@ -54,7 +54,7 @@ function makeFromDbState(initialState, config, registrations) {
54
54
  let chainConfig = ChainMap.get(config.chainMap, chain);
55
55
  return [
56
56
  chain,
57
- ChainFetcher.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrations, targetBufferSize)
57
+ ChainFetcher.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrations, targetBufferSize, reducedPollingInterval)
58
58
  ];
59
59
  });
60
60
  let chainFetchers = ChainMap.fromArrayUnsafe(chainFetchersArr);
@@ -79,7 +79,6 @@ function makeFromDbState(initialState, config, registrations) {
79
79
  return {
80
80
  committedCheckpointId: initialState.checkpointId,
81
81
  chainFetchers: chainFetchers,
82
- multichain: config.multichain,
83
82
  isInReorgThreshold: isInReorgThreshold,
84
83
  isRealtime: isRealtime
85
84
  };
@@ -93,14 +92,13 @@ function setChainFetcher(chainManager, chainFetcher) {
93
92
  return {
94
93
  committedCheckpointId: chainManager.committedCheckpointId,
95
94
  chainFetchers: ChainMap.set(chainManager.chainFetchers, ChainMap.Chain.makeUnsafe(chainFetcher.chainConfig.id), chainFetcher),
96
- multichain: chainManager.multichain,
97
95
  isInReorgThreshold: chainManager.isInReorgThreshold,
98
96
  isRealtime: chainManager.isRealtime
99
97
  };
100
98
  }
101
99
 
102
100
  function nextItemIsNone(chainManager) {
103
- return !Batch.hasMultichainReadyItem(ChainMap.map(chainManager.chainFetchers, cf => cf.fetchState), chainManager.multichain);
101
+ return !Batch.hasReadyItem(ChainMap.map(chainManager.chainFetchers, cf => cf.fetchState));
104
102
  }
105
103
 
106
104
  function createBatch(chainManager, batchSizeTarget, isRollback) {
@@ -113,7 +111,7 @@ function createBatch(chainManager, batchSizeTarget, isRollback) {
113
111
  sourceBlockNumber: cf.fetchState.knownHeight,
114
112
  totalEventsProcessed: cf.numEventsProcessed,
115
113
  chainConfig: cf.chainConfig
116
- })), chainManager.multichain, batchSizeTarget);
114
+ })), batchSizeTarget);
117
115
  }
118
116
 
119
117
  function isProgressAtHead(chainManager) {
package/src/Config.res CHANGED
@@ -15,8 +15,6 @@ type contract = {
15
15
  addresses: array<Address.t>,
16
16
  events: array<Internal.eventConfig>,
17
17
  startBlock: option<int>,
18
- // EVM-specific: event sighashes for HyperSync queries
19
- eventSignatures: array<string>,
20
18
  }
21
19
 
22
20
  // Sources are instantiated lazily in ChainFetcher from this config.
@@ -56,10 +54,6 @@ type sourceSync = {
56
54
  pollingInterval: int,
57
55
  }
58
56
 
59
- type multichain = Internal.multichain =
60
- | @as("ordered") Ordered
61
- | @as("unordered") Unordered
62
-
63
57
  type storage = {
64
58
  postgres: bool,
65
59
  clickhouse: bool,
@@ -78,7 +72,6 @@ type t = {
78
72
  shouldRollbackOnReorg: bool,
79
73
  shouldSaveFullHistory: bool,
80
74
  storage: storage,
81
- multichain: multichain,
82
75
  chainMap: ChainMap.t<chain>,
83
76
  defaultChain: option<chain>,
84
77
  ecosystem: Ecosystem.t,
@@ -207,6 +200,7 @@ let svmEventDescriptorSchema = S.schema(s =>
207
200
  "discriminatorByteLen": s.matches(S.int),
208
201
  "includeTransaction": s.matches(S.bool),
209
202
  "includeLogs": s.matches(S.bool),
203
+ "includeTokenBalances": s.matches(S.bool),
210
204
  "accountFilters": s.matches(
211
205
  S.option(
212
206
  S.array(
@@ -235,10 +229,9 @@ let svmAbiSchema = S.schema(s =>
235
229
 
236
230
  let contractEventItemSchema = S.schema(s =>
237
231
  {
238
- "event": s.matches(S.string),
239
232
  "name": s.matches(S.string),
240
233
  "sighash": s.matches(S.string),
241
- "params": s.matches(S.option(S.array(EventConfigBuilder.eventParamSchema))),
234
+ "params": s.matches(S.option(S.array(EventConfigBuilder.paramMetaSchema))),
242
235
  "kind": s.matches(S.option(S.string)),
243
236
  "blockFields": s.matches(S.option(S.array(Internal.evmBlockFieldSchema))),
244
237
  "transactionFields": s.matches(S.option(S.array(Internal.evmTransactionFieldSchema))),
@@ -280,8 +273,6 @@ let publicConfigEvmSchema = S.schema(s =>
280
273
  }
281
274
  )
282
275
 
283
- let multichainSchema = S.enum([Ordered, Unordered])
284
-
285
276
  let compositeIndexFieldSchema = S.schema(s =>
286
277
  {
287
278
  "fieldName": s.matches(S.string),
@@ -294,6 +285,7 @@ let derivedFieldSchema = S.schema(s =>
294
285
  "fieldName": s.matches(S.string),
295
286
  "derivedFromEntity": s.matches(S.string),
296
287
  "derivedFromField": s.matches(S.string),
288
+ "description": s.matches(S.option(S.string)),
297
289
  }
298
290
  )
299
291
 
@@ -309,6 +301,7 @@ let propertySchema = S.schema(s =>
309
301
  "entity": s.matches(S.option(S.string)),
310
302
  "precision": s.matches(S.option(S.int)),
311
303
  "scale": s.matches(S.option(S.int)),
304
+ "description": s.matches(S.option(S.string)),
312
305
  }
313
306
  )
314
307
 
@@ -326,6 +319,7 @@ let entityJsonSchema = S.schema(s =>
326
319
  "properties": s.matches(S.array(propertySchema)),
327
320
  "derivedFields": s.matches(S.option(S.array(derivedFieldSchema))),
328
321
  "compositeIndices": s.matches(S.option(S.array(S.array(compositeIndexFieldSchema)))),
322
+ "description": s.matches(S.option(S.string)),
329
323
  }
330
324
  )
331
325
 
@@ -418,6 +412,7 @@ let parseEntitiesFromJson = (
418
412
  ~isArray,
419
413
  ~isIndex,
420
414
  ~linkedEntity=?prop["linkedEntity"],
415
+ ~description=?prop["description"],
421
416
  )
422
417
  })
423
418
 
@@ -429,6 +424,7 @@ let parseEntitiesFromJson = (
429
424
  df["fieldName"],
430
425
  ~derivedFromEntity=df["derivedFromEntity"],
431
426
  ~derivedFromField=df["derivedFromField"],
427
+ ~description=?df["description"],
432
428
  )
433
429
  )
434
430
 
@@ -448,6 +444,7 @@ let parseEntitiesFromJson = (
448
444
  entityName,
449
445
  ~fields=Array.concat(fields, derivedFields),
450
446
  ~compositeIndices,
447
+ ~description=?entityJson["description"],
451
448
  )
452
449
 
453
450
  // Build schema dynamically from properties
@@ -509,7 +506,6 @@ let publicConfigSchema = S.schema(s =>
509
506
  "description": s.matches(S.option(S.string)),
510
507
  "handlers": s.matches(S.option(S.string)),
511
508
  "isDev": s.matches(S.option(S.bool)),
512
- "multichain": s.matches(S.option(multichainSchema)),
513
509
  "fullBatchSize": s.matches(S.option(S.int)),
514
510
  "rollbackOnReorg": s.matches(S.option(S.bool)),
515
511
  "saveFullHistory": s.matches(S.option(S.bool)),
@@ -592,7 +588,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
592
588
  | None => (Utils.Set.fromArray(EventConfigBuilder.alwaysIncludedBlockFields), Utils.Set.make())
593
589
  }
594
590
 
595
- // Build contract data lookup: ABI, event signatures, event configs (keyed by capitalized name)
596
591
  let contractDataByName: dict<{
597
592
  "abi": EvmTypes.Abi.t,
598
593
  "eventSignatures": array<string>,
@@ -607,7 +602,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
607
602
  let capitalizedName = contractName->Utils.String.capitalize
608
603
  let abi = contractConfig["abi"]->(Utils.magic: JSON.t => EvmTypes.Abi.t)
609
604
  let eventSignatures = switch contractConfig["events"] {
610
- | Some(events) => events->Array.map(eventItem => eventItem["event"])
605
+ | Some(events) => events->Array.map(eventItem => eventItem["sighash"])
611
606
  | None => []
612
607
  }
613
608
  let widened =
@@ -696,7 +691,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
696
691
  "includeLogs": bool,
697
692
  "includeTokenBalances": bool,
698
693
  "accountFilters": option<
699
- array<{"position": int, "values": array<string>}>,
694
+ array<array<{"position": int, "values": array<string>}>>,
700
695
  >,
701
696
  "isInner": option<bool>,
702
697
  "accounts": option<array<string>>,
@@ -712,10 +707,12 @@ let fromPublic = (publicConfigJson: JSON.t) => {
712
707
  )
713
708
  }
714
709
  let accountFilters =
715
- (svm["accountFilters"]->Option.getOr([]))->Array.map(af => {
716
- Internal.position: af["position"],
717
- values: af["values"]->SvmTypes.Pubkey.fromStringsUnsafe,
718
- })
710
+ (svm["accountFilters"]->Option.getOr([]))->Array.map(group =>
711
+ group->Array.map(af => {
712
+ Internal.position: af["position"],
713
+ values: af["values"]->SvmTypes.Pubkey.fromStringsUnsafe,
714
+ })
715
+ )
719
716
  (EventConfigBuilder.buildSvmInstructionEventConfig(
720
717
  ~contractName,
721
718
  ~instructionName=eventName,
@@ -824,7 +821,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
824
821
  addresses,
825
822
  events,
826
823
  startBlock,
827
- eventSignatures: contractData["eventSignatures"],
828
824
  }
829
825
  })
830
826
 
@@ -963,7 +959,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
963
959
  shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
964
960
  shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
965
961
  storage: globalStorage,
966
- multichain: publicConfig["multichain"]->Option.getOr(Unordered),
967
962
  chainMap,
968
963
  defaultChain: chains->Array.get(0),
969
964
  enableRawEvents: publicConfig["rawEvents"]->Option.getOr(false),
@@ -1094,11 +1089,18 @@ let rec canonicalJson = (json: JSON.t): JSON.t =>
1094
1089
  }
1095
1090
 
1096
1091
  // Returns dotted leaf paths (`a.b[i].c`) where `stored` differs from
1097
- // `current`. Skips subtrees whose canonical JSON matches.
1092
+ // `current`, restricted to the highest-priority top-level tier with any
1093
+ // diff. Tiers in order: version → name → storage → ecosystem
1094
+ // (evm/fuel/svm) → entities → other top-level keys. The first tier
1095
+ // containing a diff is the only one rendered; lower tiers are silenced
1096
+ // so a single noisy section doesn't bury the actionable change.
1098
1097
  let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
1098
+ let canonEq = (a: JSON.t, b: JSON.t) =>
1099
+ JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b))
1100
+
1099
1101
  let acc = []
1100
1102
  let rec go = (s: JSON.t, c: JSON.t, prefix: string) => {
1101
- if JSON.stringify(canonicalJson(s)) === JSON.stringify(canonicalJson(c)) {
1103
+ if canonEq(s, c) {
1102
1104
  ()
1103
1105
  } else {
1104
1106
  switch (s, c) {
@@ -1124,25 +1126,91 @@ let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
1124
1126
  | (Some(sv), Some(cv)) => go(sv, cv, p)
1125
1127
  }
1126
1128
  }
1127
- | _ =>
1128
- // Type mismatch or scalar diff
1129
- acc->Array.push(prefix === "" ? "<root>" : prefix)->ignore
1129
+ | _ => acc->Array.push(prefix === "" ? "<root>" : prefix)->ignore
1130
1130
  }
1131
1131
  }
1132
1132
  }
1133
- go(stored, current, "")
1133
+
1134
+ let getTopKey = (j: JSON.t, k: string) =>
1135
+ switch j {
1136
+ | Object(d) => d->Dict.get(k)
1137
+ | _ => None
1138
+ }
1139
+ let topKeyDiffers = (k: string) =>
1140
+ switch (getTopKey(stored, k), getTopKey(current, k)) {
1141
+ | (None, None) => false
1142
+ | (None, _) | (_, None) => true
1143
+ | (Some(s), Some(c)) => !canonEq(s, c)
1144
+ }
1145
+ let runTier = (keys: array<string>) =>
1146
+ keys->Array.forEach(k =>
1147
+ switch (getTopKey(stored, k), getTopKey(current, k)) {
1148
+ | (None, None) => ()
1149
+ | (None, _) | (_, None) => acc->Array.push(k)->ignore
1150
+ | (Some(s), Some(c)) => go(s, c, k)
1151
+ }
1152
+ )
1153
+
1154
+ switch (stored, current) {
1155
+ | (Object(sObj), Object(cObj)) =>
1156
+ let tiers = [["version"], ["name"], ["storage"], ["evm", "fuel", "svm"], ["entities"]]
1157
+ let firstHit = tiers->Array.reduce(None, (acc, tier) =>
1158
+ switch acc {
1159
+ | Some(_) => acc
1160
+ | None =>
1161
+ switch tier->Array.filter(topKeyDiffers) {
1162
+ | [] => None
1163
+ | hits => Some(hits)
1164
+ }
1165
+ }
1166
+ )
1167
+ switch firstHit {
1168
+ | Some(hits) => runTier(hits)
1169
+ | None =>
1170
+ let knownSet = Utils.Set.fromArray(tiers->Belt.Array.concatMany)
1171
+ let extras =
1172
+ Utils.Set.fromArray(Array.concat(sObj->Dict.keysToArray, cObj->Dict.keysToArray))
1173
+ ->Utils.Set.toArray
1174
+ ->Array.filter(k => !(knownSet->Utils.Set.has(k)))
1175
+ ->Array.toSorted(String.compare)
1176
+ ->Array.filter(topKeyDiffers)
1177
+ runTier(extras)
1178
+ }
1179
+ | _ => go(stored, current, "")
1180
+ }
1134
1181
  acc
1135
1182
  }
1136
1183
 
1137
1184
  // Throws an `incompatible config` error listing each path in `changedPaths`,
1138
- // plus the revert/reset remediation. `~resetCommand` lets each call site
1139
- // (`envio dev -r` / `envio start -r` / `envio local db-migrate setup`)
1140
- // surface the right wipe-and-restart command.
1141
- let throwIfIncompatible = (changedPaths: array<string>, ~resetCommand: string) => {
1185
+ // plus the remediation options. `~resetCommand` is rendered as-is for
1186
+ // option 2 (the wipe-and-redo). `~runCommand` controls option 3 (parallel
1187
+ // indexer recipe): when `None`, option 3 is omitted — the migrate flow
1188
+ // uses this because running a second indexer doesn't apply.
1189
+ // `~hasClickhouse` adds the extra env line so users running both
1190
+ // Postgres and Clickhouse get a complete override.
1191
+ let throwIfIncompatible = (
1192
+ changedPaths: array<string>,
1193
+ ~resetCommand: string,
1194
+ ~runCommand: option<string>,
1195
+ ~hasClickhouse: bool,
1196
+ ) => {
1142
1197
  if changedPaths->Array.length > 0 {
1143
1198
  let bullets = changedPaths->Array.map(p => ` - ${p}`)->Array.joinUnsafe("\n")
1199
+ let option1 = "Revert the changes above"
1200
+ let padTo = (s, col) => s ++ " "->String.repeat(Math.Int.max(col - String.length(s), 1))
1201
+ let col = Math.Int.max(String.length(option1), String.length(resetCommand)) + 2
1202
+ let option3 = switch runCommand {
1203
+ | None => ""
1204
+ | Some(cmd) =>
1205
+ let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : ""
1206
+ `\n 3. Run a second indexer alongside this one — keep both datasets:\n ENVIO_PG_SCHEMA=<new_schema> \\\n${clickhouseLine} ENVIO_INDEXER_PORT=<new_port> \\\n ${cmd}`
1207
+ }
1144
1208
  JsError.throwWithMessage(
1145
- `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n\n 1. Revert the changes above # resume indexing where it left off\n 2. ${resetCommand} # wipe the database and re-index from scratch`,
1209
+ `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n 1. ${option1->padTo(
1210
+ col,
1211
+ )}# resume indexing where it left off\n 2. ${resetCommand->padTo(
1212
+ col,
1213
+ )}# delete all indexed data and start over${option3}`,
1146
1214
  )
1147
1215
  }
1148
1216
  }
@@ -44,12 +44,12 @@ let schema = S$RescriptSchema.schema(s => ({
44
44
  let rowsSchema = S$RescriptSchema.array(schema);
45
45
 
46
46
  let table = Table.mkTable(name, undefined, [
47
- Table.mkField("id", "String", S$RescriptSchema.string, undefined, undefined, undefined, true, undefined, undefined),
48
- Table.mkField("chain_id", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined),
49
- Table.mkField("registration_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined),
50
- Table.mkField("registration_log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined),
51
- Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined)
52
- ]);
47
+ Table.mkField("id", "String", S$RescriptSchema.string, undefined, undefined, undefined, true, undefined, undefined, undefined),
48
+ Table.mkField("chain_id", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
49
+ Table.mkField("registration_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
50
+ Table.mkField("registration_log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
51
+ Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
52
+ ], undefined);
53
53
 
54
54
  let entityConfig_storage = {
55
55
  postgres: true,
@@ -118,6 +118,7 @@ let svmEventDescriptorSchema = S$RescriptSchema.schema(s => ({
118
118
  discriminatorByteLen: s.m(S$RescriptSchema.int),
119
119
  includeTransaction: s.m(S$RescriptSchema.bool),
120
120
  includeLogs: s.m(S$RescriptSchema.bool),
121
+ includeTokenBalances: s.m(S$RescriptSchema.bool),
121
122
  accountFilters: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.schema(s => ({
122
123
  position: s.m(S$RescriptSchema.int),
123
124
  values: s.m(S$RescriptSchema.array(S$RescriptSchema.string))
@@ -134,10 +135,9 @@ let svmAbiSchema = S$RescriptSchema.schema(s => ({
134
135
  }));
135
136
 
136
137
  let contractEventItemSchema = S$RescriptSchema.schema(s => ({
137
- event: s.m(S$RescriptSchema.string),
138
138
  name: s.m(S$RescriptSchema.string),
139
139
  sighash: s.m(S$RescriptSchema.string),
140
- params: s.m(S$RescriptSchema.option(S$RescriptSchema.array(EventConfigBuilder.eventParamSchema))),
140
+ params: s.m(S$RescriptSchema.option(S$RescriptSchema.array(EventConfigBuilder.paramMetaSchema))),
141
141
  kind: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
142
142
  blockFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.evmBlockFieldSchema))),
143
143
  transactionFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.evmTransactionFieldSchema))),
@@ -168,11 +168,6 @@ let publicConfigEvmSchema = S$RescriptSchema.schema(s => ({
168
168
  globalTransactionFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.evmTransactionFieldSchema)))
169
169
  }));
170
170
 
171
- let multichainSchema = S$RescriptSchema.$$enum([
172
- "ordered",
173
- "unordered"
174
- ]);
175
-
176
171
  let compositeIndexFieldSchema = S$RescriptSchema.schema(s => ({
177
172
  fieldName: s.m(S$RescriptSchema.string),
178
173
  direction: s.m(S$RescriptSchema.string)
@@ -181,7 +176,8 @@ let compositeIndexFieldSchema = S$RescriptSchema.schema(s => ({
181
176
  let derivedFieldSchema = S$RescriptSchema.schema(s => ({
182
177
  fieldName: s.m(S$RescriptSchema.string),
183
178
  derivedFromEntity: s.m(S$RescriptSchema.string),
184
- derivedFromField: s.m(S$RescriptSchema.string)
179
+ derivedFromField: s.m(S$RescriptSchema.string),
180
+ description: s.m(S$RescriptSchema.option(S$RescriptSchema.string))
185
181
  }));
186
182
 
187
183
  let propertySchema = S$RescriptSchema.schema(s => ({
@@ -194,7 +190,8 @@ let propertySchema = S$RescriptSchema.schema(s => ({
194
190
  enum: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
195
191
  entity: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
196
192
  precision: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
197
- scale: s.m(S$RescriptSchema.option(S$RescriptSchema.int))
193
+ scale: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
194
+ description: s.m(S$RescriptSchema.option(S$RescriptSchema.string))
198
195
  }));
199
196
 
200
197
  let entityStorageSchema = S$RescriptSchema.schema(s => ({
@@ -207,7 +204,8 @@ let entityJsonSchema = S$RescriptSchema.schema(s => ({
207
204
  storage: s.m(S$RescriptSchema.option(entityStorageSchema)),
208
205
  properties: s.m(S$RescriptSchema.array(propertySchema)),
209
206
  derivedFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(derivedFieldSchema))),
210
- compositeIndices: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(compositeIndexFieldSchema))))
207
+ compositeIndices: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(compositeIndexFieldSchema)))),
208
+ description: s.m(S$RescriptSchema.option(S$RescriptSchema.string))
211
209
  }));
212
210
 
213
211
  function getFieldTypeAndSchema(prop, enumConfigsByName) {
@@ -325,14 +323,14 @@ function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage) {
325
323
  let entityName = entityJson.name;
326
324
  let fields = entityJson.properties.map(prop => {
327
325
  let match = getFieldTypeAndSchema(prop, enumConfigsByName);
328
- return Table.mkField(prop.name, match[0], match[1], undefined, match[3], match[2], prop.name === "id", match[4], prop.linkedEntity);
326
+ return Table.mkField(prop.name, match[0], match[1], undefined, match[3], match[2], prop.name === "id", match[4], prop.linkedEntity, prop.description);
329
327
  });
330
- let derivedFields = Stdlib_Option.getOr(entityJson.derivedFields, []).map(df => Table.mkDerivedFromField(df.fieldName, df.derivedFromEntity, df.derivedFromField));
328
+ let derivedFields = Stdlib_Option.getOr(entityJson.derivedFields, []).map(df => Table.mkDerivedFromField(df.fieldName, df.derivedFromEntity, df.derivedFromField, df.description));
331
329
  let compositeIndices = Stdlib_Option.getOr(entityJson.compositeIndices, []).map(ci => ci.map(f => ({
332
330
  fieldName: f.fieldName,
333
331
  direction: f.direction === "Asc" ? "Asc" : "Desc"
334
332
  })));
335
- let table = Table.mkTable(entityName, compositeIndices, fields.concat(derivedFields));
333
+ let table = Table.mkTable(entityName, compositeIndices, fields.concat(derivedFields), entityJson.description);
336
334
  let schema = S$RescriptSchema.schema(s => {
337
335
  let dict = {};
338
336
  entityJson.properties.forEach(prop => {
@@ -378,7 +376,6 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
378
376
  description: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
379
377
  handlers: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
380
378
  isDev: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
381
- multichain: s.m(S$RescriptSchema.option(multichainSchema)),
382
379
  fullBatchSize: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
383
380
  rollbackOnReorg: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
384
381
  saveFullHistory: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
@@ -478,7 +475,7 @@ function fromPublic(publicConfigJson) {
478
475
  let capitalizedName = Utils.$$String.capitalize(param[0]);
479
476
  let abi = contractConfig.abi;
480
477
  let events = contractConfig.events;
481
- let eventSignatures = events !== undefined ? events.map(eventItem => eventItem.event) : [];
478
+ let eventSignatures = events !== undefined ? events.map(eventItem => eventItem.sighash) : [];
482
479
  contractDataByName[capitalizedName] = {
483
480
  abi: abi,
484
481
  eventSignatures: eventSignatures,
@@ -511,10 +508,10 @@ function fromPublic(publicConfigJson) {
511
508
  ) : addresses[0];
512
509
  let s = eventItem.svm;
513
510
  let svm = s !== undefined ? Primitive_option.valFromOption(s) : Stdlib_JsError.throwWithMessage(`SVM instruction ` + contractName + `.` + eventName + ` is missing the "svm" descriptor in internal config`);
514
- let accountFilters = Stdlib_Option.getOr(svm.accountFilters, []).map(af => ({
511
+ let accountFilters = Stdlib_Option.getOr(svm.accountFilters, []).map(group => group.map(af => ({
515
512
  position: af.position,
516
513
  values: af.values
517
- }));
514
+ })));
518
515
  return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, svm.discriminatorByteLen, svm.includeTransaction, svm.includeLogs, svm.includeTokenBalances, accountFilters, svm.isInner, false, undefined, undefined, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes, startBlock);
519
516
  }
520
517
  });
@@ -562,8 +559,7 @@ function fromPublic(publicConfigJson) {
562
559
  abi: contractData.abi,
563
560
  addresses: addresses,
564
561
  events: events,
565
- startBlock: startBlock,
566
- eventSignatures: contractData.eventSignatures
562
+ startBlock: startBlock
567
563
  };
568
564
  });
569
565
  let sourceConfig;
@@ -672,7 +668,6 @@ function fromPublic(publicConfigJson) {
672
668
  shouldRollbackOnReorg: Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true),
673
669
  shouldSaveFullHistory: Stdlib_Option.getOr(publicConfig.saveFullHistory, false),
674
670
  storage: globalStorage,
675
- multichain: Stdlib_Option.getOr(publicConfig.multichain, "unordered"),
676
671
  chainMap: chainMap,
677
672
  defaultChain: chains[0],
678
673
  ecosystem: ecosystem,
@@ -818,9 +813,10 @@ function canonicalJson(json) {
818
813
  }
819
814
 
820
815
  function diffPaths(stored, current) {
816
+ let canonEq = (a, b) => JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
821
817
  let acc = [];
822
818
  let go = (s, c, prefix) => {
823
- if (JSON.stringify(canonicalJson(s)) === JSON.stringify(canonicalJson(c))) {
819
+ if (canonEq(s, c)) {
824
820
  return;
825
821
  }
826
822
  if (Array.isArray(s)) {
@@ -868,16 +864,92 @@ function diffPaths(stored, current) {
868
864
  }
869
865
  acc.push(prefix === "" ? "<root>" : prefix);
870
866
  };
871
- go(stored, current, "");
867
+ let getTopKey = (j, k) => {
868
+ if (typeof j === "object" && j !== null && !Array.isArray(j)) {
869
+ return j[k];
870
+ }
871
+ };
872
+ let topKeyDiffers = k => {
873
+ let match = getTopKey(stored, k);
874
+ let match$1 = getTopKey(current, k);
875
+ if (match !== undefined) {
876
+ if (match$1 !== undefined) {
877
+ return !canonEq(match, match$1);
878
+ } else {
879
+ return true;
880
+ }
881
+ } else {
882
+ return match$1 !== undefined;
883
+ }
884
+ };
885
+ let runTier = keys => {
886
+ keys.forEach(k => {
887
+ let match = getTopKey(stored, k);
888
+ let match$1 = getTopKey(current, k);
889
+ if (match !== undefined) {
890
+ if (match$1 !== undefined) {
891
+ return go(match, match$1, k);
892
+ } else {
893
+ acc.push(k);
894
+ return;
895
+ }
896
+ } else if (match$1 !== undefined) {
897
+ acc.push(k);
898
+ return;
899
+ } else {
900
+ return;
901
+ }
902
+ });
903
+ };
904
+ if (typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof current === "object" && current !== null && !Array.isArray(current)) {
905
+ let tiers = [
906
+ ["version"],
907
+ ["name"],
908
+ ["storage"],
909
+ [
910
+ "evm",
911
+ "fuel",
912
+ "svm"
913
+ ],
914
+ ["entities"]
915
+ ];
916
+ let firstHit = Stdlib_Array.reduce(tiers, undefined, (acc, tier) => {
917
+ if (acc !== undefined) {
918
+ return acc;
919
+ }
920
+ let hits = tier.filter(topKeyDiffers);
921
+ if (hits.length !== 0) {
922
+ return hits;
923
+ }
924
+ });
925
+ if (firstHit !== undefined) {
926
+ runTier(firstHit);
927
+ } else {
928
+ let knownSet = new Set(Belt_Array.concatMany(tiers));
929
+ runTier(Array.from(new Set(Object.keys(stored).concat(Object.keys(current)))).filter(k => !knownSet.has(k)).toSorted(Primitive_string.compare).filter(topKeyDiffers));
930
+ }
931
+ } else {
932
+ go(stored, current, "");
933
+ }
872
934
  return acc;
873
935
  }
874
936
 
875
- function throwIfIncompatible(changedPaths, resetCommand) {
937
+ function throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse) {
876
938
  if (changedPaths.length === 0) {
877
939
  return;
878
940
  }
879
941
  let bullets = changedPaths.map(p => ` - ` + p).join("\n");
880
- Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n\n 1. Revert the changes above # resume indexing where it left off\n 2. ` + resetCommand + ` # wipe the database and re-index from scratch`);
942
+ let option1 = "Revert the changes above";
943
+ let padTo = (s, col) => s + " ".repeat(Math.max(col - s.length | 0, 1));
944
+ let col = Math.max(option1.length, resetCommand.length) + 2 | 0;
945
+ let option3;
946
+ if (runCommand !== undefined) {
947
+ let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : "";
948
+ option3 = `\n 3. Run a second indexer alongside this one — keep both datasets:\n ENVIO_PG_SCHEMA=<new_schema> \\\n` + clickhouseLine + ` ENVIO_INDEXER_PORT=<new_port> \\\n ` + runCommand;
949
+ } else {
950
+ option3 = "";
951
+ }
952
+ Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n 1. ` + padTo(option1, col) + `# resume indexing where it left off\n 2. ` + padTo(resetCommand, col) + `# delete all indexed data and start over` + option3);
881
953
  }
882
954
 
883
955
  function loadWithoutRegistrations() {
@@ -906,7 +978,6 @@ export {
906
978
  contractConfigSchema,
907
979
  publicConfigEcosystemSchema,
908
980
  publicConfigEvmSchema,
909
- multichainSchema,
910
981
  compositeIndexFieldSchema,
911
982
  derivedFieldSchema,
912
983
  propertySchema,
package/src/Core.res CHANGED
@@ -1,13 +1,9 @@
1
1
  // Resolution order:
2
- // 1. Production: require("envio-{os}-{arch}") platform-specific npm package
3
- // 2. Dev build: find repo cargo build --lib load from target/debug/
2
+ // 1. Production: require("envio-{os}-{arch}") - platform-specific npm package
3
+ // 2. Dev build: find repo -> cargo build --lib -> load from target/debug/
4
4
 
5
5
  // NAPI encodes Rust `Option<T>` as `null | T` (never `undefined`), so the
6
6
  // tighter `Null.t` captures the exact boundary shape.
7
- //
8
- // Opaque carriers for the NAPI class constructors. Static factories
9
- // (`newWithAgent`, `fromConfig`, `fromSignatures`) hang off these via `@send`
10
- // in `HyperSyncClient.res` / `HyperSyncSolanaClient.res`.
11
7
  type hypersyncClientCtor
12
8
  type hypersyncSolanaClientCtor
13
9
  type decoderCtor
@@ -147,8 +143,8 @@ let loadAddon = () => {
147
143
  }
148
144
 
149
145
  // Only swallow MODULE_NOT_FOUND (the optional package isn't installed on
150
- // this host). Any other failure corrupt .node, ABI mismatch, dlopen
151
- // error is a real load failure and must surface.
146
+ // this host). Any other failure - corrupt .node, ABI mismatch, dlopen
147
+ // error - is a real load failure and must surface.
152
148
  let rec tryRequire = i =>
153
149
  switch candidates[i] {
154
150
  | None => None