envio 3.13.0 → 3.14.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 (69) hide show
  1. package/bin.mjs +10 -1
  2. package/index.d.ts +11 -0
  3. package/package.json +8 -20
  4. package/rescript.json +1 -4
  5. package/src/Config.res +69 -100
  6. package/src/Config.res.mjs +50 -63
  7. package/src/Core.res +46 -6
  8. package/src/Core.res.mjs +41 -4
  9. package/src/EventConfigBuilder.res +4 -10
  10. package/src/EventConfigBuilder.res.mjs +1 -3
  11. package/src/ExitOnCaughtUp.res +5 -1
  12. package/src/ExitOnCaughtUp.res.mjs +8 -3
  13. package/src/HandlerLoader.res +74 -32
  14. package/src/HandlerLoader.res.mjs +69 -23
  15. package/src/HandlerTypeCheck.mjs +14 -0
  16. package/src/HandlerTypeCheckWorker.mjs +68 -0
  17. package/src/Internal.res +1 -1
  18. package/src/Main.res +17 -12
  19. package/src/Main.res.mjs +9 -3
  20. package/src/NodeVersion.mjs +13 -0
  21. package/src/Persistence.res +79 -38
  22. package/src/Persistence.res.mjs +41 -10
  23. package/src/PgStorage.res +62 -34
  24. package/src/PgStorage.res.mjs +65 -31
  25. package/src/ResumePlan.res +172 -0
  26. package/src/ResumePlan.res.mjs +122 -0
  27. package/src/Supervisor.res +8 -8
  28. package/src/Supervisor.res.mjs +6 -1
  29. package/src/TestIndexer.res +14 -4
  30. package/src/TestIndexer.res.mjs +3 -2
  31. package/src/TsModuleHooks.mjs +121 -0
  32. package/src/Utils.res +0 -8
  33. package/src/Worker.res +8 -0
  34. package/src/Worker.res.mjs +11 -0
  35. package/src/bindings/NodeJs.res +8 -7
  36. package/src/bindings/NodeJs.res.mjs +0 -3
  37. package/src/bindings/Pino.res +2 -2
  38. package/src/db/InternalTable.res +52 -2
  39. package/src/db/InternalTable.res.mjs +32 -3
  40. package/src/sources/FuelHyperSync.res +2 -0
  41. package/src/sources/FuelHyperSync.res.mjs +1 -0
  42. package/src/sources/FuelHyperSync.resi +1 -0
  43. package/src/sources/FuelHyperSyncClient.res +25 -8
  44. package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
  45. package/src/sources/FuelHyperSyncSource.res +24 -22
  46. package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
  47. package/src/tui/InitApi.res +85 -0
  48. package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
  49. package/src/tui/Tui.res +142 -249
  50. package/src/tui/Tui.res.mjs +123 -332
  51. package/src/Js.shim.ts +0 -11
  52. package/src/bindings/DateFns.res +0 -74
  53. package/src/bindings/DateFns.res.mjs +0 -22
  54. package/src/sources/FuelSDK.res +0 -38
  55. package/src/sources/FuelSDK.res.mjs +0 -27
  56. package/src/sources/vendored-fuel-abi-coder.js +0 -1792
  57. package/src/tui/bindings/Ink.res +0 -369
  58. package/src/tui/bindings/Ink.res.mjs +0 -66
  59. package/src/tui/bindings/Style.res +0 -123
  60. package/src/tui/bindings/Style.res.mjs +0 -2
  61. package/src/tui/components/BufferedProgressBar.res +0 -40
  62. package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
  63. package/src/tui/components/CustomHooks.res +0 -122
  64. package/src/tui/components/Messages.res +0 -40
  65. package/src/tui/components/Messages.res.mjs +0 -71
  66. package/src/tui/components/SyncETA.res +0 -179
  67. package/src/tui/components/SyncETA.res.mjs +0 -249
  68. package/src/tui/components/TuiData.res +0 -98
  69. package/src/tui/components/TuiData.res.mjs +0 -73
package/bin.mjs CHANGED
@@ -1,3 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "./src/Bin.res.mjs";
2
+ import { unsupportedNodeMessage } from "./src/NodeVersion.mjs";
3
+
4
+ // Checked before the rest loads, which an older Node may not even parse.
5
+ const unsupported = unsupportedNodeMessage();
6
+ if (unsupported !== undefined) {
7
+ console.error(unsupported);
8
+ process.exit(1);
9
+ }
10
+
11
+ const { run } = await import("./src/Bin.res.mjs");
3
12
  await run(process.argv.slice(2));
