envio 2.17.0 → 2.18.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.
@@ -0,0 +1,349 @@
1
+ open Belt
2
+
3
+ module Log = {
4
+ type t = {
5
+ address: Address.t,
6
+ data: string,
7
+ topics: array<EvmTypes.Hex.t>,
8
+ logIndex: int,
9
+ }
10
+
11
+ let fieldNames = ["address", "data", "topics", "logIndex"]
12
+ }
13
+
14
+ type hyperSyncPage<'item> = {
15
+ items: array<'item>,
16
+ nextBlock: int,
17
+ archiveHeight: int,
18
+ rollbackGuard: option<HyperSyncClient.ResponseTypes.rollbackGuard>,
19
+ events: array<HyperSyncClient.ResponseTypes.event>,
20
+ }
21
+
22
+ type logsQueryPageItem = {
23
+ log: Log.t,
24
+ block: Internal.eventBlock,
25
+ transaction: Internal.eventTransaction,
26
+ }
27
+
28
+ type logsQueryPage = hyperSyncPage<logsQueryPageItem>
29
+
30
+ type missingParams = {
31
+ queryName: string,
32
+ missingParams: array<string>,
33
+ }
34
+ type queryError = UnexpectedMissingParams(missingParams)
35
+
36
+ exception HyperSyncQueryError(queryError)
37
+
38
+ let queryErrorToExn = queryError => {
39
+ HyperSyncQueryError(queryError)
40
+ }
41
+
42
+ let queryErrorToMsq = (e: queryError): string => {
43
+ switch e {
44
+ | UnexpectedMissingParams({queryName, missingParams}) =>
45
+ `${queryName} query failed due to unexpected missing params on response:
46
+ ${missingParams->Js.Array2.joinWith(", ")}`
47
+ }
48
+ }
49
+
50
+ type queryResponse<'a> = result<'a, queryError>
51
+ let mapExn = (queryResponse: queryResponse<'a>) =>
52
+ switch queryResponse {
53
+ | Ok(v) => Ok(v)
54
+ | Error(err) => err->queryErrorToExn->Error
55
+ }
56
+
57
+ module GetLogs = {
58
+ type error =
59
+ | UnexpectedMissingParams({missingParams: array<string>})
60
+ | WrongInstance
61
+
62
+ exception Error(error)
63
+
64
+ let makeRequestBody = (
65
+ ~fromBlock,
66
+ ~toBlockInclusive,
67
+ ~addressesWithTopics,
68
+ ~fieldSelection,
69
+ ): HyperSyncClient.QueryTypes.query => {
70
+ fromBlock,
71
+ toBlockExclusive: ?switch toBlockInclusive {
72
+ | Some(toBlockInclusive) => Some(toBlockInclusive + 1)
73
+ | None => None
74
+ },
75
+ logs: addressesWithTopics,
76
+ fieldSelection,
77
+ }
78
+
79
+ let addMissingParams = (acc, fieldNames, returnedObj, ~prefix) => {
80
+ fieldNames->Array.forEach(fieldName => {
81
+ switch returnedObj
82
+ ->(Utils.magic: 'a => Js.Dict.t<unknown>)
83
+ ->Utils.Dict.dangerouslyGetNonOption(fieldName) {
84
+ | Some(_) => ()
85
+ | None => acc->Array.push(prefix ++ "." ++ fieldName)->ignore
86
+ }
87
+ })
88
+ }
89
+
90
+ //Note this function can throw an error
91
+ let convertEvent = (
92
+ event: HyperSyncClient.ResponseTypes.event,
93
+ ~nonOptionalBlockFieldNames,
94
+ ~nonOptionalTransactionFieldNames,
95
+ ): logsQueryPageItem => {
96
+ let missingParams = []
97
+ missingParams->addMissingParams(Log.fieldNames, event.log, ~prefix="log")
98
+ missingParams->addMissingParams(nonOptionalBlockFieldNames, event.block, ~prefix="block")
99
+ missingParams->addMissingParams(
100
+ nonOptionalTransactionFieldNames,
101
+ event.transaction,
102
+ ~prefix="transaction",
103
+ )
104
+ if missingParams->Array.length > 0 {
105
+ raise(Error(UnexpectedMissingParams({missingParams: missingParams})))
106
+ }
107
+
108
+ //Topics can be nullable and still need to be filtered
109
+ let logUnsanitized: Log.t = event.log->Utils.magic
110
+ let topics = event.log.topics->Option.getUnsafe->Array.keepMap(Js.Nullable.toOption)
111
+ let address = event.log.address->Option.getUnsafe
112
+ let log = {
113
+ ...logUnsanitized,
114
+ topics,
115
+ address,
116
+ }
117
+
118
+ {
119
+ log,
120
+ block: event.block->Utils.magic,
121
+ transaction: event.transaction->Utils.magic,
122
+ }
123
+ }
124
+
125
+ let convertResponse = (
126
+ res: HyperSyncClient.ResponseTypes.eventResponse,
127
+ ~nonOptionalBlockFieldNames,
128
+ ~nonOptionalTransactionFieldNames,
129
+ ): logsQueryPage => {
130
+ let {nextBlock, archiveHeight, rollbackGuard} = res
131
+ let items =
132
+ res.data->Array.map(item =>
133
+ item->convertEvent(~nonOptionalBlockFieldNames, ~nonOptionalTransactionFieldNames)
134
+ )
135
+ let page: logsQueryPage = {
136
+ items,
137
+ nextBlock,
138
+ archiveHeight: archiveHeight->Option.getWithDefault(0), //Archive Height is only None if height is 0
139
+ events: res.data,
140
+ rollbackGuard,
141
+ }
142
+ page
143
+ }
144
+
145
+ let query = async (
146
+ ~client: HyperSyncClient.t,
147
+ ~fromBlock,
148
+ ~toBlock,
149
+ ~logSelections: array<LogSelection.t>,
150
+ ~fieldSelection,
151
+ ~nonOptionalBlockFieldNames,
152
+ ~nonOptionalTransactionFieldNames,
153
+ ): logsQueryPage => {
154
+ let addressesWithTopics = logSelections->Array.flatMap(({addresses, topicSelections}) =>
155
+ topicSelections->Array.map(({topic0, topic1, topic2, topic3}) => {
156
+ let topics = HyperSyncClient.QueryTypes.makeTopicSelection(
157
+ ~topic0,
158
+ ~topic1,
159
+ ~topic2,
160
+ ~topic3,
161
+ )
162
+ HyperSyncClient.QueryTypes.makeLogSelection(~address=addresses, ~topics)
163
+ })
164
+ )
165
+
166
+ let query = makeRequestBody(
167
+ ~fromBlock,
168
+ ~toBlockInclusive=toBlock,
169
+ ~addressesWithTopics,
170
+ ~fieldSelection,
171
+ )
172
+
173
+ let res = await client.getEvents(~query)
174
+ if res.nextBlock <= fromBlock {
175
+ // Might happen when /height response was from another instance of HyperSync
176
+ raise(Error(WrongInstance))
177
+ }
178
+
179
+ res->convertResponse(~nonOptionalBlockFieldNames, ~nonOptionalTransactionFieldNames)
180
+ }
181
+ }
182
+
183
+ module BlockData = {
184
+ let makeRequestBody = (~fromBlock, ~toBlock): HyperSyncJsonApi.QueryTypes.postQueryBody => {
185
+ fromBlock,
186
+ toBlockExclusive: toBlock + 1,
187
+ fieldSelection: {
188
+ block: [Number, Hash, Timestamp],
189
+ },
190
+ includeAllBlocks: true,
191
+ }
192
+
193
+ let convertResponse = (res: HyperSyncJsonApi.ResponseTypes.queryResponse): queryResponse<
194
+ array<ReorgDetection.blockDataWithTimestamp>,
195
+ > => {
196
+ res.data
197
+ ->Array.flatMap(item => {
198
+ item.blocks->Option.mapWithDefault([], blocks => {
199
+ blocks->Array.map(
200
+ block => {
201
+ switch block {
202
+ | {number: blockNumber, timestamp, hash: blockHash} =>
203
+ let blockTimestamp = timestamp->BigInt.toInt->Option.getExn
204
+ Ok(
205
+ (
206
+ {
207
+ blockTimestamp,
208
+ blockNumber,
209
+ blockHash,
210
+ }: ReorgDetection.blockDataWithTimestamp
211
+ ),
212
+ )
213
+ | _ =>
214
+ let missingParams =
215
+ [
216
+ block.number->Utils.Option.mapNone("block.number"),
217
+ block.timestamp->Utils.Option.mapNone("block.timestamp"),
218
+ block.hash->Utils.Option.mapNone("block.hash"),
219
+ ]->Array.keepMap(p => p)
220
+
221
+ Error(
222
+ UnexpectedMissingParams({
223
+ queryName: "query block data HyperSync",
224
+ missingParams,
225
+ }),
226
+ )
227
+ }
228
+ },
229
+ )
230
+ })
231
+ })
232
+ ->Utils.Array.transposeResults
233
+ }
234
+
235
+ let rec queryBlockData = async (
236
+ ~serverUrl,
237
+ ~apiToken,
238
+ ~fromBlock,
239
+ ~toBlock,
240
+ ~logger,
241
+ ): queryResponse<array<ReorgDetection.blockDataWithTimestamp>> => {
242
+ let body = makeRequestBody(~fromBlock, ~toBlock)
243
+
244
+ let logger = Logging.createChildFrom(
245
+ ~logger,
246
+ ~params={
247
+ "logType": "HyperSync get block hash query",
248
+ "fromBlock": fromBlock,
249
+ "toBlock": toBlock,
250
+ },
251
+ )
252
+
253
+ let maybeSuccessfulRes = switch await Time.retryAsyncWithExponentialBackOff(() =>
254
+ HyperSyncJsonApi.queryRoute->Rest.fetch(
255
+ {
256
+ "query": body,
257
+ "token": apiToken,
258
+ },
259
+ ~client=Rest.client(serverUrl),
260
+ )
261
+ , ~logger) {
262
+ | exception _ => None
263
+ | res if res.nextBlock <= fromBlock => None
264
+ | res => Some(res)
265
+ }
266
+
267
+ // If the block is not found, retry the query. This can occur since replicas of hypersync might not hack caught up yet
268
+ switch maybeSuccessfulRes {
269
+ | None => {
270
+ let logger = Logging.createChild(~params={"url": serverUrl})
271
+ let delayMilliseconds = 100
272
+ logger->Logging.childInfo(
273
+ `Block #${fromBlock->Int.toString} not found in HyperSync. HyperSync has multiple instances and it's possible that they drift independently slightly from the head. Indexing should continue correctly after retrying the query in ${delayMilliseconds->Int.toString}ms.`,
274
+ )
275
+ await Time.resolvePromiseAfterDelay(~delayMilliseconds)
276
+ await queryBlockData(~serverUrl, ~apiToken, ~fromBlock, ~toBlock, ~logger)
277
+ }
278
+ | Some(res) =>
279
+ switch res->convertResponse {
280
+ | Error(_) as err => err
281
+ | Ok(datas) if res.nextBlock <= toBlock => {
282
+ let restRes = await queryBlockData(
283
+ ~serverUrl,
284
+ ~apiToken,
285
+ ~fromBlock=res.nextBlock,
286
+ ~toBlock,
287
+ ~logger,
288
+ )
289
+ restRes->Result.map(rest => datas->Array.concat(rest))
290
+ }
291
+ | Ok(_) as ok => ok
292
+ }
293
+ }
294
+ }
295
+
296
+ let queryBlockDataMulti = async (~serverUrl, ~apiToken, ~blockNumbers, ~logger) => {
297
+ switch blockNumbers->Array.get(0) {
298
+ | None => Ok([])
299
+ | Some(firstBlock) => {
300
+ let fromBlock = ref(firstBlock)
301
+ let toBlock = ref(firstBlock)
302
+ let set = Utils.Set.make()
303
+ for idx in 0 to blockNumbers->Array.length - 1 {
304
+ let blockNumber = blockNumbers->Array.getUnsafe(idx)
305
+ if blockNumber < fromBlock.contents {
306
+ fromBlock := blockNumber
307
+ }
308
+ if blockNumber > toBlock.contents {
309
+ toBlock := blockNumber
310
+ }
311
+ set->Utils.Set.add(blockNumber)->ignore
312
+ }
313
+ if toBlock.contents - fromBlock.contents > 1000 {
314
+ Js.Exn.raiseError(
315
+ `Invalid block data request. Range of block numbers is too large. Max range is 1000. Requested range: ${fromBlock.contents->Int.toString}-${toBlock.contents->Int.toString}`,
316
+ )
317
+ }
318
+ let res = await queryBlockData(
319
+ ~fromBlock=fromBlock.contents,
320
+ ~toBlock=toBlock.contents,
321
+ ~serverUrl,
322
+ ~apiToken,
323
+ ~logger,
324
+ )
325
+ let filtered = res->Result.map(datas => {
326
+ datas->Array.keep(data => set->Utils.Set.delete(data.blockNumber))
327
+ })
328
+ if set->Utils.Set.size > 0 {
329
+ Js.Exn.raiseError(
330
+ `Invalid response. Failed to get block data for block numbers: ${set
331
+ ->Utils.Set.toArray
332
+ ->Js.Array2.joinWith(", ")}`,
333
+ )
334
+ }
335
+ filtered
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ let queryBlockData = (~serverUrl, ~apiToken, ~blockNumber, ~logger) =>
342
+ BlockData.queryBlockData(
343
+ ~serverUrl,
344
+ ~apiToken,
345
+ ~fromBlock=blockNumber,
346
+ ~toBlock=blockNumber,
347
+ ~logger,
348
+ )->Promise.thenResolve(res => res->Result.map(res => res->Array.get(0)))
349
+ let queryBlockDataMulti = BlockData.queryBlockDataMulti
@@ -0,0 +1,69 @@
1
+ type hyperSyncPage<'item> = {
2
+ items: array<'item>,
3
+ nextBlock: int,
4
+ archiveHeight: int,
5
+ rollbackGuard: option<HyperSyncClient.ResponseTypes.rollbackGuard>,
6
+ events: array<HyperSyncClient.ResponseTypes.event>,
7
+ }
8
+
9
+ module Log: {
10
+ type t = {
11
+ address: Address.t,
12
+ data: string,
13
+ topics: array<EvmTypes.Hex.t>,
14
+ logIndex: int,
15
+ }
16
+ }
17
+
18
+ type logsQueryPageItem = {
19
+ log: Log.t,
20
+ block: Internal.eventBlock,
21
+ transaction: Internal.eventTransaction,
22
+ }
23
+
24
+ type logsQueryPage = hyperSyncPage<logsQueryPageItem>
25
+
26
+ type missingParams = {
27
+ queryName: string,
28
+ missingParams: array<string>,
29
+ }
30
+
31
+ type queryError = UnexpectedMissingParams(missingParams)
32
+
33
+ let queryErrorToMsq: queryError => string
34
+
35
+ type queryResponse<'a> = result<'a, queryError>
36
+
37
+ module GetLogs: {
38
+ type error =
39
+ | UnexpectedMissingParams({missingParams: array<string>})
40
+ | WrongInstance
41
+
42
+ exception Error(error)
43
+
44
+ let query: (
45
+ ~client: HyperSyncClient.t,
46
+ ~fromBlock: int,
47
+ ~toBlock: option<int>,
48
+ ~logSelections: array<LogSelection.t>,
49
+ ~fieldSelection: HyperSyncClient.QueryTypes.fieldSelection,
50
+ ~nonOptionalBlockFieldNames: array<string>,
51
+ ~nonOptionalTransactionFieldNames: array<string>,
52
+ ) => promise<logsQueryPage>
53
+ }
54
+
55
+ let queryBlockData: (
56
+ ~serverUrl: string,
57
+ ~apiToken: string,
58
+ ~blockNumber: int,
59
+ ~logger: Pino.t,
60
+ ) => promise<queryResponse<option<ReorgDetection.blockDataWithTimestamp>>>
61
+
62
+ let queryBlockDataMulti: (
63
+ ~serverUrl: string,
64
+ ~apiToken: string,
65
+ ~blockNumbers: array<int>,
66
+ ~logger: Pino.t,
67
+ ) => promise<queryResponse<array<ReorgDetection.blockDataWithTimestamp>>>
68
+
69
+ let mapExn: queryResponse<'a> => result<'a, exn>