envio 3.9.0 → 3.10.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 (110) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +35 -3
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +52 -44
  10. package/src/Config.res.mjs +51 -17
  11. package/src/Core.res +4 -0
  12. package/src/CrossChainState.res +15 -23
  13. package/src/CrossChainState.res.mjs +10 -18
  14. package/src/CrossChainState.resi +4 -1
  15. package/src/Ecosystem.res +7 -2
  16. package/src/Envio.res +6 -4
  17. package/src/EventConfigBuilder.res +169 -26
  18. package/src/EventConfigBuilder.res.mjs +124 -15
  19. package/src/HandlerRegister.res +21 -4
  20. package/src/HandlerRegister.res.mjs +24 -5
  21. package/src/InMemoryStore.res +9 -7
  22. package/src/InMemoryStore.res.mjs +3 -3
  23. package/src/IndexerState.res +43 -6
  24. package/src/IndexerState.res.mjs +36 -7
  25. package/src/IndexerState.resi +4 -4
  26. package/src/Internal.res +16 -17
  27. package/src/Metrics.res +72 -6
  28. package/src/Metrics.res.mjs +30 -2
  29. package/src/Persistence.res +36 -30
  30. package/src/Persistence.res.mjs +1 -20
  31. package/src/PgStorage.res +104 -56
  32. package/src/PgStorage.res.mjs +57 -35
  33. package/src/PruneStaleHistory.res +85 -52
  34. package/src/PruneStaleHistory.res.mjs +63 -37
  35. package/src/Rollback.res +68 -45
  36. package/src/Rollback.res.mjs +38 -19
  37. package/src/RollbackCommit.res +7 -9
  38. package/src/RollbackCommit.res.mjs +5 -6
  39. package/src/SafeCheckpointTracking.res +11 -11
  40. package/src/SafeCheckpointTracking.res.mjs +7 -6
  41. package/src/SimulateItems.res +33 -18
  42. package/src/SimulateItems.res.mjs +26 -22
  43. package/src/Sink.res +38 -26
  44. package/src/Sink.res.mjs +21 -16
  45. package/src/TestIndexer.res +5 -5
  46. package/src/TestIndexer.res.mjs +1 -1
  47. package/src/Utils.res +170 -0
  48. package/src/Utils.res.mjs +153 -0
  49. package/src/Writing.res +2 -2
  50. package/src/Writing.res.mjs +1 -1
  51. package/src/bindings/ClickHouse.res +366 -698
  52. package/src/bindings/ClickHouse.res.mjs +371 -421
  53. package/src/bindings/ClickHouseSink.res +337 -0
  54. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  55. package/src/bindings/EventSource.res +8 -2
  56. package/src/bindings/NodeJs.res +6 -0
  57. package/src/bindings/Vitest.res +23 -0
  58. package/src/bindings/Vitest.res.mjs +3 -0
  59. package/src/bindings/WebSocket.res +9 -10
  60. package/src/db/CheckpointBounds.res +53 -0
  61. package/src/db/CheckpointBounds.res.mjs +44 -0
  62. package/src/db/EntityFilter.res +36 -17
  63. package/src/db/EntityFilter.res.mjs +25 -14
  64. package/src/db/EntityHistory.res +47 -22
  65. package/src/db/EntityHistory.res.mjs +19 -12
  66. package/src/db/InternalTable.res +84 -35
  67. package/src/db/InternalTable.res.mjs +56 -23
  68. package/src/db/RollbackFloors.res +55 -0
  69. package/src/db/RollbackFloors.res.mjs +92 -0
  70. package/src/db/Table.res +22 -4
  71. package/src/db/Table.res.mjs +30 -9
  72. package/src/sources/Evm.res +2 -0
  73. package/src/sources/Evm.res.mjs +2 -0
  74. package/src/sources/EvmHyperSyncSource.res +3 -3
  75. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  76. package/src/sources/EvmRpcWs.res +124 -0
  77. package/src/sources/EvmRpcWs.res.mjs +117 -0
  78. package/src/sources/Fuel.res +2 -0
  79. package/src/sources/Fuel.res.mjs +2 -0
  80. package/src/sources/FuelHyperSyncSource.res +1 -1
  81. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  82. package/src/sources/HeightFeed.res +568 -0
  83. package/src/sources/HeightFeed.res.mjs +452 -0
  84. package/src/sources/HeightStream.res +257 -0
  85. package/src/sources/HeightStream.res.mjs +194 -0
  86. package/src/sources/HyperSync.res +5 -0
  87. package/src/sources/HyperSync.res.mjs +3 -0
  88. package/src/sources/HyperSync.resi +4 -0
  89. package/src/sources/HyperSyncSSE.res +49 -0
  90. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  91. package/src/sources/RpcSource.res +1 -1
  92. package/src/sources/RpcSource.res.mjs +2 -2
  93. package/src/sources/Source.res +54 -1
  94. package/src/sources/Source.res.mjs +25 -0
  95. package/src/sources/SourceManager.res +220 -227
  96. package/src/sources/SourceManager.res.mjs +139 -164
  97. package/src/sources/SourceManager.resi +8 -0
  98. package/src/sources/Svm.res +2 -0
  99. package/src/sources/Svm.res.mjs +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +70 -127
  101. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  102. package/src/sources/SvmHyperSyncSource.res +18 -15
  103. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  104. package/svm.schema.json +1 -75
  105. package/src/MemoryStorage.res +0 -729
  106. package/src/MemoryStorage.res.mjs +0 -586
  107. package/src/sources/HyperSyncHeightStream.res +0 -129
  108. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  109. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  110. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
