envio 3.0.1 → 3.0.2-svm-alpha.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 (43) hide show
  1. package/evm.schema.json +8 -8
  2. package/fuel.schema.json +12 -12
  3. package/index.d.ts +167 -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 +158 -94
  8. package/src/Config.res.mjs +60 -97
  9. package/src/Core.res +32 -0
  10. package/src/Env.res.mjs +1 -2
  11. package/src/Envio.res +103 -0
  12. package/src/EventConfigBuilder.res +52 -0
  13. package/src/EventConfigBuilder.res.mjs +32 -0
  14. package/src/HandlerLoader.res +12 -1
  15. package/src/HandlerLoader.res.mjs +6 -1
  16. package/src/Hasura.res +43 -0
  17. package/src/Hasura.res.mjs +38 -0
  18. package/src/Internal.res +39 -0
  19. package/src/Main.res +53 -3
  20. package/src/Main.res.mjs +34 -2
  21. package/src/Persistence.res +2 -17
  22. package/src/Persistence.res.mjs +2 -14
  23. package/src/SimulateItems.res +23 -10
  24. package/src/SimulateItems.res.mjs +21 -6
  25. package/src/SvmTypes.res +9 -0
  26. package/src/SvmTypes.res.mjs +14 -0
  27. package/src/sources/EventRouter.res +65 -0
  28. package/src/sources/EventRouter.res.mjs +43 -0
  29. package/src/sources/HyperSyncClient.res +30 -157
  30. package/src/sources/HyperSyncClient.res.mjs +20 -6
  31. package/src/sources/HyperSyncSolanaClient.res +249 -0
  32. package/src/sources/HyperSyncSolanaClient.res.mjs +25 -0
  33. package/src/sources/HyperSyncSolanaSource.res +566 -0
  34. package/src/sources/HyperSyncSolanaSource.res.mjs +488 -0
  35. package/src/sources/HyperSyncSource.res +5 -8
  36. package/src/sources/HyperSyncSource.res.mjs +1 -8
  37. package/src/sources/RpcSource.res.mjs +1 -1
  38. package/src/sources/Svm.res +2 -2
  39. package/src/sources/Svm.res.mjs +3 -2
  40. package/src/tui/Tui.res +9 -2
  41. package/src/tui/Tui.res.mjs +19 -4
  42. package/src/tui/components/TuiData.res +3 -0
  43. package/svm.schema.json +352 -4
package/src/Core.res CHANGED
@@ -4,9 +4,41 @@
4
4
 
5
5
  // NAPI encodes Rust `Option<T>` as `null | T` (never `undefined`), so the
6
6
  // tighter `Null.t` captures the exact boundary shape.
7
+ //
8
+ // Opaque carriers for the NAPI class constructors. Static factories
9
+ // (`newWithAgent`, `fromConfig`, `fromSignatures`) hang off these via `@send`
10
+ // in `HyperSyncClient.res` / `HyperSyncSolanaClient.res`.
11
+ type hypersyncClientCtor
12
+ type hypersyncSolanaClientCtor
13
+ type decoderCtor
14
+
15
+ /// JS shape of one decoded instruction. Mirrors `DecodedInstructionJson` in
16
+ /// `packages/cli/src/hypersync_source_svm/decoder.rs`. The `argsJson` /
17
+ /// `accountsJson` fields are stringified to side-step napi-rs's lack of
18
+ /// native `serde_json::Value` passthrough; callers `JSON.parse` once.
19
+ type svmDecodedInstruction = {
20
+ name: string,
21
+ argsJson: string,
22
+ accountsJson: string,
23
+ extraAccounts: array<string>,
24
+ }
25
+
7
26
  type addon = {
8
27
  getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
9
28
  runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
29
+ @as("HypersyncClient")
30
+ hypersyncClient: hypersyncClientCtor,
31
+ @as("HypersyncSolanaClient")
32
+ hypersyncSolanaClient: hypersyncSolanaClientCtor,
33
+ @as("Decoder")
34
+ decoder: decoderCtor,
35
+ setLogLevel: string => unit,
36
+ registerProgramSchema: (~descriptorJson: string) => int,
37
+ decodeInstruction: (
38
+ ~schemaHandle: int,
39
+ ~dataHex: string,
40
+ ~accounts: array<string>,
41
+ ) => Null.t<svmDecodedInstruction>,
10
42
  }
