envio 3.3.2 → 3.5.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +6 -6
  2. package/src/Api.res +1 -8
  3. package/src/Api.res.mjs +1 -5
  4. package/src/Bin.res +2 -2
  5. package/src/ChainState.res +6 -1
  6. package/src/ChainState.res.mjs +4 -3
  7. package/src/Config.res +38 -5
  8. package/src/Config.res.mjs +35 -6
  9. package/src/Core.res +65 -20
  10. package/src/Core.res.mjs +36 -6
  11. package/src/EnvioGlobal.res +0 -2
  12. package/src/EnvioGlobal.res.mjs +0 -1
  13. package/src/ExitOnCaughtUp.res +6 -2
  14. package/src/ExitOnCaughtUp.res.mjs +4 -0
  15. package/src/FetchState.res +3 -3
  16. package/src/HandlerRegister.res +357 -434
  17. package/src/HandlerRegister.res.mjs +202 -242
  18. package/src/HandlerRegister.resi +7 -15
  19. package/src/IndexerState.res +10 -0
  20. package/src/IndexerState.res.mjs +9 -3
  21. package/src/IndexerState.resi +3 -0
  22. package/src/Internal.res +10 -1
  23. package/src/Main.res.mjs +4 -4
  24. package/src/PgStorage.res +16 -1
  25. package/src/PgStorage.res.mjs +9 -1
  26. package/src/SimulateItems.res +61 -40
  27. package/src/SimulateItems.res.mjs +37 -21
  28. package/src/TestIndexer.res +453 -454
  29. package/src/TestIndexer.res.mjs +321 -344
  30. package/src/bindings/ClickHouse.res +68 -2
  31. package/src/bindings/ClickHouse.res.mjs +47 -2
  32. package/src/sources/EvmChain.res +1 -1
  33. package/src/sources/EvmChain.res.mjs +2 -2
  34. package/src/sources/FuelHyperSync.res +74 -0
  35. package/src/sources/FuelHyperSync.res.mjs +80 -0
  36. package/src/sources/FuelHyperSync.resi +22 -0
  37. package/src/sources/FuelHyperSyncClient.res +120 -0
  38. package/src/sources/FuelHyperSyncClient.res.mjs +71 -0
  39. package/src/sources/FuelHyperSyncSource.res +257 -0
  40. package/src/sources/FuelHyperSyncSource.res.mjs +252 -0
  41. package/src/sources/HyperSyncClient.res +2 -2
  42. package/src/sources/HyperSyncClient.res.mjs +1 -1
  43. package/src/sources/SvmHyperSyncClient.res +139 -102
  44. package/src/sources/SvmHyperSyncClient.res.mjs +45 -5
  45. package/src/sources/SvmHyperSyncSource.res +60 -440
  46. package/src/sources/SvmHyperSyncSource.res.mjs +42 -363
  47. package/src/TestIndexerProxyStorage.res +0 -196
  48. package/src/TestIndexerProxyStorage.res.mjs +0 -121
  49. package/src/TestIndexerWorker.res +0 -4
  50. package/src/TestIndexerWorker.res.mjs +0 -7
  51. package/src/sources/EventRouter.res +0 -165
  52. package/src/sources/EventRouter.res.mjs +0 -153
  53. package/src/sources/HyperFuel.res +0 -179
  54. package/src/sources/HyperFuel.res.mjs +0 -146
  55. package/src/sources/HyperFuel.resi +0 -36
  56. package/src/sources/HyperFuelClient.res +0 -127
  57. package/src/sources/HyperFuelClient.res.mjs +0 -20
  58. package/src/sources/HyperFuelSource.res +0 -502
  59. package/src/sources/HyperFuelSource.res.mjs +0 -481
  60. /package/src/sources/{HyperSyncSource.res → EvmHyperSyncSource.res} +0 -0
  61. /package/src/sources/{HyperSyncSource.res.mjs → EvmHyperSyncSource.res.mjs} +0 -0
@@ -8,159 +8,15 @@ type options = {
8
8
  clientTimeoutMillis: int,
9
9
  }
10
10
 
