envio 3.6.1 → 3.7.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 (80) hide show
  1. package/index.d.ts +197 -9
  2. package/package.json +6 -6
  3. package/src/Batch.res +64 -25
  4. package/src/Batch.res.mjs +69 -46
  5. package/src/ChainFetching.res +13 -17
  6. package/src/ChainFetching.res.mjs +9 -14
  7. package/src/ChainState.res +160 -119
  8. package/src/ChainState.res.mjs +75 -46
  9. package/src/ChainState.resi +15 -12
  10. package/src/Config.res +9 -2
  11. package/src/Config.res.mjs +4 -4
  12. package/src/Core.res +5 -0
  13. package/src/Ecosystem.res +0 -1
  14. package/src/Envio.res +16 -3
  15. package/src/EventConfigBuilder.res +155 -40
  16. package/src/EventConfigBuilder.res.mjs +76 -24
  17. package/src/EventProcessing.res +4 -8
  18. package/src/EventProcessing.res.mjs +4 -4
  19. package/src/EventUtils.res +3 -47
  20. package/src/FetchState.res +124 -104
  21. package/src/FetchState.res.mjs +116 -100
  22. package/src/HandlerRegister.res +108 -2
  23. package/src/HandlerRegister.res.mjs +59 -10
  24. package/src/HandlerRegister.resi +4 -0
  25. package/src/Internal.res +84 -24
  26. package/src/Internal.res.mjs +33 -2
  27. package/src/LazyLoader.res +12 -11
  28. package/src/LazyLoader.res.mjs +13 -7
  29. package/src/Main.res +22 -9
  30. package/src/Main.res.mjs +18 -5
  31. package/src/MemoryStorage.res +724 -0
  32. package/src/MemoryStorage.res.mjs +601 -0
  33. package/src/PgStorage.res +18 -7
  34. package/src/PgStorage.res.mjs +14 -11
  35. package/src/ReorgDetection.res +0 -222
  36. package/src/ReorgDetection.res.mjs +0 -163
  37. package/src/Rollback.res +14 -15
  38. package/src/Rollback.res.mjs +4 -5
  39. package/src/SimulateItems.res +2 -2
  40. package/src/Utils.res +3 -0
  41. package/src/bindings/ClickHouse.res +16 -11
  42. package/src/bindings/ClickHouse.res.mjs +10 -10
  43. package/src/bindings/Vitest.res +1 -1
  44. package/src/sources/BlockStore.res +115 -5
  45. package/src/sources/BlockStore.res.mjs +25 -0
  46. package/src/sources/Evm.res +0 -1
  47. package/src/sources/Evm.res.mjs +0 -1
  48. package/src/sources/EvmHyperSyncSource.res +19 -84
  49. package/src/sources/EvmHyperSyncSource.res.mjs +24 -60
  50. package/src/sources/Fuel.res +24 -4
  51. package/src/sources/Fuel.res.mjs +23 -3
  52. package/src/sources/FuelHyperSync.res +8 -3
  53. package/src/sources/FuelHyperSync.res.mjs +5 -4
  54. package/src/sources/FuelHyperSync.resi +3 -1
  55. package/src/sources/FuelHyperSyncClient.res +7 -10
  56. package/src/sources/FuelHyperSyncSource.res +18 -45
  57. package/src/sources/FuelHyperSyncSource.res.mjs +18 -35
  58. package/src/sources/HyperSync.res +49 -229
  59. package/src/sources/HyperSync.res.mjs +74 -191
  60. package/src/sources/HyperSync.resi +11 -35
  61. package/src/sources/HyperSyncClient.res +9 -79
  62. package/src/sources/HyperSyncClient.res.mjs +2 -6
  63. package/src/sources/RequestStat.res +5 -0
  64. package/src/sources/RequestStat.res.mjs +2 -0
  65. package/src/sources/RpcSource.res +149 -43
  66. package/src/sources/RpcSource.res.mjs +104 -41
  67. package/src/sources/SimulateSource.res +8 -5
  68. package/src/sources/SimulateSource.res.mjs +6 -6
  69. package/src/sources/Source.res +89 -16
  70. package/src/sources/Source.res.mjs +50 -1
  71. package/src/sources/SourceManager.res +255 -50
  72. package/src/sources/SourceManager.res.mjs +149 -55
  73. package/src/sources/SourceManager.resi +2 -6
  74. package/src/sources/Svm.res +0 -7
  75. package/src/sources/Svm.res.mjs +0 -8
  76. package/src/sources/SvmHyperSyncClient.res +21 -16
  77. package/src/sources/SvmHyperSyncClient.res.mjs +3 -4
  78. package/src/sources/SvmHyperSyncSource.res +25 -117
  79. package/src/sources/SvmHyperSyncSource.res.mjs +8 -121
  80. package/svm.schema.json +3 -2
