envio 3.0.1 → 3.0.2-svm-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/evm.schema.json +8 -8
- package/fuel.schema.json +12 -12
- package/index.d.ts +155 -1
- package/package.json +6 -7
- package/src/ChainFetcher.res +25 -1
- package/src/ChainFetcher.res.mjs +19 -1
- package/src/Config.res +156 -94
- package/src/Config.res.mjs +60 -97
- package/src/Core.res +32 -0
- package/src/Env.res.mjs +1 -2
- package/src/Envio.res +94 -0
- package/src/EventConfigBuilder.res +50 -0
- package/src/EventConfigBuilder.res.mjs +31 -0
- package/src/HandlerLoader.res +12 -1
- package/src/HandlerLoader.res.mjs +6 -1
- package/src/Internal.res +38 -0
- package/src/Main.res +53 -3
- package/src/Main.res.mjs +34 -2
- package/src/Persistence.res +2 -17
- package/src/Persistence.res.mjs +2 -14
- package/src/SimulateItems.res +23 -10
- package/src/SimulateItems.res.mjs +21 -6
- package/src/SvmTypes.res +9 -0
- package/src/SvmTypes.res.mjs +14 -0
- package/src/sources/EventRouter.res +65 -0
- package/src/sources/EventRouter.res.mjs +43 -0
- package/src/sources/HyperSyncClient.res +30 -157
- package/src/sources/HyperSyncClient.res.mjs +20 -6
- package/src/sources/HyperSyncSolanaClient.res +227 -0
- package/src/sources/HyperSyncSolanaClient.res.mjs +25 -0
- package/src/sources/HyperSyncSolanaSource.res +515 -0
- package/src/sources/HyperSyncSolanaSource.res.mjs +441 -0
- package/src/sources/HyperSyncSource.res +5 -8
- package/src/sources/HyperSyncSource.res.mjs +1 -8
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/Svm.res +2 -2
- package/src/sources/Svm.res.mjs +3 -2
- package/src/tui/Tui.res +9 -2
- package/src/tui/Tui.res.mjs +19 -4
- package/src/tui/components/TuiData.res +3 -0
- package/svm.schema.json +345 -4
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
open Source
|
|
2
|
+
|
|
3
|
+
exception EventRoutingFailed
|
|
4
|
+
|
|
5
|
+
type options = {
|
|
6
|
+
chain: ChainMap.Chain.t,
|
|
7
|
+
endpointUrl: string,
|
|
8
|
+
apiToken: option<string>,
|
|
9
|
+
eventConfigs: array<Internal.svmInstructionEventConfig>,
|
|
10
|
+
clientMaxRetries: int,
|
|
11
|
+
clientTimeoutMillis: int,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Build one HyperSync InstructionSelection per (programId, discriminator) pair.
|
|
15
|
+
// Empty programId means the config carries no real program (placeholder), in
|
|
16
|
+
// which case we skip — better to over-fetch nothing than ship a degenerate
|
|
17
|
+
// query.
|
|
18
|
+
let buildInstructionSelections = (
|
|
19
|
+
eventConfigs: array<Internal.svmInstructionEventConfig>,
|
|
20
|
+
): array<HyperSyncSolanaClient.QueryTypes.instructionSelection> => {
|
|
21
|
+
eventConfigs->Belt.Array.keepMap(cfg => {
|
|
22
|
+
let programIdString = cfg.programId->SvmTypes.Pubkey.toString
|
|
23
|
+
if programIdString === "" {
|
|
24
|
+
None
|
|
25
|
+
} else {
|
|
26
|
+
// Each instruction owns exactly one dN field — the one matching its
|
|
27
|
+
// declared byte length. The server-side filter is `d{N} IN [..]`.
|
|
28
|
+
let (d1, d2, d4, d8) = switch (cfg.discriminator, cfg.discriminatorByteLen) {
|
|
29
|
+
| (Some(d), 1) => (Some([d]), None, None, None)
|
|
30
|
+
| (Some(d), 2) => (None, Some([d]), None, None)
|
|
31
|
+
| (Some(d), 4) => (None, None, Some([d]), None)
|
|
32
|
+
| (Some(d), 8) => (None, None, None, Some([d]))
|
|
33
|
+
| _ => (None, None, None, None)
|
|
34
|
+
}
|
|
35
|
+
let accountFilters = cfg.accountFilters
|
|
36
|
+
let a0 = accountFilters->Belt.Array.keepMap(f =>
|
|
37
|
+
f.position == 0 ? Some(f.values->SvmTypes.Pubkey.toStrings) : None
|
|
38
|
+
)->Belt.Array.get(0)
|
|
39
|
+
let a1 = accountFilters->Belt.Array.keepMap(f =>
|
|
40
|
+
f.position == 1 ? Some(f.values->SvmTypes.Pubkey.toStrings) : None
|
|
41
|
+
)->Belt.Array.get(0)
|
|
42
|
+
let a2 = accountFilters->Belt.Array.keepMap(f =>
|
|
43
|
+
f.position == 2 ? Some(f.values->SvmTypes.Pubkey.toStrings) : None
|
|
44
|
+
)->Belt.Array.get(0)
|
|
45
|
+
let a3 = accountFilters->Belt.Array.keepMap(f =>
|
|
46
|
+
f.position == 3 ? Some(f.values->SvmTypes.Pubkey.toStrings) : None
|
|
47
|
+
)->Belt.Array.get(0)
|
|
48
|
+
let a4 = accountFilters->Belt.Array.keepMap(f =>
|
|
49
|
+
f.position == 4 ? Some(f.values->SvmTypes.Pubkey.toStrings) : None
|
|
50
|
+
)->Belt.Array.get(0)
|
|
51
|
+
let a5 = accountFilters->Belt.Array.keepMap(f =>
|
|
52
|
+
f.position == 5 ? Some(f.values->SvmTypes.Pubkey.toStrings) : None
|
|
53
|
+
)->Belt.Array.get(0)
|
|
54
|
+
Some(
|
|
55
|
+
(
|
|
56
|
+
{
|
|
57
|
+
programId: [programIdString],
|
|
58
|
+
?d1,
|
|
59
|
+
?d2,
|
|
60
|
+
?d4,
|
|
61
|
+
?d8,
|
|
62
|
+
?a0,
|
|
63
|
+
?a1,
|
|
64
|
+
?a2,
|
|
65
|
+
?a3,
|
|
66
|
+
?a4,
|
|
67
|
+
?a5,
|
|
68
|
+
isInner: ?cfg.isInner,
|
|
69
|
+
includeTransaction: cfg.includeTransaction,
|
|
70
|
+
includeLogs: cfg.includeLogs,
|
|
71
|
+
}: HyperSyncSolanaClient.QueryTypes.instructionSelection
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Synthesize a stable logIndex for an SVM instruction so the FetchState
|
|
79
|
+
// ordering machinery (which compares by `(blockNumber, logIndex)`) sorts
|
|
80
|
+
// instructions deterministically within a slot. The bit packing fits inside
|
|
81
|
+
// JS's 53-bit safe-integer range: transactionIndex ≤ ~10k per slot,
|
|
82
|
+
// instruction position ≤ 1000 per tx, depth ≤ ~10. Outer-only instructions
|
|
83
|
+
// land at `tx * 65536`; inner ones append depth-weighted offsets.
|
|
84
|
+
let synthLogIndex = (instr: HyperSyncSolanaClient.ResponseTypes.instruction) => {
|
|
85
|
+
let tx = instr.transactionIndex
|
|
86
|
+
let addrSum = instr.instructionAddress->Belt.Array.reduce(0, (acc, n) => acc * 1024 + n + 1)
|
|
87
|
+
tx * 65536 + addrSum
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let serializeInstructionAddress = (addr: array<int>) =>
|
|
91
|
+
addr->Array.map(n => n->Int.toString)->Array.joinUnsafe(",")
|
|
92
|
+
|
|
93
|
+
// Build per-program schema descriptors by grouping eventConfigs by programId.
|
|
94
|
+
// One `addon.registerProgramSchema` call per program at startup; the returned
|
|
95
|
+
// handle goes into `schemaHandlesByProgram` and gets reused for every
|
|
96
|
+
// matching instruction we decode.
|
|
97
|
+
let buildSchemaHandles = (
|
|
98
|
+
eventConfigs: array<Internal.svmInstructionEventConfig>,
|
|
99
|
+
): dict<int> => {
|
|
100
|
+
// Group by programId base58 string. Skip events that carry no schema
|
|
101
|
+
// (accounts == [] && args is JSON.Null && definedTypes is JSON.Null —
|
|
102
|
+
// the resolved-empty case from system_config.rs).
|
|
103
|
+
let descriptorsByProgram: dict<{
|
|
104
|
+
"programId": string,
|
|
105
|
+
"definedTypes": JSON.t,
|
|
106
|
+
"instructions": array<{
|
|
107
|
+
"name": string,
|
|
108
|
+
"discriminator": string,
|
|
109
|
+
"accounts": array<string>,
|
|
110
|
+
"args": JSON.t,
|
|
111
|
+
}>,
|
|
112
|
+
}> = Dict.make()
|
|
113
|
+
|
|
114
|
+
eventConfigs->Belt.Array.forEach(ec => {
|
|
115
|
+
let programIdString = ec.programId->SvmTypes.Pubkey.toString
|
|
116
|
+
if programIdString === "" {
|
|
117
|
+
// Stage 4 placeholder pattern: skip empty program ids.
|
|
118
|
+
()
|
|
119
|
+
} else {
|
|
120
|
+
let hasSchema = ec.accounts->Array.length > 0 || ec.args !== JSON.Null
|
|
121
|
+
let discriminator = ec.discriminator->Option.getOr("")
|
|
122
|
+
if hasSchema && discriminator !== "" {
|
|
123
|
+
let existing = descriptorsByProgram->Dict.get(programIdString)
|
|
124
|
+
let descriptor = switch existing {
|
|
125
|
+
| Some(d) => d
|
|
126
|
+
| None => {
|
|
127
|
+
"programId": programIdString,
|
|
128
|
+
"definedTypes": ec.definedTypes,
|
|
129
|
+
"instructions": [],
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let instruction = {
|
|
133
|
+
"name": ec.name,
|
|
134
|
+
"discriminator": discriminator,
|
|
135
|
+
"accounts": ec.accounts,
|
|
136
|
+
"args": ec.args,
|
|
137
|
+
}
|
|
138
|
+
descriptorsByProgram->Dict.set(
|
|
139
|
+
programIdString,
|
|
140
|
+
{
|
|
141
|
+
"programId": descriptor["programId"],
|
|
142
|
+
"definedTypes": descriptor["definedTypes"],
|
|
143
|
+
"instructions": descriptor["instructions"]->Array.concat([instruction]),
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
let handles = Dict.make()
|
|
151
|
+
descriptorsByProgram
|
|
152
|
+
->Dict.toArray
|
|
153
|
+
->Belt.Array.forEach(((programIdString, descriptor)) => {
|
|
154
|
+
let json = descriptor->(Utils.magic: _ => JSON.t)->JSON.stringify
|
|
155
|
+
let handle = Core.getAddon().registerProgramSchema(~descriptorJson=json)
|
|
156
|
+
handles->Dict.set(programIdString, handle)
|
|
157
|
+
})
|
|
158
|
+
handles
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let decodeIfPossible = (
|
|
162
|
+
instr: HyperSyncSolanaClient.ResponseTypes.instruction,
|
|
163
|
+
schemaHandlesByProgram: dict<int>,
|
|
164
|
+
): option<Envio.svmDecodedInstruction> => {
|
|
165
|
+
switch schemaHandlesByProgram->Dict.get(instr.programId) {
|
|
166
|
+
| None => None
|
|
167
|
+
| Some(handle) =>
|
|
168
|
+
let decoded = Core.getAddon().decodeInstruction(
|
|
169
|
+
~schemaHandle=handle,
|
|
170
|
+
~dataHex=instr.data,
|
|
171
|
+
~accounts=instr.accounts,
|
|
172
|
+
)
|
|
173
|
+
switch decoded->Null.toOption {
|
|
174
|
+
| None => None
|
|
175
|
+
| Some(d) =>
|
|
176
|
+
let args = try JSON.parseOrThrow(d.argsJson) catch {
|
|
177
|
+
| _ => JSON.Object(Dict.make())
|
|
178
|
+
}
|
|
179
|
+
let accounts = try {
|
|
180
|
+
let parsed = JSON.parseOrThrow(d.accountsJson)
|
|
181
|
+
parsed->(Utils.magic: JSON.t => dict<string>)
|
|
182
|
+
} catch {
|
|
183
|
+
| _ => Dict.make()
|
|
184
|
+
}
|
|
185
|
+
Some({
|
|
186
|
+
name: d.name,
|
|
187
|
+
args,
|
|
188
|
+
accounts,
|
|
189
|
+
extraAccounts: d.extraAccounts,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let toSvmInstruction = (
|
|
196
|
+
instr: HyperSyncSolanaClient.ResponseTypes.instruction,
|
|
197
|
+
~schemaHandlesByProgram: dict<int>,
|
|
198
|
+
): Envio.svmInstruction => {
|
|
199
|
+
programId: instr.programId->SvmTypes.Pubkey.fromStringUnsafe,
|
|
200
|
+
data: instr.data,
|
|
201
|
+
accounts: instr.accounts->SvmTypes.Pubkey.fromStringsUnsafe,
|
|
202
|
+
instructionAddress: instr.instructionAddress,
|
|
203
|
+
isInner: instr.isInner,
|
|
204
|
+
d1: ?instr.d1,
|
|
205
|
+
d2: ?instr.d2,
|
|
206
|
+
d4: ?instr.d4,
|
|
207
|
+
d8: ?instr.d8,
|
|
208
|
+
decoded: ?decodeIfPossible(instr, schemaHandlesByProgram),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let toSvmTransaction = (
|
|
212
|
+
tx: HyperSyncSolanaClient.ResponseTypes.transaction,
|
|
213
|
+
): Envio.svmTransaction => {
|
|
214
|
+
signatures: tx.signatures,
|
|
215
|
+
accountKeys: tx.accountKeys->SvmTypes.Pubkey.fromStringsUnsafe,
|
|
216
|
+
feePayer: ?tx.feePayer->Option.map(SvmTypes.Pubkey.fromStringUnsafe),
|
|
217
|
+
success: ?tx.success,
|
|
218
|
+
err: ?tx.err,
|
|
219
|
+
// u64 lamports / compute units arrive as `int` over napi. Convert to
|
|
220
|
+
// `bigint` so the public type stays defensible even for pathological values.
|
|
221
|
+
fee: ?tx.fee->Option.map(BigInt.fromInt),
|
|
222
|
+
computeUnitsConsumed: ?tx.computeUnitsConsumed->Option.map(BigInt.fromInt),
|
|
223
|
+
recentBlockhash: ?tx.recentBlockhash,
|
|
224
|
+
version: ?tx.version,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Probe the discriminator byte-length ordering longest-first. Stops at the
|
|
228
|
+
// first router hit. Falls back to the `_none` key (program-wide handler) when
|
|
229
|
+
// no discriminator-keyed handler matches.
|
|
230
|
+
let probeRouter = (
|
|
231
|
+
router: EventRouter.t<Internal.svmInstructionEventConfig>,
|
|
232
|
+
programId: SvmTypes.Pubkey.t,
|
|
233
|
+
instr: HyperSyncSolanaClient.ResponseTypes.instruction,
|
|
234
|
+
byteLengthsDesc: array<int>,
|
|
235
|
+
~contractAddress,
|
|
236
|
+
~indexingAddresses,
|
|
237
|
+
) => {
|
|
238
|
+
let probe = (dN: option<string>) => {
|
|
239
|
+
let tag = EventRouter.getSvmEventId(~programId, ~discriminator=dN)
|
|
240
|
+
router->EventRouter.get(
|
|
241
|
+
~tag,
|
|
242
|
+
~contractAddress,
|
|
243
|
+
~blockNumber=instr.slot,
|
|
244
|
+
~indexingAddresses,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let result = byteLengthsDesc->Belt.Array.reduce(None, (acc, len) =>
|
|
249
|
+
switch acc {
|
|
250
|
+
| Some(_) => acc
|
|
251
|
+
| None =>
|
|
252
|
+
let candidate = switch len {
|
|
253
|
+
| 8 => instr.d8
|
|
254
|
+
| 4 => instr.d4
|
|
255
|
+
| 2 => instr.d2
|
|
256
|
+
| 1 => instr.d1
|
|
257
|
+
| _ => None
|
|
258
|
+
}
|
|
259
|
+
switch candidate {
|
|
260
|
+
| Some(_) as d => probe(d)
|
|
261
|
+
| None => None
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
switch result {
|
|
267
|
+
| Some(_) as hit => hit
|
|
268
|
+
| None => probe(None) // program-wide fallback
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let make = ({chain, endpointUrl, apiToken, eventConfigs, clientMaxRetries, clientTimeoutMillis}: options): t => {
|
|
273
|
+
let name = "HyperSyncSolana"
|
|
274
|
+
let chainId = chain->ChainMap.Chain.toChainId
|
|
275
|
+
|
|
276
|
+
let client = HyperSyncSolanaClient.make(
|
|
277
|
+
~url=endpointUrl,
|
|
278
|
+
~apiToken=?apiToken,
|
|
279
|
+
~httpReqTimeoutMillis=clientTimeoutMillis,
|
|
280
|
+
~maxNumRetries=clientMaxRetries,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
let (eventRouter, programOrderings) =
|
|
284
|
+
EventRouter.fromSvmEventConfigsOrThrow(eventConfigs, ~chain)
|
|
285
|
+
|
|
286
|
+
// programId.toString -> sorted-desc byte lengths
|
|
287
|
+
let orderingByProgram = Dict.make()
|
|
288
|
+
programOrderings->Belt.Array.forEach(o =>
|
|
289
|
+
orderingByProgram->Dict.set(
|
|
290
|
+
o.programId->SvmTypes.Pubkey.toString,
|
|
291
|
+
o.byteLengthsDesc,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
// programId.toString -> Rust-side schema registry handle. Built once at
|
|
296
|
+
// startup; reused on every decoded instruction.
|
|
297
|
+
let schemaHandlesByProgram = buildSchemaHandles(eventConfigs)
|
|
298
|
+
|
|
299
|
+
let getItemsOrThrow = async (
|
|
300
|
+
~fromBlock,
|
|
301
|
+
~toBlock,
|
|
302
|
+
~addressesByContractName as _,
|
|
303
|
+
~indexingAddresses,
|
|
304
|
+
~knownHeight,
|
|
305
|
+
~partitionId as _,
|
|
306
|
+
~selection as _,
|
|
307
|
+
~retry,
|
|
308
|
+
~logger,
|
|
309
|
+
) => {
|
|
310
|
+
let totalTimeRef = Hrtime.makeTimer()
|
|
311
|
+
let pageFetchRef = Hrtime.makeTimer()
|
|
312
|
+
|
|
313
|
+
let instructionSelections = buildInstructionSelections(eventConfigs)
|
|
314
|
+
let query: HyperSyncSolanaClient.query = {
|
|
315
|
+
fromSlot: fromBlock,
|
|
316
|
+
toSlot: ?toBlock,
|
|
317
|
+
instructions: instructionSelections,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Prometheus.SourceRequestCount.increment(
|
|
321
|
+
~sourceName=name,
|
|
322
|
+
~chainId,
|
|
323
|
+
~method="getInstructions",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
let resp = try await client.get(~query) catch {
|
|
327
|
+
| exn =>
|
|
328
|
+
throw(
|
|
329
|
+
Source.GetItemsError(
|
|
330
|
+
Source.FailedGettingItems({
|
|
331
|
+
exn,
|
|
332
|
+
attemptedToBlock: toBlock->Option.getOr(knownHeight),
|
|
333
|
+
retry: WithBackoff({
|
|
334
|
+
message: `Unexpected issue while fetching instructions from HyperSync Solana. Attempt a retry.`,
|
|
335
|
+
backoffMillis: switch retry {
|
|
336
|
+
| 0 => 500
|
|
337
|
+
| _ => 1000 * retry
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
}),
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
let pageFetchTime = pageFetchRef->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
345
|
+
|
|
346
|
+
let parsingRef = Hrtime.makeTimer()
|
|
347
|
+
|
|
348
|
+
// Per (slot, transaction_index) lookup for parent transactions.
|
|
349
|
+
let txByKey = Dict.make()
|
|
350
|
+
resp.data.transactions->Belt.Array.forEach(tx => {
|
|
351
|
+
let key =
|
|
352
|
+
tx.slot->Int.toString ++ ":" ++ tx.transactionIndex->Int.toString
|
|
353
|
+
txByKey->Dict.set(key, tx)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Per (slot, transaction_index, instruction_address) lookup for logs
|
|
357
|
+
// scoped to a single instruction. `instructionAddress: None` logs are
|
|
358
|
+
// attached to no instruction (rare; usually only system messages).
|
|
359
|
+
let logsByKey = Dict.make()
|
|
360
|
+
resp.data.logs->Belt.Array.forEach(log => {
|
|
361
|
+
switch (log.transactionIndex, log.instructionAddress) {
|
|
362
|
+
| (Some(txIdx), Some(addr)) =>
|
|
363
|
+
let key =
|
|
364
|
+
log.slot->Int.toString ++
|
|
365
|
+
":" ++
|
|
366
|
+
txIdx->Int.toString ++
|
|
367
|
+
":" ++
|
|
368
|
+
serializeInstructionAddress(addr)
|
|
369
|
+
switch logsByKey->Dict.get(key) {
|
|
370
|
+
| Some(existing) => existing->Array.push(log)
|
|
371
|
+
| None => logsByKey->Dict.set(key, [log])
|
|
372
|
+
}
|
|
373
|
+
| _ => ()
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
let parsedQueueItems = []
|
|
378
|
+
resp.data.instructions->Belt.Array.forEach(instr => {
|
|
379
|
+
let programId = instr.programId->SvmTypes.Pubkey.fromStringUnsafe
|
|
380
|
+
let byteLengths =
|
|
381
|
+
orderingByProgram
|
|
382
|
+
->Utils.Dict.dangerouslyGetNonOption(instr.programId)
|
|
383
|
+
->Option.getOr([])
|
|
384
|
+
|
|
385
|
+
let contractAddress = instr.programId->Address.unsafeFromString
|
|
386
|
+
let maybeConfig = probeRouter(
|
|
387
|
+
eventRouter,
|
|
388
|
+
programId,
|
|
389
|
+
instr,
|
|
390
|
+
byteLengths,
|
|
391
|
+
~contractAddress,
|
|
392
|
+
~indexingAddresses,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
switch maybeConfig {
|
|
396
|
+
| None => ()
|
|
397
|
+
| Some(eventConfig) =>
|
|
398
|
+
let txKey =
|
|
399
|
+
instr.slot->Int.toString ++ ":" ++ instr.transactionIndex->Int.toString
|
|
400
|
+
let maybeTx =
|
|
401
|
+
txByKey->Utils.Dict.dangerouslyGetNonOption(txKey)->Option.map(toSvmTransaction)
|
|
402
|
+
let logKey =
|
|
403
|
+
instr.slot->Int.toString ++
|
|
404
|
+
":" ++
|
|
405
|
+
instr.transactionIndex->Int.toString ++
|
|
406
|
+
":" ++
|
|
407
|
+
serializeInstructionAddress(instr.instructionAddress)
|
|
408
|
+
let maybeLogs =
|
|
409
|
+
logsByKey
|
|
410
|
+
->Utils.Dict.dangerouslyGetNonOption(logKey)
|
|
411
|
+
->Option.map(
|
|
412
|
+
logs =>
|
|
413
|
+
logs->Array.map((log): Envio.svmLog => {
|
|
414
|
+
kind: log.kind->Option.getOr(""),
|
|
415
|
+
message: log.message->Option.getOr(""),
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
let payload: Envio.svmInstructionEvent = {
|
|
420
|
+
contractName: eventConfig.contractName,
|
|
421
|
+
eventName: eventConfig.name,
|
|
422
|
+
instruction: toSvmInstruction(instr, ~schemaHandlesByProgram),
|
|
423
|
+
transaction: eventConfig.includeTransaction ? maybeTx : None,
|
|
424
|
+
logs: eventConfig.includeLogs ? maybeLogs : None,
|
|
425
|
+
slot: instr.slot,
|
|
426
|
+
blockTime: None,
|
|
427
|
+
// Mirror EVM/Fuel: the shared ecosystem getter reads `block.height`
|
|
428
|
+
// / `block.time` / `block.hash`. C2 doesn't fetch block data, so
|
|
429
|
+
// `time` is 0 and `hash` is "" — populated by the future
|
|
430
|
+
// reorg-guard `queryBlockHash(slot)` route.
|
|
431
|
+
block: {
|
|
432
|
+
height: instr.slot,
|
|
433
|
+
time: 0,
|
|
434
|
+
hash: "",
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
parsedQueueItems
|
|
439
|
+
->Array.push(
|
|
440
|
+
Internal.Event({
|
|
441
|
+
eventConfig: (eventConfig :> Internal.eventConfig),
|
|
442
|
+
timestamp: 0,
|
|
443
|
+
chain,
|
|
444
|
+
blockNumber: instr.slot,
|
|
445
|
+
logIndex: synthLogIndex(instr),
|
|
446
|
+
event: payload->(
|
|
447
|
+
Utils.magic: Envio.svmInstructionEvent => Internal.event
|
|
448
|
+
),
|
|
449
|
+
}),
|
|
450
|
+
)
|
|
451
|
+
->ignore
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let _ = logger
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
let parsingTimeElapsed = parsingRef->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
458
|
+
let heighestSlot = resp.nextSlot - 1
|
|
459
|
+
|
|
460
|
+
// C2 ships a no-op reorg guard for SVM: finalized commitment + extremely
|
|
461
|
+
// rare reorgs at finality. C3 wires the extra `queryBlockHash(slot)`
|
|
462
|
+
// route per the Q3 answer.
|
|
463
|
+
let reorgGuard: ReorgDetection.reorgGuard = {
|
|
464
|
+
rangeLastBlock: (
|
|
465
|
+
{
|
|
466
|
+
blockNumber: heighestSlot,
|
|
467
|
+
blockTimestamp: 0,
|
|
468
|
+
blockHash: "",
|
|
469
|
+
}: ReorgDetection.blockDataWithTimestamp
|
|
470
|
+
)->ReorgDetection.generalizeBlockDataWithTimestamp,
|
|
471
|
+
prevRangeLastBlock: None,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let totalTimeElapsed = totalTimeRef->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
475
|
+
|
|
476
|
+
{
|
|
477
|
+
latestFetchedBlockTimestamp: 0,
|
|
478
|
+
parsedQueueItems,
|
|
479
|
+
latestFetchedBlockNumber: heighestSlot,
|
|
480
|
+
stats: {totalTimeElapsed, parsingTimeElapsed, pageFetchTime},
|
|
481
|
+
knownHeight,
|
|
482
|
+
reorgGuard,
|
|
483
|
+
fromBlockQueried: fromBlock,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
{
|
|
488
|
+
name,
|
|
489
|
+
sourceFor: Sync,
|
|
490
|
+
chain,
|
|
491
|
+
pollingInterval: 1000,
|
|
492
|
+
poweredByHyperSync: true,
|
|
493
|
+
getBlockHashes: (~blockNumbers as _, ~logger as _) =>
|
|
494
|
+
// No-op reorg guard means callers never need block hashes for SVM. If a
|
|
495
|
+
// caller does ask, surface the limitation rather than fabricate empty
|
|
496
|
+
// data — C3 will replace this with a real lookup.
|
|
497
|
+
JsError.throwWithMessage(
|
|
498
|
+
"HyperSyncSolanaSource does not support getBlockHashes yet (reorg detection at finalized commitment is no-op in C2)",
|
|
499
|
+
),
|
|
500
|
+
getHeightOrThrow: async () => {
|
|
501
|
+
let timer = Hrtime.makeTimer()
|
|
502
|
+
let h = await client.getHeight()
|
|
503
|
+
let seconds = timer->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
504
|
+
Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getHeight")
|
|
505
|
+
Prometheus.SourceRequestCount.addSeconds(
|
|
506
|
+
~sourceName=name,
|
|
507
|
+
~chainId,
|
|
508
|
+
~method="getHeight",
|
|
509
|
+
~seconds,
|
|
510
|
+
)
|
|
511
|
+
h
|
|
512
|
+
},
|
|
513
|
+
getItemsOrThrow,
|
|
514
|
+
}
|
|
515
|
+
}
|