envio 3.7.0 → 3.8.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 (43) hide show
  1. package/index.d.ts +331 -142
  2. package/package.json +6 -6
  3. package/src/ChainState.res +9 -1
  4. package/src/ChainState.res.mjs +6 -1
  5. package/src/Config.res +19 -10
  6. package/src/Config.res.mjs +17 -7
  7. package/src/Envio.res +54 -76
  8. package/src/EventConfigBuilder.res +159 -30
  9. package/src/EventConfigBuilder.res.mjs +120 -24
  10. package/src/HandlerRegister.res +20 -11
  11. package/src/HandlerRegister.res.mjs +22 -5
  12. package/src/Hasura.res +30 -22
  13. package/src/Hasura.res.mjs +11 -5
  14. package/src/InMemoryStore.res +1 -6
  15. package/src/InMemoryStore.res.mjs +1 -3
  16. package/src/Internal.res +33 -13
  17. package/src/Internal.res.mjs +12 -16
  18. package/src/Main.res +2 -5
  19. package/src/MemoryStorage.res +40 -8
  20. package/src/MemoryStorage.res.mjs +38 -16
  21. package/src/Persistence.res +4 -2
  22. package/src/PgStorage.res +6 -1
  23. package/src/PgStorage.res.mjs +1 -1
  24. package/src/SimulateItems.res +261 -9
  25. package/src/SimulateItems.res.mjs +218 -47
  26. package/src/TestIndexer.res +26 -10
  27. package/src/TestIndexer.res.mjs +34 -12
  28. package/src/bindings/ClickHouse.res +33 -6
  29. package/src/bindings/ClickHouse.res.mjs +25 -4
  30. package/src/db/InternalTable.res +11 -0
  31. package/src/db/InternalTable.res.mjs +7 -0
  32. package/src/sources/EvmHyperSyncSource.res +3 -2
  33. package/src/sources/SimulateSource.res +8 -4
  34. package/src/sources/SimulateSource.res.mjs +7 -3
  35. package/src/sources/Svm.res +34 -7
  36. package/src/sources/Svm.res.mjs +32 -4
  37. package/src/sources/SvmHyperSyncClient.res +10 -12
  38. package/src/sources/SvmHyperSyncClient.res.mjs +3 -2
  39. package/src/sources/SvmHyperSyncSource.res +91 -34
  40. package/src/sources/SvmHyperSyncSource.res.mjs +77 -37
  41. package/src/sources/TransactionStore.res +38 -0
  42. package/src/sources/TransactionStore.res.mjs +5 -0
  43. package/svm.schema.json +27 -78
package/src/Config.res CHANGED
@@ -33,13 +33,19 @@ type sourceConfig =
33
33
  // A `simulate` run: the items the test fed in, parsed against the chain's
34
34
  // registrations. The source itself is built with the chain's address store,
35
35
  // like every other source, so it can apply the same gates.
36
- | SimulateSourceConfig({items: array<Internal.item>, endBlock: int})
36
+ | SimulateSourceConfig({
37
+ items: array<Internal.item>,
38
+ endBlock: int,
39
+ transactionStore?: TransactionStore.t,
40
+ blockStore?: BlockStore.t,
41
+ })
37
42
  // For tests: pass custom sources directly
38
43
  | CustomSources(array<Source.t>)
39
44
 
40
45
  type chain = {
41
46
  name: string,
42
47
  id: ChainId.t,
48
+ ecosystem: Ecosystem.name,
43
49
  startBlock: int,
44
50
  endBlock?: int,
45
51
  maxReorgDepth: int,
@@ -183,6 +189,7 @@ module EnvioAddresses = {
183
189
  // The table already keys rows by chain through the composite `id`, so the
184
190
  // per-chain mode must not append a second chain-id column to it.
185
191
  crossChain: true,
192
+ internal: true,
186
193
  }->Internal.fromGenericEntityConfig
187
194
  }
188
195
 
