envio 3.5.1 → 3.6.1-subgraph

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 (84) hide show
  1. package/evm.schema.json +7 -0
  2. package/fuel.schema.json +7 -0
  3. package/index.d.ts +33 -10
  4. package/package.json +6 -6
  5. package/src/ChainState.res +22 -4
  6. package/src/ChainState.res.mjs +20 -3
  7. package/src/ChainState.resi +3 -0
  8. package/src/Config.res +72 -2
  9. package/src/Config.res.mjs +57 -15
  10. package/src/Core.res +15 -0
  11. package/src/Core.res.mjs +11 -0
  12. package/src/EffectState.res +2 -2
  13. package/src/EffectState.res.mjs +2 -2
  14. package/src/EntityTables.res +32 -0
  15. package/src/EntityTables.res.mjs +44 -0
  16. package/src/Envio.res +7 -7
  17. package/src/Envio.res.mjs +5 -6
  18. package/src/EventProcessing.res +6 -6
  19. package/src/EventProcessing.res.mjs +8 -6
  20. package/src/HandlerLoader.res +20 -8
  21. package/src/HandlerLoader.res.mjs +11 -2
  22. package/src/Hasura.res +37 -3
  23. package/src/Hasura.res.mjs +23 -5
  24. package/src/InMemoryStore.res +48 -8
  25. package/src/InMemoryStore.res.mjs +37 -7
  26. package/src/IndexerState.res +30 -24
  27. package/src/IndexerState.res.mjs +20 -35
  28. package/src/IndexerState.resi +8 -6
  29. package/src/Internal.res +29 -11
  30. package/src/Internal.res.mjs +27 -10
  31. package/src/LoadLayer.res +39 -8
  32. package/src/LoadLayer.res.mjs +36 -9
  33. package/src/LoadLayer.resi +2 -0
  34. package/src/Metrics.res +5 -5
  35. package/src/Metrics.res.mjs +5 -1
  36. package/src/Persistence.res +12 -1
  37. package/src/PgStorage.res +183 -37
  38. package/src/PgStorage.res.mjs +149 -34
  39. package/src/PruneStaleHistory.res +1 -0
  40. package/src/PruneStaleHistory.res.mjs +2 -1
  41. package/src/Sink.res +3 -3
  42. package/src/Sink.res.mjs +2 -2
  43. package/src/TestIndexer.res +128 -17
  44. package/src/TestIndexer.res.mjs +75 -9
  45. package/src/UserContext.res +375 -53
  46. package/src/UserContext.res.mjs +281 -39
  47. package/src/Writing.res +8 -16
  48. package/src/Writing.res.mjs +10 -10
  49. package/src/bindings/ClickHouse.res +40 -4
  50. package/src/bindings/ClickHouse.res.mjs +37 -5
  51. package/src/db/EntityHistory.res +64 -21
  52. package/src/db/EntityHistory.res.mjs +43 -22
  53. package/src/db/InternalTable.res.mjs +31 -31
  54. package/src/db/Table.res +24 -1
  55. package/src/db/Table.res.mjs +26 -4
  56. package/src/sources/SourceManager.res +6 -12
  57. package/src/sources/SourceManager.res.mjs +8 -5
  58. package/src/subgraph/blocks.ts +176 -0
  59. package/src/subgraph/calls.ts +213 -0
  60. package/src/subgraph/conformance.ts +99 -0
  61. package/src/subgraph/division.ts +100 -0
  62. package/src/subgraph/errors.ts +90 -0
  63. package/src/subgraph/graph-ts-types/VERSION +2 -0
  64. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  65. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  66. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  67. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  68. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  69. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  70. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  71. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  72. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  73. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  74. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  75. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  76. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  77. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  78. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  79. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  80. package/src/subgraph/graph-ts.ts +1695 -0
  81. package/src/subgraph/hosts.ts +148 -0
  82. package/src/subgraph/runtime.ts +827 -0
  83. package/src/subgraph/scope.ts +63 -0
  84. package/svm.schema.json +7 -0
package/evm.schema.json CHANGED
@@ -49,6 +49,13 @@
49
49
  }
50
50
  ]
51
51
  },
