envio 3.13.0-alpha.0 → 3.13.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 -8
- package/src/BatchProcessing.res +4 -5
- package/src/BatchProcessing.res.mjs +4 -11
- package/src/ChainFetching.res +18 -38
- package/src/ChainFetching.res.mjs +14 -20
- package/src/ChainState.res +113 -77
- package/src/ChainState.res.mjs +90 -73
- package/src/ChainState.resi +14 -5
- package/src/Config.res +14 -17
- package/src/Config.res.mjs +5 -20
- package/src/CrossChainState.res +68 -20
- package/src/CrossChainState.res.mjs +48 -5
- package/src/CrossChainState.resi +1 -0
- package/src/Env.res +2 -2
- package/src/ErrorHandling.res +15 -4
- package/src/ErrorHandling.res.mjs +16 -5
- package/src/FinalizeBackfill.res +21 -9
- package/src/FinalizeBackfill.res.mjs +17 -4
- package/src/HandlerRegister.res +0 -37
- package/src/HandlerRegister.res.mjs +0 -18
- package/src/IndexerState.res +2 -0
- package/src/IndexerState.res.mjs +5 -0
- package/src/IndexerState.resi +1 -0
- package/src/Logging.res +6 -38
- package/src/Logging.res.mjs +5 -32
- package/src/Main.res +6 -9
- package/src/Main.res.mjs +3 -5
- package/src/Metrics.res +9 -4
- package/src/Metrics.res.mjs +3 -3
- package/src/Persistence.res +6 -3
- package/src/Persistence.res.mjs +5 -3
- package/src/PgStorage.res +2 -2
- package/src/PgStorage.res.mjs +2 -2
- package/src/Rollback.res +3 -3
- package/src/Server.res +0 -3
- package/src/SimulateItems.res +1 -1
- package/src/SimulateItems.res.mjs +1 -1
- package/src/Supervisor.res +236 -109
- package/src/Supervisor.res.mjs +185 -55
- package/src/TestIndexer.res +6 -3
- package/src/TestIndexer.res.mjs +3 -3
- package/src/Worker.res +6 -0
- package/src/Worker.res.mjs +2 -1
- package/src/bindings/NodeJs.res +25 -16
- package/src/bindings/NodeJs.res.mjs +8 -1
- package/src/sources/AddressSet.res +5 -3
- package/src/sources/AddressStore.res +19 -11
- package/src/sources/AddressStore.res.mjs +2 -7
- package/src/sources/BlockStore.res +10 -9
- package/src/sources/BlockStore.res.mjs +4 -4
- package/src/sources/ChainSources.res +4 -0
- package/src/sources/ChainSources.res.mjs +2 -2
- package/src/sources/EvmChain.res +4 -0
- package/src/sources/EvmChain.res.mjs +3 -1
- package/src/sources/EvmEventItem.res +56 -0
- package/src/sources/EvmEventItem.res.mjs +32 -0
- package/src/sources/EvmHyperSyncSource.res +7 -37
- package/src/sources/EvmHyperSyncSource.res.mjs +8 -29
- package/src/sources/EvmRpcClient.res +41 -59
- package/src/sources/EvmRpcClient.res.mjs +4 -54
- package/src/sources/EvmRpcWs.res +4 -1
- package/src/sources/EvmRpcWs.res.mjs +6 -2
- package/src/sources/FuelHyperSyncSource.res +6 -4
- package/src/sources/FuelHyperSyncSource.res.mjs +7 -9
- package/src/sources/HeightFeed.res +7 -1
- package/src/sources/HeightFeed.res.mjs +9 -7
- package/src/sources/HyperSyncClient.res +1 -14
- package/src/sources/RequestStat.res +7 -0
- package/src/sources/RequestStat.res.mjs +14 -1
- package/src/sources/RpcSource.res +100 -1043
- package/src/sources/RpcSource.res.mjs +81 -1056
- package/src/sources/SimulateSource.res +2 -5
- package/src/sources/SimulateSource.res.mjs +2 -3
- package/src/sources/Source.res +34 -12
- package/src/sources/Source.res.mjs +11 -0
- package/src/sources/SourceManager.res +111 -31
- package/src/sources/SourceManager.res.mjs +65 -31
- package/src/sources/SourceManager.resi +4 -0
- package/src/sources/StartBlockResolver.res +8 -5
- package/src/sources/StartBlockResolver.res.mjs +4 -2
- package/src/sources/SvmHyperSyncSource.res +4 -2
- package/src/sources/SvmHyperSyncSource.res.mjs +4 -6
- package/src/sources/TransactionStore.res +9 -6
- package/src/sources/TransactionStore.res.mjs +2 -2
- package/src/tui/Tui.res +4 -18
- package/src/tui/Tui.res.mjs +3 -6
- package/src/LazyLoader.res +0 -135
- package/src/LazyLoader.res.mjs +0 -118
- package/src/bindings/SDSL.res +0 -12
- package/src/bindings/SDSL.res.mjs +0 -9
- package/src/bindings/Yargs.res +0 -8
- package/src/bindings/Yargs.res.mjs +0 -2
- package/src/sources/Rpc.res +0 -185
- package/src/sources/Rpc.res.mjs +0 -183
|
@@ -1,685 +1,5 @@
|
|
|
1
1
|
open Source
|
|
2
2
|
|
|
3
|
-
// eth_getTransactionByHash/eth_getTransactionReceipt returning null is usually
|
|
4
|
-
// transient: a load-balanced provider can route the lookup to a node that
|
|
5
|
-
// hasn't caught up with the one that served eth_getLogs. Must stay retryable,
|
|
6
|
-
// unlike other field-selection failures which disable the source.
|
|
7
|
-
exception TransactionDataNotFound({message: string})
|
|
8
|
-
|
|
9
|
-
// Minimal block data needed for infrastructure (reorg guard, timestamps, etc.)
|
|
10
|
-
type blockInfo = {
|
|
11
|
-
number: int,
|
|
12
|
-
timestamp: int,
|
|
13
|
-
hash: string,
|
|
14
|
-
parentHash: string,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let getKnownRawBlock = async (~client, ~blockNumber) =>
|
|
18
|
-
switch await Rpc.getRawBlock(~client, ~blockNumber) {
|
|
19
|
-
| Some(json) => json
|
|
20
|
-
| None =>
|
|
21
|
-
JsError.throwWithMessage(`RPC returned null for blockNumber ${blockNumber->Int.toString}`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Extract infrastructure fields (number, timestamp, hash) from raw block JSON
|
|
25
|
-
let parseBlockInfo = (json: JSON.t): blockInfo => {
|
|
26
|
-
let jsonDict = json->(Utils.magic: JSON.t => dict<JSON.t>)
|
|
27
|
-
{
|
|
28
|
-
number: jsonDict
|
|
29
|
-
->Dict.getUnsafe("number")
|
|
30
|
-
->S.parseOrThrow(Rpc.hexIntSchema),
|
|
31
|
-
timestamp: jsonDict
|
|
32
|
-
->Dict.getUnsafe("timestamp")
|
|
33
|
-
->S.parseOrThrow(Rpc.hexIntSchema),
|
|
34
|
-
hash: jsonDict
|
|
35
|
-
->Dict.getUnsafe("hash")
|
|
36
|
-
->S.parseOrThrow(S.string),
|
|
37
|
-
parentHash: jsonDict
|
|
38
|
-
->Dict.getUnsafe("parentHash")
|
|
39
|
-
->S.parseOrThrow(S.string),
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let getKnownRawBlockWithBackoff = async (
|
|
44
|
-
~client,
|
|
45
|
-
~sourceName,
|
|
46
|
-
~chainId,
|
|
47
|
-
~blockNumber,
|
|
48
|
-
~backoffMsOnFailure,
|
|
49
|
-
~recordRequest: (~method: string, ~seconds: float) => unit,
|
|
50
|
-
) => {
|
|
51
|
-
let currentBackoff = ref(backoffMsOnFailure)
|
|
52
|
-
let result = ref(None)
|
|
53
|
-
|
|
54
|
-
while result.contents->Option.isNone {
|
|
55
|
-
let timerRef = Performance.now()
|
|
56
|
-
switch await getKnownRawBlock(~client, ~blockNumber) {
|
|
57
|
-
| exception err =>
|
|
58
|
-
recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
|
|
59
|
-
Logging.warn({
|
|
60
|
-
"err": err->Utils.prettifyExn,
|
|
61
|
-
"msg": `Issue while running fetching batch of events from the RPC. Will wait ${currentBackoff.contents->Int.toString}ms and try again.`,
|
|
62
|
-
"source": sourceName,
|
|
63
|
-
"chainId": chainId,
|
|
64
|
-
"type": "EXPONENTIAL_BACKOFF",
|
|
65
|
-
})
|
|
66
|
-
await Time.resolvePromiseAfterDelay(~delayMilliseconds=currentBackoff.contents)
|
|
67
|
-
currentBackoff := currentBackoff.contents * 2
|
|
68
|
-
| json =>
|
|
69
|
-
recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
|
|
70
|
-
result := Some(json)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
result.contents->Option.getOrThrow
|
|
74
|
-
}
|
|
75
|
-
// Pulls the underlying provider error message back out of a caught exn, for
|
|
76
|
-
// logging/debugging. Provider JSON-RPC errors surface as `Rpc.JsonRpcError`;
|
|
77
|
-
// the paging retry decision (see `parseGetNextPageRetryError` below) surfaces
|
|
78
|
-
// as a napi `JsExn` whose message is the JSON payload `EvmRpcClient.getNextPage`
|
|
79
|
-
// throws, carrying the classified message (if any) under `errorMessage`.
|
|
80
|
-
let getErrorMessage = (exn: exn): option<string> =>
|
|
81
|
-
switch exn {
|
|
82
|
-
| Rpc.JsonRpcError({message}) => Some(message)
|
|
83
|
-
| JsExn(e) =>
|
|
84
|
-
switch e->JsExn.message {
|
|
85
|
-
| Some(msg) =>
|
|
86
|
-
switch msg->JSON.parseOrThrow->JSON.Decode.object {
|
|
87
|
-
| exception _ => None
|
|
88
|
-
| None => None
|
|
89
|
-
| Some(obj) =>
|
|
90
|
-
switch obj->Dict.get("errorMessage") {
|
|
91
|
-
| Some(String(message)) => Some(message)
|
|
92
|
-
| _ => None
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
| None => None
|
|
96
|
-
}
|
|
97
|
-
| _ => None
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// `EvmRpcClient.getNextPage` throws a napi error whose message is a JSON
|
|
101
|
-
// payload describing the retry decision:
|
|
102
|
-
// `{"kind":"Retry","attemptedToBlock":int,"errorMessage":string|null,
|
|
103
|
-
// "requestStats":[{"method":string,"seconds":float}],"retry":
|
|
104
|
-
// {"tag":"WithSuggestedToBlock","toBlock":int} |
|
|
105
|
-
// {"tag":"WithBackoff","message":string,"backoffMillis":int}}`.
|
|
106
|
-
let parseGetNextPageRetryError = (exn: exn): option<(
|
|
107
|
-
int,
|
|
108
|
-
Source.getItemsRetry,
|
|
109
|
-
array<Source.requestStat>,
|
|
110
|
-
)> =>
|
|
111
|
-
switch exn {
|
|
112
|
-
| JsExn(e) =>
|
|
113
|
-
switch e->JsExn.message {
|
|
114
|
-
| Some(msg) =>
|
|
115
|
-
switch msg->JSON.parseOrThrow->JSON.Decode.object {
|
|
116
|
-
| exception _ => None
|
|
117
|
-
| None => None
|
|
118
|
-
| Some(obj) =>
|
|
119
|
-
switch (obj->Dict.get("kind"), obj->Dict.get("attemptedToBlock"), obj->Dict.get("retry")) {
|
|
120
|
-
| (Some(String("Retry")), Some(Number(attemptedToBlock)), Some(Object(retryObj))) =>
|
|
121
|
-
let requestStats = switch obj->Dict.get("requestStats") {
|
|
122
|
-
| Some(Array(stats)) =>
|
|
123
|
-
stats->Array.filterMap(s =>
|
|
124
|
-
switch s->JSON.Decode.object {
|
|
125
|
-
| Some(o) =>
|
|
126
|
-
switch (o->Dict.get("method"), o->Dict.get("seconds")) {
|
|
127
|
-
| (Some(String(method)), Some(Number(seconds))) => Some({Source.method, seconds})
|
|
128
|
-
| _ => None
|
|
129
|
-
}
|
|
130
|
-
| None => None
|
|
131
|
-
}
|
|
132
|
-
)
|
|
133
|
-
| _ => []
|
|
134
|
-
}
|
|
135
|
-
let retry = switch retryObj->Dict.get("tag") {
|
|
136
|
-
| Some(String("WithSuggestedToBlock")) =>
|
|
137
|
-
switch retryObj->Dict.get("toBlock") {
|
|
138
|
-
| Some(Number(toBlock)) =>
|
|
139
|
-
Some(Source.WithSuggestedToBlock({toBlock: toBlock->Float.toInt}))
|
|
140
|
-
| _ => None
|
|
141
|
-
}
|
|
142
|
-
| Some(String("WithBackoff")) =>
|
|
143
|
-
switch (retryObj->Dict.get("message"), retryObj->Dict.get("backoffMillis")) {
|
|
144
|
-
| (Some(String(message)), Some(Number(backoffMillis))) =>
|
|
145
|
-
Some(Source.WithBackoff({message, backoffMillis: backoffMillis->Float.toInt}))
|
|
146
|
-
| _ => None
|
|
147
|
-
}
|
|
148
|
-
| _ => None
|
|
149
|
-
}
|
|
150
|
-
retry->Option.map(retry => (attemptedToBlock->Float.toInt, retry, requestStats))
|
|
151
|
-
| _ => None
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
| None => None
|
|
155
|
-
}
|
|
156
|
-
| _ => None
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Type-erase a schema for storage in the field registry
|
|
160
|
-
external toFieldSchema: S.t<'a> => S.t<JSON.t> = "%identity"
|
|
161
|
-
|
|
162
|
-
let lowercaseAddressSchema: S.t<JSON.t> =
|
|
163
|
-
S.string
|
|
164
|
-
->S.transform(_ => {
|
|
165
|
-
parser: str => str->String.toLowerCase->Address.unsafeFromString,
|
|
166
|
-
})
|
|
167
|
-
->toFieldSchema
|
|
168
|
-
|
|
169
|
-
let checksumAddressSchema: S.t<JSON.t> =
|
|
170
|
-
S.string
|
|
171
|
-
->S.transform(_ => {
|
|
172
|
-
parser: str => str->Address.Evm.fromStringOrThrow,
|
|
173
|
-
})
|
|
174
|
-
->toFieldSchema
|
|
175
|
-
|
|
176
|
-
// Block field definition for per-field parsing
|
|
177
|
-
type blockFieldDef = {
|
|
178
|
-
location: Internal.evmBlockField,
|
|
179
|
-
jsonKey: string,
|
|
180
|
-
schema: S.t<JSON.t>, // Type-erased schema
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Block field registry: maps field location (= JS property name) to parsing info.
|
|
184
|
-
let makeBlockFieldRegistry = (addressSchema: S.t<JSON.t>): Utils.Record.t<
|
|
185
|
-
Internal.evmBlockField,
|
|
186
|
-
blockFieldDef,
|
|
187
|
-
> =>
|
|
188
|
-
[
|
|
189
|
-
{location: Number, jsonKey: "number", schema: Rpc.hexIntSchema->toFieldSchema},
|
|
190
|
-
{location: Timestamp, jsonKey: "timestamp", schema: Rpc.hexIntSchema->toFieldSchema},
|
|
191
|
-
{location: Hash, jsonKey: "hash", schema: S.string->toFieldSchema},
|
|
192
|
-
{location: ParentHash, jsonKey: "parentHash", schema: S.string->toFieldSchema},
|
|
193
|
-
{location: Nonce, jsonKey: "nonce", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
194
|
-
{location: Sha3Uncles, jsonKey: "sha3Uncles", schema: S.string->toFieldSchema},
|
|
195
|
-
{location: LogsBloom, jsonKey: "logsBloom", schema: S.string->toFieldSchema},
|
|
196
|
-
{location: TransactionsRoot, jsonKey: "transactionsRoot", schema: S.string->toFieldSchema},
|
|
197
|
-
{location: StateRoot, jsonKey: "stateRoot", schema: S.string->toFieldSchema},
|
|
198
|
-
{location: ReceiptsRoot, jsonKey: "receiptsRoot", schema: S.string->toFieldSchema},
|
|
199
|
-
{location: Miner, jsonKey: "miner", schema: addressSchema},
|
|
200
|
-
{location: Difficulty, jsonKey: "difficulty", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
201
|
-
{
|
|
202
|
-
location: TotalDifficulty,
|
|
203
|
-
jsonKey: "totalDifficulty",
|
|
204
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
205
|
-
},
|
|
206
|
-
{location: ExtraData, jsonKey: "extraData", schema: S.string->toFieldSchema},
|
|
207
|
-
{location: Size, jsonKey: "size", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
208
|
-
{location: GasLimit, jsonKey: "gasLimit", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
209
|
-
{location: GasUsed, jsonKey: "gasUsed", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
210
|
-
{location: Uncles, jsonKey: "uncles", schema: S.array(S.string)->toFieldSchema},
|
|
211
|
-
{location: BaseFeePerGas, jsonKey: "baseFeePerGas", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
212
|
-
{location: BlobGasUsed, jsonKey: "blobGasUsed", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
213
|
-
{location: ExcessBlobGas, jsonKey: "excessBlobGas", schema: Rpc.hexBigintSchema->toFieldSchema},
|
|
214
|
-
{
|
|
215
|
-
location: ParentBeaconBlockRoot,
|
|
216
|
-
jsonKey: "parentBeaconBlockRoot",
|
|
217
|
-
schema: S.string->toFieldSchema,
|
|
218
|
-
},
|
|
219
|
-
{location: WithdrawalsRoot, jsonKey: "withdrawalsRoot", schema: S.string->toFieldSchema},
|
|
220
|
-
{location: L1BlockNumber, jsonKey: "l1BlockNumber", schema: Rpc.hexIntSchema->toFieldSchema},
|
|
221
|
-
{location: SendCount, jsonKey: "sendCount", schema: S.string->toFieldSchema},
|
|
222
|
-
{location: SendRoot, jsonKey: "sendRoot", schema: S.string->toFieldSchema},
|
|
223
|
-
{location: MixHash, jsonKey: "mixHash", schema: S.string->toFieldSchema},
|
|
224
|
-
]
|
|
225
|
-
->Array.map(def => (
|
|
226
|
-
def.location,
|
|
227
|
-
if Internal.evmNullableBlockFields->Utils.Set.has(def.location) {
|
|
228
|
-
{...def, schema: S.nullable(def.schema)->toFieldSchema}
|
|
229
|
-
} else {
|
|
230
|
-
def
|
|
231
|
-
},
|
|
232
|
-
))
|
|
233
|
-
->Utils.Record.fromArray
|
|
234
|
-
|
|
235
|
-
let blockFieldRegistryLowercase = makeBlockFieldRegistry(lowercaseAddressSchema)
|
|
236
|
-
let blockFieldRegistryChecksum = makeBlockFieldRegistry(checksumAddressSchema)
|
|
237
|
-
|
|
238
|
-
// Parse block fields from raw JSON, similar to parseFieldsFromJson for transactions
|
|
239
|
-
let parseBlockFieldsFromJson = (
|
|
240
|
-
mutBlockAcc: dict<JSON.t>,
|
|
241
|
-
fields: array<blockFieldDef>,
|
|
242
|
-
json: JSON.t,
|
|
243
|
-
) => {
|
|
244
|
-
let jsonDict = json->(Utils.magic: JSON.t => dict<JSON.t>)
|
|
245
|
-
fields->Array.forEach(def => {
|
|
246
|
-
let raw = jsonDict->Dict.getUnsafe(def.jsonKey)
|
|
247
|
-
try {
|
|
248
|
-
let parsed = raw->S.parseOrThrow(def.schema)
|
|
249
|
-
mutBlockAcc->Dict.set((def.location :> string), parsed)
|
|
250
|
-
} catch {
|
|
251
|
-
| S.Raised(error) =>
|
|
252
|
-
JsError.throwWithMessage(
|
|
253
|
-
`Invalid block field "${(def.location :> string)}" found in the RPC response. Error: ${error->S.Error.reason}`,
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
let makeThrowingGetEventBlock = (
|
|
260
|
-
~getBlockJson: int => promise<JSON.t>,
|
|
261
|
-
~lowercaseAddresses: bool,
|
|
262
|
-
) => {
|
|
263
|
-
let blockFieldRegistry = if lowercaseAddresses {
|
|
264
|
-
blockFieldRegistryLowercase
|
|
265
|
-
} else {
|
|
266
|
-
blockFieldRegistryChecksum
|
|
267
|
-
}
|
|
268
|
-
let fnsCache = Utils.WeakMap.make()
|
|
269
|
-
(log: Rpc.GetLogs.log, ~selectedBlockFields: Utils.Set.t<Internal.evmBlockField>) => {
|
|
270
|
-
(
|
|
271
|
-
switch fnsCache->Utils.WeakMap.get(selectedBlockFields) {
|
|
272
|
-
| Some(fn) => fn
|
|
273
|
-
// Build per-field parser on first call, then cache in WeakMap
|
|
274
|
-
| None => {
|
|
275
|
-
let fields: array<blockFieldDef> = []
|
|
276
|
-
selectedBlockFields->Utils.Set.forEach(fieldName => {
|
|
277
|
-
fields->Array.push(blockFieldRegistry->Utils.Record.getUnsafe(fieldName))->ignore
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
let fn = if selectedBlockFields->Utils.Set.size == 0 {
|
|
281
|
-
_ => %raw(`{}`)->(Utils.magic: 'a => Internal.eventBlock)->Promise.resolve
|
|
282
|
-
} else {
|
|
283
|
-
(log: Rpc.GetLogs.log) => {
|
|
284
|
-
getBlockJson(log.blockNumber)->Promise.thenResolve(json => {
|
|
285
|
-
let mutBlockAcc = Dict.make()
|
|
286
|
-
parseBlockFieldsFromJson(mutBlockAcc, fields, json)
|
|
287
|
-
mutBlockAcc->(Utils.magic: dict<JSON.t> => Internal.eventBlock)
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
let _ = fnsCache->Utils.WeakMap.set(selectedBlockFields, fn)
|
|
292
|
-
fn
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
)(log)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// `number` is always part of the selected block fields, so it can be read
|
|
300
|
-
// from the assembled block for the item's own `blockNumber`.
|
|
301
|
-
@get external getBlockNumber: Internal.eventBlock => int = "number"
|
|
302
|
-
|
|
303
|
-
// Field source classification for RPC calls
|
|
304
|
-
type fieldSource = TransactionOnly | ReceiptOnly | Both
|
|
305
|
-
|
|
306
|
-
type fieldDef = {
|
|
307
|
-
location: Internal.evmTransactionField,
|
|
308
|
-
jsonKey: string,
|
|
309
|
-
schema: S.t<JSON.t>, // Type-erased schema (S.nullable for optional fields)
|
|
310
|
-
source: fieldSource,
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Field registry: maps field location (= JS property name) to parsing info.
|
|
314
|
-
// Only includes fields that require an RPC call. Log-derived fields (hash, transactionIndex) are special-cased.
|
|
315
|
-
// Nullable fields are wrapped with S.nullable during registry construction based on Internal.evmNullableTransactionFields
|
|
316
|
-
let makeFieldRegistry = (addressSchema: S.t<JSON.t>): Utils.Record.t<
|
|
317
|
-
Internal.evmTransactionField,
|
|
318
|
-
fieldDef,
|
|
319
|
-
> =>
|
|
320
|
-
[
|
|
321
|
-
// TransactionOnly fields (only in eth_getTransactionByHash)
|
|
322
|
-
{
|
|
323
|
-
location: Gas,
|
|
324
|
-
jsonKey: "gas",
|
|
325
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
326
|
-
source: TransactionOnly,
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
location: GasPrice,
|
|
330
|
-
jsonKey: "gasPrice",
|
|
331
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
332
|
-
source: TransactionOnly,
|
|
333
|
-
},
|
|
334
|
-
{location: Input, jsonKey: "input", schema: S.string->toFieldSchema, source: TransactionOnly},
|
|
335
|
-
{
|
|
336
|
-
location: Nonce,
|
|
337
|
-
jsonKey: "nonce",
|
|
338
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
339
|
-
source: TransactionOnly,
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
location: Value,
|
|
343
|
-
jsonKey: "value",
|
|
344
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
345
|
-
source: TransactionOnly,
|
|
346
|
-
},
|
|
347
|
-
{location: V, jsonKey: "v", schema: S.string->toFieldSchema, source: TransactionOnly},
|
|
348
|
-
{location: R, jsonKey: "r", schema: S.string->toFieldSchema, source: TransactionOnly},
|
|
349
|
-
{location: S, jsonKey: "s", schema: S.string->toFieldSchema, source: TransactionOnly},
|
|
350
|
-
{
|
|
351
|
-
location: YParity,
|
|
352
|
-
jsonKey: "yParity",
|
|
353
|
-
schema: S.string->toFieldSchema,
|
|
354
|
-
source: TransactionOnly,
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
location: MaxPriorityFeePerGas,
|
|
358
|
-
jsonKey: "maxPriorityFeePerGas",
|
|
359
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
360
|
-
source: TransactionOnly,
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
location: MaxFeePerGas,
|
|
364
|
-
jsonKey: "maxFeePerGas",
|
|
365
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
366
|
-
source: TransactionOnly,
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
location: MaxFeePerBlobGas,
|
|
370
|
-
jsonKey: "maxFeePerBlobGas",
|
|
371
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
372
|
-
source: TransactionOnly,
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
location: BlobVersionedHashes,
|
|
376
|
-
jsonKey: "blobVersionedHashes",
|
|
377
|
-
schema: S.array(S.string)->toFieldSchema,
|
|
378
|
-
source: TransactionOnly,
|
|
379
|
-
},
|
|
380
|
-
// ReceiptOnly fields (only in eth_getTransactionReceipt)
|
|
381
|
-
{
|
|
382
|
-
location: GasUsed,
|
|
383
|
-
jsonKey: "gasUsed",
|
|
384
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
385
|
-
source: ReceiptOnly,
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
location: CumulativeGasUsed,
|
|
389
|
-
jsonKey: "cumulativeGasUsed",
|
|
390
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
391
|
-
source: ReceiptOnly,
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
location: EffectiveGasPrice,
|
|
395
|
-
jsonKey: "effectiveGasPrice",
|
|
396
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
397
|
-
source: ReceiptOnly,
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
location: ContractAddress,
|
|
401
|
-
jsonKey: "contractAddress",
|
|
402
|
-
schema: addressSchema,
|
|
403
|
-
source: ReceiptOnly,
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
location: LogsBloom,
|
|
407
|
-
jsonKey: "logsBloom",
|
|
408
|
-
schema: S.string->toFieldSchema,
|
|
409
|
-
source: ReceiptOnly,
|
|
410
|
-
},
|
|
411
|
-
{location: Root, jsonKey: "root", schema: S.string->toFieldSchema, source: ReceiptOnly},
|
|
412
|
-
{
|
|
413
|
-
location: Status,
|
|
414
|
-
jsonKey: "status",
|
|
415
|
-
schema: Rpc.hexIntSchema->toFieldSchema,
|
|
416
|
-
source: ReceiptOnly,
|
|
417
|
-
},
|
|
418
|
-
{
|
|
419
|
-
location: L1Fee,
|
|
420
|
-
jsonKey: "l1Fee",
|
|
421
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
422
|
-
source: ReceiptOnly,
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
location: L1GasPrice,
|
|
426
|
-
jsonKey: "l1GasPrice",
|
|
427
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
428
|
-
source: ReceiptOnly,
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
location: L1GasUsed,
|
|
432
|
-
jsonKey: "l1GasUsed",
|
|
433
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
434
|
-
source: ReceiptOnly,
|
|
435
|
-
},
|
|
436
|
-
{
|
|
437
|
-
location: L1FeeScalar,
|
|
438
|
-
jsonKey: "l1FeeScalar",
|
|
439
|
-
schema: Rpc.decimalFloatSchema->toFieldSchema,
|
|
440
|
-
source: ReceiptOnly,
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
location: GasUsedForL1,
|
|
444
|
-
jsonKey: "gasUsedForL1",
|
|
445
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
446
|
-
source: ReceiptOnly,
|
|
447
|
-
},
|
|
448
|
-
// Both fields (available in both eth_getTransactionByHash and eth_getTransactionReceipt)
|
|
449
|
-
{location: From, jsonKey: "from", schema: addressSchema, source: Both},
|
|
450
|
-
{location: To, jsonKey: "to", schema: addressSchema, source: Both},
|
|
451
|
-
{location: Type, jsonKey: "type", schema: Rpc.hexIntSchema->toFieldSchema, source: Both},
|
|
452
|
-
]
|
|
453
|
-
->Array.map(def => (
|
|
454
|
-
def.location,
|
|
455
|
-
if Internal.evmNullableTransactionFields->Utils.Set.has(def.location) {
|
|
456
|
-
{...def, schema: S.nullable(def.schema)->toFieldSchema}
|
|
457
|
-
} else {
|
|
458
|
-
def
|
|
459
|
-
},
|
|
460
|
-
))
|
|
461
|
-
->Utils.Record.fromArray
|
|
462
|
-
|
|
463
|
-
let fieldRegistryLowercase = makeFieldRegistry(lowercaseAddressSchema)
|
|
464
|
-
let fieldRegistryChecksum = makeFieldRegistry(checksumAddressSchema)
|
|
465
|
-
|
|
466
|
-
// Whether an RPC source can populate a transaction field. The getter skips a
|
|
467
|
-
// field it can't parse, so this is what separates "absent because the chain has
|
|
468
|
-
// none" from "absent because RPC never provides it". `hash` and
|
|
469
|
-
// `transactionIndex` come off the log itself, so they have no registry entry.
|
|
470
|
-
// Blocks need no equivalent: `eth_getBlockByNumber` carries every block field,
|
|
471
|
-
// which `RpcFieldSelection_test.res` holds the registry to.
|
|
472
|
-
let isRpcTransactionField = (name: string) =>
|
|
473
|
-
switch name {
|
|
474
|
-
| "transactionIndex" | "hash" => true
|
|
475
|
-
| _ =>
|
|
476
|
-
fieldRegistryChecksum
|
|
477
|
-
->Utils.Record.get(name->(Utils.magic: string => Internal.evmTransactionField))
|
|
478
|
-
->Option.isSome
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
type fetchStrategy = NoRpc | TransactionOnly | ReceiptOnly | TransactionAndReceipt
|
|
482
|
-
|
|
483
|
-
// Parse fields from a raw JSON object into a result dict.
|
|
484
|
-
// Uses unsafeGet so nullable schemas (S.nullable) handle both null and undefined.
|
|
485
|
-
let parseFieldsFromJson = (
|
|
486
|
-
mutTransactionAcc: dict<JSON.t>,
|
|
487
|
-
fields: array<fieldDef>,
|
|
488
|
-
json: JSON.t,
|
|
489
|
-
) => {
|
|
490
|
-
let jsonDict = json->(Utils.magic: JSON.t => dict<JSON.t>)
|
|
491
|
-
fields->Array.forEach(def => {
|
|
492
|
-
let raw = jsonDict->Dict.getUnsafe(def.jsonKey)
|
|
493
|
-
try {
|
|
494
|
-
let parsed = raw->S.parseOrThrow(def.schema)
|
|
495
|
-
mutTransactionAcc->Dict.set((def.location :> string), parsed)
|
|
496
|
-
} catch {
|
|
497
|
-
| S.Raised(error) =>
|
|
498
|
-
JsError.throwWithMessage(
|
|
499
|
-
`Invalid transaction field "${(def.location :> string)}" found in the RPC response. Error: ${error->S.Error.reason}`,
|
|
500
|
-
)
|
|
501
|
-
}
|
|
502
|
-
})
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
let effectiveGasPriceKey = (Internal.EffectiveGasPrice: Internal.evmTransactionField :> string)
|
|
506
|
-
|
|
507
|
-
// Pre-EIP-1559 receipts carry no `effectiveGasPrice` — every Optimism block
|
|
508
|
-
// below the Bedrock migration at 105235063, for one. Those chains price every
|
|
509
|
-
// transaction the legacy way, so the transaction's `gasPrice` is the effective
|
|
510
|
-
// price, which is the same substitution HyperSync serves for those blocks.
|
|
511
|
-
// Reads `gasPrice` but stores under `effectiveGasPrice`.
|
|
512
|
-
let effectiveGasPriceFallbackDef = {
|
|
513
|
-
location: EffectiveGasPrice,
|
|
514
|
-
jsonKey: "gasPrice",
|
|
515
|
-
schema: Rpc.hexBigintSchema->toFieldSchema,
|
|
516
|
-
source: TransactionOnly,
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
let fillEffectiveGasPriceOrThrow = (mutTransactionAcc: dict<JSON.t>, txJson: JSON.t) => {
|
|
520
|
-
let gasPrice =
|
|
521
|
-
txJson->(Utils.magic: JSON.t => dict<JSON.t>)->Utils.Dict.dangerouslyGetNonOption("gasPrice")
|
|
522
|
-
switch gasPrice {
|
|
523
|
-
| None =>
|
|
524
|
-
JsError.throwWithMessage(`Neither "effectiveGasPrice" nor "gasPrice" is present in the RPC response for the transaction. Remove "effectiveGasPrice" from the field selection, or index this chain via HyperSync.`)
|
|
525
|
-
| Some(_) => parseFieldsFromJson(mutTransactionAcc, [effectiveGasPriceFallbackDef], txJson)
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
let makeThrowingGetEventTransaction = (
|
|
530
|
-
~getTransactionJson: string => promise<JSON.t>,
|
|
531
|
-
~getReceiptJson: string => promise<JSON.t>,
|
|
532
|
-
~lowercaseAddresses: bool,
|
|
533
|
-
) => {
|
|
534
|
-
let fieldRegistry = if lowercaseAddresses {
|
|
535
|
-
fieldRegistryLowercase
|
|
536
|
-
} else {
|
|
537
|
-
fieldRegistryChecksum
|
|
538
|
-
}
|
|
539
|
-
let fnsCache = Utils.WeakMap.make()
|
|
540
|
-
(log, ~selectedTransactionFields: Utils.Set.t<Internal.evmTransactionField>) => {
|
|
541
|
-
(
|
|
542
|
-
switch fnsCache->Utils.WeakMap.get(selectedTransactionFields) {
|
|
543
|
-
| Some(fn) => fn
|
|
544
|
-
// Build per-field parser on first call, then cache in WeakMap
|
|
545
|
-
| None => {
|
|
546
|
-
// Classify fields: log-derived vs RPC fields
|
|
547
|
-
let hasTransactionIndex = ref(false)
|
|
548
|
-
let hasHash = ref(false)
|
|
549
|
-
let hasEffectiveGasPrice = ref(false)
|
|
550
|
-
let txFields: array<fieldDef> = []
|
|
551
|
-
let receiptFields: array<fieldDef> = []
|
|
552
|
-
let bothFields: array<fieldDef> = []
|
|
553
|
-
|
|
554
|
-
selectedTransactionFields->Utils.Set.forEach(fieldName => {
|
|
555
|
-
switch fieldName {
|
|
556
|
-
| TransactionIndex => hasTransactionIndex := true
|
|
557
|
-
| Hash => hasHash := true
|
|
558
|
-
| _ =>
|
|
559
|
-
switch fieldRegistry->Utils.Record.get(fieldName) {
|
|
560
|
-
| Some(def) =>
|
|
561
|
-
// Absent from the receipt means fall back to the transaction's
|
|
562
|
-
// `gasPrice`, so this one field parses leniently rather than
|
|
563
|
-
// throwing on a receipt that omits it.
|
|
564
|
-
let def = if fieldName === EffectiveGasPrice {
|
|
565
|
-
hasEffectiveGasPrice := true
|
|
566
|
-
{...def, schema: S.nullable(def.schema)->toFieldSchema}
|
|
567
|
-
} else {
|
|
568
|
-
def
|
|
569
|
-
}
|
|
570
|
-
switch def.source {
|
|
571
|
-
| TransactionOnly => txFields->Array.push(def)->ignore
|
|
572
|
-
| ReceiptOnly => receiptFields->Array.push(def)->ignore
|
|
573
|
-
| Both => bothFields->Array.push(def)->ignore
|
|
574
|
-
}
|
|
575
|
-
| None => () // Unknown field — skip silently
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
// Determine fetch strategy
|
|
581
|
-
let strategy = switch (txFields->Array.length > 0, receiptFields->Array.length > 0) {
|
|
582
|
-
| (true, true) => TransactionAndReceipt
|
|
583
|
-
| (true, false) => TransactionOnly
|
|
584
|
-
| (false, true) => ReceiptOnly
|
|
585
|
-
| (false, false) if bothFields->Array.length > 0 => TransactionOnly
|
|
586
|
-
| (false, false) => NoRpc
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Assign Both fields to whichever source is already being fetched; default to transaction
|
|
590
|
-
let targetForBoth = strategy == ReceiptOnly ? receiptFields : txFields
|
|
591
|
-
bothFields->Array.forEach(f => targetForBoth->Array.push(f)->ignore)
|
|
592
|
-
|
|
593
|
-
// Set log-derived fields on the mutable accumulator
|
|
594
|
-
let setLogFields = (mutTransactionAcc: dict<JSON.t>, log: Rpc.GetLogs.log) => {
|
|
595
|
-
if hasTransactionIndex.contents {
|
|
596
|
-
mutTransactionAcc->Dict.set(
|
|
597
|
-
"transactionIndex",
|
|
598
|
-
log.transactionIndex->(Utils.magic: int => JSON.t),
|
|
599
|
-
)
|
|
600
|
-
}
|
|
601
|
-
if hasHash.contents {
|
|
602
|
-
mutTransactionAcc->Dict.set(
|
|
603
|
-
"hash",
|
|
604
|
-
log.transactionHash->(Utils.magic: string => JSON.t),
|
|
605
|
-
)
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
let fn = if selectedTransactionFields->Utils.Set.size == 0 {
|
|
610
|
-
_ => %raw(`{}`)->Promise.resolve
|
|
611
|
-
} else {
|
|
612
|
-
switch strategy {
|
|
613
|
-
| NoRpc =>
|
|
614
|
-
(log: Rpc.GetLogs.log) => {
|
|
615
|
-
let mutTransactionAcc = Dict.make()
|
|
616
|
-
setLogFields(mutTransactionAcc, log)
|
|
617
|
-
mutTransactionAcc->(Utils.magic: dict<JSON.t> => 'a)->Promise.resolve
|
|
618
|
-
}
|
|
619
|
-
| _ =>
|
|
620
|
-
(log: Rpc.GetLogs.log) => {
|
|
621
|
-
let txJsonPromise = switch strategy {
|
|
622
|
-
| TransactionOnly | TransactionAndReceipt =>
|
|
623
|
-
getTransactionJson(log.transactionHash)->Promise.thenResolve(v => Some(v))
|
|
624
|
-
| _ => Promise.resolve(None)
|
|
625
|
-
}
|
|
626
|
-
let receiptJsonPromise = switch strategy {
|
|
627
|
-
| ReceiptOnly | TransactionAndReceipt =>
|
|
628
|
-
getReceiptJson(log.transactionHash)->Promise.thenResolve(v => Some(v))
|
|
629
|
-
| _ => Promise.resolve(None)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
Promise.all2((txJsonPromise, receiptJsonPromise))
|
|
633
|
-
->Promise.then(((txJson, receiptJson)) => {
|
|
634
|
-
let mutTransactionAcc = Dict.make()
|
|
635
|
-
setLogFields(mutTransactionAcc, log)
|
|
636
|
-
|
|
637
|
-
switch txJson {
|
|
638
|
-
| Some(json) => parseFieldsFromJson(mutTransactionAcc, txFields, json)
|
|
639
|
-
| None => ()
|
|
640
|
-
}
|
|
641
|
-
switch receiptJson {
|
|
642
|
-
| Some(json) => parseFieldsFromJson(mutTransactionAcc, receiptFields, json)
|
|
643
|
-
| None => ()
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Only the chains that omit it pay for the extra request, and
|
|
647
|
-
// only when the receipt has already come back without it.
|
|
648
|
-
if (
|
|
649
|
-
hasEffectiveGasPrice.contents &&
|
|
650
|
-
mutTransactionAcc
|
|
651
|
-
->Utils.Dict.dangerouslyGetNonOption(effectiveGasPriceKey)
|
|
652
|
-
->Option.isNone
|
|
653
|
-
) {
|
|
654
|
-
switch txJson {
|
|
655
|
-
| Some(json) => {
|
|
656
|
-
fillEffectiveGasPriceOrThrow(mutTransactionAcc, json)
|
|
657
|
-
Promise.resolve(mutTransactionAcc)
|
|
658
|
-
}
|
|
659
|
-
| None =>
|
|
660
|
-
getTransactionJson(log.transactionHash)->Promise.thenResolve(json => {
|
|
661
|
-
fillEffectiveGasPriceOrThrow(mutTransactionAcc, json)
|
|
662
|
-
mutTransactionAcc
|
|
663
|
-
})
|
|
664
|
-
}
|
|
665
|
-
} else {
|
|
666
|
-
Promise.resolve(mutTransactionAcc)
|
|
667
|
-
}
|
|
668
|
-
})
|
|
669
|
-
->Promise.thenResolve(mutTransactionAcc => {
|
|
670
|
-
mutTransactionAcc->(Utils.magic: dict<JSON.t> => 'a)
|
|
671
|
-
})
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
let _ = fnsCache->Utils.WeakMap.set(selectedTransactionFields, fn)
|
|
676
|
-
fn
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
)(log)
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
3
|
type options = {
|
|
684
4
|
sourceFor: Source.sourceFor,
|
|
685
5
|
syncConfig: Config.sourceSync,
|
|
@@ -690,6 +10,10 @@ type options = {
|
|
|
690
10
|
lowercaseAddresses: bool,
|
|
691
11
|
// The chain's address index; the client reads it while routing.
|
|
692
12
|
addressStore: AddressStore.t,
|
|
13
|
+
// Read by the client while planning; the pages it returns are separate, and
|
|
14
|
+
// the caller merges them.
|
|
15
|
+
blockStore: BlockStore.t,
|
|
16
|
+
transactionStore: TransactionStore.t,
|
|
693
17
|
ws?: string,
|
|
694
18
|
headers?: dict<string>,
|
|
695
19
|
}
|
|
@@ -703,6 +27,8 @@ let make = (
|
|
|
703
27
|
onEventRegistrations,
|
|
704
28
|
lowercaseAddresses,
|
|
705
29
|
addressStore,
|
|
30
|
+
blockStore,
|
|
31
|
+
transactionStore,
|
|
706
32
|
?ws,
|
|
707
33
|
?headers,
|
|
708
34
|
}: options,
|
|
@@ -716,7 +42,6 @@ let make = (
|
|
|
716
42
|
}
|
|
717
43
|
let name = `RPC (${urlHost})`
|
|
718
44
|
|
|
719
|
-
let client = Rpc.makeClient(url, ~headers?)
|
|
720
45
|
let rpcClient = EvmRpcClient.make(
|
|
721
46
|
~url,
|
|
722
47
|
~eventRegistrations=HyperSyncClient.Registration.fromOnEventRegistrations(onEventRegistrations),
|
|
@@ -726,153 +51,6 @@ let make = (
|
|
|
726
51
|
~addressStore,
|
|
727
52
|
)
|
|
728
53
|
|
|
729
|
-
// Requests are made from shared, memoized loaders, so they can't be
|
|
730
|
-
// attributed to a single getItemsOrThrow/getHeightOrThrow/getBlockHashes
|
|
731
|
-
// call at its call site. Every actual request (cache/dedup hits never reach
|
|
732
|
-
// recordRequest) pushes here; each method drains whatever is pending when it
|
|
733
|
-
// returns. Since a push always lands in exactly one drain, per-source totals
|
|
734
|
-
// stay exact even with concurrent in-flight calls — which call happens to
|
|
735
|
-
// drain a given entry doesn't matter, since SourceManager aggregates by
|
|
736
|
-
// (source, method) regardless of which call returned it.
|
|
737
|
-
let pendingRequestStats: array<Source.requestStat> = []
|
|
738
|
-
let recordRequest = (~method, ~seconds) => {
|
|
739
|
-
pendingRequestStats->Array.push({Source.method, seconds})->ignore
|
|
740
|
-
}
|
|
741
|
-
let drainRequestStats = () => {
|
|
742
|
-
let stats = pendingRequestStats->Utils.Array.copy
|
|
743
|
-
pendingRequestStats->Utils.Array.clearInPlace
|
|
744
|
-
stats
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
let makeTransactionLoader = () =>
|
|
748
|
-
LazyLoader.make(
|
|
749
|
-
~loaderFn=transactionHash => {
|
|
750
|
-
let timerRef = Performance.now()
|
|
751
|
-
Rpc.GetTransactionByHash.rawRoute
|
|
752
|
-
->Rest.fetch(transactionHash, ~client)
|
|
753
|
-
->Promise.thenResolve(res => {
|
|
754
|
-
recordRequest(
|
|
755
|
-
~method="eth_getTransactionByHash",
|
|
756
|
-
~seconds=timerRef->Performance.secondsSince,
|
|
757
|
-
)
|
|
758
|
-
res
|
|
759
|
-
})
|
|
760
|
-
},
|
|
761
|
-
~onError=(am, ~exn) => {
|
|
762
|
-
Logging.error({
|
|
763
|
-
"err": exn->Utils.prettifyExn,
|
|
764
|
-
"msg": `Top level promise timeout reached. Please review other errors or warnings in the code. This function will retry in ${(am._retryDelayMillis / 1000)
|
|
765
|
-
->Int.toString} seconds. It is highly likely that your indexer isn't syncing on one or more chains currently. Also take a look at the "suggestedFix" in the metadata of this command`,
|
|
766
|
-
"source": name,
|
|
767
|
-
"chainId": chainId,
|
|
768
|
-
"metadata": {
|
|
769
|
-
{
|
|
770
|
-
"asyncTaskName": "transactionLoader: fetching transaction data - `getTransaction` rpc call",
|
|
771
|
-
"suggestedFix": "This likely means the RPC url you are using is not responding correctly. Please try another RPC endipoint.",
|
|
772
|
-
}
|
|
773
|
-
},
|
|
774
|
-
})
|
|
775
|
-
},
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
let makeBlockLoader = () =>
|
|
779
|
-
LazyLoader.make(
|
|
780
|
-
~loaderFn=blockNumber => {
|
|
781
|
-
getKnownRawBlockWithBackoff(
|
|
782
|
-
~client,
|
|
783
|
-
~sourceName=name,
|
|
784
|
-
~chainId,
|
|
785
|
-
~backoffMsOnFailure=1000,
|
|
786
|
-
~blockNumber,
|
|
787
|
-
~recordRequest,
|
|
788
|
-
)
|
|
789
|
-
},
|
|
790
|
-
~onError=(am, ~exn) => {
|
|
791
|
-
Logging.error({
|
|
792
|
-
"err": exn->Utils.prettifyExn,
|
|
793
|
-
"msg": `Top level promise timeout reached. Please review other errors or warnings in the code. This function will retry in ${(am._retryDelayMillis / 1000)
|
|
794
|
-
->Int.toString} seconds. It is highly likely that your indexer isn't syncing on one or more chains currently. Also take a look at the "suggestedFix" in the metadata of this command`,
|
|
795
|
-
"source": name,
|
|
796
|
-
"chainId": chainId,
|
|
797
|
-
"metadata": {
|
|
798
|
-
{
|
|
799
|
-
"asyncTaskName": "blockLoader: fetching block data - `getBlock` rpc call",
|
|
800
|
-
"suggestedFix": "This likely means the RPC url you are using is not responding correctly. Please try another RPC endipoint.",
|
|
801
|
-
}
|
|
802
|
-
},
|
|
803
|
-
})
|
|
804
|
-
},
|
|
805
|
-
)
|
|
806
|
-
|
|
807
|
-
let makeReceiptLoader = () =>
|
|
808
|
-
LazyLoader.make(
|
|
809
|
-
~loaderFn=transactionHash => {
|
|
810
|
-
let timerRef = Performance.now()
|
|
811
|
-
Rpc.GetTransactionReceipt.rawRoute
|
|
812
|
-
->Rest.fetch(transactionHash, ~client)
|
|
813
|
-
->Promise.thenResolve(res => {
|
|
814
|
-
recordRequest(
|
|
815
|
-
~method="eth_getTransactionReceipt",
|
|
816
|
-
~seconds=timerRef->Performance.secondsSince,
|
|
817
|
-
)
|
|
818
|
-
res
|
|
819
|
-
})
|
|
820
|
-
},
|
|
821
|
-
~onError=(am, ~exn) => {
|
|
822
|
-
Logging.error({
|
|
823
|
-
"err": exn->Utils.prettifyExn,
|
|
824
|
-
"msg": `Top level promise timeout reached. Please review other errors or warnings in the code. This function will retry in ${(am._retryDelayMillis / 1000)
|
|
825
|
-
->Int.toString} seconds. It is highly likely that your indexer isn't syncing on one or more chains currently. Also take a look at the "suggestedFix" in the metadata of this command`,
|
|
826
|
-
"source": name,
|
|
827
|
-
"chainId": chainId,
|
|
828
|
-
"metadata": {
|
|
829
|
-
{
|
|
830
|
-
"asyncTaskName": "receiptLoader: fetching transaction receipt - `getTransactionReceipt` rpc call",
|
|
831
|
-
"suggestedFix": "This likely means the RPC url you are using is not responding correctly. Please try another RPC endipoint.",
|
|
832
|
-
}
|
|
833
|
-
},
|
|
834
|
-
})
|
|
835
|
-
},
|
|
836
|
-
)
|
|
837
|
-
|
|
838
|
-
let blockLoader = ref(makeBlockLoader())
|
|
839
|
-
let transactionLoader = ref(makeTransactionLoader())
|
|
840
|
-
let receiptLoader = ref(makeReceiptLoader())
|
|
841
|
-
|
|
842
|
-
let resetCachedLoaders = () => {
|
|
843
|
-
blockLoader := makeBlockLoader()
|
|
844
|
-
transactionLoader := makeTransactionLoader()
|
|
845
|
-
receiptLoader := makeReceiptLoader()
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
let getEventBlockOrThrow = makeThrowingGetEventBlock(
|
|
849
|
-
~getBlockJson=blockNumber => blockLoader.contents->LazyLoader.get(blockNumber),
|
|
850
|
-
~lowercaseAddresses,
|
|
851
|
-
)
|
|
852
|
-
let getEventTransactionOrThrow = makeThrowingGetEventTransaction(
|
|
853
|
-
~getTransactionJson=async transactionHash => {
|
|
854
|
-
switch await transactionLoader.contents->LazyLoader.get(transactionHash) {
|
|
855
|
-
| Some(json) => json
|
|
856
|
-
| None =>
|
|
857
|
-
throw(
|
|
858
|
-
TransactionDataNotFound({message: `Transaction not found for hash: ${transactionHash}`}),
|
|
859
|
-
)
|
|
860
|
-
}
|
|
861
|
-
},
|
|
862
|
-
~getReceiptJson=async transactionHash => {
|
|
863
|
-
switch await receiptLoader.contents->LazyLoader.get(transactionHash) {
|
|
864
|
-
| Some(json) => json
|
|
865
|
-
| None =>
|
|
866
|
-
throw(
|
|
867
|
-
TransactionDataNotFound({
|
|
868
|
-
message: `Transaction receipt not found for hash: ${transactionHash}`,
|
|
869
|
-
}),
|
|
870
|
-
)
|
|
871
|
-
}
|
|
872
|
-
},
|
|
873
|
-
~lowercaseAddresses,
|
|
874
|
-
)
|
|
875
|
-
|
|
876
54
|
let getItemsOrThrow = async (
|
|
877
55
|
~fromBlock,
|
|
878
56
|
~toBlock,
|
|
@@ -885,7 +63,7 @@ let make = (
|
|
|
885
63
|
~retry,
|
|
886
64
|
~logger as _,
|
|
887
65
|
) => {
|
|
888
|
-
let
|
|
66
|
+
let totalTimeRef = Performance.now()
|
|
889
67
|
|
|
890
68
|
// Always have a toBlock for an RPC worker
|
|
891
69
|
let toBlock = switch toBlock {
|
|
@@ -893,19 +71,6 @@ let make = (
|
|
|
893
71
|
| None => knownHeight
|
|
894
72
|
}
|
|
895
73
|
|
|
896
|
-
// The seam block (`fromBlock - 1`) is the only block this range shares with
|
|
897
|
-
// what the store already scanned, so it is where a reorg at the boundary
|
|
898
|
-
// shows up. Reading it directly would answer from the block cache — it was
|
|
899
|
-
// the previous range's `toBlock`, so it is always cached, and always on the
|
|
900
|
-
// chain that range saw. Read `fromBlock` instead, which no earlier range
|
|
901
|
-
// touched, and take the seam's hash from its `parentHash`.
|
|
902
|
-
let firstBlockPromise =
|
|
903
|
-
fromBlock > 0 && fromBlock <= toBlock
|
|
904
|
-
? blockLoader.contents
|
|
905
|
-
->LazyLoader.get(fromBlock)
|
|
906
|
-
->Promise.thenResolve(json => Some(parseBlockInfo(json)))
|
|
907
|
-
: Promise.resolve(None)
|
|
908
|
-
|
|
909
74
|
if selection.onEventRegistrations->Utils.Array.isEmpty {
|
|
910
75
|
throw(
|
|
911
76
|
Source.GetItemsError(
|
|
@@ -916,216 +81,116 @@ let make = (
|
|
|
916
81
|
)
|
|
917
82
|
}
|
|
918
83
|
|
|
919
|
-
let
|
|
84
|
+
let pageFetchTimeRef = Performance.now()
|
|
85
|
+
let (result, pageBlockStore, pageTransactionStore) = await rpcClient.getNextPage(
|
|
920
86
|
{
|
|
921
87
|
fromBlock,
|
|
922
88
|
toBlockCeiling: toBlock,
|
|
923
89
|
partitionId,
|
|
924
90
|
registrationIndexes: selection.onEventRegistrations->Array.map(reg => reg.index),
|
|
925
91
|
clientFilteredContracts: selection.clientFilteredContracts,
|
|
92
|
+
retry,
|
|
926
93
|
},
|
|
927
94
|
addressSet,
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
| 0 => 500
|
|
946
|
-
| _ => 1000 * retry
|
|
947
|
-
},
|
|
948
|
-
}),
|
|
949
|
-
}),
|
|
950
|
-
),
|
|
951
|
-
)
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
requestStats->Array.forEach(stat => recordRequest(~method=stat.method, ~seconds=stat.seconds))
|
|
955
|
-
|
|
956
|
-
let latestFetchedBlockInfo = await blockLoader.contents
|
|
957
|
-
->LazyLoader.get(queriedToBlock)
|
|
958
|
-
->Promise.thenResolve(parseBlockInfo)
|
|
959
|
-
|
|
960
|
-
let parsedQueueItems = await items
|
|
961
|
-
->Array.map(({log, onEventRegistrationIndex, params: decoded}: EvmRpcClient.rpcEventItem) => {
|
|
962
|
-
// `log.address` comes back already normalized to the client's casing.
|
|
963
|
-
let onEventRegistration = onEventRegistrations->Array.getUnsafe(onEventRegistrationIndex)
|
|
964
|
-
let eventConfig =
|
|
965
|
-
onEventRegistration.eventConfig->(
|
|
966
|
-
Utils.magic: Internal.eventConfig => Internal.evmEventConfig
|
|
967
|
-
)
|
|
968
|
-
|
|
969
|
-
(
|
|
970
|
-
async () => {
|
|
971
|
-
let (block, transaction) = try await Promise.all2((
|
|
972
|
-
log->getEventBlockOrThrow(
|
|
973
|
-
~selectedBlockFields=onEventRegistration.fieldSelection.blockFields->(
|
|
974
|
-
Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.evmBlockField>
|
|
975
|
-
),
|
|
976
|
-
),
|
|
977
|
-
log->getEventTransactionOrThrow(
|
|
978
|
-
~selectedTransactionFields=onEventRegistration.fieldSelection.transactionFields->(
|
|
979
|
-
Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.evmTransactionField>
|
|
980
|
-
),
|
|
981
|
-
),
|
|
982
|
-
)) catch {
|
|
983
|
-
| TransactionDataNotFound({message}) =>
|
|
984
|
-
let backoffMillis = switch retry {
|
|
985
|
-
| 0 => 100
|
|
986
|
-
| _ => 500 * retry
|
|
987
|
-
}
|
|
988
|
-
throw(
|
|
989
|
-
Source.GetItemsError(
|
|
990
|
-
FailedGettingItems({
|
|
991
|
-
exn: %raw(`null`),
|
|
992
|
-
attemptedToBlock: toBlock,
|
|
993
|
-
retry: WithBackoff({
|
|
994
|
-
message: `${message}. The RPC provider might be load-balanced between nodes that drift independently slightly from the head. Indexing should continue correctly after retrying the query in ${backoffMillis->Int.toString}ms.`,
|
|
995
|
-
backoffMillis,
|
|
996
|
-
}),
|
|
997
|
-
}),
|
|
998
|
-
),
|
|
999
|
-
)
|
|
1000
|
-
| exn =>
|
|
1001
|
-
throw(
|
|
1002
|
-
Source.GetItemsError(
|
|
1003
|
-
FailedGettingFieldSelection({
|
|
1004
|
-
message: "Failed getting selected fields. Please double-check your RPC provider returns correct data.",
|
|
1005
|
-
exn,
|
|
1006
|
-
blockNumber: log.blockNumber,
|
|
1007
|
-
logIndex: log.logIndex,
|
|
1008
|
-
}),
|
|
1009
|
-
),
|
|
1010
|
-
)
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
Internal.Event({
|
|
1014
|
-
onEventRegistration: (onEventRegistration :> Internal.onEventRegistration),
|
|
1015
|
-
blockNumber: block->getBlockNumber,
|
|
1016
|
-
chainId,
|
|
1017
|
-
logIndex: log.logIndex,
|
|
1018
|
-
transactionIndex: log.transactionIndex,
|
|
1019
|
-
payload: {
|
|
1020
|
-
contractName: eventConfig.contractName,
|
|
1021
|
-
eventName: eventConfig.name,
|
|
1022
|
-
chainId,
|
|
1023
|
-
params: decoded,
|
|
1024
|
-
block,
|
|
1025
|
-
transaction,
|
|
1026
|
-
srcAddress: log.address,
|
|
1027
|
-
logIndex: log.logIndex,
|
|
1028
|
-
}->Evm.fromPayload,
|
|
1029
|
-
})
|
|
1030
|
-
}
|
|
1031
|
-
)()
|
|
1032
|
-
})
|
|
1033
|
-
->Promise.all
|
|
1034
|
-
|
|
1035
|
-
let optFirstBlock = await firstBlockPromise
|
|
95
|
+
blockStore,
|
|
96
|
+
transactionStore,
|
|
97
|
+
)
|
|
98
|
+
let pageFetchTime = pageFetchTimeRef->Performance.secondsSince
|
|
99
|
+
|
|
100
|
+
// The cause travels as a real error so it reaches the logs the way every
|
|
101
|
+
// other source's does. Absent where the message is the whole story.
|
|
102
|
+
let exn = result.providerMessage->Option.map(message => JsError.make(message)->JsExn.anyToExnInternal)
|
|
103
|
+
|
|
104
|
+
let failedGettingItems = (decision): exn => Source.GetItemsError(
|
|
105
|
+
FailedGettingItems({
|
|
106
|
+
requestStats: result.requestStats,
|
|
107
|
+
?exn,
|
|
108
|
+
attemptedToBlock: result.toBlock,
|
|
109
|
+
retry: decision,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
1036
112
|
|
|
1037
|
-
let
|
|
113
|
+
let missing = field =>
|
|
114
|
+
JsError.throwWithMessage(
|
|
115
|
+
`The RPC client returned a "${(result.kind :> string)}" outcome without a ${field}. Please, report to the Envio team.`,
|
|
116
|
+
)
|
|
1038
117
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
118
|
+
switch result.kind {
|
|
119
|
+
| Ok => ()
|
|
120
|
+
| FieldSelection =>
|
|
121
|
+
throw(
|
|
122
|
+
Source.GetItemsError(
|
|
123
|
+
FailedGettingFieldSelection({
|
|
124
|
+
requestStats: result.requestStats,
|
|
125
|
+
message: switch result.message {
|
|
126
|
+
| Some(message) => message
|
|
127
|
+
| None => missing("message")
|
|
128
|
+
},
|
|
129
|
+
?exn,
|
|
130
|
+
blockNumber: switch result.blockNumber {
|
|
131
|
+
| Some(blockNumber) => blockNumber
|
|
132
|
+
| None => missing("blockNumber")
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
| SuggestedToBlock =>
|
|
138
|
+
throw(
|
|
139
|
+
failedGettingItems(
|
|
140
|
+
WithSuggestedToBlock({
|
|
141
|
+
toBlock: switch result.retryToBlock {
|
|
142
|
+
| Some(toBlock) => toBlock
|
|
143
|
+
| None => missing("retryToBlock")
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
| Backoff =>
|
|
149
|
+
throw(
|
|
150
|
+
failedGettingItems(
|
|
151
|
+
WithBackoff({
|
|
152
|
+
message: switch result.message {
|
|
153
|
+
| Some(message) => message
|
|
154
|
+
| None => missing("message")
|
|
155
|
+
},
|
|
156
|
+
backoffMillis: switch result.backoffMillis {
|
|
157
|
+
| Some(backoffMillis) => backoffMillis
|
|
158
|
+
| None => missing("backoffMillis")
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
1064
163
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
->
|
|
1069
|
-
|
|
164
|
+
|
|
165
|
+
let parsingTimeRef = Performance.now()
|
|
166
|
+
let parsedQueueItems =
|
|
167
|
+
result.items->EvmEventItem.toInternalItems(~onEventRegistrations, ~chainId)
|
|
168
|
+
let parsingTimeElapsed = parsingTimeRef->Performance.secondsSince
|
|
1070
169
|
|
|
1071
170
|
{
|
|
1072
|
-
latestFetchedBlockNumber: latestFetchedBlockInfo.number,
|
|
1073
171
|
parsedQueueItems,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
blockStore: BlockStore.fromJs(
|
|
1078
|
-
observedBlocks,
|
|
1079
|
-
~ecosystem=Evm,
|
|
1080
|
-
~shouldChecksum=!lowercaseAddresses,
|
|
1081
|
-
),
|
|
172
|
+
transactionStore: Some(pageTransactionStore),
|
|
173
|
+
blockStore: pageBlockStore,
|
|
174
|
+
latestFetchedBlockNumber: result.toBlock,
|
|
1082
175
|
stats: {
|
|
1083
|
-
totalTimeElapsed:
|
|
176
|
+
totalTimeElapsed: totalTimeRef->Performance.secondsSince,
|
|
177
|
+
parsingTimeElapsed,
|
|
178
|
+
pageFetchTime,
|
|
1084
179
|
},
|
|
1085
180
|
knownHeight,
|
|
1086
|
-
|
|
1087
|
-
requestStats: drainRequestStats(),
|
|
181
|
+
requestStats: result.requestStats,
|
|
1088
182
|
}
|
|
1089
183
|
}
|
|
1090
184
|
|
|
1091
|
-
let
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
->Promise.all
|
|
1101
|
-
->Promise.thenResolve(rawBlocks => {
|
|
1102
|
-
// Each block is fetched in its own request, so responses can mix forks.
|
|
1103
|
-
// The parent hash becomes a minimal extra row: when consecutive blocks
|
|
1104
|
-
// are requested, the page's own hash-collision check cross-validates the
|
|
1105
|
-
// separately fetched responses.
|
|
1106
|
-
let observedBlocks: array<BlockStore.inputBlock> = []
|
|
1107
|
-
rawBlocks->Array.forEach(json => {
|
|
1108
|
-
let b = parseBlockInfo(json)
|
|
1109
|
-
observedBlocks
|
|
1110
|
-
->Array.push({
|
|
1111
|
-
BlockStore.blockNumber: b.number,
|
|
1112
|
-
blockHash: b.hash,
|
|
1113
|
-
blockTimestamp: b.timestamp,
|
|
1114
|
-
})
|
|
1115
|
-
->ignore
|
|
1116
|
-
if b.number > 0 {
|
|
1117
|
-
observedBlocks
|
|
1118
|
-
->Array.push({BlockStore.blockNumber: b.number - 1, blockHash: b.parentHash})
|
|
1119
|
-
->ignore
|
|
1120
|
-
}
|
|
1121
|
-
})
|
|
1122
|
-
let blockStore =
|
|
1123
|
-
observedBlocks->BlockStore.fromJs(~ecosystem=Evm, ~shouldChecksum=!lowercaseAddresses)
|
|
1124
|
-
{Source.result: Ok(blockStore), requestStats: drainRequestStats()}
|
|
1125
|
-
})
|
|
1126
|
-
->Promise.catch(exn =>
|
|
1127
|
-
{Source.result: Error(exn), requestStats: drainRequestStats()}->Promise.resolve
|
|
1128
|
-
)
|
|
185
|
+
let getBlockHashes = async (~blockNumbers, ~logger as _) => {
|
|
186
|
+
let (result, pageBlockStore) = await rpcClient.getBlockHashes(blockNumbers)
|
|
187
|
+
{
|
|
188
|
+
Source.result: switch result.message {
|
|
189
|
+
| None => Ok(pageBlockStore)
|
|
190
|
+
| Some(message) => Error(JsError.make(message)->JsExn.anyToExnInternal)
|
|
191
|
+
},
|
|
192
|
+
requestStats: result.requestStats,
|
|
193
|
+
}
|
|
1129
194
|
}
|
|
1130
195
|
|
|
1131
196
|
let createHeightSubscription =
|
|
@@ -1140,18 +205,10 @@ let make = (
|
|
|
1140
205
|
poweredByHyperSync: false,
|
|
1141
206
|
pollingInterval: syncConfig.pollingInterval,
|
|
1142
207
|
getBlockHashes,
|
|
1143
|
-
onReorg,
|
|
208
|
+
onReorg: () => rpcClient.onReorg(),
|
|
1144
209
|
getHeightOrThrow: async () => {
|
|
1145
|
-
let
|
|
1146
|
-
|
|
1147
|
-
await rpcClient.getHeight()
|
|
1148
|
-
} catch {
|
|
1149
|
-
| exn =>
|
|
1150
|
-
recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
|
|
1151
|
-
exn->throw
|
|
1152
|
-
}
|
|
1153
|
-
recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
|
|
1154
|
-
{height, requestStats: drainRequestStats()}
|
|
210
|
+
let (height, requestStats) = await rpcClient.getHeight()
|
|
211
|
+
{height, requestStats}
|
|
1155
212
|
},
|
|
1156
213
|
getItemsOrThrow,
|
|
1157
214
|
?createHeightSubscription,
|