envio 3.7.0 → 3.9.0-resolvers.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 (118) hide show
  1. package/README.md +2 -2
  2. package/evm.schema.json +7 -0
  3. package/fuel.schema.json +7 -0
  4. package/index.d.ts +674 -161
  5. package/index.js +10 -0
  6. package/package.json +6 -7
  7. package/src/AddressRows.res +121 -0
  8. package/src/AddressRows.res.mjs +143 -0
  9. package/src/Batch.res +11 -0
  10. package/src/Batch.res.mjs +2 -1
  11. package/src/Bin.res +47 -0
  12. package/src/Bin.res.mjs +34 -1
  13. package/src/ChainState.res +108 -98
  14. package/src/ChainState.res.mjs +86 -67
  15. package/src/ChainState.resi +28 -15
  16. package/src/Config.res +61 -131
  17. package/src/Config.res.mjs +47 -79
  18. package/src/ContractMapping.res +66 -0
  19. package/src/ContractMapping.res.mjs +73 -0
  20. package/src/Core.res +24 -0
  21. package/src/Core.res.mjs +4 -0
  22. package/src/Ecosystem.res +7 -2
  23. package/src/Env.res +128 -1
  24. package/src/Env.res.mjs +123 -1
  25. package/src/Envio.res +54 -76
  26. package/src/EventConfigBuilder.res +316 -46
  27. package/src/EventConfigBuilder.res.mjs +231 -28
  28. package/src/FetchState.res +59 -67
  29. package/src/FetchState.res.mjs +34 -44
  30. package/src/HandlerRegister.res +26 -15
  31. package/src/HandlerRegister.res.mjs +27 -7
  32. package/src/Hasura.res +30 -22
  33. package/src/Hasura.res.mjs +11 -5
  34. package/src/InMemoryStore.res +23 -38
  35. package/src/InMemoryStore.res.mjs +13 -27
  36. package/src/IndexerState.res +40 -3
  37. package/src/IndexerState.res.mjs +30 -4
  38. package/src/IndexerState.resi +4 -1
  39. package/src/Internal.res +49 -30
  40. package/src/Internal.res.mjs +12 -16
  41. package/src/Main.res +27 -20
  42. package/src/Main.res.mjs +19 -14
  43. package/src/Metrics.res +15 -1
  44. package/src/Metrics.res.mjs +5 -1
  45. package/src/Persistence.res +46 -21
  46. package/src/Persistence.res.mjs +8 -5
  47. package/src/PgStorage.res +238 -79
  48. package/src/PgStorage.res.mjs +115 -73
  49. package/src/PruneStaleHistory.res +1 -1
  50. package/src/PruneStaleHistory.res.mjs +1 -1
  51. package/src/Rollback.res +62 -41
  52. package/src/Rollback.res.mjs +61 -19
  53. package/src/RollbackCommit.res +7 -9
  54. package/src/RollbackCommit.res.mjs +5 -6
  55. package/src/SimulateItems.res +254 -9
  56. package/src/SimulateItems.res.mjs +214 -47
  57. package/src/Sink.res +35 -26
  58. package/src/Sink.res.mjs +20 -16
  59. package/src/TestIndexer.res +121 -123
  60. package/src/TestIndexer.res.mjs +97 -102
  61. package/src/Utils.res +38 -0
  62. package/src/Utils.res.mjs +39 -2
  63. package/src/Writing.res +4 -2
  64. package/src/Writing.res.mjs +3 -2
  65. package/src/bindings/ClickHouse.res +311 -656
  66. package/src/bindings/ClickHouse.res.mjs +294 -395
  67. package/src/bindings/ClickHouseSink.res +303 -0
  68. package/src/bindings/ClickHouseSink.res.mjs +216 -0
  69. package/src/bindings/NodeJs.res +9 -0
  70. package/src/bindings/NodeJs.res.mjs +8 -1
  71. package/src/bindings/Postgres.res +9 -0
  72. package/src/bindings/Postgres.res.mjs +3 -0
  73. package/src/db/EntityHistory.res +29 -24
  74. package/src/db/EntityHistory.res.mjs +11 -19
  75. package/src/db/InternalTable.res +258 -83
  76. package/src/db/InternalTable.res.mjs +223 -85
  77. package/src/db/RollbackScope.res +29 -0
  78. package/src/db/RollbackScope.res.mjs +31 -0
  79. package/src/db/Table.res +28 -1
  80. package/src/db/Table.res.mjs +26 -2
  81. package/src/resolvers/ResolverProcess.res +809 -0
  82. package/src/resolvers/ResolverProcess.res.mjs +667 -0
  83. package/src/resolvers/ResolverQuery.res +317 -0
  84. package/src/resolvers/ResolverQuery.res.mjs +220 -0
  85. package/src/resolvers/collisions.js +108 -0
  86. package/src/resolvers/db.js +363 -0
  87. package/src/resolvers/dispatch.js +165 -0
  88. package/src/resolvers/errors.js +30 -0
  89. package/src/resolvers/graphqlSelection.js +292 -0
  90. package/src/resolvers/hasuraAction.js +84 -0
  91. package/src/resolvers/hasuraApply.js +323 -0
  92. package/src/resolvers/hasuraMetadata.js +119 -0
  93. package/src/resolvers/index.js +95 -0
  94. package/src/resolvers/manifest.js +370 -0
  95. package/src/resolvers/server.js +270 -0
  96. package/src/sources/AddressSet.res +0 -22
  97. package/src/sources/AddressStore.res +48 -88
  98. package/src/sources/AddressStore.res.mjs +11 -22
  99. package/src/sources/Evm.res +2 -0
  100. package/src/sources/Evm.res.mjs +2 -0
  101. package/src/sources/EvmHyperSyncSource.res +3 -2
  102. package/src/sources/Fuel.res +2 -0
  103. package/src/sources/Fuel.res.mjs +2 -0
  104. package/src/sources/SimulateSource.res +8 -4
  105. package/src/sources/SimulateSource.res.mjs +7 -3
  106. package/src/sources/Svm.res +36 -7
  107. package/src/sources/Svm.res.mjs +34 -4
  108. package/src/sources/SvmHyperSyncClient.res +20 -116
  109. package/src/sources/SvmHyperSyncClient.res.mjs +5 -7
  110. package/src/sources/SvmHyperSyncSource.res +89 -37
  111. package/src/sources/SvmHyperSyncSource.res.mjs +71 -39
  112. package/src/sources/TransactionStore.res +38 -0
  113. package/src/sources/TransactionStore.res.mjs +5 -0
  114. package/src/tui/Tui.res +1 -1
  115. package/src/tui/Tui.res.mjs +1 -1
  116. package/svm.schema.json +44 -156
  117. package/src/MemoryStorage.res +0 -724
  118. package/src/MemoryStorage.res.mjs +0 -601