52
+ "disable_default_cross_chain": {
53
+ "description": "Make entities and effect caches per-chain instead of shared across every chain (recommended). Sharing then becomes explicit — add `@crossChain` to an entity in schema.graphql or `crossChain: true` to an effect. (default: false)",
54
+ "type": [
55
+ "boolean",
56
+ "null"
57
+ ]
58
+ },
52
59
  "ecosystem": {
53
60
  "description": "Ecosystem of the project.",
54
61
  "anyOf": [
package/fuel.schema.json CHANGED
@@ -49,6 +49,13 @@
49
49
  }
50
50
  ]
51
51
  },
52
+ "disable_default_cross_chain": {
53
+ "description": "Make entities and effect caches per-chain instead of shared across every chain (recommended). Sharing then becomes explicit — add `@crossChain` to an entity in schema.graphql or `crossChain: true` to an effect. (default: false)",
54
+ "type": [
55
+ "boolean",
56
+ "null"
57
+ ]
58
+ },
52
59
  "ecosystem": {
53
60
  "description": "Ecosystem of the project.",
54
61
  "$ref": "#/$defs/EcosystemTag"
package/index.d.ts CHANGED
@@ -48,8 +48,7 @@ export type EffectCaller = <I, O>(
48
48
  input: I extends undefined ? undefined : I
49
49
  ) => Promise<O>;
50
50
 
51
- /** The chain an Effect was called on. Available only on chain-scoped effects
52
- * (`crossChain: false`). */
51
+ /** The chain an Effect was called on. Available only on chain-scoped effects. */
53
52
  export type EffectChain = {
54
53
  /** The chain id the effect handler was called on. */
55
54
  readonly id: number;
@@ -64,8 +63,8 @@ export type EffectContext = {
64
63
  /** Whether to cache this call's result. Defaults to the effect's `cache`
65
64
  * option; set to `false` to skip caching for this specific invocation. */
66
65
  cache: boolean;
67
- /** The chain the effect was called on. Only available on effects created with
68
- * `crossChain: false`; accessing it on a cross-chain effect throws. */
66
+ /** The chain the effect was called on. Only available on chain-scoped
67
+ * effects; accessing it on a cross-chain effect throws. */
69
68
  readonly chain: EffectChain;
70
69
  };
71
70
 
@@ -93,9 +92,10 @@ export type EffectOptions<Input, Output> = {
93
92
  /** Whether the effect should be cached. */
94
93
  readonly cache?: boolean;
95
94
  /** Whether the effect's cache is shared across all chains. Defaults to
96
- * `true`. Set to `false` to isolate the cache and rate limiting per chain and
97
- * enable `context.chain.id` inside the handler. Changing this changes the
98
- * effect's cache identity. */
95
+ * `true`, or to `false` when config.yaml sets
96
+ * `disable_default_cross_chain: true`. Set to `false` to isolate the cache
97
+ * and rate limiting per chain and enable `context.chain.id` inside the
98
+ * handler. Changing this changes the effect's cache identity. */
99
99
  readonly crossChain?: boolean;
100
100
  };
101
101
 
@@ -225,8 +225,10 @@ export function createEffect<
225
225
  /** Whether the effect should be cached. */
226
226
  readonly cache?: boolean;
227
227
  /** Whether the effect's cache is shared across all chains. Defaults to
228
- * `true`. Set to `false` to isolate the cache and rate limiting per chain
229
- * and enable `context.chain.id` inside the handler. */
228
+ * `true`, or to `false` when config.yaml sets
229
+ * `disable_default_cross_chain: true`. Set to `false` to isolate the cache
230
+ * and rate limiting per chain and enable `context.chain.id` inside the
231
+ * handler. */
230
232
  readonly crossChain?: boolean;
231
233
  },
232
234
  handler: (args: EffectArgs<I>) => Promise<R>
@@ -502,6 +504,8 @@ type IndexerConfigTypes = {
502
504
  };
503
505
  svm?: { chains: Record<string, { id: number }> };
504
506
  entities?: Record<string, object>;
507
+ // Union of the entity names whose rows belong to a single chain, or `never`.
508
+ perChainEntities?: string;
505
509
  enums?: Record<string, string>;
506
510
  };
507
511
 