package/src/Core.res CHANGED
@@ -10,6 +10,7 @@ type svmHyperSyncClientCtor
10
10
  type fuelHyperSyncClientCtor
11
11
  type transactionStoreCtor
12
12
  type blockStoreCtor
13
+ type clickHouseSinkCtor
13
14
  type addressStoreCtor
14
15
  // Test-only: a local HyperSync server, bound by MockHyperSyncServer in envio-tests.
15
16
  type mockHyperSyncServerCtor
@@ -29,6 +30,7 @@ type fromUserApiResult = {
29
30
  type addon = {
30
31
  getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
31
32
  encodeIndexedTopic: (~abiType: string, ~value: unknown) => EvmTypes.Hex.t,
33
+ isSvmPubkey: (~value: string) => bool,
32
34
  fromUserApi: (string, fromUserApiOptions) => fromUserApiResult,
33
35
  runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
34
36
  @as("EvmHyperSyncClient")
@@ -45,6 +47,8 @@ type addon = {
45
47
  blockStore: blockStoreCtor,
46
48
  @as("AddressStore")
47
49
  addressStore: addressStoreCtor,
50
+ @as("ClickHouseSink")
51
+ clickHouseSink: clickHouseSinkCtor,
48
52
  @as("MockHyperSyncServer")
49
53
  mockHyperSyncServer: mockHyperSyncServerCtor,
50
54
  encodeAddresses: (~ecosystem: string, ~addresses: array<Address.t>) => array<NodeJs.Buffer.t>,
@@ -72,31 +72,23 @@ let totalReadyCount = (crossChainState: t) => {
72
72
  let nextItemIsNone = (crossChainState: t): bool =>
73
73
  !(crossChainState.chainStates->Dict.valuesToArray->Array.some(ChainState.hasReadyItem))
74
74
 
75
- let getSafeCheckpointId = (crossChainState: t) => {
76
- let result: ref<option<bigint>> = ref(None)
77
-
78
- for i in 0 to crossChainState.chainIds->Array.length - 1 {
79
- let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
80
- switch cs->ChainState.safeCheckpointTracking {
81
- | None => () // Skip chains with maxReorgDepth = 0
82
- | Some(safeCheckpointTracking) =>
83
- let safeCheckpointId =
84
- safeCheckpointTracking->SafeCheckpointTracking.getSafeCheckpointId(
75
+ // Each chain's safe checkpoint: the last one a reorg on that chain can no longer
76
+ // reach, or None while it has nothing safe yet. A chain that can't be rolled
77
+ // back (maxReorgDepth = 0) tracks none, and everything it has committed is safe.
78
+ let getSafeCheckpointIdByChain = (crossChainState: t, ~committedCheckpointId) =>
79
+ crossChainState.chainIds->Array.map(chainId => {
80
+ let cs = crossChainState->getChainState(chainId)
81
+ (
82
+ chainId,
83
+ switch cs->ChainState.safeCheckpointTracking {
84
+ | None => Some(committedCheckpointId)
85
+ | Some(tracking) =>
86
+ tracking->SafeCheckpointTracking.getSafeCheckpointId(
85
87
  ~sourceBlockNumber=cs->ChainState.knownHeight,
86
88
  )
87
- switch result.contents {
88
- | None => result := Some(safeCheckpointId)
89
- | Some(current) if safeCheckpointId < current => result := Some(safeCheckpointId)
90
- | _ => ()
91
- }
92
- }
93
- }
94
-
95
- switch result.contents {
96
- | Some(id) if id > 0n => Some(id)
97
- | _ => None // No safe checkpoint found
98
- }
99
- }
89
+ },
90
+ )
91
+ })
100
92
 
101
93
  // --- Cross-chain transitions. ---
102
94
 
@@ -65,23 +65,15 @@ function nextItemIsNone(crossChainState) {
65
65
  return !Object.values(crossChainState.chainStates).some(ChainState.hasReadyItem);
66
66
  }
67
67
 
68
- function getSafeCheckpointId(crossChainState) {
69
- let result;
70
- for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
71
- let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
72
- let safeCheckpointTracking = ChainState.safeCheckpointTracking(cs);
73
- if (safeCheckpointTracking !== undefined) {
74
- let safeCheckpointId = SafeCheckpointTracking.getSafeCheckpointId(safeCheckpointTracking, ChainState.knownHeight(cs));
75
- let current = result;
76
- if (!(current !== undefined && safeCheckpointId >= current)) {
77
- result = safeCheckpointId;
78
- }
79
- }
80
- }
81
- let id = result;
82
- if (id !== undefined && id > 0n) {
83
- return id;
84
- }
68
+ function getSafeCheckpointIdByChain(crossChainState, committedCheckpointId) {
69
+ return crossChainState.chainIds.map(chainId => {
70
+ let cs = crossChainState.chainStates[chainId];
71
+ let tracking = ChainState.safeCheckpointTracking(cs);
72
+ return [
73
+ chainId,
74
+ tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : committedCheckpointId
75
+ ];
76
+ });
85
77
  }
86
78
 
87
79
  function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
@@ -261,7 +253,7 @@ export {
261
253
  isInReorgThreshold,
262
254
  targetBufferSize,
263
255
  nextItemIsNone,
264
- getSafeCheckpointId,
256
+ getSafeCheckpointIdByChain,
265
257
  createBatch,
266
258
  enterReorgThreshold,
267
259
  applyBatchProgress,
@@ -21,7 +21,10 @@ let targetBufferSize: t => int
21
21
 
22
22
  // Derived (pure).
23
23
  let nextItemIsNone: t => bool
24
- let getSafeCheckpointId: t => option<Internal.checkpointId>
24
+ let getSafeCheckpointIdByChain: (
25
+ t,
26
+ ~committedCheckpointId: Internal.checkpointId,
27
+ ) => array<(ChainId.t, option<Internal.checkpointId>)>
25
28
 
26
29
  // Cross-chain transitions.
27
30
  let createBatch: (
package/src/Ecosystem.res CHANGED
@@ -10,6 +10,11 @@ type t = {
10
10
  for SVM. Centralised here so adding a new ecosystem only requires a
11
11
  new ecosystem record, not another switch in `Main.res`. */
12
12
  onBlockMethodName: string,
13
+ /** What the ecosystem calls a contract and one of its events, for
14
+ registration errors that are shared across ecosystems — "contract" /
15
+ "event" on EVM and Fuel, "program" / "instruction" on SVM. */
16
+ contractNoun: string,
17
+ eventNoun: string,
13
18
  /** Schema that unwraps the ecosystem-specific outer wrapper around the
14
19
  user's `where`-returned filter (`block.number` on EVM, `block.height`
15
20
  on Fuel, `slot` on SVM) and surfaces the raw inner `{_gte?, _lte?,
@@ -23,8 +28,8 @@ type t = {
23
28
  `option<unknown>`. Separate from `onBlockFilterSchema` because event
24
29
  block filters support only `_gte` (→ per-event `startBlock`) — `_lte`
25
30
  and `_every` are rejected by the inner `eventBlockRangeSchema` in
26
- `LogSelection.res`. SVM does not support event handlers, so its
27
- schema always surfaces `None`. */
31
+ `LogSelection.res`. SVM parses its own `where` (including the
32
+ `block.slot` chunk), so its schema always surfaces `None`. */
28
33
  onEventBlockFilterSchema: S.t<option<unknown>>,
29
34
  /** Base logger injected at construction. Used to build per-item child
30
35
  loggers (see `getItemLogger`). */
package/src/Envio.res CHANGED
@@ -83,10 +83,12 @@ type svmInstruction = {
83
83
  instructionName: string,
84
84
  discriminator: string,
85
85
  programId?: SvmTypes.Pubkey.t,
86
- data?: string,
86
+ data?: Uint8Array.t,
87
87
  path?: array<int>,
88
88
  isInner?: bool,
89
- args?: JSON.t,
89
+ // Decoded Borsh args; wide integers (u64/u128/i64/i128) are bigint, so the
90
+ // tree is not valid JSON.
91
+ args?: unknown,
90
92
  accounts?: dict<svmInstructionAccount>,
91
93
  accountArguments?: array<SvmTypes.Pubkey.t>,
92
94
  transaction?: svmTransaction,
@@ -283,9 +285,9 @@ type svmSimulateItem = {
283
285
  slot?: int,
284
286
  path?: array<int>,
285
287
  programId?: string,
286
- data?: string,
288
+ data?: Uint8Array.t,
287
289
  isInner?: bool,
288
- args?: JSON.t,
290
+ args?: unknown,
289
291
  accounts?: dict<{address: string}>,
290
292
  accountArguments?: array<string>,
291
293
  logs?: array<{kind?: string, message?: string}>,
@@ -317,9 +317,6 @@ let svmSelectionKinds = ["instruction", "transaction", "accountActivity", "block
317
317
  // typo, and an unrecognised key would read as an empty selection — silently
318
318
  // dropping every field `config.yaml` selected. Rejected here so the option is
319
319
  // held to the same shape whether or not the project type-checks it.
320
- let quotedJoin = (names: array<string>) =>
321
- names->Array.map(name => `"${name}"`)->Array.joinUnsafe(", ")
322
-
323
320
  let validateFieldsShapeOrThrow = (
324
321
  fields: unknown,
325
322
  ~registration: string,
@@ -337,7 +334,7 @@ let validateFieldsShapeOrThrow = (
337
334
  ->Array.forEach(key =>
338
335
  if !(validKeys->Array.includes(key)) {
339
336
  JsError.throwWithMessage(
340
- `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${quotedJoin(
337
+ `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${Utils.Array.quotedJoin(
341
338
  validKeys,
342
339
  )}.`,
343
340
  )
@@ -373,7 +370,7 @@ let parseFieldsOrThrow = (
373
370
  fields->Array.forEach(name => {
374
371
  if !(valid->Utils.Set.has(name)) {
375
372
  JsError.throwWithMessage(
376
- `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${quotedJoin(
373
+ `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${Utils.Array.quotedJoin(
377
374
  valid->Utils.Set.toArray,
378
375
  )}.`,
379
376
  )
@@ -577,7 +574,7 @@ let resolveSvmInlineFieldSelection = (
577
574
  ~contractName: string,
578
575
  ~eventName: string,
579
576
  ): Internal.fieldSelection => {
580
- let registration = `the "${eventName}" event registration on contract "${contractName}"`
577
+ let registration = `the "${eventName}" instruction on program "${contractName}"`
581
578
  validateFieldsShapeOrThrow(
582
579
  fields,
583
580
  ~registration,
@@ -638,9 +635,6 @@ let buildSvmInstructionEventConfig = (
638
635
  ~instructionName: string,
639
636
  ~programId: SvmTypes.Pubkey.t,
640
637
  ~discriminator: option<string>,
641
- ~discriminatorByteLen: int,
642
- ~accountFilters: array<Internal.svmAccountFilterGroup>,
643
- ~isInner: option<bool>,
644
638
  ~accounts: array<string>=[],
645
639
  ~args: JSON.t=JSON.Null,
646
640
  ~definedTypes: JSON.t=JSON.Null,
@@ -667,38 +661,187 @@ let buildSvmInstructionEventConfig = (
667
661
  simulateParamsSchema: paramsSchema,
668
662
  programId,
669
663
  discriminator,
670
- discriminatorByteLen,
671
664
  fieldSelection,
672
- accountFilters,
673
- isInner,
674
665
  accounts,
675
666
  args,
676
667
  definedTypes,
677
668
  }
678
669
  }
679
670
 
680
- // Enrich an SVM definition into a registration. SVM has no `where`; only the
681
- // 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.
682
805
  let buildSvmOnEventRegistration = (
683
806
  ~eventConfig: Internal.svmInstructionEventConfig,
684
807
  ~isWildcard: bool,
685
808
  ~handler: option<Internal.handler>,
686
809
  ~contractRegister: option<Internal.contractRegister>,
810
+ ~where: option<JSON.t>,
687
811
  ~fieldSelection: option<Internal.fieldSelection>=?,
688
812
  ~startBlock: option<int>=?,
689
813
  ): Internal.svmOnEventRegistration => {
690
- index: -1,
691
- eventConfig: (eventConfig :> Internal.eventConfig),
692
- handler,
693
- contractRegister,
694
- isWildcard,
695
- filterByAddresses: false,
696
- dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
697
- startBlock,
698
- fieldSelection: switch fieldSelection {
699
- | Some(fieldSelection) => fieldSelection
700
- | None => eventConfig.fieldSelection
701
- },
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
+ }
702
845
  }
703
846
 
704
847
  // ============== Build Fuel event config ==============
@@ -14,6 +14,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
14
14
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
15
15
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
16
16
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
17
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
17
18
 
18
19
  let paramMetaSchema = S$RescriptSchema.recursive(self => S$RescriptSchema.object(s => ({
19
20
  name: s.f("name", S$RescriptSchema.string),
@@ -261,17 +262,13 @@ let svmSelectionKinds = [
261
262
  "log"
262
263
  ];
263
264
 
264
- function quotedJoin(names) {
265
- return names.map(name => `"` + name + `"`).join(", ");
266
- }
267
-
268
265
  function validateFieldsShapeOrThrow(fields, registration, validKeys, shapeNoun) {
269
266
  if (typeof fields !== "object" || fields === null || Array.isArray(fields)) {
270
267
  Stdlib_JsError.throwWithMessage(`The fields option of ` + registration + ` must be an object of ` + shapeNoun + ` field names.`);
271
268
  }
272
269
  Object.keys(fields).forEach(key => {
273
270
  if (!validKeys.includes(key)) {
274
- return Stdlib_JsError.throwWithMessage(`Invalid "` + key + `" key in the fields option of ` + registration + `. Valid keys: ` + quotedJoin(validKeys) + `.`);
271
+ return Stdlib_JsError.throwWithMessage(`Invalid "` + key + `" key in the fields option of ` + registration + `. Valid keys: ` + Utils.$$Array.quotedJoin(validKeys) + `.`);
275
272
  }
276
273
  });
277
274
  }
@@ -295,7 +292,7 @@ function parseFieldsOrThrow(fields, valid, kind, registration, rejectEmptyOpt) {
295
292
  ) : [];
296
293
  fields$1.forEach(name => {
297
294
  if (!valid.has(name)) {
298
- Stdlib_JsError.throwWithMessage(`Invalid "` + name + `" field in the fields.` + kind + ` option of ` + registration + `. Valid ` + kind + ` fields: ` + quotedJoin(Array.from(valid)) + `.`);
295
+ Stdlib_JsError.throwWithMessage(`Invalid "` + name + `" field in the fields.` + kind + ` option of ` + registration + `. Valid ` + kind + ` fields: ` + Utils.$$Array.quotedJoin(Array.from(valid)) + `.`);
299
296
  }
300
297
  if (seen.has(name)) {
301
298
  Stdlib_JsError.throwWithMessage(`Duplicate "` + name + `" field in the fields.` + kind + ` option of ` + registration + `.`);
@@ -420,7 +417,7 @@ let validSvmLogFields = new Set([
420
417
  ]);
421
418
 
422
419
  function resolveSvmInlineFieldSelection(fields, contractName, eventName) {
423
- let registration = `the "` + eventName + `" event registration on contract "` + contractName + `"`;
420
+ let registration = `the "` + eventName + `" instruction on program "` + contractName + `"`;
424
421
  validateFieldsShapeOrThrow(fields, registration, svmSelectionKinds, "instruction, transaction, accountActivity, block and log");
425
422
  let accountActivity = selectionList(fields, "accountActivity");
426
423
  let log = selectionList(fields, "log");
@@ -436,7 +433,7 @@ function resolveSvmInlineFieldSelection(fields, contractName, eventName) {
436
433
  return Internal.makeFieldSelection(blockFields, transactionFields, Primitive_option.some(instructionFields), Primitive_option.some(accountActivityFields), Primitive_option.some(logFields), Svm.eventBlockFieldMask, Svm.eventTransactionFieldMask);
437
434
  }
438
435
 
439
- function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator, discriminatorByteLen, accountFilters, isInner, accountsOpt, argsOpt, definedTypesOpt) {
436
+ function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator, accountsOpt, argsOpt, definedTypesOpt) {
440
437
  let accounts = accountsOpt !== undefined ? accountsOpt : [];
441
438
  let args = argsOpt !== undefined ? argsOpt : null;
442
439
  let definedTypes = definedTypesOpt !== undefined ? definedTypesOpt : null;
@@ -451,16 +448,121 @@ function buildSvmInstructionEventConfig(contractName, instructionName, programId
451
448
  fieldSelection: fieldSelection,
452
449
  programId: programId,
453
450
  discriminator: discriminator,
454
- discriminatorByteLen: discriminatorByteLen,
455
- accountFilters: accountFilters,
456
- isInner: isInner,
457
451
  accounts: accounts,
458
452
  args: args,
459
453
  definedTypes: definedTypes
460
454
  };
461
455
  }
462
456
 
463
- function buildSvmOnEventRegistration(eventConfig, isWildcard, handler, contractRegister, fieldSelection, startBlock) {
457
+ let validSvmWhereKeys = [
458
+ "accounts",
459
+ "isInner",
460
+ "block"
461
+ ];
462
+
463
+ let svmBlockFilterSchema = S$RescriptSchema.strict(S$RescriptSchema.object(s => ({
464
+ slot: s.f("slot", S$RescriptSchema.option(LogSelection.eventBlockRangeSchema))
465
+ })));
466
+
467
+ function resolveSvmWhereOrThrow(where, contractName, eventName, accountNames) {
468
+ let invalid = message => Stdlib_JsError.throwWithMessage(`Invalid where configuration for the "` + eventName + `" instruction on program "` + contractName + `". ` + message);
469
+ let obj;
470
+ obj = typeof where === "object" && where !== null && !Array.isArray(where) ? where : invalid("Expected an object.");
471
+ Utils.Dict.forEachWithKey(obj, (param, key) => {
472
+ if (!validSvmWhereKeys.includes(key)) {
473
+ return invalid(`Unknown field "` + key + `". Valid fields: ` + Utils.$$Array.quotedJoin(validSvmWhereKeys) + `.`);
474
+ }
475
+ });
476
+ let match = obj["isInner"];
477
+ let isInner = match !== undefined ? (
478
+ typeof match === "boolean" ? match : invalid(`The "isInner" filter must be a boolean.`)
479
+ ) : undefined;
480
+ let parseGroup = group => Object.entries(group).map(param => {
481
+ let name = param[0];
482
+ let position = accountNames.indexOf(name);
483
+ let position$1 = position !== -1 ? (
484
+ position >= 10 ? invalid(`Account "` + name + `" is at position ` + position.toString() + `, and only the first ` + (10).toString() + ` accounts of an instruction can be filtered.`) : position
485
+ ) : (
486
+ Utils.$$Array.isEmpty(accountNames) ? invalid("The instruction has no named accounts to filter on. Add `accounts` and `args` to it in config.yaml, or attach an IDL.") : invalid(`The instruction has no account named "` + name + `" to filter on. Named accounts: ` + Utils.$$Array.quotedJoin(accountNames) + `.`)
487
+ );
488
+ let values = normalizeOrThrow(param[1]);
489
+ if (Utils.$$Array.isEmpty(values)) {
490
+ invalid(`The "` + name + `" filter must list at least one pubkey.`);
491
+ }
492
+ return {
493
+ position: position$1,
494
+ values: values.map(value => {
495
+ if (typeof value === "string") {
496
+ if (Core.getAddon().isSvmPubkey(value)) {
497
+ return value;
498
+ } else {
499
+ return invalid(`The "` + name + `" filter value "` + value + `" is not a base58 SVM pubkey.`);
500
+ }
501
+ } else {
502
+ return invalid(`The "` + name + `" filter must list base58 pubkeys as strings.`);
503
+ }
504
+ })
505
+ };
506
+ });
507
+ let match$1 = obj["accounts"];
508
+ let groups;
509
+ if (match$1 !== undefined) {
510
+ if (Array.isArray(match$1)) {
511
+ groups = match$1.map(entry => {
512
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
513
+ return parseGroup(entry);
514
+ } else {
515
+ return invalid(`Each entry in "accounts" must be an object.`);
516
+ }
517
+ });
518
+ } else if (match$1 === null) {
519
+ groups = invalid(`Expected "accounts" to be an object or an array of objects.`);
520
+ } else {
521
+ switch (typeof match$1) {
522
+ case "object" :
523
+ groups = [parseGroup(match$1)];
524
+ break;
525
+ default:
526
+ groups = invalid(`Expected "accounts" to be an object or an array of objects.`);
527
+ }
528
+ }
529
+ } else {
530
+ groups = [];
531
+ }
532
+ let accountFilters = groups.some(Utils.$$Array.isEmpty) ? [] : groups;
533
+ let block = obj["block"];
534
+ let startBlock;
535
+ if (block !== undefined) {
536
+ let filter;
537
+ try {
538
+ filter = S$RescriptSchema.parseOrThrow(block, svmBlockFilterSchema);
539
+ } catch (raw_exn) {
540
+ let exn = Primitive_exceptions.internalToException(raw_exn);
541
+ if (exn.RE_EXN_ID === S$RescriptSchema.Raised) {
542
+ filter = invalid(`The "block" filter is invalid: ` + Utils.prettifyExn(exn._1) + `. Only \`_gte\` is supported on instruction filters — use \`indexer.onSlot\` for \`_lte\` or \`_every\`.`);
543
+ } else {
544
+ throw exn;
545
+ }
546
+ }
547
+ let match$2 = filter.slot;
548
+ startBlock = match$2 !== undefined ? match$2._gte : undefined;
549
+ } else {
550
+ startBlock = undefined;
551
+ }
552
+ return {
553
+ accountFilters: accountFilters,
554
+ isInner: isInner,
555
+ startBlock: startBlock
556
+ };
557
+ }
558
+
559
+ function buildSvmOnEventRegistration(eventConfig, isWildcard, handler, contractRegister, where, fieldSelection, startBlock) {
560
+ let resolvedWhere = where !== undefined ? resolveSvmWhereOrThrow(where, eventConfig.contractName, eventConfig.name, eventConfig.accounts) : ({
561
+ accountFilters: [],
562
+ isInner: undefined,
563
+ startBlock: undefined
564
+ });
565
+ let sb = resolvedWhere.startBlock;
464
566
  return {
465
567
  index: -1,
466
568
  eventConfig: eventConfig,
@@ -469,8 +571,10 @@ function buildSvmOnEventRegistration(eventConfig, isWildcard, handler, contractR
469
571
  isWildcard: isWildcard,
470
572
  filterByAddresses: false,
471
573
  dependsOnAddresses: Internal.dependsOnAddresses(isWildcard, false),
472
- startBlock: startBlock,
473
- fieldSelection: fieldSelection !== undefined ? fieldSelection : eventConfig.fieldSelection
574
+ startBlock: sb !== undefined ? sb : startBlock,
575
+ fieldSelection: fieldSelection !== undefined ? fieldSelection : eventConfig.fieldSelection,
576
+ accountFilters: resolvedWhere.accountFilters,
577
+ isInner: resolvedWhere.isInner
474
578
  };
475
579
  }
476
580
 
@@ -542,6 +646,8 @@ function buildFuelOnEventRegistration(eventConfig, isWildcard, handler, contract
542
646
  };
543
647
  }
544
648
 
649
+ let filterableAccountCount = 10;
650
+
545
651
  export {
546
652
  paramMetaSchema,
547
653
  normalizeOrThrow,
@@ -561,7 +667,6 @@ export {
561
667
  rawEventBlockFields,
562
668
  evmSelectionKinds,
563
669
  svmSelectionKinds,
564
- quotedJoin,
565
670
  validateFieldsShapeOrThrow,
566
671
  selectionList,
567
672
  validBlockFields,
@@ -578,6 +683,10 @@ export {
578
683
  validSvmLogFields,
579
684
  resolveSvmInlineFieldSelection,
580
685
  buildSvmInstructionEventConfig,
686
+ validSvmWhereKeys,
687
+ svmBlockFilterSchema,
688
+ filterableAccountCount,
689
+ resolveSvmWhereOrThrow,
581
690
  buildSvmOnEventRegistration,
582
691
  buildFuelEventConfig,
583
692
  buildFuelOnEventRegistration,