envio 3.0.1 → 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 +156 -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 +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/Internal.res +38 -0
  17. package/src/Main.res +53 -3
  18. package/src/Main.res.mjs +34 -2
  19. package/src/Persistence.res +2 -17
  20. package/src/Persistence.res.mjs +2 -14
  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/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,100 @@ 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 svmTransaction = {
62
+ signatures: array<string>,
63
+ feePayer?: SvmTypes.Pubkey.t,
64
+ success?: bool,
65
+ err?: string,
66
+ fee?: bigint,
67
+ computeUnitsConsumed?: bigint,
68
+ accountKeys: array<SvmTypes.Pubkey.t>,
69
+ recentBlockhash?: string,
70
+ version?: string,
71
+ }
72
+
73
+ type svmLog = {
74
+ kind: string,
75
+ message: string,
76
+ }
77
+
78
+ /** Inner block record on `svmInstructionEvent`. Field names follow EVM/Fuel
79
+ (`height`, `time`, `hash`) so the shared `Ecosystem.t` getters in
80
+ `Svm.res` work uniformly across ecosystems — `height` carries the slot. */
81
+ type svmInstructionEventBlock = {
82
+ /** Slot number. Named `height` so the shared ecosystem getter reads it. */
83
+ height: int,
84
+ /** Unix block time (seconds). `0` when HyperSync didn't return a block
85
+ for this instruction's slot. */
86
+ time: int,
87
+ /** Block hash. Currently always empty — populated by the future
88
+ reorg-guard `queryBlockHash(slot)` route. */
89
+ hash: string,
90
+ }
91
+
92
+ /** The per-instruction payload handlers receive on `.event`. Mirrors the
93
+ EVM `type event` shape inside generated per-event modules. */
94
+ type svmInstructionEvent = {
95
+ contractName: string,
96
+ eventName: string,
97
+ instruction: svmInstruction,
98
+ /** Parent transaction. `None` when the per-instruction
99
+ `include_transaction` flag is `false`. */
100
+ transaction: option<svmTransaction>,
101
+ /** Program log entries scoped to this instruction. `None` when the
102
+ per-instruction `include_logs` flag is `false`. */
103
+ logs: option<array<svmLog>>,
104
+ /** Convenience alias for `block.height`. */
105
+ slot: int,
106
+ /** Convenience alias for `block.time`. */
107
+ blockTime: option<int>,
108
+ block: svmInstructionEventBlock,
109
+ }
110
+
111
+ /** Arguments passed to handlers registered via `indexer.onInstruction`. */
112
+ type svmOnInstructionArgs<'context> = {
113
+ event: svmInstructionEvent,
114
+ context: 'context,
115
+ }
116
+
23
117
  // Internal-only type for the `indexer.onBlock` (and SVM `onSlot`) `where`
24
118
  // callback argument. The canonical TypeScript shape lives in
25
119
  // `packages/envio/index.d.ts`; the ReScript declaration here is free to
@@ -484,6 +484,56 @@ 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
+ ~accountFilters: array<Internal.svmAccountFilter>,
496
+ ~isInner: option<bool>,
497
+ ~isWildcard: bool,
498
+ ~handler: option<Internal.handler>,
499
+ ~contractRegister: option<Internal.contractRegister>,
500
+ ~accounts: array<string>=[],
501
+ ~args: JSON.t=JSON.Null,
502
+ ~definedTypes: JSON.t=JSON.Null,
503
+ ~startBlock: option<int>=?,
504
+ ): Internal.svmInstructionEventConfig => {
505
+ let paramsSchema =
506
+ S.json(~validate=false)
507
+ ->Utils.Schema.coerceToJsonPgType
508
+ ->(Utils.magic: S.t<JSON.t> => S.t<Internal.eventParams>)
509
+ {
510
+ id: switch discriminator {
511
+ | Some(d) => d
512
+ | None => "none"
513
+ },
514
+ name: instructionName,
515
+ contractName,
516
+ isWildcard,
517
+ handler,
518
+ contractRegister,
519
+ paramsRawEventSchema: paramsSchema,
520
+ simulateParamsSchema: paramsSchema,
521
+ filterByAddresses: false,
522
+ dependsOnAddresses: !isWildcard,
523
+ startBlock,
524
+ programId,
525
+ discriminator,
526
+ discriminatorByteLen,
527
+ includeTransaction,
528
+ includeLogs,
529
+ accountFilters,
530
+ isInner,
531
+ accounts,
532
+ args,
533
+ definedTypes,
534
+ }
535
+ }
536
+
487
537
  let buildFuelEventConfig = (
488
538
  ~contractName: string,
489
539
  ~eventName: string,
@@ -350,6 +350,36 @@ function buildEvmEventConfig(contractName, eventName, sighash, params, isWildcar
350
350
  };
351
351
  }