@@ -1529,6 +1533,21 @@ type AddressRegistration = {
1529
1533
  type ConfigEntities<Config extends IndexerConfigTypes = GlobalConfig> =
1530
1534
  Config["entities"] extends Record<string, object> ? Config["entities"] : {};
1531
1535
 
1536
+ /** Entity names whose rows belong to a single chain. */
1537
+ type PerChainEntityNames<Config extends IndexerConfigTypes = GlobalConfig> =
1538
+ Config extends { perChainEntities: infer Names extends string } ? Names : never;
1539
+
1540
+ /** The row shape the chain-agnostic test-indexer operations exchange. A
1541
+ * per-chain entity's row is only identified together with its chain, so the
1542
+ * chain id travels alongside the entity fields. */
1543
+ type TestIndexerEntityRow<
1544
+ Config extends IndexerConfigTypes,
1545
+ Name,
1546
+ Entity
1547
+ > = Name extends PerChainEntityNames<Config>
1548
+ ? Entity & { readonly chainId: number }
1549
+ : Entity;
1550
+
1532
1551
  /** Entity operations available on test indexer for direct entity manipulation. */
1533
1552
  type TestIndexerEntityOperations<Entity> = {
1534
1553
  /** Get an entity by ID. Returns undefined if not found. */
@@ -1537,6 +1556,10 @@ type TestIndexerEntityOperations<Entity> = {
1537
1556
  readonly getOrThrow: (id: EntityId<Entity>, message?: string) => Promise<Entity>;
1538
1557
  /** Get all entities. */
1539
1558
  readonly getAll: () => Promise<Entity[]>;
1559
+ /** Get the entities matching a filter. For a per-chain entity the filter
1560
+ * accepts `chainId`, which is how an id present on several chains is
1561
+ * narrowed to one. */
1562
+ readonly getWhere: (filter: GetWhereFilter<Entity>) => Promise<Entity[]>;
1540
1563
  /** Set (create or update) an entity. */
1541
1564
  readonly set: (entity: Entity) => void;
1542
1565
  };
@@ -1711,7 +1734,7 @@ export type TestIndexerFromConfig<Config extends IndexerConfigTypes = GlobalConf
1711
1734
  } & SingleEcosystemTestChains<Config> & {
1712
1735
  /** Entity operations for direct manipulation outside of handlers. */
1713
1736
  readonly [K in keyof ConfigEntities<Config>]: TestIndexerEntityOperations<
1714
- ConfigEntities<Config>[K]
1737
+ TestIndexerEntityRow<Config, K, ConfigEntities<Config>[K]>
1715
1738
  >;
1716
1739
  };
1717
1740
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.5.1",
3
+ "version": "3.6.1-subgraph",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -69,10 +69,10 @@
69
69
  "tsx": "4.21.0"
70
70
  },
71
71
  "optionalDependencies": {
72
- "envio-linux-x64": "3.5.1",
73
- "envio-linux-x64-musl": "3.5.1",
74
- "envio-linux-arm64": "3.5.1",
75
- "envio-darwin-x64": "3.5.1",
76
- "envio-darwin-arm64": "3.5.1"
72
+ "envio-linux-x64": "3.6.1-subgraph",
73
+ "envio-linux-x64-musl": "3.6.1-subgraph",
74
+ "envio-linux-arm64": "3.6.1-subgraph",
75
+ "envio-darwin-x64": "3.6.1-subgraph",
76
+ "envio-darwin-arm64": "3.6.1-subgraph"
77
77
  }
78
78
  }
