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.
- package/package.json +6 -6
- package/src/Api.res +1 -8
- package/src/Api.res.mjs +1 -5
- package/src/Bin.res +2 -2
- package/src/ChainState.res +6 -1
- package/src/ChainState.res.mjs +4 -3
- package/src/Config.res +38 -5
- package/src/Config.res.mjs +35 -6
- package/src/Core.res +65 -20
- package/src/Core.res.mjs +36 -6
- package/src/EnvioGlobal.res +0 -2
- package/src/EnvioGlobal.res.mjs +0 -1
- package/src/ExitOnCaughtUp.res +6 -2
- package/src/ExitOnCaughtUp.res.mjs +4 -0
- package/src/FetchState.res +3 -3
- package/src/HandlerRegister.res +357 -434
- package/src/HandlerRegister.res.mjs +202 -242
- package/src/HandlerRegister.resi +7 -15
- package/src/IndexerState.res +10 -0
- package/src/IndexerState.res.mjs +9 -3
- package/src/IndexerState.resi +3 -0
- package/src/Internal.res +10 -1
- package/src/Main.res.mjs +4 -4
- package/src/PgStorage.res +16 -1
- package/src/PgStorage.res.mjs +9 -1
- package/src/SimulateItems.res +61 -40
- package/src/SimulateItems.res.mjs +37 -21
- package/src/TestIndexer.res +453 -454
- package/src/TestIndexer.res.mjs +321 -344
- package/src/bindings/ClickHouse.res +68 -2
- package/src/bindings/ClickHouse.res.mjs +47 -2
- package/src/sources/EvmChain.res +1 -1
- package/src/sources/EvmChain.res.mjs +2 -2
- package/src/sources/FuelHyperSync.res +74 -0
- package/src/sources/FuelHyperSync.res.mjs +80 -0
- package/src/sources/FuelHyperSync.resi +22 -0
- package/src/sources/FuelHyperSyncClient.res +120 -0
- package/src/sources/FuelHyperSyncClient.res.mjs +71 -0
- package/src/sources/FuelHyperSyncSource.res +257 -0
- package/src/sources/FuelHyperSyncSource.res.mjs +252 -0
- package/src/sources/HyperSyncClient.res +2 -2
- package/src/sources/HyperSyncClient.res.mjs +1 -1
- package/src/sources/SvmHyperSyncClient.res +139 -102
- package/src/sources/SvmHyperSyncClient.res.mjs +45 -5
- package/src/sources/SvmHyperSyncSource.res +60 -440
- package/src/sources/SvmHyperSyncSource.res.mjs +42 -363
- package/src/TestIndexerProxyStorage.res +0 -196
- package/src/TestIndexerProxyStorage.res.mjs +0 -121
- package/src/TestIndexerWorker.res +0 -4
- package/src/TestIndexerWorker.res.mjs +0 -7
- package/src/sources/EventRouter.res +0 -165
- package/src/sources/EventRouter.res.mjs +0 -153
- package/src/sources/HyperFuel.res +0 -179
- package/src/sources/HyperFuel.res.mjs +0 -146
- package/src/sources/HyperFuel.resi +0 -36
- package/src/sources/HyperFuelClient.res +0 -127
- package/src/sources/HyperFuelClient.res.mjs +0 -20
- package/src/sources/HyperFuelSource.res +0 -502
- package/src/sources/HyperFuelSource.res.mjs +0 -481
- /package/src/sources/{HyperSyncSource.res → EvmHyperSyncSource.res} +0 -0
- /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 = (
|
|
76
|
-
let
|
|
77
|
-
|
|
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
|
-
|
|
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:
|
|
197
|
-
data:
|
|
198
|
-
accounts:
|
|
199
|
-
instructionAddress:
|
|
200
|
-
isInner:
|
|
201
|
-
d1: ?
|
|
202
|
-
d2: ?
|
|
203
|
-
d4: ?
|
|
204
|
-
d8: ?
|
|
205
|
-
params: ?(
|
|
206
|
-
?
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
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
|
-
~
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
380
|
-
~contractNameByAddress,
|
|
88
|
+
~addressesByContractName,
|
|
89
|
+
~contractNameByAddress as _,
|
|
381
90
|
~knownHeight,
|
|
382
91
|
~partitionId as _,
|
|
383
|
-
~selection
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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.
|
|
179
|
+
let blockHashes = resp.blocks->Array.map((b): ReorgDetection.blockData => {
|
|
560
180
|
blockNumber: b.slot,
|
|
561
181
|
blockHash: b.blockhash,
|
|
562
182
|
})
|