@@ -82,6 +82,36 @@ let startRegistration = (~config: Config.t) => {
82
82
  }
83
83
  }
84
84
 
85
+ // A registration a caller owns, rather than the single implicit one
86
+ // `startRegistration` installs. Tests run several indexers in one process, each
87
+ // with its own config, and handlers must register against the config they were
88
+ // written for.
89
+ type registration = activeRegistration
90
+
91
+ let makeRegistration = (~config: Config.t): registration => {
92
+ config,
93
+ registrationsByChainId: Dict.make(),
94
+ finished: false,
95
+ }
96
+
97
+ // Makes `registration` the target of `indexer.onEvent` & co. for the duration
98
+ // of `fn`, restoring whatever was active before — so one caller's handlers
99
+ // never land in another's registration. Concurrent scopes would still clobber
100
+ // each other: the pointer they swap is process-global.
101
+ let useRegistration = async (registration: registration, fn) => {
102
+ let previous = EnvioGlobal.value.activeRegistration
103
+ EnvioGlobal.value.activeRegistration = Some(registration->(Utils.magic: registration => unknown))
104
+ let restore = () => EnvioGlobal.value.activeRegistration = previous
105
+ switch await fn() {
106
+ | result =>
107
+ restore()
108
+ result
109
+ | exception exn =>
110
+ restore()
111
+ throw(exn)
112
+ }
113
+ }
114
+
85
115
  let getChainRegistrations = (r: activeRegistration, ~chainId: ChainId.t): chainRegistrations => {
86
116
  let key = chainId->ChainId.toString
87
117
  switch r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key) {
@@ -104,6 +134,7 @@ let buildOnEventRegistrationWith = (
104
134
  ~handler: option<Internal.handler>,
105
135
  ~contractRegister: option<Internal.contractRegister>,
106
136
  ~where: option<JSON.t>,
137
+ ~fieldSelection: option<Internal.fieldSelection>,
107
138
  ~startBlock=?,
108
139
  ): Internal.onEventRegistration => {
109
140
  switch config.ecosystem.name {
@@ -134,6 +165,7 @@ let buildOnEventRegistrationWith = (
134
165
  ~where,
135
166
  ~chainId,
136
167
  ~onEventBlockFilterSchema=config.ecosystem.onEventBlockFilterSchema,
168
+ ~fieldSelection?,
137
169
  ~startBlock?,
138
170
  ) :> Internal.onEventRegistration)
139
171
  }
@@ -163,6 +195,19 @@ let sameEventAndFilter = (
163
195
  | Fuel | Svm => true
164
196
  }
165
197
 
198
+ // The merged registration keeps `base`'s handler and slot, picks up the
199
+ // contractRegister from `other`, and carries the union of what both callbacks
200
+ // read. Relies on `onEventRegistration` having an optional field so the spread
201
+ // stays a runtime spread and keeps `other`'s ecosystem-only fields.
202
+ let mergeInto = (
203
+ base: Internal.onEventRegistration,
204
+ ~other: Internal.onEventRegistration,
205
+ ): Internal.onEventRegistration => {
206
+ ...base,
207
+ contractRegister: other.contractRegister,
208
+ fieldSelection: Internal.unionFieldSelection(base.fieldSelection, other.fieldSelection),
209
+ }
210
+
166
211
  // Merge each contractRegister into a matching handler registration (either
167
212
  // registration order; the merged registration takes the handler's slot so
168
213
  // dispatch order follows handler registration order). Two handlers (or two
@@ -188,7 +233,7 @@ let mergeRegistrations = (resolved: array<Internal.onEventRegistration>, ~config
188
233
  merged :=
189
234
  merged.contents
190
235
  ->Array.filterWithIndex((_, j) => j !== i)
191
- ->Array.concat([{...reg, contractRegister: target.contractRegister}])
236
+ ->Array.concat([reg->mergeInto(~other=target)])
192
237
  }
193
238
  } else {
194
239
  // A contractRegister merges into a matching handler registration,
@@ -202,7 +247,7 @@ let mergeRegistrations = (resolved: array<Internal.onEventRegistration>, ~config
202
247
  | i =>
203
248
  let target = merged.contents->Array.getUnsafe(i)
204
249
  let next = merged.contents->Array.copy
205
- next->Array.setUnsafe(i, {...target, contractRegister: reg.contractRegister})
250
+ next->Array.setUnsafe(i, target->mergeInto(~other=reg))
206
251
  merged := next
207
252
  }
208
253
  }
@@ -249,6 +294,27 @@ let addOnEventRegistration = (
249
294
  ) => {
250
295
  let isWildcard = eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)
251
296
  let where = eventOptions->Option.flatMap(v => v.where)
