envio 3.10.0 → 3.11.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 (42) hide show
  1. package/evm.schema.json +21 -4
  2. package/fuel.schema.json +21 -4
  3. package/index.d.ts +7 -6
  4. package/package.json +6 -6
  5. package/src/ChainState.res +9 -73
  6. package/src/ChainState.res.mjs +6 -58
  7. package/src/Config.res +73 -18
  8. package/src/Config.res.mjs +69 -16
  9. package/src/EventConfigBuilder.res +14 -10
  10. package/src/EventConfigBuilder.res.mjs +6 -4
  11. package/src/HandlerRegister.res +12 -11
  12. package/src/HandlerRegister.res.mjs +6 -5
  13. package/src/Internal.res +20 -4
  14. package/src/Internal.res.mjs +9 -0
  15. package/src/Main.res +8 -1
  16. package/src/Main.res.mjs +3 -3
  17. package/src/Persistence.res +11 -1
  18. package/src/Persistence.res.mjs +6 -2
  19. package/src/PgStorage.res +1 -1
  20. package/src/PgStorage.res.mjs +1 -1
  21. package/src/SimulateItems.res +13 -6
  22. package/src/SimulateItems.res.mjs +7 -6
  23. package/src/TestIndexer.res +6 -5
  24. package/src/TestIndexer.res.mjs +5 -4
  25. package/src/db/InternalTable.res +1 -1
  26. package/src/db/InternalTable.res.mjs +2 -1
  27. package/src/sources/ChainSources.res +72 -0
  28. package/src/sources/ChainSources.res.mjs +55 -0
  29. package/src/sources/EvmHyperSyncSource.res +10 -22
  30. package/src/sources/EvmHyperSyncSource.res.mjs +9 -41
  31. package/src/sources/FuelHyperSyncSource.res +10 -18
  32. package/src/sources/FuelHyperSyncSource.res.mjs +8 -38
  33. package/src/sources/HyperSync.res +31 -0
  34. package/src/sources/HyperSync.res.mjs +31 -0
  35. package/src/sources/HyperSync.resi +16 -0
  36. package/src/sources/StartBlockResolver.res +173 -0
  37. package/src/sources/StartBlockResolver.res.mjs +134 -0
  38. package/src/sources/Svm.res +0 -51
  39. package/src/sources/Svm.res.mjs +0 -46
  40. package/src/sources/SvmHyperSyncSource.res +20 -9
  41. package/src/sources/SvmHyperSyncSource.res.mjs +34 -12
  42. package/svm.schema.json +80 -66
package/evm.schema.json CHANGED
@@ -494,10 +494,8 @@
494
494
  "minimum": 0
495
495
  },
496
496
  "start_block": {
497
- "description": "The block at which the indexer should start ingesting data",
498
- "type": "integer",
499
- "format": "uint64",
500
- "minimum": 0
497
+ "description": "The block at which the indexer should start ingesting data, or \"latest\" to start from the chain's current head block when the indexer is first deployed. Once resolved, the concrete block is persisted and reused every time the indexer resumes normally (for example recovering from a crash), so downtime is backfilled instead of skipped. Running `envio start`/`dev` with -r (--restart) resets this like any other config change: \"latest\" resolves again, against the head at that time.",
498
+ "$ref": "#/$defs/StartBlock"
501
499
  },
502
500
  "end_block": {
503
501
  "description": "The block at which the indexer should terminate.",
@@ -685,6 +683,25 @@
685
683
  "url"
686
684
  ]
687
685
  },
686
+ "StartBlock": {
687
+ "description": "A chain's configured start block: either a concrete block number or the\nliteral \"latest\". Config parsing never touches the network, so `Latest`\nstays unresolved here — it's resolved once at runtime, right before the\nindexer's first-ever persisted state is written, and never re-resolved\non a normal resume (see packages/envio/src/sources/StartBlockResolver.res).\nNote: this repo's `-r`/`--restart` CLI flag wipes the DB and re-deploys\nfrom scratch, so it re-resolves \"latest\" too — \"resume\" here means the\nopposite: recovering from a crash or process restart without `-r`.",
688
+ "anyOf": [
689
+ {
690
+ "type": "integer",
691
+ "format": "uint64",
692
+ "minimum": 0
693
+ },
694
+ {
695
+ "$ref": "#/$defs/StartBlockTag"
696
+ }
697
+ ]
698
+ },
699
+ "StartBlockTag": {
700
+ "type": "string",
701
+ "enum": [
702
+ "latest"
703
+ ]
704
+ },
688
705
  "ChainContract": {
689
706
  "type": "object",
690
707
  "properties": {
package/fuel.schema.json CHANGED
@@ -328,10 +328,8 @@
328
328
  ]
329
329
  },