@@ -31,6 +31,10 @@ type t = {
31
31
  // then smoothed with an EMA on every batch (see applyBatchProgress). None
32
32
  // until the chain has processed at least one event.
33
33
  mutable chainDensity: option<float>,
34
+ // In-memory tables for the entities whose rows belong to a single chain.
35
+ // Empty in the default cross-chain mode, where every entity's table lives on
36
+ // the indexer instead.
37
+ mutable entities: EntityTables.t,
34
38
  mutable reorgDetection: ReorgDetection.t,
35
39
  mutable safeCheckpointTracking: option<SafeCheckpointTracking.t>,
36
40
  // Holds this chain's transactions (kept in Rust) keyed by (blockNumber,
@@ -96,11 +100,13 @@ let make = (
96
100
  ~chainDensity=None,
97
101
  ~blockStore=BlockStore.make(~ecosystem=Ecosystem.Evm, ~shouldChecksum=false),
98
102
  ~reorgThresholdReadyTolerance=100,
103
+ ~perChainEntities: array<Internal.entityConfig>=[],
99
104
  ~logger: Pino.t,
100
105
  ): t => {
101
106
  validateOnEventRegistrations(~chainId=chainConfig.id, onEventRegistrations)
102
107
  {
103
108
  logger,
109
+ entities: EntityTables.make(perChainEntities),
104
110
  onEventRegistrations,
105
111
  fetchState,
106
112
  addressStore,
@@ -318,6 +324,7 @@ let makeInternal = (
318
324
  ~chainReorgCheckpoints,
319
325
  ),
320
326
  ~committedProgressBlockNumber=progressBlockNumber,
327
+ ~perChainEntities=config.allEntities->EntityTables.perChain,
321
328
  ~timestampCaughtUpToHeadOrEndblock,
322
329
  ~numEventsProcessed,
323
330
  ~transactionStore=TransactionStore.make(
@@ -408,6 +415,11 @@ let makeFromDbState = (
408
415
  // --- Read accessors. ---
409
416
 
410
417
  let logger = (cs: t) => cs.logger
418
+ let entities = (cs: t) => cs.entities
419
+
420
+ // Rollback discards every uncommitted change, so this chain's partition is
421
+ // rebuilt from scratch alongside the indexer's cross-chain one.
422
+ let resetEntities = (cs: t, ~perChainEntities) => cs.entities = EntityTables.make(perChainEntities)
411
423
  let sourceManager = (cs: t) => cs.sourceManager
412
424
  let chainConfig = (cs: t) => cs.chainConfig
413
425
  let reorgDetection = (cs: t) => cs.reorgDetection
@@ -537,10 +549,16 @@ let targetBlock = (cs: t, ~chainTargetItems: float) => {
537
549
  let bufferBlockNumber = fetchState->FetchState.bufferBlockNumber
538
550
  switch cs->effectiveDensity {
539
551
  | Some(density) if density > 0. =>
540
- Pervasives.min(
541
- fetchCeiling,
542
- bufferBlockNumber + Math.ceil(chainTargetItems /. density)->Float.toInt,
543
- )
552
+ // Decided by comparison so no unbounded value is ever converted to int:
553
+ // at low densities chainTargetItems /. density exceeds the int range, and
554
+ // truncating it wraps negative — the target collapses below the frontier
555
+ // and the chain stops querying. The division only runs when its result is
556
+ // provably below the ceiling-bounded range.
557
+ if density *. (fetchCeiling - bufferBlockNumber)->Int.toFloat <= chainTargetItems {
558
+ fetchCeiling
559
+ } else {
560
+ bufferBlockNumber + Math.ceil(chainTargetItems /. density)->Float.toInt
561
+ }
544
562
  | _ => Pervasives.min(bufferBlockNumber + coldTargetRange, fetchCeiling)
545
563
  }
546
564
  }
@@ -13,6 +13,7 @@ import * as BlockStore from "./sources/BlockStore.res.mjs";
13
13
  import * as FetchState from "./FetchState.res.mjs";
14
14
  import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
15
15
  import * as AddressStore from "./sources/AddressStore.res.mjs";
16
+ import * as EntityTables from "./EntityTables.res.mjs";
16
17
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
17
18
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
18
19
  import * as SourceManager from "./sources/SourceManager.res.mjs";
@@ -49,7 +50,7 @@ function validateOnEventRegistrations(chainId, registrations) {
49
50
  });
50
51
  }
51
52
 
52
- function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, sourceManager, reorgDetection, committedProgressBlockNumber, safeCheckpointTrackingOpt, numEventsProcessedOpt, timestampCaughtUpToHeadOrEndblockOpt, isProgressAtHeadOpt, transactionStoreOpt, chainDensityOpt, blockStoreOpt, reorgThresholdReadyToleranceOpt, logger) {
53
+ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, sourceManager, reorgDetection, committedProgressBlockNumber, safeCheckpointTrackingOpt, numEventsProcessedOpt, timestampCaughtUpToHeadOrEndblockOpt, isProgressAtHeadOpt, transactionStoreOpt, chainDensityOpt, blockStoreOpt, reorgThresholdReadyToleranceOpt, perChainEntitiesOpt, logger) {
53
54
  let onEventRegistrations = onEventRegistrationsOpt !== undefined ? onEventRegistrationsOpt : [];
54
55
  let safeCheckpointTracking = safeCheckpointTrackingOpt !== undefined ? Primitive_option.valFromOption(safeCheckpointTrackingOpt) : undefined;
55
56
  let numEventsProcessed = numEventsProcessedOpt !== undefined ? numEventsProcessedOpt : 0;
@@ -59,6 +60,7 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
59
60
  let chainDensity = chainDensityOpt !== undefined ? Primitive_option.valFromOption(chainDensityOpt) : undefined;
60
61
  let blockStore = blockStoreOpt !== undefined ? Primitive_option.valFromOption(blockStoreOpt) : BlockStore.make("evm", false);
61
62
  let reorgThresholdReadyTolerance = reorgThresholdReadyToleranceOpt !== undefined ? reorgThresholdReadyToleranceOpt : 100;
63
+ let perChainEntities = perChainEntitiesOpt !== undefined ? perChainEntitiesOpt : [];
62
64
  validateOnEventRegistrations(chainConfig.id, onEventRegistrations);
63
65
  return {
64
66
  logger: logger,
@@ -74,6 +76,7 @@ function make(chainConfig, fetchState, onEventRegistrationsOpt, addressStore, so
74
76
  numEventsProcessed: numEventsProcessed,
75
77
  pendingBudget: 0,
76
78
  chainDensity: chainDensity,
79
+ entities: EntityTables.make(perChainEntities),
77
80
  reorgDetection: reorgDetection,
78
81
  safeCheckpointTracking: safeCheckpointTracking,
79
82
  transactionStore: transactionStore,
@@ -186,7 +189,7 @@ function makeInternal(chainConfig, indexingAddresses, startBlock, endBlock, firs
186
189
  }
187
190
  let firstEventBlock$1 = fetchState.firstEventBlock;
188
191
  let chainDensity = firstEventBlock$1 !== undefined && progressBlockNumber > firstEventBlock$1 && numEventsProcessed > 0 ? numEventsProcessed / (progressBlockNumber - firstEventBlock$1 | 0) : undefined;
189
- return make(chainConfig, fetchState, onEventRegistrations, addressStore, SourceManager.make(sources$1, isRealtime, undefined, undefined, undefined, reducedPollingInterval, undefined, undefined), ReorgDetection.make(chainReorgCheckpoints, maxReorgDepth, config.shouldRollbackOnReorg), progressBlockNumber, Primitive_option.some(SafeCheckpointTracking.make(maxReorgDepth, config.shouldRollbackOnReorg, chainReorgCheckpoints)), numEventsProcessed, Primitive_option.some(timestampCaughtUpToHeadOrEndblock), undefined, Primitive_option.some(TransactionStore.make(config.ecosystem.name, !lowercaseAddresses)), Primitive_option.some(chainDensity), Primitive_option.some(BlockStore.make(config.ecosystem.name, !lowercaseAddresses)), config.reorgThresholdReadyTolerance, logger);
192
+ return make(chainConfig, fetchState, onEventRegistrations, addressStore, SourceManager.make(sources$1, isRealtime, undefined, undefined, undefined, reducedPollingInterval, undefined, undefined), ReorgDetection.make(chainReorgCheckpoints, maxReorgDepth, config.shouldRollbackOnReorg), progressBlockNumber, Primitive_option.some(SafeCheckpointTracking.make(maxReorgDepth, config.shouldRollbackOnReorg, chainReorgCheckpoints)), numEventsProcessed, Primitive_option.some(timestampCaughtUpToHeadOrEndblock), undefined, Primitive_option.some(TransactionStore.make(config.ecosystem.name, !lowercaseAddresses)), Primitive_option.some(chainDensity), Primitive_option.some(BlockStore.make(config.ecosystem.name, !lowercaseAddresses)), config.reorgThresholdReadyTolerance, EntityTables.perChain(config.allEntities), logger);
190
193
  }
191
194
 
192
195
  function makeFromConfig(chainConfig, config, registrationsByChainId, knownHeight) {
@@ -209,6 +212,14 @@ function logger(cs) {
209
212
  return cs.logger;
210
213
  }
211
214
 
215
+ function entities(cs) {
216
+ return cs.entities;
217
+ }
218
+
219
+ function resetEntities(cs, perChainEntities) {
220
+ cs.entities = EntityTables.make(perChainEntities);
221
+ }
222
+
212
223
  function sourceManager(cs) {
213
224
  return cs.sourceManager;
214
225
  }
@@ -357,7 +368,11 @@ function targetBlock(cs, chainTargetItems) {
357
368
  let bufferBlockNumber = FetchState.bufferBlockNumber(fetchState);
358
369
  let density = effectiveDensity(cs);
359
370
  if (density !== undefined && density > 0) {
360
- return Primitive_int.min(fetchCeiling$1, bufferBlockNumber + (Math.ceil(chainTargetItems / density) | 0) | 0);
371
+ if (density * (fetchCeiling$1 - bufferBlockNumber | 0) <= chainTargetItems) {
372
+ return fetchCeiling$1;
373
+ } else {
374
+ return bufferBlockNumber + (Math.ceil(chainTargetItems / density) | 0) | 0;
375
+ }
361
376
  } else {
362
377
  return Primitive_int.min(bufferBlockNumber + 20000 | 0, fetchCeiling$1);
363
378
  }
@@ -841,6 +856,8 @@ export {
841
856
  makeFromConfig,
842
857
  makeFromDbState,
843
858
  logger,
859
+ entities,
860
+ resetEntities,
844
861
  sourceManager,
845
862
  chainConfig,
846
863
  reorgDetection,
@@ -21,6 +21,7 @@ let make: (
21
21
  ~chainDensity: option<float>=?,
22
22
  ~blockStore: BlockStore.t=?,
23
23
  ~reorgThresholdReadyTolerance: int=?,
24
+ ~perChainEntities: array<Internal.entityConfig>=?,
24
25
  ~logger: Pino.t,
25
26
  ) => t
26
27
 
@@ -44,6 +45,8 @@ let makeFromDbState: (
44
45
 
45
46
  // Accessors.
46
47
  let logger: t => Pino.t
48
+ let entities: t => EntityTables.t
49
+ let resetEntities: (t, ~perChainEntities: array<Internal.entityConfig>) => unit
47
50
  let sourceManager: t => SourceManager.t
48
51
  let chainConfig: t => Config.chain
49
52
  let reorgDetection: t => ReorgDetection.t
package/src/Config.res CHANGED
@@ -59,11 +59,26 @@ type sourceSync = {
59
59
  pollingInterval: int,
60
60
  }
61
61
 
62
+ // How a backend spells column names, mirroring `column_name_format` in
63
+ // config.yaml. Only the internal columns the runtime appends need it — user
64
+ // field names arrive pre-resolved from the CLI.
65
+ type columnNameFormat = | @as("original") Original | @as("snake_case") SnakeCase
66
+
62
67
  type storage = {
63
68
  postgres: bool,
64
69
  clickhouse: bool,
70
+ postgresColumnNameFormat: columnNameFormat,
71
+ clickhouseColumnNameFormat: columnNameFormat,
65
72
  }
66
73
 
74
+ let chainIdFieldName = "chainId"
75
+
76
+ let chainIdColumnName = format =>
77
+ switch format {
78
+ | Original => chainIdFieldName
79
+ | SnakeCase => "chain_id"
80
+ }
81
+
67
82
  type contractHandler = {
68
83
  name: string,
69
84
  handler: option<string>,
@@ -76,6 +91,10 @@ type t = {
76
91
  contractHandlers: array<contractHandler>,
77
92
  shouldRollbackOnReorg: bool,
78
93
  shouldSaveFullHistory: bool,
94
+ // False when `disable_default_cross_chain: true` in config.yaml. Entities
95
+ // carry their own resolved `crossChain`; this only decides the default for
96
+ // effects that don't state one.
97
+ defaultCrossChain: bool,
79
98
  storage: storage,
80
99
  // Widest scalar the internal chain-id columns need, resolved by the CLI from
81
100
  // the maximum active chain id. Older configs predate the field, and every id
@@ -100,6 +119,9 @@ type t = {
100
119
  userEntities: array<Internal.entityConfig>,
101
120
  allEntities: array<Internal.entityConfig>,
102
121
  allEnums: array<Table.enumConfig<Table.enum>>,
122
+ // Set only in subgraph mode: the translated manifest, which is what makes
123
+ // the subgraph runtime take over handler registration.
124
+ subgraph: option<JSON.t>,
103
125
  }
104
126
 
105
127
  module EnvioAddresses = {
@@ -161,6 +183,9 @@ module EnvioAddresses = {
161
183
  // always required to have Postgres enabled (Storage::resolve forbids
162
184
  // a Postgres-disabled global), so this is safe regardless of mode.
163
185
  storage: {postgres: true, clickhouse: false},
186
+ // The table already keys rows by chain through the composite `id`, so the
187
+ // per-chain mode must not append a second chain-id column to it.
188
+ crossChain: true,
164
189
  }->Internal.fromGenericEntityConfig
165
190
  }
166
191
 
@@ -353,6 +378,7 @@ let entityStorageSchema = S.schema(s =>
353
378
  let entityJsonSchema = S.schema(s =>
354
379
  {
355
380
  "name": s.matches(S.string),
381
+ "crossChain": s.matches(S.option(S.bool)),
356
382
  "storage": s.matches(S.option(entityStorageSchema)),
357
383
  "properties": s.matches(S.array(propertySchema)),
358
384
  "derivedFields": s.matches(S.option(S.array(derivedFieldSchema))),
@@ -424,13 +450,30 @@ let parseEnumsFromJson = (enumsJson: dict<array<string>>): array<Table.enumConfi
424
450
  )
425
451
  }
426
452
 
453
+ // The chain-id column appended to a per-chain entity's table. It completes the
454
+ // primary key so the same id can exist independently on every chain, and it's
455
+ // spelled per backend because the two can be configured with different
456
+ // `column_name_format`s.
457
+ let makeChainIdField = (~globalStorage: storage) =>
458
+ Table.mkField(
459
+ chainIdFieldName,
460
+ ChainId,
461
+ ~fieldSchema=ChainId.schema,
462
+ ~isPrimaryKey=true,
463
+ ~isChainId=true,
464
+ ~postgresDbName=globalStorage.postgresColumnNameFormat->chainIdColumnName,
465
+ ~clickhouseDbName=globalStorage.clickhouseColumnNameFormat->chainIdColumnName,
466
+ )
467
+
427
468
  let parseEntitiesFromJson = (
428
469
  entitiesJson: array<'entityJson>,
429
470
  ~enumConfigsByName: dict<Table.enumConfig<Table.enum>>,
430
471
  ~globalStorage: storage,
472
+ ~defaultCrossChain: bool,
431
473
  ): array<Internal.entityConfig> => {
432
474
  entitiesJson->Array.mapWithIndex((entityJson, index) => {
433
475
  let entityName = entityJson["name"]
476
+ let crossChain = entityJson["crossChain"]->Option.getOr(defaultCrossChain)
434
477
 
435
478
  let fields: array<Table.fieldOrDerived> = entityJson["properties"]->Array.map(prop => {
436
479
  let (fieldType, fieldSchema, isNullable, isArray, isIndex) = getFieldTypeAndSchema(
@@ -478,7 +521,10 @@ let parseEntitiesFromJson = (
478
521
 
479
522
  let table = Table.mkTable(
480
523
  entityName,
481
- ~fields=Array.concat(fields, derivedFields),
524
+ ~fields=Array.concatMany(
525
+ fields,
526
+ [crossChain ? [] : [makeChainIdField(~globalStorage)], derivedFields],
527
+ ),
482
528
  ~compositeIndexes,
483
529
  ~description=?entityJson["description"],
484
530
  )
@@ -536,14 +582,19 @@ let parseEntitiesFromJson = (
536
582
  schema: schema->(Utils.magic: S.t<dict<unknown>> => S.t<Internal.entity>),
537
583
  table,
538
584
  storage,
585
+ crossChain,
539
586
  }->Internal.fromGenericEntityConfig
540
587
  })
541
588
  }
542
589
 
590
+ let columnNameFormatSchema = S.enum([Original, SnakeCase])
591
+
543
592
  let publicConfigStorageSchema = S.schema(s =>
544
593
  {
545
594
  "postgres": s.matches(S.bool),
546
595
  "clickhouse": s.matches(S.option(S.bool)),
596
+ "postgresColumnNameFormat": s.matches(S.option(columnNameFormatSchema)),
597
+ "clickhouseColumnNameFormat": s.matches(S.option(columnNameFormatSchema)),
547
598
  }
548
599
  )
549
600
 
@@ -558,12 +609,14 @@ let publicConfigSchema = S.schema(s =>
558
609
  "saveFullHistory": s.matches(S.option(S.bool)),
559
610
  "rawEvents": s.matches(S.option(S.bool)),
560
611
  "chainIdMode": s.matches(S.option(ChainId.modeSchema)),
612
+ "defaultCrossChain": s.matches(S.option(S.bool)),
561
613
  "storage": s.matches(publicConfigStorageSchema),
562
614
  "evm": s.matches(S.option(publicConfigEvmSchema)),
563
615
  "fuel": s.matches(S.option(publicConfigEcosystemSchema)),
564
616
  "svm": s.matches(S.option(publicConfigEcosystemSchema)),
565
617
  "enums": s.matches(S.option(S.dict(S.array(S.string)))),
566
618
  "entities": s.matches(S.option(S.array(entityJsonSchema))),
619
+ "subgraph": s.matches(S.option(S.json(~validate=false))),
567
620
  }
568
621
  )
569
622
 
@@ -995,12 +1048,20 @@ let fromPublic = (publicConfigJson: JSON.t) => {
995
1048
  let globalStorage: storage = {
996
1049
  postgres: publicConfig["storage"]["postgres"],
997
1050
  clickhouse: publicConfig["storage"]["clickhouse"]->Option.getOr(false),
1051
+ postgresColumnNameFormat: publicConfig["storage"]["postgresColumnNameFormat"]->Option.getOr(
1052
+ Original,
1053
+ ),
1054
+ clickhouseColumnNameFormat: publicConfig["storage"]["clickhouseColumnNameFormat"]->Option.getOr(
1055
+ Original,
1056
+ ),
998
1057
  }
999
1058
 
1059
+ let defaultCrossChain = publicConfig["defaultCrossChain"]->Option.getOr(true)
1060
+
1000
1061
  let userEntities =
1001
1062
  publicConfig["entities"]
1002
1063
  ->Option.getOr([])
1003
- ->parseEntitiesFromJson(~enumConfigsByName, ~globalStorage)
1064
+ ->parseEntitiesFromJson(~enumConfigsByName, ~globalStorage, ~defaultCrossChain)
1004
1065
 
1005
1066
  let allEntities = userEntities->Array.concat([EnvioAddresses.entityConfig])
1006
1067
 
@@ -1036,6 +1097,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1036
1097
  contractHandlers,
1037
1098
  shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
1038
1099
  shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
1100
+ defaultCrossChain,
1039
1101
  storage: globalStorage,
1040
1102
  chainIdMode: publicConfig["chainIdMode"]->Option.getOr(Int32),
1041
1103
  chainMap,
@@ -1052,6 +1114,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1052
1114
  userEntities,
1053
1115
  allEntities,
1054
1116
  allEnums,
1117
+ subgraph: publicConfig["subgraph"],
1055
1118
  }
1056
1119
  }
1057
1120
 
@@ -1188,6 +1251,13 @@ let stripSensitiveData = (json: JSON.t): JSON.t => {
1188
1251
  stripChains(obj->Dict.get("evm"))
1189
1252
  stripChains(obj->Dict.get("fuel"))
1190
1253
  stripChains(obj->Dict.get("svm"))
1254
+ // A subgraph names its endpoint through ENVIO_SUBGRAPH_RPC, and those
1255
+ // URLs usually carry a provider key. Rotating one must not read as an
1256
+ // incompatible config either.
1257
+ switch obj->Dict.get("subgraph") {
1258
+ | Some(Object(subgraph)) => subgraph->Utils.Dict.deleteInPlace("rpcUrls")
1259
+ | _ => ()
1260
+ }
1191
1261
  }
1192
1262
  | _ => ()
1193
1263
  }