11
43
 
12
44
  @module("node:module") external createRequire: string => {..} = "createRequire"
package/src/Env.res.mjs CHANGED
@@ -7,7 +7,6 @@ import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js";
7
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
8
  import * as HyperSyncClient from "./sources/HyperSyncClient.res.mjs";
9
9
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
10
- import * as HypersyncClient from "@envio-dev/hypersync-client";
11
10
 
12
11
  import 'dotenv/config'
13
12
  ;
@@ -62,7 +61,7 @@ let hypersyncClientEnableQueryCaching = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_CL
62
61
 
63
62
  let hypersyncLogLevel = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_LOG_LEVEL", HyperSyncClient.logLevelSchema, undefined, "info", undefined, undefined);
64
63
 
65
- HypersyncClient.setLogLevel(hypersyncLogLevel);
64
+ HyperSyncClient.setLogLevel(hypersyncLogLevel);
66
65
 
67
66
  let logStrategy = EnvSafe.get(envSafe, "LOG_STRATEGY", S$RescriptSchema.$$enum([
68
67
  "ecs-file",
package/src/Envio.res CHANGED
@@ -20,6 +20,109 @@ type svmOnSlotArgs<'context> = {
20
20
  context: 'context,
21
21
  }
22
22
 
23
+ /** Borsh-decoded instruction view. Present whenever a `ProgramSchema` was
24
+ attached to the program (bundled schema, Anchor IDL, or hand-written YAML
25
+ `accounts`/`args`). Absent (`None`) when no schema applied or the
26
+ discriminator didn't match any registered instruction. */
27
+ type svmDecodedInstruction = {
28
+ /** Schema-declared instruction name (matches the codegen module suffix). */
29
+ name: string,
30
+ /** Borsh-decoded args. `JSON.Object({})` for no-arg instructions
31
+ (e.g. `VerifyCollection`). POC types this as raw `JSON.t`; cast at the
32
+ handler with `(json :> MyArgsType)` until typed codegen lands. */
33
+ args: JSON.t,
34
+ /** Named accounts in schema order. Keys are exactly the schema-declared
35
+ names; values are base58 pubkey strings. */
36
+ accounts: dict<string>,
37
+ /** Accounts beyond the schema's named list (Anchor `remaining_accounts`,
38
+ IDL drift). `[]` when counts match. */
39
+ extraAccounts: array<string>,
40
+ }
41
+
42
+ type svmInstruction = {
43
+ programId: SvmTypes.Pubkey.t,
44
+ /** Raw instruction bytes as `0x`-prefixed hex. */
45
+ data: string,
46
+ accounts: array<SvmTypes.Pubkey.t>,
47
+ /** Path through the call tree: `[outerIndex]` for top-level instructions,
48
+ appended child indices for inner CPI calls. */
49
+ instructionAddress: array<int>,
50
+ isInner: bool,
51
+ /** Discriminator prefixes pre-extracted by HyperSync. Each is `Some` only
52
+ when the underlying instruction is at least that long. */
53
+ d1?: string,
54
+ d2?: string,
55
+ d4?: string,
56
+ d8?: string,
57
+ /** Borsh-decoded view. See [[svmDecodedInstruction]]. */
58
+ decoded?: svmDecodedInstruction,
59
+ }
60
+
61
+ type svmTokenBalance = {
62
+ account?: SvmTypes.Pubkey.t,
63
+ mint?: SvmTypes.Pubkey.t,
64
+ owner?: SvmTypes.Pubkey.t,
65
+ preAmount?: string,
66
+ postAmount?: string,
67
+ }
68
+
69
+ type svmTransaction = {
70
+ signatures: array<string>,
71
+ feePayer?: SvmTypes.Pubkey.t,
72
+ success?: bool,
73
+ err?: string,
74
+ fee?: bigint,
75
+ computeUnitsConsumed?: bigint,
76
+ accountKeys: array<SvmTypes.Pubkey.t>,
77
+ recentBlockhash?: string,
78
+ version?: string,
79
+ tokenBalances?: array<svmTokenBalance>,
80
+ }
81
+
82
+ type svmLog = {
83
+ kind: string,
84
+ message: string,
85
+ }
86
+
87
+ /** Inner block record on `svmInstructionEvent`. Field names follow EVM/Fuel
88
+ (`height`, `time`, `hash`) so the shared `Ecosystem.t` getters in
89
+ `Svm.res` work uniformly across ecosystems — `height` carries the slot. */
90
+ type svmInstructionEventBlock = {
91
+ /** Slot number. Named `height` so the shared ecosystem getter reads it. */
92
+ height: int,
93
+ /** Unix block time (seconds). `0` when HyperSync didn't return a block
94
+ for this instruction's slot. */
95
+ time: int,
96
+ /** Block hash. Currently always empty — populated by the future
97
+ reorg-guard `queryBlockHash(slot)` route. */
98
+ hash: string,
99
+ }
100
+
101
+ /** The per-instruction payload handlers receive on `.event`. Mirrors the
102
+ EVM `type event` shape inside generated per-event modules. */
103
+ type svmInstructionEvent = {
104
+ contractName: string,
105
+ eventName: string,
106
+ instruction: svmInstruction,
107
+ /** Parent transaction. `None` when the per-instruction
108
+ `include_transaction` flag is `false`. */
109
+ transaction: option<svmTransaction>,
110
+ /** Program log entries scoped to this instruction. `None` when the
111
+ per-instruction `include_logs` flag is `false`. */
112
+ logs: option<array<svmLog>>,
113
+ /** Convenience alias for `block.height`. */
114
+ slot: int,
115
+ /** Convenience alias for `block.time`. */
116
+ blockTime: option<int>,
117
+ block: svmInstructionEventBlock,
118
+ }
119
+
120
+ /** Arguments passed to handlers registered via `indexer.onInstruction`. */
121
+ type svmOnInstructionArgs<'context> = {
122
+ event: svmInstructionEvent,
123
+ context: 'context,
124
+ }
125
+
23
126
  // Internal-only type for the `indexer.onBlock` (and SVM `onSlot`) `where`
24
127
  // callback argument. The canonical TypeScript shape lives in
25
128
  // `packages/envio/index.d.ts`; the ReScript declaration here is free to
@@ -484,6 +484,58 @@ let buildEvmEventConfig = (
484
484
 
485
485
  // ============== Build Fuel event config ==============
486
486
 
487
+ let buildSvmInstructionEventConfig = (
488
+ ~contractName: string,
489
+ ~instructionName: string,
490
+ ~programId: SvmTypes.Pubkey.t,
491
+ ~discriminator: option<string>,
492
+ ~discriminatorByteLen: int,
493
+ ~includeTransaction: bool,
494
+ ~includeLogs: bool,
495
+ ~includeTokenBalances: bool,
496
+ ~accountFilters: array<Internal.svmAccountFilter>,
497
+ ~isInner: option<bool>,
498
+ ~isWildcard: bool,
499
+ ~handler: option<Internal.handler>,
500
+ ~contractRegister: option<Internal.contractRegister>,
501
+ ~accounts: array<string>=[],
502
+ ~args: JSON.t=JSON.Null,
503
+ ~definedTypes: JSON.t=JSON.Null,
504
+ ~startBlock: option<int>=?,
505
+ ): Internal.svmInstructionEventConfig => {
506
+ let paramsSchema =
507
+ S.json(~validate=false)
508
+ ->Utils.Schema.coerceToJsonPgType
509
+ ->(Utils.magic: S.t<JSON.t> => S.t<Internal.eventParams>)
510
+ {
511
+ id: switch discriminator {
512
+ | Some(d) => d
513
+ | None => "none"
514
+ },
515
+ name: instructionName,
516
+ contractName,
517
+ isWildcard,
518
+ handler,
519
+ contractRegister,
520
+ paramsRawEventSchema: paramsSchema,
521
+ simulateParamsSchema: paramsSchema,
522
+ filterByAddresses: false,
523
+ dependsOnAddresses: !isWildcard,
524
+ startBlock,
525
+ programId,
526
+ discriminator,
527
+ discriminatorByteLen,
528
+ includeTransaction,
529
+ includeLogs,
530
+ includeTokenBalances,
531
+ accountFilters,
532
+ isInner,
533
+ accounts,
534
+ args,
535
+ definedTypes,
536
+ }
537
+ }
538
+
487
539
  let buildFuelEventConfig = (
488
540
  ~contractName: string,
489
541
  ~eventName: string,
@@ -350,6 +350,37 @@ function buildEvmEventConfig(contractName, eventName, sighash, params, isWildcar
350
350
  };
351
351
  }
352
352
 
353
+ function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator, discriminatorByteLen, includeTransaction, includeLogs, includeTokenBalances, accountFilters, isInner, isWildcard, handler, contractRegister, accountsOpt, argsOpt, definedTypesOpt, startBlock) {
354
+ let accounts = accountsOpt !== undefined ? accountsOpt : [];
355
+ let args = argsOpt !== undefined ? argsOpt : null;
356
+ let definedTypes = definedTypesOpt !== undefined ? definedTypesOpt : null;
357
+ let paramsSchema = Utils.Schema.coerceToJsonPgType(S$RescriptSchema.json(false));
358
+ return {
359
+ id: discriminator !== undefined ? discriminator : "none",
360
+ name: instructionName,
361
+ contractName: contractName,
362
+ isWildcard: isWildcard,
363
+ filterByAddresses: false,
364
+ dependsOnAddresses: !isWildcard,
365
+ handler: handler,
366
+ contractRegister: contractRegister,
367
+ paramsRawEventSchema: paramsSchema,
368
+ simulateParamsSchema: paramsSchema,
369
+ startBlock: startBlock,
370
+ programId: programId,
371
+ discriminator: discriminator,
372
+ discriminatorByteLen: discriminatorByteLen,
373
+ includeTransaction: includeTransaction,
374
+ includeLogs: includeLogs,
375
+ includeTokenBalances: includeTokenBalances,
376
+ accountFilters: accountFilters,
377
+ isInner: isInner,
378
+ accounts: accounts,
379
+ args: args,
380
+ definedTypes: definedTypes
381
+ };
382
+ }
383
+
353
384
  function buildFuelEventConfig(contractName, eventName, kind, sighash, rawAbi, isWildcard, handler, contractRegister, startBlock) {
354
385
  let fuelKind;
355
386
  switch (kind) {
@@ -428,6 +459,7 @@ export {
428
459
  alwaysIncludedBlockFields,
429
460
  resolveFieldSelection,
430
461
  buildEvmEventConfig,
462
+ buildSvmInstructionEventConfig,
431
463
  buildFuelEventConfig,
432
464
  }
433
465
  /* eventParamComponentSchema Not a pure module */
@@ -142,7 +142,18 @@ let applyRegistrations = (~config: Config.t): Config.t => {
142
142
  dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses),
143
143
  } :> Internal.eventConfig)
144
144
  | Svm =>
145
- JsError.throwWithMessage(`SVM does not support indexer.onEvent or indexer.contractRegister. Use indexer.onSlot for per-slot handlers.`)
145
+ let svmEv =
146
+ ev->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
147
+ ({
148
+ ...svmEv,
149
+ isWildcard,
150
+ handler,
151
+ contractRegister,
152
+ dependsOnAddresses: Internal.dependsOnAddresses(
153
+ ~isWildcard,
154
+ ~filterByAddresses=false,
155
+ ),
156
+ } :> Internal.eventConfig)
146
157
  }