330
330
  "start_block": {
331
- "description": "The block at which the indexer should start ingesting data",
332
- "type": "integer",
333
- "format": "uint64",
334
- "minimum": 0
331
+ "description": "The block at which the indexer should start ingesting data, or \"latest\" to start from the chain's current head block when the indexer is first deployed. Once resolved, the concrete block is persisted and reused every time the indexer resumes normally (for example recovering from a crash), so downtime is backfilled instead of skipped. Running `envio start`/`dev` with -r (--restart) resets this like any other config change: \"latest\" resolves again, against the head at that time.",
332
+ "$ref": "#/$defs/StartBlock"
335
333
  },
336
334
  "end_block": {
337
335
  "description": "The block at which the indexer should terminate.",
@@ -388,6 +386,25 @@
388
386
  "start_block"
389
387
  ]
390
388
  },
389
+ "StartBlock": {
390
+ "description": "A chain's configured start block: either a concrete block number or the\nliteral \"latest\". Config parsing never touches the network, so `Latest`\nstays unresolved here — it's resolved once at runtime, right before the\nindexer's first-ever persisted state is written, and never re-resolved\non a normal resume (see packages/envio/src/sources/StartBlockResolver.res).\nNote: this repo's `-r`/`--restart` CLI flag wipes the DB and re-deploys\nfrom scratch, so it re-resolves \"latest\" too — \"resume\" here means the\nopposite: recovering from a crash or process restart without `-r`.",
391
+ "anyOf": [
392
+ {
393
+ "type": "integer",
394
+ "format": "uint64",
395
+ "minimum": 0
396
+ },
397
+ {
398
+ "$ref": "#/$defs/StartBlockTag"
399
+ }
400
+ ]
401
+ },
402
+ "StartBlockTag": {
403
+ "type": "string",
404
+ "enum": [
405
+ "latest"
406
+ ]
407
+ },
391
408
  "HyperfuelConfig": {
392
409
  "type": "object",
393
410
  "properties": {
package/index.d.ts CHANGED
@@ -1377,7 +1377,7 @@ type SvmNamedAccounts<
1377
1377
  Acc extends Readonly<Record<string, unknown>>,
1378
1378
  Fields extends SvmFieldsSelection,
1379
1379
  > = {
1380
- readonly [K in keyof Acc & string]: SvmInstructionAccount<Fields, K>;
1380
+ readonly [K in keyof Acc]: SvmInstructionAccount<Fields, K & string>;
1381
1381
  };
1382
1382
 
1383
1383
  /** The parent transaction of a {@link SvmInstruction}, narrowed to the
@@ -1524,8 +1524,8 @@ export type SvmOnInstructionOptions<
1524
1524
  /** Program name as declared under `chains[].programs[].name` in
1525
1525
  * `config.yaml`. */
1526
1526
  readonly program: P;
1527
- /** Instruction name as declared under
1528
- * `chains[].programs[].instructions[].name` in `config.yaml`. */
1527
+ /** Instruction name from the program's IDL, or from
1528
+ * `chains[].programs[].instructions[].name` when the layout is inline. */
1529
1529
  readonly instruction: I;
1530
1530
  readonly fields?: Fields & SvmFieldsLiteralCheck<Fields>;
1531
1531
  readonly where?: SvmOnInstructionWhere<SvmAccountsOf<P, I>>;
@@ -1768,9 +1768,10 @@ type SvmEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
1768
1768
  }
1769
1769
  ? {
1770
1770
  /**
1771
- * Register an instruction handler. Dispatch matches on
1772
- * `(programId, discriminator)` from the YAML config.
1773
- * Handler `fields` is the only source of payload selection.
1771
+ * Register an instruction handler. `program` and `instruction`
1772
+ * name an entry from the IDL or YAML. Dispatch uses that
1773
+ * instruction's discriminator. Handler `fields` is the only
1774
+ * source of payload selection.
1774
1775
  */
1775
1776
  readonly onInstruction: <
1776
1777
  P extends keyof Programs & string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -68,10 +68,10 @@
68
68
  "tsx": "4.21.0"
69
69
  },
70
70
  "optionalDependencies": {
71
- "envio-linux-x64": "3.10.0",
72
- "envio-linux-x64-musl": "3.10.0",
73
- "envio-linux-arm64": "3.10.0",
74
- "envio-darwin-x64": "3.10.0",
75
- "envio-darwin-arm64": "3.10.0"
71
+ "envio-linux-x64": "3.11.0",
72
+ "envio-linux-x64-musl": "3.11.0",
73
+ "envio-linux-arm64": "3.11.0",
74
+ "envio-darwin-x64": "3.11.0",
75
+ "envio-darwin-arm64": "3.11.0"
76
76
  }
77
77
  }