11
- // Build HyperSync InstructionSelections from event configs. Each AND-group in
12
- // `cfg.accountFilters` becomes its own selection; selections sharing the same
13
- // `(programId, dN)` are OR-ed by the wire protocol. Empty outer array emits
14
- // one selection with no `aN` set (no account filtering).
15
- //
16
- // Empty programId means the config carries no real program (placeholder), in
17
- // which case we skip — better to over-fetch nothing than ship a degenerate
18
- // query.
19
- let buildInstructionSelections = (eventConfigs: array<Internal.svmInstructionEventConfig>): array<
20
- SvmHyperSyncClient.QueryTypes.instructionSelection,
21
- > => {
22
- eventConfigs->Array.flatMap(cfg => {
23
- let programIdString = cfg.programId->SvmTypes.Pubkey.toString
24
- if programIdString === "" {
25
- []
26
- } else {
27
- // Each instruction owns exactly one dN field — the one matching its
28
- // declared byte length. The server-side filter is `d{N} IN [..]`.
29
- let (d1, d2, d4, d8) = switch (cfg.discriminator, cfg.discriminatorByteLen) {
30
- | (Some(d), 1) => (Some([d]), None, None, None)
31
- | (Some(d), 2) => (None, Some([d]), None, None)
32
- | (Some(d), 4) => (None, None, Some([d]), None)
33
- | (Some(d), 8) => (None, None, None, Some([d]))
34
- | _ => (None, None, None, None)
35
- }
36
- let groups = switch cfg.accountFilters {
37
- | [] => [[]]
38
- | gs => gs
39
- }
40
- groups->Array.map(group => {
41
- let pick = position =>
42
- group
43
- ->Array.filterMap(
44
- f => f.position == position ? Some(f.values->SvmTypes.Pubkey.toStrings) : None,
45
- )
46
- ->Array.get(0)
47
-
48
- (
49
- {
50
- programId: [programIdString],
51
- ?d1,
52
- ?d2,
53
- ?d4,
54
- ?d8,
55
- a0: ?pick(0),
56
- a1: ?pick(1),
57
- a2: ?pick(2),
58
- a3: ?pick(3),
59
- a4: ?pick(4),
60
- a5: ?pick(5),
61
- isInner: ?cfg.isInner,
62
- }: SvmHyperSyncClient.QueryTypes.instructionSelection
63
- )
64
- })
65
- }
66
- })
67
- }
68
-
69
11
  // Synthesize a stable logIndex for an SVM instruction so the FetchState
70
12
  // ordering machinery (which compares by `(blockNumber, logIndex)`) sorts
71
13
  // instructions deterministically within a slot. The bit packing fits inside
72
14
  // JS's 53-bit safe-integer range: transactionIndex ≤ ~10k per slot,
73
15
  // instruction position ≤ 1000 per tx, depth ≤ ~10. Outer-only instructions
74
16
  // land at `tx * 65536`; inner ones append depth-weighted offsets.