@@ -235,9 +242,6 @@ let svmEventDescriptorSchema = S.schema(s =>
235
242
  {
236
243
  "discriminator": s.matches(S.option(S.string)),
237
244
  "discriminatorByteLen": s.matches(S.int),
238
- "transactionFields": s.matches(S.array(Internal.svmTransactionFieldSchema)),
239
- "blockFields": s.matches(S.option(S.array(Internal.svmBlockFieldSchema))),
240
- "includeLogs": s.matches(S.bool),
241
245
  // An array of AND-groups OR-ed together: the CLI normalizes both the flat
242
246
  // and `any_of` YAML shapes to `Vec<Vec<SvmAccountFilterJson>>`.
243
247
  "accountFilters": s.matches(
@@ -348,10 +352,18 @@ let propertySchema = S.schema(s =>
348
352
  }
349
353
  )
350
354
 
355
+ let clickhouseSkippingIndexSchema: S.t<Internal.clickhouseSkippingIndex> = S.object(s => {
356
+ Internal.name: s.field("name", S.string),
357
+ expr: s.field("expr", S.string),
358
+ type_: s.field("type", S.string),
359
+ granularity: ?s.field("granularity", S.option(S.int)),
360
+ })
361
+
351
362
  let clickhouseTableOptionsSchema: S.t<Internal.clickhouseTableOptions> = S.object(s => {
352
363
  Internal.partitionBy: ?s.field("partitionBy", S.option(S.string)),
353
364
  orderBy: ?s.field("orderBy", S.option(S.array(S.string))),
354
365
  ttl: ?s.field("ttl", S.option(S.string)),
366
+ skippingIndexes: ?s.field("skippingIndexes", S.option(S.array(clickhouseSkippingIndexSchema))),
355
367
  })
356
368
 
357
369
  // The entity's `clickhouse` storage arg mirrors the @storage directive:
@@ -377,6 +389,7 @@ let entityJsonSchema = S.schema(s =>
377
389
  "name": s.matches(S.string),
378
390
  "crossChain": s.matches(S.option(S.bool)),
379
391
  "storage": s.matches(S.option(entityStorageSchema)),
392
+ "internal": s.matches(S.option(S.bool)),
380
393
  "properties": s.matches(S.array(propertySchema)),
381
394
  "derivedFields": s.matches(S.option(S.array(derivedFieldSchema))),
382
395
  "compositeIndices": s.matches(S.option(S.array(S.array(compositeIndexFieldSchema)))),
@@ -580,6 +593,7 @@ let parseEntitiesFromJson = (
580
593
  table,
581
594
  storage,
582
595
  crossChain,
596
+ internal: entityJson["internal"]->Option.getOr(false),
583
597
  }->Internal.fromGenericEntityConfig
584
598
  })
585
599
  }