@@ -203,7 +203,9 @@ let makeInternal = (
203
203
 
204
204
  chainConfig.contracts->Array.forEach(contract => {
205
205
  switch contract.startBlock {
206
- | Some(startBlock) if startBlock < chainConfig.startBlock =>
206
+ // Against the resolved `~startBlock`, which came from storage:
207
+ // `chainConfig.startBlock` still says whatever config.yaml said.
208
+ | Some(contractStartBlock) if contractStartBlock < startBlock =>
207
209
  JsError.throwWithMessage(
208
210
  `The start block for contract "${contract.name}" is less than the chain start block. This is not supported yet.`,
209
211
  )
@@ -262,78 +264,12 @@ let makeInternal = (
262
264
  })
263
265
 
264
266
  // Create sources lazily here - this is where API token validation happens
265
- let chainId = chainConfig.id
266
- let sources = switch chainConfig.sourceConfig {
267
- | Config.EvmSourceConfig({hypersync, rpcs}) =>
268
- let evmRpcs: array<EvmChain.rpc> = rpcs->Array.map((rpc): EvmChain.rpc => {
269
- let syncConfig = rpc.syncConfig
270
- let ws = rpc.ws
271
- let headers = rpc.headers
272
- {
273
- url: rpc.url,
274
- sourceFor: rpc.sourceFor,
275
- ?syncConfig,
276
- ?ws,
277
- ?headers,
278
- }
279
- })
280
- EvmChain.makeSources(
281
- ~chainId,
282
- ~onEventRegistrations=onEventRegistrations->(
283
- Utils.magic: array<Internal.onEventRegistration> => array<Internal.evmOnEventRegistration>
284
- ),
285
- ~hyperSync=hypersync,
286
- ~rpcs=evmRpcs,
287
- ~lowercaseAddresses,
288
- ~addressStore,
289
- )
290
- | Config.FuelSourceConfig({hypersync}) => [
291
- FuelHyperSyncSource.make({
292
- chainId,
293
- endpointUrl: hypersync,
294
- apiToken: Env.envioApiToken,
295
- onEventRegistrations,
296
- addressStore,
297
- }),
298
- ]
299
- | Config.SvmSourceConfig({hypersync, rpc}) =>
300
- switch (hypersync, rpc) {
301
- | (None, None) =>
302
- JsError.throwWithMessage(`Chain ${chainId->ChainId.toString} has no SVM data source`)
303
- | (None, Some(rpc)) => [Svm.makeRPCSource(~chainId, ~rpc)]
304
- | (Some(hypersyncUrl), _) =>
305
- // HyperSync drives instruction sync. A configured RPC is ignored for now
306
- // (RPC fallback isn't wired up yet).
307
- let apiToken = Env.envioApiToken
308
- [
309
- SvmHyperSyncSource.make({
310
- chainId,
311
- endpointUrl: hypersyncUrl,
312
- apiToken,
313
- onEventRegistrations: onEventRegistrations->(
314
- Utils.magic: array<Internal.onEventRegistration> => array<
315
- Internal.svmOnEventRegistration,
316
- >
317
- ),
318
- clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
319
- addressStore,
320
- }),
321
- ]
322
- }
323
- | Config.SimulateSourceConfig({items, endBlock, ?transactionStore, ?blockStore}) => [
324
- SimulateSource.make(
325
- ~items,
326
- ~endBlock,
327
- ~chainId,
328
- ~addressStore,
329
- ~ecosystem=config.ecosystem.name,
330
- ~transactionStore,
331
- ~blockStore,
332
- ),
333
- ]
334
- // For tests: use ready-to-use sources directly
335
- | Config.CustomSources(sources) => sources
336
- }
267
+ let sources = ChainSources.make(
268
+ ~chainConfig,
269
+ ~onEventRegistrations,
270
+ ~addressStore,
271
+ ~lowercaseAddresses,
272
+ )
337
273
 
338
274
  let blockStore = BlockStore.make(
339
275
  ~ecosystem=config.ecosystem.name,
@@ -1,32 +1,28 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Env from "./Env.res.mjs";
4
3
  import * as Svm from "./sources/Svm.res.mjs";
5
4
  import * as Core from "./Core.res.mjs";
6
5
  import * as Batch from "./Batch.res.mjs";
7
6
  import * as Utils from "./Utils.res.mjs";
8
7
  import * as ChainId from "./ChainId.res.mjs";
9
8
  import * as Logging from "./Logging.res.mjs";
10
- import * as EvmChain from "./sources/EvmChain.res.mjs";
11
9
  import * as FieldMask from "./sources/FieldMask.res.mjs";
12
10
  import * as BlockStore from "./sources/BlockStore.res.mjs";
13
11
  import * as FetchState from "./FetchState.res.mjs";
14
12
  import * as AddressRows from "./AddressRows.res.mjs";
15
13
  import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
16
14
  import * as AddressStore from "./sources/AddressStore.res.mjs";
15
+ import * as ChainSources from "./sources/ChainSources.res.mjs";
17
16
  import * as EntityTables from "./EntityTables.res.mjs";
18
17
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
19
18
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
20
19
  import * as SourceManager from "./sources/SourceManager.res.mjs";
21
20
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
22
- import * as SimulateSource from "./sources/SimulateSource.res.mjs";
23
21
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
24
22
  import * as ContractMapping from "./ContractMapping.res.mjs";
25
23
  import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
26
24
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
27
25
  import * as TransactionStore from "./sources/TransactionStore.res.mjs";
28
- import * as SvmHyperSyncSource from "./sources/SvmHyperSyncSource.res.mjs";
29
- import * as FuelHyperSyncSource from "./sources/FuelHyperSyncSource.res.mjs";
30
26
  import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
31
27
 
32
28
  function configStorageRows(chainConfig, ecosystem, contractMapping) {
@@ -128,8 +124,8 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
128
124
  });
129
125
  let onEventRegistrations = match.onEventRegistrations;
130
126
  chainConfig.contracts.forEach(contract => {
131
- let startBlock = contract.startBlock;
132
- if (startBlock !== undefined && startBlock < chainConfig.startBlock) {
127
+ let contractStartBlock = contract.startBlock;
128
+ if (contractStartBlock !== undefined && contractStartBlock < startBlock) {
133
129
  return Stdlib_JsError.throwWithMessage(`The start block for contract "` + contract.name + `" is less than the chain start block. This is not supported yet.`);
134
130
  }
135
131
  });
@@ -152,55 +148,7 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
152
148
  return reorgCheckpoint;
153
149
  }
154
150
  });
155
- let chainId$1 = chainConfig.id;
156
- let sources = chainConfig.sourceConfig;
157
- let sources$1;
158
- switch (sources.TAG) {
159
- case "EvmSourceConfig" :
160
- let evmRpcs = sources.rpcs.map(rpc => {
161
- let syncConfig = rpc.syncConfig;
162
- let ws = rpc.ws;
163
- let headers = rpc.headers;
164
- return {
165
- url: rpc.url,
166
- sourceFor: rpc.sourceFor,
167
- syncConfig: syncConfig,
168
- ws: ws,
169
- headers: headers
170
- };
171
- });
172
- sources$1 = EvmChain.makeSources(chainId$1, onEventRegistrations, sources.hypersync, evmRpcs, lowercaseAddresses, addressStore);
173
- break;
174
- case "FuelSourceConfig" :
175
- sources$1 = [FuelHyperSyncSource.make({
176
- chainId: chainId$1,
177
- endpointUrl: sources.hypersync,
178
- apiToken: Env.envioApiToken,
179
- onEventRegistrations: onEventRegistrations,
180
- addressStore: addressStore
181
- })];
182
- break;
183
- case "SvmSourceConfig" :
184
- let rpc = sources.rpc;
185
- let hypersync = sources.hypersync;
186
- sources$1 = hypersync !== undefined ? [SvmHyperSyncSource.make({
187
- chainId: chainId$1,
188
- endpointUrl: hypersync,
189
- apiToken: Env.envioApiToken,
190
- onEventRegistrations: onEventRegistrations,
191
- clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
192
- addressStore: addressStore
193
- })] : (
194
- rpc !== undefined ? [Svm.makeRPCSource(chainId$1, rpc, undefined)] : Stdlib_JsError.throwWithMessage(`Chain ` + ChainId.toString(chainId$1) + ` has no SVM data source`)
195
- );
196
- break;
197
- case "SimulateSourceConfig" :
198
- sources$1 = [SimulateSource.make(sources.items, sources.endBlock, chainId$1, addressStore, config.ecosystem.name, Primitive_option.some(sources.transactionStore), Primitive_option.some(sources.blockStore))];
199
- break;
200
- case "CustomSources" :
201
- sources$1 = sources._0;
202
- break;
203
- }
151
+ let sources = ChainSources.make(chainConfig, onEventRegistrations, addressStore, lowercaseAddresses);
204
152
  let blockStore = BlockStore.make(config.ecosystem.name, !lowercaseAddresses);
205
153
  if (Utils.$$Array.notEmpty(chainReorgCheckpoints)) {
206
154
  let seedPage = BlockStore.fromJs(chainReorgCheckpoints.map(cp => ({
@@ -211,7 +159,7 @@ function makeFromDbState(chainConfig, resumedChainState, reorgCheckpoints, isInR
211
159
  }
212
160
  let firstEventBlock$1 = fetchState.firstEventBlock;
213
161
  let chainDensity = firstEventBlock$1 !== undefined && progressBlockNumber > firstEventBlock$1 && numEventsProcessed > 0 ? numEventsProcessed / (progressBlockNumber - firstEventBlock$1 | 0) : undefined;
214
- return make(chainConfig, fetchState, onEventRegistrations, addressStore, SourceManager.make(sources$1, isRealtime, undefined, undefined, undefined, reducedPollingInterval, undefined, undefined), progressBlockNumber, Primitive_option.some(SafeCheckpointTracking.make(maxReorgDepth, config.shouldRollbackOnReorg, chainReorgCheckpoints)), config.shouldRollbackOnReorg, maxReorgDepth, numEventsProcessed, Primitive_option.some(timestampCaughtUpToHeadOrEndblock), undefined, Primitive_option.some(TransactionStore.make(config.ecosystem.name, !lowercaseAddresses)), Primitive_option.some(chainDensity), Primitive_option.some(blockStore), config.reorgThresholdReadyTolerance, EntityTables.perChain(config.userEntities), logger);
162
+ return make(chainConfig, fetchState, onEventRegistrations, addressStore, SourceManager.make(sources, isRealtime, undefined, undefined, undefined, reducedPollingInterval, undefined, undefined), progressBlockNumber, Primitive_option.some(SafeCheckpointTracking.make(maxReorgDepth, config.shouldRollbackOnReorg, chainReorgCheckpoints)), config.shouldRollbackOnReorg, maxReorgDepth, numEventsProcessed, Primitive_option.some(timestampCaughtUpToHeadOrEndblock), undefined, Primitive_option.some(TransactionStore.make(config.ecosystem.name, !lowercaseAddresses)), Primitive_option.some(chainDensity), Primitive_option.some(blockStore), config.reorgThresholdReadyTolerance, EntityTables.perChain(config.userEntities), logger);
215
163
  }
216
164
 
217
165
  function logger(cs) {
@@ -959,4 +907,4 @@ export {
959
907
  markReady,
960
908
  rollback,
961
909
  }
962
- /* Env Not a pure module */
910
+ /* Svm Not a pure module */
package/src/Config.res CHANGED
@@ -26,10 +26,18 @@ type evmRpcConfig = {
26
26
  headers: option<dict<string>>,
27
27
  }
28
28
 
29
+ // Unboxed so the runtime value is exactly what the public config JSON holds -
30
+ // a number, or the string "latest" - which is why `startBlockSchema` only has
31
+ // to validate it rather than convert it.
32
+ @unboxed
33
+ type startBlock =
34
+ | Block(int)
35
+ | @as("latest") Latest
36
+
29
37
  type sourceConfig =
30
38
  | EvmSourceConfig({hypersync: option<string>, rpcs: array<evmRpcConfig>})
31
39
  | FuelSourceConfig({hypersync: string})
32
- | SvmSourceConfig({hypersync: option<string>, rpc: option<string>})
40
+ | SvmSourceConfig({hypersync: string})
33
41
  // A `simulate` run: the items the test fed in, parsed against the chain's
34
42
  // registrations. The source itself is built with the chain's address store,
35
43
  // like every other source, so it can apply the same gates.
@@ -46,7 +54,11 @@ type chain = {
46
54
  name: string,
47
55
  id: ChainId.t,
48
56
  ecosystem: Ecosystem.name,
49
- startBlock: int,
57
+ // What config.yaml says, never rewritten. Once `Latest` is resolved against
58
+ // the chain's head the block lives in the database
59
+ // (`envio_chains.start_block`), and that is what every consumer past startup
60
+ // reads.
61
+ startBlock: startBlock,
50
62
  endBlock?: int,
51
63
  maxReorgDepth: int,
52
64
  blockLag: int,
@@ -157,10 +169,36 @@ let chainContractSchema = S.schema(s =>
157
169
  }
158
170
  )
159
171
 
172
+ // For everything downstream of `StartBlockResolver`, which rewrites `Latest`
173
+ // into the chain's head before storage is initialized. The throw is an
174
+ // invariant check, not a case a user can reach.
175
+ let startBlockOrThrow = (chain: chain) =>
176
+ switch chain.startBlock {
177
+ | Block(startBlock) => startBlock
178
+ | Latest =>
179
+ JsError.throwWithMessage(
180
+ `Chain ${chain.id->ChainId.toString}: the "latest" start block was read before it was resolved. This is a bug in envio - please report it.`,
181
+ )
182
+ }
183
+
184
+ // For the paths that have no chain to read a head from - the test indexer and
185
+ // simulated items - where `Latest` never gets resolved and every simulated
186
+ // block should be in range.
187
+ let startBlockOrZero = (chain: chain) =>
188
+ switch chain.startBlock {
189
+ | Block(startBlock) => startBlock
190
+ | Latest => 0
191
+ }
192
+
193
+ let startBlockSchema = S.union([
194
+ S.int->S.shape(n => Block(n)),
195
+ S.literal("latest")->S.shape(_ => Latest),
196
+ ])
197
+
160
198
  let publicConfigChainSchema = S.schema(s =>
161
199
  {
162
200
  "id": s.matches(ChainId.schema),
163
- "startBlock": s.matches(S.int),
201
+ "startBlock": s.matches(startBlockSchema),
164
202
  "endBlock": s.matches(S.option(S.int)),
165
203
  "maxReorgDepth": s.matches(S.option(S.int)),
166
204
  "blockLag": s.matches(S.option(S.int)),
@@ -168,23 +206,37 @@ let publicConfigChainSchema = S.schema(s =>
168
206
  "hypersync": s.matches(S.option(S.string)),
169
207
  "rpcs": s.matches(S.option(S.array(rpcConfigSchema))),
170
208
  // SVM source config
171
- "rpc": s.matches(S.option(S.string)),
172
209
  // Per-chain contract data (addresses and optional start block)
173
210
  "contracts": s.matches(S.option(S.dict(chainContractSchema))),
174
211
  }
175
212
  )
176
213
 
214
+ type svmAccountSlotItem = {"name": option<string>, "optional": option<bool>}
215
+
216
+ let svmAccountSlotSchema: S.t<svmAccountSlotItem> = S.schema(s =>
217
+ {
218
+ "name": s.matches(S.option(S.string)),
219
+ "optional": s.matches(S.option(S.bool)),
220
+ }
221
+ )
222
+
223
+ let svmAccountSlotFromItem = (slot: svmAccountSlotItem): Internal.svmAccountSlot =>
224
+ switch (slot["name"], slot["optional"]) {
225
+ | (None, _) => Unnamed
226
+ | (Some(name), Some(true)) => Optional(name)
227
+ | (Some(name), _) => Required(name)
228
+ }
229
+
177
230
  let svmEventDescriptorSchema = S.schema(s =>
178
231
  {
179
232
  "discriminator": s.matches(S.option(S.string)),
180
- "accounts": s.matches(S.option(S.array(S.string))),
233
+ "accounts": s.matches(S.option(S.array(svmAccountSlotSchema))),
181
234
  "args": s.matches(S.option(S.json(~validate=false))),
182
235
  }
183
236
  )
184
237
 
185
238
  let svmAbiSchema = S.schema(s =>
186
239
  {
187
- "programId": s.matches(S.string),
188
240
  "definedTypes": s.matches(S.json(~validate=false)),
189
241
  "source": s.matches(S.string),
190
242
  }
@@ -640,7 +692,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
640
692
  "eventSignatures": array<string>,
641
693
  "events": option<array<_>>,
642
694
  "svmAbi": option<{
643
- "programId": string,
644
695
  "definedTypes": JSON.t,
645
696
  "source": string,
646
697
  }>,
@@ -660,7 +711,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
660
711
  contractConfig->(
661
712
  Utils.magic: _ => {
662
713
  "svmAbi": option<{
663
- "programId": string,
664
714
  "definedTypes": JSON.t,
665
715
  "source": string,
666
716
  }>,
@@ -736,7 +786,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
736
786
  Utils.magic: _ => {
737
787
  "svm": option<{
738
788
  "discriminator": option<string>,
739
- "accounts": option<array<string>>,
789
+ "accounts": option<array<svmAccountSlotItem>>,
740
790
  "args": option<JSON.t>,
741
791
  }>,
742
792
  }
@@ -753,7 +803,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
753
803
  ~instructionName=eventName,
754
804
  ~programId,
755
805
  ~discriminator=svm["discriminator"],
756
- ~accounts=svm["accounts"]->Option.getOr([]),
806
+ ~accounts=svm["accounts"]->Option.getOr([])->Array.map(svmAccountSlotFromItem),
757
807
  ~args=svm["args"]->Option.getOr(JSON.Null),
758
808
  ~definedTypes=svmDefinedTypes,
759
809
  ) :> Internal.eventConfig)
@@ -807,6 +857,15 @@ let fromPublic = (publicConfigJson: JSON.t) => {
807
857
  let contracts =
808
858
  contractDataByName
809
859
  ->Dict.toArray
860
+ // Svm programs are defined once for the project and placed per chain by
861
+ // `program_id`. A program the config left off this chain has nothing to
862
+ // index here, and no dynamic registration can add it later.
863
+ ->Array.filter(((capitalizedName, _)) =>
864
+ switch ecosystemName {
865
+ | Ecosystem.Svm => chainContracts->Dict.get(capitalizedName)->Option.isSome
866
+ | _ => true
867
+ }
868
+ )
810
869
  ->Array.map(((capitalizedName, contractData)) => {
811
870
  let chainContract = chainContracts->Dict.get(capitalizedName)
812
871
  let rawAddresses =
@@ -914,14 +973,11 @@ let fromPublic = (publicConfigJson: JSON.t) => {
914
973
  JsError.throwWithMessage(`Chain ${chainName} is missing hypersync endpoint in config`)
915
974
  }
916
975
  | Ecosystem.Svm =>
917
- let hypersync = publicChainConfig["hypersync"]
918
- let rpc = publicChainConfig["rpc"]
919
- if hypersync->Option.isNone && rpc->Option.isNone {
920
- JsError.throwWithMessage(
921
- `Chain ${chainName} is missing a data source: provide either an rpc endpoint or an experimental hypersync config`,
922
- )
976
+ switch publicChainConfig["hypersync"] {
977
+ | Some(hypersync) => SvmSourceConfig({hypersync: hypersync})
978
+ | None =>
979
+ JsError.throwWithMessage(`Chain ${chainName} is missing hypersync endpoint in config`)
923
980
  }
924
- SvmSourceConfig({hypersync, rpc})
925
981
  }
926
982
 
927
983
  {
@@ -1168,7 +1224,6 @@ let stripSensitiveData = (json: JSON.t): JSON.t => {
1168
1224
  switch chainJson {
1169
1225
  | Object(chain) => {
1170
1226
  chain->Utils.Dict.deleteInPlace("rpcs")
1171
- chain->Utils.Dict.deleteInPlace("rpc")
1172
1227
  chain->Utils.Dict.deleteInPlace("hypersync")
1173
1228
  }
1174
1229
  | _ => ()