envio 3.0.0 → 3.0.2-svm-alpha.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 (41) hide show
  1. package/evm.schema.json +8 -8
  2. package/fuel.schema.json +12 -12
  3. package/index.d.ts +155 -1
  4. package/package.json +6 -7
  5. package/src/ChainFetcher.res +25 -1
  6. package/src/ChainFetcher.res.mjs +19 -1
  7. package/src/Config.res +145 -10
  8. package/src/Config.res.mjs +56 -16
  9. package/src/Core.res +32 -0
  10. package/src/Env.res.mjs +1 -2
  11. package/src/Envio.res +94 -0
  12. package/src/EventConfigBuilder.res +50 -0
  13. package/src/EventConfigBuilder.res.mjs +31 -0
  14. package/src/HandlerLoader.res +12 -1
  15. package/src/HandlerLoader.res.mjs +6 -1
  16. package/src/InMemoryTable.res +20 -24
  17. package/src/InMemoryTable.res.mjs +3 -19
  18. package/src/Internal.res +38 -0
  19. package/src/Main.res +53 -1
  20. package/src/Main.res.mjs +32 -0
  21. package/src/SimulateItems.res +23 -10
  22. package/src/SimulateItems.res.mjs +21 -6
  23. package/src/SvmTypes.res +9 -0
  24. package/src/SvmTypes.res.mjs +14 -0
  25. package/src/sources/EventRouter.res +65 -0
  26. package/src/sources/EventRouter.res.mjs +43 -0
  27. package/src/sources/HyperSyncClient.res +30 -157
  28. package/src/sources/HyperSyncClient.res.mjs +20 -6
  29. package/src/sources/HyperSyncSolanaClient.res +227 -0
  30. package/src/sources/HyperSyncSolanaClient.res.mjs +25 -0
  31. package/src/sources/HyperSyncSolanaSource.res +515 -0
  32. package/src/sources/HyperSyncSolanaSource.res.mjs +441 -0
  33. package/src/sources/HyperSyncSource.res +5 -8
  34. package/src/sources/HyperSyncSource.res.mjs +1 -8
  35. package/src/sources/RpcSource.res.mjs +1 -1
  36. package/src/sources/Svm.res +2 -2
  37. package/src/sources/Svm.res.mjs +3 -2
  38. package/src/tui/Tui.res +9 -2
  39. package/src/tui/Tui.res.mjs +19 -4
  40. package/src/tui/components/TuiData.res +3 -0
  41. package/svm.schema.json +345 -4
package/evm.schema.json CHANGED
@@ -4,6 +4,10 @@
4
4
  "description": "Schema for a YAML config for an envio indexer",
5
5
  "type": "object",
6
6
  "properties": {
7
+ "name": {
8
+ "description": "Name of the project",
9
+ "type": "string"
10
+ },
7
11
  "description": {
8
12
  "description": "Description of the project",
9
13
  "type": [
@@ -11,10 +15,6 @@
11
15
  "null"
12
16
  ]
13
17
  },
14
- "name": {
15
- "description": "Name of the project",
16
- "type": "string"
17
- },
18
18
  "schema": {
19
19
  "description": "Custom path to schema.graphql file",
20
20
  "type": [
@@ -67,7 +67,7 @@
67
67
  "null"
68
68
  ],
69
69
  "items": {
70
- "$ref": "#/$defs/GlobalContract_for_ContractConfig"
70
+ "$ref": "#/$defs/GlobalContract"
71
71
  }
72
72
  },
73
73
  "chains": {
@@ -189,7 +189,7 @@
189
189
  "evm"
190
190
  ]
191
191
  },
192
- "GlobalContract_for_ContractConfig": {
192
+ "GlobalContract": {
193
193
  "type": "object",
194
194
  "properties": {
195
195
  "name": {
@@ -418,7 +418,7 @@
418
418
  "null"
419
419
  ],
420
420
  "items": {
421
- "$ref": "#/$defs/ChainContract_for_ContractConfig"
421
+ "$ref": "#/$defs/ChainContract"
422
422
  }
423
423
  }
424
424
  },
@@ -578,7 +578,7 @@
578
578
  "url"
579
579
  ]
580
580
  },
