ponder 0.8.13 → 0.8.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ponder",
3
- "version": "0.8.13",
3
+ "version": "0.8.15",
4
4
  "description": "An open-source framework for crypto application backends",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -87,7 +87,7 @@ export async function run({
87
87
  case "block": {
88
88
  // Events must be run block-by-block, so that `database.complete` can accurately
89
89
  // update the temporary `checkpoint` value set in the trigger.
90
- for (const events of splitEvents(event.events)) {
90
+ for (const { checkpoint, events } of splitEvents(event.events)) {
91
91
  const result = await handleEvents(
92
92
  decodeEvents(common, indexingBuild.sources, events),
93
93
  event.checkpoint,
@@ -96,7 +96,7 @@ export async function run({
96
96
  if (result.status === "error") onReloadableError(result.error);
97
97
 
98
98
  // Set reorg table `checkpoint` column for newly inserted rows.
99
- await database.complete({ checkpoint: event.checkpoint });
99
+ await database.complete({ checkpoint });
100
100
  }
101
101
 
102
102
  await metadataStore.setStatus(event.status);
package/src/sync/index.ts CHANGED
@@ -161,17 +161,27 @@ const min = (...checkpoints: (string | undefined)[]) => {
161
161
  })!;
162
162
  };
163
163
 
164
- export const splitEvents = (events: RawEvent[]): RawEvent[][] => {
164
+ export const splitEvents = (
165
+ events: RawEvent[],
166
+ ): { checkpoint: string; events: RawEvent[] }[] => {
165
167
  let prevHash: Hash | undefined;
166
- const result: RawEvent[][] = [];
168
+ const result: { checkpoint: string; events: RawEvent[] }[] = [];
167
169
 
168
170
  for (const event of events) {
169
171
  if (prevHash === undefined || prevHash !== event.block.hash) {
170
- result.push([]);
172
+ result.push({
173
+ checkpoint: encodeCheckpoint({
174
+ ...maxCheckpoint,
175
+ blockTimestamp: Number(event.block.timestamp),
176
+ chainId: BigInt(event.chainId),
177
+ blockNumber: event.block.number,
178
+ }),
179
+ events: [],
180
+ });
171
181
  prevHash = event.block.hash;
172
182
  }
173
183
 
174
- result[result.length - 1]!.push(event);
184
+ result[result.length - 1]!.events.push(event);
175
185
  }
176
186
 
177
187
  return result;
@@ -18,7 +18,12 @@ import {
18
18
  shouldGetTransactionReceipt,
19
19
  } from "@/sync/source.js";
20
20
  import type { Source, TransactionFilter } from "@/sync/source.js";
21
- import type { SyncBlock, SyncLog, SyncTrace } from "@/types/sync.js";
21
+ import type {
22
+ SyncBlock,
23
+ SyncLog,
24
+ SyncTrace,
25
+ SyncTransactionReceipt,
26
+ } from "@/types/sync.js";
22
27
  import {
23
28
  type Interval,
24
29
  getChunks,
@@ -30,6 +35,7 @@ import type { RequestQueue } from "@/utils/requestQueue.js";
30
35
  import {
31
36
  _debug_traceBlockByNumber,
32
37
  _eth_getBlockByNumber,
38
+ _eth_getBlockReceipts,
33
39
  _eth_getLogs,
34
40
  _eth_getTransactionReceipt,
35
41
  } from "@/utils/rpc.js";
@@ -66,7 +72,10 @@ export const createHistoricalSync = async (
66
72
  args: CreateHistoricalSyncParameters,
67
73
  ): Promise<HistoricalSync> => {
68
74
  let isKilled = false;
69
-
75
+ /**
76
+ * Flag to fetch transaction receipts through _eth_getBlockReceipts (true) or _eth_getTransactionReceipt (false)
77
+ */
78
+ let isBlockReceipts = true;
70
79
  /**
71
80
  * Blocks that have already been extracted.
72
81
  * Note: All entries are deleted at the end of each call to `sync()`.
@@ -82,6 +91,20 @@ export const createHistoricalSync = async (
82
91
  * Note: All entries are deleted at the end of each call to `sync()`.
83
92
  */
84
93
  const transactionsCache = new Set<Hash>();
94
+ /**
95
+ * Block transaction receipts that have already been fetched.
96
+ * Note: All entries are deleted at the end of each call to `sync()`.
97
+ */
98
+ const blockReceiptsCache = new Map<Hash, Promise<SyncTransactionReceipt[]>>();
99
+ /**
100
+ * Transaction receipts that have already been fetched.
101
+ * Note: All entries are deleted at the end of each call to `sync()`.
102
+ */
103
+ const transactionReceiptsCache = new Map<
104
+ Hash,
105
+ Promise<SyncTransactionReceipt>
106
+ >();
107
+
85
108
  /**
86
109
  * Data about the range passed to "eth_getLogs" for all log
87
110
  * filters and log factories.
@@ -318,6 +341,79 @@ export const createHistoricalSync = async (
318
341
  }
319
342
  };
320
343
 
344
+ const syncTransactionReceipts = async (
345
+ block: Hash,
346
+ transactionHashes: Set<Hash>,
347
+ ): Promise<SyncTransactionReceipt[]> => {
348
+ if (isBlockReceipts === false) {
349
+ const transactionReceipts = await Promise.all(
350
+ Array.from(transactionHashes).map((hash) =>
351
+ syncTransactionReceipt(hash),
352
+ ),
353
+ );
354
+
355
+ return transactionReceipts;
356
+ }
357
+
358
+ let blockReceipts: SyncTransactionReceipt[];
359
+ try {
360
+ blockReceipts = await syncBlockReceipts(block);
361
+ } catch (_error) {
362
+ const error = _error as Error;
363
+ args.common.logger.warn({
364
+ service: "sync",
365
+ msg: `Caught eth_getBlockReceipts error on '${
366
+ args.network.name
367
+ }', switching to eth_getTransactionReceipt method.`,
368
+ error,
369
+ });
370
+
371
+ isBlockReceipts = false;
372
+ return syncTransactionReceipts(block, transactionHashes);
373
+ }
374
+
375
+ const blockReceiptsTransactionHashes = new Set(
376
+ blockReceipts.map((r) => r.transactionHash),
377
+ );
378
+ // Validate that block transaction receipts include all required transactions
379
+ for (const hash of Array.from(transactionHashes)) {
380
+ if (blockReceiptsTransactionHashes.has(hash) === false) {
381
+ throw new Error(
382
+ `Detected inconsistent RPC responses. 'transaction.hash' ${hash} not found in eth_getBlockReceipts response for block '${block}'`,
383
+ );
384
+ }
385
+ }
386
+ const transactionReceipts = blockReceipts.filter((receipt) =>
387
+ transactionHashes.has(receipt.transactionHash),
388
+ );
389
+
390
+ return transactionReceipts;
391
+ };
392
+
393
+ const syncTransactionReceipt = async (transaction: Hash) => {
394
+ if (transactionReceiptsCache.has(transaction)) {
395
+ return await transactionReceiptsCache.get(transaction)!;
396
+ } else {
397
+ const receipt = _eth_getTransactionReceipt(args.requestQueue, {
398
+ hash: transaction,
399
+ });
400
+ transactionReceiptsCache.set(transaction, receipt);
401
+ return await receipt;
402
+ }
403
+ };
404
+
405
+ const syncBlockReceipts = async (block: Hash) => {
406
+ if (blockReceiptsCache.has(block)) {
407
+ return await blockReceiptsCache.get(block)!;
408
+ } else {
409
+ const blockReceipts = _eth_getBlockReceipts(args.requestQueue, {
410
+ blockHash: block,
411
+ });
412
+ blockReceiptsCache.set(block, blockReceipts);
413
+ return await blockReceipts;
414
+ }
415
+ };
416
+
321
417
  /** Extract and insert the log-based addresses that match `filter` + `interval`. */
322
418
  const syncLogFactory = async (filter: LogFactory, interval: Interval) => {
323
419
  const logs = await syncLogsDynamic({
@@ -380,6 +476,8 @@ export const createHistoricalSync = async (
380
476
  logs.map((log) => syncBlock(hexToNumber(log.blockNumber))),
381
477
  );
382
478
 
479
+ const requiredBlocks = new Set(blocks.map((b) => b.hash));
480
+
383
481
  // Validate that logs point to the valid transaction hash in the block
384
482
  for (let i = 0; i < logs.length; i++) {
385
483
  const log = logs[i]!;
@@ -418,10 +516,15 @@ export const createHistoricalSync = async (
418
516
 
419
517
  if (shouldGetTransactionReceipt(filter)) {
420
518
  const transactionReceipts = await Promise.all(
421
- Array.from(transactionHashes).map((hash) =>
422
- _eth_getTransactionReceipt(args.requestQueue, { hash }),
423
- ),
424
- );
519
+ Array.from(requiredBlocks).map((blockHash) => {
520
+ const blockTransactionHashes = new Set(
521
+ logs
522
+ .filter((l) => l.blockHash === blockHash)
523
+ .map((l) => l.transactionHash),
524
+ );
525
+ return syncTransactionReceipts(blockHash, blockTransactionHashes);
526
+ }),
527
+ ).then((receipts) => receipts.flat());
425
528
 
426
529
  if (isKilled) return;
427
530
 
@@ -472,6 +575,7 @@ export const createHistoricalSync = async (
472
575
  if (isKilled) return;
473
576
 
474
577
  const transactionHashes: Set<Hash> = new Set();
578
+ const requiredBlocks: Set<SyncBlock> = new Set();
475
579
 
476
580
  for (const block of blocks) {
477
581
  block.transactions.map((transaction) => {
@@ -485,6 +589,7 @@ export const createHistoricalSync = async (
485
589
  })
486
590
  ) {
487
591
  transactionHashes.add(transaction.hash);
592
+ requiredBlocks.add(block);
488
593
  }
489
594
  });
490
595
  }
@@ -496,10 +601,15 @@ export const createHistoricalSync = async (
496
601
  if (isKilled) return;
497
602
 
498
603
  const transactionReceipts = await Promise.all(
499
- Array.from(transactionHashes).map((hash) =>
500
- _eth_getTransactionReceipt(args.requestQueue, { hash }),
501
- ),
502
- );
604
+ Array.from(requiredBlocks).map((block) => {
605
+ const blockTransactionHashes = new Set(
606
+ block.transactions
607
+ .filter((t) => transactionHashes.has(t.hash))
608
+ .map((t) => t.hash),
609
+ );
610
+ return syncTransactionReceipts(block.hash, blockTransactionHashes);
611
+ }),
612
+ ).then((receipts) => receipts.flat());
503
613
 
504
614
  if (isKilled) return;
505
615
 
@@ -521,6 +631,7 @@ export const createHistoricalSync = async (
521
631
  ? await syncAddressFactory(filter.toAddress, interval)
522
632
  : undefined;
523
633
 
634
+ const requiredBlocks: Set<Hash> = new Set();
524
635
  const traces = await Promise.all(
525
636
  intervalRange(interval).map(async (number) => {
526
637
  let traces = await syncTrace(number);
@@ -555,6 +666,7 @@ export const createHistoricalSync = async (
555
666
  if (traces.length === 0) return [];
556
667
 
557
668
  const block = await syncBlock(number);
669
+ requiredBlocks.add(block.hash);
558
670
 
559
671
  return traces.map((trace) => {
560
672
  const transaction = block.transactions.find(
@@ -576,10 +688,6 @@ export const createHistoricalSync = async (
576
688
 
577
689
  if (isKilled) return;
578
690
 
579
- const transactionHashes = new Set(
580
- traces.map(({ transaction }) => transaction.hash),
581
- );
582
-
583
691
  await args.syncStore.insertTraces({
584
692
  traces,
585
693
  chainId: args.network.chainId,
@@ -589,10 +697,15 @@ export const createHistoricalSync = async (
589
697
 
590
698
  if (shouldGetTransactionReceipt(filter)) {
591
699
  const transactionReceipts = await Promise.all(
592
- Array.from(transactionHashes).map((hash) =>
593
- _eth_getTransactionReceipt(args.requestQueue, { hash }),
594
- ),
595
- );
700
+ Array.from(requiredBlocks).map((blockHash) => {
701
+ const blockTransactionHashes = new Set(
702
+ traces
703
+ .filter((t) => t.block.hash === blockHash)
704
+ .map((t) => t.transaction.hash),
705
+ );
706
+ return syncTransactionReceipts(blockHash, blockTransactionHashes);
707
+ }),
708
+ ).then((receipts) => receipts.flat());
596
709
 
597
710
  if (isKilled) return;
598
711
 
@@ -722,6 +835,8 @@ export const createHistoricalSync = async (
722
835
  blockCache.clear();
723
836
  traceCache.clear();
724
837
  transactionsCache.clear();
838
+ blockReceiptsCache.clear();
839
+ transactionReceiptsCache.clear();
725
840
 
726
841
  return latestBlock;
727
842
  },
@@ -28,6 +28,7 @@ import {
28
28
  _debug_traceBlockByHash,
29
29
  _eth_getBlockByHash,
30
30
  _eth_getBlockByNumber,
31
+ _eth_getBlockReceipts,
31
32
  _eth_getLogs,
32
33
  _eth_getTransactionReceipt,
33
34
  } from "@/utils/rpc.js";
@@ -100,6 +101,7 @@ export const createRealtimeSync = (
100
101
  // state
101
102
  ////////
102
103
  let isKilled = false;
104
+ let isBlockReceipts = true;
103
105
  let finalizedBlock: LightBlock;
104
106
  let finalizedChildAddresses: Map<Factory, Set<Address>>;
105
107
  const unfinalizedChildAddresses = new Map<Factory, Set<Address>>();
@@ -575,6 +577,57 @@ export const createRealtimeSync = (
575
577
  }
576
578
  };
577
579
 
580
+ const syncTransactionReceipts = async (
581
+ blockHash: Hash,
582
+ transactionHashes: Set<Hash>,
583
+ ): Promise<SyncTransactionReceipt[]> => {
584
+ if (isBlockReceipts === false) {
585
+ const transactionReceipts = await Promise.all(
586
+ Array.from(transactionHashes).map(async (hash) =>
587
+ _eth_getTransactionReceipt(args.requestQueue, { hash }),
588
+ ),
589
+ );
590
+
591
+ return transactionReceipts;
592
+ }
593
+
594
+ let blockReceipts: SyncTransactionReceipt[];
595
+ try {
596
+ blockReceipts = await _eth_getBlockReceipts(args.requestQueue, {
597
+ blockHash,
598
+ });
599
+ } catch (_error) {
600
+ const error = _error as Error;
601
+ args.common.logger.warn({
602
+ service: "realtime",
603
+ msg: `Caught eth_getBlockReceipts error on '${
604
+ args.network.name
605
+ }', switching to eth_getTransactionReceipt method.`,
606
+ error,
607
+ });
608
+
609
+ isBlockReceipts = false;
610
+ return syncTransactionReceipts(blockHash, transactionHashes);
611
+ }
612
+
613
+ const blockReceiptsTransactionHashes = new Set(
614
+ blockReceipts.map((r) => r.transactionHash),
615
+ );
616
+ // Validate that block transaction receipts include all required transactions
617
+ for (const hash of Array.from(transactionHashes)) {
618
+ if (blockReceiptsTransactionHashes.has(hash) === false) {
619
+ throw new Error(
620
+ `Detected inconsistent RPC responses. Transaction receipt with transactionHash ${hash} is missing in \`blockReceipts\`.`,
621
+ );
622
+ }
623
+ }
624
+ const transactionReceipts = blockReceipts.filter((receipt) =>
625
+ transactionHashes.has(receipt.transactionHash),
626
+ );
627
+
628
+ return transactionReceipts;
629
+ };
630
+
578
631
  /**
579
632
  * Fetch all data (logs, traces, receipts) for the specified block required by `args.sources`
580
633
  *
@@ -769,7 +822,7 @@ export const createRealtimeSync = (
769
822
  for (const hash of Array.from(requiredTransactions)) {
770
823
  if (blockTransactionsHashes.has(hash) === false) {
771
824
  throw new Error(
772
- `Detected inconsistent RPC responses. Transaction with hash ${hash} is missing in \`block.transactions\`.`,
825
+ `Detected inconsistent RPC responses. 'transaction.hash' ${hash} not found in eth_getBlockReceipts response for block '${block.hash}'.`,
773
826
  );
774
827
  }
775
828
  }
@@ -778,12 +831,9 @@ export const createRealtimeSync = (
778
831
  // Transaction Receipts
779
832
  ////////
780
833
 
781
- const transactionReceipts = await Promise.all(
782
- block.transactions
783
- .filter(({ hash }) => requiredTransactionReceipts.has(hash))
784
- .map(({ hash }) =>
785
- _eth_getTransactionReceipt(args.requestQueue, { hash }),
786
- ),
834
+ const transactionReceipts = await syncTransactionReceipts(
835
+ block.hash,
836
+ requiredTransactionReceipts,
787
837
  );
788
838
 
789
839
  return {
package/src/utils/rpc.ts CHANGED
@@ -146,6 +146,20 @@ export const _eth_getTransactionReceipt = (
146
146
  return receipt as SyncTransactionReceipt;
147
147
  });
148
148
 
149
+ /**
150
+ * Helper function for "eth_getBlockReceipts" request.
151
+ */
152
+ export const _eth_getBlockReceipts = (
153
+ requestQueue: RequestQueue,
154
+ { blockHash }: { blockHash: Hash },
155
+ ): Promise<SyncTransactionReceipt[]> =>
156
+ requestQueue
157
+ .request({
158
+ method: "eth_getBlockReceipts",
159
+ params: [blockHash],
160
+ } as any)
161
+ .then((receipts) => receipts as unknown as SyncTransactionReceipt[]);
162
+
149
163
  /**
150
164
  * Helper function for "debug_traceBlockByNumber" request.
151
165
  */