297
+ // The inline selection doesn't vary by chain: resolve it once here and share
298
+ // the one value (and its sets) across every chain's registration.
299
+ let fieldSelection = switch eventOptions->Option.flatMap(v => v.fields) {
300
+ | None => None
301
+ | Some(fields) =>
302
+ // Only EVM resolves a registration selection of its own; Fuel and SVM take
303
+ // theirs from the event config, so an inline one would be silently dropped.
304
+ if registration.config.ecosystem.name !== Evm {
305
+ JsError.throwWithMessage(
306
+ `The fields option of the "${eventName}" event registration on contract "${contractName}" is only supported on EVM. Select the fields in your config instead.`,
307
+ )
308
+ }
309
+ Some(
310
+ EventConfigBuilder.resolveInlineFieldSelection(
311
+ fields,
312
+ ~contractName,
313
+ ~eventName,
314
+ ~enableRawEvents=registration.config.enableRawEvents,
315
+ ),
316
+ )
317
+ }
252
318
  let matched = ref(false)
253
319
  registration.config.chainMap
254
320
  ->ChainMap.values
@@ -268,6 +334,7 @@ let addOnEventRegistration = (
268
334
  ~handler,
269
335
  ~contractRegister,
270
336
  ~where,
337
+ ~fieldSelection,
271
338
  ~startBlock=?contract.startBlock,
272
339
  )
273
340
  (registration->getChainRegistrations(~chainId=chainConfig.id)).onEventRegistrations
@@ -397,11 +464,47 @@ let getSimulateOnEventRegistrations = (
397
464
  ~handler=None,
398
465
  ~contractRegister=None,
399
466
  ~where=None,
467
+ ~fieldSelection=None,
400
468
  ),
401
469
  ]
402
470
  }
403
471
  }
404
472
 
473
+ // An RPC source can only deliver the fields it knows how to parse; the rest are
474
+ // silently skipped at materialisation. Every RPC on the chain counts, whatever
475
+ // it's for — a fallback or realtime source runs the same parsers as a sync one,
476
+ // so a field it can't deliver would go missing for whichever blocks it served.
477
+ // The selection can come from either `config.yaml` or the inline `fields`
478
+ // option, so the message names neither. Runs on the registrations a chain
479
+ // actually keeps, so a handler whose `where` opts out of this chain isn't held
480
+ // to its limits.
481
+ //
482
+ // The `config.yaml` half of this is also rejected at codegen, by the
483
+ // `RpcTransactionField` subenum in `system_config.rs`. That one reports every
484
+ // offending field at once and before the project builds; this one is the only
485
+ // check an inline selection reaches. `RpcFieldSelection_test.res` pins the two
486
+ // to the same field set.
487
+ let validateRpcFieldSelection = (
488
+ chainConfig: Config.chain,
489
+ registrations: array<Internal.onEventRegistration>,
490
+ ) => {
491
+ let hasRpc = switch chainConfig.sourceConfig {
492
+ | EvmSourceConfig({rpcs}) => !(rpcs->Utils.Array.isEmpty)
493
+ | _ => false
494
+ }
495
+ if hasRpc {
496
+ registrations->Array.forEach(reg =>
497
+ reg.fieldSelection.transactionFields->Utils.Set.forEach(name =>
498
+ if !RpcSource.isRpcTransactionField(name) {
499
+ JsError.throwWithMessage(
500
+ `The "${name}" transaction field selected for the "${reg.eventConfig.name}" event on contract "${reg.eventConfig.contractName}" is unavailable for indexing via RPC. Remove it from the field selection, or remove chain ${chainConfig.id->ChainId.toString}'s RPC source — even an RPC the chain only falls back to has to deliver the selection.`,
501
+ )
502
+ }
503
+ )
504
+ )
505
+ }
506
+ }
507
+
405
508
  let finishRegistration = (~config: Config.t): registrationsByChainId => {
406
509
  switch getActiveRegistration() {
407
510
  | Some(r) => {
@@ -452,6 +555,7 @@ let finishRegistration = (~config: Config.t): registrationsByChainId => {
452
555
  ~handler=None,
453
556
  ~contractRegister=None,
454
557
  ~where=None,
558
+ ~fieldSelection=None,
455
559
  ~startBlock=?contract.startBlock,
456
560
  ),
457
561
  )
@@ -487,6 +591,8 @@ let finishRegistration = (~config: Config.t): registrationsByChainId => {
487
591
  }
488
592
  })
489
593
 