352
352
 
353
+ function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator, discriminatorByteLen, includeTransaction, includeLogs, 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
+ accountFilters: accountFilters,
376
+ isInner: isInner,
377
+ accounts: accounts,
378
+ args: args,
379
+ definedTypes: definedTypes
380
+ };
381
+ }
382
+
353
383
  function buildFuelEventConfig(contractName, eventName, kind, sighash, rawAbi, isWildcard, handler, contractRegister, startBlock) {
354
384
  let fuelKind;
355
385
  switch (kind) {
@@ -428,6 +458,7 @@ export {
428
458
  alwaysIncludedBlockFields,
429
459
  resolveFieldSelection,
430
460
  buildEvmEventConfig,
461
+ buildSvmInstructionEventConfig,
431
462
  buildFuelEventConfig,
432
463
  }
433
464
  /* 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/Internal.res CHANGED
@@ -422,6 +422,44 @@ 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
+ accountFilters: array<svmAccountFilter>,
443
+ /** `None` matches both outer and inner (CPI-invoked) instructions. */
444
+ isInner: option<bool>,
445
+ /** Positional account names from the Borsh schema, in declared order.
446
+ `[]` means no schema is attached for this instruction. */
447
+ accounts: array<string>,
448
+ /** Borsh args layout as `Vec<ArgDef>` JSON (see `human_config::svm::ArgDef`
449
+ on the Rust side). `JSON.Null` means no schema is attached. */
450
+ args: JSON.t,
451
+ /** Program-level nominal-type registry (`BTreeMap<String, ArgType>` JSON).
452
+ Duplicated on every event of the same program — the runtime dedups by
453
+ `programId` when registering. `JSON.Null` when empty. */
454
+ definedTypes: JSON.t,
455
+ }
456
+
457
+ type svmProgramConfig = {
458
+ name: string,
459
+ programId: SvmTypes.Pubkey.t,
460
+ instructions: array<svmInstructionEventConfig>,
461
+ }
462
+
425
463
  type indexingAddress = {
426
464
  address: Address.t,
427
465
  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 => {
@@ -27,7 +27,7 @@ function make(userEntities, allEnums, storage) {
27
27
  };
28
28
  }
29
29
 
30
- async function init(persistence, chainConfigs, envioInfo, resetCommand, runCommand, resetOpt) {
30
+ async function init(persistence, chainConfigs, envioInfo, resetCommand, resetOpt) {
31
31
  let reset = resetOpt !== undefined ? resetOpt : false;
32
32
  try {
33
33
  let promise = persistence.storageStatus;
@@ -70,19 +70,7 @@ async function init(persistence, chainConfigs, envioInfo, resetCommand, runComma
70
70
  let initialState$1 = await persistence.storage.resumeInitialState();
71
71
  let stored = initialState$1.envioInfo;
72
72
  let changedPaths = stored !== undefined ? Config.diffPaths(stored, envioInfo) : ["envio info is missing — storage initialized by an older envio"];
73
- let hasClickhouse;
74
- if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
75
- let match$1 = envioInfo["storage"];
76
- if (typeof match$1 === "object" && match$1 !== null && !Array.isArray(match$1)) {
77
- let match$2 = match$1["clickhouse"];
78
- hasClickhouse = match$2 === true;
79
- } else {
80
- hasClickhouse = false;
81
- }
82
- } else {
83
- hasClickhouse = false;
84
- }
85
- Config.throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse);
73
+ Config.throwIfIncompatible(changedPaths, resetCommand);
86
74
  persistence.storageStatus = {
87
75
  TAG: "Ready",
88
76
  _0: initialState$1
@@ -320,21 +320,34 @@ let patchConfig = (~config: Config.t, ~processConfig: JSON.t): Config.t => {
320
320
  let chainIdStr = chain->ChainMap.Chain.toChainId->Int.toString
321
321
  switch chainsDict->Dict.get(chainIdStr) {
322
322
  | Some(processChainJson) =>
323
- let simulateRaw: option<array<JSON.t>> =
324
- (processChainJson->(Utils.magic: JSON.t => {..}))["simulate"]->Nullable.toOption
323
+ let raw = processChainJson->(Utils.magic: JSON.t => {..})
324
+ let simulateRaw: option<array<JSON.t>> = raw["simulate"]->Nullable.toOption
325
325
  switch simulateRaw {
326
326
  | Some(simulateItems) =>
327
327
  let items = parse(~simulateItems, ~config, ~chainConfig)
328
- // Use endBlock from processConfig (the user-specified range)
329
- let startBlock: int =
330
- (processChainJson->(Utils.magic: JSON.t => {..}))["startBlock"]->(
331
- Utils.magic: 'a => int
332
- )
333
- let endBlock: int =
334
- (processChainJson->(Utils.magic: JSON.t => {..}))["endBlock"]->(Utils.magic: 'a => int)
328
+ let startBlock: int = raw["startBlock"]->(Utils.magic: 'a => int)
329
+ let endBlock: int = raw["endBlock"]->(Utils.magic: 'a => int)
335
330
  let source = SimulateSource.make(~items, ~endBlock, ~chain)
336
331
  {...chainConfig, startBlock, endBlock, sourceConfig: Config.CustomSources([source])}
337
- | None => chainConfig
332
+ | None =>
333
+ // No simulate items: still honor `startBlock` / `endBlock` overrides
334
+ // so non-EVM/Fuel ecosystems (notably SVM `indexer.onInstruction`,
335
+ // which doesn't support simulate items) can bound the run window
336
+ // via `testIndexer.process({chains: { id: {startBlock, endBlock} }})`.
337
+ let startBlockOpt: option<int> =
338
+ raw["startBlock"]
339
+ ->(Utils.magic: 'a => Nullable.t<int>)
340
+ ->Nullable.toOption
341
+ let endBlockOpt: option<int> =
342
+ raw["endBlock"]->(Utils.magic: 'a => Nullable.t<int>)->Nullable.toOption
343
+ let withStart = switch startBlockOpt {
344
+ | Some(sb) => {...chainConfig, startBlock: sb}
345
+ | None => chainConfig
346
+ }
347
+ switch endBlockOpt {
348
+ | Some(eb) => {...withStart, endBlock: eb}
349
+ | None => withStart
350
+ }
338
351
  }
339
352
  | None => chainConfig
340
353
  }
@@ -266,20 +266,35 @@ function patchConfig(config, processConfig) {
266
266
  }
267
267
  let simulateRaw = processChainJson.simulate;
268
268
  if (simulateRaw == null) {
269
- return chainConfig;
269
+ let startBlockOpt = processChainJson.startBlock;
270
+ let endBlockOpt = processChainJson.endBlock;
271
+ let withStart;
272
+ if (startBlockOpt == null) {
273
+ withStart = chainConfig;
274
+ } else {
275
+ let newrecord = {...chainConfig};
276
+ newrecord.startBlock = startBlockOpt;
277
+ withStart = newrecord;
278
+ }
279
+ if (endBlockOpt == null) {
280
+ return withStart;
281
+ }
282
+ let newrecord$1 = {...withStart};
283
+ newrecord$1.endBlock = endBlockOpt;
284
+ return newrecord$1;
270
285
  }
271
286
  let items = parse(simulateRaw, config, chainConfig);
272
287
  let startBlock = processChainJson.startBlock;
273
288
  let endBlock = processChainJson.endBlock;
274
289
  let source = SimulateSource.make(items, endBlock, chain);
275
- let newrecord = {...chainConfig};
276
- newrecord.sourceConfig = {
290
+ let newrecord$2 = {...chainConfig};
291
+ newrecord$2.sourceConfig = {
277
292
  TAG: "CustomSources",
278
293
  _0: [source]
279
294
  };
280
- newrecord.endBlock = endBlock;
281
- newrecord.startBlock = startBlock;
282
- return newrecord;
295
+ newrecord$2.endBlock = endBlock;
296
+ newrecord$2.startBlock = startBlock;
297
+ return newrecord$2;
283
298
  });
284
299
  let newrecord = {...config};
285
300
  newrecord.chainMap = newChainMap;
@@ -0,0 +1,9 @@
1
+ module Pubkey = {
2
+ type t
3
+ let schema =
4
+ S.string->S.setName("SVM.Pubkey")->(Utils.magic: S.t<string> => S.t<t>)
5
+ external fromStringUnsafe: string => t = "%identity"
6
+ external fromStringsUnsafe: array<string> => array<t> = "%identity"
7
+ external toString: t => string = "%identity"
8
+ external toStrings: array<t> => array<string> = "%identity"
9
+ }