@@ -796,9 +810,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
796
810
  "svm": option<{
797
811
  "discriminator": option<string>,
798
812
  "discriminatorByteLen": int,
799
- "transactionFields": array<Internal.svmTransactionField>,
800
- "blockFields": option<array<Internal.svmBlockField>>,
801
- "includeLogs": bool,
802
813
  "accountFilters": option<
803
814
  array<array<{"position": int, "values": array<string>}>>,
804
815
  >,
@@ -832,9 +843,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
832
843
  ~programId,
833
844
  ~discriminator=svm["discriminator"],
834
845
  ~discriminatorByteLen=svm["discriminatorByteLen"],
835
- ~transactionFields=svm["transactionFields"],
836
- ~blockFields=?svm["blockFields"],
837
- ~includeLogs=svm["includeLogs"],
838
846
  ~accountFilters,
839
847
  ~isInner=svm["isInner"],
840
848
  ~accounts=svm["accounts"]->Option.getOr([]),
@@ -1012,6 +1020,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
1012
1020
  {
1013
1021
  name: chainName,
1014
1022
  id: chainId,
1023
+ ecosystem: ecosystemName,
1015
1024
  startBlock: publicChainConfig["startBlock"],
1016
1025
  endBlock: ?publicChainConfig["endBlock"],
1017
1026
  maxReorgDepth: switch ecosystemName {
@@ -70,7 +70,8 @@ let entityConfig = {
70
70
  schema: schema,
71
71
  table: table,
72
72
  storage: entityConfig_storage,
73
- crossChain: true
73
+ crossChain: true,
74
+ internal: true
74
75
  };
75
76
 
76
77
  let EnvioAddresses = {
@@ -124,9 +125,6 @@ let publicConfigChainSchema = S$RescriptSchema.schema(s => ({
124
125
  let svmEventDescriptorSchema = S$RescriptSchema.schema(s => ({
125
126
  discriminator: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
126
127
  discriminatorByteLen: s.m(S$RescriptSchema.int),
127
- transactionFields: s.m(S$RescriptSchema.array(Internal.svmTransactionFieldSchema)),
128
- blockFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.svmBlockFieldSchema))),
129
- includeLogs: s.m(S$RescriptSchema.bool),
130
128
  accountFilters: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(S$RescriptSchema.schema(s => ({
131
129
  position: s.m(S$RescriptSchema.int),
132
130
  values: s.m(S$RescriptSchema.array(S$RescriptSchema.string))
@@ -204,10 +202,18 @@ let propertySchema = S$RescriptSchema.schema(s => ({
204
202
  description: s.m(S$RescriptSchema.option(S$RescriptSchema.string))
205
203
  }));
206
204
 
205
+ let clickhouseSkippingIndexSchema = S$RescriptSchema.object(s => ({
206
+ name: s.f("name", S$RescriptSchema.string),
207
+ expr: s.f("expr", S$RescriptSchema.string),
208
+ type: s.f("type", S$RescriptSchema.string),
209
+ granularity: s.f("granularity", S$RescriptSchema.option(S$RescriptSchema.int))
210
+ }));
211
+
207
212
  let clickhouseTableOptionsSchema = S$RescriptSchema.object(s => ({
208
213
  partitionBy: s.f("partitionBy", S$RescriptSchema.option(S$RescriptSchema.string)),
209
214
  orderBy: s.f("orderBy", S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.string))),
210
- ttl: s.f("ttl", S$RescriptSchema.option(S$RescriptSchema.string))
215
+ ttl: s.f("ttl", S$RescriptSchema.option(S$RescriptSchema.string)),
216
+ skippingIndexes: s.f("skippingIndexes", S$RescriptSchema.option(S$RescriptSchema.array(clickhouseSkippingIndexSchema)))
211
217
  }));
212
218
 
213
219
  let entityClickhouseStorageJsonSchema = S$RescriptSchema.union([
@@ -230,6 +236,7 @@ let entityJsonSchema = S$RescriptSchema.schema(s => ({
230
236
  name: s.m(S$RescriptSchema.string),
231
237
  crossChain: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
232
238
  storage: s.m(S$RescriptSchema.option(entityStorageSchema)),
239
+ internal: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
233
240
  properties: s.m(S$RescriptSchema.array(propertySchema)),
234
241
  derivedFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(derivedFieldSchema))),
235
242
  compositeIndices: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(compositeIndexFieldSchema)))),
@@ -400,7 +407,8 @@ function parseEntitiesFromJson(entitiesJson, enumConfigsByName, globalStorage, d
400
407
  schema: schema,
401
408
  table: table,
402
409
  storage: storage,
403
- crossChain: crossChain
410
+ crossChain: crossChain,
411
+ internal: Stdlib_Option.getOr(entityJson.internal, false)
404
412
  };
405
413
  });
406
414
  }
@@ -564,7 +572,7 @@ function fromPublic(publicConfigJson) {
564
572
  position: af.position,
565
573
  values: af.values
566
574
  })));
567
- return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, svm.discriminatorByteLen, svm.includeLogs, svm.transactionFields, svm.blockFields, accountFilters, svm.isInner, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes);
575
+ return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, svm.discriminatorByteLen, accountFilters, svm.isInner, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes);
568
576
  }
569
577
  });