581
- "ChainContract_for_ContractConfig": {
581
+ "ChainContract": {
582
582
  "type": "object",
583
583
  "properties": {
584
584
  "name": {
package/fuel.schema.json CHANGED
@@ -4,6 +4,10 @@
4
4
  "description": "Schema for a YAML config for an envio indexer",
5
5
  "type": "object",
6
6
  "properties": {
7
+ "name": {
8
+ "description": "Name of the project",
9
+ "type": "string"
10
+ },
7
11
  "description": {
8
12
  "description": "Description of the project",
9
13
  "type": [
@@ -11,10 +15,6 @@
11
15
  "null"
12
16
  ]
13
17
  },
14
- "name": {
15
- "description": "Name of the project",
16
- "type": "string"
17
- },
18
18
  "schema": {
19
19
  "description": "Custom path to schema.graphql file",
20
20
  "type": [
@@ -60,7 +60,7 @@
60
60
  "null"
61
61
  ],
62
62
  "items": {
63
- "$ref": "#/$defs/GlobalContract_for_ContractConfig"
63
+ "$ref": "#/$defs/GlobalContract"
64
64
  }
65
65
  },
66
66
  "chains": {
@@ -147,7 +147,7 @@
147
147
  "fuel"
148
148
  ]
149
149
  },
150
- "GlobalContract_for_ContractConfig": {
150
+ "GlobalContract": {
151
151
  "type": "object",
152
152
  "properties": {
153
153
  "name": {
@@ -183,6 +183,10 @@
183
183
  "EventConfig": {
184
184
  "type": "object",
185
185
  "properties": {
186
+ "name": {
187
+ "description": "Name of the event in the HyperIndex generated code",
188
+ "type": "string"
189
+ },
186
190
  "type": {
187
191
  "description": "Explicitly set the event type you want to index. It's derived from the event name and fallbacks to LogData.",
188
192
  "anyOf": [
@@ -194,10 +198,6 @@
194
198
  }
195
199
  ]
196
200
  },
197
- "name": {
198
- "description": "Name of the event in the HyperIndex generated code",
199
- "type": "string"
200
- },
201
201
  "logId": {
202
202
  "description": "An identifier of a logged type from ABI. Used for indexing LogData receipts. The option can be omitted when the event name matches the logged struct/enum name.",
203
203
  "type": [
@@ -281,7 +281,7 @@
281
281
  "null"
282
282
  ],
283
283
  "items": {
284
- "$ref": "#/$defs/ChainContract_for_ContractConfig"
284
+ "$ref": "#/$defs/ChainContract"
285
285
  }
286
286
  }
287
287
  },
@@ -304,7 +304,7 @@
304
304
  "url"
305
305
  ]
306
306
  },
307
- "ChainContract_for_ContractConfig": {
307
+ "ChainContract": {
308
308
  "type": "object",
309
309
  "properties": {
310
310
  "name": {
package/index.d.ts CHANGED
@@ -961,6 +961,125 @@ export type SvmOnSlotOptions<Config extends IndexerConfigTypes = GlobalConfig> =
961
961
  readonly where?: (args: SvmOnSlotWhereArgs<Config>) => SvmOnSlotWhereResult;
962
962
  };
963
963
 
964
+ // ============== SVM onInstruction types ==============
965
+
966
+ /** Borsh-decoded view of an instruction. Present whenever a `ProgramSchema`
967
+ * was attached to the program (bundled, Anchor IDL, or hand-written
968
+ * `accounts`/`args` in YAML). Absent when no schema applies or the
969
+ * discriminator didn't match any registered instruction. */
970
+ export type SvmDecodedInstruction = {
971
+ /** Schema-declared instruction name. */
972
+ readonly name: string;
973
+ /** Borsh-decoded args object. POC types this as `unknown`; narrow with a
974
+ * locally-declared type until the typed-args codegen lands. */
975
+ readonly args: unknown;
976
+ /** Named accounts in schema order. Keys are exactly the schema-declared
977
+ * names; values are base58 pubkeys. */
978
+ readonly accounts: Readonly<Record<string, string>>;
979
+ /** Accounts beyond the schema's named list (Anchor `remaining_accounts`,
980
+ * IDL drift). Empty when counts match the schema. */
981
+ readonly extraAccounts: readonly string[];
982
+ };
983
+
984
+ /** A single Solana instruction matched by the indexer.
985
+ *
986
+ * `data` and discriminator prefixes are `0x`-prefixed hex strings; accounts
987
+ * are base58 strings. When a Borsh schema is configured (bundled, Anchor
988
+ * IDL, or hand-written YAML), `decoded` carries the named-accounts +
989
+ * decoded-args view. */
990
+ export type SvmInstruction = {
991
+ readonly programId: string;
992
+ readonly data: string;
993
+ readonly accounts: readonly string[];
994
+ readonly instructionAddress: readonly number[];
995
+ readonly isInner: boolean;
996
+ readonly d1?: string;
997
+ readonly d2?: string;
998
+ readonly d4?: string;
999
+ readonly d8?: string;
1000
+ readonly decoded?: SvmDecodedInstruction;
1001
+ };
1002
+
1003
+ /** Parent transaction surfaced when an instruction's
1004
+ * `include_transaction` flag is `true`. */
1005
+ export type SvmTransaction = {
1006
+ readonly signatures: readonly string[];
1007
+ readonly feePayer?: string;
1008
+ readonly success?: boolean;
1009
+ readonly err?: string;
1010
+ /** Lamports. */
1011
+ readonly fee?: bigint;
1012
+ readonly computeUnitsConsumed?: bigint;
1013
+ readonly accountKeys: readonly string[];
1014
+ readonly recentBlockhash?: string;
1015
+ readonly version?: string;
1016
+ };
1017
+
1018
+ export type SvmLog = {
1019
+ readonly kind: string;
1020
+ readonly message: string;
1021
+ };
1022
+
1023
+ /** A single Solana instruction event delivered to a handler. Parameterised
1024
+ * over `Decoded` so the per-(program, instruction) overload of
1025
+ * `onInstruction` can narrow `event.instruction.decoded` to the
1026
+ * codegen-generated `{ args, accounts }` shape. */
1027
+ export type SvmInstructionEvent<
1028
+ Decoded extends SvmDecodedInstruction = SvmDecodedInstruction,
1029
+ > = {
1030
+ readonly contractName: string;
1031
+ readonly eventName: string;
1032
+ readonly instruction: Omit<SvmInstruction, "decoded"> & {
1033
+ readonly decoded?: Decoded;
1034
+ };
1035
+ /** Present when the instruction's `include_transaction` is `true`. */
1036
+ readonly transaction?: SvmTransaction;
1037
+ /** Present when the instruction's `include_logs` is `true`; only logs
1038
+ * scoped to this exact instruction (matching `instruction_address`). */
1039
+ readonly logs?: readonly SvmLog[];
1040
+ readonly slot: number;
1041
+ readonly blockTime?: number;
1042
+ };
1043
+
1044
+ /** Arguments passed to handlers registered via `indexer.onInstruction`. */
1045
+ export type SvmOnInstructionHandlerArgs<
1046
+ Config extends IndexerConfigTypes = GlobalConfig,
1047
+ Event extends SvmInstructionEvent = SvmInstructionEvent,
1048
+ > = {
1049
+ readonly event: Event;
1050
+ readonly context: SvmOnSlotContext<Config>;
1051
+ };
1052
+
1053
+ /** Shape extracted from `Global.config.svm.programs[P][I]`. The codegen
1054
+ * emits `{ args: ...; accounts: ... }` per (program, instruction); this
1055
+ * helper turns that into a `SvmDecodedInstruction`-compatible record. */
1056
+ type SvmDecodedFromProgramTable<TInstr> = TInstr extends {
1057
+ args: infer A;
1058
+ accounts: infer Acc extends Readonly<Record<string, string>>;
1059
+ }
1060
+ ? {
1061
+ readonly name: string;
1062
+ readonly args: A;
1063
+ readonly accounts: Acc;
1064
+ readonly extraAccounts: readonly string[];
1065
+ }
1066
+ : SvmDecodedInstruction;
1067
+
1068
+ /** Options for an SVM `indexer.onInstruction` registration. */
1069
+ export type SvmOnInstructionOptions<P extends string = string, I extends string = string> = {
1070
+ /** Program name as declared under `chains[].programs[].name` in
1071
+ * `config.yaml`. */
1072
+ readonly program: P;
1073
+ /** Instruction name as declared under
1074
+ * `chains[].programs[].instructions[].name` in `config.yaml`. */
1075
+ readonly instruction: I;
1076
+ };
1077
+
1078
+ /** Handler function for an SVM `indexer.onInstruction` registration. */
1079
+ export type SvmOnInstructionHandler<
1080
+ Config extends IndexerConfigTypes = GlobalConfig,
1081
+ > = (args: SvmOnInstructionHandlerArgs<Config>) => Promise<void>;
1082
+
964
1083
  // ============== Indexer Types ==============
965
1084
 
966
1085
  // Helper: Check if an ecosystem is configured. Single-ecosystem indexers only
@@ -1138,7 +1257,7 @@ type FuelEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
1138
1257
  : never
1139
1258
  : never;
1140
1259
 
1141
- // SVM ecosystem type — chains plus onSlot handler method. SVM has no onEvent yet.
1260
+ // SVM ecosystem type — chains plus instruction + slot handler methods.
1142
1261
  type SvmEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
1143
1262
  "svm" extends keyof Config
1144
1263
  ? Config["svm"] extends { chains: infer Chains }
@@ -1162,7 +1281,41 @@ type SvmEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
1162
1281
  options: SvmOnSlotOptions<Config>,
1163
1282
  handler: SvmOnSlotHandler<Config>,
1164
1283
  ) => void;
1284
+ } & (Config["svm"] extends {
1285
+ programs: infer Programs extends Record<string, Record<string, any>>;
1165
1286
  }
1287
+ ? {
1288
+ /**
1289
+ * Register an instruction handler. Dispatch matches on
1290
+ * `(programId, discriminator)` from the YAML config.
1291
+ * `event.instruction.decoded.args` and
1292
+ * `event.instruction.decoded.accounts` are typed from the
1293
+ * program's Borsh schema (Anchor IDL, bundled, or
1294
+ * hand-written `accounts`/`args` in YAML). `decoded` stays
1295
+ * optional at runtime because schema-matching can fail on
1296
+ * IDL drift or unknown discriminators.
1297
+ */
1298
+ readonly onInstruction: <
1299
+ P extends keyof Programs & string,
1300
+ I extends keyof Programs[P] & string,
1301
+ >(
1302
+ options: SvmOnInstructionOptions<P, I>,
1303
+ handler: (
1304
+ args: SvmOnInstructionHandlerArgs<
1305
+ Config,
1306
+ SvmInstructionEvent<SvmDecodedFromProgramTable<Programs[P][I]>>
1307
+ >,
1308
+ ) => Promise<void>,
1309
+ ) => void;
1310
+ }
1311
+ : {
1312
+ /** Untyped fallback for indexers with no `programs` in
1313
+ * config. `decoded` stays the generic shape. */
1314
+ readonly onInstruction: (
1315
+ options: SvmOnInstructionOptions,
1316
+ handler: SvmOnInstructionHandler<Config>,
1317
+ ) => void;
1318
+ })
1166
1319
  : never
1167
1320
  : never
1168
1321
  : never;
@@ -1178,6 +1331,7 @@ type CodegenRequiredHint =
1178
1331
  "Run 'envio codegen' to generate handler types from config.yaml. Without codegen, the indexer has no contracts, chains, or events to register handlers for.";
1179
1332
  type CodegenRequiredFallback = {
1180
1333
  readonly onEvent: (...hint: CodegenRequiredHint[]) => void;
1334
+ readonly onInstruction: (...hint: CodegenRequiredHint[]) => void;
1181
1335
  readonly onBlock: (...hint: CodegenRequiredHint[]) => void;
1182
1336
  readonly onSlot: (...hint: CodegenRequiredHint[]) => void;
1183
1337
  readonly contractRegister: (...hint: CodegenRequiredHint[]) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.0.0",
3
+ "version": "3.0.2-svm-alpha.0",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -43,7 +43,6 @@
43
43
  "@clickhouse/client": "1.17.0",
44
44
  "@elastic/ecs-pino-format": "1.4.0",
45
45
  "@envio-dev/hyperfuel-client": "1.2.2",
46
- "@envio-dev/hypersync-client": "1.3.0",
47
46
  "@fuel-ts/crypto": "0.96.1",
48
47
  "@fuel-ts/errors": "0.96.1",
49
48
  "@fuel-ts/hasher": "0.96.1",
@@ -71,10 +70,10 @@
71
70
  "tsx": "4.21.0"
72
71
  },
73
72
  "optionalDependencies": {
74
- "envio-linux-x64": "3.0.0",
75
- "envio-linux-x64-musl": "3.0.0",
76
- "envio-linux-arm64": "3.0.0",
77
- "envio-darwin-x64": "3.0.0",
78
- "envio-darwin-arm64": "3.0.0"
73
+ "envio-linux-x64": "3.0.2-svm-alpha.0",
74
+ "envio-linux-x64-musl": "3.0.2-svm-alpha.0",
75
+ "envio-linux-arm64": "3.0.2-svm-alpha.0",
76
+ "envio-darwin-x64": "3.0.2-svm-alpha.0",
77
+ "envio-darwin-arm64": "3.0.2-svm-alpha.0"
79
78
  }
80
79
  }
@@ -233,7 +233,31 @@ let make = (
233
233
  ~lowercaseAddresses,
234
234
  )
235
235
  | Config.FuelSourceConfig({hypersync}) => [HyperFuelSource.make({chain, endpointUrl: hypersync})]
236
- | Config.SvmSourceConfig({rpc}) => [Svm.makeRPCSource(~chain, ~rpc)]
236
+ | Config.SvmSourceConfig({hypersync, rpc}) =>
237
+ switch hypersync {
238
+ | None => [Svm.makeRPCSource(~chain, ~rpc)]
239
+ | Some(hypersyncUrl) =>
240
+ // HyperSync drives instruction sync; RPC remains the height oracle
241
+ // (Svm.makeRPCSource's `getFinalizedSlot` route) and the fallback.
242
+ let svmEventConfigs =
243
+ chainConfig.contracts
244
+ ->Array.flatMap(contract => contract.events)
245
+ ->(
246
+ Utils.magic: array<Internal.eventConfig> => array<Internal.svmInstructionEventConfig>
247
+ )
248
+ let apiToken = Env.envioApiToken
249
+ [
250
+ HyperSyncSolanaSource.make({
251
+ chain,
252
+ endpointUrl: hypersyncUrl,
253
+ apiToken,
254
+ eventConfigs: svmEventConfigs,
255
+ clientMaxRetries: Env.hyperSyncClientMaxRetries,
256
+ clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
257
+ }),
258
+ Svm.makeRPCSource(~chain, ~rpc, ~sourceFor=Fallback),
259
+ ]
260
+ }
237
261
  // For tests: use ready-to-use sources directly
238
262
  | Config.CustomSources(sources) => sources
239
263
  }
@@ -21,6 +21,7 @@ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
21
21
  import * as HyperFuelSource from "./sources/HyperFuelSource.res.mjs";
22
22
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
23
23
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
24
+ import * as HyperSyncSolanaSource from "./sources/HyperSyncSolanaSource.res.mjs";
24
25
  import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
25
26
 
26
27
  function configAddresses(chainConfig) {
@@ -133,7 +134,24 @@ function make(chainConfig, indexingAddresses, startBlock, endBlock, firstEventBl
133
134
  })];
134
135
  break;
135
136
  case "SvmSourceConfig" :
136
- sources$1 = [Svm.makeRPCSource(chain, sources.rpc)];
137
+ let rpc = sources.rpc;
138
+ let hypersync = sources.hypersync;
139
+ if (hypersync !== undefined) {
140
+ let svmEventConfigs = chainConfig.contracts.flatMap(contract => contract.events);
141
+ sources$1 = [
142
+ HyperSyncSolanaSource.make({
143
+ chain: chain,
144
+ endpointUrl: hypersync,
145
+ apiToken: Env.envioApiToken,
146
+ eventConfigs: svmEventConfigs,
147
+ clientMaxRetries: Env.hyperSyncClientMaxRetries,
148
+ clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis
149
+ }),
150
+ Svm.makeRPCSource(chain, rpc, "Fallback")
151
+ ];
152
+ } else {
153
+ sources$1 = [Svm.makeRPCSource(chain, rpc, undefined)];
154
+ }
137
155
  break;
138
156
  case "CustomSources" :
139
157
  sources$1 = sources._0;
package/src/Config.res CHANGED
@@ -30,7 +30,7 @@ type evmRpcConfig = {
30
30
  type sourceConfig =
31
31
  | EvmSourceConfig({hypersync: option<string>, rpcs: array<evmRpcConfig>})
32
32
  | FuelSourceConfig({hypersync: string})
33
- | SvmSourceConfig({rpc: string})
33
+ | SvmSourceConfig({hypersync: option<string>, rpc: string})
34
34
  // For tests: pass custom sources directly
35
35
  | CustomSources(array<Source.t>)
36
36
 
@@ -201,6 +201,38 @@ let publicConfigChainSchema = S.schema(s =>
201
201
  }
202
202
  )
203
203
 
204
+ let svmEventDescriptorSchema = S.schema(s =>
205
+ {
206
+ "discriminator": s.matches(S.option(S.string)),
207
+ "discriminatorByteLen": s.matches(S.int),
208
+ "includeTransaction": s.matches(S.bool),
209
+ "includeLogs": s.matches(S.bool),
210
+ "accountFilters": s.matches(
211
+ S.option(
212
+ S.array(
213
+ S.schema(
214
+ s => {
215
+ "position": s.matches(S.int),
216
+ "values": s.matches(S.array(S.string)),
217
+ },
218
+ ),
219
+ ),
220
+ ),
221
+ ),
222
+ "isInner": s.matches(S.option(S.bool)),
223
+ "accounts": s.matches(S.option(S.array(S.string))),
224
+ "args": s.matches(S.option(S.json(~validate=false))),
225
+ }
226
+ )
227
+
228
+ let svmAbiSchema = S.schema(s =>
229
+ {
230
+ "programId": s.matches(S.string),
231
+ "definedTypes": s.matches(S.json(~validate=false)),
232
+ "source": s.matches(S.string),
233
+ }
234
+ )
235
+
204
236
  let contractEventItemSchema = S.schema(s =>
205
237
  {
206
238
  "event": s.matches(S.string),
@@ -210,6 +242,7 @@ let contractEventItemSchema = S.schema(s =>
210
242
  "kind": s.matches(S.option(S.string)),
211
243
  "blockFields": s.matches(S.option(S.array(Internal.evmBlockFieldSchema))),
212
244
  "transactionFields": s.matches(S.option(S.array(Internal.evmTransactionFieldSchema))),
245
+ "svm": s.matches(S.option(svmEventDescriptorSchema)),
213
246
  }
214
247
  )
215
248
 
@@ -219,6 +252,8 @@ let contractConfigSchema = S.schema(s =>
219
252
  "handler": s.matches(S.option(S.string)),
220
253
  // EVM-specific: event signatures for HyperSync queries
221
254
  "events": s.matches(S.option(S.array(contractEventItemSchema))),
255
+ // SVM-only: program-level Borsh schema (defined-types registry, source).
256
+ "svmAbi": s.matches(S.option(svmAbiSchema)),
222
257
  }
223
258
  )
224
259
 
@@ -226,6 +261,10 @@ let publicConfigEcosystemSchema = S.schema(s =>
226
261
  {
227
262
  "chains": s.matches(S.dict(publicConfigChainSchema)),
228
263
  "contracts": s.matches(S.option(S.dict(contractConfigSchema))),
264
+ // SVM-only alias: programs are the SVM analog of EVM/Fuel contracts.
265
+ // Parsed via the same `contractConfigSchema` and read in `fromPublic`'s
266
+ // `publicContractsConfig` switch.
267
+ "programs": s.matches(S.option(S.dict(contractConfigSchema))),
229
268
  }
230
269
  )
231
270
 
@@ -523,10 +562,19 @@ let fromPublic = (publicConfigJson: JSON.t) => {
523
562
  | None => false
524
563
  }
525
564
 
526
- // Parse contract configs (ABIs, events, handlers)
527
- let publicContractsConfig = switch (ecosystemName, publicConfig["evm"], publicConfig["fuel"]) {
528
- | (Ecosystem.Evm, Some(evm), _) => evm["contracts"]
529
- | (Ecosystem.Fuel, _, Some(fuel)) => fuel["contracts"]
565
+ // Parse contract configs (ABIs, events, handlers).
566
+ // SVM stores them under `svm.programs` in the public JSON — the per-program
567
+ // events drive `indexer.onInstruction` registration the same way EVM/Fuel
568
+ // contracts drive `onEvent`.
569
+ let publicContractsConfig = switch (
570
+ ecosystemName,
571
+ publicConfig["evm"],
572
+ publicConfig["fuel"],
573
+ publicConfig["svm"],
574
+ ) {
575
+ | (Ecosystem.Evm, Some(evm), _, _) => evm["contracts"]
576
+ | (Ecosystem.Fuel, _, Some(fuel), _) => fuel["contracts"]
577
+ | (Ecosystem.Svm, _, _, Some(svm)) => svm["programs"]
530
578
  | _ => None
531
579
  }
532
580
 
@@ -549,6 +597,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
549
597
  "abi": EvmTypes.Abi.t,
550
598
  "eventSignatures": array<string>,
551
599
  "events": option<array<_>>,
600
+ "svmAbi": option<{"programId": string, "definedTypes": JSON.t, "source": string}>,
552
601
  }> = Dict.make()
553
602
  switch publicContractsConfig {
554
603
  | Some(contractsDict) =>
@@ -561,21 +610,40 @@ let fromPublic = (publicConfigJson: JSON.t) => {
561
610
  | Some(events) => events->Array.map(eventItem => eventItem["event"])
562
611
  | None => []
563
612
  }
613
+ let widened =
614
+ contractConfig->(
615
+ Utils.magic: _ => {
616
+ "svmAbi": option<{"programId": string, "definedTypes": JSON.t, "source": string}>,
617
+ }
618
+ )
564
619
  contractDataByName->Dict.set(
565
620
  capitalizedName,
566
- {"abi": abi, "eventSignatures": eventSignatures, "events": contractConfig["events"]},
621
+ {
622
+ "abi": abi,
623
+ "eventSignatures": eventSignatures,
624
+ "events": contractConfig["events"],
625
+ "svmAbi": widened["svmAbi"],
626
+ },
567
627
  )
568
628
  })
569
629
  | None => ()
570
630
  }
571
631
 
572
- // Build event configs for a contract from JSON event items
632
+ // Build event configs for a contract from JSON event items.
633
+ //
634
+ // `~addresses` is the chain-side address list. For SVM programs it's the
635
+ // single base58 program_id — wired onto each instruction's event config so
636
+ // the source can build `(programId, discriminator)`-keyed InstructionSelections.
637
+ // EVM and Fuel ignore it (the address lives in `ChainContract.addresses` and
638
+ // is looked up at dispatch time, not stamped on the event).
573
639
  let buildContractEvents = (
574
640
  ~contractName,
575
641
  ~events: option<array<_>>,
576
642
  ~abi,
577
643
  ~chainId: int,
578
644
  ~startBlock: option<int>,
645
+ ~addresses: array<string>,
646
+ ~svmDefinedTypes: JSON.t=JSON.Null,
579
647
  ) => {
580
648
  switch events {
581
649
  | None => []
@@ -606,6 +674,65 @@ let fromPublic = (publicConfigJson: JSON.t) => {
606
674
  `Fuel event ${contractName}.${eventName} is missing "kind" in internal config`,
607
675
  )
608
676
  }
677
+ | Ecosystem.Svm =>
678
+ let programId = switch addresses {
679
+ | [pid] => pid->SvmTypes.Pubkey.fromStringUnsafe
680
+ | [] =>
681
+ JsError.throwWithMessage(
682
+ `SVM program ${contractName} on chain ${chainId->Int.toString} is missing a program_id`,
683
+ )
684
+ | _ =>
685
+ JsError.throwWithMessage(
686
+ `SVM program ${contractName} on chain ${chainId->Int.toString} has multiple addresses; a program is uniquely identified by a single program_id`,
687
+ )
688
+ }
689
+ let widenedEventItem =
690
+ eventItem->(
691
+ Utils.magic: _ => {
692
+ "svm": option<{
693
+ "discriminator": option<string>,
694
+ "discriminatorByteLen": int,
695
+ "includeTransaction": bool,
696
+ "includeLogs": bool,
697
+ "accountFilters": option<
698
+ array<{"position": int, "values": array<string>}>,
699
+ >,
700
+ "isInner": option<bool>,
701
+ "accounts": option<array<string>>,
702
+ "args": option<JSON.t>,
703
+ }>,
704
+ }
705
+ )
706
+ let svm = switch widenedEventItem["svm"] {
707
+ | Some(s) => s
708
+ | None =>
709
+ JsError.throwWithMessage(
710
+ `SVM instruction ${contractName}.${eventName} is missing the "svm" descriptor in internal config`,
711
+ )
712
+ }
713
+ let accountFilters =
714
+ (svm["accountFilters"]->Option.getOr([]))->Array.map(af => {
715
+ Internal.position: af["position"],
716
+ values: af["values"]->SvmTypes.Pubkey.fromStringsUnsafe,
717
+ })
718
+ (EventConfigBuilder.buildSvmInstructionEventConfig(
719
+ ~contractName,
720
+ ~instructionName=eventName,
721
+ ~programId,
722
+ ~discriminator=svm["discriminator"],
723
+ ~discriminatorByteLen=svm["discriminatorByteLen"],
724
+ ~includeTransaction=svm["includeTransaction"],
725
+ ~includeLogs=svm["includeLogs"],
726
+ ~accountFilters,
727
+ ~isInner=svm["isInner"],
728
+ ~isWildcard=false,
729
+ ~handler=None,
730
+ ~contractRegister=None,
731
+ ~accounts=svm["accounts"]->Option.getOr([]),
732
+ ~args=svm["args"]->Option.getOr(JSON.Null),
733
+ ~definedTypes=svmDefinedTypes,
734
+ ~startBlock?,
735
+ ) :> Internal.eventConfig)
609
736
  | _ =>
610
737
  (EventConfigBuilder.buildEvmEventConfig(
611
738
  ~contractName,
@@ -665,11 +792,11 @@ let fromPublic = (publicConfigJson: JSON.t) => {
665
792
  ->Dict.toArray
666
793
  ->Array.map(((capitalizedName, contractData)) => {
667
794
  let chainContract = chainContracts->Dict.get(capitalizedName)
668
- let addresses =
795
+ let rawAddresses =
669
796
  chainContract
670
797
  ->Option.flatMap(cc => cc["addresses"])
671
798
  ->Option.getOr([])
672
- ->Array.map(parseAddress)
799
+ let addresses = rawAddresses->Array.map(parseAddress)
673
800
  let startBlock = chainContract->Option.flatMap(cc => cc["startBlock"])
674
801
 
675
802
  // Build event configs from JSON (field selections resolved inline)
@@ -683,6 +810,10 @@ let fromPublic = (publicConfigJson: JSON.t) => {
683
810
  ~abi=contractData["abi"],
684
811
  ~chainId,
685
812
  ~startBlock,
813
+ ~addresses=rawAddresses,
814
+ ~svmDefinedTypes=contractData["svmAbi"]
815
+ ->Option.map(a => a["definedTypes"])
816
+ ->Option.getOr(JSON.Null),
686
817
  )
687
818
 
688
819
  {
@@ -748,7 +879,11 @@ let fromPublic = (publicConfigJson: JSON.t) => {
748
879
  }
749
880
  | Ecosystem.Svm =>
750
881
  switch publicChainConfig["rpc"] {
751
- | Some(rpc) => SvmSourceConfig({rpc: rpc})
882
+ | Some(rpc) =>
883
+ SvmSourceConfig({
884
+ hypersync: publicChainConfig["hypersync"],
885
+ rpc,
886
+ })
752
887
  | None => JsError.throwWithMessage(`Chain ${chainName} is missing rpc endpoint in config`)
753
888
  }
754
889
  }