ponder 0.9.3 → 0.9.4

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.
@@ -23,10 +23,11 @@ import {
23
23
  defaultTransactionReceiptInclude,
24
24
  defaultTransferFilterInclude,
25
25
  } from "@/sync/filter.js";
26
+ import type { SyncBlock } from "@/types/sync.js";
26
27
  import { chains } from "@/utils/chains.js";
27
28
  import { toLowerCase } from "@/utils/lowercase.js";
28
29
  import { dedupe } from "@ponder/common";
29
- import type { Hex, LogTopic } from "viem";
30
+ import { BlockNotFoundError, type Hex, type LogTopic, hexToNumber } from "viem";
30
31
  import { buildLogFactory } from "./factory.js";
31
32
 
32
33
  const flattenSources = <
@@ -73,6 +74,45 @@ export async function buildConfigAndIndexingFunctions({
73
74
  }> {
74
75
  const logs: { level: "warn" | "info" | "debug"; msg: string }[] = [];
75
76
 
77
+ const perNetworkLatestBlockNumber = new Map<string, Promise<number>>();
78
+
79
+ const resolveBlockNumber = async (
80
+ blockNumberOrTag: number | "latest" | undefined,
81
+ network: Network,
82
+ ) => {
83
+ if (blockNumberOrTag === undefined) {
84
+ return undefined;
85
+ }
86
+
87
+ if (Number.isNaN(blockNumberOrTag)) {
88
+ return undefined;
89
+ }
90
+ if (blockNumberOrTag === "latest") {
91
+ if (perNetworkLatestBlockNumber.has(network.name)) {
92
+ return perNetworkLatestBlockNumber.get(network.name)!;
93
+ } else {
94
+ const blockPromise = network.transport
95
+ .request({
96
+ method: "eth_getBlockByNumber",
97
+ params: ["latest", false],
98
+ })
99
+ .then((block) => {
100
+ if (!block)
101
+ throw new BlockNotFoundError({ blockNumber: "latest" as any });
102
+ return hexToNumber((block as SyncBlock).number);
103
+ })
104
+ .catch((e) => {
105
+ throw new Error(
106
+ `Unable to fetch "latest" block for network '${network.name}':\n${e.message}`,
107
+ );
108
+ });
109
+ perNetworkLatestBlockNumber.set(network.name, blockPromise);
110
+ return blockPromise;
111
+ }
112
+ }
113
+ return blockNumberOrTag;
114
+ };
115
+
76
116
  const networks: Network[] = await Promise.all(
77
117
  Object.entries(config.networks).map(async ([networkName, network]) => {
78
118
  const { chainId, transport } = network;
@@ -217,25 +257,6 @@ export async function buildConfigAndIndexingFunctions({
217
257
  );
218
258
  }
219
259
 
220
- const startBlockMaybeNan = source.startBlock;
221
- const startBlock = Number.isNaN(startBlockMaybeNan)
222
- ? undefined
223
- : startBlockMaybeNan;
224
- const endBlockMaybeNan = source.endBlock;
225
- const endBlock = Number.isNaN(endBlockMaybeNan)
226
- ? undefined
227
- : endBlockMaybeNan;
228
-
229
- if (
230
- startBlock !== undefined &&
231
- endBlock !== undefined &&
232
- endBlock < startBlock
233
- ) {
234
- throw new Error(
235
- `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`,
236
- );
237
- }
238
-
239
260
  const network = networks.find((n) => n.name === source.network);
240
261
  if (!network) {
241
262
  throw new Error(
@@ -246,511 +267,527 @@ export async function buildConfigAndIndexingFunctions({
246
267
  .join(", ")}].`,
247
268
  );
248
269
  }
270
+
271
+ const startBlock = await resolveBlockNumber(source.startBlock, network);
272
+ const endBlock = await resolveBlockNumber(source.endBlock, network);
273
+
274
+ if (
275
+ startBlock !== undefined &&
276
+ endBlock !== undefined &&
277
+ endBlock < startBlock
278
+ ) {
279
+ throw new Error(
280
+ `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`,
281
+ );
282
+ }
249
283
  }
250
284
 
251
- const contractSources: ContractSource[] = flattenSources(
252
- config.contracts ?? {},
253
- )
254
- .flatMap((source): ContractSource[] => {
255
- const network = networks.find((n) => n.name === source.network)!;
256
-
257
- // Get indexing function that were registered for this contract
258
- const registeredLogEvents: string[] = [];
259
- const registeredCallTraceEvents: string[] = [];
260
- for (const eventName of Object.keys(indexingFunctions)) {
261
- // log event
262
- if (eventName.includes(":")) {
263
- const [logContractName, logEventName] = eventName.split(":") as [
264
- string,
265
- string,
266
- ];
267
- if (logContractName === source.name && logEventName !== "setup") {
268
- registeredLogEvents.push(logEventName);
285
+ const contractSources: ContractSource[] = (
286
+ await Promise.all(
287
+ flattenSources(config.contracts ?? {}).map(
288
+ async (source): Promise<ContractSource[]> => {
289
+ const network = networks.find((n) => n.name === source.network)!;
290
+
291
+ // Get indexing function that were registered for this contract
292
+ const registeredLogEvents: string[] = [];
293
+ const registeredCallTraceEvents: string[] = [];
294
+ for (const eventName of Object.keys(indexingFunctions)) {
295
+ // log event
296
+ if (eventName.includes(":")) {
297
+ const [logContractName, logEventName] = eventName.split(":") as [
298
+ string,
299
+ string,
300
+ ];
301
+ if (logContractName === source.name && logEventName !== "setup") {
302
+ registeredLogEvents.push(logEventName);
303
+ }
304
+ }
305
+
306
+ // trace event
307
+ if (eventName.includes(".")) {
308
+ const [functionContractName, functionName] = eventName.split(
309
+ ".",
310
+ ) as [string, string];
311
+ if (functionContractName === source.name) {
312
+ registeredCallTraceEvents.push(functionName);
313
+ }
314
+ }
269
315
  }
270
- }
271
316
 
272
- // trace event
273
- if (eventName.includes(".")) {
274
- const [functionContractName, functionName] = eventName.split(".") as [
275
- string,
276
- string,
277
- ];
278
- if (functionContractName === source.name) {
279
- registeredCallTraceEvents.push(functionName);
317
+ // Note: This can probably throw for invalid ABIs. Consider adding explicit ABI validation before this line.
318
+ const abiEvents = buildAbiEvents({ abi: source.abi });
319
+ const abiFunctions = buildAbiFunctions({ abi: source.abi });
320
+
321
+ const registeredEventSelectors: Hex[] = [];
322
+ // Validate that the registered log events exist in the abi
323
+ for (const logEvent of registeredLogEvents) {
324
+ const abiEvent = abiEvents.bySafeName[logEvent];
325
+ if (abiEvent === undefined) {
326
+ throw new Error(
327
+ `Validation failed: Event name for event '${logEvent}' not found in the contract ABI. Got '${logEvent}', expected one of [${Object.keys(
328
+ abiEvents.bySafeName,
329
+ )
330
+ .map((eventName) => `'${eventName}'`)
331
+ .join(", ")}].`,
332
+ );
333
+ }
334
+
335
+ registeredEventSelectors.push(abiEvent.selector);
280
336
  }
281
- }
282
- }
283
337
 
284
- // Note: This can probably throw for invalid ABIs. Consider adding explicit ABI validation before this line.
285
- const abiEvents = buildAbiEvents({ abi: source.abi });
286
- const abiFunctions = buildAbiFunctions({ abi: source.abi });
287
-
288
- const registeredEventSelectors: Hex[] = [];
289
- // Validate that the registered log events exist in the abi
290
- for (const logEvent of registeredLogEvents) {
291
- const abiEvent = abiEvents.bySafeName[logEvent];
292
- if (abiEvent === undefined) {
293
- throw new Error(
294
- `Validation failed: Event name for event '${logEvent}' not found in the contract ABI. Got '${logEvent}', expected one of [${Object.keys(
295
- abiEvents.bySafeName,
296
- )
297
- .map((eventName) => `'${eventName}'`)
298
- .join(", ")}].`,
299
- );
300
- }
338
+ const registeredFunctionSelectors: Hex[] = [];
339
+ for (const _function of registeredCallTraceEvents) {
340
+ const abiFunction = abiFunctions.bySafeName[_function];
341
+ if (abiFunction === undefined) {
342
+ throw new Error(
343
+ `Validation failed: Function name for function '${_function}' not found in the contract ABI. Got '${_function}', expected one of [${Object.keys(
344
+ abiFunctions.bySafeName,
345
+ )
346
+ .map((eventName) => `'${eventName}'`)
347
+ .join(", ")}].`,
348
+ );
349
+ }
350
+
351
+ registeredFunctionSelectors.push(abiFunction.selector);
352
+ }
301
353
 
302
- registeredEventSelectors.push(abiEvent.selector);
303
- }
354
+ const topicsArray: {
355
+ topic0: LogTopic;
356
+ topic1: LogTopic;
357
+ topic2: LogTopic;
358
+ topic3: LogTopic;
359
+ }[] = [];
360
+
361
+ if (source.filter !== undefined) {
362
+ const eventFilters = Array.isArray(source.filter)
363
+ ? source.filter
364
+ : [source.filter];
365
+
366
+ for (const filter of eventFilters) {
367
+ const abiEvent = abiEvents.bySafeName[filter.event];
368
+ if (!abiEvent) {
369
+ throw new Error(
370
+ `Validation failed: Invalid filter for contract '${
371
+ source.name
372
+ }'. Got event name '${filter.event}', expected one of [${Object.keys(
373
+ abiEvents.bySafeName,
374
+ )
375
+ .map((n) => `'${n}'`)
376
+ .join(", ")}].`,
377
+ );
378
+ }
379
+ }
380
+
381
+ topicsArray.push(...buildTopics(source.abi, eventFilters));
382
+
383
+ // event selectors that have a filter
384
+ const filteredEventSelectors: Hex[] = topicsArray.map(
385
+ (t) => t.topic0 as Hex,
386
+ );
387
+ // event selectors that are registered but don't have a filter
388
+ const excludedRegisteredEventSelectors =
389
+ registeredEventSelectors.filter(
390
+ (s) => filteredEventSelectors.includes(s) === false,
391
+ );
392
+
393
+ for (const selector of filteredEventSelectors) {
394
+ if (registeredEventSelectors.includes(selector) === false) {
395
+ throw new Error(
396
+ `Validation failed: Event selector '${abiEvents.bySelector[selector]?.safeName}' is used in a filter but does not have a corresponding indexing function.`,
397
+ );
398
+ }
399
+ }
400
+
401
+ if (excludedRegisteredEventSelectors.length > 0) {
402
+ topicsArray.push({
403
+ topic0: excludedRegisteredEventSelectors,
404
+ topic1: null,
405
+ topic2: null,
406
+ topic3: null,
407
+ });
408
+ }
409
+ } else {
410
+ topicsArray.push({
411
+ topic0: registeredEventSelectors,
412
+ topic1: null,
413
+ topic2: null,
414
+ topic3: null,
415
+ });
416
+ }
304
417
 
305
- const registeredFunctionSelectors: Hex[] = [];
306
- for (const _function of registeredCallTraceEvents) {
307
- const abiFunction = abiFunctions.bySafeName[_function];
308
- if (abiFunction === undefined) {
309
- throw new Error(
310
- `Validation failed: Function name for function '${_function}' not found in the contract ABI. Got '${_function}', expected one of [${Object.keys(
311
- abiFunctions.bySafeName,
312
- )
313
- .map((eventName) => `'${eventName}'`)
314
- .join(", ")}].`,
418
+ const fromBlock = await resolveBlockNumber(
419
+ source.startBlock,
420
+ network,
315
421
  );
316
- }
422
+ const toBlock = await resolveBlockNumber(source.endBlock, network);
317
423
 
318
- registeredFunctionSelectors.push(abiFunction.selector);
319
- }
424
+ const contractMetadata = {
425
+ type: "contract",
426
+ abi: source.abi,
427
+ abiEvents,
428
+ abiFunctions,
429
+ name: source.name,
430
+ network,
431
+ } as const;
320
432
 
321
- const topicsArray: {
322
- topic0: LogTopic;
323
- topic1: LogTopic;
324
- topic2: LogTopic;
325
- topic3: LogTopic;
326
- }[] = [];
327
-
328
- if (source.filter !== undefined) {
329
- const eventFilters = Array.isArray(source.filter)
330
- ? source.filter
331
- : [source.filter];
332
-
333
- for (const filter of eventFilters) {
334
- const abiEvent = abiEvents.bySafeName[filter.event];
335
- if (!abiEvent) {
336
- throw new Error(
337
- `Validation failed: Invalid filter for contract '${
338
- source.name
339
- }'. Got event name '${filter.event}', expected one of [${Object.keys(
340
- abiEvents.bySafeName,
341
- )
342
- .map((n) => `'${n}'`)
343
- .join(", ")}].`,
433
+ const resolvedAddress = source?.address;
434
+
435
+ if (
436
+ typeof resolvedAddress === "object" &&
437
+ !Array.isArray(resolvedAddress)
438
+ ) {
439
+ // Note that this can throw.
440
+ const logFactory = buildLogFactory({
441
+ chainId: network.chainId,
442
+ ...resolvedAddress,
443
+ });
444
+
445
+ const logSources = topicsArray.map(
446
+ (topics) =>
447
+ ({
448
+ ...contractMetadata,
449
+ filter: {
450
+ type: "log",
451
+ chainId: network.chainId,
452
+ address: logFactory,
453
+ topic0: topics.topic0,
454
+ topic1: topics.topic1,
455
+ topic2: topics.topic2,
456
+ topic3: topics.topic3,
457
+ fromBlock,
458
+ toBlock,
459
+ include: defaultLogFilterInclude.concat(
460
+ source.includeTransactionReceipts
461
+ ? defaultTransactionReceiptInclude
462
+ : [],
463
+ ),
464
+ },
465
+ }) satisfies ContractSource,
344
466
  );
467
+
468
+ if (source.includeCallTraces) {
469
+ return [
470
+ ...logSources,
471
+ {
472
+ ...contractMetadata,
473
+ filter: {
474
+ type: "trace",
475
+ chainId: network.chainId,
476
+ fromAddress: undefined,
477
+ toAddress: logFactory,
478
+ callType: "CALL",
479
+ functionSelector: registeredFunctionSelectors,
480
+ includeReverted: false,
481
+ fromBlock,
482
+ toBlock,
483
+ include: defaultTraceFilterInclude.concat(
484
+ source.includeTransactionReceipts
485
+ ? defaultTransactionReceiptInclude
486
+ : [],
487
+ ),
488
+ },
489
+ } satisfies ContractSource,
490
+ ];
491
+ }
492
+
493
+ return logSources;
494
+ } else if (resolvedAddress !== undefined) {
495
+ for (const address of Array.isArray(resolvedAddress)
496
+ ? resolvedAddress
497
+ : [resolvedAddress]) {
498
+ if (!address!.startsWith("0x"))
499
+ throw new Error(
500
+ `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
501
+ 0,
502
+ 2,
503
+ )}', expected '0x'.`,
504
+ );
505
+ if (address!.length !== 42)
506
+ throw new Error(
507
+ `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
508
+ );
509
+ }
345
510
  }
346
- }
347
511
 
348
- topicsArray.push(...buildTopics(source.abi, eventFilters));
512
+ const validatedAddress = Array.isArray(resolvedAddress)
513
+ ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
514
+ : resolvedAddress !== undefined
515
+ ? toLowerCase(resolvedAddress)
516
+ : undefined;
517
+
518
+ const logSources = topicsArray.map(
519
+ (topics) =>
520
+ ({
521
+ ...contractMetadata,
522
+ filter: {
523
+ type: "log",
524
+ chainId: network.chainId,
525
+ address: validatedAddress,
526
+ topic0: topics.topic0,
527
+ topic1: topics.topic1,
528
+ topic2: topics.topic2,
529
+ topic3: topics.topic3,
530
+ fromBlock,
531
+ toBlock,
532
+ include: defaultLogFilterInclude.concat(
533
+ source.includeTransactionReceipts
534
+ ? defaultTransactionReceiptInclude
535
+ : [],
536
+ ),
537
+ },
538
+ }) satisfies ContractSource,
539
+ );
349
540
 
350
- // event selectors that have a filter
351
- const filteredEventSelectors: Hex[] = topicsArray.map(
352
- (t) => t.topic0 as Hex,
353
- );
354
- // event selectors that are registered but don't have a filter
355
- const excludedRegisteredEventSelectors =
356
- registeredEventSelectors.filter(
357
- (s) => filteredEventSelectors.includes(s) === false,
541
+ if (source.includeCallTraces) {
542
+ return [
543
+ ...logSources,
544
+ {
545
+ ...contractMetadata,
546
+ filter: {
547
+ type: "trace",
548
+ chainId: network.chainId,
549
+ fromAddress: undefined,
550
+ toAddress: Array.isArray(validatedAddress)
551
+ ? validatedAddress
552
+ : validatedAddress === undefined
553
+ ? undefined
554
+ : [validatedAddress],
555
+ callType: "CALL",
556
+ functionSelector: registeredFunctionSelectors,
557
+ includeReverted: false,
558
+ fromBlock,
559
+ toBlock,
560
+ include: defaultTraceFilterInclude.concat(
561
+ source.includeTransactionReceipts
562
+ ? defaultTransactionReceiptInclude
563
+ : [],
564
+ ),
565
+ },
566
+ } satisfies ContractSource,
567
+ ];
568
+ } else return logSources;
569
+ },
570
+ ),
571
+ )
572
+ )
573
+ .flat() // Remove sources with no registered indexing functions
574
+ .filter((source) => {
575
+ const hasNoRegisteredIndexingFunctions =
576
+ source.filter.type === "trace"
577
+ ? Array.isArray(source.filter.functionSelector) &&
578
+ source.filter.functionSelector.length === 0
579
+ : Array.isArray(source.filter.topic0) &&
580
+ source.filter.topic0?.length === 0;
581
+ if (hasNoRegisteredIndexingFunctions) {
582
+ logs.push({
583
+ level: "debug",
584
+ msg: `No indexing functions were registered for '${
585
+ source.name
586
+ }' ${source.filter.type === "trace" ? "traces" : "logs"}`,
587
+ });
588
+ }
589
+ return hasNoRegisteredIndexingFunctions === false;
590
+ });
591
+
592
+ const accountSources: AccountSource[] = (
593
+ await Promise.all(
594
+ flattenSources(config.accounts ?? {}).map(
595
+ async (source): Promise<AccountSource[]> => {
596
+ const network = networks.find((n) => n.name === source.network)!;
597
+
598
+ const fromBlock = await resolveBlockNumber(
599
+ source.startBlock,
600
+ network,
358
601
  );
602
+ const toBlock = await resolveBlockNumber(source.endBlock, network);
359
603
 
360
- for (const selector of filteredEventSelectors) {
361
- if (registeredEventSelectors.includes(selector) === false) {
604
+ const resolvedAddress = source?.address;
605
+
606
+ if (resolvedAddress === undefined) {
362
607
  throw new Error(
363
- `Validation failed: Event selector '${abiEvents.bySelector[selector]?.safeName}' is used in a filter but does not have a corresponding indexing function.`,
608
+ `Validation failed: Account '${source.name}' must specify an 'address'.`,
364
609
  );
365
610
  }
366
- }
367
611
 
368
- if (excludedRegisteredEventSelectors.length > 0) {
369
- topicsArray.push({
370
- topic0: excludedRegisteredEventSelectors,
371
- topic1: null,
372
- topic2: null,
373
- topic3: null,
374
- });
375
- }
376
- } else {
377
- topicsArray.push({
378
- topic0: registeredEventSelectors,
379
- topic1: null,
380
- topic2: null,
381
- topic3: null,
382
- });
383
- }
612
+ if (
613
+ typeof resolvedAddress === "object" &&
614
+ !Array.isArray(resolvedAddress)
615
+ ) {
616
+ // Note that this can throw.
617
+ const logFactory = buildLogFactory({
618
+ chainId: network.chainId,
619
+ ...resolvedAddress,
620
+ });
621
+
622
+ return [
623
+ {
624
+ type: "account",
625
+ name: source.name,
626
+ network,
627
+ filter: {
628
+ type: "transaction",
629
+ chainId: network.chainId,
630
+ fromAddress: undefined,
631
+ toAddress: logFactory,
632
+ includeReverted: false,
633
+ fromBlock,
634
+ toBlock,
635
+ include: defaultTransactionFilterInclude,
636
+ },
637
+ } satisfies AccountSource,
638
+ {
639
+ type: "account",
640
+ name: source.name,
641
+ network,
642
+ filter: {
643
+ type: "transaction",
644
+ chainId: network.chainId,
645
+ fromAddress: logFactory,
646
+ toAddress: undefined,
647
+ includeReverted: false,
648
+ fromBlock,
649
+ toBlock,
650
+ include: defaultTransactionFilterInclude,
651
+ },
652
+ } satisfies AccountSource,
653
+ {
654
+ type: "account",
655
+ name: source.name,
656
+ network,
657
+ filter: {
658
+ type: "transfer",
659
+ chainId: network.chainId,
660
+ fromAddress: undefined,
661
+ toAddress: logFactory,
662
+ includeReverted: false,
663
+ fromBlock,
664
+ toBlock,
665
+ include: defaultTransferFilterInclude.concat(
666
+ source.includeTransactionReceipts
667
+ ? defaultTransactionReceiptInclude
668
+ : [],
669
+ ),
670
+ },
671
+ } satisfies AccountSource,
672
+ {
673
+ type: "account",
674
+ name: source.name,
675
+ network,
676
+ filter: {
677
+ type: "transfer",
678
+ chainId: network.chainId,
679
+ fromAddress: logFactory,
680
+ toAddress: undefined,
681
+ includeReverted: false,
682
+ fromBlock,
683
+ toBlock,
684
+ include: defaultTransferFilterInclude.concat(
685
+ source.includeTransactionReceipts
686
+ ? defaultTransactionReceiptInclude
687
+ : [],
688
+ ),
689
+ },
690
+ } satisfies AccountSource,
691
+ ];
692
+ }
384
693
 
385
- const startBlockMaybeNan = source.startBlock;
386
- const fromBlock = Number.isNaN(startBlockMaybeNan)
387
- ? undefined
388
- : startBlockMaybeNan;
389
- const endBlockMaybeNan = source.endBlock;
390
- const toBlock = Number.isNaN(endBlockMaybeNan)
391
- ? undefined
392
- : endBlockMaybeNan;
393
-
394
- const contractMetadata = {
395
- type: "contract",
396
- abi: source.abi,
397
- abiEvents,
398
- abiFunctions,
399
- name: source.name,
400
- network,
401
- } as const;
402
-
403
- const resolvedAddress = source?.address;
694
+ for (const address of Array.isArray(resolvedAddress)
695
+ ? resolvedAddress
696
+ : [resolvedAddress]) {
697
+ if (!address!.startsWith("0x"))
698
+ throw new Error(
699
+ `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
700
+ 0,
701
+ 2,
702
+ )}', expected '0x'.`,
703
+ );
704
+ if (address!.length !== 42)
705
+ throw new Error(
706
+ `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
707
+ );
708
+ }
404
709
 
405
- if (
406
- typeof resolvedAddress === "object" &&
407
- !Array.isArray(resolvedAddress)
408
- ) {
409
- // Note that this can throw.
410
- const logFactory = buildLogFactory({
411
- chainId: network.chainId,
412
- ...resolvedAddress,
413
- });
710
+ const validatedAddress = Array.isArray(resolvedAddress)
711
+ ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
712
+ : resolvedAddress !== undefined
713
+ ? toLowerCase(resolvedAddress)
714
+ : undefined;
414
715
 
415
- const logSources = topicsArray.map(
416
- (topics) =>
417
- ({
418
- ...contractMetadata,
716
+ return [
717
+ {
718
+ type: "account",
719
+ name: source.name,
720
+ network,
419
721
  filter: {
420
- type: "log",
722
+ type: "transaction",
421
723
  chainId: network.chainId,
422
- address: logFactory,
423
- topic0: topics.topic0,
424
- topic1: topics.topic1,
425
- topic2: topics.topic2,
426
- topic3: topics.topic3,
724
+ fromAddress: undefined,
725
+ toAddress: validatedAddress,
726
+ includeReverted: false,
427
727
  fromBlock,
428
728
  toBlock,
429
- include: defaultLogFilterInclude.concat(
729
+ include: defaultTransactionFilterInclude,
730
+ },
731
+ } satisfies AccountSource,
732
+ {
733
+ type: "account",
734
+ name: source.name,
735
+ network,
736
+ filter: {
737
+ type: "transaction",
738
+ chainId: network.chainId,
739
+ fromAddress: validatedAddress,
740
+ toAddress: undefined,
741
+ includeReverted: false,
742
+ fromBlock,
743
+ toBlock,
744
+ include: defaultTransactionFilterInclude,
745
+ },
746
+ } satisfies AccountSource,
747
+ {
748
+ type: "account",
749
+ name: source.name,
750
+ network,
751
+ filter: {
752
+ type: "transfer",
753
+ chainId: network.chainId,
754
+ fromAddress: undefined,
755
+ toAddress: validatedAddress,
756
+ includeReverted: false,
757
+ fromBlock,
758
+ toBlock,
759
+ include: defaultTransferFilterInclude.concat(
430
760
  source.includeTransactionReceipts
431
761
  ? defaultTransactionReceiptInclude
432
762
  : [],
433
763
  ),
434
764
  },
435
- }) satisfies ContractSource,
436
- );
437
-
438
- if (source.includeCallTraces) {
439
- return [
440
- ...logSources,
765
+ } satisfies AccountSource,
441
766
  {
442
- ...contractMetadata,
767
+ type: "account",
768
+ name: source.name,
769
+ network,
443
770
  filter: {
444
- type: "trace",
771
+ type: "transfer",
445
772
  chainId: network.chainId,
446
- fromAddress: undefined,
447
- toAddress: logFactory,
448
- callType: "CALL",
449
- functionSelector: registeredFunctionSelectors,
773
+ fromAddress: validatedAddress,
774
+ toAddress: undefined,
450
775
  includeReverted: false,
451
776
  fromBlock,
452
777
  toBlock,
453
- include: defaultTraceFilterInclude.concat(
778
+ include: defaultTransferFilterInclude.concat(
454
779
  source.includeTransactionReceipts
455
780
  ? defaultTransactionReceiptInclude
456
781
  : [],
457
782
  ),
458
783
  },
459
- } satisfies ContractSource,
784
+ } satisfies AccountSource,
460
785
  ];
461
- }
462
-
463
- return logSources;
464
- } else if (resolvedAddress !== undefined) {
465
- for (const address of Array.isArray(resolvedAddress)
466
- ? resolvedAddress
467
- : [resolvedAddress]) {
468
- if (!address!.startsWith("0x"))
469
- throw new Error(
470
- `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
471
- 0,
472
- 2,
473
- )}', expected '0x'.`,
474
- );
475
- if (address!.length !== 42)
476
- throw new Error(
477
- `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
478
- );
479
- }
480
- }
481
-
482
- const validatedAddress = Array.isArray(resolvedAddress)
483
- ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
484
- : resolvedAddress !== undefined
485
- ? toLowerCase(resolvedAddress)
486
- : undefined;
487
-
488
- const logSources = topicsArray.map(
489
- (topics) =>
490
- ({
491
- ...contractMetadata,
492
- filter: {
493
- type: "log",
494
- chainId: network.chainId,
495
- address: validatedAddress,
496
- topic0: topics.topic0,
497
- topic1: topics.topic1,
498
- topic2: topics.topic2,
499
- topic3: topics.topic3,
500
- fromBlock,
501
- toBlock,
502
- include: defaultLogFilterInclude.concat(
503
- source.includeTransactionReceipts
504
- ? defaultTransactionReceiptInclude
505
- : [],
506
- ),
507
- },
508
- }) satisfies ContractSource,
509
- );
510
-
511
- if (source.includeCallTraces) {
512
- return [
513
- ...logSources,
514
- {
515
- ...contractMetadata,
516
- filter: {
517
- type: "trace",
518
- chainId: network.chainId,
519
- fromAddress: undefined,
520
- toAddress: Array.isArray(validatedAddress)
521
- ? validatedAddress
522
- : validatedAddress === undefined
523
- ? undefined
524
- : [validatedAddress],
525
- callType: "CALL",
526
- functionSelector: registeredFunctionSelectors,
527
- includeReverted: false,
528
- fromBlock,
529
- toBlock,
530
- include: defaultTraceFilterInclude.concat(
531
- source.includeTransactionReceipts
532
- ? defaultTransactionReceiptInclude
533
- : [],
534
- ),
535
- },
536
- } satisfies ContractSource,
537
- ];
538
- } else return logSources;
539
- }) // Remove sources with no registered indexing functions
540
- .filter((source) => {
541
- const hasNoRegisteredIndexingFunctions =
542
- source.filter.type === "trace"
543
- ? Array.isArray(source.filter.functionSelector) &&
544
- source.filter.functionSelector.length === 0
545
- : Array.isArray(source.filter.topic0) &&
546
- source.filter.topic0?.length === 0;
547
- if (hasNoRegisteredIndexingFunctions) {
548
- logs.push({
549
- level: "debug",
550
- msg: `No indexing functions were registered for '${
551
- source.name
552
- }' ${source.filter.type === "trace" ? "traces" : "logs"}`,
553
- });
554
- }
555
- return hasNoRegisteredIndexingFunctions === false;
556
- });
557
-
558
- const accountSources: AccountSource[] = flattenSources(config.accounts ?? {})
559
- .flatMap((source): AccountSource[] => {
560
- const network = networks.find((n) => n.name === source.network)!;
561
-
562
- const startBlockMaybeNan = source.startBlock;
563
- const fromBlock = Number.isNaN(startBlockMaybeNan)
564
- ? undefined
565
- : startBlockMaybeNan;
566
- const endBlockMaybeNan = source.endBlock;
567
- const toBlock = Number.isNaN(endBlockMaybeNan)
568
- ? undefined
569
- : endBlockMaybeNan;
570
-
571
- const resolvedAddress = source?.address;
572
-
573
- if (resolvedAddress === undefined) {
574
- throw new Error(
575
- `Validation failed: Account '${source.name}' must specify an 'address'.`,
576
- );
577
- }
578
-
579
- if (
580
- typeof resolvedAddress === "object" &&
581
- !Array.isArray(resolvedAddress)
582
- ) {
583
- // Note that this can throw.
584
- const logFactory = buildLogFactory({
585
- chainId: network.chainId,
586
- ...resolvedAddress,
587
- });
588
-
589
- return [
590
- {
591
- type: "account",
592
- name: source.name,
593
- network,
594
- filter: {
595
- type: "transaction",
596
- chainId: network.chainId,
597
- fromAddress: undefined,
598
- toAddress: logFactory,
599
- includeReverted: false,
600
- fromBlock,
601
- toBlock,
602
- include: defaultTransactionFilterInclude,
603
- },
604
- } satisfies AccountSource,
605
- {
606
- type: "account",
607
- name: source.name,
608
- network,
609
- filter: {
610
- type: "transaction",
611
- chainId: network.chainId,
612
- fromAddress: logFactory,
613
- toAddress: undefined,
614
- includeReverted: false,
615
- fromBlock,
616
- toBlock,
617
- include: defaultTransactionFilterInclude,
618
- },
619
- } satisfies AccountSource,
620
- {
621
- type: "account",
622
- name: source.name,
623
- network,
624
- filter: {
625
- type: "transfer",
626
- chainId: network.chainId,
627
- fromAddress: undefined,
628
- toAddress: logFactory,
629
- includeReverted: false,
630
- fromBlock,
631
- toBlock,
632
- include: defaultTransferFilterInclude.concat(
633
- source.includeTransactionReceipts
634
- ? defaultTransactionReceiptInclude
635
- : [],
636
- ),
637
- },
638
- } satisfies AccountSource,
639
- {
640
- type: "account",
641
- name: source.name,
642
- network,
643
- filter: {
644
- type: "transfer",
645
- chainId: network.chainId,
646
- fromAddress: logFactory,
647
- toAddress: undefined,
648
- includeReverted: false,
649
- fromBlock,
650
- toBlock,
651
- include: defaultTransferFilterInclude.concat(
652
- source.includeTransactionReceipts
653
- ? defaultTransactionReceiptInclude
654
- : [],
655
- ),
656
- },
657
- } satisfies AccountSource,
658
- ];
659
- }
660
-
661
- for (const address of Array.isArray(resolvedAddress)
662
- ? resolvedAddress
663
- : [resolvedAddress]) {
664
- if (!address!.startsWith("0x"))
665
- throw new Error(
666
- `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
667
- 0,
668
- 2,
669
- )}', expected '0x'.`,
670
- );
671
- if (address!.length !== 42)
672
- throw new Error(
673
- `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
674
- );
675
- }
676
-
677
- const validatedAddress = Array.isArray(resolvedAddress)
678
- ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
679
- : resolvedAddress !== undefined
680
- ? toLowerCase(resolvedAddress)
681
- : undefined;
682
-
683
- return [
684
- {
685
- type: "account",
686
- name: source.name,
687
- network,
688
- filter: {
689
- type: "transaction",
690
- chainId: network.chainId,
691
- fromAddress: undefined,
692
- toAddress: validatedAddress,
693
- includeReverted: false,
694
- fromBlock,
695
- toBlock,
696
- include: defaultTransactionFilterInclude,
697
- },
698
- } satisfies AccountSource,
699
- {
700
- type: "account",
701
- name: source.name,
702
- network,
703
- filter: {
704
- type: "transaction",
705
- chainId: network.chainId,
706
- fromAddress: validatedAddress,
707
- toAddress: undefined,
708
- includeReverted: false,
709
- fromBlock,
710
- toBlock,
711
- include: defaultTransactionFilterInclude,
712
- },
713
- } satisfies AccountSource,
714
- {
715
- type: "account",
716
- name: source.name,
717
- network,
718
- filter: {
719
- type: "transfer",
720
- chainId: network.chainId,
721
- fromAddress: undefined,
722
- toAddress: validatedAddress,
723
- includeReverted: false,
724
- fromBlock,
725
- toBlock,
726
- include: defaultTransferFilterInclude.concat(
727
- source.includeTransactionReceipts
728
- ? defaultTransactionReceiptInclude
729
- : [],
730
- ),
731
- },
732
- } satisfies AccountSource,
733
- {
734
- type: "account",
735
- name: source.name,
736
- network,
737
- filter: {
738
- type: "transfer",
739
- chainId: network.chainId,
740
- fromAddress: validatedAddress,
741
- toAddress: undefined,
742
- includeReverted: false,
743
- fromBlock,
744
- toBlock,
745
- include: defaultTransferFilterInclude.concat(
746
- source.includeTransactionReceipts
747
- ? defaultTransactionReceiptInclude
748
- : [],
749
- ),
750
- },
751
- } satisfies AccountSource,
752
- ];
753
- })
786
+ },
787
+ ),
788
+ )
789
+ )
790
+ .flat()
754
791
  .filter((source) => {
755
792
  const eventName =
756
793
  source.filter.type === "transaction"
@@ -772,43 +809,41 @@ export async function buildConfigAndIndexingFunctions({
772
809
  return hasRegisteredIndexingFunction;
773
810
  });
774
811
 
775
- const blockSources: BlockSource[] = flattenSources(config.blocks ?? {})
776
- .map((source) => {
777
- const network = networks.find((n) => n.name === source.network)!;
812
+ const blockSources: BlockSource[] = (
813
+ await Promise.all(
814
+ flattenSources(config.blocks ?? {}).map(async (source) => {
815
+ const network = networks.find((n) => n.name === source.network)!;
778
816
 
779
- const intervalMaybeNan = source.interval ?? 1;
780
- const interval = Number.isNaN(intervalMaybeNan) ? 0 : intervalMaybeNan;
817
+ const intervalMaybeNan = source.interval ?? 1;
818
+ const interval = Number.isNaN(intervalMaybeNan) ? 0 : intervalMaybeNan;
781
819
 
782
- if (!Number.isInteger(interval) || interval === 0) {
783
- throw new Error(
784
- `Validation failed: Invalid interval for block source '${source.name}'. Got ${interval}, expected a non-zero integer.`,
785
- );
786
- }
820
+ if (!Number.isInteger(interval) || interval === 0) {
821
+ throw new Error(
822
+ `Validation failed: Invalid interval for block source '${source.name}'. Got ${interval}, expected a non-zero integer.`,
823
+ );
824
+ }
787
825
 
788
- const startBlockMaybeNan = source.startBlock;
789
- const fromBlock = Number.isNaN(startBlockMaybeNan)
790
- ? undefined
791
- : startBlockMaybeNan;
792
- const endBlockMaybeNan = source.endBlock;
793
- const toBlock = Number.isNaN(endBlockMaybeNan)
794
- ? undefined
795
- : endBlockMaybeNan;
826
+ const fromBlock = await resolveBlockNumber(source.startBlock, network);
827
+ const toBlock = await resolveBlockNumber(source.endBlock, network);
796
828
 
797
- return {
798
- type: "block",
799
- name: source.name,
800
- network,
801
- filter: {
829
+ return {
802
830
  type: "block",
803
- chainId: network.chainId,
804
- interval: interval,
805
- offset: (fromBlock ?? 0) % interval,
806
- fromBlock,
807
- toBlock,
808
- include: defaultBlockFilterInclude,
809
- },
810
- } satisfies BlockSource;
811
- })
831
+ name: source.name,
832
+ network,
833
+ filter: {
834
+ type: "block",
835
+ chainId: network.chainId,
836
+ interval: interval,
837
+ offset: (fromBlock ?? 0) % interval,
838
+ fromBlock,
839
+ toBlock,
840
+ include: defaultBlockFilterInclude,
841
+ },
842
+ } satisfies BlockSource;
843
+ }),
844
+ )
845
+ )
846
+ .flat()
812
847
  .filter((blockSource) => {
813
848
  const hasRegisteredIndexingFunction =
814
849
  indexingFunctions[`${blockSource.name}:block`] !== undefined;