75
- let synthLogIndex = (instr: SvmHyperSyncClient.ResponseTypes.instruction) => {
76
- let tx = instr.transactionIndex
77
- let addrSum = instr.instructionAddress->Array.reduce(0, (acc, n) => acc * 1024 + n + 1)
78
- tx * 65536 + addrSum
79
- }
80
-
81
- let serializeInstructionAddress = (addr: array<int>) =>
82
- addr->Array.map(n => n->Int.toString)->Array.joinUnsafe(",")
83
-
84
- // Build per-program schema descriptors by grouping eventConfigs by programId,
85
- // returning one descriptor JSON per program. These are handed to the Solana
86
- // client at creation; it builds them into decoders and decodes matching
87
- // instructions inline on `get`.
88
- let buildProgramSchemas = (eventConfigs: array<Internal.svmInstructionEventConfig>): array<
89
- string,
90
- > => {
91
- // Group by programId base58 string. Skip events that carry no schema
92
- // (accounts == [] && args is JSON.Null && definedTypes is JSON.Null —
93
- // the resolved-empty case from system_config.rs).
94
- let descriptorsByProgram: dict<{
95
- "programId": string,
96
- "definedTypes": JSON.t,
97
- "instructions": array<{
98
- "name": string,
99
- "discriminator": string,
100
- "accounts": array<string>,
101
- "args": JSON.t,
102
- }>,
103
- }> = Dict.make()
104
-
105
- eventConfigs->Array.forEach(ec => {
106
- let programIdString = ec.programId->SvmTypes.Pubkey.toString
107
- if programIdString === "" {
108
- // Stage 4 placeholder pattern: skip empty program ids.
109
- ()
110
- } else {
111
- let hasSchema = ec.accounts->Array.length > 0 || ec.args !== JSON.Null
112
- let discriminator = ec.discriminator->Option.getOr("")
113
- if hasSchema && discriminator !== "" {
114
- // Inline-schema programs declare no custom types, so `definedTypes`
115
- // arrives as JSON.Null; the Rust descriptor's `#[serde(default)]` only
116
- // covers an absent field, not an explicit null, so coalesce here.
117
- let definedTypes = switch ec.definedTypes {
118
- | JSON.Null => JSON.Object(Dict.make())
119
- | other => other
120
- }
121
- let existing = descriptorsByProgram->Dict.get(programIdString)
122
- let descriptor = switch existing {
123
- | Some(d) => d
124
- | None => {
125
- "programId": programIdString,
126
- "definedTypes": definedTypes,
127
- "instructions": [],
128
- }
129
- }
130
- let instruction = {
131
- "name": ec.name,
132
- "discriminator": discriminator,
133
- "accounts": ec.accounts,
134
- "args": ec.args,
135
- }
136
- descriptorsByProgram->Dict.set(
137
- programIdString,
138
- {
139
- "programId": descriptor["programId"],
140
- "definedTypes": descriptor["definedTypes"],
141
- "instructions": descriptor["instructions"]->Array.concat([instruction]),
142
- },
143
- )
144
- }
145
- }
146
- })
147
-
148
- descriptorsByProgram
149
- ->Dict.valuesToArray
150
- ->Array.map(descriptor =>
151
- descriptor
152
- ->(Utils.magic: {
153
- "programId": string,
154
- "definedTypes": JSON.t,
155
- "instructions": array<{
156
- "name": string,
157
- "discriminator": string,
158
- "accounts": array<string>,
159
- "args": JSON.t,
160
- }>,
161
- } => JSON.t)
162
- ->JSON.stringify
163
- )
17
+ let synthLogIndex = (~transactionIndex, ~instructionAddress) => {
18
+ let addrSum = instructionAddress->Array.reduce(0, (acc, n) => acc * 1024 + n + 1)
19
+ transactionIndex * 65536 + addrSum
164
20
  }
165
21
 
166
22
  // Parse the Rust-decoded instruction (args/accounts arrive as JSON strings to
@@ -186,234 +42,70 @@ let parseDecoded = (
186
42
 
187
43
  // `block` is omitted; it's materialised from the block store at batch prep.
188
44
  let toSvmInstruction = (
189
- instr: SvmHyperSyncClient.ResponseTypes.instruction,
45
+ item: SvmHyperSyncClient.EventItems.item,
190
46
  ~programName,
191
47
  ~instructionName,
192
- ~logs,
193
48
  ): Envio.svmInstruction => {
194
49
  programName,
195
50
  instructionName,
196
- programId: instr.programId->SvmTypes.Pubkey.fromStringUnsafe,
197
- data: instr.data,
198
- accounts: instr.accounts->SvmTypes.Pubkey.fromStringsUnsafe,
199
- instructionAddress: instr.instructionAddress,
200
- isInner: instr.isInner,
201
- d1: ?instr.d1,
202
- d2: ?instr.d2,
203
- d4: ?instr.d4,
204
- d8: ?instr.d8,
205
- params: ?(instr.decoded->Option.map(parseDecoded)),
206
- ?logs,
207
- }
208
-
209
- // Probe the discriminator byte-length ordering longest-first. Stops at the
210
- // first router hit. Falls back to the `_none` key (program-wide handler) when
211
- // no discriminator-keyed handler matches.
212
- let probeRouter = (
213
- router: EventRouter.t<Internal.svmOnEventRegistration>,
214
- programId: SvmTypes.Pubkey.t,
215
- instr: SvmHyperSyncClient.ResponseTypes.instruction,
216
- byteLengthsDesc: array<int>,
217
- ~contractAddress,
218
- ~contractNameByAddress,
219
- ) => {
220
- let probe = (dN: option<string>) => {
221
- let tag = EventRouter.getSvmEventId(~programId, ~discriminator=dN)
222
- router->EventRouter.get(~tag, ~contractAddress, ~contractNameByAddress)
223
- }
224
-
225
- let result = byteLengthsDesc->Array.reduce(None, (acc, len) =>
226
- switch acc {
227
- | Some(_) => acc
228
- | None =>
229
- let candidate = switch len {
230
- | 8 => instr.d8
231
- | 4 => instr.d4
232
- | 2 => instr.d2
233
- | 1 => instr.d1
234
- | _ => None
235
- }
236
- switch candidate {
237
- | Some(_) as d => probe(d)
238
- | None => None
239
- }
240
- }
241
- )
242
-
243
- switch result {
244
- | Some(_) as hit => hit
245
- | None => probe(None) // program-wide fallback
246
- }
51
+ programId: item.programId->SvmTypes.Pubkey.fromStringUnsafe,
52
+ data: item.data,
53
+ accounts: item.accounts->SvmTypes.Pubkey.fromStringsUnsafe,
54
+ instructionAddress: item.instructionAddress,
55
+ isInner: item.isInner,
56
+ d1: ?item.d1,
57
+ d2: ?item.d2,
58
+ d4: ?item.d4,
59
+ d8: ?item.d8,
60
+ params: ?(item.decoded->Option.map(parseDecoded)),
61
+ logs: ?(
62
+ item.logs->Option.map(logs =>
63
+ logs->Array.map((log): Envio.svmLog => {kind: log.kind, message: log.message})
64
+ )
65
+ ),
247
66
  }
248
67
 
249
- // Map a selected transaction field to the extra query-side column it needs.
250
- // `transactionIndex` is always fetched as the store key, and `tokenBalances`
251
- // lives in a separate table (requested via `needsTokenBalances`), so neither
252
- // adds a transaction column here.
253
- let toQueryTxField = (field: Internal.svmTransactionField): option<
254
- SvmHyperSyncClient.QueryTypes.transactionField,
255
- > =>
256
- switch field {
257
- | TransactionIndex => None
258
- | Signatures => Some(Signatures)
259
- | FeePayer => Some(FeePayer)
260
- | Success => Some(Success)
261
- | Err => Some(Err)
262
- | Fee => Some(Fee)
263
- | ComputeUnitsConsumed => Some(ComputeUnitsConsumed)
264
- | AccountKeys => Some(AccountKeys)
265
- | RecentBlockhash => Some(RecentBlockhash)
266
- | Version => Some(Version)
267
- | TokenBalances => None
268
- }
269
-
270
- // Map a selected block field to its HyperSync query column. Slot/Blockhash/
271
- // BlockTime are requested unconditionally (needed for reorg detection and the
272
- // item's slot/timestamp), so selecting `slot`/`time`/`hash` adds no extra column.
273
- let toQueryBlockField = (field: Internal.svmBlockField): option<
274
- SvmHyperSyncClient.QueryTypes.blockField,
275
- > =>
276
- switch field {
277
- | Slot | Time | Hash => None
278
- | Height => Some(BlockHeight)
279
- | ParentSlot => Some(ParentSlot)
280
- | ParentHash => Some(ParentBlockhash)
281
- }
282
-
283
68
  let make = (
284
69
  {chain, endpointUrl, apiToken, onEventRegistrations, clientTimeoutMillis}: options,
285
70
  ): t => {
286
71
  let name = "SvmHyperSync"
287
72
 
288
- // Definitions drive query/decode building; registrations drive routing and
289
- // are attached directly to decoded runtime items.
290
- let eventConfigs =
291
- onEventRegistrations->Array.map(reg =>
292
- reg.eventConfig->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
293
- )
294
-
295
- // Built once at startup and handed to the client so `get` decodes matching
296
- // instructions in Rust rather than per-instruction over the napi boundary.
297
- let programSchemas = buildProgramSchemas(eventConfigs)
73
+ // The whole per-(instruction, chain) registration set crosses the boundary
74
+ // once at construction; the client derives instruction selections, field
75
+ // selections, Borsh decoders, and the routing index from it.
298
76
  let client = SvmHyperSyncClient.make(
299
77
  ~url=endpointUrl,
300
78
  ~apiToken?,
301
79
  ~httpReqTimeoutMillis=clientTimeoutMillis,
302
- ~programSchemas=?switch programSchemas {
303
- | [] => None
304
- | arr => Some(arr)
305
- },
80
+ ~eventRegistrations=SvmHyperSyncClient.Registration.fromOnEventRegistrations(
81
+ onEventRegistrations,
82
+ ),
306
83
  )
307
84
 
308
- let (eventRouter, programOrderings) = EventRouter.fromSvmEventConfigsOrThrow(
309
- onEventRegistrations,
310
- ~chain,
311
- )
312
-
313
- // programId.toString -> sorted-desc byte lengths
314
- let orderingByProgram = Dict.make()
315
- programOrderings->Array.forEach(o =>
316
- orderingByProgram->Dict.set(o.programId->SvmTypes.Pubkey.toString, o.byteLengthsDesc)
317
- )
318
-
319
- let needsLogs = eventConfigs->Array.some(cfg => cfg.includeLogs)
320
-
321
- // Union of selected transaction fields across the chain's events. Drives both
322
- // the query column selection (fetch only what's used) and the materialisation
323
- // mask. `slot`/`transactionIndex` are always fetched as the store key.
324
- let selectedTxFields = Utils.Set.make()
325
- eventConfigs->Array.forEach(cfg =>
326
- cfg.selectedTransactionFields
327
- ->(Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.svmTransactionField>)
328
- ->Utils.Set.forEach(field => selectedTxFields->Utils.Set.add(field)->ignore)
329
- )
330
- let needsTokenBalances = selectedTxFields->Utils.Set.has(Internal.TokenBalances)
331
- let txQueryFields = {
332
- // Slot + TransactionIndex are always fetched so the store can be keyed by
333
- // (slot, transactionIndex).
334
- let fields: array<SvmHyperSyncClient.QueryTypes.transactionField> = [Slot, TransactionIndex]
335
- selectedTxFields
336
- ->Utils.Set.toArray
337
- ->Array.forEach(field =>
338
- switch toQueryTxField(field) {
339
- | Some(queryField) => fields->Array.push(queryField)
340
- | None => ()
341
- }
342
- )
343
- fields
344
- }
345
- // The transaction table is fetched only when a selected field is actually read
346
- // off a stored transaction record. `transactionIndex` materialises from the
347
- // store key and `tokenBalances` lives in its own table, so neither requires it.
348
- let needsTransactions =
349
- selectedTxFields
350
- ->Utils.Set.toArray
351
- ->Array.some(field =>
352
- switch field {
353
- | Internal.TransactionIndex | Internal.TokenBalances => false
354
- | _ => true
355
- }
356
- )
357
-
358
- // Union of selected block fields across the chain's events. `slot`/`time`/
359
- // `hash` are always fetched (as Slot/BlockTime/Blockhash); the rest are added
360
- // only when an instruction selected them.
361
- let blockQueryFields = {
362
- let fields: array<SvmHyperSyncClient.QueryTypes.blockField> = [Slot, Blockhash, BlockTime]
363
- let selected = Utils.Set.make()
364
- eventConfigs->Array.forEach(cfg =>
365
- cfg.selectedBlockFields->Utils.Set.forEach(field => selected->Utils.Set.add(field)->ignore)
366
- )
367
- selected->Utils.Set.forEach(field =>
368
- switch field->toQueryBlockField {
369
- | Some(queryField) => fields->Array.push(queryField)->ignore
370
- | None => ()
371
- }
372
- )
373
- fields
374
- }
375
-
376
85
  let getItemsOrThrow = async (
377
86
  ~fromBlock,
378
87
  ~toBlock,
379
- ~addressesByContractName as _,
380
- ~contractNameByAddress,
88
+ ~addressesByContractName,
89
+ ~contractNameByAddress as _,
381
90
  ~knownHeight,
382
91
  ~partitionId as _,
383
- ~selection as _,
92
+ ~selection: FetchState.selection,
384
93
  ~itemsTarget,
385
94
  ~retry,
386
- ~logger,
95
+ ~logger as _,
387
96
  ) => {
388
97
  let totalTimeRef = Performance.now()
389
98
  let pageFetchRef = Performance.now()
390
99
 
391
- let instructionSelections = buildInstructionSelections(eventConfigs)
392
- // Under the server's default merge mode, requesting a table's columns is
393
- // what opts the matched result set into that join — a table with an empty
394
- // field list returns no rows (instructions and blocks are exempt), so each
395
- // opted-into table needs its columns spelled out here.
396
- let fields: SvmHyperSyncClient.QueryTypes.fieldSelection = {
397
- block: blockQueryFields,
398
- transaction: ?(needsTransactions ? Some(txQueryFields) : None),
399
- log: ?(needsLogs ? Some([Slot, TransactionIndex, InstructionAddress, Kind, Message]) : None),
400
- tokenBalance: ?(
401
- needsTokenBalances
402
- ? Some([Slot, TransactionIndex, Account, Mint, Owner, PreAmount, PostAmount])
403
- : None
404
- ),
405
- }
406
- // `toBlock` is inclusive, but `toSlot` is exclusive on the wire — without
407
- // the +1 a bounded range stalls one slot short of its end.
408
- let query: SvmHyperSyncClient.query = {
100
+ let query: SvmHyperSyncClient.EventItems.query = {
409
101
  fromSlot: fromBlock,
410
- toSlot: ?(toBlock->Option.map(toBlock => toBlock + 1)),
411
- instructions: instructionSelections,
412
- fields,
102
+ toSlot: toBlock,
413
103
  maxNumInstructions: itemsTarget,
104
+ registrationIndexes: selection.onEventRegistrations->Array.map(reg => reg.index),
105
+ addressesByContractName,
414
106
  }
415
107
 
416
- let (resp, transactionStore, blockStore) = try await client.get(~query) catch {
108
+ let (resp, transactionStore, blockStore) = try await client.getEventItems(~query) catch {
417
109
  | exn =>
418
110
  throw(
419
111
  Source.GetItemsError(
@@ -440,110 +132,38 @@ let make = (
440
132
  // batch's `latestFetchedBlockTimestamp`. Slots without a block row (rare;
441
133
  // usually skipped slots) fall back to `None`.
442
134
  let blockTimeBySlot = Dict.make()
443
- resp.data.blocks->Array.forEach(b => {
135
+ resp.blocks->Array.forEach(b => {
444
136
  switch b.blockTime {
445
137
  | Some(t) => blockTimeBySlot->Dict.set(b.slot->Int.toString, t)
446
138
  | None => ()
447
139
  }
448
140
  })
449
141
 
450
- // Per (slot, transaction_index, instruction_address) lookup for logs
451
- // scoped to a single instruction. `instructionAddress: None` logs are
452
- // attached to no instruction (rare; usually only system messages).
453
- let logsByKey = Dict.make()
454
- resp.data.logs->Array.forEach(log => {
455
- switch (log.transactionIndex, log.instructionAddress) {
456
- | (Some(txIdx), Some(addr)) =>
457
- let key =
458
- log.slot->Int.toString ++
459
- ":" ++
460
- txIdx->Int.toString ++
461
- ":" ++
462
- serializeInstructionAddress(addr)
463
- switch logsByKey->Dict.get(key) {
464
- | Some(existing) => existing->Array.push(log)
465
- | None => logsByKey->Dict.set(key, [log])
466
- }
467
- | _ => ()
468
- }
469
- })
470
-
471
- let parsedQueueItems = []
472
- resp.data.instructions->Array.forEach(instr => {
473
- let programId = instr.programId->SvmTypes.Pubkey.fromStringUnsafe
474
- let byteLengths =
475
- orderingByProgram
476
- ->Utils.Dict.dangerouslyGetNonOption(instr.programId)
477
- ->Option.getOr([])
478
-
479
- let contractAddress = instr.programId->Address.unsafeFromString
480
- let maybeConfig = probeRouter(
481
- eventRouter,
482
- programId,
483
- instr,
484
- byteLengths,
485
- ~contractAddress,
486
- ~contractNameByAddress,
487
- )
488
-
489
- switch maybeConfig {
490
- | None => ()
491
- // Exclude instructions from failed transactions. HyperSync has no
492
- // server-side predicate to filter instructions by parent-transaction
493
- // success (`InstructionSelection` only exposes `is_inner`, and
494
- // instruction/transaction selections union at block level rather than
495
- // joining), so we filter client-side on the `isCommitted` flag HyperSync
496
- // already delivers on every instruction row (a required column, zero extra
497
- // bandwidth). When HyperSync adds a server-side `is_committed` predicate the
498
- // query can push this down and the client-side check becomes a redundant
499
- // safety net.
500
- | Some(_) if !instr.isCommitted => ()
501
- | Some(onEventRegistration) =>
502
- let eventConfig =
503
- onEventRegistration.eventConfig->(
504
- Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig
505
- )
506
- let logKey =
507
- instr.slot->Int.toString ++
508
- ":" ++
509
- instr.transactionIndex->Int.toString ++
510
- ":" ++
511
- serializeInstructionAddress(instr.instructionAddress)
512
- let maybeLogs =
513
- logsByKey
514
- ->Utils.Dict.dangerouslyGetNonOption(logKey)
515
- ->Option.map(logs =>
516
- logs->Array.map(
517
- (log): Envio.svmLog => {
518
- kind: log.kind->Option.getOr(""),
519
- message: log.message->Option.getOr(""),
520
- },
521
- )
522
- )
523
-
524
- let payload = toSvmInstruction(
525
- instr,
526
- ~programName=eventConfig.contractName,
527
- ~instructionName=eventConfig.name,
528
- ~logs=eventConfig.includeLogs ? maybeLogs : None,
142
+ let parsedQueueItems = resp.items->Array.map(item => {
143
+ // Routing happened in Rust; the item references its registration by
144
+ // chain-scoped index.
145
+ let onEventRegistration = onEventRegistrations->Array.getUnsafe(item.onEventRegistrationIndex)
146
+ let eventConfig =
147
+ onEventRegistration.eventConfig->(
148
+ Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig
529
149
  )
530
-
531
- parsedQueueItems
532
- ->Array.push(
533
- Internal.Event({
534
- onEventRegistration: (onEventRegistration :> Internal.onEventRegistration),
535
- chain,
536
- blockNumber: instr.slot,
537
- logIndex: synthLogIndex(instr),
538
- // The parent transaction is materialised from the store at batch prep.
539
- transactionIndex: instr.transactionIndex,
540
- payload: payload->(Utils.magic: Envio.svmInstruction => Internal.eventPayload),
541
- }),
542
- )
543
- ->ignore
544
- }
545
-
546
- let _ = logger
150
+ let payload = toSvmInstruction(
151
+ item,
152
+ ~programName=eventConfig.contractName,
153
+ ~instructionName=eventConfig.name,
154
+ )
155
+ Internal.Event({
156
+ onEventRegistration,
157
+ chain,
158
+ blockNumber: item.slot,
159
+ logIndex: synthLogIndex(
160
+ ~transactionIndex=item.transactionIndex,
161
+ ~instructionAddress=item.instructionAddress,
162
+ ),
163
+ // The parent transaction is materialised from the store at batch prep.
164
+ transactionIndex: item.transactionIndex,
165
+ payload: payload->(Utils.magic: Envio.svmInstruction => Internal.eventPayload),
166
+ })
547
167
  })
548
168
 
549
169
  let parsingTimeElapsed = parsingRef->Performance.secondsSince
@@ -556,7 +176,7 @@ let make = (
556
176
  // Best-effort (slot, blockhash) pairs from the blocks the server returned
557
177
  // for this range. Gaps (skipped slots, or slots without matched data) are
558
178
  // fine — reorg detection only compares hashes for slots it has observed.
559
- let blockHashes = resp.data.blocks->Array.map((b): ReorgDetection.blockData => {
179
+ let blockHashes = resp.blocks->Array.map((b): ReorgDetection.blockData => {
560
180
  blockNumber: b.slot,
561
181
  blockHash: b.blockhash,
562
182
  })