Repository: ponder-sh/ponder
Pull request #1485 (2025-02-04 19:57:11)

config: latest tag
Added latest tag to the config for startBlock and endBlock of the account, contract and block sources. This would be resolved to the `blockNumber` of the latest block per network at the startup. 


Patch:
diff --git a/.changeset/cool-points-drum.md b/.changeset/cool-points-drum.md
deleted file mode 100644
index 69d8d5e1..00000000
--- a/.changeset/cool-points-drum.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-"ponder": patch
----
-
-Removed metrics `ponder_indexing_total_seconds` and `ponder_indexing_completed_seconds`. Added metrics `ponder_historical_total_indexing_seconds`, `ponder_historical_cached_indexing_seconds`, and `ponder_historical_completed_indexing_seconds`. Renamed metric `ponder_indexing_completed_timestamp` to `ponder_indexing_timestamp`.
-
diff --git a/.changeset/grumpy-pots-clap.md b/.changeset/grumpy-pots-clap.md
deleted file mode 100644
index 64d675a8..00000000
--- a/.changeset/grumpy-pots-clap.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"ponder": patch
----
-
-Improved shutdown behavior and hot reload reliability.
diff --git a/.changeset/late-peas-fail.md b/.changeset/late-peas-fail.md
new file mode 100644
index 00000000..155a97e6
--- /dev/null
+++ b/.changeset/late-peas-fail.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Added support for the `"latest"` block tag as a `startBlock` or `endBlock` value in `ponder.config.ts`. This option fetches the latest block during the build step and uses that block number.
\ No newline at end of file
diff --git a/.changeset/nasty-chairs-switch.md b/.changeset/nasty-chairs-switch.md
deleted file mode 100644
index 9c791c7a..00000000
--- a/.changeset/nasty-chairs-switch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"ponder": patch
----
-
-Added debug and info-level logs to the realtime and historical sync to improve observability.
\ No newline at end of file
diff --git a/.changeset/nice-tigers-promise.md b/.changeset/nice-tigers-promise.md
deleted file mode 100644
index 608c4936..00000000
--- a/.changeset/nice-tigers-promise.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"ponder": patch
----
-
-Added a new optional `ordering` field to `ponder.config.ts`, which specifies how events across multiple chains should be ordered. The options are `"omnichain"` (default, current behavior) and `"multichain"` (new strategy, opt-in). [Read more](https://ponder.sh/docs/api-reference/config#event-ordering).
\ No newline at end of file
diff --git a/docs/pages/docs/api-reference/config.mdx b/docs/pages/docs/api-reference/config.mdx
index a044be6f..4ad56849 100644
--- a/docs/pages/docs/api-reference/config.mdx
+++ b/docs/pages/docs/api-reference/config.mdx
@@ -114,8 +114,8 @@ The `contracts` field is an object where each key is a contract name containing
 | **network**                    |                       `string`                       | The name of the network this contract is deployed to. References the `networks` field. Also supports [multiple networks](/docs/contracts-and-networks#multiple-networks).                                                |
 | **address**                    | `0x{string} \| 0x{string}[] \| Factory \| undefined` | One or more contract addresses or factory configuration.                                                                                                                                                                 |
 | **filter**                     |                 [`Filter`](#filter)                  | Event filter criteria.                                                                                                                                                                                                   |
-| **startBlock**                 |                `number \| undefined`                 | **Default: `0`**. Block number to start syncing events. Usually set to the contract deployment block number.                                                                                                             |
-| **endBlock**                   |                `number \| undefined`                 | **Default: `undefined`**. Block number to stop syncing events. If this field is specified, the contract will not be indexed in realtime. This field can be used alongside `startBlock` to index a specific block range.  |
+| **startBlock**                 |          `number \| "latest" \| undefined`           | **Default: `0`**. Block number or tag to start indexing. Usually set to the contract deployment block number.                                                                                                            |
+| **endBlock**                   |          `number \| "latest" \| undefined`           | **Default: `undefined`**. Block number or tag to stop indexing. If this field is specified, the contract will not be indexed in realtime. This field can be used alongside `startBlock` to index a specific block range. |
 | **includeTransactionReceipts** |                `boolean \| undefined`                | **Default: `false`**. If this field is `true`, `transactionReceipt` will be included in `event`.                                                                                                                         |
 | **includeCallTraces**          |                `boolean \| undefined`                | **Default: `false`**. If this field is `true`, each function in the abi will be available as an indexing function event name. See the [call traces guide](/docs/call-traces#register-an-indexing-function) for details.  |
 
diff --git a/docs/pages/docs/block-intervals.mdx b/docs/pages/docs/block-intervals.mdx
index 734d5f5b..dd9151cf 100644
--- a/docs/pages/docs/block-intervals.mdx
+++ b/docs/pages/docs/block-intervals.mdx
@@ -164,6 +164,19 @@ The optional `startBlock` and `endBlock` options specify the block range to inde
 
 If `endBlock` is defined, no events will be indexed after that block number. This option is useful if you're only interested in a slice of historical data, or to enable faster feedback loops during development where it's not necessary to index the entire history.
 
+## Block range
+
+The `startBlock` and `endBlock` options specify the block range that this interval should run in.
+
+| field          |               type                |                                                                 |
+| :------------- | :-------------------------------: | :-------------------------------------------------------------- |
+| **startBlock** | `number \| "latest" \| undefined` | **Default: `0`**. Block number or tag to start indexing.        |
+| **endBlock**   | `number \| "latest" \| undefined` | **Default: `undefined`**. Block number or tag to stop indexing. |
+
+The `"latest"{:ts}` option fetches the latest block during the build step and uses that block number. Use this option with `startBlock` to only index in realtime, or `endBlock` to only index historical data.
+
+When `endBlock` is `undefined{:ts}`, indexing will continue indefinitely in realtime. Set `endBlock` to index only a slice of history, or to enable faster feedback loops during development.
+
 ```ts filename="ponder.config.ts" {9}
 import { createConfig } from "ponder";
 import { http } from "viem";
diff --git a/docs/pages/docs/contracts-and-networks.mdx b/docs/pages/docs/contracts-and-networks.mdx
index 1c0b06f9..1572c194 100644
--- a/docs/pages/docs/contracts-and-networks.mdx
+++ b/docs/pages/docs/contracts-and-networks.mdx
@@ -538,7 +538,7 @@ ponder.on("ERC20:Transfer", async ({ event }) => {
 
 The `includeCallTraces{:ts}` option specifies whether to enable call trace indexing for a contract. By default, call traces are **disabled**.
 
-After enabling this option, each function in the contract ABI will become available as an indexing function event name. [Read more](/docs/indexing/call-traces) about call traces.
+After enabling this option, each function in the contract ABI will become available as an indexing function event name. [Read more](/docs/call-traces) about call traces.
 
 <div className="code-columns">
 
@@ -616,12 +616,14 @@ ponder.on("Blitmap.mintOriginal()", async ({ event }) => {
 
 The optional `startBlock` and `endBlock` options specify the block range to index.
 
-| Option       | Default          |
-| :----------- | :--------------- |
-| `startBlock` | `0{:ts}`         |
-| `endBlock`   | `undefined{:ts}` |
+| field          |               type                |                                                                                                               |
+| :------------- | :-------------------------------: | :------------------------------------------------------------------------------------------------------------ |
+| **startBlock** | `number \| "latest" \| undefined` | **Default: `0`**. Block number or tag to start indexing. Usually set to the contract deployment block number. |
+| **endBlock**   | `number \| "latest" \| undefined` | **Default: `undefined`**. Block number or tag to stop indexing.                                               |
 
-If `endBlock` is `undefined{:ts}`, the contract will be indexed in realtime. If `endBlock` is defined, no events will be indexed after that block. This option can be useful if you're only interested in a slice of historical data, or to enable faster feedback loops during development where it's not necessary to index the entire history.
+The `"latest"{:ts}` option fetches the latest block during the build step and uses that block number. Use this option with `startBlock` to only index in realtime, or `endBlock` to only index historical data.
+
+When `endBlock` is `undefined{:ts}`, indexing will continue indefinitely in realtime. Set `endBlock` to index only a slice of history, or to enable faster feedback loops during development.
 
 ```ts filename="ponder.config.ts" {15-16}
 import { createConfig } from "ponder";
@@ -638,7 +640,7 @@ export default createConfig({
       abi: BlitmapAbi,
       network: "mainnet",
       address: "0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63",
-      startBlock: 12439123,
+      startBlock: "latest",
       endBlock: 16500000,
     },
   },
diff --git a/examples/feature-accounts/ponder.config.ts b/examples/feature-accounts/ponder.config.ts
index 0ba6e428..67341319 100644
--- a/examples/feature-accounts/ponder.config.ts
+++ b/examples/feature-accounts/ponder.config.ts
@@ -1,9 +1,5 @@
 import { createConfig } from "ponder";
-import { http, createPublicClient } from "viem";
-
-const latestBlockMainnet = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_1),
-}).getBlock();
+import { http } from "viem";
 
 export default createConfig({
   networks: {
@@ -15,7 +11,7 @@ export default createConfig({
   accounts: {
     BeaverBuilder: {
       network: "mainnet",
-      startBlock: Number(latestBlockMainnet.number) - 100,
+      startBlock: "latest",
       address: "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5",
     },
   },
diff --git a/examples/feature-multichain/ponder.config.ts b/examples/feature-multichain/ponder.config.ts
index 9dc5cf94..76bd0044 100644
--- a/examples/feature-multichain/ponder.config.ts
+++ b/examples/feature-multichain/ponder.config.ts
@@ -1,64 +1,37 @@
 import { createConfig } from "ponder";
-import { http, createPublicClient } from "viem";
+import { http } from "viem";
 
 import { weth9Abi } from "./abis/weth9Abi";
 
-const latestBlockMainnet = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_1),
-}).getBlock();
-const latestBlockBase = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_8453),
-}).getBlock();
-const latestBlockOptimism = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_10),
-}).getBlock();
-const latestBlockPolygon = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_137),
-}).getBlock();
-
 export default createConfig({
   ordering: "multichain",
   networks: {
     mainnet: {
       chainId: 1,
       transport: http(process.env.PONDER_RPC_URL_1),
-      // pollingInterval: 15_000,
     },
     base: {
       chainId: 8453,
       transport: http(process.env.PONDER_RPC_URL_8453),
-      // pollingInterval: 15_000,
     },
     optimism: {
       chainId: 10,
       transport: http(process.env.PONDER_RPC_URL_10),
-      // pollingInterval: 15_000,
     },
     polygon: {
       chainId: 137,
       transport: http(process.env.PONDER_RPC_URL_137),
-      // pollingInterval: 15_000,
     },
   },
   contracts: {
     weth9: {
       abi: weth9Abi,
-      address: "0x4200000000000000000000000000000000000006",
+      startBlock: "latest",
       network: {
-        mainnet: {
-          address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
-          startBlock: Number(latestBlockMainnet.number),
-        },
-        base: {
-          startBlock: Number(latestBlockBase.number),
-        },
-        optimism: {
-          startBlock: Number(latestBlockOptimism.number),
-        },
-        polygon: {
-          address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
-          startBlock: Number(latestBlockPolygon.number),
-        },
+        mainnet: { address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" },
+        base: { address: "0x4200000000000000000000000000000000000006" },
+        optimism: { address: "0x4200000000000000000000000000000000000006" },
+        polygon: { address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619" },
       },
     },
   },
diff --git a/examples/with-client/ponder/ponder.config.ts b/examples/with-client/ponder/ponder.config.ts
index 754ea87a..afc24270 100644
--- a/examples/with-client/ponder/ponder.config.ts
+++ b/examples/with-client/ponder/ponder.config.ts
@@ -1,12 +1,8 @@
 import { createConfig } from "ponder";
-import { http, createPublicClient } from "viem";
+import { http } from "viem";
 
 import { weth9Abi } from "./abis/weth9Abi";
 
-const latestBlockBase = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_8453),
-}).getBlock();
-
 export default createConfig({
   networks: {
     base: {
@@ -19,7 +15,7 @@ export default createConfig({
       abi: weth9Abi,
       network: "base",
       address: "0x4200000000000000000000000000000000000006",
-      startBlock: Number(latestBlockBase.number),
+      startBlock: "latest",
     },
   },
 });
diff --git a/examples/with-nextjs/ponder/ponder.config.ts b/examples/with-nextjs/ponder/ponder.config.ts
index 1049be79..412ea264 100644
--- a/examples/with-nextjs/ponder/ponder.config.ts
+++ b/examples/with-nextjs/ponder/ponder.config.ts
@@ -1,12 +1,8 @@
 import { createConfig } from "ponder";
-import { http, createPublicClient } from "viem";
+import { http } from "viem";
 
 import { weth9Abi } from "./abis/Weth9Abi";
 
-const latestBlockBase = await createPublicClient({
-  transport: http(process.env.PONDER_RPC_URL_8453),
-}).getBlock();
-
 export default createConfig({
   networks: {
     base: {
@@ -19,7 +15,7 @@ export default createConfig({
       abi: weth9Abi,
       network: "base",
       address: "0x4200000000000000000000000000000000000006",
-      startBlock: Number(latestBlockBase.number),
+      startBlock: "latest",
     },
   },
 });
diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md
index 9279a45d..2a4c4e39 100644
--- a/packages/client/CHANGELOG.md
+++ b/packages/client/CHANGELOG.md
@@ -1,5 +1,7 @@
 # @ponder/client
 
+## 0.9.3
+
 ## 0.9.2
 
 ## 0.9.1
diff --git a/packages/client/package.json b/packages/client/package.json
index c58f83f2..ff519a79 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/client",
-  "version": "0.9.2",
+  "version": "0.9.3",
   "description": "",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 808f3150..11f946c3 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,17 @@
 # ponder
 
+## 0.9.3
+
+### Patch Changes
+
+- [#1441](https://github.com/ponder-sh/ponder/pull/1441) [`626e5244aca1d32ad4686d434975e28b691f618b`](https://github.com/ponder-sh/ponder/commit/626e5244aca1d32ad4686d434975e28b691f618b) Thanks [@kyscott18](https://github.com/kyscott18)! - Removed metrics `ponder_indexing_total_seconds` and `ponder_indexing_completed_seconds`. Added metrics `ponder_historical_total_indexing_seconds`, `ponder_historical_cached_indexing_seconds`, and `ponder_historical_completed_indexing_seconds`. Renamed metric `ponder_indexing_completed_timestamp` to `ponder_indexing_timestamp`.
+
+- [#1441](https://github.com/ponder-sh/ponder/pull/1441) [`626e5244aca1d32ad4686d434975e28b691f618b`](https://github.com/ponder-sh/ponder/commit/626e5244aca1d32ad4686d434975e28b691f618b) Thanks [@kyscott18](https://github.com/kyscott18)! - Improved shutdown behavior and hot reload reliability.
+
+- [#1441](https://github.com/ponder-sh/ponder/pull/1441) [`626e5244aca1d32ad4686d434975e28b691f618b`](https://github.com/ponder-sh/ponder/commit/626e5244aca1d32ad4686d434975e28b691f618b) Thanks [@kyscott18](https://github.com/kyscott18)! - Added debug and info-level logs to the realtime and historical sync to improve observability.
+
+- [#1441](https://github.com/ponder-sh/ponder/pull/1441) [`626e5244aca1d32ad4686d434975e28b691f618b`](https://github.com/ponder-sh/ponder/commit/626e5244aca1d32ad4686d434975e28b691f618b) Thanks [@kyscott18](https://github.com/kyscott18)! - Added a new optional `ordering` field to `ponder.config.ts`, which specifies how events across multiple chains should be ordered. The options are `"omnichain"` (default, current behavior) and `"multichain"` (new strategy, opt-in). [Read more](https://ponder.sh/docs/api-reference/config#event-ordering).
+
 ## 0.9.2
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index e7fa8a43..a909223b 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ponder",
-  "version": "0.9.2",
+  "version": "0.9.3",
   "description": "An open-source framework for crypto application backends",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/src/build/configAndIndexingFunctions.ts b/packages/core/src/build/configAndIndexingFunctions.ts
index 8bc128f8..3171e5c7 100644
--- a/packages/core/src/build/configAndIndexingFunctions.ts
+++ b/packages/core/src/build/configAndIndexingFunctions.ts
@@ -23,10 +23,11 @@ import {
   defaultTransactionReceiptInclude,
   defaultTransferFilterInclude,
 } from "@/sync/filter.js";
+import type { SyncBlock } from "@/types/sync.js";
 import { chains } from "@/utils/chains.js";
 import { toLowerCase } from "@/utils/lowercase.js";
 import { dedupe } from "@ponder/common";
-import type { Hex, LogTopic } from "viem";
+import { BlockNotFoundError, type Hex, type LogTopic, hexToNumber } from "viem";
 import { buildLogFactory } from "./factory.js";
 
 const flattenSources = <
@@ -73,6 +74,45 @@ export async function buildConfigAndIndexingFunctions({
 }> {
   const logs: { level: "warn" | "info" | "debug"; msg: string }[] = [];
 
+  const perNetworkLatestBlockNumber = new Map<string, Promise<number>>();
+
+  const resolveBlockNumber = async (
+    blockNumberOrTag: number | "latest" | undefined,
+    network: Network,
+  ) => {
+    if (blockNumberOrTag === undefined) {
+      return undefined;
+    }
+
+    if (Number.isNaN(blockNumberOrTag)) {
+      return undefined;
+    }
+    if (blockNumberOrTag === "latest") {
+      if (perNetworkLatestBlockNumber.has(network.name)) {
+        return perNetworkLatestBlockNumber.get(network.name)!;
+      } else {
+        const blockPromise = network.transport
+          .request({
+            method: "eth_getBlockByNumber",
+            params: ["latest", false],
+          })
+          .then((block) => {
+            if (!block)
+              throw new BlockNotFoundError({ blockNumber: "latest" as any });
+            return hexToNumber((block as SyncBlock).number);
+          })
+          .catch((e) => {
+            throw new Error(
+              `Unable to fetch "latest" block for network '${network.name}':\n${e.message}`,
+            );
+          });
+        perNetworkLatestBlockNumber.set(network.name, blockPromise);
+        return blockPromise;
+      }
+    }
+    return blockNumberOrTag;
+  };
+
   const networks: Network[] = await Promise.all(
     Object.entries(config.networks).map(async ([networkName, network]) => {
       const { chainId, transport } = network;
@@ -217,25 +257,6 @@ export async function buildConfigAndIndexingFunctions({
       );
     }
 
-    const startBlockMaybeNan = source.startBlock;
-    const startBlock = Number.isNaN(startBlockMaybeNan)
-      ? undefined
-      : startBlockMaybeNan;
-    const endBlockMaybeNan = source.endBlock;
-    const endBlock = Number.isNaN(endBlockMaybeNan)
-      ? undefined
-      : endBlockMaybeNan;
-
-    if (
-      startBlock !== undefined &&
-      endBlock !== undefined &&
-      endBlock < startBlock
-    ) {
-      throw new Error(
-        `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`,
-      );
-    }
-
     const network = networks.find((n) => n.name === source.network);
     if (!network) {
       throw new Error(
@@ -246,511 +267,527 @@ export async function buildConfigAndIndexingFunctions({
           .join(", ")}].`,
       );
     }
+
+    const startBlock = await resolveBlockNumber(source.startBlock, network);
+    const endBlock = await resolveBlockNumber(source.endBlock, network);
+
+    if (
+      startBlock !== undefined &&
+      endBlock !== undefined &&
+      endBlock < startBlock
+    ) {
+      throw new Error(
+        `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`,
+      );
+    }
   }
 
-  const contractSources: ContractSource[] = flattenSources(
-    config.contracts ?? {},
-  )
-    .flatMap((source): ContractSource[] => {
-      const network = networks.find((n) => n.name === source.network)!;
-
-      // Get indexing function that were registered for this contract
-      const registeredLogEvents: string[] = [];
-      const registeredCallTraceEvents: string[] = [];
-      for (const eventName of Object.keys(indexingFunctions)) {
-        // log event
-        if (eventName.includes(":")) {
-          const [logContractName, logEventName] = eventName.split(":") as [
-            string,
-            string,
-          ];
-          if (logContractName === source.name && logEventName !== "setup") {
-            registeredLogEvents.push(logEventName);
+  const contractSources: ContractSource[] = (
+    await Promise.all(
+      flattenSources(config.contracts ?? {}).map(
+        async (source): Promise<ContractSource[]> => {
+          const network = networks.find((n) => n.name === source.network)!;
+
+          // Get indexing function that were registered for this contract
+          const registeredLogEvents: string[] = [];
+          const registeredCallTraceEvents: string[] = [];
+          for (const eventName of Object.keys(indexingFunctions)) {
+            // log event
+            if (eventName.includes(":")) {
+              const [logContractName, logEventName] = eventName.split(":") as [
+                string,
+                string,
+              ];
+              if (logContractName === source.name && logEventName !== "setup") {
+                registeredLogEvents.push(logEventName);
+              }
+            }
+
+            //  trace event
+            if (eventName.includes(".")) {
+              const [functionContractName, functionName] = eventName.split(
+                ".",
+              ) as [string, string];
+              if (functionContractName === source.name) {
+                registeredCallTraceEvents.push(functionName);
+              }
+            }
           }
-        }
 
-        //  trace event
-        if (eventName.includes(".")) {
-          const [functionContractName, functionName] = eventName.split(".") as [
-            string,
-            string,
-          ];
-          if (functionContractName === source.name) {
-            registeredCallTraceEvents.push(functionName);
+          // Note: This can probably throw for invalid ABIs. Consider adding explicit ABI validation before this line.
+          const abiEvents = buildAbiEvents({ abi: source.abi });
+          const abiFunctions = buildAbiFunctions({ abi: source.abi });
+
+          const registeredEventSelectors: Hex[] = [];
+          // Validate that the registered log events exist in the abi
+          for (const logEvent of registeredLogEvents) {
+            const abiEvent = abiEvents.bySafeName[logEvent];
+            if (abiEvent === undefined) {
+              throw new Error(
+                `Validation failed: Event name for event '${logEvent}' not found in the contract ABI. Got '${logEvent}', expected one of [${Object.keys(
+                  abiEvents.bySafeName,
+                )
+                  .map((eventName) => `'${eventName}'`)
+                  .join(", ")}].`,
+              );
+            }
+
+            registeredEventSelectors.push(abiEvent.selector);
           }
-        }
-      }
 
-      // Note: This can probably throw for invalid ABIs. Consider adding explicit ABI validation before this line.
-      const abiEvents = buildAbiEvents({ abi: source.abi });
-      const abiFunctions = buildAbiFunctions({ abi: source.abi });
-
-      const registeredEventSelectors: Hex[] = [];
-      // Validate that the registered log events exist in the abi
-      for (const logEvent of registeredLogEvents) {
-        const abiEvent = abiEvents.bySafeName[logEvent];
-        if (abiEvent === undefined) {
-          throw new Error(
-            `Validation failed: Event name for event '${logEvent}' not found in the contract ABI. Got '${logEvent}', expected one of [${Object.keys(
-              abiEvents.bySafeName,
-            )
-              .map((eventName) => `'${eventName}'`)
-              .join(", ")}].`,
-          );
-        }
+          const registeredFunctionSelectors: Hex[] = [];
+          for (const _function of registeredCallTraceEvents) {
+            const abiFunction = abiFunctions.bySafeName[_function];
+            if (abiFunction === undefined) {
+              throw new Error(
+                `Validation failed: Function name for function '${_function}' not found in the contract ABI. Got '${_function}', expected one of [${Object.keys(
+                  abiFunctions.bySafeName,
+                )
+                  .map((eventName) => `'${eventName}'`)
+                  .join(", ")}].`,
+              );
+            }
+
+            registeredFunctionSelectors.push(abiFunction.selector);
+          }
 
-        registeredEventSelectors.push(abiEvent.selector);
-      }
+          const topicsArray: {
+            topic0: LogTopic;
+            topic1: LogTopic;
+            topic2: LogTopic;
+            topic3: LogTopic;
+          }[] = [];
+
+          if (source.filter !== undefined) {
+            const eventFilters = Array.isArray(source.filter)
+              ? source.filter
+              : [source.filter];
+
+            for (const filter of eventFilters) {
+              const abiEvent = abiEvents.bySafeName[filter.event];
+              if (!abiEvent) {
+                throw new Error(
+                  `Validation failed: Invalid filter for contract '${
+                    source.name
+                  }'. Got event name '${filter.event}', expected one of [${Object.keys(
+                    abiEvents.bySafeName,
+                  )
+                    .map((n) => `'${n}'`)
+                    .join(", ")}].`,
+                );
+              }
+            }
+
+            topicsArray.push(...buildTopics(source.abi, eventFilters));
+
+            // event selectors that have a filter
+            const filteredEventSelectors: Hex[] = topicsArray.map(
+              (t) => t.topic0 as Hex,
+            );
+            // event selectors that are registered but don't have a filter
+            const excludedRegisteredEventSelectors =
+              registeredEventSelectors.filter(
+                (s) => filteredEventSelectors.includes(s) === false,
+              );
+
+            for (const selector of filteredEventSelectors) {
+              if (registeredEventSelectors.includes(selector) === false) {
+                throw new Error(
+                  `Validation failed: Event selector '${abiEvents.bySelector[selector]?.safeName}' is used in a filter but does not have a corresponding indexing function.`,
+                );
+              }
+            }
+
+            if (excludedRegisteredEventSelectors.length > 0) {
+              topicsArray.push({
+                topic0: excludedRegisteredEventSelectors,
+                topic1: null,
+                topic2: null,
+                topic3: null,
+              });
+            }
+          } else {
+            topicsArray.push({
+              topic0: registeredEventSelectors,
+              topic1: null,
+              topic2: null,
+              topic3: null,
+            });
+          }
 
-      const registeredFunctionSelectors: Hex[] = [];
-      for (const _function of registeredCallTraceEvents) {
-        const abiFunction = abiFunctions.bySafeName[_function];
-        if (abiFunction === undefined) {
-          throw new Error(
-            `Validation failed: Function name for function '${_function}' not found in the contract ABI. Got '${_function}', expected one of [${Object.keys(
-              abiFunctions.bySafeName,
-            )
-              .map((eventName) => `'${eventName}'`)
-              .join(", ")}].`,
+          const fromBlock = await resolveBlockNumber(
+            source.startBlock,
+            network,
           );
-        }
+          const toBlock = await resolveBlockNumber(source.endBlock, network);
 
-        registeredFunctionSelectors.push(abiFunction.selector);
-      }
+          const contractMetadata = {
+            type: "contract",
+            abi: source.abi,
+            abiEvents,
+            abiFunctions,
+            name: source.name,
+            network,
+          } as const;
 
-      const topicsArray: {
-        topic0: LogTopic;
-        topic1: LogTopic;
-        topic2: LogTopic;
-        topic3: LogTopic;
-      }[] = [];
-
-      if (source.filter !== undefined) {
-        const eventFilters = Array.isArray(source.filter)
-          ? source.filter
-          : [source.filter];
-
-        for (const filter of eventFilters) {
-          const abiEvent = abiEvents.bySafeName[filter.event];
-          if (!abiEvent) {
-            throw new Error(
-              `Validation failed: Invalid filter for contract '${
-                source.name
-              }'. Got event name '${filter.event}', expected one of [${Object.keys(
-                abiEvents.bySafeName,
-              )
-                .map((n) => `'${n}'`)
-                .join(", ")}].`,
+          const resolvedAddress = source?.address;
+
+          if (
+            typeof resolvedAddress === "object" &&
+            !Array.isArray(resolvedAddress)
+          ) {
+            // Note that this can throw.
+            const logFactory = buildLogFactory({
+              chainId: network.chainId,
+              ...resolvedAddress,
+            });
+
+            const logSources = topicsArray.map(
+              (topics) =>
+                ({
+                  ...contractMetadata,
+                  filter: {
+                    type: "log",
+                    chainId: network.chainId,
+                    address: logFactory,
+                    topic0: topics.topic0,
+                    topic1: topics.topic1,
+                    topic2: topics.topic2,
+                    topic3: topics.topic3,
+                    fromBlock,
+                    toBlock,
+                    include: defaultLogFilterInclude.concat(
+                      source.includeTransactionReceipts
+                        ? defaultTransactionReceiptInclude
+                        : [],
+                    ),
+                  },
+                }) satisfies ContractSource,
             );
+
+            if (source.includeCallTraces) {
+              return [
+                ...logSources,
+                {
+                  ...contractMetadata,
+                  filter: {
+                    type: "trace",
+                    chainId: network.chainId,
+                    fromAddress: undefined,
+                    toAddress: logFactory,
+                    callType: "CALL",
+                    functionSelector: registeredFunctionSelectors,
+                    includeReverted: false,
+                    fromBlock,
+                    toBlock,
+                    include: defaultTraceFilterInclude.concat(
+                      source.includeTransactionReceipts
+                        ? defaultTransactionReceiptInclude
+                        : [],
+                    ),
+                  },
+                } satisfies ContractSource,
+              ];
+            }
+
+            return logSources;
+          } else if (resolvedAddress !== undefined) {
+            for (const address of Array.isArray(resolvedAddress)
+              ? resolvedAddress
+              : [resolvedAddress]) {
+              if (!address!.startsWith("0x"))
+                throw new Error(
+                  `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
+                    0,
+                    2,
+                  )}', expected '0x'.`,
+                );
+              if (address!.length !== 42)
+                throw new Error(
+                  `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
+                );
+            }
           }
-        }
 
-        topicsArray.push(...buildTopics(source.abi, eventFilters));
+          const validatedAddress = Array.isArray(resolvedAddress)
+            ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
+            : resolvedAddress !== undefined
+              ? toLowerCase(resolvedAddress)
+              : undefined;
+
+          const logSources = topicsArray.map(
+            (topics) =>
+              ({
+                ...contractMetadata,
+                filter: {
+                  type: "log",
+                  chainId: network.chainId,
+                  address: validatedAddress,
+                  topic0: topics.topic0,
+                  topic1: topics.topic1,
+                  topic2: topics.topic2,
+                  topic3: topics.topic3,
+                  fromBlock,
+                  toBlock,
+                  include: defaultLogFilterInclude.concat(
+                    source.includeTransactionReceipts
+                      ? defaultTransactionReceiptInclude
+                      : [],
+                  ),
+                },
+              }) satisfies ContractSource,
+          );
 
-        // event selectors that have a filter
-        const filteredEventSelectors: Hex[] = topicsArray.map(
-          (t) => t.topic0 as Hex,
-        );
-        // event selectors that are registered but don't have a filter
-        const excludedRegisteredEventSelectors =
-          registeredEventSelectors.filter(
-            (s) => filteredEventSelectors.includes(s) === false,
+          if (source.includeCallTraces) {
+            return [
+              ...logSources,
+              {
+                ...contractMetadata,
+                filter: {
+                  type: "trace",
+                  chainId: network.chainId,
+                  fromAddress: undefined,
+                  toAddress: Array.isArray(validatedAddress)
+                    ? validatedAddress
+                    : validatedAddress === undefined
+                      ? undefined
+                      : [validatedAddress],
+                  callType: "CALL",
+                  functionSelector: registeredFunctionSelectors,
+                  includeReverted: false,
+                  fromBlock,
+                  toBlock,
+                  include: defaultTraceFilterInclude.concat(
+                    source.includeTransactionReceipts
+                      ? defaultTransactionReceiptInclude
+                      : [],
+                  ),
+                },
+              } satisfies ContractSource,
+            ];
+          } else return logSources;
+        },
+      ),
+    )
+  )
+    .flat() // Remove sources with no registered indexing functions
+    .filter((source) => {
+      const hasNoRegisteredIndexingFunctions =
+        source.filter.type === "trace"
+          ? Array.isArray(source.filter.functionSelector) &&
+            source.filter.functionSelector.length === 0
+          : Array.isArray(source.filter.topic0) &&
+            source.filter.topic0?.length === 0;
+      if (hasNoRegisteredIndexingFunctions) {
+        logs.push({
+          level: "debug",
+          msg: `No indexing functions were registered for '${
+            source.name
+          }' ${source.filter.type === "trace" ? "traces" : "logs"}`,
+        });
+      }
+      return hasNoRegisteredIndexingFunctions === false;
+    });
+
+  const accountSources: AccountSource[] = (
+    await Promise.all(
+      flattenSources(config.accounts ?? {}).map(
+        async (source): Promise<AccountSource[]> => {
+          const network = networks.find((n) => n.name === source.network)!;
+
+          const fromBlock = await resolveBlockNumber(
+            source.startBlock,
+            network,
           );
+          const toBlock = await resolveBlockNumber(source.endBlock, network);
 
-        for (const selector of filteredEventSelectors) {
-          if (registeredEventSelectors.includes(selector) === false) {
+          const resolvedAddress = source?.address;
+
+          if (resolvedAddress === undefined) {
             throw new Error(
-              `Validation failed: Event selector '${abiEvents.bySelector[selector]?.safeName}' is used in a filter but does not have a corresponding indexing function.`,
+              `Validation failed: Account '${source.name}' must specify an 'address'.`,
             );
           }
-        }
 
-        if (excludedRegisteredEventSelectors.length > 0) {
-          topicsArray.push({
-            topic0: excludedRegisteredEventSelectors,
-            topic1: null,
-            topic2: null,
-            topic3: null,
-          });
-        }
-      } else {
-        topicsArray.push({
-          topic0: registeredEventSelectors,
-          topic1: null,
-          topic2: null,
-          topic3: null,
-        });
-      }
+          if (
+            typeof resolvedAddress === "object" &&
+            !Array.isArray(resolvedAddress)
+          ) {
+            // Note that this can throw.
+            const logFactory = buildLogFactory({
+              chainId: network.chainId,
+              ...resolvedAddress,
+            });
+
+            return [
+              {
+                type: "account",
+                name: source.name,
+                network,
+                filter: {
+                  type: "transaction",
+                  chainId: network.chainId,
+                  fromAddress: undefined,
+                  toAddress: logFactory,
+                  includeReverted: false,
+                  fromBlock,
+                  toBlock,
+                  include: defaultTransactionFilterInclude,
+                },
+              } satisfies AccountSource,
+              {
+                type: "account",
+                name: source.name,
+                network,
+                filter: {
+                  type: "transaction",
+                  chainId: network.chainId,
+                  fromAddress: logFactory,
+                  toAddress: undefined,
+                  includeReverted: false,
+                  fromBlock,
+                  toBlock,
+                  include: defaultTransactionFilterInclude,
+                },
+              } satisfies AccountSource,
+              {
+                type: "account",
+                name: source.name,
+                network,
+                filter: {
+                  type: "transfer",
+                  chainId: network.chainId,
+                  fromAddress: undefined,
+                  toAddress: logFactory,
+                  includeReverted: false,
+                  fromBlock,
+                  toBlock,
+                  include: defaultTransferFilterInclude.concat(
+                    source.includeTransactionReceipts
+                      ? defaultTransactionReceiptInclude
+                      : [],
+                  ),
+                },
+              } satisfies AccountSource,
+              {
+                type: "account",
+                name: source.name,
+                network,
+                filter: {
+                  type: "transfer",
+                  chainId: network.chainId,
+                  fromAddress: logFactory,
+                  toAddress: undefined,
+                  includeReverted: false,
+                  fromBlock,
+                  toBlock,
+                  include: defaultTransferFilterInclude.concat(
+                    source.includeTransactionReceipts
+                      ? defaultTransactionReceiptInclude
+                      : [],
+                  ),
+                },
+              } satisfies AccountSource,
+            ];
+          }
 
-      const startBlockMaybeNan = source.startBlock;
-      const fromBlock = Number.isNaN(startBlockMaybeNan)
-        ? undefined
-        : startBlockMaybeNan;
-      const endBlockMaybeNan = source.endBlock;
-      const toBlock = Number.isNaN(endBlockMaybeNan)
-        ? undefined
-        : endBlockMaybeNan;
-
-      const contractMetadata = {
-        type: "contract",
-        abi: source.abi,
-        abiEvents,
-        abiFunctions,
-        name: source.name,
-        network,
-      } as const;
-
-      const resolvedAddress = source?.address;
+          for (const address of Array.isArray(resolvedAddress)
+            ? resolvedAddress
+            : [resolvedAddress]) {
+            if (!address!.startsWith("0x"))
+              throw new Error(
+                `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
+                  0,
+                  2,
+                )}', expected '0x'.`,
+              );
+            if (address!.length !== 42)
+              throw new Error(
+                `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
+              );
+          }
 
-      if (
-        typeof resolvedAddress === "object" &&
-        !Array.isArray(resolvedAddress)
-      ) {
-        // Note that this can throw.
-        const logFactory = buildLogFactory({
-          chainId: network.chainId,
-          ...resolvedAddress,
-        });
+          const validatedAddress = Array.isArray(resolvedAddress)
+            ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
+            : resolvedAddress !== undefined
+              ? toLowerCase(resolvedAddress)
+              : undefined;
 
-        const logSources = topicsArray.map(
-          (topics) =>
-            ({
-              ...contractMetadata,
+          return [
+            {
+              type: "account",
+              name: source.name,
+              network,
               filter: {
-                type: "log",
+                type: "transaction",
                 chainId: network.chainId,
-                address: logFactory,
-                topic0: topics.topic0,
-                topic1: topics.topic1,
-                topic2: topics.topic2,
-                topic3: topics.topic3,
+                fromAddress: undefined,
+                toAddress: validatedAddress,
+                includeReverted: false,
                 fromBlock,
                 toBlock,
-                include: defaultLogFilterInclude.concat(
+                include: defaultTransactionFilterInclude,
+              },
+            } satisfies AccountSource,
+            {
+              type: "account",
+              name: source.name,
+              network,
+              filter: {
+                type: "transaction",
+                chainId: network.chainId,
+                fromAddress: validatedAddress,
+                toAddress: undefined,
+                includeReverted: false,
+                fromBlock,
+                toBlock,
+                include: defaultTransactionFilterInclude,
+              },
+            } satisfies AccountSource,
+            {
+              type: "account",
+              name: source.name,
+              network,
+              filter: {
+                type: "transfer",
+                chainId: network.chainId,
+                fromAddress: undefined,
+                toAddress: validatedAddress,
+                includeReverted: false,
+                fromBlock,
+                toBlock,
+                include: defaultTransferFilterInclude.concat(
                   source.includeTransactionReceipts
                     ? defaultTransactionReceiptInclude
                     : [],
                 ),
               },
-            }) satisfies ContractSource,
-        );
-
-        if (source.includeCallTraces) {
-          return [
-            ...logSources,
+            } satisfies AccountSource,
             {
-              ...contractMetadata,
+              type: "account",
+              name: source.name,
+              network,
               filter: {
-                type: "trace",
+                type: "transfer",
                 chainId: network.chainId,
-                fromAddress: undefined,
-                toAddress: logFactory,
-                callType: "CALL",
-                functionSelector: registeredFunctionSelectors,
+                fromAddress: validatedAddress,
+                toAddress: undefined,
                 includeReverted: false,
                 fromBlock,
                 toBlock,
-                include: defaultTraceFilterInclude.concat(
+                include: defaultTransferFilterInclude.concat(
                   source.includeTransactionReceipts
                     ? defaultTransactionReceiptInclude
                     : [],
                 ),
               },
-            } satisfies ContractSource,
+            } satisfies AccountSource,
           ];
-        }
-
-        return logSources;
-      } else if (resolvedAddress !== undefined) {
-        for (const address of Array.isArray(resolvedAddress)
-          ? resolvedAddress
-          : [resolvedAddress]) {
-          if (!address!.startsWith("0x"))
-            throw new Error(
-              `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
-                0,
-                2,
-              )}', expected '0x'.`,
-            );
-          if (address!.length !== 42)
-            throw new Error(
-              `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
-            );
-        }
-      }
-
-      const validatedAddress = Array.isArray(resolvedAddress)
-        ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
-        : resolvedAddress !== undefined
-          ? toLowerCase(resolvedAddress)
-          : undefined;
-
-      const logSources = topicsArray.map(
-        (topics) =>
-          ({
-            ...contractMetadata,
-            filter: {
-              type: "log",
-              chainId: network.chainId,
-              address: validatedAddress,
-              topic0: topics.topic0,
-              topic1: topics.topic1,
-              topic2: topics.topic2,
-              topic3: topics.topic3,
-              fromBlock,
-              toBlock,
-              include: defaultLogFilterInclude.concat(
-                source.includeTransactionReceipts
-                  ? defaultTransactionReceiptInclude
-                  : [],
-              ),
-            },
-          }) satisfies ContractSource,
-      );
-
-      if (source.includeCallTraces) {
-        return [
-          ...logSources,
-          {
-            ...contractMetadata,
-            filter: {
-              type: "trace",
-              chainId: network.chainId,
-              fromAddress: undefined,
-              toAddress: Array.isArray(validatedAddress)
-                ? validatedAddress
-                : validatedAddress === undefined
-                  ? undefined
-                  : [validatedAddress],
-              callType: "CALL",
-              functionSelector: registeredFunctionSelectors,
-              includeReverted: false,
-              fromBlock,
-              toBlock,
-              include: defaultTraceFilterInclude.concat(
-                source.includeTransactionReceipts
-                  ? defaultTransactionReceiptInclude
-                  : [],
-              ),
-            },
-          } satisfies ContractSource,
-        ];
-      } else return logSources;
-    }) // Remove sources with no registered indexing functions
-    .filter((source) => {
-      const hasNoRegisteredIndexingFunctions =
-        source.filter.type === "trace"
-          ? Array.isArray(source.filter.functionSelector) &&
-            source.filter.functionSelector.length === 0
-          : Array.isArray(source.filter.topic0) &&
-            source.filter.topic0?.length === 0;
-      if (hasNoRegisteredIndexingFunctions) {
-        logs.push({
-          level: "debug",
-          msg: `No indexing functions were registered for '${
-            source.name
-          }' ${source.filter.type === "trace" ? "traces" : "logs"}`,
-        });
-      }
-      return hasNoRegisteredIndexingFunctions === false;
-    });
-
-  const accountSources: AccountSource[] = flattenSources(config.accounts ?? {})
-    .flatMap((source): AccountSource[] => {
-      const network = networks.find((n) => n.name === source.network)!;
-
-      const startBlockMaybeNan = source.startBlock;
-      const fromBlock = Number.isNaN(startBlockMaybeNan)
-        ? undefined
-        : startBlockMaybeNan;
-      const endBlockMaybeNan = source.endBlock;
-      const toBlock = Number.isNaN(endBlockMaybeNan)
-        ? undefined
-        : endBlockMaybeNan;
-
-      const resolvedAddress = source?.address;
-
-      if (resolvedAddress === undefined) {
-        throw new Error(
-          `Validation failed: Account '${source.name}' must specify an 'address'.`,
-        );
-      }
-
-      if (
-        typeof resolvedAddress === "object" &&
-        !Array.isArray(resolvedAddress)
-      ) {
-        // Note that this can throw.
-        const logFactory = buildLogFactory({
-          chainId: network.chainId,
-          ...resolvedAddress,
-        });
-
-        return [
-          {
-            type: "account",
-            name: source.name,
-            network,
-            filter: {
-              type: "transaction",
-              chainId: network.chainId,
-              fromAddress: undefined,
-              toAddress: logFactory,
-              includeReverted: false,
-              fromBlock,
-              toBlock,
-              include: defaultTransactionFilterInclude,
-            },
-          } satisfies AccountSource,
-          {
-            type: "account",
-            name: source.name,
-            network,
-            filter: {
-              type: "transaction",
-              chainId: network.chainId,
-              fromAddress: logFactory,
-              toAddress: undefined,
-              includeReverted: false,
-              fromBlock,
-              toBlock,
-              include: defaultTransactionFilterInclude,
-            },
-          } satisfies AccountSource,
-          {
-            type: "account",
-            name: source.name,
-            network,
-            filter: {
-              type: "transfer",
-              chainId: network.chainId,
-              fromAddress: undefined,
-              toAddress: logFactory,
-              includeReverted: false,
-              fromBlock,
-              toBlock,
-              include: defaultTransferFilterInclude.concat(
-                source.includeTransactionReceipts
-                  ? defaultTransactionReceiptInclude
-                  : [],
-              ),
-            },
-          } satisfies AccountSource,
-          {
-            type: "account",
-            name: source.name,
-            network,
-            filter: {
-              type: "transfer",
-              chainId: network.chainId,
-              fromAddress: logFactory,
-              toAddress: undefined,
-              includeReverted: false,
-              fromBlock,
-              toBlock,
-              include: defaultTransferFilterInclude.concat(
-                source.includeTransactionReceipts
-                  ? defaultTransactionReceiptInclude
-                  : [],
-              ),
-            },
-          } satisfies AccountSource,
-        ];
-      }
-
-      for (const address of Array.isArray(resolvedAddress)
-        ? resolvedAddress
-        : [resolvedAddress]) {
-        if (!address!.startsWith("0x"))
-          throw new Error(
-            `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice(
-              0,
-              2,
-            )}', expected '0x'.`,
-          );
-        if (address!.length !== 42)
-          throw new Error(
-            `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`,
-          );
-      }
-
-      const validatedAddress = Array.isArray(resolvedAddress)
-        ? dedupe(resolvedAddress).map((r) => toLowerCase(r))
-        : resolvedAddress !== undefined
-          ? toLowerCase(resolvedAddress)
-          : undefined;
-
-      return [
-        {
-          type: "account",
-          name: source.name,
-          network,
-          filter: {
-            type: "transaction",
-            chainId: network.chainId,
-            fromAddress: undefined,
-            toAddress: validatedAddress,
-            includeReverted: false,
-            fromBlock,
-            toBlock,
-            include: defaultTransactionFilterInclude,
-          },
-        } satisfies AccountSource,
-        {
-          type: "account",
-          name: source.name,
-          network,
-          filter: {
-            type: "transaction",
-            chainId: network.chainId,
-            fromAddress: validatedAddress,
-            toAddress: undefined,
-            includeReverted: false,
-            fromBlock,
-            toBlock,
-            include: defaultTransactionFilterInclude,
-          },
-        } satisfies AccountSource,
-        {
-          type: "account",
-          name: source.name,
-          network,
-          filter: {
-            type: "transfer",
-            chainId: network.chainId,
-            fromAddress: undefined,
-            toAddress: validatedAddress,
-            includeReverted: false,
-            fromBlock,
-            toBlock,
-            include: defaultTransferFilterInclude.concat(
-              source.includeTransactionReceipts
-                ? defaultTransactionReceiptInclude
-                : [],
-            ),
-          },
-        } satisfies AccountSource,
-        {
-          type: "account",
-          name: source.name,
-          network,
-          filter: {
-            type: "transfer",
-            chainId: network.chainId,
-            fromAddress: validatedAddress,
-            toAddress: undefined,
-            includeReverted: false,
-            fromBlock,
-            toBlock,
-            include: defaultTransferFilterInclude.concat(
-              source.includeTransactionReceipts
-                ? defaultTransactionReceiptInclude
-                : [],
-            ),
-          },
-        } satisfies AccountSource,
-      ];
-    })
+        },
+      ),
+    )
+  )
+    .flat()
     .filter((source) => {
       const eventName =
         source.filter.type === "transaction"
@@ -772,43 +809,41 @@ export async function buildConfigAndIndexingFunctions({
       return hasRegisteredIndexingFunction;
     });
 
-  const blockSources: BlockSource[] = flattenSources(config.blocks ?? {})
-    .map((source) => {
-      const network = networks.find((n) => n.name === source.network)!;
+  const blockSources: BlockSource[] = (
+    await Promise.all(
+      flattenSources(config.blocks ?? {}).map(async (source) => {
+        const network = networks.find((n) => n.name === source.network)!;
 
-      const intervalMaybeNan = source.interval ?? 1;
-      const interval = Number.isNaN(intervalMaybeNan) ? 0 : intervalMaybeNan;
+        const intervalMaybeNan = source.interval ?? 1;
+        const interval = Number.isNaN(intervalMaybeNan) ? 0 : intervalMaybeNan;
 
-      if (!Number.isInteger(interval) || interval === 0) {
-        throw new Error(
-          `Validation failed: Invalid interval for block source '${source.name}'. Got ${interval}, expected a non-zero integer.`,
-        );
-      }
+        if (!Number.isInteger(interval) || interval === 0) {
+          throw new Error(
+            `Validation failed: Invalid interval for block source '${source.name}'. Got ${interval}, expected a non-zero integer.`,
+          );
+        }
 
-      const startBlockMaybeNan = source.startBlock;
-      const fromBlock = Number.isNaN(startBlockMaybeNan)
-        ? undefined
-        : startBlockMaybeNan;
-      const endBlockMaybeNan = source.endBlock;
-      const toBlock = Number.isNaN(endBlockMaybeNan)
-        ? undefined
-        : endBlockMaybeNan;
+        const fromBlock = await resolveBlockNumber(source.startBlock, network);
+        const toBlock = await resolveBlockNumber(source.endBlock, network);
 
-      return {
-        type: "block",
-        name: source.name,
-        network,
-        filter: {
+        return {
           type: "block",
-          chainId: network.chainId,
-          interval: interval,
-          offset: (fromBlock ?? 0) % interval,
-          fromBlock,
-          toBlock,
-          include: defaultBlockFilterInclude,
-        },
-      } satisfies BlockSource;
-    })
+          name: source.name,
+          network,
+          filter: {
+            type: "block",
+            chainId: network.chainId,
+            interval: interval,
+            offset: (fromBlock ?? 0) % interval,
+            fromBlock,
+            toBlock,
+            include: defaultBlockFilterInclude,
+          },
+        } satisfies BlockSource;
+      }),
+    )
+  )
+    .flat()
     .filter((blockSource) => {
       const hasRegisteredIndexingFunction =
         indexingFunctions[`${blockSource.name}:block`] !== undefined;
diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts
index d7f714e1..2e859547 100644
--- a/packages/core/src/config/index.ts
+++ b/packages/core/src/config/index.ts
@@ -65,9 +65,9 @@ type DatabaseConfig =
 
 type BlockConfig = {
   /** Block number at which to start indexing events (inclusive). If `undefined`, events will be processed from block 0. Default: `undefined`. */
-  startBlock?: number;
+  startBlock?: number | "latest";
   /** Block number at which to stop indexing events (inclusive). If `undefined`, events will be processed in real-time. Default: `undefined`. */
-  endBlock?: number;
+  endBlock?: number | "latest";
 };
 
 type TransactionReceiptConfig = {
@@ -215,9 +215,9 @@ type AccountsConfig<networks, accounts> = {} extends accounts
 
 type BlockFilterConfig = {
   /** Block number at which to start indexing events (inclusive). If `undefined`, events will be processed from block 0. Default: `undefined`. */
-  startBlock?: number;
+  startBlock?: number | "latest";
   /** Block number at which to stop indexing events (inclusive). If `undefined`, events will be processed in real-time. Default: `undefined`. */
-  endBlock?: number;
+  endBlock?: number | "latest";
   interval?: number;
 };
 
diff --git a/packages/create-ponder/CHANGELOG.md b/packages/create-ponder/CHANGELOG.md
index 3f98013f..277c50c7 100644
--- a/packages/create-ponder/CHANGELOG.md
+++ b/packages/create-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # create-ponder
 
+## 0.9.3
+
 ## 0.9.2
 
 ## 0.9.1
diff --git a/packages/create-ponder/package.json b/packages/create-ponder/package.json
index 4c7614a0..11ce878a 100644
--- a/packages/create-ponder/package.json
+++ b/packages/create-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-ponder",
-  "version": "0.9.2",
+  "version": "0.9.3",
   "type": "module",
   "description": "A CLI tool to create Ponder apps",
   "license": "MIT",
diff --git a/packages/eslint-config-ponder/CHANGELOG.md b/packages/eslint-config-ponder/CHANGELOG.md
index 946830d5..b09bfff2 100644
--- a/packages/eslint-config-ponder/CHANGELOG.md
+++ b/packages/eslint-config-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # eslint-config-ponder
 
+## 0.9.3
+
 ## 0.9.2
 
 ## 0.9.1
diff --git a/packages/eslint-config-ponder/package.json b/packages/eslint-config-ponder/package.json
index 4dd1e0d7..09246229 100644
--- a/packages/eslint-config-ponder/package.json
+++ b/packages/eslint-config-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-config-ponder",
-  "version": "0.9.2",
+  "version": "0.9.3",
   "description": "ESLint config for Ponder apps",
   "license": "MIT",
   "main": "./index.js",
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 77de050b..ec1b58e8 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,12 @@
 # @ponder/react
 
+## 0.9.3
+
+### Patch Changes
+
+- Updated dependencies []:
+  - @ponder/client@0.9.3
+
 ## 0.9.2
 
 ### Patch Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 8146074d..1a62da02 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/react",
-  "version": "0.9.2",
+  "version": "0.9.3",
   "description": "React hooks for Ponder",
   "license": "MIT",
   "repository": {
@@ -37,7 +37,7 @@
     "superjson": "^2.2.2"
   },
   "peerDependencies": {
-    "@ponder/client": ">=0.9.2",
+    "@ponder/client": ">=0.9.3",
     "@tanstack/react-query": ">=5.0.0",
     "react": ">=18",
     "typescript": ">=5.0.4"


Test patch:
diff --git a/packages/core/src/build/configAndIndexingFunctions.test.ts b/packages/core/src/build/configAndIndexingFunctions.test.ts
index bd0b6249..c755502f 100644
--- a/packages/core/src/build/configAndIndexingFunctions.test.ts
+++ b/packages/core/src/build/configAndIndexingFunctions.test.ts
@@ -1,3 +1,5 @@
+import { setupAnvil } from "@/_test/setup.js";
+import { poolId } from "@/_test/utils.js";
 import { factory } from "@/config/address.js";
 import type { LogFactory, LogFilter, TraceFilter } from "@/internal/types.js";
 import { shouldGetTransactionReceipt } from "@/sync/filter.js";
@@ -9,7 +11,7 @@ import {
   toFunctionSelector,
   zeroAddress,
 } from "viem";
-import { expect, test } from "vitest";
+import { beforeEach, expect, test } from "vitest";
 import { type Config, createConfig } from "../config/index.js";
 import {
   buildConfigAndIndexingFunctions,
@@ -32,6 +34,8 @@ const bytes1 =
 const bytes2 =
   "0x0000000000000000000000000000000000000000000000000000000000000002";
 
+beforeEach(setupAnvil);
+
 test("buildConfigAndIndexingFunctions() builds topics for multiple events", async () => {
   const config = createConfig({
     networks: {
@@ -466,6 +470,31 @@ test("buildConfigAndIndexingFunctions() coerces NaN startBlock to undefined", as
   expect(sources[0]?.filter.fromBlock).toBe(undefined);
 });
 
+test("buildConfigAndIndexingFunctions() coerces `latest` to number", async () => {
+  const config = createConfig({
+    networks: {
+      mainnet: {
+        chainId: 1,
+        transport: http(`http://127.0.0.1:8545/${poolId}`),
+      },
+    },
+    contracts: {
+      a: {
+        network: { mainnet: {} },
+        abi: [event0, event1],
+        startBlock: "latest",
+      },
+    },
+  });
+
+  const { sources } = await buildConfigAndIndexingFunctions({
+    config,
+    rawIndexingFunctions: [{ name: "a:Event0", fn: () => {} }],
+  });
+
+  expect(sources[0]?.filter.fromBlock).toBeTypeOf("number");
+});
+
 test("buildConfigAndIndexingFunctions() includeTransactionReceipts", async () => {
   const config = createConfig({
     networks: {