ponder 0.17.6 → 0.17.8

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,4 +1,4 @@
1
- import { type Address, hexToNumber } from "viem";
1
+ import { type Address, type Hex, hexToNumber, numberToHex } from "viem";
2
2
  import type {
3
3
  BlockFilter,
4
4
  Factory,
@@ -23,6 +23,8 @@ import type {
23
23
  TransactionFilter,
24
24
  TransferFilter,
25
25
  } from "@/internal/types.js";
26
+ import { type RequestParameters, sanitizeLogTopics } from "@/rpc/index.js";
27
+ import type { ChildAddresses, IntervalWithFilter } from "@/runtime/index.js";
26
28
  import type {
27
29
  Block,
28
30
  Log,
@@ -30,6 +32,7 @@ import type {
30
32
  Transaction,
31
33
  TransactionReceipt,
32
34
  } from "@/types/eth.js";
35
+ import type { Interval } from "@/utils/interval.js";
33
36
  import { toLowerCase } from "@/utils/lowercase.js";
34
37
 
35
38
  /** Returns true if `address` is an address filter. */
@@ -46,6 +49,179 @@ export const isAddressFactory = (
46
49
  return Array.isArray(address) ? isAddressFactory(address[0]) : true;
47
50
  };
48
51
 
52
+ type EthGetLogsRequest = Extract<
53
+ RequestParameters,
54
+ { method: "eth_getLogs" }
55
+ > & {
56
+ params: [
57
+ Extract<
58
+ Extract<RequestParameters, { method: "eth_getLogs" }>["params"][0],
59
+ { blockHash?: undefined }
60
+ > & { fromBlock: Hex; toBlock: Hex },
61
+ ];
62
+ };
63
+
64
+ const logRequestKeys = [
65
+ "address",
66
+ "topic0",
67
+ "topic1",
68
+ "topic2",
69
+ "topic3",
70
+ ] as const;
71
+
72
+ const getLogRequestValue = (
73
+ request: EthGetLogsRequest,
74
+ key: (typeof logRequestKeys)[number],
75
+ ): Hex | Hex[] | undefined => {
76
+ if (key === "address") return request.params[0].address;
77
+
78
+ const topic = request.params[0].topics?.[Number(key.slice(-1))];
79
+ return topic === null ? undefined : topic;
80
+ };
81
+
82
+ /** Returns `true` if `left` and `right` overlap by at least 80% of the smaller interval's length. */
83
+ const hasSufficientLogIntervalOverlap = (
84
+ left: Interval,
85
+ right: Interval,
86
+ ): boolean => {
87
+ const overlapStart = Math.max(left[0], right[0]);
88
+ const overlapEnd = Math.min(left[1], right[1]);
89
+ if (overlapStart > overlapEnd) return false;
90
+
91
+ const overlapLength = overlapEnd - overlapStart + 1;
92
+ const smallerIntervalLength = Math.min(
93
+ left[1] - left[0] + 1,
94
+ right[1] - right[0] + 1,
95
+ );
96
+
97
+ return overlapLength / smallerIntervalLength >= 0.8;
98
+ };
99
+
100
+ const mergeLogFilterValue = <value extends Hex>(
101
+ left: value | value[] | undefined,
102
+ right: value | value[] | undefined,
103
+ ): value[] | undefined =>
104
+ left === undefined || right === undefined
105
+ ? undefined
106
+ : [
107
+ ...new Set([
108
+ ...(Array.isArray(left) ? left : [left]),
109
+ ...(Array.isArray(right) ? right : [right]),
110
+ ]),
111
+ ];
112
+
113
+ /**
114
+ * Merge `LogFilter`s into the smallest possible set of "eth_getLogs" requests.
115
+ *
116
+ * @dev Two filters are merged into a single request when:
117
+ * 1. their required intervals overlap by at least 80% (of the smaller
118
+ * interval's length), and
119
+ * 2. at most one of `address`, `topic0`, `topic1`, `topic2`, or `topic3`
120
+ * differs between them.
121
+ * The one differing dimension (if any) is unioned into an array.
122
+ *
123
+ * @dev Factory addresses are resolved into concrete child addresses (or
124
+ * `undefined`, above `factoryAddressCountThreshold`) before merging, so
125
+ * requests originating from different factories can still be merged.
126
+ */
127
+ export const mergeLogFiltersToRequests = (
128
+ filters: IntervalWithFilter[],
129
+ childAddresses: ChildAddresses,
130
+ factoryAddressCountThreshold: number,
131
+ ): EthGetLogsRequest[] => {
132
+ const requests: EthGetLogsRequest[] = [];
133
+
134
+ for (const { filter, interval } of filters) {
135
+ if (filter.type !== "log") continue;
136
+
137
+ let address: Hex | Hex[] | undefined;
138
+ if (isAddressFactory(filter.address)) {
139
+ const addresses = childAddresses.get(filter.address.id)!;
140
+ if (addresses.size === 0) continue;
141
+
142
+ address =
143
+ addresses.size >= factoryAddressCountThreshold
144
+ ? undefined
145
+ : Array.from(addresses.keys());
146
+ } else {
147
+ address = filter.address;
148
+ }
149
+
150
+ const candidate: EthGetLogsRequest = {
151
+ method: "eth_getLogs",
152
+ params: [
153
+ {
154
+ address,
155
+ topics: sanitizeLogTopics([
156
+ filter.topic0,
157
+ filter.topic1 ?? null,
158
+ filter.topic2 ?? null,
159
+ filter.topic3 ?? null,
160
+ ]),
161
+ fromBlock: numberToHex(interval[0]),
162
+ toBlock: numberToHex(interval[1]),
163
+ },
164
+ ],
165
+ };
166
+
167
+ let isMerged = false;
168
+
169
+ for (const request of requests) {
170
+ if (
171
+ hasSufficientLogIntervalOverlap(
172
+ [
173
+ hexToNumber(request.params[0].fromBlock),
174
+ hexToNumber(request.params[0].toBlock),
175
+ ],
176
+ interval,
177
+ ) === false
178
+ ) {
179
+ continue;
180
+ }
181
+
182
+ const numberOfDifferences = logRequestKeys.filter(
183
+ (key) =>
184
+ JSON.stringify(getLogRequestValue(request, key)) !==
185
+ JSON.stringify(getLogRequestValue(candidate, key)),
186
+ ).length;
187
+ if (numberOfDifferences > 1) continue;
188
+
189
+ const requestParams = request.params[0];
190
+ for (const key of logRequestKeys) {
191
+ const requestValue = getLogRequestValue(request, key);
192
+ const candidateValue = getLogRequestValue(candidate, key);
193
+ if (JSON.stringify(requestValue) === JSON.stringify(candidateValue)) {
194
+ continue;
195
+ }
196
+
197
+ const mergedValue = mergeLogFilterValue(requestValue, candidateValue);
198
+ if (key === "address") {
199
+ requestParams.address = mergedValue;
200
+ } else {
201
+ const topics = [...(requestParams.topics ?? [])];
202
+ topics[Number(key.slice(-1))] = mergedValue ?? null;
203
+ requestParams.topics = sanitizeLogTopics(topics);
204
+ }
205
+ }
206
+
207
+ requestParams.fromBlock = numberToHex(
208
+ Math.min(hexToNumber(requestParams.fromBlock), interval[0]),
209
+ );
210
+ requestParams.toBlock = numberToHex(
211
+ Math.max(hexToNumber(requestParams.toBlock), interval[1]),
212
+ );
213
+ isMerged = true;
214
+ break;
215
+ }
216
+
217
+ if (isMerged === false) {
218
+ requests.push(candidate);
219
+ }
220
+ }
221
+
222
+ return requests;
223
+ };
224
+
49
225
  export const getChildAddress = ({
50
226
  log,
51
227
  factory,
@@ -4,7 +4,6 @@ import {
4
4
  type Hash,
5
5
  type Hex,
6
6
  hexToNumber,
7
- type LogTopic,
8
7
  numberToHex,
9
8
  type RpcError,
10
9
  toHex,
@@ -37,7 +36,7 @@ import {
37
36
  validateTracesAndBlock,
38
37
  validateTransactionsAndBlock,
39
38
  } from "@/rpc/actions.js";
40
- import { type Rpc, sanitizeLogTopics } from "@/rpc/index.js";
39
+ import type { RequestParameters, Rpc } from "@/rpc/index.js";
41
40
  import {
42
41
  getChildAddress,
43
42
  isAddressFactory,
@@ -49,6 +48,7 @@ import {
49
48
  isTraceFilterMatched,
50
49
  isTransactionFilterMatched,
51
50
  isTransferFilterMatched,
51
+ mergeLogFiltersToRequests,
52
52
  } from "@/runtime/filter.js";
53
53
  import type {
54
54
  ChildAddresses,
@@ -79,6 +79,7 @@ export type HistoricalSync = {
79
79
  }): Promise<SyncLog[]>;
80
80
  /**
81
81
  * Sync block data that must be queried for a single block (block, transactions, receipts, traces).
82
+ * @returns Closest-to-tip synced block.
82
83
  */
83
84
  syncBlockData(params: {
84
85
  interval: Interval;
@@ -120,48 +121,37 @@ export const createHistoricalSync = (
120
121
  // Helper functions for sync tasks
121
122
  ////////
122
123
 
123
- type EthGetLogsParams = {
124
- address: Address | Address[] | undefined;
125
- topic0?: LogTopic;
126
- topic1?: LogTopic;
127
- topic2?: LogTopic;
128
- topic3?: LogTopic;
129
- interval: Interval;
130
- };
131
-
132
124
  /**
133
125
  * Split "eth_getLogs" requests into ranges inferred from errors
134
126
  * and batch requests.
135
127
  */
136
128
  const syncLogsDynamic = async (
137
- { address, topic0, topic1, topic2, topic3, interval }: EthGetLogsParams,
129
+ params: Extract<RequestParameters, { method: "eth_getLogs" }>["params"][0],
138
130
  context?: Parameters<Rpc["request"]>[1],
139
131
  ): Promise<SyncLog[]> => {
132
+ const { address, topics } = params;
133
+
140
134
  const intervals = getChunks({
141
- interval,
135
+ interval: [
136
+ hexToNumber(params.fromBlock as Hex),
137
+ hexToNumber(params.toBlock as Hex),
138
+ ],
142
139
  maxChunkSize:
143
140
  args.chain.ethGetLogsBlockRange ??
144
141
  logsRequestMetadata.confirmedRange ??
145
142
  logsRequestMetadata.estimatedRange,
146
143
  });
147
144
 
148
- const topics = sanitizeLogTopics([
149
- topic0 ?? null,
150
- topic1 ?? null,
151
- topic2 ?? null,
152
- topic3 ?? null,
153
- ])!;
154
-
155
145
  const logs = await Promise.all(
156
- intervals.map((interval) =>
146
+ intervals.map(([fromBlock, toBlock]) =>
157
147
  eth_getLogs(
158
148
  args.rpc,
159
149
  [
160
150
  {
161
151
  address,
162
152
  topics,
163
- fromBlock: numberToHex(interval[0]),
164
- toBlock: numberToHex(interval[1]),
153
+ fromBlock: numberToHex(fromBlock),
154
+ toBlock: numberToHex(toBlock),
165
155
  },
166
156
  ],
167
157
  context,
@@ -177,8 +167,8 @@ export const createHistoricalSync = (
177
167
  {
178
168
  address,
179
169
  topics,
180
- fromBlock: toHex(interval[0]),
181
- toBlock: toHex(interval[1]),
170
+ fromBlock: toHex(fromBlock),
171
+ toBlock: toHex(toBlock),
182
172
  },
183
173
  ],
184
174
  error: error as RpcError,
@@ -205,7 +195,12 @@ export const createHistoricalSync = (
205
195
  };
206
196
 
207
197
  return syncLogsDynamic(
208
- { address, topic0, topic1, topic2, topic3, interval },
198
+ {
199
+ address,
200
+ topics,
201
+ fromBlock: numberToHex(fromBlock),
202
+ toBlock: numberToHex(toBlock),
203
+ },
209
204
  context,
210
205
  );
211
206
  }),
@@ -318,14 +313,15 @@ export const createHistoricalSync = (
318
313
  const logs = await syncLogsDynamic(
319
314
  {
320
315
  address: factory.address,
321
- topic0: factory.eventSelector,
322
- interval,
316
+ topics: [factory.eventSelector],
317
+ fromBlock: numberToHex(interval[0]),
318
+ toBlock: numberToHex(interval[1]),
323
319
  },
324
320
  context,
325
321
  );
326
322
 
327
323
  const childAddresses = new Map<Address, number>();
328
- const childAddressesRecord = args.childAddresses.get(factory.id)!;
324
+ const factoryChildAddresses = args.childAddresses.get(factory.id)!;
329
325
 
330
326
  const childAddressDecodeFailureIds = new Set<string>();
331
327
  let childAddressDecodeFailureCount = 0;
@@ -358,7 +354,7 @@ export const createHistoricalSync = (
358
354
  throw error;
359
355
  }
360
356
  }
361
- const existingBlockNumber = childAddressesRecord.get(address);
357
+ const existingBlockNumber = factoryChildAddresses.get(address);
362
358
  const newBlockNumber = hexToNumber(log.blockNumber);
363
359
 
364
360
  if (
@@ -366,7 +362,7 @@ export const createHistoricalSync = (
366
362
  existingBlockNumber > newBlockNumber
367
363
  ) {
368
364
  childAddresses.set(address, newBlockNumber);
369
- childAddressesRecord.set(address, newBlockNumber);
365
+ factoryChildAddresses.set(address, newBlockNumber);
370
366
  }
371
367
  }
372
368
  }
@@ -428,132 +424,17 @@ export const createHistoricalSync = (
428
424
  }),
429
425
  );
430
426
 
431
- const mergedEthGetLogsParams: Map<string, EthGetLogsParams> = new Map();
432
- const singleEthGetLogsParams: EthGetLogsParams[] = [];
433
-
434
- for (const { filter, interval } of requiredIntervals) {
435
- if (filter.type !== "log") continue;
436
-
437
- const hasAddress = filter.address !== undefined;
438
- const hasTopic1 = filter.topic1 !== undefined && filter.topic1 !== null;
439
- const hasTopic2 = filter.topic2 !== undefined && filter.topic2 !== null;
440
- const hasTopic3 = filter.topic3 !== undefined && filter.topic3 !== null;
441
-
442
- if (hasAddress === false || hasTopic1 || hasTopic2 || hasTopic3) {
443
- if (isAddressFactory(filter.address)) {
444
- const childAddresses = args.childAddresses.get(filter.address.id)!;
445
-
446
- if (childAddresses.size === 0) {
447
- continue;
448
- }
449
-
450
- singleEthGetLogsParams.push({
451
- address:
452
- childAddresses.size >=
453
- args.common.options.factoryAddressCountThreshold
454
- ? undefined
455
- : Array.from(childAddresses.keys()),
456
- topic0: filter.topic0,
457
- topic1: filter.topic1,
458
- topic2: filter.topic2,
459
- topic3: filter.topic3,
460
- interval,
461
- });
462
- } else {
463
- singleEthGetLogsParams.push({
464
- address: filter.address,
465
- topic0: filter.topic0,
466
- topic1: filter.topic1,
467
- topic2: filter.topic2,
468
- topic3: filter.topic3,
469
- interval,
470
- });
471
- }
472
-
473
- continue;
474
- }
475
-
476
- let addressKey: string;
477
- if (isAddressFactory(filter.address)) {
478
- addressKey = filter.address.id;
479
- } else if (Array.isArray(filter.address)) {
480
- addressKey = filter.address.join("_");
481
- } else {
482
- addressKey = filter.address as Address;
483
- }
484
-
485
- if (mergedEthGetLogsParams.has(addressKey) === false) {
486
- if (isAddressFactory(filter.address)) {
487
- const childAddresses = args.childAddresses.get(filter.address.id)!;
488
-
489
- if (childAddresses.size === 0) {
490
- continue;
491
- }
492
-
493
- mergedEthGetLogsParams.set(addressKey, {
494
- address:
495
- childAddresses.size >=
496
- args.common.options.factoryAddressCountThreshold
497
- ? undefined
498
- : Array.from(childAddresses.keys()),
499
- topic0: filter.topic0,
500
- topic1: filter.topic1,
501
- topic2: filter.topic2,
502
- topic3: filter.topic3,
503
- interval,
504
- });
505
- } else {
506
- mergedEthGetLogsParams.set(addressKey, {
507
- address: filter.address,
508
- topic0: filter.topic0,
509
- topic1: filter.topic1,
510
- topic2: filter.topic2,
511
- topic3: filter.topic3,
512
- interval,
513
- });
514
- }
515
- } else {
516
- const existingInterval =
517
- mergedEthGetLogsParams.get(addressKey)!.interval;
518
- const existingTopic0 = mergedEthGetLogsParams.get(addressKey)!
519
- .topic0 as Hex | Hex[];
520
-
521
- if (Array.isArray(existingTopic0)) {
522
- if (existingTopic0.includes(filter.topic0) === false) {
523
- mergedEthGetLogsParams.get(addressKey)!.topic0 = [
524
- ...existingTopic0,
525
- filter.topic0,
526
- ];
527
- }
528
- } else {
529
- if (existingTopic0 !== filter.topic0) {
530
- mergedEthGetLogsParams.get(addressKey)!.topic0 = [
531
- existingTopic0,
532
- filter.topic0,
533
- ];
534
- }
535
- }
536
-
537
- mergedEthGetLogsParams.get(addressKey)!.interval = intervalBounds([
538
- existingInterval,
539
- interval,
540
- ]);
541
- }
542
- }
543
-
544
- const ethGetLogsParams = dedupe(
545
- [
546
- ...singleEthGetLogsParams,
547
- ...Array.from(mergedEthGetLogsParams.values()),
548
- ],
549
- (params) => JSON.stringify(params),
427
+ const ethGetLogsRequests = mergeLogFiltersToRequests(
428
+ requiredIntervals,
429
+ args.childAddresses,
430
+ args.common.options.factoryAddressCountThreshold,
550
431
  );
551
432
 
552
433
  let logs: SyncLog[] = [];
553
434
 
554
435
  await Promise.all(
555
- ethGetLogsParams.map(async (params) => {
556
- const _logs = await syncLogsDynamic(params, context);
436
+ ethGetLogsRequests.map(async (request) => {
437
+ const _logs = await syncLogsDynamic(request.params[0], context);
557
438
  for (const log of _logs) {
558
439
  logs.push(log);
559
440
  }
@@ -351,11 +351,15 @@ export const createSyncStore = ({
351
351
  const batchSize = 200;
352
352
 
353
353
  for (let i = 0; i < queries.length; i += batchSize) {
354
+ const batch = queries.slice(i, i + batchSize);
354
355
  const _rows = await qb.wrap(
355
356
  { label: "select_intervals" },
356
- () =>
357
+ () => {
358
+ if (batch.length === 1) return batch[0]!.execute();
359
+
357
360
  // @ts-expect-error
358
- unionAll(...queries.slice(i, i + batchSize)),
361
+ return unionAll(...batch);
362
+ },
359
363
  context,
360
364
  );
361
365