package/index.d.ts CHANGED
@@ -12,6 +12,17 @@ declare const bigintSchema: Sury.Schema<bigint>;
12
12
  /** Ethereum address — a 20-byte hex string prefixed with `0x`. */
13
13
  export type Address = `0x${string}`;
14
14
 
15
+ /** A value a `Json` entity field can hold. A `bigint` isn't JSON — store its
16
+ * decimal string with `.toString()`. Shadows the global `JSON` interface only
17
+ * where imported; the `JSON` object itself is unaffected. */
18
+ export type JSON =
19
+ | string
20
+ | number
21
+ | boolean
22
+ | null
23
+ | readonly JSON[]
24
+ | { readonly [key: string]: JSON };
25
+
15
26
  /** Structured logger bound to an event or handler context. Messages are
16
27
  * displayed in the console and the Envio Hosted Service. */
17
28
  export type Logger = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.13.0",
3
+ "version": "3.14.0",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -27,7 +27,7 @@
27
27
  "dapp"
28
28
  ],
29
29
  "engines": {
30
- "node": ">=22.0.0"
30
+ "node": ">=22.15.0"
31
31
  },
32
32
  "files": [
33
33
  "bin.mjs",
@@ -42,11 +42,6 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "@elastic/ecs-pino-format": "1.4.0",
45
- "@fuel-ts/crypto": "0.96.1",
46
- "@fuel-ts/errors": "0.96.1",
47
- "@fuel-ts/hasher": "0.96.1",
48
- "@fuel-ts/math": "0.96.1",
49
- "@fuel-ts/utils": "0.96.1",
50
45
  "bignumber.js": "9.3.1",
51
46
  "eventsource": "4.1.0",
52
47
  "express": "4.19.2",
@@ -56,20 +51,13 @@
56
51
  "rescript-schema": "9.5.1",
57
52
  "viem": "2.54.0",
58
53
  "dotenv": "16.4.5",
59
- "date-fns": "3.3.1",
60
- "@rescript/react": "0.14.1",
61
- "react": "19.2.5",
62
- "ink": "6.8.0",
63
- "ink-big-text": "2.0.0",
64
- "ink-spinner": "5.0.0",
65
- "postgres": "3.4.8",
66
- "tsx": "4.21.0"
54
+ "postgres": "3.4.8"
67
55
  },
68
56
  "optionalDependencies": {
69
- "envio-linux-x64": "3.13.0",
70
- "envio-linux-x64-musl": "3.13.0",
71
- "envio-linux-arm64": "3.13.0",
72
- "envio-darwin-x64": "3.13.0",
73
- "envio-darwin-arm64": "3.13.0"
57
+ "envio-linux-x64": "3.14.0",
58
+ "envio-linux-x64-musl": "3.14.0",
59
+ "envio-linux-arm64": "3.14.0",
60
+ "envio-darwin-x64": "3.14.0",
61
+ "envio-darwin-arm64": "3.14.0"
74
62
  }
75
63
  }
package/rescript.json CHANGED
@@ -12,10 +12,7 @@
12
12
  "module": "esmodule",
13
13
  "in-source": true
14
14
  },
15
- "jsx": {
16
- "version": 4
17
- },
18
- "dependencies": ["rescript-schema", "@rescript/react"],
15
+ "dependencies": ["rescript-schema"],
19
16
  "compiler-flags": ["-open RescriptSchema"],
