envio 3.5.1 → 3.6.1

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 (52) 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 +60 -2
  9. package/src/Config.res.mjs +49 -13
  10. package/src/EffectState.res +2 -2
  11. package/src/EffectState.res.mjs +2 -2
  12. package/src/EntityTables.res +32 -0
  13. package/src/EntityTables.res.mjs +44 -0
  14. package/src/Envio.res +7 -7
  15. package/src/Envio.res.mjs +5 -6
  16. package/src/Hasura.res +37 -3
  17. package/src/Hasura.res.mjs +23 -5
  18. package/src/InMemoryStore.res +48 -8
  19. package/src/InMemoryStore.res.mjs +37 -7
  20. package/src/IndexerState.res +30 -24
  21. package/src/IndexerState.res.mjs +20 -35
  22. package/src/IndexerState.resi +8 -6
  23. package/src/Internal.res +29 -11
  24. package/src/Internal.res.mjs +27 -10
  25. package/src/LoadLayer.res +39 -8
  26. package/src/LoadLayer.res.mjs +36 -9
  27. package/src/LoadLayer.resi +2 -0
  28. package/src/Metrics.res +5 -5
  29. package/src/Metrics.res.mjs +5 -1
  30. package/src/Persistence.res +12 -1
  31. package/src/PgStorage.res +183 -37
  32. package/src/PgStorage.res.mjs +149 -34
  33. package/src/PruneStaleHistory.res +1 -0
  34. package/src/PruneStaleHistory.res.mjs +2 -1
  35. package/src/Sink.res +3 -3
  36. package/src/Sink.res.mjs +2 -2
  37. package/src/TestIndexer.res +128 -17
  38. package/src/TestIndexer.res.mjs +75 -9
  39. package/src/UserContext.res +19 -4
  40. package/src/UserContext.res.mjs +15 -9
  41. package/src/Writing.res +8 -16
  42. package/src/Writing.res.mjs +10 -10
  43. package/src/bindings/ClickHouse.res +40 -4
  44. package/src/bindings/ClickHouse.res.mjs +37 -5
  45. package/src/db/EntityHistory.res +64 -21
  46. package/src/db/EntityHistory.res.mjs +43 -22
  47. package/src/db/InternalTable.res.mjs +31 -31
  48. package/src/db/Table.res +24 -1
  49. package/src/db/Table.res.mjs +26 -4
  50. package/src/sources/SourceManager.res +6 -12
  51. package/src/sources/SourceManager.res.mjs +8 -5
  52. 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",
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",
73
+ "envio-linux-x64-musl": "3.6.1",
74
+ "envio-linux-arm64": "3.6.1",
75
+ "envio-darwin-x64": "3.6.1",
76
+ "envio-darwin-arm64": "3.6.1"
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
@@ -161,6 +180,9 @@ module EnvioAddresses = {
161
180
  // always required to have Postgres enabled (Storage::resolve forbids
162
181
  // a Postgres-disabled global), so this is safe regardless of mode.
163
182
  storage: {postgres: true, clickhouse: false},
183
+ // The table already keys rows by chain through the composite `id`, so the
184
+ // per-chain mode must not append a second chain-id column to it.
185
+ crossChain: true,
164
186
  }->Internal.fromGenericEntityConfig
165
187
  }
166
188
 