594
+ validateRpcFieldSelection(chainConfig, onEventRegistrations)
595
+
490
596
  registrationsByChainId->Dict.set(
491
597
  key,
492
598
  {
@@ -4,6 +4,8 @@ import * as Utils from "./Utils.res.mjs";
4
4
  import * as ChainId from "./ChainId.res.mjs";
5
5
  import * as Logging from "./Logging.res.mjs";
6
6
  import * as ChainMap from "./ChainMap.res.mjs";
7
+ import * as Internal from "./Internal.res.mjs";
8
+ import * as RpcSource from "./sources/RpcSource.res.mjs";
7
9
  import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
8
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
@@ -54,6 +56,28 @@ function startRegistration(config) {
54
56
  queued.forEach(fn => fn(r));
55
57
  }
56
58
 
59
+ function makeRegistration(config) {
60
+ return {
61
+ config: config,
62
+ registrationsByChainId: {},
63
+ finished: false
64
+ };
65
+ }
66
+
67
+ async function useRegistration(registration, fn) {
68
+ let previous = EnvioGlobal.value.activeRegistration;
69
+ EnvioGlobal.value.activeRegistration = Primitive_option.some(registration);
70
+ let result;
71
+ try {
72
+ result = await fn();
73
+ } catch (exn) {
74
+ EnvioGlobal.value.activeRegistration = previous;
75
+ throw exn;
76
+ }
77
+ EnvioGlobal.value.activeRegistration = previous;
78
+ return result;
79
+ }
80
+
57
81
  function getChainRegistrations(r, chainId) {
58
82
  let key = ChainId.toString(chainId);
59
83
  let existing = r.registrationsByChainId[key];
@@ -70,11 +94,11 @@ function getChainRegistrations(r, chainId) {
70
94
  return fresh;
71
95
  }
72
96
 
73
- function buildOnEventRegistrationWith(config, chainId, eventConfig, isWildcard, handler, contractRegister, where, startBlock) {
97
+ function buildOnEventRegistrationWith(config, chainId, eventConfig, isWildcard, handler, contractRegister, where, fieldSelection, startBlock) {
74
98
  let match = config.ecosystem.name;
75
99
  switch (match) {
76
100
  case "evm" :
77
- return EventConfigBuilder.buildEvmOnEventRegistration(eventConfig, isWildcard, handler, contractRegister, where, chainId, config.ecosystem.onEventBlockFilterSchema, startBlock);
101
+ return EventConfigBuilder.buildEvmOnEventRegistration(eventConfig, isWildcard, handler, contractRegister, where, chainId, config.ecosystem.onEventBlockFilterSchema, fieldSelection, startBlock);
78
102
  case "fuel" :
79
103
  return EventConfigBuilder.buildFuelOnEventRegistration(eventConfig, isWildcard, handler, contractRegister, startBlock);
80
104
  case "svm" :
@@ -96,6 +120,13 @@ function sameEventAndFilter(a, b, config) {
96
120
  }
97
121
  }
98
122
 
123
+ function mergeInto(base, other) {
124
+ let newrecord = {...base};
125
+ newrecord.fieldSelection = Internal.unionFieldSelection(base.fieldSelection, other.fieldSelection);
126
+ newrecord.contractRegister = other.contractRegister;
127
+ return newrecord;
128
+ }
129
+
99
130
  function mergeRegistrations(resolved, config) {
100
131
  let merged = {
101
132
  contents: []
@@ -114,8 +145,7 @@ function mergeRegistrations(resolved, config) {
114
145
  return;
115
146
  }
116
147
  let target = merged.contents[i];
117
- let newrecord = {...reg};
118
- merged.contents = merged.contents.filter((param, j) => j !== i).concat([(newrecord.contractRegister = target.contractRegister, newrecord)]);
148
+ merged.contents = merged.contents.filter((param, j) => j !== i).concat([mergeInto(reg, target)]);
119
149
  return;
120
150
  }
121
151
  let i$1 = merged.contents.findIndex(m => {
@@ -131,9 +161,7 @@ function mergeRegistrations(resolved, config) {
131
161
  }
132
162
  let target$1 = merged.contents[i$1];
133
163
  let next = merged.contents.slice();
134
- let newrecord$1 = {...target$1};
135
- newrecord$1.contractRegister = reg.contractRegister;
136
- next[i$1] = newrecord$1;
164
+ next[i$1] = mergeInto(target$1, reg);
137
165
  merged.contents = next;
138
166
  });
139
167
  return merged.contents;
@@ -171,6 +199,8 @@ function describeConfigured(registration, contractName) {
171
199
  function addOnEventRegistration(registration, contractName, eventName, handler, contractRegister, eventOptions) {
172
200
  let isWildcard = Stdlib_Option.getOr(Stdlib_Option.flatMap(eventOptions, v => v.wildcard), false);
173
201
  let where = Stdlib_Option.flatMap(eventOptions, v => v.where);
202
+ let fields = Stdlib_Option.flatMap(eventOptions, v => v.fields);
203
+ let fieldSelection = fields !== undefined ? (registration.config.ecosystem.name !== "evm" ? Stdlib_JsError.throwWithMessage(`The fields option of the "` + eventName + `" event registration on contract "` + contractName + `" is only supported on EVM. Select the fields in your config instead.`) : undefined, EventConfigBuilder.resolveInlineFieldSelection(fields, contractName, eventName, registration.config.enableRawEvents)) : undefined;
174
204
  let matched = {
175
205
  contents: false
176
206
  };
@@ -184,7 +214,7 @@ function addOnEventRegistration(registration, contractName, eventName, handler,
184
214
  return;
185
215
  }
186
216
  matched.contents = true;
187
- let reg = buildOnEventRegistrationWith(registration.config, chainConfig.id, eventConfig, isWildcard, handler, contractRegister, where, contract.startBlock);
217
+ let reg = buildOnEventRegistrationWith(registration.config, chainConfig.id, eventConfig, isWildcard, handler, contractRegister, where, fieldSelection, contract.startBlock);
188
218
  getChainRegistrations(registration, chainConfig.id).onEventRegistrations.push(reg);
189
219
  });
190
220
  if (matched.contents) {
@@ -257,7 +287,23 @@ function getSimulateOnEventRegistrations(config, chainId, eventConfig) {
257
287
  if (Utils.$$Array.notEmpty(matching)) {
258
288
  return matching;
259
289
  } else {
260
- return [buildOnEventRegistrationWith(config, chainId, eventConfig, false, undefined, undefined, undefined, undefined)];
290
+ return [buildOnEventRegistrationWith(config, chainId, eventConfig, false, undefined, undefined, undefined, undefined, undefined)];
291
+ }
292
+ }
293
+
294
+ function validateRpcFieldSelection(chainConfig, registrations) {
295
+ let match = chainConfig.sourceConfig;
296
+ let hasRpc;
297
+ hasRpc = match.TAG === "EvmSourceConfig" ? !Utils.$$Array.isEmpty(match.rpcs) : false;
298
+ if (hasRpc) {
299
+ registrations.forEach(reg => {
300
+ reg.fieldSelection.transactionFields.forEach(name => {
301
+ if (!RpcSource.isRpcTransactionField(name)) {
302
+ return Stdlib_JsError.throwWithMessage(`The "` + name + `" transaction field selected for the "` + reg.eventConfig.name + `" event on contract "` + reg.eventConfig.contractName + `" is unavailable for indexing via RPC. Remove it from the field selection, or remove chain ` + ChainId.toString(chainConfig.id) + `'s RPC source — even an RPC the chain only falls back to has to deliver the selection.`);
303
+ }
304
+ });
305
+ });
306
+ return;
261
307
  }
262
308
  }
263
309
 
@@ -284,7 +330,7 @@ function finishRegistration(config) {
284
330
  return;
285
331
  }
286
332
  if (config.enableRawEvents) {
287
- rawEventRegs.push(buildOnEventRegistrationWith(config, chainId, eventConfig, false, undefined, undefined, undefined, contract.startBlock));
333
+ rawEventRegs.push(buildOnEventRegistrationWith(config, chainId, eventConfig, false, undefined, undefined, undefined, undefined, contract.startBlock));
288
334
  return;
289
335
  }
290
336
  let set = notRegisteredEventsByContract[contract.name];
@@ -307,6 +353,7 @@ function finishRegistration(config) {
307
353
  let newrecord = {...reg};
308
354
  onEventRegistrations.push((newrecord.index = onEventRegistrations.length, newrecord));
309
355
  });
356
+ validateRpcFieldSelection(chainConfig, onEventRegistrations);
310
357
  let chainRegs = r.registrationsByChainId[key];
311
358
  registrationsByChainId[key] = {
312
359
  onEventRegistrations: onEventRegistrations,
@@ -435,6 +482,8 @@ function registerOnBlock(name, where, handler, getChainsObject) {
435
482
 
436
483
  export {
437
484
  startRegistration,
485
+ makeRegistration,
486
+ useRegistration,
438
487
  resetOnEventRegistrations,
439
488
  isPendingRegistration,
440
489
  finishRegistration,
@@ -5,6 +5,10 @@ type chainRegistrations = {
5
5
  type registrationsByChainId = dict<chainRegistrations>
6
6
 
7
7
  let startRegistration: (~config: Config.t) => unit
8
+
9
+ type registration
10
+ let makeRegistration: (~config: Config.t) => registration
11
+ let useRegistration: (registration, unit => promise<'a>) => promise<'a>
8
12
  let resetOnEventRegistrations: unit => unit
9
13
  let isPendingRegistration: unit => bool
10
14
  let finishRegistration: (~config: Config.t) => registrationsByChainId
package/src/Internal.res CHANGED
@@ -140,7 +140,7 @@ let evmTransactionFieldSchema = S.enum(allEvmTransactionFields)
140
140
  // position in the selection mask) and `Svm.res` `transactionFields`.
141
141
  type svmTransactionField =
142
142
  | @as("transactionIndex") TransactionIndex
143
- | @as("signatures") Signatures
143
+ | @as("signature") Signature
144
144
  | @as("feePayer") FeePayer
145
145
  | @as("success") Success
146
146
  | @as("err") Err
@@ -150,10 +150,11 @@ type svmTransactionField =
150
150
  | @as("recentBlockhash") RecentBlockhash
151
151
  | @as("version") Version
152
152
  | @as("tokenBalances") TokenBalances
153
+ | @as("allSignatures") AllSignatures
153
154
 
154
155
  let allSvmTransactionFields: array<svmTransactionField> = [
155
156
  TransactionIndex,
156
- Signatures,
157
+ Signature,
157
158
  FeePayer,
158
159
  Success,
159
160
  Err,
@@ -163,6 +164,7 @@ let allSvmTransactionFields: array<svmTransactionField> = [
163
164
  RecentBlockhash,
164
165
  Version,
165
166
  TokenBalances,
167
+ AllSignatures,
166
168
  ]
167
169
  let svmTransactionFieldSchema = S.enum(allSvmTransactionFields)
168
170
 
@@ -416,6 +418,51 @@ type indexingContract = {
416
418
  effectiveStartBlock: int,
417
419
  }
418
420
 
421
+ // What a single registration fetches and materialises. Field names are strings
422
+ // so every ecosystem shares one type — the typed field variants are strings at
423
+ // runtime. Always built through `makeFieldSelection`/`unionFieldSelection`
424
+ // below, so a set and its mask are never derived from different inputs. Unlike
425
+ // `eventConfig`, not `private` — that would block those two constructors too,
426
+ // since a private record can only be coerced from a distinct record type.
427
+ type fieldSelection = {
428
+ blockFields: Utils.Set.t<string>,
429
+ transactionFields: Utils.Set.t<string>,
430
+ // The sets precompiled to the store selections `ChainState` materialises with.
431
+ blockMask: float,
432
+ transactionMask: float,
433
+ }
434
+
435
+ // `~blockMaskFn`/`~transactionMaskFn` are the ecosystem's `Evm`/`Svm`/`Fuel`
436
+ // mask functions, which the ecosystem modules pass in (they depend on this
437
+ // module, so it can't reach them).
438
+ let makeFieldSelection = (
439
+ ~blockFields: Utils.Set.t<string>,
440
+ ~transactionFields: Utils.Set.t<string>,
441
+ ~blockMaskFn: Utils.Set.t<string> => float,
442
+ ~transactionMaskFn: Utils.Set.t<string> => float,
443
+ ): fieldSelection => {
444
+ blockFields,
445
+ transactionFields,
446
+ blockMask: blockMaskFn(blockFields),
447
+ transactionMask: transactionMaskFn(transactionFields),
448
+ }
449
+
450
+ // Two registrations merged into one dispatch off a single item, so it has to
451
+ // carry the union of what both callbacks read. Each callback's type only claims
452
+ // its own selection, so the extra fields are unread rather than unsound.
453
+ //
454
+ // The overwhelmingly common case is two registrations that never named fields
455
+ // inline and so share their event config's sets by reference — returning those
456
+ // unchanged keeps the merge allocation-free.
457
+ let unionFields = (a, b) => a === b ? a : a->Utils.Set.union(b)
458
+
459
+ let unionFieldSelection = (a: fieldSelection, b: fieldSelection): fieldSelection => {
460
+ blockFields: unionFields(a.blockFields, b.blockFields),
461
+ transactionFields: unionFields(a.transactionFields, b.transactionFields),
462
+ blockMask: FieldMask.orMask(a.blockMask, b.blockMask),
463
+ transactionMask: FieldMask.orMask(a.transactionMask, b.transactionMask),
464
+ }
465
+
419
466
  // Definition of an event/instruction we know how to decode: identity + decode
420
467
  // schemas + chain-independent field selection. A pure function of the ABI +
421
468
  // config, shared across chains. `private` so it can only be coerced from an
@@ -427,22 +474,12 @@ type eventConfig = private {
427
474
  contractName: string,
428
475
  paramsRawEventSchema: S.schema<eventParams>,
429
476
  simulateParamsSchema: S.schema<eventParams>,
430
- // Field names selected for the chain's transaction-store materialisation
431
- // (camelCase, matching the ecosystem's `transactionFields`). Stored as a
432
- // string set so the shared mask logic is ecosystem-agnostic; sources recover
433
- // the typed view where they need it.
434
- selectedTransactionFields: Utils.Set.t<string>,
435
- // `selectedTransactionFields` precompiled to the transaction-store selection
436
- // bitmask (bit per ecosystem field code). Materialisation reads this per item
437
- // so each transaction decodes only the fields its event selected. `0.` when
438
- // nothing is selected or the ecosystem carries the transaction inline (Fuel).
439
- transactionFieldMask: float,
440
- // Selected block fields precompiled to the block-store selection bitmask (bit
441
- // per ecosystem field code). `0.` for ecosystems that carry the block fully
442
- // inline (RPC/Fuel). The EVM selection always includes number/timestamp/hash,
443
- // so an EVM mask always has their bits set; SVM stamps slot/time/hash inline
444
- // from the response and its mask is the user's selection alone.
445
- blockFieldMask: float,
477
+ // The `config.yaml` selection, which every registration of this event
478
+ // inherits unless it names its own fields inline. Held by reference, so two
479
+ // un-customized registrations share one set which is what keeps a merge
480
+ // allocation-free and the per-source parser caches (keyed on set identity)
481
+ // effective.
482
+ fieldSelection: fieldSelection,
446
483
  }
447
484
 
448
485
  type fuelEventKind =
@@ -497,7 +534,6 @@ type onEventWhereArgs<'chain> = {chain: 'chain}
497
534
 
498
535
  type evmEventConfig = {
499
536
  ...eventConfig,
500
- selectedBlockFields: Utils.Set.t<evmBlockField>,
501
537
  sighash: string,
502
538
  topicCount: int,
503
539
  paramsMetadata: array<paramMeta>,
@@ -525,10 +561,6 @@ type svmAccountFilterGroup = array<svmAccountFilter>
525
561
 
526
562
  type svmInstructionEventConfig = {
527
563
  ...eventConfig,
528
- /** Block fields selected via `field_selection.block_fields` (`slot` is always
529
- included and excluded from this set). Drives the block query columns;
530
- precompiled to `blockFieldMask` for store materialisation. */
531
- selectedBlockFields: Utils.Set.t<svmBlockField>,
532
564
  /** Base58 Solana program id this instruction belongs to. */
533
565
  programId: SvmTypes.Pubkey.t,
534
566
  /** Hex-encoded discriminator. `None` matches every instruction in the program. */
@@ -589,6 +621,10 @@ type onEventRegistration = {
589
621
  // Final start block: the contract/chain config value, overridden by a
590
622
  // `where.block.number._gte` when the registered `where` supplies one.
591
623
  startBlock: option<int>,
624
+ // The `eventConfig` selection unless the registration passed an inline
625
+ // `fields` option, which replaces it. Two registrations of one event can
626
+ // select different fields, so this is per-registration rather than shared.
627
+ fieldSelection: fieldSelection,
592
628
  }
593
629
 
594
630
  type evmOnEventRegistration = {
@@ -625,6 +661,7 @@ type eventItem = private {
625
661
  chainId: ChainId.t,
626
662
  blockNumber: int,
627
663
  logIndex: int,
664
+ orderPath?: array<int>,
628
665
  // Within-block transaction index — the key into the per-chain transaction
629
666
  // store. Unused (0) for ecosystems that carry the transaction inline (Fuel).
630
667
  transactionIndex: int,
@@ -678,17 +715,31 @@ type item =
678
715
  chainId: ChainId.t,
679
716
  blockNumber: int,
680
717
  logIndex: int,
718
+ // Ordering tiebreak for ecosystems whose within-block order isn't a
719
+ // scalar. SVM keys an instruction by `(transactionIndex, path)`: the
720
+ // logIndex above is the transaction, this is its position in that
721
+ // transaction's CPI tree. Absent on EVM and Fuel, whose log/receipt
722
+ // index already totally orders a block.
723
+ orderPath?: array<int>,
681
724
  transactionIndex: int,
682
725
  payload: eventPayload,
683
726
  })
684
- | @as(1) Block({onBlockRegistration: onBlockRegistration, blockNumber: int, logIndex: int})
727
+ | @as(1) Block({onBlockRegistration: onBlockRegistration, blockNumber: int})
685
728
 
686
729
  external castUnsafeEventItem: item => eventItem = "%identity"
687
730
 
688
731
  @get
689
732
  external getItemBlockNumber: item => int = "blockNumber"
733
+ // Only meaningful on an `Event`: a block item has no log index, and the buffer
734
+ // comparator reads this only after the kinds match.
690
735
  @get
691
736
  external getItemLogIndex: item => int = "logIndex"
737
+ @get
738
+ external getItemOrderPath: item => Nullable.t<array<int>> = "orderPath"
739
+ // The variant tag. Read directly so the comparator can order every event of a
740
+ // block ahead of that block's handlers without a `switch`.
741
+ @get
742
+ external getItemKind: item => int = "kind"
692
743
 
693
744
  let getItemChainId = item =>
694
745
  switch item {
@@ -696,9 +747,18 @@ let getItemChainId = item =>
696
747
  | Block({onBlockRegistration: {chainId}}) => chainId
697
748
  }
698
749
 
750
+ // The `fields` option of an EVM `onEvent`/`contractRegister` registration:
751
+ // the block and transaction fields the handler reads. Replaces the config
752
+ // `field_selection` for this registration.
753
+ type evmFieldsSelection = {
754
+ block?: array<string>,
755
+ transaction?: array<string>,
756
+ }
757
+
699
758
  type eventOptions<'where> = {
700
759
  wildcard?: bool,
701
760
  where?: 'where,
761
+ fields?: evmFieldsSelection,
702
762
  }
703
763
 
704
764
  type fuelSupplyParams = {
@@ -4,6 +4,7 @@ import * as Table from "./db/Table.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
5
  import * as Address from "./Address.res.mjs";
6
6
  import * as ChainId from "./ChainId.res.mjs";
7
+ import * as FieldMask from "./sources/FieldMask.res.mjs";
7
8
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
9
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
9
10
 
@@ -78,7 +79,7 @@ let evmTransactionFieldSchema = S$RescriptSchema.$$enum(allEvmTransactionFields)
78
79
 
79
80
  let allSvmTransactionFields = [
80
81
  "transactionIndex",
81
- "signatures",
82
+ "signature",
82
83
  "feePayer",
83
84
  "success",
84
85
  "err",
@@ -87,7 +88,8 @@ let allSvmTransactionFields = [
87
88
  "accountKeys",
88
89
  "recentBlockhash",
89
90
  "version",
90
- "tokenBalances"
91
+ "tokenBalances",
92
+ "allSignatures"
91
93
  ];
92
94
 
93
95
  let svmTransactionFieldSchema = S$RescriptSchema.$$enum(allSvmTransactionFields);
@@ -139,6 +141,32 @@ let evmNullableTransactionFields = new Set([
139
141
  "type"
140
142
  ]);
141
143
 
144
+ function makeFieldSelection(blockFields, transactionFields, blockMaskFn, transactionMaskFn) {
145
+ return {
146
+ blockFields: blockFields,
147
+ transactionFields: transactionFields,
148
+ blockMask: blockMaskFn(blockFields),
149
+ transactionMask: transactionMaskFn(transactionFields)
150
+ };
151
+ }
152
+
153
+ function unionFields(a, b) {
154
+ if (a === b) {
155
+ return a;
156
+ } else {
157
+ return a.union(b);
158
+ }
159
+ }
160
+
161
+ function unionFieldSelection(a, b) {
162
+ return {
163
+ blockFields: unionFields(a.blockFields, b.blockFields),
164
+ transactionFields: unionFields(a.transactionFields, b.transactionFields),
165
+ blockMask: FieldMask.orMask(a.blockMask, b.blockMask),
166
+ transactionMask: FieldMask.orMask(a.transactionMask, b.transactionMask)
167
+ };
168
+ }
169
+
142
170
  function dependsOnAddresses(isWildcard, filterByAddresses) {
143
171
  if (isWildcard) {
144
172
  return filterByAddresses;
@@ -306,6 +334,9 @@ export {
306
334
  svmBlockFieldSchema,
307
335
  evmNullableBlockFields,
308
336
  evmNullableTransactionFields,
337
+ makeFieldSelection,
338
+ unionFields,
339
+ unionFieldSelection,
309
340
  dependsOnAddresses,
310
341
  getItemChainId,
311
342
  fuelSupplyParamsSchema,
@@ -58,6 +58,17 @@ let timeoutAfter = timeoutMillis =>
58
58
  )
59
59
  )
60
60
 
61
+ // Remember `k` as cached, evicting the oldest entry once the cache is full.
62
+ let rememberLoaded = (am: asyncMap<'key, 'value>, k: 'key) => {
63
+ if am.loadedKeys->SDSL.Queue.push(k) > am._cacheSize {
64
+ switch am.loadedKeys->SDSL.Queue.pop {
65
+ | None => ()
66
+ | Some(old) =>
67
+ let _ = am.externalPromises->Utils.Map.delete(old)
68
+ }
69
+ }
70
+ }
71
+
61
72
  let rec loadNext = async (am: asyncMap<'key, 'value>, k: 'key) => {
62
73
  // Track that we are loading it now
63
74
  let _ = am.inProgress->Utils.Set.add(k)
@@ -75,17 +86,7 @@ let rec loadNext = async (am: asyncMap<'key, 'value>, k: 'key) => {
75
86
  // Track that it is no longer in progress
76
87
  let _ = am.inProgress->Utils.Set.delete(k)
77
88
 
78
- // Track that we've loaded this key
79
- let loadedKeysNumber = am.loadedKeys->SDSL.Queue.push(k)
80
-
81
- // Delete the oldest key if the cache is overly full
82
- if loadedKeysNumber > am._cacheSize {
83
- switch am.loadedKeys->SDSL.Queue.pop {
84
- | None => ()
85
- | Some(old) =>
86
- let _ = am.externalPromises->Utils.Map.delete(old)
87
- }
88
- }
89
+ am->rememberLoaded(k)
89
90
 
90
91
  // Load the next one, if there is anything in the queue
91
92
  switch am.loaderQueue->SDSL.Queue.pop {