20
17
  "warnings": {
21
18
  "error": "+a"
package/src/Config.res CHANGED
@@ -136,6 +136,9 @@ type t = {
136
136
  reorgThresholdReadyTolerance: int,
137
137
  lowercaseAddresses: bool,
138
138
  isDev: bool,
139
+ // The operator's `-d` / `--config` flags, repeated in every command printed
140
+ // for them to run next. Empty when they gave neither.
141
+ projectFlags: string,
139
142
  // An `envio start --chain` process: drives a subset of the schema's chains
140
143
  // while sibling processes drive the rest, so it only touches what its own
141
144
  // chains own — their partitions' indexes, their `ready_at`, their resume.
@@ -147,6 +150,7 @@ type t = {
147
150
  // the schema alone: a cross-chain entity has rows any chain's reorg can
148
151
  // reach, so its checkpoints have to be comparable across chains.
149
152
  checkpointSequence: CheckpointSequence.t,
153
+ envioInfo: JSON.t,
150
154
  }
151
155
 
152
156
  type rpcSourceFor = | @as("sync") Sync | @as("fallback") Fallback | @as("realtime") Realtime
@@ -595,6 +599,7 @@ let publicConfigSchema = S.schema(s =>
595
599
  "handlers": s.matches(S.option(S.string)),
596
600
  "isDev": s.matches(S.option(S.bool)),
597
601
  "isolatedChains": s.matches(S.option(S.array(ChainId.schema))),
602
+ "projectFlags": s.matches(S.option(S.string)),
598
603
  "fullBatchSize": s.matches(S.option(S.int)),
599
604
  "rollbackOnReorg": s.matches(S.option(S.bool)),
600
605
  "saveFullHistory": s.matches(S.option(S.bool)),
@@ -666,6 +671,41 @@ let isolate = (config: t, ~chainIds: array<ChainId.t>) => {
666
671
  }
667
672
  }
668
673
 
674
+ // What the command decided rather than the project's files: `envio dev` vs
675
+ // `envio start`, which chains this process drives, and how it found the
676
+ // project.
677
+ let commandFields = ["isDev", "isolatedChains", "projectFlags"]
678
+
679
+ let withProjectFlags = (command, ~config: t) =>
680
+ switch config.projectFlags {
681
+ | "" => command
682
+ | flags => `${command} ${flags}`
683
+ }
684
+
685
+ let ecosystemFields = ["evm", "fuel", "svm"]
686
+
687
+ // What `envio_info` records: the public config without the command's fields
688
+ // or its chains. A chain is recorded by its own `envio_chains` row and its
689
+ // config's `envio_addresses` rows instead, so adding one never rewrites this
690
+ // record. The rest of a chain's config — its sources, its block lag, a
691
+ // contract's start block — is read from config.yaml on every start.
692
+ let toEnvioInfo = (publicConfigJson: JSON.t): JSON.t => {
693
+ let envioInfo = publicConfigJson->JSON.stringify->JSON.parseOrThrow
694
+ switch envioInfo {
695
+ | Object(obj) => {
696
+ commandFields->Array.forEach(field => obj->Utils.Dict.deleteInPlace(field))
697
+ ecosystemFields->Array.forEach(ecosystem =>
698
+ switch obj->Dict.get(ecosystem) {
699
+ | Some(Object(ecosystemDict)) => ecosystemDict->Utils.Dict.deleteInPlace("chains")
700
+ | _ => ()
701
+ }
702
+ )
703
+ }
704
+ | _ => ()
705
+ }
706
+ envioInfo
707
+ }
708
+
669
709
  let fromPublic = (publicConfigJson: JSON.t) => {
670
710
  let maxAddrInPartition = Env.maxAddrInPartition
671
711
  // Parse public config
@@ -1136,11 +1176,13 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1136
1176
  reorgThresholdReadyTolerance: 100,
1137
1177
  lowercaseAddresses,
1138
1178
  isDev: publicConfig["isDev"]->Option.getOr(false),
1179
+ projectFlags: publicConfig["projectFlags"]->Option.getOr(""),
1139
1180
  isolated: false,
1140
1181
  userEntitiesByName,
1141
1182
  userEntities,
1142
1183
  allEnums,
1143
1184
  checkpointSequence: CheckpointSequence.fromEntities(userEntities),
1185
+ envioInfo: publicConfigJson->toEnvioInfo,
1144
1186
  }
1145
1187
 
1146
1188
  switch publicConfig["isolatedChains"] {
@@ -1255,49 +1297,6 @@ let getPublicConfigJson = () =>
1255
1297
  | None => Core.getConfigJson()->JSON.parseOrThrow
1256
1298
  }
1257
1299
 
1258
- // Drops source URLs from each chain so RPC/hypersync edits don't trigger
1259
- // the resume-time compat check (and don't end up in `envio_info`). Also
1260
- // drops `isDev`, which toggles between `envio dev` and `envio start` and
1261
- // has no bearing on schema/indexing compatibility.
1262
- let stripSensitiveData = (json: JSON.t): JSON.t => {
1263
- let cloned = json->JSON.stringify->JSON.parseOrThrow
1264
- let stripChains = (ecosystem: option<JSON.t>) =>
1265
- switch ecosystem {
1266
- | Some(Object(ecosystemDict)) =>
1267
- switch ecosystemDict->Dict.get("chains") {
1268
- | Some(Object(chains)) =>
1269
- chains
1270
- ->Dict.valuesToArray
1271
- ->Array.forEach(chainJson =>
1272
- switch chainJson {
1273
- | Object(chain) => {
1274
- chain->Utils.Dict.deleteInPlace("rpcs")
1275
- chain->Utils.Dict.deleteInPlace("hypersync")
1276
- }
1277
- | _ => ()
1278
- }
1279
- )
1280
- | _ => ()
1281
- }
1282
- | _ => ()
1283
- }
1284
- switch cloned {
1285
- | Object(obj) => {
1286
- obj->Utils.Dict.deleteInPlace("isDev")
1287
- obj->Utils.Dict.deleteInPlace("isolatedChains")
1288
- stripChains(obj->Dict.get("evm"))
1289
- stripChains(obj->Dict.get("fuel"))
1290
- stripChains(obj->Dict.get("svm"))
1291
- }
1292
- | _ => ()
1293
- }
1294
- cloned
1295
- }
1296
-
1297
- // What the storage layer records as the config this schema was built from,
1298
- // and checks a resuming run against.
1299
- let envioInfo = () => getPublicConfigJson()->stripSensitiveData
1300
-
1301
1300
  // Postgres jsonb doesn't preserve key order, so canonicalize with sorted
1302
1301
  // keys before string-comparing.
1303
1302
  let rec canonicalJson = (json: JSON.t): JSON.t =>
@@ -1317,9 +1316,10 @@ let rec canonicalJson = (json: JSON.t): JSON.t =>
1317
1316
  // Returns dotted leaf paths (`a.b[i].c`) where `stored` differs from
1318
1317
  // `current`, restricted to the highest-priority top-level tier with any
1319
1318
  // diff. Tiers in order: version → name → storage → ecosystem
1320
- // (evm/fuel/svm) → entities → other top-level keys. The first tier
1321
- // containing a diff is the only one rendered; lower tiers are silenced
1322
- // so a single noisy section doesn't bury the actionable change.
1319
+ // (evm/fuel/svm, and the chains a resume puts beside them) → entities →
1320
+ // other top-level keys. The first tier containing a diff is the only one
1321
+ // rendered; lower tiers are silenced so a single noisy section doesn't bury
1322
+ // the actionable change.
1323
1323
  let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
1324
1324
  let canonEq = (a: JSON.t, b: JSON.t) =>
1325
1325
  JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b))
