envio 3.1.2 → 3.2.1
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 +83 -11
- package/fuel.schema.json +83 -11
- package/index.d.ts +184 -3
- package/package.json +6 -6
- package/src/Batch.res +2 -2
- package/src/ChainFetcher.res +27 -3
- package/src/ChainFetcher.res.mjs +17 -3
- package/src/ChainManager.res +163 -0
- package/src/ChainManager.res.mjs +136 -0
- package/src/Config.res +213 -30
- package/src/Config.res.mjs +102 -41
- package/src/Core.res +16 -10
- package/src/Ecosystem.res +0 -3
- package/src/Env.res +2 -2
- package/src/Env.res.mjs +2 -2
- package/src/Envio.res +101 -2
- package/src/Envio.res.mjs +2 -3
- package/src/EventConfigBuilder.res +87 -0
- package/src/EventConfigBuilder.res.mjs +53 -0
- package/src/EventUtils.res +2 -2
- package/src/FetchState.res +63 -67
- package/src/FetchState.res.mjs +44 -42
- package/src/GlobalState.res +219 -363
- package/src/GlobalState.res.mjs +314 -491
- package/src/GlobalStateManager.res +49 -59
- package/src/GlobalStateManager.res.mjs +5 -4
- package/src/GlobalStateManager.resi +1 -1
- package/src/HandlerLoader.res +18 -2
- package/src/HandlerLoader.res.mjs +16 -34
- package/src/HandlerRegister.res +9 -9
- package/src/HandlerRegister.res.mjs +9 -9
- package/src/Hasura.res +102 -32
- package/src/Hasura.res.mjs +88 -34
- package/src/InMemoryStore.res +10 -1
- package/src/InMemoryStore.res.mjs +4 -1
- package/src/InMemoryTable.res +83 -136
- package/src/InMemoryTable.res.mjs +57 -86
- package/src/Internal.res +70 -5
- package/src/Internal.res.mjs +2 -8
- package/src/LazyLoader.res +2 -2
- package/src/LazyLoader.res.mjs +3 -3
- package/src/LoadLayer.res +47 -60
- package/src/LoadLayer.res.mjs +28 -50
- package/src/LoadLayer.resi +2 -5
- package/src/LogSelection.res +90 -21
- package/src/LogSelection.res.mjs +72 -21
- package/src/Logging.res +1 -1
- package/src/Main.res +61 -2
- package/src/Main.res.mjs +37 -1
- package/src/Persistence.res +3 -16
- package/src/PgStorage.res +125 -114
- package/src/PgStorage.res.mjs +112 -95
- package/src/Ports.res +5 -0
- package/src/Ports.res.mjs +9 -0
- package/src/Prometheus.res +3 -3
- package/src/Prometheus.res.mjs +4 -4
- package/src/ReorgDetection.res +4 -4
- package/src/ReorgDetection.res.mjs +4 -5
- package/src/SafeCheckpointTracking.res +16 -16
- package/src/SafeCheckpointTracking.res.mjs +2 -2
- package/src/SimulateItems.res +10 -14
- package/src/SimulateItems.res.mjs +5 -2
- package/src/Sink.res +1 -1
- package/src/Sink.res.mjs +1 -2
- package/src/SvmTypes.res +9 -0
- package/src/SvmTypes.res.mjs +14 -0
- package/src/TestIndexer.res +35 -68
- package/src/TestIndexer.res.mjs +17 -48
- package/src/TestIndexerProxyStorage.res +23 -23
- package/src/TestIndexerProxyStorage.res.mjs +12 -15
- package/src/Throttler.res +2 -2
- package/src/Time.res +2 -2
- package/src/Time.res.mjs +2 -2
- package/src/UserContext.res +19 -118
- package/src/UserContext.res.mjs +10 -66
- package/src/Utils.res +15 -15
- package/src/Utils.res.mjs +7 -8
- package/src/adapters/MarkBatchProcessedAdapter.res +5 -0
- package/src/adapters/MarkBatchProcessedAdapter.res.mjs +14 -0
- package/src/bindings/BigDecimal.res +1 -1
- package/src/bindings/BigDecimal.res.mjs +2 -2
- package/src/bindings/ClickHouse.res +8 -6
- package/src/bindings/ClickHouse.res.mjs +5 -5
- package/src/bindings/Hrtime.res +1 -1
- package/src/bindings/Pino.res +2 -2
- package/src/bindings/Pino.res.mjs +3 -4
- package/src/db/EntityFilter.res +410 -0
- package/src/db/EntityFilter.res.mjs +424 -0
- package/src/db/EntityHistory.res +1 -1
- package/src/db/EntityHistory.res.mjs +1 -1
- package/src/db/InternalTable.res +10 -10
- package/src/db/InternalTable.res.mjs +41 -45
- package/src/db/Schema.res +2 -2
- package/src/db/Schema.res.mjs +3 -3
- package/src/db/Table.res +106 -22
- package/src/db/Table.res.mjs +84 -35
- package/src/sources/EventRouter.res +67 -2
- package/src/sources/EventRouter.res.mjs +45 -3
- package/src/sources/Evm.res +0 -7
- package/src/sources/Evm.res.mjs +0 -15
- package/src/sources/EvmChain.res +1 -1
- package/src/sources/EvmChain.res.mjs +1 -2
- package/src/sources/EvmRpcClient.res +42 -0
- package/src/sources/EvmRpcClient.res.mjs +64 -0
- package/src/sources/Fuel.res +0 -7
- package/src/sources/Fuel.res.mjs +0 -15
- package/src/sources/HyperFuelSource.res +5 -4
- package/src/sources/HyperFuelSource.res.mjs +2 -2
- package/src/sources/HyperSyncClient.res +9 -5
- package/src/sources/HyperSyncClient.res.mjs +2 -2
- package/src/sources/HyperSyncHeightStream.res +2 -2
- package/src/sources/HyperSyncHeightStream.res.mjs +2 -2
- package/src/sources/HyperSyncSource.res +12 -11
- package/src/sources/HyperSyncSource.res.mjs +6 -6
- package/src/sources/Rpc.res +1 -5
- package/src/sources/Rpc.res.mjs +1 -9
- package/src/sources/RpcSource.res +57 -21
- package/src/sources/RpcSource.res.mjs +47 -20
- package/src/sources/RpcWebSocketHeightStream.res +1 -1
- package/src/sources/SourceManager.res +3 -2
- package/src/sources/SourceManager.res.mjs +1 -1
- package/src/sources/Svm.res +3 -10
- package/src/sources/Svm.res.mjs +4 -18
- package/src/sources/SvmHyperSyncClient.res +265 -0
- package/src/sources/SvmHyperSyncClient.res.mjs +28 -0
- package/src/sources/SvmHyperSyncSource.res +638 -0
- package/src/sources/SvmHyperSyncSource.res.mjs +557 -0
- package/src/tui/Tui.res +9 -2
- package/src/tui/Tui.res.mjs +18 -3
- package/src/tui/components/BufferedProgressBar.res +2 -2
- package/src/tui/components/TuiData.res +3 -0
- package/svm.schema.json +523 -14
- package/src/TableIndices.res +0 -115
- package/src/TableIndices.res.mjs +0 -144
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
open Source
|
|
2
|
+
|
|
3
|
+
type options = {
|
|
4
|
+
chain: ChainMap.Chain.t,
|
|
5
|
+
endpointUrl: string,
|
|
6
|
+
apiToken: option<string>,
|
|
7
|
+
eventConfigs: array<Internal.svmInstructionEventConfig>,
|
|
8
|
+
clientTimeoutMillis: int,
|
|
9
|
+
}
|
|
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
|
+
// Synthesize a stable logIndex for an SVM instruction so the FetchState
|
|
70
|
+
// ordering machinery (which compares by `(blockNumber, logIndex)`) sorts
|
|
71
|
+
// instructions deterministically within a slot. The bit packing fits inside
|
|
72
|
+
// JS's 53-bit safe-integer range: transactionIndex ≤ ~10k per slot,
|
|
73
|
+
// instruction position ≤ 1000 per tx, depth ≤ ~10. Outer-only instructions
|
|
74
|
+
// 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
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Parse the Rust-decoded instruction (args/accounts arrive as JSON strings to
|
|
167
|
+
// side-step napi-rs's lack of native JSON passthrough) into the public shape.
|
|
168
|
+
let parseDecoded = (
|
|
169
|
+
d: SvmHyperSyncClient.ResponseTypes.decodedInstruction,
|
|
170
|
+
): Envio.svmInstructionParams => {
|
|
171
|
+
let args = try JSON.parseOrThrow(d.argsJson) catch {
|
|
172
|
+
| _ => JSON.Object(Dict.make())
|
|
173
|
+
}
|
|
174
|
+
let accounts = try {
|
|
175
|
+
JSON.parseOrThrow(d.accountsJson)->(Utils.magic: JSON.t => dict<string>)
|
|
176
|
+
} catch {
|
|
177
|
+
| _ => Dict.make()
|
|
178
|
+
}
|
|
179
|
+
{
|
|
180
|
+
name: d.name,
|
|
181
|
+
args,
|
|
182
|
+
accounts,
|
|
183
|
+
extraAccounts: d.extraAccounts,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let toSvmInstruction = (
|
|
188
|
+
instr: SvmHyperSyncClient.ResponseTypes.instruction,
|
|
189
|
+
~programName,
|
|
190
|
+
~instructionName,
|
|
191
|
+
~transaction,
|
|
192
|
+
~logs,
|
|
193
|
+
~block,
|
|
194
|
+
): Envio.svmInstruction => {
|
|
195
|
+
programName,
|
|
196
|
+
instructionName,
|
|
197
|
+
programId: instr.programId->SvmTypes.Pubkey.fromStringUnsafe,
|
|
198
|
+
data: instr.data,
|
|
199
|
+
accounts: instr.accounts->SvmTypes.Pubkey.fromStringsUnsafe,
|
|
200
|
+
instructionAddress: instr.instructionAddress,
|
|
201
|
+
isInner: instr.isInner,
|
|
202
|
+
d1: ?instr.d1,
|
|
203
|
+
d2: ?instr.d2,
|
|
204
|
+
d4: ?instr.d4,
|
|
205
|
+
d8: ?instr.d8,
|
|
206
|
+
params: ?(instr.decoded->Option.map(parseDecoded)),
|
|
207
|
+
?transaction,
|
|
208
|
+
?logs,
|
|
209
|
+
block,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let toSvmTransaction = (tx: SvmHyperSyncClient.ResponseTypes.transaction): Envio.svmTransaction => {
|
|
213
|
+
signatures: tx.signatures,
|
|
214
|
+
accountKeys: tx.accountKeys->SvmTypes.Pubkey.fromStringsUnsafe,
|
|
215
|
+
feePayer: ?(tx.feePayer->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
|
|
216
|
+
success: ?tx.success,
|
|
217
|
+
err: ?tx.err,
|
|
218
|
+
// u64 lamports / compute units arrive as `int` over napi. Convert to
|
|
219
|
+
// `bigint` so the public type stays defensible even for pathological values.
|
|
220
|
+
fee: ?(tx.fee->Option.map(BigInt.fromInt)),
|
|
221
|
+
computeUnitsConsumed: ?(tx.computeUnitsConsumed->Option.map(BigInt.fromInt)),
|
|
222
|
+
recentBlockhash: ?tx.recentBlockhash,
|
|
223
|
+
version: ?tx.version,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let toSvmTokenBalance = (
|
|
227
|
+
tb: SvmHyperSyncClient.ResponseTypes.tokenBalance,
|
|
228
|
+
): Envio.svmTokenBalance => {
|
|
229
|
+
account: ?(tb.account->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
|
|
230
|
+
mint: ?(tb.mint->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
|
|
231
|
+
owner: ?(tb.owner->Option.map(SvmTypes.Pubkey.fromStringUnsafe)),
|
|
232
|
+
preAmount: ?tb.preAmount,
|
|
233
|
+
postAmount: ?tb.postAmount,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Probe the discriminator byte-length ordering longest-first. Stops at the
|
|
237
|
+
// first router hit. Falls back to the `_none` key (program-wide handler) when
|
|
238
|
+
// no discriminator-keyed handler matches.
|
|
239
|
+
let probeRouter = (
|
|
240
|
+
router: EventRouter.t<Internal.svmInstructionEventConfig>,
|
|
241
|
+
programId: SvmTypes.Pubkey.t,
|
|
242
|
+
instr: SvmHyperSyncClient.ResponseTypes.instruction,
|
|
243
|
+
byteLengthsDesc: array<int>,
|
|
244
|
+
~contractAddress,
|
|
245
|
+
~indexingAddresses,
|
|
246
|
+
) => {
|
|
247
|
+
let probe = (dN: option<string>) => {
|
|
248
|
+
let tag = EventRouter.getSvmEventId(~programId, ~discriminator=dN)
|
|
249
|
+
router->EventRouter.get(~tag, ~contractAddress, ~blockNumber=instr.slot, ~indexingAddresses)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let result = byteLengthsDesc->Array.reduce(None, (acc, len) =>
|
|
253
|
+
switch acc {
|
|
254
|
+
| Some(_) => acc
|
|
255
|
+
| None =>
|
|
256
|
+
let candidate = switch len {
|
|
257
|
+
| 8 => instr.d8
|
|
258
|
+
| 4 => instr.d4
|
|
259
|
+
| 2 => instr.d2
|
|
260
|
+
| 1 => instr.d1
|
|
261
|
+
| _ => None
|
|
262
|
+
}
|
|
263
|
+
switch candidate {
|
|
264
|
+
| Some(_) as d => probe(d)
|
|
265
|
+
| None => None
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
switch result {
|
|
271
|
+
| Some(_) as hit => hit
|
|
272
|
+
| None => probe(None) // program-wide fallback
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let make = ({chain, endpointUrl, apiToken, eventConfigs, clientTimeoutMillis}: options): t => {
|
|
277
|
+
let name = "SvmHyperSync"
|
|
278
|
+
let chainId = chain->ChainMap.Chain.toChainId
|
|
279
|
+
|
|
280
|
+
// Built once at startup and handed to the client so `get` decodes matching
|
|
281
|
+
// instructions in Rust rather than per-instruction over the napi boundary.
|
|
282
|
+
let programSchemas = buildProgramSchemas(eventConfigs)
|
|
283
|
+
let client = SvmHyperSyncClient.make(
|
|
284
|
+
~url=endpointUrl,
|
|
285
|
+
~apiToken?,
|
|
286
|
+
~httpReqTimeoutMillis=clientTimeoutMillis,
|
|
287
|
+
~programSchemas=?switch programSchemas {
|
|
288
|
+
| [] => None
|
|
289
|
+
| arr => Some(arr)
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
let (eventRouter, programOrderings) = EventRouter.fromSvmEventConfigsOrThrow(eventConfigs, ~chain)
|
|
294
|
+
|
|
295
|
+
// programId.toString -> sorted-desc byte lengths
|
|
296
|
+
let orderingByProgram = Dict.make()
|
|
297
|
+
programOrderings->Array.forEach(o =>
|
|
298
|
+
orderingByProgram->Dict.set(o.programId->SvmTypes.Pubkey.toString, o.byteLengthsDesc)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
let needsTransactions = eventConfigs->Array.some(cfg => cfg.includeTransaction)
|
|
302
|
+
let needsLogs = eventConfigs->Array.some(cfg => cfg.includeLogs)
|
|
303
|
+
let needsTokenBalances = eventConfigs->Array.some(cfg => cfg.includeTokenBalances)
|
|
304
|
+
|
|
305
|
+
let getItemsOrThrow = async (
|
|
306
|
+
~fromBlock,
|
|
307
|
+
~toBlock,
|
|
308
|
+
~addressesByContractName as _,
|
|
309
|
+
~indexingAddresses,
|
|
310
|
+
~knownHeight,
|
|
311
|
+
~partitionId as _,
|
|
312
|
+
~selection as _,
|
|
313
|
+
~retry,
|
|
314
|
+
~logger,
|
|
315
|
+
) => {
|
|
316
|
+
let totalTimeRef = Hrtime.makeTimer()
|
|
317
|
+
let pageFetchRef = Hrtime.makeTimer()
|
|
318
|
+
|
|
319
|
+
let instructionSelections = buildInstructionSelections(eventConfigs)
|
|
320
|
+
// Under the server's default merge mode, requesting a table's columns is
|
|
321
|
+
// what opts the matched result set into that join — a table with an empty
|
|
322
|
+
// field list returns no rows (instructions and blocks are exempt), so each
|
|
323
|
+
// opted-into table needs its columns spelled out here.
|
|
324
|
+
let fields: SvmHyperSyncClient.QueryTypes.fieldSelection = {
|
|
325
|
+
block: [Slot, Blockhash, BlockTime],
|
|
326
|
+
transaction: ?(
|
|
327
|
+
needsTransactions
|
|
328
|
+
? Some([
|
|
329
|
+
Slot,
|
|
330
|
+
TransactionIndex,
|
|
331
|
+
Signatures,
|
|
332
|
+
FeePayer,
|
|
333
|
+
Success,
|
|
334
|
+
Err,
|
|
335
|
+
Fee,
|
|
336
|
+
ComputeUnitsConsumed,
|
|
337
|
+
AccountKeys,
|
|
338
|
+
RecentBlockhash,
|
|
339
|
+
Version,
|
|
340
|
+
])
|
|
341
|
+
: None
|
|
342
|
+
),
|
|
343
|
+
log: ?(needsLogs ? Some([Slot, TransactionIndex, InstructionAddress, Kind, Message]) : None),
|
|
344
|
+
tokenBalance: ?(
|
|
345
|
+
needsTokenBalances
|
|
346
|
+
? Some([Slot, TransactionIndex, Account, Mint, Owner, PreAmount, PostAmount])
|
|
347
|
+
: None
|
|
348
|
+
),
|
|
349
|
+
}
|
|
350
|
+
// `toBlock` is inclusive, but `toSlot` is exclusive on the wire — without
|
|
351
|
+
// the +1 a bounded range stalls one slot short of its end.
|
|
352
|
+
let query: SvmHyperSyncClient.query = {
|
|
353
|
+
fromSlot: fromBlock,
|
|
354
|
+
toSlot: ?(toBlock->Option.map(toBlock => toBlock + 1)),
|
|
355
|
+
instructions: instructionSelections,
|
|
356
|
+
fields,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getInstructions")
|
|
360
|
+
|
|
361
|
+
let resp = try await client.get(~query) catch {
|
|
362
|
+
| exn =>
|
|
363
|
+
throw(
|
|
364
|
+
Source.GetItemsError(
|
|
365
|
+
Source.FailedGettingItems({
|
|
366
|
+
exn,
|
|
367
|
+
attemptedToBlock: toBlock->Option.getOr(knownHeight),
|
|
368
|
+
retry: WithBackoff({
|
|
369
|
+
message: `Unexpected issue while fetching instructions from SVM HyperSync. Attempt a retry.`,
|
|
370
|
+
backoffMillis: switch retry {
|
|
371
|
+
| 0 => 500
|
|
372
|
+
| _ => 1000 * retry
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
}),
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
let pageFetchTime = pageFetchRef->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
380
|
+
|
|
381
|
+
let parsingRef = Hrtime.makeTimer()
|
|
382
|
+
|
|
383
|
+
// Per-slot unix timestamp lookup from the response's `blocks` table. Slots
|
|
384
|
+
// without a block row (rare; usually skipped slots) fall back to `None`.
|
|
385
|
+
let blockTimeBySlot = Dict.make()
|
|
386
|
+
resp.data.blocks->Array.forEach(b => {
|
|
387
|
+
switch b.blockTime {
|
|
388
|
+
| Some(t) => blockTimeBySlot->Dict.set(b.slot->Int.toString, t)
|
|
389
|
+
| None => ()
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// Per (slot, transaction_index) lookup for parent transactions.
|
|
394
|
+
let txByKey = Dict.make()
|
|
395
|
+
resp.data.transactions->Array.forEach(tx => {
|
|
396
|
+
let key = tx.slot->Int.toString ++ ":" ++ tx.transactionIndex->Int.toString
|
|
397
|
+
txByKey->Dict.set(key, tx)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// Per (slot, transaction_index, instruction_address) lookup for logs
|
|
401
|
+
// scoped to a single instruction. `instructionAddress: None` logs are
|
|
402
|
+
// attached to no instruction (rare; usually only system messages).
|
|
403
|
+
let logsByKey = Dict.make()
|
|
404
|
+
resp.data.logs->Array.forEach(log => {
|
|
405
|
+
switch (log.transactionIndex, log.instructionAddress) {
|
|
406
|
+
| (Some(txIdx), Some(addr)) =>
|
|
407
|
+
let key =
|
|
408
|
+
log.slot->Int.toString ++
|
|
409
|
+
":" ++
|
|
410
|
+
txIdx->Int.toString ++
|
|
411
|
+
":" ++
|
|
412
|
+
serializeInstructionAddress(addr)
|
|
413
|
+
switch logsByKey->Dict.get(key) {
|
|
414
|
+
| Some(existing) => existing->Array.push(log)
|
|
415
|
+
| None => logsByKey->Dict.set(key, [log])
|
|
416
|
+
}
|
|
417
|
+
| _ => ()
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
let tokenBalancesByTx = Dict.make()
|
|
422
|
+
if needsTokenBalances {
|
|
423
|
+
resp.data.tokenBalances->Array.forEach(tb => {
|
|
424
|
+
switch tb.transactionIndex {
|
|
425
|
+
| Some(txIdx) =>
|
|
426
|
+
let key = tb.slot->Int.toString ++ ":" ++ txIdx->Int.toString
|
|
427
|
+
switch tokenBalancesByTx->Dict.get(key) {
|
|
428
|
+
| Some(existing) => existing->Array.push(tb)
|
|
429
|
+
| None => tokenBalancesByTx->Dict.set(key, [tb])
|
|
430
|
+
}
|
|
431
|
+
| None => ()
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let parsedQueueItems = []
|
|
437
|
+
resp.data.instructions->Array.forEach(instr => {
|
|
438
|
+
let programId = instr.programId->SvmTypes.Pubkey.fromStringUnsafe
|
|
439
|
+
let byteLengths =
|
|
440
|
+
orderingByProgram
|
|
441
|
+
->Utils.Dict.dangerouslyGetNonOption(instr.programId)
|
|
442
|
+
->Option.getOr([])
|
|
443
|
+
|
|
444
|
+
let contractAddress = instr.programId->Address.unsafeFromString
|
|
445
|
+
let maybeConfig = probeRouter(
|
|
446
|
+
eventRouter,
|
|
447
|
+
programId,
|
|
448
|
+
instr,
|
|
449
|
+
byteLengths,
|
|
450
|
+
~contractAddress,
|
|
451
|
+
~indexingAddresses,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
switch maybeConfig {
|
|
455
|
+
| None => ()
|
|
456
|
+
| Some(eventConfig) =>
|
|
457
|
+
let txKey = instr.slot->Int.toString ++ ":" ++ instr.transactionIndex->Int.toString
|
|
458
|
+
let maybeTx =
|
|
459
|
+
txByKey->Utils.Dict.dangerouslyGetNonOption(txKey)->Option.map(toSvmTransaction)
|
|
460
|
+
let maybeTx = if eventConfig.includeTokenBalances {
|
|
461
|
+
maybeTx->Option.map(tx => {
|
|
462
|
+
let maybeBalances =
|
|
463
|
+
tokenBalancesByTx
|
|
464
|
+
->Utils.Dict.dangerouslyGetNonOption(txKey)
|
|
465
|
+
->Option.map(bals => bals->Array.map(toSvmTokenBalance))
|
|
466
|
+
{...tx, tokenBalances: ?maybeBalances}
|
|
467
|
+
})
|
|
468
|
+
} else {
|
|
469
|
+
maybeTx
|
|
470
|
+
}
|
|
471
|
+
let logKey =
|
|
472
|
+
instr.slot->Int.toString ++
|
|
473
|
+
":" ++
|
|
474
|
+
instr.transactionIndex->Int.toString ++
|
|
475
|
+
":" ++
|
|
476
|
+
serializeInstructionAddress(instr.instructionAddress)
|
|
477
|
+
let maybeLogs =
|
|
478
|
+
logsByKey
|
|
479
|
+
->Utils.Dict.dangerouslyGetNonOption(logKey)
|
|
480
|
+
->Option.map(logs =>
|
|
481
|
+
logs->Array.map(
|
|
482
|
+
(log): Envio.svmLog => {
|
|
483
|
+
kind: log.kind->Option.getOr(""),
|
|
484
|
+
message: log.message->Option.getOr(""),
|
|
485
|
+
},
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
let slotKey = instr.slot->Int.toString
|
|
490
|
+
let blockTime = blockTimeBySlot->Utils.Dict.dangerouslyGetNonOption(slotKey)
|
|
491
|
+
let payload = toSvmInstruction(
|
|
492
|
+
instr,
|
|
493
|
+
~programName=eventConfig.contractName,
|
|
494
|
+
~instructionName=eventConfig.name,
|
|
495
|
+
~transaction=eventConfig.includeTransaction ? maybeTx : None,
|
|
496
|
+
~logs=eventConfig.includeLogs ? maybeLogs : None,
|
|
497
|
+
~block={
|
|
498
|
+
slot: instr.slot,
|
|
499
|
+
time: blockTime->Option.getOr(0),
|
|
500
|
+
hash: "",
|
|
501
|
+
},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
parsedQueueItems
|
|
505
|
+
->Array.push(
|
|
506
|
+
Internal.Event({
|
|
507
|
+
eventConfig: (eventConfig :> Internal.eventConfig),
|
|
508
|
+
timestamp: blockTime->Option.getOr(0),
|
|
509
|
+
chain,
|
|
510
|
+
blockNumber: instr.slot,
|
|
511
|
+
blockHash: "",
|
|
512
|
+
logIndex: synthLogIndex(instr),
|
|
513
|
+
event: payload->(Utils.magic: Envio.svmInstruction => Internal.event),
|
|
514
|
+
}),
|
|
515
|
+
)
|
|
516
|
+
->ignore
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let _ = logger
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
let parsingTimeElapsed = parsingRef->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
523
|
+
let highestSlot = resp.nextSlot - 1
|
|
524
|
+
let latestBlockTime =
|
|
525
|
+
blockTimeBySlot
|
|
526
|
+
->Utils.Dict.dangerouslyGetNonOption(highestSlot->Int.toString)
|
|
527
|
+
->Option.getOr(0)
|
|
528
|
+
|
|
529
|
+
// Best-effort (slot, blockhash) pairs from the blocks the server returned
|
|
530
|
+
// for this range. Gaps (skipped slots, or slots without matched data) are
|
|
531
|
+
// fine — reorg detection only compares hashes for slots it has observed.
|
|
532
|
+
let blockHashes = resp.data.blocks->Array.map((b): ReorgDetection.blockData => {
|
|
533
|
+
blockNumber: b.slot,
|
|
534
|
+
blockHash: b.blockhash,
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
let totalTimeElapsed = totalTimeRef->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
538
|
+
|
|
539
|
+
{
|
|
540
|
+
latestFetchedBlockTimestamp: latestBlockTime,
|
|
541
|
+
parsedQueueItems,
|
|
542
|
+
latestFetchedBlockNumber: highestSlot,
|
|
543
|
+
stats: {totalTimeElapsed, parsingTimeElapsed, pageFetchTime},
|
|
544
|
+
knownHeight,
|
|
545
|
+
blockHashes,
|
|
546
|
+
fromBlockQueried: fromBlock,
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Fetch (slot, blockhash, blockTime) for blocks in an inclusive slot range,
|
|
551
|
+
// paginating on the server's `nextSlot` cursor. `toSlot` is exclusive on the
|
|
552
|
+
// wire, so we request `maxSlot + 1`; the caller filters to the exact slots.
|
|
553
|
+
let queryBlockDataRange = async (~fromSlot, ~toSlot) => {
|
|
554
|
+
let blockDatas = []
|
|
555
|
+
let fromRef = ref(fromSlot)
|
|
556
|
+
let keepGoing = ref(true)
|
|
557
|
+
while keepGoing.contents {
|
|
558
|
+
let query: SvmHyperSyncClient.query = {
|
|
559
|
+
fromSlot: fromRef.contents,
|
|
560
|
+
toSlot: toSlot + 1,
|
|
561
|
+
includeAllBlocks: true,
|
|
562
|
+
fields: {block: [Slot, Blockhash, BlockTime]},
|
|
563
|
+
maxNumBlocks: 1000,
|
|
564
|
+
}
|
|
565
|
+
Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getBlockHashes")
|
|
566
|
+
let resp = await client.get(~query)
|
|
567
|
+
resp.data.blocks->Array.forEach(b =>
|
|
568
|
+
blockDatas
|
|
569
|
+
->Array.push({
|
|
570
|
+
ReorgDetection.blockNumber: b.slot,
|
|
571
|
+
blockHash: b.blockhash,
|
|
572
|
+
blockTimestamp: b.blockTime->Option.getOr(0),
|
|
573
|
+
})
|
|
574
|
+
->ignore
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
// `nextSlot` is the (exclusive) resume cursor. Stop once it passes the
|
|
578
|
+
// range, or fails to advance — the latter guards against an infinite loop.
|
|
579
|
+
if resp.nextSlot > toSlot || resp.nextSlot <= fromRef.contents {
|
|
580
|
+
keepGoing := false
|
|
581
|
+
} else {
|
|
582
|
+
fromRef := resp.nextSlot
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
blockDatas
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let getBlockHashes = async (~blockNumbers, ~logger as _) =>
|
|
589
|
+
switch blockNumbers->Array.get(0) {
|
|
590
|
+
| None => Ok([])
|
|
591
|
+
| Some(firstSlot) =>
|
|
592
|
+
try {
|
|
593
|
+
let minSlot = ref(firstSlot)
|
|
594
|
+
let maxSlot = ref(firstSlot)
|
|
595
|
+
let requested = Utils.Set.make()
|
|
596
|
+
blockNumbers->Array.forEach(slot => {
|
|
597
|
+
if slot < minSlot.contents {
|
|
598
|
+
minSlot := slot
|
|
599
|
+
}
|
|
600
|
+
if slot > maxSlot.contents {
|
|
601
|
+
maxSlot := slot
|
|
602
|
+
}
|
|
603
|
+
requested->Utils.Set.add(slot)->ignore
|
|
604
|
+
})
|
|
605
|
+
let blockDatas = await queryBlockDataRange(
|
|
606
|
+
~fromSlot=minSlot.contents,
|
|
607
|
+
~toSlot=maxSlot.contents,
|
|
608
|
+
)
|
|
609
|
+
// Keep one entry per requested slot; drop duplicates and unrelated slots.
|
|
610
|
+
Ok(blockDatas->Array.filter(data => requested->Utils.Set.delete(data.blockNumber)))
|
|
611
|
+
} catch {
|
|
612
|
+
| exn => Error(exn)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
{
|
|
617
|
+
name,
|
|
618
|
+
sourceFor: Sync,
|
|
619
|
+
chain,
|
|
620
|
+
pollingInterval: 1000,
|
|
621
|
+
poweredByHyperSync: true,
|
|
622
|
+
getBlockHashes,
|
|
623
|
+
getHeightOrThrow: async () => {
|
|
624
|
+
let timer = Hrtime.makeTimer()
|
|
625
|
+
let h = await client.getHeight()
|
|
626
|
+
let seconds = timer->Hrtime.timeSince->Hrtime.toSecondsFloat
|
|
627
|
+
Prometheus.SourceRequestCount.increment(~sourceName=name, ~chainId, ~method="getHeight")
|
|
628
|
+
Prometheus.SourceRequestCount.addSeconds(
|
|
629
|
+
~sourceName=name,
|
|
630
|
+
~chainId,
|
|
631
|
+
~method="getHeight",
|
|
632
|
+
~seconds,
|
|
633
|
+
)
|
|
634
|
+
h
|
|
635
|
+
},
|
|
636
|
+
getItemsOrThrow,
|
|
637
|
+
}
|
|
638
|
+
}
|