package/src/Envio.res CHANGED
@@ -20,58 +20,48 @@ 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 svmInstructionParams = {
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>,
23
+ type svmLamports = {
24
+ pre: bigint,
25
+ post: bigint,
40
26
  }
41
27
 
42
- type svmTokenBalance = {
43
- account?: SvmTypes.Pubkey.t,
44
- mint?: SvmTypes.Pubkey.t,
45
- /** Owner at the end of the transaction, falling back to the owner on entry
46
- when the account was closed during it. Pre and post owners differ only when
47
- a `SetAuthority(AccountOwner)` runs mid-transaction. */
48
- owner?: SvmTypes.Pubkey.t,
49
- /** Mint decimals, for scaling the raw amounts below. */
50
- decimals?: int,
51
- /** Raw amount in base units before the transaction. Absent when the token
52
- account was created during it. */
28
+ type svmAccountToken = {
29
+ mint: SvmTypes.Pubkey.t,
30
+ owner: SvmTypes.Pubkey.t,
31
+ decimals: int,
53
32
  preAmount?: bigint,
54
- /** Raw amount in base units after the transaction. Absent when the token
55
- account was closed during it. */
56
33
  postAmount?: bigint,
57
34
  }
58
35
 
36
+ type svmAccountActivity = {
37
+ address: SvmTypes.Pubkey.t,
38
+ transactionAccountIndex?: int,
39
+ isSigner?: bool,
40
+ isWritable?: bool,
41
+ lamports?: svmLamports,
42
+ token?: svmAccountToken,
43
+ }
44
+
45
+ type svmInstructionAccount = {
46
+ address: SvmTypes.Pubkey.t,
47
+ accountName: string,
48
+ instructionAccountIndex: int,
49
+ activity?: svmAccountActivity,
50
+ }
51
+
59
52
  type svmTransaction = {
60
53
  transactionIndex?: int,
61
- /** The transaction's identifying signature (`allSignatures[0]`). */
62
- signature: string,
54
+ signature?: string,
63
55
  feePayer?: SvmTypes.Pubkey.t,
64
56
  success?: bool,
65
57
  err?: string,
66
58
  fee?: bigint,
67
59
  computeUnitsConsumed?: bigint,
68
- accountKeys: array<SvmTypes.Pubkey.t>,
60
+ accountKeys?: array<SvmTypes.Pubkey.t>,
69
61
  recentBlockhash?: string,
70
62
  version?: string,
71
- /** Every signature on the transaction, in message order; the first is
72
- `signature`. Longer than one element only for a multi-signer transaction. */
73
- allSignatures: array<string>,
74
- tokenBalances?: array<svmTokenBalance>,
63
+ allSignatures?: array<string>,
64
+ accountActivities?: array<svmAccountActivity>,
75
65
  }
76
66
 
77
67
  type svmLog = {
@@ -79,57 +69,29 @@ type svmLog = {
79
69
  message: string,
80
70
  }
81
71
 
82
- /** Block context for a matched instruction. `slot`/`hash` are always present;
83
- `time` can still be absent — HyperSync/Solana may not report a block time for
84
- every slot. Every other field is opt-in via `field_selection.block_fields`,
85
- materialised from the per-chain block store at batch prep. */
86
- type svmInstructionBlock = {
87
- /** Slot this instruction's block was matched in. */
72
+ type svmBlock = {
88
73
  slot: int,
89
- /** Unix block time (seconds). */
90
74
  time?: int,
91
- hash: string,
92
- /** Block height (distinct from slot). Absent when not selected via
93
- `field_selection.block_fields`. */
75
+ hash?: string,
94
76
  height?: int,
95
77
  parentSlot?: int,
96
78
  parentHash?: string,
97
79
  }
98
80
 
99
- /** The per-instruction payload handlers receive as their `instruction`
100
- argument. Carries the matched instruction's own fields plus the
101
- program/instruction names, parent transaction, scoped logs, and block. */
102
81
  type svmInstruction = {
103
- /** Program name as declared under `programs[].name` in `config.yaml`. */
104
82
  programName: string,
105
- /** Instruction name as declared under `instructions[].name` in
106
- `config.yaml`. */
107
83
  instructionName: string,
108
- programId: SvmTypes.Pubkey.t,
109
- /** Raw instruction bytes as `0x`-prefixed hex. */
110
- data: string,
111
- accounts: array<SvmTypes.Pubkey.t>,
112
- /** Path through the call tree: `[outerIndex]` for top-level instructions,
113
- appended child indices for inner CPI calls. */
114
- instructionAddress: array<int>,
115
- isInner: bool,
116
- /** Discriminator prefixes pre-extracted by HyperSync. Each is `Some` only
117
- when the underlying instruction is at least that long. */
118
- d1?: string,
119
- d2?: string,
120
- d4?: string,
121
- d8?: string,
122
- /** Borsh-decoded params view. See [[svmInstructionParams]]. */
123
- params?: svmInstructionParams,
124
- /** Parent transaction. Carries only the fields selected via
125
- `field_selection.transaction_fields`; absent when none are selected. */
84
+ discriminator: string,
85
+ programId?: SvmTypes.Pubkey.t,
86
+ data?: string,
87
+ path?: array<int>,
88
+ isInner?: bool,
89
+ args?: JSON.t,
90
+ accounts?: dict<svmInstructionAccount>,
91
+ accountArguments?: array<SvmTypes.Pubkey.t>,
126
92
  transaction?: svmTransaction,
127
- /** Program log entries scoped to this instruction. Absent when the
128
- per-instruction `include_logs` flag is `false`. */
129
93
  logs?: array<svmLog>,
130
- // Omitted on construction; materialised onto the payload at batch prep,
131
- // before a handler ever reads it.
132
- block?: svmInstructionBlock,
94
+ block?: svmBlock,
133
95
  }
134
96
 
135
97
  /** Arguments passed to handlers registered via `indexer.onInstruction`. */
@@ -315,6 +277,22 @@ type fuelSimulateItem = {
315
277
  transaction?: fuelTransactionInput,
316
278
  }
317
279
 
280
+ type svmSimulateItem = {
281
+ program: string,
282
+ instruction: string,
283
+ slot?: int,
284
+ path?: array<int>,
285
+ programId?: string,
286
+ data?: string,
287
+ isInner?: bool,
288
+ args?: JSON.t,
289
+ accounts?: dict<{address: string}>,
290
+ accountArguments?: array<string>,
291
+ logs?: array<{kind?: string, message?: string}>,
292
+ block?: svmBlock,
293
+ transaction?: unknown,
294
+ }
295
+
318
296
  // Detects contexts where a full-screen TUI is counter-productive: piped/redirected
319
297
  // stdout, CI, and coding agents. `CLAUDECODE` is set by Claude Code; `CI` is the
320
298
  // de-facto convention across CI providers; `TERM=dumb` is set by editors/tools
@@ -310,33 +310,44 @@ let internalBlockFields = ["number"]
310
310
  // selected.
311
311
  let rawEventBlockFields = ["hash", "timestamp"]
312
312
 
313
- let selectionKinds = ["block", "transaction"]
313
+ let evmSelectionKinds = ["block", "transaction"]
314
+ let svmSelectionKinds = ["instruction", "transaction", "accountActivity", "block", "log"]
314
315
 
315
316
  // A handler written in plain JS gets no type error for a `blocks`/`transactions`
316
317
  // typo, and an unrecognised key would read as an empty selection — silently
317
318
  // dropping every field `config.yaml` selected. Rejected here so the option is
318
319
  // held to the same shape whether or not the project type-checks it.
319
- let validateFieldsShapeOrThrow = (fields: Internal.evmFieldsSelection, ~registration: string) => {
320
- let raw = fields->(Utils.magic: Internal.evmFieldsSelection => unknown)
321
- if typeof(raw) !== #object || raw === %raw(`null`) || Array.isArray(raw) {
320
+ let validateFieldsShapeOrThrow = (
321
+ fields: unknown,
322
+ ~registration: string,
323
+ ~validKeys: array<string>,
324
+ ~shapeNoun: string,
325
+ ) => {
326
+ if typeof(fields) !== #object || fields === %raw(`null`) || Array.isArray(fields) {
322
327
  JsError.throwWithMessage(
323
- `The fields option of ${registration} must be an object of block and transaction field names.`,
328
+ `The fields option of ${registration} must be an object of ${shapeNoun} field names.`,
324
329
  )
325
330
  }
326
- raw
331
+ fields
327
332
  ->(Utils.magic: unknown => dict<unknown>)
328
333
  ->Dict.keysToArray
329
334
  ->Array.forEach(key =>
330
- if !(selectionKinds->Array.includes(key)) {
335
+ if !(validKeys->Array.includes(key)) {
331
336
  JsError.throwWithMessage(
332
- `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${selectionKinds->Array.joinUnsafe(
333
- ", ",
337
+ `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${Utils.Array.quotedJoin(
338
+ validKeys,
334
339
  )}.`,
335
340
  )
336
341
  }
337
342
  )
338
343
  }
339
344
 
345
+ let selectionList = (fields: unknown, key: string): option<array<string>> =>
346
+ switch (fields->(Utils.magic: unknown => dict<unknown>))->Dict.get(key) {
347
+ | None => None
348
+ | Some(value) => Some(value->(Utils.magic: unknown => array<string>))
349
+ }
350
+
340
351
  let validBlockFields = Utils.Set.fromArray(Evm.blockFields)
341
352
  let validTransactionFields = Utils.Set.fromArray(Evm.transactionFields)
342
353
 
@@ -345,6 +356,7 @@ let parseFieldsOrThrow = (
345
356
  ~valid: Utils.Set.t<string>,
346
357
  ~kind: string,
347
358
  ~registration: string,
359
+ ~rejectEmpty=false,
348
360
  ) => {
349
361
  let seen = Utils.Set.make()
350
362
  let fields = switch fields {
@@ -358,9 +370,9 @@ let parseFieldsOrThrow = (
358
370
  fields->Array.forEach(name => {
359
371
  if !(valid->Utils.Set.has(name)) {
360
372
  JsError.throwWithMessage(
361
- `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${valid
362
- ->Utils.Set.toArray
363
- ->Array.joinUnsafe(", ")}.`,
373
+ `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${Utils.Array.quotedJoin(
374
+ valid->Utils.Set.toArray,
375
+ )}.`,
364
376
  )
365
377
  }
366
378
  if seen->Utils.Set.has(name) {
@@ -370,6 +382,11 @@ let parseFieldsOrThrow = (
370
382
  }
371
383
  seen->Utils.Set.add(name)->ignore
372
384
  })
385
+ if rejectEmpty && fields->Array.length === 0 {
386
+ JsError.throwWithMessage(
387
+ `The fields.${kind} option of ${registration} must list at least one field.`,
388
+ )
389
+ }
373
390
  seen
374
391
  }
375
392
 
@@ -377,21 +394,26 @@ let parseFieldsOrThrow = (
377
394
  // option itself (the names are for error messages), never on the chain, so one
378
395
  // `onEvent` call resolves once and shares the result across every chain.
379
396
  let resolveInlineFieldSelection = (
380
- fields: Internal.evmFieldsSelection,
397
+ fields: unknown,
381
398
  ~contractName: string,
382
399
  ~eventName: string,
383
400
  ~enableRawEvents: bool,
384
401
  ): Internal.fieldSelection => {
385
402
  let registration = `the "${eventName}" event registration on contract "${contractName}"`
386
- validateFieldsShapeOrThrow(fields, ~registration)
403
+ validateFieldsShapeOrThrow(
404
+ fields,
405
+ ~registration,
406
+ ~validKeys=evmSelectionKinds,
407
+ ~shapeNoun="block and transaction",
408
+ )
387
409
  let blockFields = parseFieldsOrThrow(
388
- fields.block,
410
+ selectionList(fields, "block"),
389
411
  ~valid=validBlockFields,
390
412
  ~kind="block",
391
413
  ~registration,
392
414
  )
393
415
  let transactionFields = parseFieldsOrThrow(
394
- fields.transaction,
416
+ selectionList(fields, "transaction"),
395
417
  ~valid=validTransactionFields,
396
418
  ~kind="transaction",
397
419
  ~registration,
@@ -498,21 +520,121 @@ let buildEvmOnEventRegistration = (
498
520
 
499
521
  // ============== Build SVM instruction event config ==============
500
522
 
501
- // Always-included block fields (slot, time, hash) are prepended at runtime so
502
- // they're always present regardless of config.
503
- let alwaysIncludedSvmBlockFields: array<Internal.svmBlockField> = [Slot, Time, Hash]
523
+ // `block.slot` is the item's own key, so an inline selection carries it
524
+ // whether or not the handler listed it.
525
+ let alwaysIncludedSvmBlockFields = ["slot"]
526
+
527
+ let validSvmInstructionFields = Utils.Set.fromArray([
528
+ "args",
529
+ "accounts",
530
+ "accountArguments",
531
+ "programId",
532
+ "data",
533
+ "path",
534
+ "isInner",
535
+ ])
536
+ let validSvmTransactionFields = Utils.Set.fromArray([
537
+ "transactionIndex",
538
+ "signature",
539
+ "feePayer",
540
+ "success",
541
+ "err",
542
+ "fee",
543
+ "computeUnitsConsumed",
544
+ "accountKeys",
545
+ "recentBlockhash",
546
+ "version",
547
+ "allSignatures",
548
+ ])
549
+ let validSvmAccountActivityFields = Utils.Set.fromArray([
550
+ "address",
551
+ "transactionAccountIndex",
552
+ "isSigner",
553
+ "isWritable",
554
+ "lamports.pre",
555
+ "lamports.post",
556
+ "token.mint",
557
+ "token.owner",
558
+ "token.decimals",
559
+ "token.preAmount",
560
+ "token.postAmount",
561
+ ])
562
+ let validSvmBlockFields = Utils.Set.fromArray([
563
+ "slot",
564
+ "time",
565
+ "hash",
566
+ "height",
567
+ "parentSlot",
568
+ "parentHash",
569
+ ])
570
+ let validSvmLogFields = Utils.Set.fromArray(["kind", "message"])
571
+
572
+ let resolveSvmInlineFieldSelection = (
573
+ fields: unknown,
574
+ ~contractName: string,
575
+ ~eventName: string,
576
+ ): Internal.fieldSelection => {
577
+ let registration = `the "${eventName}" instruction on program "${contractName}"`
578
+ validateFieldsShapeOrThrow(
579
+ fields,
580
+ ~registration,
581
+ ~validKeys=svmSelectionKinds,
582
+ ~shapeNoun="instruction, transaction, accountActivity, block and log",
583
+ )
584
+ let accountActivity = selectionList(fields, "accountActivity")
585
+ let log = selectionList(fields, "log")
586
+ let instructionFields = parseFieldsOrThrow(
587
+ selectionList(fields, "instruction"),
588
+ ~valid=validSvmInstructionFields,
589
+ ~kind="instruction",
590
+ ~registration,
591
+ )
592
+ let transactionFields = parseFieldsOrThrow(
593
+ selectionList(fields, "transaction"),
594
+ ~valid=validSvmTransactionFields,
595
+ ~kind="transaction",
596
+ ~registration,
597
+ )
598
+ let accountActivityFields = parseFieldsOrThrow(
599
+ accountActivity,
600
+ ~valid=validSvmAccountActivityFields,
601
+ ~kind="accountActivity",
602
+ ~registration,
603
+ ~rejectEmpty=accountActivity->Option.isSome,
604
+ )
605
+ let blockFields = parseFieldsOrThrow(
606
+ selectionList(fields, "block"),
607
+ ~valid=validSvmBlockFields,
608
+ ~kind="block",
609
+ ~registration,
610
+ )
611
+ let logFields = parseFieldsOrThrow(
612
+ log,
613
+ ~valid=validSvmLogFields,
614
+ ~kind="log",
615
+ ~registration,
616
+ ~rejectEmpty=log->Option.isSome,
617
+ )
618
+ blockFields->Utils.Set.addMany(alwaysIncludedSvmBlockFields)
619
+ if accountActivityFields->Utils.Set.size > 0 {
620
+ transactionFields->Utils.Set.add("accountActivities")->ignore
621
+ }
622
+ Internal.makeFieldSelection(
623
+ ~blockFields,
624
+ ~transactionFields,
625
+ ~instructionFields,
626
+ ~accountActivityFields,
627
+ ~logFields,
628
+ ~blockMaskFn=Svm.eventBlockFieldMask,
629
+ ~transactionMaskFn=Svm.eventTransactionFieldMask,
630
+ )
631
+ }
504
632
 
505
633
  let buildSvmInstructionEventConfig = (
506
634
  ~contractName: string,
507
635
  ~instructionName: string,
508
636
  ~programId: SvmTypes.Pubkey.t,
509
637
  ~discriminator: option<string>,
510
- ~discriminatorByteLen: int,
511
- ~includeLogs: bool,
512
- ~transactionFields: array<Internal.svmTransactionField>=[],
513
- ~blockFields: array<Internal.svmBlockField>=[],
514
- ~accountFilters: array<Internal.svmAccountFilterGroup>,
515
- ~isInner: option<bool>,
516
638
  ~accounts: array<string>=[],
517
639
  ~args: JSON.t=JSON.Null,
518
640
  ~definedTypes: JSON.t=JSON.Null,
@@ -523,12 +645,8 @@ let buildSvmInstructionEventConfig = (
523
645
  ->(Utils.magic: S.t<JSON.t> => S.t<Internal.eventParams>)
524
646
 
525
647
  let fieldSelection = Internal.makeFieldSelection(
526
- ~blockFields=Utils.Set.fromArray(Array.concat(alwaysIncludedSvmBlockFields, blockFields))->(
527
- Utils.magic: Utils.Set.t<Internal.svmBlockField> => Utils.Set.t<string>
528
- ),
529
- ~transactionFields=Utils.Set.fromArray(transactionFields)->(
530
- Utils.magic: Utils.Set.t<Internal.svmTransactionField> => Utils.Set.t<string>
531
- ),
648
+ ~blockFields=Utils.Set.fromArray(alwaysIncludedSvmBlockFields),
649
+ ~transactionFields=Utils.Set.make(),
532
650
  ~blockMaskFn=Svm.eventBlockFieldMask,
533
651
  ~transactionMaskFn=Svm.eventTransactionFieldMask,
534
652
  )
@@ -543,35 +661,187 @@ let buildSvmInstructionEventConfig = (
543
661
  simulateParamsSchema: paramsSchema,
544
662
  programId,
545
663
  discriminator,
546
- discriminatorByteLen,
547
- includeLogs,
548
664
  fieldSelection,
549
- accountFilters,
550
- isInner,
551
665
  accounts,
552
666
  args,
553
667
  definedTypes,
554
668
  }
555
669
  }
556
670
 
557
- // Enrich an SVM definition into a registration. SVM has no `where`; only the
558
- // handler binding + wildcard-derived address gate are registration state.
671
+ // ============== SVM `where` ==============
672
+
673
+ type parsedSvmWhere = {
674
+ // Disjunctive normal form: outer array is OR of AND-groups. Empty means the
675
+ // registration accepts any accounts.
676
+ accountFilters: array<Internal.svmAccountFilterGroup>,
677
+ isInner: option<bool>,
678
+ startBlock: option<int>,
679
+ }
680
+
681
+ let validSvmWhereKeys = ["accounts", "isInner", "block"]
682
+
683
+ // `{slot: {_gte?}}`. Both levels are strict: the inner `eventBlockRangeSchema`
684
+ // rejects the `_lte` / `_every` that only `onSlot` supports, the outer rejects
685
+ // a wrapper key other than `slot`.
686
+ type svmBlockFilter = {slot?: LogSelection.eventBlockRange}
687
+ let svmBlockFilterSchema: S.t<svmBlockFilter> = S.object(s => {
688
+ slot: ?s.field("slot", S.option(LogSelection.eventBlockRangeSchema)),
689
+ })->S.strict
690
+
691
+ // The source query narrows accounts through `a0..a9`, so only an instruction's
692
+ // leading account slots can be filtered.
693
+ let filterableAccountCount = 10
694
+
695
+ // Resolve the `where` option of an `onInstruction` registration. Account names
696
+ // are resolved against the instruction's declared accounts here, so the
697
+ // registration carries positions and nothing downstream needs the names.
698
+ let resolveSvmWhereOrThrow = (
699
+ where: JSON.t,
700
+ ~contractName: string,
701
+ ~eventName: string,
702
+ ~accountNames: array<string>,
703
+ ): parsedSvmWhere => {
704
+ let invalid = message =>
705
+ JsError.throwWithMessage(
706
+ `Invalid where configuration for the "${eventName}" instruction on program "${contractName}". ${message}`,
707
+ )
708
+
709
+ let obj = switch where {
710
+ | Object(obj) => obj
711
+ | _ => invalid("Expected an object.")
712
+ }
713
+ obj->Utils.Dict.forEachWithKey((_, key) =>
714
+ if !(validSvmWhereKeys->Array.includes(key)) {
715
+ invalid(`Unknown field "${key}". Valid fields: ${Utils.Array.quotedJoin(validSvmWhereKeys)}.`)
716
+ }
717
+ )
718
+
719
+ let isInner = switch obj->Dict.get("isInner") {
720
+ | None => None
721
+ | Some(Boolean(isInner)) => Some(isInner)
722
+ | Some(_) => invalid(`The "isInner" filter must be a boolean.`)
723
+ }
724
+
725
+ let parseGroup = (group: dict<JSON.t>): Internal.svmAccountFilterGroup =>
726
+ group
727
+ ->Dict.toArray
728
+ ->Array.map(((name, value)) => {
729
+ let position = switch accountNames->Array.indexOf(name) {
730
+ | -1 if accountNames->Utils.Array.isEmpty =>
731
+ invalid(
732
+ "The instruction has no named accounts to filter on. Add `accounts` and `args` to it in config.yaml, or attach an IDL.",
733
+ )
734
+ | -1 =>
735
+ invalid(
736
+ `The instruction has no account named "${name}" to filter on. Named accounts: ${Utils.Array.quotedJoin(
737
+ accountNames,
738
+ )}.`,
739
+ )
740
+ | position if position >= filterableAccountCount =>
741
+ invalid(
742
+ `Account "${name}" is at position ${position->Int.toString}, and only the first ${filterableAccountCount->Int.toString} accounts of an instruction can be filtered.`,
743
+ )
744
+ | position => position
745
+ }
746
+ let values = value->normalizeOrThrow
747
+ if values->Utils.Array.isEmpty {
748
+ invalid(`The "${name}" filter must list at least one pubkey.`)
749
+ }
750
+ {
751
+ Internal.position,
752
+ values: values->Array.map(value =>
753
+ switch value {
754
+ | JSON.String(pubkey) if Core.getAddon().isSvmPubkey(~value=pubkey) =>
755
+ pubkey->SvmTypes.Pubkey.fromStringUnsafe
756
+ | JSON.String(pubkey) =>
757
+ invalid(`The "${name}" filter value "${pubkey}" is not a base58 SVM pubkey.`)
758
+ | _ => invalid(`The "${name}" filter must list base58 pubkeys as strings.`)
759
+ }
760
+ ),
761
+ }
762
+ })
763
+
764
+ // An empty AND-group matches every instruction, so it makes the whole OR
765
+ // vacuous. Normalized away here so the source builds one unfiltered
766
+ // selection instead of a match-all selection beside the narrower ones.
767
+ let groups = switch obj->Dict.get("accounts") {
768
+ | None => []
769
+ | Some(Object(group)) => [parseGroup(group)]
770
+ | Some(Array(entries)) =>
771
+ entries->Array.map(entry =>
772
+ switch entry {
773
+ | Object(group) => parseGroup(group)
774
+ | _ => invalid(`Each entry in "accounts" must be an object.`)
775
+ }
776
+ )
777
+ | Some(_) => invalid(`Expected "accounts" to be an object or an array of objects.`)
778
+ }
779
+ let accountFilters = groups->Array.some(Utils.Array.isEmpty) ? [] : groups
780
+
781
+ let startBlock = switch obj->Dict.get("block") {
782
+ | None => None
783
+ | Some(block) =>
784
+ let filter = try block->S.parseOrThrow(svmBlockFilterSchema) catch {
785
+ | S.Raised(exn) =>
786
+ invalid(
787
+ `The "block" filter is invalid: ${exn
788
+ ->Utils.prettifyExn
789
+ ->(
790
+ Utils.magic: exn => string
791
+ )}. Only \`_gte\` is supported on instruction filters — use \`indexer.onSlot\` for \`_lte\` or \`_every\`.`,
792
+ )
793
+ }
794
+ switch filter.slot {
795
+ | Some({_gte}) => _gte
796
+ | None => None
797
+ }
798
+ }
799
+
800
+ {accountFilters, isInner, startBlock}
801
+ }
802
+
803
+ // Enrich an SVM definition into a registration: the handler binding plus the
804
+ // `where`-derived account/isInner filters and startBlock override.
559
805
  let buildSvmOnEventRegistration = (
560
806
  ~eventConfig: Internal.svmInstructionEventConfig,
561
807
  ~isWildcard: bool,
562
808
  ~handler: option<Internal.handler>,
563
809
  ~contractRegister: option<Internal.contractRegister>,
810
+ ~where: option<JSON.t>,
811
+ ~fieldSelection: option<Internal.fieldSelection>=?,
564
812
  ~startBlock: option<int>=?,
565
813
  ): Internal.svmOnEventRegistration => {
566
- index: -1,
567
- eventConfig: (eventConfig :> Internal.eventConfig),
568
- handler,
569
- contractRegister,
570
- isWildcard,
571
- filterByAddresses: false,
572
- dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
573
- startBlock,
574
- fieldSelection: eventConfig.fieldSelection,
814
+ let resolvedWhere = switch where {
815
+ | None => {accountFilters: [], isInner: None, startBlock: None}
816
+ | Some(where) =>
817
+ where->resolveSvmWhereOrThrow(
818
+ ~contractName=eventConfig.contractName,
819
+ ~eventName=eventConfig.name,
820
+ ~accountNames=eventConfig.accounts,
821
+ )
822
+ }
823
+
824
+ {
825
+ index: -1,
826
+ eventConfig: (eventConfig :> Internal.eventConfig),
827
+ handler,
828
+ contractRegister,
829
+ isWildcard,
830
+ filterByAddresses: false,
831
+ dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
832
+ // `where.block.slot._gte` overrides the program-level startBlock when
833
+ // present, mirroring EVM's `where.block.number._gte`.
834
+ startBlock: switch resolvedWhere.startBlock {
835
+ | Some(_) as sb => sb
836
+ | None => startBlock
837
+ },
838
+ fieldSelection: switch fieldSelection {
839
+ | Some(fieldSelection) => fieldSelection
840
+ | None => eventConfig.fieldSelection
841
+ },
842
+ accountFilters: resolvedWhere.accountFilters,
843
+ isInner: resolvedWhere.isInner,
844
+ }
575
845
  }
576
846
 
577
847
  // ============== Build Fuel event config ==============