147
158
  },
148
159
  )
@@ -109,7 +109,12 @@ function applyRegistrations(config) {
109
109
  kind: ev.kind
110
110
  };
111
111
  case "svm" :
112
- return Stdlib_JsError.throwWithMessage(`SVM does not support indexer.onEvent or indexer.contractRegister. Use indexer.onSlot for per-slot handlers.`);
112
+ let newrecord = {...ev};
113
+ newrecord.contractRegister = contractRegister;
114
+ newrecord.handler = handler;
115
+ newrecord.dependsOnAddresses = Internal.dependsOnAddresses(isWildcard, false);
116
+ newrecord.isWildcard = isWildcard;
117
+ return newrecord;
113
118
  }
114
119
  });
115
120
  return {
package/src/Hasura.res CHANGED
@@ -40,6 +40,19 @@ let clearMetadataRoute = Rest.route(() => {
40
40
  responses,
41
41
  })
42
42
 
43
+ let reloadMetadataRoute = Rest.route(() => {
44
+ method: Post,
45
+ path: "",
46
+ input: s => {
47
+ let _ = s.field("type", S.literal("reload_metadata"))
48
+ {
49
+ "args": s.field("args", S.json(~validate=false)),
50
+ "auth": s->auth,
51
+ }
52
+ },
53
+ responses,
54
+ })
55
+
43
56
  let trackTablesRoute = Rest.route(() => {
44
57
  method: Post,
45
58
  path: "",
@@ -110,6 +123,31 @@ let clearHasuraMetadata = async (~endpoint, ~auth) => {
110
123
  }
111
124
  }
112
125
 
126
+ let reloadHasuraMetadata = async (~endpoint, ~auth) => {
127
+ try {
128
+ let result = await reloadMetadataRoute->Rest.fetch(
129
+ {
130
+ "auth": auth,
131
+ "args": {
132
+ "reload_sources": ["default"],
133
+ }->(Utils.magic: 'a => JSON.t),
134
+ },
135
+ ~client=Rest.client(endpoint),
136
+ )
137
+ let msg = switch result {
138
+ | QuerySucceeded => "Hasura metadata reloaded"
139
+ | AlreadyDone => "Hasura metadata reload acknowledged"
140
+ }
141
+ Logging.trace(msg)
142
+ } catch {
143
+ | exn =>
144
+ Logging.error({
145
+ "msg": `There was an issue reloading hasura metadata - table tracking may race with schema creation.`,
146
+ "err": exn->Utils.prettifyExn,
147
+ })
148
+ }
149
+ }
150
+
113
151
  let trackTables = async (~endpoint, ~auth, ~pgSchema, ~tableNames: array<string>) => {
114
152
  try {
115
153
  let result = await trackTablesRoute->Rest.fetch(
@@ -242,6 +280,11 @@ let trackDatabase = async (
242
280
 
243
281
  let _ = await clearHasuraMetadata(~endpoint, ~auth)
244
282
 
283
+ // Force Hasura to re-introspect the source schema before tracking, otherwise
284
+ // freshly-created user tables may be invisible to pg_track_tables and the call
285
+ // returns `metadata-warnings` (HTTP 400), leaving tracking permanently broken.
286
+ await reloadHasuraMetadata(~endpoint, ~auth)
287
+
245
288
  await trackTables(~endpoint, ~auth, ~pgSchema, ~tableNames)
246
289
 
247
290
  for i in 0 to tableNames->Array.length - 1 {
@@ -46,6 +46,21 @@ function clearMetadataRoute() {
46
46
  };
47
47
  }
48
48
 
49
+ function reloadMetadataRoute() {
50
+ return {
51
+ method: "POST",
52
+ path: "",
53
+ input: s => {
54
+ s.field("type", S$RescriptSchema.literal("reload_metadata"));
55
+ return {
56
+ args: s.field("args", S$RescriptSchema.json(false)),
57
+ auth: auth(s)
58
+ };
59
+ },
60
+ responses: responses
61
+ };
62
+ }
63
+
49
64
  function trackTablesRoute() {
50
65
  return {
51
66
  method: "POST",
@@ -112,6 +127,26 @@ async function clearHasuraMetadata(endpoint, auth) {
112
127
  }
113
128
  }
114
129
 
130
+ async function reloadHasuraMetadata(endpoint, auth) {
131
+ try {
132
+ let result = await Rest.fetch(reloadMetadataRoute, {
133
+ auth: auth,
134
+ args: {
135
+ reload_sources: ["default"]
136
+ }
137
+ }, Rest.client(endpoint, undefined));
138
+ let tmp;
139
+ tmp = result === "QuerySucceeded" ? "Hasura metadata reloaded" : "Hasura metadata reload acknowledged";
140
+ return Logging.trace(tmp);
141
+ } catch (raw_exn) {
142
+ let exn = Primitive_exceptions.internalToException(raw_exn);
143
+ return Logging.error({
144
+ msg: `There was an issue reloading hasura metadata - table tracking may race with schema creation.`,
145
+ err: Utils.prettifyExn(exn)
146
+ });
147
+ }
148
+ }
149
+
115
150
  async function trackTables(endpoint, auth, pgSchema, tableNames) {
116
151
  try {
117
152
  let result = await Rest.fetch(trackTablesRoute, {
@@ -202,6 +237,7 @@ async function trackDatabase(endpoint, auth, pgSchema, userEntities, aggregateEn
202
237
  ]);
203
238
  Logging.info("Tracking tables in Hasura");
204
239
  await clearHasuraMetadata(endpoint, auth);
240
+ await reloadHasuraMetadata(endpoint, auth);
205
241
  await trackTables(endpoint, auth, pgSchema, tableNames);
206
242
  for (let i = 0, i_finish = tableNames.length; i < i_finish; ++i) {
207
243
  let tableName = tableNames[i];
@@ -231,10 +267,12 @@ export {
231
267
  auth,
232
268
  responses,
233
269
  clearMetadataRoute,
270
+ reloadMetadataRoute,
234
271
  trackTablesRoute,
235
272
  rawBodyRoute,
236
273
  sendOperation,
237
274
  clearHasuraMetadata,
275
+ reloadHasuraMetadata,
238
276
  trackTables,
239
277
  createSelectPermission,
240
278
  createEntityRelationship,
package/src/Internal.res CHANGED
@@ -422,6 +422,45 @@ type evmContractConfig = {
422
422
  events: array<evmEventConfig>,
423
423
  }
424
424
 
425
+ type svmAccountFilter = {
426
+ position: int,
427
+ values: array<SvmTypes.Pubkey.t>,
428
+ }
429
+
430
+ type svmInstructionEventConfig = {
431
+ ...eventConfig,
432
+ /** Base58 Solana program id this instruction belongs to. */
433
+ programId: SvmTypes.Pubkey.t,
434
+ /** Hex-encoded discriminator. `None` matches every instruction in the program. */
435
+ discriminator: option<string>,
436
+ /** Length of the discriminator in bytes (0 / 1 / 2 / 4 / 8). Drives the
437
+ `dN` selector at query time and the dispatch-key precomputation in the
438
+ router. */
439
+ discriminatorByteLen: int,
440
+ includeTransaction: bool,
441
+ includeLogs: bool,
442
+ includeTokenBalances: bool,
443
+ accountFilters: array<svmAccountFilter>,
444
+ /** `None` matches both outer and inner (CPI-invoked) instructions. */
445
+ isInner: option<bool>,
446
+ /** Positional account names from the Borsh schema, in declared order.
447
+ `[]` means no schema is attached for this instruction. */
448
+ accounts: array<string>,
449
+ /** Borsh args layout as `Vec<ArgDef>` JSON (see `human_config::svm::ArgDef`
450
+ on the Rust side). `JSON.Null` means no schema is attached. */
451
+ args: JSON.t,
452
+ /** Program-level nominal-type registry (`BTreeMap<String, ArgType>` JSON).
453
+ Duplicated on every event of the same program — the runtime dedups by
454
+ `programId` when registering. `JSON.Null` when empty. */
455
+ definedTypes: JSON.t,
456
+ }
457
+
458
+ type svmProgramConfig = {
459
+ name: string,
460
+ programId: SvmTypes.Pubkey.t,
461
+ instructions: array<svmInstructionEventConfig>,
462
+ }
463
+
425
464
  type indexingAddress = {
426
465
  address: Address.t,
427
466
  contractName: string,
package/src/Main.res CHANGED
@@ -298,6 +298,57 @@ let getGlobalIndexer = (): 'indexer => {
298
298
  )
299
299
  }
300
300
 
301
+ // SVM identity: `{program, instruction}` from TS or
302
+ // `{instruction: GADT{contract, _0}}` from ReScript. Same two-format dance
303
+ // as the EVM `parseIdentityConfig`, but reading the SVM-native field names.
304
+ let parseSvmIdentityConfig = (identityConfig: 'a) => {
305
+ let raw =
306
+ identityConfig->(
307
+ Utils.magic: 'a => {
308
+ "program": unknown,
309
+ "instruction": unknown,
310
+ "where": option<JSON.t>,
311
+ }
312
+ )
313
+ let (programName, instructionName) = if typeof(raw["program"]) === #string {
314
+ (
315
+ raw["program"]->(Utils.magic: unknown => string),
316
+ raw["instruction"]->(Utils.magic: unknown => string),
317
+ )
318
+ } else {
319
+ let inst =
320
+ raw["instruction"]->(Utils.magic: unknown => {"contract": string, "_0": string})
321
+ (inst["contract"], inst["_0"])
322
+ }
323
+ let where = raw["where"]
324
+ let eventOptions: option<Internal.eventOptions<_>> = switch where {
325
+ | None => None
326
+ | Some(_) =>
327
+ Some({
328
+ where: ?(where->(Utils.magic: option<JSON.t> => option<_>)),
329
+ })
330
+ }
331
+ (programName, instructionName, eventOptions)
332
+ }
333
+
334
+ // onInstruction: delegates to HandlerRegister.setHandler. The SVM analog of
335
+ // onEvent; the registration store keys on `(contractName, eventName)` which
336
+ // for SVM is `(programName, instructionName)`.
337
+ let onInstructionFn = (identityConfig: 'a, handler: 'b) => {
338
+ HandlerRegister.throwIfFinishedRegistration(~methodName="onInstruction")
339
+ let (programName, instructionName, eventOptions) = parseSvmIdentityConfig(identityConfig)
340
+ HandlerRegister.setHandler(
341
+ ~contractName=programName,
342
+ ~eventName=instructionName,
343
+ handler->(
344
+ Utils.magic: 'b => Internal.genericHandler<
345
+ Internal.genericHandlerArgs<Internal.event, Internal.handlerContext>,
346
+ >
347
+ ),
348
+ ~eventOptions,
349
+ )
350
+ }
351
+
301
352
  // contractRegister: delegates to HandlerRegister.setContractRegister
302
353
  let contractRegisterFn = (identityConfig: 'a, handler: 'b) => {
303
354
  HandlerRegister.throwIfFinishedRegistration(~methodName="contractRegister")
@@ -456,7 +507,7 @@ let getGlobalIndexer = (): 'indexer => {
456
507
  "contractRegister",
457
508
  "onBlock",
458
509
  ]
459
- | Svm => ["name", "description", "chainIds", "chains", "onSlot"]
510
+ | Svm => ["name", "description", "chainIds", "chains", "onInstruction", "onSlot"]
460
511
  }
461
512
  keysMemo := Some(keys)
462
513
  keys
@@ -477,6 +528,7 @@ let getGlobalIndexer = (): 'indexer => {
477
528
  chains->(Utils.magic: {..} => unknown)
478
529
  }
479
530
  | "onEvent" => onEventFn->Utils.magic
531
+ | "onInstruction" => onInstructionFn->Utils.magic
480
532
  | "contractRegister" => contractRegisterFn->Utils.magic
481
533
  | "onBlock" | "onSlot" => onBlockFn->Utils.magic
482
534
  | _ =>
@@ -622,7 +674,6 @@ let migrate = async (~reset) => {
622
674
  ~chainConfigs=config.chainMap->ChainMap.values,
623
675
  ~envioInfo=getEnvioInfo(),
624
676
  ~resetCommand="envio local db-migrate setup",
625
- ~runCommand=None,
626
677
  )
627
678
  await persistence.storage.close()
628
679
  }
@@ -669,7 +720,6 @@ let start = async (
669
720
  ~chainConfigs=configWithoutRegistrations.chainMap->ChainMap.values,
670
721
  ~envioInfo=getEnvioInfo(),
671
722
  ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
672
- ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
673
723
  )
674
724
 
675
725
  // `Config.loadWithoutRegistrations` never sees registration state; handler,
package/src/Main.res.mjs CHANGED
@@ -249,6 +249,35 @@ function getGlobalIndexer() {
249
249
  let match = parseIdentityConfig(identityConfig);
250
250
  HandlerRegister.setHandler(match[0], match[1], handler, match[2], undefined);
251
251
  };
252
+ let parseSvmIdentityConfig = identityConfig => {
253
+ let match;
254
+ if (typeof identityConfig.program === "string") {
255
+ match = [
256
+ identityConfig.program,
257
+ identityConfig.instruction
258
+ ];
259
+ } else {
260
+ let inst = identityConfig.instruction;
261
+ match = [
262
+ inst.contract,
263
+ inst._0
264
+ ];
265
+ }
266
+ let where = identityConfig.where;
267
+ let eventOptions = where !== undefined ? ({
268
+ where: where
269
+ }) : undefined;
270
+ return [
271
+ match[0],
272
+ match[1],
273
+ eventOptions
274
+ ];
275
+ };
276
+ let onInstructionFn = (identityConfig, handler) => {
277
+ HandlerRegister.throwIfFinishedRegistration("onInstruction");
278
+ let match = parseSvmIdentityConfig(identityConfig);
279
+ HandlerRegister.setHandler(match[0], match[1], handler, match[2], undefined);
280
+ };
252
281
  let contractRegisterFn = (identityConfig, handler) => {
253
282
  HandlerRegister.throwIfFinishedRegistration("contractRegister");
254
283
  let match = parseIdentityConfig(identityConfig);
@@ -340,6 +369,7 @@ function getGlobalIndexer() {
340
369
  "description",
341
370
  "chainIds",
342
371
  "chains",
372
+ "onInstruction",
343
373
  "onSlot"
344
374
  ];
345
375
  break;
@@ -372,6 +402,8 @@ function getGlobalIndexer() {
372
402
  return Config.loadWithoutRegistrations().name;
373
403
  case "onEvent" :
374
404
  return onEventFn;
405
+ case "onInstruction" :
406
+ return onInstructionFn;
375
407
  case "onBlock" :
376
408
  case "onSlot" :
377
409
  return onBlockFn;
@@ -472,7 +504,7 @@ function getEnvioInfo() {
472
504
  async function migrate(reset) {
473
505
  let config = Config.loadWithoutRegistrations();
474
506
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
475
- await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset);
507
+ await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", reset);
476
508
  return await persistence.storage.close();
477
509
  }
478
510
 
@@ -497,7 +529,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
497
529
  let isDevelopmentMode = !isTest && configWithoutRegistrations.isDev;
498
530
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(configWithoutRegistrations, undefined);
499
531
  globalPersistenceRef.contents = persistence$1;
500
- await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
532
+ await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", reset);
501
533
  let match = await HandlerLoader.registerAllHandlers(configWithoutRegistrations);
502
534
  let registrations = match[1];
503
535
  let config = match[0];
@@ -168,7 +168,7 @@ let make = (
168
168
  }
169
169
 
170
170
  let init = {
171
- async (persistence, ~chainConfigs, ~envioInfo, ~resetCommand, ~runCommand, ~reset=false) => {
171
+ async (persistence, ~chainConfigs, ~envioInfo, ~resetCommand, ~reset=false) => {
172
172
  try {
173
173
  let shouldRun = switch persistence.storageStatus {
174
174
  | Unknown => true
@@ -212,22 +212,7 @@ let init = {
212
212
  | None => ["envio info is missing — storage initialized by an older envio"]
213
213
  | Some(stored) => Config.diffPaths(~stored, ~current=envioInfo)
214
214
  }
215
- // `storage.clickhouse` is serialized as a plain bool by the
216
- // public config (see Rust `StorageConfig`), so probe for
217
- // `Boolean(true)`, not an object.
218
- let hasClickhouse = switch envioInfo {
219
- | Object(d) =>
220
- switch d->Dict.get("storage") {
221
- | Some(Object(s)) =>
222
- switch s->Dict.get("clickhouse") {
223
- | Some(Boolean(true)) => true
224
- | _ => false
225
- }
226
- | _ => false
227
- }
228
- | _ => false
229
- }
230
- Config.throwIfIncompatible(changedPaths, ~resetCommand, ~runCommand, ~hasClickhouse)
215
+ Config.throwIfIncompatible(changedPaths, ~resetCommand)
231
216
  persistence.storageStatus = Ready(initialState)
232
217
  let progress = Dict.make()
233
218
  initialState.chains->Array.forEach(c => {