@apibara/evm-rpc 2.1.0-beta.49 → 2.1.0-beta.51

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.
@@ -1,7 +1,14 @@
1
1
  import type { LogFilter } from "@apibara/evm";
2
2
  import type { Bytes } from "@apibara/protocol";
3
3
  import type { RpcLog } from "viem";
4
- import { hexToNumber, isHex, numberToHex, pad, trim } from "viem";
4
+ import {
5
+ hexToNumber,
6
+ isAddressEqual,
7
+ isHex,
8
+ numberToHex,
9
+ pad,
10
+ trim,
11
+ } from "viem";
5
12
  import type { Log } from "./block";
6
13
  import type { Filter } from "./filter";
7
14
  import type { ViemRpcClient } from "./stream-config";
@@ -11,56 +18,53 @@ export async function fetchLogsByBlockHash({
11
18
  client,
12
19
  blockHash,
13
20
  filter,
21
+ mergeGetLogs,
14
22
  }: {
15
23
  client: ViemRpcClient;
16
24
  blockHash: Bytes;
17
25
  filter: Filter;
26
+ mergeGetLogs: boolean;
18
27
  }): Promise<{ logs: Log[] }> {
19
28
  if (!filter.logs || filter.logs.length === 0) {
20
29
  return { logs: [] };
21
30
  }
22
31
 
23
- const responses = await Promise.all(
24
- filter.logs.map(async (logFilter) => {
25
- const logs = await client.request({
26
- method: "eth_getLogs",
27
- params: [
28
- {
29
- blockHash,
30
- address: logFilter.address,
31
- topics: logFilter.topics ? [...logFilter.topics] : undefined,
32
- },
33
- ],
32
+ const responses = mergeGetLogs
33
+ ? await mergedGetLogsCalls({
34
+ client,
35
+ filter,
36
+ blockHash,
37
+ })
38
+ : await standardGetLogsCalls({
39
+ client,
40
+ filter,
41
+ blockHash,
34
42
  });
35
- return { logs, logFilter };
36
- }),
37
- );
38
43
 
39
- // Multiple calls may have produced the same log.
40
- // We track all the logs (by their logIndex, which is unique within a block).
41
- // logIndex -> position
42
44
  const allLogs: Log[] = [];
43
45
  const seenLogsByIndex: Record<number, number> = {};
44
46
 
45
- for (const { logFilter, logs } of responses) {
47
+ for (const { logFilters, logs } of responses) {
46
48
  for (const log of logs) {
47
49
  if (log.blockNumber === null) {
48
50
  throw new Error("Log block number is null");
49
51
  }
50
52
 
51
- const refinedLog = refineLog(log, logFilter);
53
+ for (const logFilter of logFilters) {
54
+ const refinedLog = refineLog(log, logFilter);
52
55
 
53
- if (refinedLog) {
54
- const existingPosition = seenLogsByIndex[refinedLog.logIndex];
56
+ if (refinedLog) {
57
+ const existingPosition = seenLogsByIndex[refinedLog.logIndex];
55
58
 
56
- if (existingPosition !== undefined) {
57
- const existingLog = allLogs[existingPosition];
58
- (existingLog.filterIds as number[]).push(logFilter.id ?? 0);
59
- } else {
60
- (refinedLog.filterIds as number[]).push(logFilter.id ?? 0);
59
+ if (existingPosition !== undefined) {
60
+ const existingLog = allLogs[existingPosition];
61
+ (existingLog.filterIds as number[]).push(logFilter.id ?? 0);
62
+ } else {
63
+ (refinedLog.filterIds as number[]).push(logFilter.id ?? 0);
61
64
 
62
- allLogs.push(refinedLog);
63
- seenLogsByIndex[refinedLog.logIndex] = allLogs.length - 1;
65
+ allLogs.push(refinedLog);
66
+ seenLogsByIndex[refinedLog.logIndex] = allLogs.length - 1;
67
+ }
64
68
  }
65
69
  }
66
70
  }
@@ -74,11 +78,13 @@ export async function fetchLogsForRange({
74
78
  fromBlock,
75
79
  toBlock,
76
80
  filter,
81
+ mergeGetLogs,
77
82
  }: {
78
83
  client: ViemRpcClient;
79
84
  fromBlock: bigint;
80
85
  toBlock: bigint;
81
86
  filter: Filter;
87
+ mergeGetLogs: boolean;
82
88
  }): Promise<{ logs: Record<number, Log[]>; blockNumbers: bigint[] }> {
83
89
  const logsByBlock: Record<number, Log[]> = {};
84
90
 
@@ -86,68 +92,61 @@ export async function fetchLogsForRange({
86
92
  return { logs: logsByBlock, blockNumbers: [] };
87
93
  }
88
94
 
89
- const responses = await Promise.all(
90
- filter.logs.map(async (logFilter) => {
91
- const logs = await client.request({
92
- method: "eth_getLogs",
93
- params: [
94
- {
95
- fromBlock: numberToHex(fromBlock),
96
- toBlock: numberToHex(toBlock),
97
- address: logFilter.address,
98
- topics:
99
- logFilter.topics !== undefined
100
- ? [...logFilter.topics]
101
- : undefined,
102
- },
103
- ],
95
+ const responses = mergeGetLogs
96
+ ? await mergedGetLogsCalls({
97
+ client,
98
+ filter,
99
+ fromBlock: numberToHex(fromBlock),
100
+ toBlock: numberToHex(toBlock),
101
+ })
102
+ : await standardGetLogsCalls({
103
+ client,
104
+ filter,
105
+ fromBlock: numberToHex(fromBlock),
106
+ toBlock: numberToHex(toBlock),
104
107
  });
105
- return { logs, logFilter };
106
- }),
107
- );
108
108
 
109
109
  const blockNumbers = new Set<bigint>();
110
110
 
111
- // Multiple calls may have produced the same log.
112
- // We track all the logs (by their logIndex, which is unique within a block).
113
- // blockNumber -> logIndex -> position
114
111
  const seenLogsByBlockNumberAndIndex: Record<
115
112
  number,
116
113
  Record<number, number>
117
114
  > = {};
118
115
 
119
- for (const { logFilter, logs } of responses) {
116
+ for (const { logFilters, logs } of responses) {
120
117
  for (const log of logs) {
121
118
  if (log.blockNumber === null) {
122
119
  throw new Error("Log block number is null");
123
120
  }
124
121
 
125
- const refinedLog = refineLog(log, logFilter);
122
+ for (const logFilter of logFilters) {
123
+ const refinedLog = refineLog(log, logFilter);
126
124
 
127
- if (refinedLog) {
128
- const blockNumber = hexToNumber(log.blockNumber);
129
- blockNumbers.add(BigInt(blockNumber));
125
+ if (refinedLog) {
126
+ const blockNumber = hexToNumber(log.blockNumber);
127
+ blockNumbers.add(BigInt(blockNumber));
130
128
 
131
- if (!logsByBlock[blockNumber]) {
132
- logsByBlock[blockNumber] = [];
133
- }
129
+ if (!logsByBlock[blockNumber]) {
130
+ logsByBlock[blockNumber] = [];
131
+ }
134
132
 
135
- if (!seenLogsByBlockNumberAndIndex[blockNumber]) {
136
- seenLogsByBlockNumberAndIndex[blockNumber] = {};
137
- }
133
+ if (!seenLogsByBlockNumberAndIndex[blockNumber]) {
134
+ seenLogsByBlockNumberAndIndex[blockNumber] = {};
135
+ }
138
136
 
139
- const existingPosition =
140
- seenLogsByBlockNumberAndIndex[blockNumber][refinedLog.logIndex];
137
+ const existingPosition =
138
+ seenLogsByBlockNumberAndIndex[blockNumber][refinedLog.logIndex];
141
139
 
142
- if (existingPosition !== undefined) {
143
- const existingLog = logsByBlock[blockNumber][existingPosition];
144
- (existingLog.filterIds as number[]).push(logFilter.id ?? 0);
145
- } else {
146
- (refinedLog.filterIds as number[]).push(logFilter.id ?? 0);
140
+ if (existingPosition !== undefined) {
141
+ const existingLog = logsByBlock[blockNumber][existingPosition];
142
+ (existingLog.filterIds as number[]).push(logFilter.id ?? 0);
143
+ } else {
144
+ (refinedLog.filterIds as number[]).push(logFilter.id ?? 0);
147
145
 
148
- logsByBlock[blockNumber].push(refinedLog);
149
- seenLogsByBlockNumberAndIndex[blockNumber][refinedLog.logIndex] =
150
- logsByBlock[blockNumber].length - 1;
146
+ logsByBlock[blockNumber].push(refinedLog);
147
+ seenLogsByBlockNumberAndIndex[blockNumber][refinedLog.logIndex] =
148
+ logsByBlock[blockNumber].length - 1;
149
+ }
151
150
  }
152
151
  }
153
152
  }
@@ -165,8 +164,11 @@ function refineLog(log: RpcLog, filter: LogFilter): Log | null {
165
164
  return null;
166
165
  }
167
166
 
167
+ if (filter.address && !isAddressEqual(log.address, filter.address)) {
168
+ return null;
169
+ }
170
+
168
171
  const filterTopics = filter.topics ?? [];
169
- // Strict mode
170
172
  if (filter.strict && log.topics.length !== filterTopics.length) {
171
173
  return null;
172
174
  }
@@ -201,3 +203,102 @@ function refineLog(log: RpcLog, filter: LogFilter): Log | null {
201
203
 
202
204
  return viemRpcLogToDna(log);
203
205
  }
206
+
207
+ async function mergedGetLogsCalls({
208
+ client,
209
+ filter,
210
+ blockHash,
211
+ fromBlock,
212
+ toBlock,
213
+ }: {
214
+ client: ViemRpcClient;
215
+ filter: Filter;
216
+ blockHash?: Bytes;
217
+ fromBlock?: `0x${string}`;
218
+ toBlock?: `0x${string}`;
219
+ }) {
220
+ const blockParams = blockHash ? { blockHash } : { fromBlock, toBlock };
221
+
222
+ const filtersWithAddress = filter.logs.filter((f) => f.address !== undefined);
223
+ const filtersWithoutAddress = filter.logs.filter(
224
+ (f) => f.address === undefined,
225
+ );
226
+
227
+ const promises: Promise<{
228
+ logs: RpcLog[];
229
+ logFilters: typeof filter.logs;
230
+ }>[] = [];
231
+
232
+ if (filtersWithAddress.length > 0) {
233
+ const addresses = filtersWithAddress.map((f) => f.address!);
234
+
235
+ promises.push(
236
+ client
237
+ .request({
238
+ method: "eth_getLogs",
239
+ params: [
240
+ {
241
+ address: addresses,
242
+ ...blockParams,
243
+ },
244
+ ],
245
+ })
246
+ .then((logs: RpcLog[]) => ({ logs, logFilters: filtersWithAddress })),
247
+ );
248
+ }
249
+
250
+ if (filtersWithoutAddress.length > 0) {
251
+ promises.push(
252
+ client
253
+ .request({
254
+ method: "eth_getLogs",
255
+ params: [
256
+ {
257
+ ...blockParams,
258
+ },
259
+ ],
260
+ })
261
+ .then((logs: RpcLog[]) => ({
262
+ logs,
263
+ logFilters: filtersWithoutAddress,
264
+ })),
265
+ );
266
+ }
267
+
268
+ return await Promise.all(promises);
269
+ }
270
+
271
+ async function standardGetLogsCalls({
272
+ client,
273
+ filter,
274
+ blockHash,
275
+ fromBlock,
276
+ toBlock,
277
+ }: {
278
+ client: ViemRpcClient;
279
+ filter: Filter;
280
+ blockHash?: Bytes;
281
+ fromBlock?: `0x${string}`;
282
+ toBlock?: `0x${string}`;
283
+ }) {
284
+ const blockParams = blockHash ? { blockHash } : { fromBlock, toBlock };
285
+ return await Promise.all(
286
+ filter.logs.map(async (logFilter) => {
287
+ const logs = await client.request({
288
+ method: "eth_getLogs",
289
+ params: [
290
+ {
291
+ address: logFilter.address,
292
+ topics:
293
+ logFilter.topics !== undefined
294
+ ? [...logFilter.topics]
295
+ : undefined,
296
+ ...blockParams,
297
+ },
298
+ ],
299
+ });
300
+
301
+ return { logs, logFilters: [logFilter] };
302
+ }),
303
+ );
304
+ }
package/src/retry.ts ADDED
@@ -0,0 +1,22 @@
1
+ export async function retry<T>({
2
+ fn,
3
+ maxAttempts = 3,
4
+ delay = 100,
5
+ }: {
6
+ fn: () => Promise<T>;
7
+ maxAttempts?: number;
8
+ delay?: number;
9
+ }): Promise<T> {
10
+ let attempts = 0;
11
+
12
+ while (true) {
13
+ try {
14
+ return await fn();
15
+ } catch (error) {
16
+ attempts++;
17
+ if (attempts >= maxAttempts) throw error;
18
+ console.warn(`RPC error ${error}. Retrying in ${delay}ms`);
19
+ await new Promise((resolve) => setTimeout(resolve, delay));
20
+ }
21
+ }
22
+ }
@@ -22,6 +22,7 @@ import type { Block, Log } from "./block";
22
22
  import { type Filter, validateFilter } from "./filter";
23
23
  import { fetchLogsByBlockHash, fetchLogsForRange } from "./log-fetcher";
24
24
  import { type BlockRangeOracle, createBlockRangeOracle } from "./range-oracle";
25
+ import { retry } from "./retry";
25
26
  import { rpcBlockHeaderToDna } from "./transform";
26
27
 
27
28
  export type RequestParameters = EIP1193Parameters<PublicRpcSchema>;
@@ -45,6 +46,8 @@ export type EvmRpcStreamOptions = {
45
46
  finalizedRefreshIntervalMs?: number;
46
47
  /** Force sending accepted headers even if no data matched. */
47
48
  alwaysSendAcceptedHeaders?: boolean;
49
+ /** Merge multiple `eth_getLogs` calls into a single one, filtering data on the client. */
50
+ mergeGetLogsFilter?: "always" | "accepted" | false;
48
51
  };
49
52
 
50
53
  export class EvmRpcStream extends RpcStreamConfig<Filter, Block> {
@@ -269,13 +272,16 @@ export class EvmRpcStream extends RpcStreamConfig<Filter, Block> {
269
272
  toBlock: bigint;
270
273
  filter: Filter;
271
274
  }): Promise<{ logs: Record<number, Log[]>; blockNumbers: bigint[] }> {
272
- // TODO: implement retry
273
275
  try {
274
- return await fetchLogsForRange({
275
- client: this.client,
276
- fromBlock,
277
- toBlock,
278
- filter,
276
+ return await retry({
277
+ fn: () =>
278
+ fetchLogsForRange({
279
+ client: this.client,
280
+ fromBlock,
281
+ toBlock,
282
+ filter,
283
+ mergeGetLogs: this.options.mergeGetLogsFilter === "always",
284
+ }),
279
285
  });
280
286
  } catch (error) {
281
287
  this.blockRangeOracle.handleError(error);
@@ -290,11 +296,16 @@ export class EvmRpcStream extends RpcStreamConfig<Filter, Block> {
290
296
  blockHash: Bytes;
291
297
  filter: Filter;
292
298
  }): Promise<{ logs: Log[] }> {
293
- // TODO: implement retry
294
- return await fetchLogsByBlockHash({
295
- client: this.client,
296
- blockHash,
297
- filter,
299
+ return await retry({
300
+ fn: () =>
301
+ fetchLogsByBlockHash({
302
+ client: this.client,
303
+ blockHash,
304
+ filter,
305
+ mergeGetLogs:
306
+ this.options.mergeGetLogsFilter === "always" ||
307
+ this.options.mergeGetLogsFilter === "accepted",
308
+ }),
298
309
  });
299
310
  }
300
311
 
@@ -303,10 +314,12 @@ export class EvmRpcStream extends RpcStreamConfig<Filter, Block> {
303
314
  }: {
304
315
  blockNumber: bigint;
305
316
  }) {
306
- // TODO: implement retry
307
- const block = await this.client.request({
308
- method: "eth_getBlockByNumber",
309
- params: [numberToHex(blockNumber), false],
317
+ const block = await retry({
318
+ fn: () =>
319
+ this.client.request({
320
+ method: "eth_getBlockByNumber",
321
+ params: [numberToHex(blockNumber), false],
322
+ }),
310
323
  });
311
324
 
312
325
  if (block === null) {