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.
@@ -260,14 +260,8 @@ export const isAddressMatched = ({
260
260
  childAddresses: Map<Address, number>;
261
261
  }) => {
262
262
  if (address === undefined) return false;
263
- if (
264
- childAddresses.has(toLowerCase(address)) &&
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
- Array.isArray(filterValue) &&
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
- if (filterValue === toLowerCase(eventValue)) return true;
293
-
294
- return false;
288
+ return filterValue === value;
295
289
  };
296
290
 
297
291
  /**
@@ -1,5 +1,5 @@
1
- import { type Hex, hexToBytes, hexToNumber, keccak256 } from "viem";
2
- import type { LogFilter, SyncBlock } from "@/internal/types.js";
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 const isInBloom = (_bloom: Hex, input: Hex): boolean => {
15
- const bloom = hexToBytes(_bloom);
16
- const hash = hexToBytes(keccak256(input));
17
-
18
- for (const i of [0, 2, 4]) {
19
- const bit = (hash[i + 1]! + (hash[i]! << 8)) & 0x7ff;
20
- if (
21
- (bloom[BLOOM_SIZE_BYTES - 1 - Math.floor(bit / 8)]! &
22
- (1 << (bit % 8))) ===
23
- 0
24
- )
25
- return false;
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 true;
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
- block,
125
+ blockNumber,
126
+ bloom,
42
127
  filter,
43
128
  }: {
44
- block: Pick<SyncBlock, "number" | "logsBloom">;
45
- filter: LogFilter;
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
- const isTopicsInBloom = [
56
- filter.topic0,
57
- filter.topic1,
58
- filter.topic2,
59
- filter.topic3,
60
- ].every((topic) => {
61
- if (topic === null || topic === undefined) {
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 isAddressInBloom && isTopicsInBloom;
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 { isFilterInBloom, isInBloom, zeroLogsBloom } from "./bloom.js";
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
- const shouldRequestLogs = isAsyncExecution
218
- ? logFilters.length > 0
219
- : maybeBlockHeader.logsBloom === zeroLogsBloom ||
220
- logFilters.some((filter) =>
221
- isFilterInBloom({ block: maybeBlockHeader, filter }),
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
- const copyOnWriteCopies = new WeakMap<object, object>();
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
- function inspectCopyOnWrite(this: object) {
11
- return copyOnWriteCopies.get(this) ?? (this as any)[COPY_ON_WRITE] ?? this;
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 an object.
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 object>(obj: T): T => {
18
- let copiedObject: T | undefined;
19
-
20
- Object.defineProperty(obj, util.inspect.custom, {
21
- value: inspectCopyOnWrite,
22
- configurable: true,
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 = inputs?.some((x) => !("name" in x && x.name));
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
- return Object.values(args).length > 0 ? args : undefined;
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+)?\]$/;