@@ -353,6 +375,7 @@ let entityStorageSchema = S.schema(s =>
353
375
  let entityJsonSchema = S.schema(s =>
354
376
  {
355
377
  "name": s.matches(S.string),
378
+ "crossChain": s.matches(S.option(S.bool)),
356
379
  "storage": s.matches(S.option(entityStorageSchema)),
357
380
  "properties": s.matches(S.array(propertySchema)),
358
381
  "derivedFields": s.matches(S.option(S.array(derivedFieldSchema))),
@@ -424,13 +447,30 @@ let parseEnumsFromJson = (enumsJson: dict<array<string>>): array<Table.enumConfi
424
447
  )
425
448
  }
426
449
 
450
+ // The chain-id column appended to a per-chain entity's table. It completes the
451
+ // primary key so the same id can exist independently on every chain, and it's
452
+ // spelled per backend because the two can be configured with different
453
+ // `column_name_format`s.
454
+ let makeChainIdField = (~globalStorage: storage) =>
455
+ Table.mkField(
456
+ chainIdFieldName,
457
+ ChainId,
458
+ ~fieldSchema=ChainId.schema,
459
+ ~isPrimaryKey=true,
460
+ ~isChainId=true,
461
+ ~postgresDbName=globalStorage.postgresColumnNameFormat->chainIdColumnName,
462
+ ~clickhouseDbName=globalStorage.clickhouseColumnNameFormat->chainIdColumnName,
463
+ )
464
+
427
465
  let parseEntitiesFromJson = (
428
466
  entitiesJson: array<'entityJson>,
429
467
  ~enumConfigsByName: dict<Table.enumConfig<Table.enum>>,
430
468
  ~globalStorage: storage,
469
+ ~defaultCrossChain: bool,
431
470
  ): array<Internal.entityConfig> => {
432
471
  entitiesJson->Array.mapWithIndex((entityJson, index) => {
433
472
  let entityName = entityJson["name"]
473
+ let crossChain = entityJson["crossChain"]->Option.getOr(defaultCrossChain)
434
474
 
435
475
  let fields: array<Table.fieldOrDerived> = entityJson["properties"]->Array.map(prop => {
436
476
  let (fieldType, fieldSchema, isNullable, isArray, isIndex) = getFieldTypeAndSchema(
@@ -478,7 +518,10 @@ let parseEntitiesFromJson = (
478
518
 
479
519
  let table = Table.mkTable(
480
520
  entityName,
481
- ~fields=Array.concat(fields, derivedFields),
521
+ ~fields=Array.concatMany(
522
+ fields,
523
+ [crossChain ? [] : [makeChainIdField(~globalStorage)], derivedFields],
524
+ ),
482
525
  ~compositeIndexes,
483
526
  ~description=?entityJson["description"],
484
527
  )
@@ -536,14 +579,19 @@ let parseEntitiesFromJson = (
536
579
  schema: schema->(Utils.magic: S.t<dict<unknown>> => S.t<Internal.entity>),
537
580
  table,
538
581
  storage,
582
+ crossChain,
539
583
  }->Internal.fromGenericEntityConfig
540
584
  })
541
585
  }
542
586
 
587
+ let columnNameFormatSchema = S.enum([Original, SnakeCase])
588
+
543
589
  let publicConfigStorageSchema = S.schema(s =>
544
590
  {
545
591
  "postgres": s.matches(S.bool),
546
592
  "clickhouse": s.matches(S.option(S.bool)),
593
+ "postgresColumnNameFormat": s.matches(S.option(columnNameFormatSchema)),
594
+ "clickhouseColumnNameFormat": s.matches(S.option(columnNameFormatSchema)),
547
595
  }
548
596
  )
549
597
 
@@ -558,6 +606,7 @@ let publicConfigSchema = S.schema(s =>
558
606
  "saveFullHistory": s.matches(S.option(S.bool)),
559
607
  "rawEvents": s.matches(S.option(S.bool)),
560
608
  "chainIdMode": s.matches(S.option(ChainId.modeSchema)),
609
+ "defaultCrossChain": s.matches(S.option(S.bool)),
561
610
  "storage": s.matches(publicConfigStorageSchema),
562
611
  "evm": s.matches(S.option(publicConfigEvmSchema)),
563
612
  "fuel": s.matches(S.option(publicConfigEcosystemSchema)),
@@ -995,12 +1044,20 @@ let fromPublic = (publicConfigJson: JSON.t) => {
995
1044
  let globalStorage: storage = {
996
1045
  postgres: publicConfig["storage"]["postgres"],
997
1046
  clickhouse: publicConfig["storage"]["clickhouse"]->Option.getOr(false),
1047
+ postgresColumnNameFormat: publicConfig["storage"]["postgresColumnNameFormat"]->Option.getOr(
1048
+ Original,
1049
+ ),
1050
+ clickhouseColumnNameFormat: publicConfig["storage"]["clickhouseColumnNameFormat"]->Option.getOr(
1051
+ Original,
1052
+ ),
998
1053
  }
999
1054
 
1055
+ let defaultCrossChain = publicConfig["defaultCrossChain"]->Option.getOr(true)
1056
+
1000
1057
  let userEntities =
1001
1058
  publicConfig["entities"]
1002
1059
  ->Option.getOr([])
1003
- ->parseEntitiesFromJson(~enumConfigsByName, ~globalStorage)
1060
+ ->parseEntitiesFromJson(~enumConfigsByName, ~globalStorage, ~defaultCrossChain)
1004
1061
 
1005
1062
  let allEntities = userEntities->Array.concat([EnvioAddresses.entityConfig])
1006
1063
 
@@ -1036,6 +1093,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1036
1093
  contractHandlers,
1037
1094
  shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
1038
1095
  shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
1096
+ defaultCrossChain,
1039
1097
  storage: globalStorage,
1040
1098
  chainIdMode: publicConfig["chainIdMode"]->Option.getOr(Int32),
1041
1099
  chainMap,
@@ -22,6 +22,16 @@ import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
22
22
  import * as EventConfigBuilder from "./EventConfigBuilder.res.mjs";
23
23
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
24
24
 
25
+ let chainIdFieldName = "chainId";
26
+
27
+ function chainIdColumnName(format) {
28
+ if (format === "original") {
29
+ return chainIdFieldName;
30
+ } else {
31
+ return "chain_id";
32
+ }
33
+ }
34
+
25
35
  let name = "envio_addresses";
26
36
 
27
37
  function makeId(chainId, address) {
@@ -42,11 +52,11 @@ let schema = S$RescriptSchema.schema(s => ({
42
52
  }));
43
53
 
44
54
  let table = Table.mkTable(name, undefined, [
45
- Table.mkField("id", "String", S$RescriptSchema.string, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined),
46
- Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
47
- Table.mkField("registration_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
48
- Table.mkField("registration_log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
49
- Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
55
+ Table.mkField("id", "String", S$RescriptSchema.string, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined, undefined),
56
+ Table.mkField("chain_id", "ChainId", ChainId.schema, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
57
+ Table.mkField("registration_block", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
58
+ Table.mkField("registration_log_index", "Int32", S$RescriptSchema.int, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined),
59
+ Table.mkField("contract_name", "String", S$RescriptSchema.string, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
50
60
  ], undefined);
51
61
 
52
62
  let entityConfig_storage = {
@@ -59,7 +69,8 @@ let entityConfig = {
59
69
  index: -1,
60
70
  schema: schema,
61
71
  table: table,
62
- storage: entityConfig_storage
72
+ storage: entityConfig_storage,
73
+ crossChain: true
63
74
  };
64
75
 
65
76
  let EnvioAddresses = {
@@ -217,6 +228,7 @@ let entityStorageSchema = S$RescriptSchema.schema(s => ({
217
228
 
218
229
  let entityJsonSchema = S$RescriptSchema.schema(s => ({
219
230
  name: s.m(S$RescriptSchema.string),
231
+ crossChain: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
220
232
  storage: s.m(S$RescriptSchema.option(entityStorageSchema)),
221
233
  properties: s.m(S$RescriptSchema.array(propertySchema)),
222
234
  derivedFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(derivedFieldSchema))),
@@ -324,19 +336,24 @@ function parseEnumsFromJson(enumsJson) {
324
336
  return Object.entries(enumsJson).map(param => Table.makeEnumConfig(param[0], param[1]));
325
337
  }
326
338
 
327
- function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage) {
339
+ function makeChainIdField(globalStorage) {
340
+ return Table.mkField(chainIdFieldName, "ChainId", ChainId.schema, undefined, undefined, undefined, true, undefined, true, undefined, undefined, chainIdColumnName(globalStorage.postgresColumnNameFormat), chainIdColumnName(globalStorage.clickhouseColumnNameFormat));
341
+ }
342
+
343
+ function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage, defaultCrossChain) {
328
344
  return entitiesJson.map((entityJson, index) => {
329
345
  let entityName = entityJson.name;
346
+ let crossChain = Stdlib_Option.getOr(entityJson.crossChain, defaultCrossChain);
330
347
  let fields = entityJson.properties.map(prop => {
331
348
  let match = getFieldTypeAndSchema(prop, enumConfigsByName);
332
- return Table.mkField(prop.name, match[0], match[1], undefined, match[3], match[2], prop.name === "id", match[4], prop.linkedEntity, prop.description, prop.postgresDbName, prop.clickhouseDbName);
349
+ return Table.mkField(prop.name, match[0], match[1], undefined, match[3], match[2], prop.name === "id", match[4], undefined, prop.linkedEntity, prop.description, prop.postgresDbName, prop.clickhouseDbName);
333
350
  });
334
351
  let derivedFields = Stdlib_Option.getOr(entityJson.derivedFields, []).map(df => Table.mkDerivedFromField(df.fieldName, df.derivedFromEntity, df.derivedFromField, df.description));
335
352
  let compositeIndexes = Stdlib_Option.getOr(entityJson.compositeIndices, []).map(ci => ci.map(f => ({
336
353
  fieldName: f.fieldName,
337
354
  direction: f.direction === "Asc" ? "Asc" : "Desc"
338
355
  })));
339
- let table = Table.mkTable(entityName, compositeIndexes, fields.concat(derivedFields), entityJson.description);
356
+ let table = Table.mkTable(entityName, compositeIndexes, fields.concat(crossChain ? [] : [makeChainIdField(globalStorage)], derivedFields), entityJson.description);
340
357
  let getApiFieldName = prop => {
341
358
  let match = prop.linkedEntity;
342
359
  if (match !== undefined) {
@@ -382,14 +399,22 @@ function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage) {
382
399
  index: index,
383
400
  schema: schema,
384
401
  table: table,
385
- storage: storage
402
+ storage: storage,
403
+ crossChain: crossChain
386
404
  };
387
405
  });
388
406
  }
389
407
 
408
+ let columnNameFormatSchema = S$RescriptSchema.$$enum([
409
+ "original",
410
+ "snake_case"
411
+ ]);
412
+
390
413
  let publicConfigStorageSchema = S$RescriptSchema.schema(s => ({
391
414
  postgres: s.m(S$RescriptSchema.bool),
392
- clickhouse: s.m(S$RescriptSchema.option(S$RescriptSchema.bool))
415
+ clickhouse: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
416
+ postgresColumnNameFormat: s.m(S$RescriptSchema.option(columnNameFormatSchema)),
417
+ clickhouseColumnNameFormat: s.m(S$RescriptSchema.option(columnNameFormatSchema))
393
418
  }));
394
419
 
395
420
  let publicConfigSchema = S$RescriptSchema.schema(s => ({
@@ -402,6 +427,7 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
402
427
  saveFullHistory: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
403
428
  rawEvents: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
404
429
  chainIdMode: s.m(S$RescriptSchema.option(ChainId.modeSchema)),
430
+ defaultCrossChain: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
405
431
  storage: s.m(publicConfigStorageSchema),
406
432
  evm: s.m(S$RescriptSchema.option(publicConfigEvmSchema)),
407
433
  fuel: s.m(S$RescriptSchema.option(publicConfigEcosystemSchema)),
@@ -689,11 +715,16 @@ function fromPublic(publicConfigJson) {
689
715
  ]));
690
716
  let globalStorage_postgres = publicConfig.storage.postgres;
691
717
  let globalStorage_clickhouse = Stdlib_Option.getOr(publicConfig.storage.clickhouse, false);
718
+ let globalStorage_postgresColumnNameFormat = Stdlib_Option.getOr(publicConfig.storage.postgresColumnNameFormat, "original");
719
+ let globalStorage_clickhouseColumnNameFormat = Stdlib_Option.getOr(publicConfig.storage.clickhouseColumnNameFormat, "original");
692
720
  let globalStorage = {
693
721
  postgres: globalStorage_postgres,
694
- clickhouse: globalStorage_clickhouse
722
+ clickhouse: globalStorage_clickhouse,
723
+ postgresColumnNameFormat: globalStorage_postgresColumnNameFormat,
724
+ clickhouseColumnNameFormat: globalStorage_clickhouseColumnNameFormat
695
725
  };
696
- let userEntities = parseEntitiesFromJson(Stdlib_Option.getOr(publicConfig.entities, []), enumConfigsByName, globalStorage);
726
+ let defaultCrossChain = Stdlib_Option.getOr(publicConfig.defaultCrossChain, true);
727
+ let userEntities = parseEntitiesFromJson(Stdlib_Option.getOr(publicConfig.entities, []), enumConfigsByName, globalStorage, defaultCrossChain);
697
728
  let allEntities = userEntities.concat([entityConfig]);
698
729
  let userEntitiesByName = Object.fromEntries(userEntities.map(entityConfig => [
699
730
  Utils.$$String.capitalize(entityConfig.name),
@@ -710,6 +741,7 @@ function fromPublic(publicConfigJson) {
710
741
  contractHandlers: contractHandlers,
711
742
  shouldRollbackOnReorg: Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true),
712
743
  shouldSaveFullHistory: Stdlib_Option.getOr(publicConfig.saveFullHistory, false),
744
+ defaultCrossChain: defaultCrossChain,
713
745
  storage: globalStorage,
714
746
  chainIdMode: Stdlib_Option.getOr(publicConfig.chainIdMode, "int32"),
715
747
  chainMap: chainMap,
@@ -1049,6 +1081,8 @@ function getPgUserEntities(config) {
1049
1081
  }
1050
1082
 
1051
1083
  export {
1084
+ chainIdFieldName,
1085
+ chainIdColumnName,
1052
1086
  EnvioAddresses,
1053
1087
  rpcSourceForSchema,
1054
1088
  rpcConfigSchema,
@@ -1069,7 +1103,9 @@ export {
1069
1103
  entityJsonSchema,
1070
1104
  getFieldTypeAndSchema,
1071
1105
  parseEnumsFromJson,
1106
+ makeChainIdField,
1072
1107
  parseEntitiesFromJson,
1108
+ columnNameFormatSchema,
1073
1109
  publicConfigStorageSchema,
1074
1110
  publicConfigSchema,
1075
1111
  fromPublic,
@@ -131,7 +131,7 @@ let commitCacheCount = (inMemTable: effectCacheInMemTable, ~count) => {
131
131
 
132
132
  let statsToMetrics = (stats: effectStats): Metrics.effectMetrics => {
133
133
  Metrics.effect: stats.effectName,
134
- scope: stats.scope->Internal.EffectCache.scopeToString,
134
+ scope: stats.scope->Internal.chainScopeToString,
135
135
  callSeconds: stats.callSeconds,
136
136
  callSecondsTotal: stats.callSecondsTotal,
137
137
  callCount: stats.callCount,
@@ -152,7 +152,7 @@ let toMetrics = (self: t): array<Metrics.effectMetrics> => {
152
152
  metrics
153
153
  ->Array.push({
154
154
  Metrics.effect: effectName,
155
- scope: scope->Internal.EffectCache.scopeToString,
155
+ scope: scope->Internal.chainScopeToString,
156
156
  callSeconds: 0.,
157
157
  callSecondsTotal: 0.,
158
158
  callCount: 0.,
@@ -68,7 +68,7 @@ function toMetrics(self) {
68
68
  let stats = t.stats;
69
69
  return {
70
70
  effect: stats.effectName,
71
- scope: Internal.EffectCache.scopeToString(stats.scope),
71
+ scope: Internal.chainScopeToString(stats.scope),
72
72
  callSeconds: stats.callSeconds,
73
73
  callSecondsTotal: stats.callSecondsTotal,
74
74
  callCount: stats.callCount,
@@ -82,7 +82,7 @@ function toMetrics(self) {
82
82
  Utils.Dict.forEach(self.unregisteredCacheCounts, param => {
83
83
  metrics.push({
84
84
  effect: param.effectName,
85
- scope: Internal.EffectCache.scopeToString(param.scope),
85
+ scope: Internal.chainScopeToString(param.scope),
86
86
  callSeconds: 0,
87
87
  callSecondsTotal: 0,
88
88
  callCount: 0,