ponder 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/esm/indexing-store/index.js +8 -4
- package/dist/esm/indexing-store/index.js.map +1 -1
- package/dist/esm/indexing-store/utils.js +3 -1
- package/dist/esm/indexing-store/utils.js.map +1 -1
- package/dist/esm/runtime/filter.js +10 -12
- package/dist/esm/runtime/filter.js.map +1 -1
- package/dist/esm/sync-realtime/bloom.js +66 -56
- package/dist/esm/sync-realtime/bloom.js.map +1 -1
- package/dist/esm/sync-realtime/index.js +64 -43
- package/dist/esm/sync-realtime/index.js.map +1 -1
- package/dist/esm/utils/copy.js +77 -56
- package/dist/esm/utils/copy.js.map +1 -1
- package/dist/esm/utils/decodeEventLog.js +20 -6
- package/dist/esm/utils/decodeEventLog.js.map +1 -1
- package/dist/types/indexing-store/index.d.ts.map +1 -1
- package/dist/types/indexing-store/utils.d.ts.map +1 -1
- package/dist/types/runtime/filter.d.ts.map +1 -1
- package/dist/types/sync-realtime/bloom.d.ts +36 -5
- package/dist/types/sync-realtime/bloom.d.ts.map +1 -1
- package/dist/types/sync-realtime/index.d.ts.map +1 -1
- package/dist/types/utils/copy.d.ts +10 -2
- package/dist/types/utils/copy.d.ts.map +1 -1
- package/dist/types/utils/decodeEventLog.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/indexing-store/index.ts +8 -4
- package/src/indexing-store/utils.ts +3 -1
- package/src/runtime/filter.ts +11 -17
- package/src/sync-realtime/bloom.ts +116 -67
- package/src/sync-realtime/index.ts +87 -58
- package/src/utils/copy.ts +83 -64
- package/src/utils/decodeEventLog.ts +28 -6
package/src/runtime/filter.ts
CHANGED
|
@@ -260,14 +260,8 @@ export const isAddressMatched = ({
|
|
|
260
260
|
childAddresses: Map<Address, number>;
|
|
261
261
|
}) => {
|
|
262
262
|
if (address === undefined) return false;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
childAddresses.get(toLowerCase(address))! <= blockNumber
|
|
266
|
-
) {
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return false;
|
|
263
|
+
const childBlockNumber = childAddresses.get(toLowerCase(address));
|
|
264
|
+
return childBlockNumber !== undefined && childBlockNumber <= blockNumber;
|
|
271
265
|
};
|
|
272
266
|
|
|
273
267
|
const isValueMatched = <T extends string>(
|
|
@@ -280,18 +274,18 @@ const isValueMatched = <T extends string>(
|
|
|
280
274
|
// missing value
|
|
281
275
|
if (eventValue === undefined) return false;
|
|
282
276
|
|
|
277
|
+
// Note: `filterValue` is lowercase, so an exact match avoids allocating
|
|
278
|
+
// a lowercase copy of `eventValue`.
|
|
279
|
+
if (filterValue === eventValue) return true;
|
|
280
|
+
|
|
281
|
+
const value = toLowerCase(eventValue);
|
|
282
|
+
|
|
283
283
|
// array
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
filterValue.some((v) => v === toLowerCase(eventValue))
|
|
287
|
-
) {
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
284
|
+
if (Array.isArray(filterValue))
|
|
285
|
+
return (filterValue as string[]).includes(value);
|
|
290
286
|
|
|
291
287
|
// single
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return false;
|
|
288
|
+
return filterValue === value;
|
|
295
289
|
};
|
|
296
290
|
|
|
297
291
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Hex,
|
|
2
|
-
import type { LogFilter
|
|
1
|
+
import { type Hex, keccak256 } from "viem";
|
|
2
|
+
import type { LogFilter } from "@/internal/types.js";
|
|
3
3
|
import {
|
|
4
4
|
getFilterFromBlock,
|
|
5
5
|
getFilterToBlock,
|
|
@@ -11,21 +11,105 @@ export const zeroLogsBloom =
|
|
|
11
11
|
|
|
12
12
|
const BLOOM_SIZE_BYTES = 256;
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
export type BloomBits = {
|
|
15
|
+
byte0: number;
|
|
16
|
+
mask0: number;
|
|
17
|
+
byte1: number;
|
|
18
|
+
mask1: number;
|
|
19
|
+
byte2: number;
|
|
20
|
+
mask2: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A `LogFilter` with each address and topic converted to `BloomBits`.
|
|
25
|
+
*
|
|
26
|
+
* `undefined` matches every bloom. An array matches if any item is in the bloom.
|
|
27
|
+
*/
|
|
28
|
+
export type LogFilterBloom = {
|
|
29
|
+
fromBlock: number;
|
|
30
|
+
toBlock: number;
|
|
31
|
+
address: BloomBits[] | undefined;
|
|
32
|
+
topics: (BloomBits[] | undefined)[];
|
|
33
|
+
factory:
|
|
34
|
+
| { address: BloomBits[] | undefined; eventSelector: BloomBits }
|
|
35
|
+
| undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getBloomBits = (input: Hex): BloomBits => {
|
|
39
|
+
const hash = keccak256(input, "bytes");
|
|
40
|
+
const bytes = [0, 0, 0];
|
|
41
|
+
const masks = [0, 0, 0];
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < 3; i++) {
|
|
44
|
+
const bit = (hash[i * 2 + 1]! + (hash[i * 2]! << 8)) & 0x7ff;
|
|
45
|
+
bytes[i] = BLOOM_SIZE_BYTES - 1 - Math.floor(bit / 8);
|
|
46
|
+
masks[i] = 1 << (bit % 8);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
byte0: bytes[0]!,
|
|
51
|
+
mask0: masks[0]!,
|
|
52
|
+
byte1: bytes[1]!,
|
|
53
|
+
mask1: masks[1]!,
|
|
54
|
+
byte2: bytes[2]!,
|
|
55
|
+
mask2: masks[2]!,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const isBloomBitsInBloom = (
|
|
60
|
+
bloom: Uint8Array,
|
|
61
|
+
bits: BloomBits,
|
|
62
|
+
): boolean =>
|
|
63
|
+
(bloom[bits.byte0]! & bits.mask0) !== 0 &&
|
|
64
|
+
(bloom[bits.byte1]! & bits.mask1) !== 0 &&
|
|
65
|
+
(bloom[bits.byte2]! & bits.mask2) !== 0;
|
|
66
|
+
|
|
67
|
+
export const isInBloom = (bloom: Uint8Array, input: Hex): boolean =>
|
|
68
|
+
isBloomBitsInBloom(bloom, getBloomBits(input));
|
|
69
|
+
|
|
70
|
+
const isAnyInBloom = (
|
|
71
|
+
bloom: Uint8Array,
|
|
72
|
+
bits: BloomBits[] | undefined,
|
|
73
|
+
): boolean =>
|
|
74
|
+
bits === undefined || bits.some((b) => isBloomBitsInBloom(bloom, b));
|
|
75
|
+
|
|
76
|
+
const toBloomBits = (
|
|
77
|
+
input: Hex | Hex[] | null | undefined,
|
|
78
|
+
): BloomBits[] | undefined => {
|
|
79
|
+
if (input === null || input === undefined) return undefined;
|
|
80
|
+
if (Array.isArray(input)) return input.map(getBloomBits);
|
|
81
|
+
return [getBloomBits(input)];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert `filter` to a `LogFilterBloom`. Call once for each filter,
|
|
86
|
+
* so that the inputs are not hashed again for each block.
|
|
87
|
+
*/
|
|
88
|
+
export const getLogFilterBloom = (filter: LogFilter): LogFilterBloom => {
|
|
89
|
+
let address: BloomBits[] | undefined;
|
|
90
|
+
let factory: LogFilterBloom["factory"];
|
|
91
|
+
|
|
92
|
+
if (isAddressFactory(filter.address)) {
|
|
93
|
+
address = undefined;
|
|
94
|
+
factory = {
|
|
95
|
+
address: toBloomBits(filter.address.address),
|
|
96
|
+
eventSelector: getBloomBits(filter.address.eventSelector),
|
|
97
|
+
};
|
|
98
|
+
} else if (Array.isArray(filter.address) && filter.address.length === 0) {
|
|
99
|
+
address = undefined;
|
|
100
|
+
} else {
|
|
101
|
+
address = toBloomBits(filter.address);
|
|
26
102
|
}
|
|
27
103
|
|
|
28
|
-
return
|
|
104
|
+
return {
|
|
105
|
+
fromBlock: getFilterFromBlock(filter),
|
|
106
|
+
toBlock: getFilterToBlock(filter),
|
|
107
|
+
address,
|
|
108
|
+
topics: [filter.topic0, filter.topic1, filter.topic2, filter.topic3].map(
|
|
109
|
+
toBloomBits,
|
|
110
|
+
),
|
|
111
|
+
factory,
|
|
112
|
+
};
|
|
29
113
|
};
|
|
30
114
|
|
|
31
115
|
/**
|
|
@@ -38,65 +122,30 @@ export const isInBloom = (_bloom: Hex, input: Hex): boolean => {
|
|
|
38
122
|
* Note: False positives are possible.
|
|
39
123
|
*/
|
|
40
124
|
export function isFilterInBloom({
|
|
41
|
-
|
|
125
|
+
blockNumber,
|
|
126
|
+
bloom,
|
|
42
127
|
filter,
|
|
43
128
|
}: {
|
|
44
|
-
|
|
45
|
-
|
|
129
|
+
blockNumber: number;
|
|
130
|
+
bloom: Uint8Array;
|
|
131
|
+
filter: LogFilterBloom;
|
|
46
132
|
}): boolean {
|
|
47
133
|
// Return `false` for out of range blocks
|
|
48
|
-
if (
|
|
49
|
-
hexToNumber(block.number) < getFilterFromBlock(filter) ||
|
|
50
|
-
hexToNumber(block.number) > getFilterToBlock(filter)
|
|
51
|
-
) {
|
|
134
|
+
if (blockNumber < filter.fromBlock || blockNumber > filter.toBlock) {
|
|
52
135
|
return false;
|
|
53
136
|
}
|
|
54
137
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
filter.
|
|
58
|
-
filter.
|
|
59
|
-
filter.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return true;
|
|
63
|
-
} else if (Array.isArray(topic)) {
|
|
64
|
-
return topic.some((t) => isInBloom(block.logsBloom, t));
|
|
65
|
-
} else {
|
|
66
|
-
return isInBloom(block.logsBloom, topic);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
let isAddressInBloom: boolean;
|
|
71
|
-
|
|
72
|
-
if (filter.address === undefined) isAddressInBloom = true;
|
|
73
|
-
else if (isAddressFactory(filter.address)) {
|
|
74
|
-
// Return true if the `Factory` is matched.
|
|
75
|
-
if (
|
|
76
|
-
(filter.address.address === undefined ||
|
|
77
|
-
(Array.isArray(filter.address.address)
|
|
78
|
-
? filter.address.address.some((address) =>
|
|
79
|
-
isInBloom(block.logsBloom, address),
|
|
80
|
-
)
|
|
81
|
-
: isInBloom(block.logsBloom, filter.address.address))) &&
|
|
82
|
-
isInBloom(block.logsBloom, filter.address.eventSelector)
|
|
83
|
-
) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
isAddressInBloom = true;
|
|
88
|
-
} else if (Array.isArray(filter.address)) {
|
|
89
|
-
if (filter.address.length === 0) {
|
|
90
|
-
isAddressInBloom = true;
|
|
91
|
-
} else {
|
|
92
|
-
isAddressInBloom = filter.address.some((address) =>
|
|
93
|
-
isInBloom(block.logsBloom, address),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
// single address case
|
|
98
|
-
isAddressInBloom = isInBloom(block.logsBloom, filter.address);
|
|
138
|
+
// Return true if the `Factory` is matched.
|
|
139
|
+
if (
|
|
140
|
+
filter.factory !== undefined &&
|
|
141
|
+
isAnyInBloom(bloom, filter.factory.address) &&
|
|
142
|
+
isBloomBitsInBloom(bloom, filter.factory.eventSelector)
|
|
143
|
+
) {
|
|
144
|
+
return true;
|
|
99
145
|
}
|
|
100
146
|
|
|
101
|
-
return
|
|
147
|
+
return (
|
|
148
|
+
isAnyInBloom(bloom, filter.address) &&
|
|
149
|
+
filter.topics.every((topic) => isAnyInBloom(bloom, topic))
|
|
150
|
+
);
|
|
102
151
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Address,
|
|
3
3
|
type Hash,
|
|
4
|
+
type Hex,
|
|
5
|
+
hexToBytes,
|
|
4
6
|
hexToNumber,
|
|
5
7
|
numberToHex,
|
|
6
8
|
zeroHash,
|
|
@@ -60,7 +62,12 @@ import { isAsyncExecutionChain } from "@/utils/finality.js";
|
|
|
60
62
|
import { createLock } from "@/utils/mutex.js";
|
|
61
63
|
import { range } from "@/utils/range.js";
|
|
62
64
|
import { startClock } from "@/utils/timer.js";
|
|
63
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getLogFilterBloom,
|
|
67
|
+
isFilterInBloom,
|
|
68
|
+
isInBloom,
|
|
69
|
+
zeroLogsBloom,
|
|
70
|
+
} from "./bloom.js";
|
|
64
71
|
|
|
65
72
|
export type RealtimeSync = {
|
|
66
73
|
/**
|
|
@@ -176,6 +183,8 @@ export const createRealtimeSync = (
|
|
|
176
183
|
}
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
const logFilterBlooms = logFilters.map(getLogFilterBloom);
|
|
187
|
+
|
|
179
188
|
const getLatestUnfinalizedBlock = () => {
|
|
180
189
|
if (unfinalizedBlocks.length === 0) {
|
|
181
190
|
return finalizedBlock;
|
|
@@ -214,12 +223,18 @@ export const createRealtimeSync = (
|
|
|
214
223
|
// "eth_getLogs" calls can be skipped if no filters match `newHeadBlock.logsBloom`.
|
|
215
224
|
// Async-execution chains cannot reliably use blooms to filter or validate logs.
|
|
216
225
|
const isAsyncExecution = isAsyncExecutionChain(args.chain.id);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
let shouldRequestLogs: boolean;
|
|
227
|
+
if (isAsyncExecution) {
|
|
228
|
+
shouldRequestLogs = logFilters.length > 0;
|
|
229
|
+
} else if (maybeBlockHeader.logsBloom === zeroLogsBloom) {
|
|
230
|
+
shouldRequestLogs = true;
|
|
231
|
+
} else {
|
|
232
|
+
const blockNumber = hexToNumber(maybeBlockHeader.number);
|
|
233
|
+
const bloom = hexToBytes(maybeBlockHeader.logsBloom);
|
|
234
|
+
shouldRequestLogs = logFilterBlooms.some((filter) =>
|
|
235
|
+
isFilterInBloom({ blockNumber, bloom, filter }),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
223
238
|
|
|
224
239
|
let logs: SyncLog[] = [];
|
|
225
240
|
if (shouldRequestLogs) {
|
|
@@ -259,57 +274,6 @@ export const createRealtimeSync = (
|
|
|
259
274
|
isAsyncExecutionChain(args.chain.id),
|
|
260
275
|
);
|
|
261
276
|
|
|
262
|
-
if (isAsyncExecutionChain(args.chain.id) === false) {
|
|
263
|
-
// Note: Exact `logsBloom` validations were considered too strict to add to `validateLogsAndBlock`.
|
|
264
|
-
let isInvalidLogsBloom = false;
|
|
265
|
-
for (const log of logs) {
|
|
266
|
-
if (isInBloom(block.logsBloom, log.address) === false) {
|
|
267
|
-
isInvalidLogsBloom = true;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
log.topics[0] &&
|
|
272
|
-
isInBloom(block.logsBloom, log.topics[0]) === false
|
|
273
|
-
) {
|
|
274
|
-
isInvalidLogsBloom = true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
log.topics[1] &&
|
|
279
|
-
isInBloom(block.logsBloom, log.topics[1]) === false
|
|
280
|
-
) {
|
|
281
|
-
isInvalidLogsBloom = true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
log.topics[2] &&
|
|
286
|
-
isInBloom(block.logsBloom, log.topics[2]) === false
|
|
287
|
-
) {
|
|
288
|
-
isInvalidLogsBloom = true;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
log.topics[3] &&
|
|
293
|
-
isInBloom(block.logsBloom, log.topics[3]) === false
|
|
294
|
-
) {
|
|
295
|
-
isInvalidLogsBloom = true;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (isInvalidLogsBloom) {
|
|
299
|
-
args.common.logger.warn({
|
|
300
|
-
msg: "Detected inconsistent RPC responses. Log not found in block.logsBloom.",
|
|
301
|
-
action: "fetch_block_data",
|
|
302
|
-
chain: args.chain.name,
|
|
303
|
-
chain_id: args.chain.id,
|
|
304
|
-
number: hexToNumber(block.number),
|
|
305
|
-
hash: block.hash,
|
|
306
|
-
logIndex: hexToNumber(log.logIndex),
|
|
307
|
-
});
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
277
|
for (const log of logs) {
|
|
314
278
|
if (log.transactionHash === zeroHash) {
|
|
315
279
|
args.common.logger.warn({
|
|
@@ -387,6 +351,7 @@ export const createRealtimeSync = (
|
|
|
387
351
|
|
|
388
352
|
// Record `blockChildAddresses` that contain factory child addresses
|
|
389
353
|
const blockChildAddresses: BlockWithEventData["childAddresses"] = new Map();
|
|
354
|
+
const factoryLogs = new Set<SyncLog>();
|
|
390
355
|
|
|
391
356
|
const childAddressDecodeFailureIds = new Set<string>();
|
|
392
357
|
let childAddressDecodeFailureCount = 0;
|
|
@@ -395,6 +360,7 @@ export const createRealtimeSync = (
|
|
|
395
360
|
for (const factory of factories) {
|
|
396
361
|
for (const log of logs) {
|
|
397
362
|
if (isLogFactoryMatched({ factory, log })) {
|
|
363
|
+
factoryLogs.add(log);
|
|
398
364
|
let address: Address;
|
|
399
365
|
try {
|
|
400
366
|
address = getChildAddress({ log, factory });
|
|
@@ -462,6 +428,69 @@ export const createRealtimeSync = (
|
|
|
462
428
|
return isMatched;
|
|
463
429
|
});
|
|
464
430
|
|
|
431
|
+
if (isAsyncExecution === false && block !== undefined) {
|
|
432
|
+
// Note: Exact `logsBloom` validations were considered too strict to add to `validateLogsAndBlock`.
|
|
433
|
+
const logsBloom = hexToBytes(block.logsBloom);
|
|
434
|
+
const checkedInputs = new Set<Hex>();
|
|
435
|
+
let isInvalidLogsBloom = false;
|
|
436
|
+
for (const log of [...factoryLogs, ...logs]) {
|
|
437
|
+
if (
|
|
438
|
+
checkedInputs.has(log.address) === false &&
|
|
439
|
+
isInBloom(logsBloom, log.address) === false
|
|
440
|
+
) {
|
|
441
|
+
isInvalidLogsBloom = true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (
|
|
445
|
+
log.topics[0] &&
|
|
446
|
+
checkedInputs.has(log.topics[0]) === false &&
|
|
447
|
+
isInBloom(logsBloom, log.topics[0]) === false
|
|
448
|
+
) {
|
|
449
|
+
isInvalidLogsBloom = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (
|
|
453
|
+
log.topics[1] &&
|
|
454
|
+
checkedInputs.has(log.topics[1]) === false &&
|
|
455
|
+
isInBloom(logsBloom, log.topics[1]) === false
|
|
456
|
+
) {
|
|
457
|
+
isInvalidLogsBloom = true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (
|
|
461
|
+
log.topics[2] &&
|
|
462
|
+
checkedInputs.has(log.topics[2]) === false &&
|
|
463
|
+
isInBloom(logsBloom, log.topics[2]) === false
|
|
464
|
+
) {
|
|
465
|
+
isInvalidLogsBloom = true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
log.topics[3] &&
|
|
470
|
+
checkedInputs.has(log.topics[3]) === false &&
|
|
471
|
+
isInBloom(logsBloom, log.topics[3]) === false
|
|
472
|
+
) {
|
|
473
|
+
isInvalidLogsBloom = true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (isInvalidLogsBloom) {
|
|
477
|
+
args.common.logger.warn({
|
|
478
|
+
msg: "Detected inconsistent RPC responses. Log not found in block.logsBloom.",
|
|
479
|
+
action: "fetch_block_data",
|
|
480
|
+
chain: args.chain.name,
|
|
481
|
+
chain_id: args.chain.id,
|
|
482
|
+
number: hexToNumber(block.number),
|
|
483
|
+
hash: block.hash,
|
|
484
|
+
logIndex: hexToNumber(log.logIndex),
|
|
485
|
+
});
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
checkedInputs.add(log.address);
|
|
490
|
+
for (const topic of log.topics) checkedInputs.add(topic);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
465
494
|
// Initial weak trace filtering before full filtering with factory addresses in handleBlock
|
|
466
495
|
traces = traces.filter((trace) => {
|
|
467
496
|
let isMatched = false;
|
package/src/utils/copy.ts
CHANGED
|
@@ -5,75 +5,94 @@ import util from "node:util";
|
|
|
5
5
|
*/
|
|
6
6
|
export const COPY_ON_WRITE = Symbol.for("ponder:copyOnWrite");
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Target of a copy-on-write proxy.
|
|
10
|
+
*
|
|
11
|
+
* Note: The handler is shared by all proxies, so the state of each proxy
|
|
12
|
+
* is stored in its target.
|
|
13
|
+
*/
|
|
14
|
+
class CopyOnWriteTarget {
|
|
15
|
+
obj: object;
|
|
16
|
+
copiedObject: object | undefined;
|
|
17
|
+
|
|
18
|
+
constructor(obj: object) {
|
|
19
|
+
this.obj = obj;
|
|
20
|
+
this.copiedObject = undefined;
|
|
21
|
+
}
|
|
9
22
|
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
// Note: `this` is the proxy when called by `util.inspect`.
|
|
24
|
+
[util.inspect.custom]() {
|
|
25
|
+
// @ts-expect-error
|
|
26
|
+
return this[COPY_ON_WRITE] ?? this.copiedObject ?? this.obj;
|
|
27
|
+
}
|
|
12
28
|
}
|
|
13
29
|
|
|
30
|
+
const getCopiedObject = (target: CopyOnWriteTarget) => {
|
|
31
|
+
if (target.copiedObject === undefined) {
|
|
32
|
+
target.copiedObject = structuredClone(target.obj);
|
|
33
|
+
}
|
|
34
|
+
return target.copiedObject;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const copyOnWriteHandler: ProxyHandler<CopyOnWriteTarget> = {
|
|
38
|
+
get(target, prop, receiver) {
|
|
39
|
+
if (prop === COPY_ON_WRITE) {
|
|
40
|
+
return target.copiedObject ?? target.obj;
|
|
41
|
+
}
|
|
42
|
+
let result = Reflect.get(target.copiedObject ?? target.obj, prop, receiver);
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
typeof result === "object" &&
|
|
46
|
+
result !== null &&
|
|
47
|
+
target.copiedObject === undefined
|
|
48
|
+
) {
|
|
49
|
+
result = Reflect.get(getCopiedObject(target), prop, receiver);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
set(target, prop, newValue, receiver) {
|
|
55
|
+
return Reflect.set(getCopiedObject(target), prop, newValue, receiver);
|
|
56
|
+
},
|
|
57
|
+
deleteProperty(target, prop) {
|
|
58
|
+
return Reflect.deleteProperty(getCopiedObject(target), prop);
|
|
59
|
+
},
|
|
60
|
+
defineProperty(target, prop, descriptor) {
|
|
61
|
+
return Reflect.defineProperty(getCopiedObject(target), prop, descriptor);
|
|
62
|
+
},
|
|
63
|
+
ownKeys(target) {
|
|
64
|
+
return Reflect.ownKeys(target.copiedObject ?? target.obj);
|
|
65
|
+
},
|
|
66
|
+
has(target, prop) {
|
|
67
|
+
return Reflect.has(target.copiedObject ?? target.obj, prop);
|
|
68
|
+
},
|
|
69
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
70
|
+
return Reflect.getOwnPropertyDescriptor(
|
|
71
|
+
target.copiedObject ?? target.obj,
|
|
72
|
+
prop,
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
getPrototypeOf(target) {
|
|
76
|
+
return Reflect.getPrototypeOf(target.copiedObject ?? target.obj);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
14
80
|
/**
|
|
15
|
-
* Create a copy-on-write proxy for
|
|
81
|
+
* Create a copy-on-write proxy for a plain object.
|
|
82
|
+
*
|
|
83
|
+
* @dev Arrays and other exotic objects are not supported. The proxy target
|
|
84
|
+
* is a `CopyOnWriteTarget`, not `obj`, so the proxy invariants break for
|
|
85
|
+
* non-configurable properties such as `Array.length`:
|
|
86
|
+
* `Object.keys()` and spread throw a `TypeError`, and `Array.isArray()`
|
|
87
|
+
* returns `false`.
|
|
16
88
|
*/
|
|
17
|
-
export const copyOnWrite = <T extends
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const proxy = new Proxy<T>(obj, {
|
|
26
|
-
get(target, prop, receiver) {
|
|
27
|
-
if (prop === COPY_ON_WRITE) {
|
|
28
|
-
return copiedObject ?? target;
|
|
29
|
-
}
|
|
30
|
-
let result = Reflect.get(copiedObject ?? target, prop, receiver);
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
typeof result === "object" &&
|
|
34
|
-
result !== null &&
|
|
35
|
-
copiedObject === undefined
|
|
36
|
-
) {
|
|
37
|
-
copiedObject = structuredClone(target);
|
|
38
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
39
|
-
result = Reflect.get(copiedObject, prop, receiver);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return result;
|
|
43
|
-
},
|
|
44
|
-
set(target, prop, newValue, receiver) {
|
|
45
|
-
if (copiedObject === undefined) {
|
|
46
|
-
copiedObject = structuredClone(target);
|
|
47
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
48
|
-
}
|
|
49
|
-
return Reflect.set(copiedObject, prop, newValue, receiver);
|
|
50
|
-
},
|
|
51
|
-
deleteProperty(target, prop) {
|
|
52
|
-
if (copiedObject === undefined) {
|
|
53
|
-
copiedObject = structuredClone(target);
|
|
54
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
55
|
-
}
|
|
56
|
-
return Reflect.deleteProperty(copiedObject, prop);
|
|
57
|
-
},
|
|
58
|
-
defineProperty(target, prop, descriptor) {
|
|
59
|
-
if (copiedObject === undefined) {
|
|
60
|
-
copiedObject = structuredClone(target);
|
|
61
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
62
|
-
}
|
|
63
|
-
return Reflect.defineProperty(copiedObject, prop, descriptor);
|
|
64
|
-
},
|
|
65
|
-
ownKeys(target) {
|
|
66
|
-
return Reflect.ownKeys(copiedObject ?? target);
|
|
67
|
-
},
|
|
68
|
-
has(target, prop) {
|
|
69
|
-
return Reflect.has(copiedObject ?? target, prop);
|
|
70
|
-
},
|
|
71
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
72
|
-
return Reflect.getOwnPropertyDescriptor(copiedObject ?? target, prop);
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return proxy;
|
|
89
|
+
export const copyOnWrite = <T extends { [key: string]: unknown }>(
|
|
90
|
+
obj: T,
|
|
91
|
+
): T => {
|
|
92
|
+
return new Proxy(
|
|
93
|
+
new CopyOnWriteTarget(obj),
|
|
94
|
+
copyOnWriteHandler,
|
|
95
|
+
) as unknown as T;
|
|
77
96
|
};
|
|
78
97
|
|
|
79
98
|
/**
|
|
@@ -12,6 +12,31 @@ import {
|
|
|
12
12
|
} from "./decodeAbiParameters.js";
|
|
13
13
|
import { toLowerCase } from "./lowercase.js";
|
|
14
14
|
|
|
15
|
+
type DecodeEventLogInputs = {
|
|
16
|
+
isUnnamed: boolean;
|
|
17
|
+
indexedInputs: (readonly [AbiParameter, number])[];
|
|
18
|
+
nonIndexedInputs: AbiParameter[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Inputs of each `abiItem`, so that they aren't recomputed for every log. */
|
|
22
|
+
const inputsCache = new WeakMap<AbiEvent, DecodeEventLogInputs>();
|
|
23
|
+
|
|
24
|
+
const getInputs = (abiItem: AbiEvent): DecodeEventLogInputs => {
|
|
25
|
+
let result = inputsCache.get(abiItem);
|
|
26
|
+
if (result === undefined) {
|
|
27
|
+
const { inputs } = abiItem;
|
|
28
|
+
result = {
|
|
29
|
+
isUnnamed: inputs?.some((x) => !("name" in x && x.name)),
|
|
30
|
+
indexedInputs: inputs
|
|
31
|
+
.map((x, i) => [x, i] as const)
|
|
32
|
+
.filter(([x]) => "indexed" in x && x.indexed),
|
|
33
|
+
nonIndexedInputs: inputs.filter((x) => !("indexed" in x && x.indexed)),
|
|
34
|
+
};
|
|
35
|
+
inputsCache.set(abiItem, result);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
|
|
15
40
|
/**
|
|
16
41
|
* Decode an event log.
|
|
17
42
|
*
|
|
@@ -27,14 +52,11 @@ export function decodeEventLog({
|
|
|
27
52
|
data: Hex;
|
|
28
53
|
}): any {
|
|
29
54
|
const { inputs } = abiItem;
|
|
30
|
-
const isUnnamed
|
|
55
|
+
const { isUnnamed, indexedInputs, nonIndexedInputs } = getInputs(abiItem);
|
|
31
56
|
|
|
32
57
|
const args: any = isUnnamed ? [] : {};
|
|
33
58
|
|
|
34
59
|
// Decode topics (indexed args).
|
|
35
|
-
const indexedInputs = inputs
|
|
36
|
-
.map((x, i) => [x, i] as const)
|
|
37
|
-
.filter(([x]) => "indexed" in x && x.indexed);
|
|
38
60
|
for (let i = 0; i < indexedInputs.length; i++) {
|
|
39
61
|
const [param, argIndex] = indexedInputs[i]!;
|
|
40
62
|
const topic = topics[i + 1];
|
|
@@ -52,7 +74,6 @@ export function decodeEventLog({
|
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
// Decode data (non-indexed args).
|
|
55
|
-
const nonIndexedInputs = inputs.filter((x) => !("indexed" in x && x.indexed));
|
|
56
77
|
if (nonIndexedInputs.length > 0) {
|
|
57
78
|
if (data && data !== "0x") {
|
|
58
79
|
const out = [] as DecodeAbiParametersReturnType<typeof nonIndexedInputs>;
|
|
@@ -82,7 +103,8 @@ export function decodeEventLog({
|
|
|
82
103
|
}
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
|
|
106
|
+
// Note: Every input sets a value in `args`.
|
|
107
|
+
return inputs.length > 0 ? args : undefined;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
const ARRAY_REGEX = /^(.*)\[(\d+)?\]$/;
|