@@ -1387,7 +1387,7 @@ let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
1387
1387
  ["chainIdMode"],
1388
1388
  ["name"],
1389
1389
  ["storage"],
1390
- ["evm", "fuel", "svm"],
1390
+ ["evm", "fuel", "svm", "chains"],
1391
1391
  ["entities"],
1392
1392
  ]
1393
1393
  let firstHit = tiers->Array.reduce(None, (acc, tier) =>
@@ -1417,65 +1417,34 @@ let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
1417
1417
  acc
1418
1418
  }
1419
1419
 
1420
- // Throws an `incompatible config` error listing each path in `changedPaths`,
1421
- // plus the remediation options. `~resetCommand` is rendered as-is for
1422
- // option 2 (the wipe-and-redo). `~runCommand` controls option 3 (parallel
1423
- // indexer recipe): when `None`, option 3 is omitted — the migrate flow
1424
- // uses this because running a second indexer doesn't apply.
1425
- // `~hasClickhouse` adds the extra env line so users running both
1426
- // Postgres and Clickhouse get a complete override.
1427
- let throwIfIncompatible = (
1420
+ // The `incompatible config` error listing each path in `changedPaths`, plus
1421
+ // the remediation options. `~resetCommand` is rendered as-is for option 2
1422
+ // (the wipe-and-redo). `~runCommand` controls option 3 (parallel indexer
1423
+ // recipe): when `None`, option 3 is omitted — the migrate flow uses this
1424
+ // because running a second indexer doesn't apply. `~hasClickhouse` adds the
1425
+ // extra env line so users running both Postgres and Clickhouse get a complete
1426
+ // override.
1427
+ let incompatibleMessage = (
1428
1428
  changedPaths: array<string>,
1429
1429
  ~resetCommand: string,
1430
1430
  ~runCommand: option<string>,
1431
1431
  ~hasClickhouse: bool,
1432
1432
  ) => {
1433
- if changedPaths->Array.length > 0 {
1434
- let bullets = changedPaths->Array.map(p => ` - ${p}`)->Array.joinUnsafe("\n")
1435
- let option1 = "Revert the changes above"
1436
- let padTo = (s, col) => s ++ " "->String.repeat(Math.Int.max(col - String.length(s), 1))
1437
- let col = Math.Int.max(String.length(option1), String.length(resetCommand)) + 2
1438
- let option3 = switch runCommand {
1439
- | None => ""
1440
- | Some(cmd) =>
1441
- let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : ""
1442
- `\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}`
1443
- }
1444
- JsError.throwWithMessage(
1445
- `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n 1. ${option1->padTo(
1446
- col,
1447
- )}# resume indexing where it left off\n 2. ${resetCommand->padTo(
1448
- col,
1449
- )}# delete all indexed data and start over${option3}`,
1450
- )
1451
- }
1452
- }
1453
-
1454
- let throwIfResumeIncompatible = (
1455
- ~storedEnvioInfo: option<JSON.t>,
1456
- ~storedContractMapping: ContractMapping.t,
1457
- ~envioInfo: JSON.t,
1458
- ~contractMapping: ContractMapping.t,
1459
- ~resetCommand: string,
1460
- ~runCommand: option<string>,
1461
- ) => {
1462
- let changedPaths = switch storedEnvioInfo {
1463
- | None => ["storage was initialized by an older envio version"]
1464
- | Some(stored) => diffPaths(~stored, ~current=envioInfo)
1465
- }
1466
- let changedPaths =
1467
- storedContractMapping->ContractMapping.isEqual(contractMapping)
1468
- ? changedPaths
1469
- : changedPaths->Array.concat(["contracts"])
1470
- let hasClickhouse = switch envioInfo {
1471
- | Object(d) =>
1472
- switch d->Dict.get("storage") {
1473
- | Some(Object(s)) => s->Dict.get("clickhouse") == Some(Boolean(true))
1474
- | _ => false
1475
- }
1476
- | _ => false
1433
+ let bullets = changedPaths->Array.map(p => ` - ${p}`)->Array.joinUnsafe("\n")
1434
+ let option1 = "Revert the changes above"
1435
+ let padTo = (s, col) => s ++ " "->String.repeat(Math.Int.max(col - String.length(s), 1))
1436
+ let col = Math.Int.max(String.length(option1), String.length(resetCommand)) + 2
1437
+ let option3 = switch runCommand {
1438
+ | None => ""
1439
+ | Some(cmd) =>
1440
+ let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : ""
1441
+ `\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}`
1477
1442
  }
1478
- throwIfIncompatible(changedPaths, ~resetCommand, ~runCommand, ~hasClickhouse)
1443
+ `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n 1. ${option1->padTo(
1444
+ col,
1445
+ )}# resume indexing where it left off\n 2. ${resetCommand->padTo(
1446
+ col,
1447
+ )}# delete all indexed data and start over${option3}`
1479
1448
  }
1480
1449
 
1481
1450
  // The returned value is a pure function of the JSON: it holds only event
@@ -18,7 +18,6 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
18
18
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
19
19
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
20
20
  import * as ContractMapping from "./ContractMapping.res.mjs";
21
- import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
22
21
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
23
22
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
24
23
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
@@ -439,6 +438,7 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
439
438
  handlers: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
440
439
  isDev: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
441
440
  isolatedChains: s.m(S$RescriptSchema.option(S$RescriptSchema.array(ChainId.schema))),
441
+ projectFlags: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
442
442
  fullBatchSize: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
443
443
  rollbackOnReorg: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
444
444
  saveFullHistory: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
@@ -495,6 +495,41 @@ function isolate(config, chainIds) {
495
495
  return newrecord;
496
496
  }
497
497
 
498
+ let commandFields = [
499
+ "isDev",
500
+ "isolatedChains",
501
+ "projectFlags"
502
+ ];
503
+
504
+ function withProjectFlags(command, config) {
505
+ let flags = config.projectFlags;
506
+ if (flags === "") {
507
+ return command;
508
+ } else {
509
+ return command + ` ` + flags;
510
+ }
511
+ }
512
+
513
+ let ecosystemFields = [
514
+ "evm",
515
+ "fuel",
516
+ "svm"
517
+ ];
518
+
519
+ function toEnvioInfo(publicConfigJson) {
520
+ let envioInfo = JSON.parse(JSON.stringify(publicConfigJson));
521
+ if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
522
+ commandFields.forEach(field => Utils.Dict.deleteInPlace(envioInfo, field));
523
+ ecosystemFields.forEach(ecosystem => {
524
+ let match = envioInfo[ecosystem];
525
+ if (typeof match === "object" && match !== null && !Array.isArray(match)) {
526
+ return Utils.Dict.deleteInPlace(match, "chains");
527
+ }
528
+ });
529
+ }
530
+ return envioInfo;
531
+ }
532
+
498
533
  function fromPublic(publicConfigJson) {
499
534
  let publicConfig;
500
535
  try {
@@ -801,7 +836,9 @@ function fromPublic(publicConfigJson) {
801
836
  let config_enableRawEvents = Stdlib_Option.getOr(publicConfig.rawEvents, false);
802
837
  let config_batchSize = Stdlib_Option.getOr(publicConfig.fullBatchSize, 5000);
803
838
  let config_isDev = Stdlib_Option.getOr(publicConfig.isDev, false);
839
+ let config_projectFlags = Stdlib_Option.getOr(publicConfig.projectFlags, "");
804
840
  let config_checkpointSequence = CheckpointSequence.fromEntities(userEntities);
841
+ let config_envioInfo = toEnvioInfo(publicConfigJson);
805
842
  let config = {
806
843
  name: config_name,
807
844
  description: config_description,
@@ -823,11 +860,13 @@ function fromPublic(publicConfigJson) {
823
860
  reorgThresholdReadyTolerance: 100,
824
861
  lowercaseAddresses: lowercaseAddresses,
825
862
  isDev: config_isDev,
863
+ projectFlags: config_projectFlags,
826
864
  isolated: false,
827
865
  userEntitiesByName: userEntitiesByName,
828
866
  userEntities: userEntities,
829
867
  allEnums: allEnums,
830
- checkpointSequence: config_checkpointSequence
868
+ checkpointSequence: config_checkpointSequence,
869
+ envioInfo: config_envioInfo
831
870
  };
832
871
  let chainIds = publicConfig.isolatedChains;
833
872
  if (chainIds !== undefined) {
@@ -933,44 +972,6 @@ function getPublicConfigJson() {
933
972
  }
934
973
  }
935
974
 
936
- function stripSensitiveData(json) {
937
- let cloned = JSON.parse(JSON.stringify(json));
938
- let stripChains = ecosystem => {
939
- if (ecosystem === undefined) {
940
- return;
941
- }
942
- if (typeof ecosystem !== "object" || ecosystem === null || Array.isArray(ecosystem)) {
943
- return;
944
- }
945
- let match = ecosystem["chains"];
946
- if (match === undefined) {
947
- return;
948
- }
949
- if (typeof match !== "object" || match === null || Array.isArray(match)) {
950
- return;
951
- }
952
- Object.values(match).forEach(chainJson => {
953
- if (typeof chainJson !== "object" || chainJson === null || Array.isArray(chainJson)) {
954
- return;
955
- }
956
- Utils.Dict.deleteInPlace(chainJson, "rpcs");
957
- Utils.Dict.deleteInPlace(chainJson, "hypersync");
958
- });
959
- };
960
- if (typeof cloned === "object" && cloned !== null && !Array.isArray(cloned)) {
961
- Utils.Dict.deleteInPlace(cloned, "isDev");
962
- Utils.Dict.deleteInPlace(cloned, "isolatedChains");
963
- stripChains(cloned["evm"]);
964
- stripChains(cloned["fuel"]);
965
- stripChains(cloned["svm"]);
966
- }
967
- return cloned;
968
- }
969
-
970
- function envioInfo() {
971
- return stripSensitiveData(getPublicConfigJson());
972
- }
973
-
974
975
  function canonicalJson(json) {
975
976
  if (Array.isArray(json)) {
976
977
  return json.map(canonicalJson);
@@ -1088,7 +1089,8 @@ function diffPaths(stored, current) {
1088
1089
  [
1089
1090
  "evm",
1090
1091
  "fuel",
1091
- "svm"
1092
+ "svm",
1093
+ "chains"
1092
1094
  ],
1093
1095
  ["entities"]
1094
1096
  ];
@@ -1113,10 +1115,7 @@ function diffPaths(stored, current) {
1113
1115
  return acc;
1114
1116
  }
1115
1117
 
1116
- function throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse) {
1117
- if (changedPaths.length === 0) {
1118
- return;
1119
- }
1118
+ function incompatibleMessage(changedPaths, resetCommand, runCommand, hasClickhouse) {
1120
1119
  let bullets = changedPaths.map(p => ` - ` + p).join("\n");
1121
1120
  let option1 = "Revert the changes above";
1122
1121
  let padTo = (s, col) => s + " ".repeat(Math.max(col - s.length | 0, 1));
@@ -1128,20 +1127,7 @@ function throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhou
1128
1127
  } else {
1129
1128
  option3 = "";
1130
1129
  }
1131
- 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);
1132
- }
1133
-
1134
- function throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand) {
1135
- let changedPaths = storedEnvioInfo !== undefined ? diffPaths(storedEnvioInfo, envioInfo) : ["storage was initialized by an older envio version"];
1136
- let changedPaths$1 = ContractMapping.isEqual(storedContractMapping, contractMapping) ? changedPaths : changedPaths.concat(["contracts"]);
1137
- let hasClickhouse;
1138
- if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
1139
- let match = envioInfo["storage"];
1140
- hasClickhouse = typeof match === "object" && match !== null && !Array.isArray(match) ? Primitive_object.equal(match["clickhouse"], true) : false;
1141
- } else {
1142
- hasClickhouse = false;
1143
- }
1144
- throwIfIncompatible(changedPaths$1, resetCommand, runCommand, hasClickhouse);
1130
+ return `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;
1145
1131
  }
1146
1132
 
1147
1133
  function load() {
@@ -1195,6 +1181,10 @@ export {
1195
1181
  getChain,
1196
1182
  isPerChain,
1197
1183
  isolate,
1184
+ commandFields,
1185
+ withProjectFlags,
1186
+ ecosystemFields,
1187
+ toEnvioInfo,
1198
1188
  fromPublic,
1199
1189
  normalizeUserAddress,
1200
1190
  normalizeSimulateAddress,
@@ -1202,12 +1192,9 @@ export {
1202
1192
  prime,
1203
1193
  withCommandFields,
1204
1194
  getPublicConfigJson,
1205
- stripSensitiveData,
1206
- envioInfo,
1207
1195
  canonicalJson,
1208
1196
  diffPaths,
1209
- throwIfIncompatible,
1210
- throwIfResumeIncompatible,
1197
+ incompatibleMessage,
1211
1198
  load,
1212
1199
  getPgUserEntities,
1213
1200
  }
package/src/Core.res CHANGED
@@ -12,6 +12,7 @@ type transactionStoreCtor
12
12
  type blockStoreCtor
13
13
  type clickHouseSinkCtor
14
14
  type addressStoreCtor
15
+ type tuiCtor
15
16
  // Test-only: a local HyperSync server, bound by MockHyperSyncServer in envio-tests.
16
17
  type mockHyperSyncServerCtor
17
18
  type fromUserApiOptions = {
@@ -33,6 +34,10 @@ type addon = {
33
34
  isSvmPubkey: (~value: string) => bool,
34
35
  fromUserApi: (string, fromUserApiOptions) => fromUserApiResult,
35
36
  runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
37
+ loadTs: string => string,
38
+ tsCheckHandlerFormat: string => unit,
39
+ tsResolveCandidates: (string, Null.t<string>) => array<string>,
40
+ tsNotFoundCandidates: (string, Null.t<string>, string) => array<string>,
36
41
  @as("EvmHyperSyncClient")
37
42
  evmHyperSyncClient: evmHyperSyncClientCtor,
38
43
  @as("EvmRpcClient")
@@ -49,6 +54,8 @@ type addon = {
49
54
  addressStore: addressStoreCtor,
50
55
  @as("ClickHouseSink")
51
56
  clickHouseSink: clickHouseSinkCtor,
57
+ @as("Tui")
58
+ tui: tuiCtor,
52
59
  @as("MockHyperSyncServer")
53
60
  mockHyperSyncServer: mockHyperSyncServerCtor,
54
61
  encodeAddresses: (~ecosystem: string, ~addresses: array<Address.t>) => array<NodeJs.Buffer.t>,
@@ -99,7 +106,7 @@ let callRequire: ({..}, string) => addon = %raw(`(req, id) => req(id)`)
99
106
  let envioPackageDir = pathDirname(pathDirname(fileURLToPath(importMetaUrl)))
100
107
 
101
108
  // Runs `cargo build` on every invocation (like `cargo run`).
102
- let loadDevAddon: ({..}, string) => addon = %raw(`function(req, envioDir) {
109
+ let loadDevAddon: ({..}, string) => Null.t<addon> = %raw(`function(req, envioDir) {
103
110
  var cp = Nodechild_process;
104
111
  var path = Nodepath;
105
112
  var fs = Nodefs;
@@ -177,6 +184,22 @@ let loadDevAddon: ({..}, string) => addon = %raw(`function(req, envioDir) {
177
184
  // `code`, and any other fields a diagnostic might rely on.
178
185
  let rethrow: JsExn.t => 'a = %raw(`function(e) { throw e }`)
179
186
 
187
+ type packageJson = {version: string}
188
+ let requirePackageJson: ({..}, string) => packageJson = %raw(`(req, p) => req(p)`)
189
+ let devVersion = "0.0.1-dev"
190
+
191
+ let isGlibc: unit => bool = %raw(`() => Boolean(process.report?.getReport().header.glibcVersionRuntime)`)
192
+
193
+ @val external npmUserAgent: option<string> = "process.env.npm_config_user_agent"
194
+
195
+ let addCommand = () =>
196
+ switch npmUserAgent {
197
+ | Some(ua) if ua->String.startsWith("pnpm/") => "pnpm add"
198
+ | Some(ua) if ua->String.startsWith("yarn/") => "yarn add"
199
+ | Some(ua) if ua->String.startsWith("bun/") => "bun add"
200
+ | _ => "npm install"
201
+ }
202
+
180
203
  let loadAddon = () => {
181
204
  let req = createRequire(importMetaUrl)
182
205
 
@@ -212,15 +235,32 @@ let loadAddon = () => {
212
235
  switch tryRequire(0) {
213
236
  | Some(addon) => addon
214
237
  | None =>
215
- // Dev build fallback (cargo build on every run)
216
- switch loadDevAddon(req, envioPackageDir)->(Utils.magic: addon => option<addon>) {
238
+ let version = requirePackageJson(req, "../package.json").version
239
+ // Publishing stamps the real version, so only a monorepo checkout (or a
240
+ // `file:` link to one) can have a dev build to fall back to. Skipping it
241
+ // spares published installs a `pnpm list` spawn before the error below.
242
+ let devAddon = if version === devVersion {
243
+ loadDevAddon(req, envioPackageDir)->Null.toOption
244
+ } else {
245
+ None
246
+ }
247
+ switch devAddon {
217
248
  | Some(addon) => addon
218
249
  | None =>
219
250
  let host = `${processPlatform}-${processArch}`
220
- let msg = if candidates->Array.length === 0 {
251
+ let msg = switch candidates {
252
+ | []
253
+ if processPlatform === "win32" => `envio doesn't run natively on Windows. Use WSL 2 instead: https://learn.microsoft.com/windows/wsl/install`
254
+ | [] =>
221
255
  `envio doesn't support ${host}. Supported: linux-x64 (glibc/musl), linux-arm64, darwin-x64, darwin-arm64.`
222
- } else {
223
- `Couldn't load the envio native addon for ${host}. Reinstall envio (ensure optional dependencies aren't skipped).`
256
+ | _ =>
257
+ let pkg = if processPlatform === "linux" && processArch === "x64" && !isGlibc() {
258
+ `envio-linux-x64-musl`
259
+ } else {
260
+ candidates->Array.getUnsafe(0)
261
+ }
262
+ `envio's native binary for ${host} isn't installed (package "${pkg}"). This happens when optional dependencies are skipped (--omit=optional, --no-optional) or the lockfile was generated on another platform.
263
+ Reinstall dependencies, or add the package explicitly: ${addCommand()} ${pkg}@${version}`
224
264
  }
225
265
  JsError.throwWithMessage(msg)
226
266
  }