570
578
  } else {
@@ -696,6 +704,7 @@ function fromPublic(publicConfigJson) {
696
704
  return {
697
705
  name: chainName,
698
706
  id: chainId,
707
+ ecosystem: ecosystemName,
699
708
  startBlock: publicChainConfig.startBlock,
700
709
  endBlock: publicChainConfig.endBlock,
701
710
  maxReorgDepth: tmp,
@@ -1097,6 +1106,7 @@ export {
1097
1106
  compositeIndexFieldSchema,
1098
1107
  derivedFieldSchema,
1099
1108
  propertySchema,
1109
+ clickhouseSkippingIndexSchema,
1100
1110
  clickhouseTableOptionsSchema,
1101
1111
  entityClickhouseStorageJsonSchema,
1102
1112
  entityStorageSchema,
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,47 @@ 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 quotedJoin = (names: array<string>) =>
321
+ names->Array.map(name => `"${name}"`)->Array.joinUnsafe(", ")
322
+
323
+ let validateFieldsShapeOrThrow = (
324
+ fields: unknown,
325
+ ~registration: string,
326
+ ~validKeys: array<string>,
327
+ ~shapeNoun: string,
328
+ ) => {
329
+ if typeof(fields) !== #object || fields === %raw(`null`) || Array.isArray(fields) {
322
330
  JsError.throwWithMessage(
323
- `The fields option of ${registration} must be an object of block and transaction field names.`,
331
+ `The fields option of ${registration} must be an object of ${shapeNoun} field names.`,
324
332
  )
325
333
  }
326
- raw
334
+ fields
327
335
  ->(Utils.magic: unknown => dict<unknown>)
328
336
  ->Dict.keysToArray
329
337
  ->Array.forEach(key =>
330
- if !(selectionKinds->Array.includes(key)) {
338
+ if !(validKeys->Array.includes(key)) {
331
339
  JsError.throwWithMessage(
332
- `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${selectionKinds->Array.joinUnsafe(
333
- ", ",
340
+ `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${quotedJoin(
341
+ validKeys,
334
342
  )}.`,
335
343
  )
336
344
  }
337
345
  )
338
346
  }
339
347
 
348
+ let selectionList = (fields: unknown, key: string): option<array<string>> =>
349
+ switch (fields->(Utils.magic: unknown => dict<unknown>))->Dict.get(key) {
350
+ | None => None
351
+ | Some(value) => Some(value->(Utils.magic: unknown => array<string>))
352
+ }
353
+
340
354
  let validBlockFields = Utils.Set.fromArray(Evm.blockFields)
341
355
  let validTransactionFields = Utils.Set.fromArray(Evm.transactionFields)
342
356
 
@@ -345,6 +359,7 @@ let parseFieldsOrThrow = (
345
359
  ~valid: Utils.Set.t<string>,
346
360
  ~kind: string,
347
361
  ~registration: string,
362
+ ~rejectEmpty=false,
348
363
  ) => {
349
364
  let seen = Utils.Set.make()
350
365
  let fields = switch fields {
@@ -358,9 +373,9 @@ let parseFieldsOrThrow = (
358
373
  fields->Array.forEach(name => {
359
374
  if !(valid->Utils.Set.has(name)) {
360
375
  JsError.throwWithMessage(
361
- `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${valid
362
- ->Utils.Set.toArray
363
- ->Array.joinUnsafe(", ")}.`,
376
+ `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${quotedJoin(
377
+ valid->Utils.Set.toArray,
378
+ )}.`,
364
379
  )
365
380
  }
366
381
  if seen->Utils.Set.has(name) {
@@ -370,6 +385,11 @@ let parseFieldsOrThrow = (
370
385
  }
371
386
  seen->Utils.Set.add(name)->ignore
372
387
  })
388
+ if rejectEmpty && fields->Array.length === 0 {
389
+ JsError.throwWithMessage(
390
+ `The fields.${kind} option of ${registration} must list at least one field.`,
391
+ )
392
+ }
373
393
  seen
374
394
  }
375
395
 
@@ -377,21 +397,26 @@ let parseFieldsOrThrow = (
377
397
  // option itself (the names are for error messages), never on the chain, so one
378
398
  // `onEvent` call resolves once and shares the result across every chain.
379
399
  let resolveInlineFieldSelection = (
380
- fields: Internal.evmFieldsSelection,
400
+ fields: unknown,
381
401
  ~contractName: string,
382
402
  ~eventName: string,
383
403
  ~enableRawEvents: bool,
384
404
  ): Internal.fieldSelection => {
385
405
  let registration = `the "${eventName}" event registration on contract "${contractName}"`
386
- validateFieldsShapeOrThrow(fields, ~registration)
406
+ validateFieldsShapeOrThrow(
407
+ fields,
408
+ ~registration,
409
+ ~validKeys=evmSelectionKinds,
410
+ ~shapeNoun="block and transaction",
411
+ )
387
412
  let blockFields = parseFieldsOrThrow(
388
- fields.block,
413
+ selectionList(fields, "block"),
389
414
  ~valid=validBlockFields,
390
415
  ~kind="block",
391
416
  ~registration,
392
417
  )
393
418
  let transactionFields = parseFieldsOrThrow(
394
- fields.transaction,
419
+ selectionList(fields, "transaction"),
395
420
  ~valid=validTransactionFields,
396
421
  ~kind="transaction",
397
422
  ~registration,
@@ -498,9 +523,117 @@ let buildEvmOnEventRegistration = (
498
523
 
499
524
  // ============== Build SVM instruction event config ==============
500
525
 
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]
526
+ // `block.slot` is the item's own key, so an inline selection carries it
527
+ // whether or not the handler listed it.
528
+ let alwaysIncludedSvmBlockFields = ["slot"]
529
+
530
+ let validSvmInstructionFields = Utils.Set.fromArray([
531
+ "args",
532
+ "accounts",
533
+ "accountArguments",
534
+ "programId",
535
+ "data",
536
+ "path",
537
+ "isInner",
538
+ ])
539
+ let validSvmTransactionFields = Utils.Set.fromArray([
540
+ "transactionIndex",
541
+ "signature",
542
+ "feePayer",
543
+ "success",
544
+ "err",
545
+ "fee",
546
+ "computeUnitsConsumed",
547
+ "accountKeys",
548
+ "recentBlockhash",
549
+ "version",
550
+ "allSignatures",
551
+ ])
552
+ let validSvmAccountActivityFields = Utils.Set.fromArray([
553
+ "address",
554
+ "transactionAccountIndex",
555
+ "isSigner",
556
+ "isWritable",
557
+ "lamports",
558
+ "lamports.pre",
559
+ "lamports.post",
560
+ "token",
561
+ "token.mint",
562
+ "token.owner",
563
+ "token.decimals",
564
+ "token.preAmount",
565
+ "token.postAmount",
566
+ ])
567
+ let validSvmBlockFields = Utils.Set.fromArray([
568
+ "slot",
569
+ "time",
570
+ "hash",
571
+ "height",
572
+ "parentSlot",
573
+ "parentHash",
574
+ ])
575
+ let validSvmLogFields = Utils.Set.fromArray(["kind", "message"])
576
+
577
+ let resolveSvmInlineFieldSelection = (
578
+ fields: unknown,
579
+ ~contractName: string,
580
+ ~eventName: string,
581
+ ): Internal.fieldSelection => {
582
+ let registration = `the "${eventName}" event registration on contract "${contractName}"`
583
+ validateFieldsShapeOrThrow(
584
+ fields,
585
+ ~registration,
586
+ ~validKeys=svmSelectionKinds,
587
+ ~shapeNoun="instruction, transaction, accountActivity, block and log",
588
+ )
589
+ let accountActivity = selectionList(fields, "accountActivity")
590
+ let log = selectionList(fields, "log")
591
+ let instructionFields = parseFieldsOrThrow(
592
+ selectionList(fields, "instruction"),
593
+ ~valid=validSvmInstructionFields,
594
+ ~kind="instruction",
595
+ ~registration,
596
+ )
597
+ let transactionFields = parseFieldsOrThrow(
598
+ selectionList(fields, "transaction"),
599
+ ~valid=validSvmTransactionFields,
600
+ ~kind="transaction",
601
+ ~registration,
602
+ )
603
+ let accountActivityFields = parseFieldsOrThrow(
604
+ accountActivity,
605
+ ~valid=validSvmAccountActivityFields,
606
+ ~kind="accountActivity",
607
+ ~registration,
608
+ ~rejectEmpty=accountActivity->Option.isSome,
609
+ )
610
+ let blockFields = parseFieldsOrThrow(
611
+ selectionList(fields, "block"),
612
+ ~valid=validSvmBlockFields,
613
+ ~kind="block",
614
+ ~registration,
615
+ )
616
+ let logFields = parseFieldsOrThrow(
617
+ log,
618
+ ~valid=validSvmLogFields,
619
+ ~kind="log",
620
+ ~registration,
621
+ ~rejectEmpty=log->Option.isSome,
622
+ )
623
+ blockFields->Utils.Set.addMany(alwaysIncludedSvmBlockFields)
624
+ if accountActivityFields->Utils.Set.size > 0 {
625
+ transactionFields->Utils.Set.add("accountActivities")->ignore
626
+ }
627
+ Internal.makeFieldSelection(
628
+ ~blockFields,
629
+ ~transactionFields,
630
+ ~instructionFields,
631
+ ~accountActivityFields,
632
+ ~logFields,
633
+ ~blockMaskFn=Svm.eventBlockFieldMask,
634
+ ~transactionMaskFn=Svm.eventTransactionFieldMask,
635
+ )
636
+ }
504
637
 
505
638
  let buildSvmInstructionEventConfig = (
506
639
  ~contractName: string,
@@ -508,9 +641,6 @@ let buildSvmInstructionEventConfig = (
508
641
  ~programId: SvmTypes.Pubkey.t,
509
642
  ~discriminator: option<string>,
510
643
  ~discriminatorByteLen: int,
511
- ~includeLogs: bool,
512
- ~transactionFields: array<Internal.svmTransactionField>=[],
513
- ~blockFields: array<Internal.svmBlockField>=[],
514
644
  ~accountFilters: array<Internal.svmAccountFilterGroup>,
515
645
  ~isInner: option<bool>,
516
646
  ~accounts: array<string>=[],
@@ -523,12 +653,8 @@ let buildSvmInstructionEventConfig = (
523
653
  ->(Utils.magic: S.t<JSON.t> => S.t<Internal.eventParams>)
524
654
 
525
655
  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
- ),
656
+ ~blockFields=Utils.Set.fromArray(alwaysIncludedSvmBlockFields),
657
+ ~transactionFields=Utils.Set.make(),
532
658
  ~blockMaskFn=Svm.eventBlockFieldMask,
533
659
  ~transactionMaskFn=Svm.eventTransactionFieldMask,
534
660
  )
@@ -544,7 +670,6 @@ let buildSvmInstructionEventConfig = (
544
670
  programId,
545
671
  discriminator,
546
672
  discriminatorByteLen,
547
- includeLogs,
548
673
  fieldSelection,
549
674
  accountFilters,
550
675
  isInner,
@@ -561,6 +686,7 @@ let buildSvmOnEventRegistration = (
561
686
  ~isWildcard: bool,
562
687
  ~handler: option<Internal.handler>,
563
688
  ~contractRegister: option<Internal.contractRegister>,
689
+ ~fieldSelection: option<Internal.fieldSelection>=?,
564
690
  ~startBlock: option<int>=?,
565
691
  ): Internal.svmOnEventRegistration => {
566
692
  index: -1,
@@ -571,7 +697,10 @@ let buildSvmOnEventRegistration = (
571
697
  filterByAddresses: false,
572
698
  dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
573
699
  startBlock,
574
- fieldSelection: eventConfig.fieldSelection,
700
+ fieldSelection: switch fieldSelection {
701
+ | Some(fieldSelection) => fieldSelection
702
+ | None => eventConfig.fieldSelection
703
+ },
575
704
  }
576
705
 
577
706
  // ============== Build Fuel event config ==============