Repository: ponder-sh/ponder
Pull request #1438 (2025-01-12 03:26:33)

Added `ponder_realtime_latency` metric


Patch:
diff --git a/.changeset/late-cats-tan.md b/.changeset/late-cats-tan.md
new file mode 100644
index 00000000..7243b204
--- /dev/null
+++ b/.changeset/late-cats-tan.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Added "ponder_realtime_latency" metric.
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 30649020..e146c95e 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,11 @@
 # ponder
 
+## 0.8.25
+
+### Patch Changes
+
+- [#1437](https://github.com/ponder-sh/ponder/pull/1437) [`229419634521e287dd7f642f8b08dfe16cd17653`](https://github.com/ponder-sh/ponder/commit/229419634521e287dd7f642f8b08dfe16cd17653) Thanks [@kyscott18](https://github.com/kyscott18)! - Improved the granularity of rpc cache.
+
 ## 0.8.24
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index 2de5e02e..faac7459 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ponder",
-  "version": "0.8.24",
+  "version": "0.8.25",
   "description": "An open-source framework for crypto application backends",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/src/common/metrics.ts b/packages/core/src/common/metrics.ts
index f2e6964f..32eaa132 100644
--- a/packages/core/src/common/metrics.ts
+++ b/packages/core/src/common/metrics.ts
@@ -38,6 +38,7 @@ export class MetricsService {
   ponder_historical_completed_blocks: prometheus.Gauge<"network">;
 
   ponder_realtime_reorg_total: prometheus.Counter<"network">;
+  ponder_realtime_latency: prometheus.Histogram<"network">;
 
   ponder_database_method_duration: prometheus.Histogram<"service" | "method">;
   ponder_database_method_error_total: prometheus.Counter<"service" | "method">;
@@ -151,10 +152,17 @@ export class MetricsService {
 
     this.ponder_realtime_reorg_total = new prometheus.Counter({
       name: "ponder_realtime_reorg_total",
-      help: "Count of how many re-orgs have occurred.",
+      help: "Count of how many re-orgs have occurred",
       labelNames: ["network"] as const,
       registers: [this.registry],
     });
+    this.ponder_realtime_latency = new prometheus.Histogram({
+      name: "ponder_realtime_latency",
+      help: "Time elapsed between receiving a block and fully processing it",
+      labelNames: ["network"] as const,
+      buckets: httpRequestDurationMs,
+      registers: [this.registry],
+    });
 
     this.ponder_database_method_duration = new prometheus.Histogram({
       name: "ponder_database_method_duration",
diff --git a/packages/core/src/sync-historical/index.ts b/packages/core/src/sync-historical/index.ts
index ba6874e6..ee88d2f2 100644
--- a/packages/core/src/sync-historical/index.ts
+++ b/packages/core/src/sync-historical/index.ts
@@ -6,10 +6,12 @@ import {
   isTransferFilterMatched,
 } from "@/sync-realtime/filter.js";
 import type { SyncStore } from "@/sync-store/index.js";
+import { type Fragment, recoverFilter } from "@/sync/fragments.js";
 import {
   type BlockFilter,
   type Factory,
   type Filter,
+  type FilterWithoutBlocks,
   type LogFactory,
   type LogFilter,
   type TraceFilter,
@@ -27,10 +29,10 @@ import type {
 import {
   type Interval,
   getChunks,
+  intervalBounds,
   intervalDifference,
   intervalRange,
 } from "@/utils/interval.js";
-import { never } from "@/utils/never.js";
 import type { RequestQueue } from "@/utils/requestQueue.js";
 import {
   _debug_traceBlockByNumber,
@@ -51,7 +53,7 @@ import {
 } from "viem";
 
 export type HistoricalSync = {
-  intervalsCache: Map<Filter, Interval[]>;
+  intervalsCache: Map<Filter, { fragment: Fragment; intervals: Interval[] }[]>;
   /**
    * Extract raw data for `interval` and return the closest-to-tip block
    * that is synced.
@@ -124,7 +126,10 @@ export const createHistoricalSync = async (
    *
    * Note: `intervalsCache` is not updated after a new interval is synced.
    */
-  let intervalsCache: Map<Filter, Interval[]>;
+  let intervalsCache: Map<
+    Filter,
+    { fragment: Fragment; intervals: Interval[] }[]
+  >;
   if (args.network.disableCache) {
     intervalsCache = new Map();
     for (const { filter } of args.sources) {
@@ -741,71 +746,102 @@ export const createHistoricalSync = async (
   return {
     intervalsCache,
     async sync(_interval) {
-      const syncedIntervals: { filter: Filter; interval: Interval }[] = [];
+      const intervalsToSync: {
+        interval: Interval;
+        filter: FilterWithoutBlocks;
+      }[] = [];
 
-      await Promise.all(
-        args.sources.map(async (source) => {
-          const filter = source.filter;
-
-          // Compute the required interval to sync, accounting for cached
-          // intervals and start + end block.
-
-          // Skip sync if the interval is after the `toBlock` or before
-          // the `fromBlock`.
-          if (
-            (filter.fromBlock !== undefined &&
-              filter.fromBlock > _interval[1]) ||
-            (filter.toBlock !== undefined && filter.toBlock < _interval[0])
-          ) {
-            return;
-          }
-          const interval: Interval = [
-            Math.max(filter.fromBlock ?? 0, _interval[0]),
-            Math.min(filter.toBlock ?? Number.POSITIVE_INFINITY, _interval[1]),
-          ];
-          const completedIntervals = intervalsCache.get(filter)!;
-          const requiredIntervals = intervalDifference(
+      // Determine the requests that need to be made, and which intervals need to be inserted.
+      // Fragments are used to create a minimal filter, to avoid refetching data even if a filter
+      // is only partially synced.
+
+      for (const { filter } of args.sources) {
+        if (
+          (filter.fromBlock !== undefined && filter.fromBlock > _interval[1]) ||
+          (filter.toBlock !== undefined && filter.toBlock < _interval[0])
+        ) {
+          continue;
+        }
+
+        const interval: Interval = [
+          Math.max(filter.fromBlock ?? 0, _interval[0]),
+          Math.min(filter.toBlock ?? Number.POSITIVE_INFINITY, _interval[1]),
+        ];
+
+        const completedIntervals = intervalsCache.get(filter)!;
+        const requiredIntervals: {
+          fragment: Fragment;
+          intervals: Interval[];
+        }[] = [];
+
+        for (const {
+          fragment,
+          intervals: fragmentIntervals,
+        } of completedIntervals) {
+          const requiredFragmentIntervals = intervalDifference(
             [interval],
-            completedIntervals,
+            fragmentIntervals,
           );
 
-          // Skip sync if the interval is already complete.
-          if (requiredIntervals.length === 0) return;
+          if (requiredFragmentIntervals.length > 0) {
+            requiredIntervals.push({
+              fragment,
+              intervals: requiredFragmentIntervals,
+            });
+          }
+        }
 
+        if (requiredIntervals.length > 0) {
+          const requiredInterval = intervalBounds(
+            requiredIntervals.flatMap(({ intervals }) => intervals),
+          );
+
+          const requiredFilter = recoverFilter(
+            filter,
+            requiredIntervals.map(({ fragment }) => fragment),
+          );
+
+          intervalsToSync.push({
+            filter: requiredFilter,
+            interval: requiredInterval,
+          });
+        }
+      }
+
+      await Promise.all(
+        intervalsToSync.map(async ({ filter, interval }) => {
           // Request last block of interval
           const blockPromise = syncBlock(interval[1]);
 
           try {
-            // sync required intervals, account for chunk sizes
-            await Promise.all(
-              requiredIntervals.map(async (interval) => {
-                switch (filter.type) {
-                  case "log": {
-                    await syncLogFilter(filter, interval);
-                    break;
-                  }
-
-                  case "block": {
-                    await syncBlockFilter(filter, interval);
-                    break;
-                  }
-
-                  case "transaction": {
-                    await syncTransactionFilter(filter, interval);
-                    break;
-                  }
-
-                  case "trace":
-                  case "transfer": {
-                    await syncTraceOrTransferFilter(filter, interval);
-                    break;
-                  }
-
-                  default:
-                    never(filter);
-                }
-              }),
-            );
+            switch (filter.type) {
+              case "log": {
+                await syncLogFilter(filter as LogFilter, interval);
+                break;
+              }
+
+              case "block": {
+                await syncBlockFilter(filter as BlockFilter, interval);
+                break;
+              }
+
+              case "transaction": {
+                await syncTransactionFilter(
+                  filter as TransactionFilter,
+                  interval,
+                );
+                break;
+              }
+
+              case "trace":
+              case "transfer": {
+                await syncTraceOrTransferFilter(
+                  filter as TraceFilter | TransferFilter,
+                  interval,
+                );
+                break;
+              }
+            }
           } catch (_error) {
             const error = _error as Error;
 
@@ -823,11 +859,11 @@ export const createHistoricalSync = async (
           if (isKilled) return;
 
           await blockPromise;
-
-          syncedIntervals.push({ filter, interval });
         }),
       );
 
+      if (isKilled) return;
+
       const blocks = await Promise.all(blockCache.values());
 
       await Promise.all([
@@ -845,11 +881,13 @@ export const createHistoricalSync = async (
         }),
       ]);
 
+      if (isKilled) return;
+
       // Add corresponding intervals to the sync-store
       // Note: this should happen after so the database doesn't become corrupted
       if (args.network.disableCache === false) {
         await args.syncStore.insertIntervals({
-          intervals: syncedIntervals,
+          intervals: intervalsToSync,
           chainId: args.network.chainId,
         });
       }
diff --git a/packages/core/src/sync-realtime/index.ts b/packages/core/src/sync-realtime/index.ts
index bfc2eef0..deaf6530 100644
--- a/packages/core/src/sync-realtime/index.ts
+++ b/packages/core/src/sync-realtime/index.ts
@@ -32,6 +32,7 @@ import {
   _eth_getLogs,
   _eth_getTransactionReceipt,
 } from "@/utils/rpc.js";
+import { startClock } from "@/utils/timer.js";
 import { wait } from "@/utils/wait.js";
 import { type Queue, createQueue } from "@ponder/common";
 import { type Address, type Hash, hexToNumber, zeroHash } from "viem";
@@ -78,6 +79,7 @@ export type RealtimeSyncEvent =
   | ({
       type: "block";
       hasMatchedFilter: boolean;
+      endClock?: () => number;
     } & BlockWithEventData)
   | {
       type: "finalize";
@@ -113,7 +115,7 @@ export const createRealtimeSync = (
    * `parentHash` => `hash`.
    */
   let unfinalizedBlocks: LightBlock[] = [];
-  let queue: Queue<void, BlockWithEventData>;
+  let queue: Queue<void, BlockWithEventData & { endClock?: () => number }>;
   let consecutiveErrors = 0;
   let interval: NodeJS.Timeout | undefined;
 
@@ -188,7 +190,8 @@ export const createRealtimeSync = (
     traces,
     transactions,
     transactionReceipts,
-  }: BlockWithEventData) => {
+    endClock,
+  }: BlockWithEventData & { endClock?: () => number }) => {
     args.common.logger.debug({
       service: "realtime",
       msg: `Started syncing '${args.network.name}' block ${hexToNumber(block.number)}`,
@@ -409,6 +412,7 @@ export const createRealtimeSync = (
       traces,
       transactions,
       transactionReceipts,
+      endClock,
     });
 
     // Determine if a new range has become finalized by evaluating if the
@@ -1042,11 +1046,13 @@ export const createRealtimeSync = (
             return;
           }
 
+          const endClock = startClock();
+
           const blockWithEventData = await fetchBlockEventData(block);
 
           consecutiveErrors = 0;
 
-          return queue.add(blockWithEventData);
+          return queue.add({ ...blockWithEventData, endClock });
         } catch (_error) {
           if (isKilled) return;
 
diff --git a/packages/core/src/sync-store/index.ts b/packages/core/src/sync-store/index.ts
index fca8bdf7..cd377e61 100644
--- a/packages/core/src/sync-store/index.ts
+++ b/packages/core/src/sync-store/index.ts
@@ -1,11 +1,17 @@
 import type { Common } from "@/common/common.js";
 import type { HeadlessKysely } from "@/database/kysely.js";
 import type { RawEvent } from "@/sync/events.js";
-import { type FragmentId, getFragmentIds } from "@/sync/fragments.js";
+import {
+  type Fragment,
+  type FragmentId,
+  fragmentToId,
+  getFragments,
+} from "@/sync/fragments.js";
 import {
   type BlockFilter,
   type Factory,
   type Filter,
+  type FilterWithoutBlocks,
   type LogFactory,
   type LogFilter,
   type TraceFilter,
@@ -24,7 +30,7 @@ import type {
   SyncTransactionReceipt,
 } from "@/types/sync.js";
 import type { NonNull } from "@/types/utils.js";
-import { type Interval, intervalIntersectionMany } from "@/utils/interval.js";
+import type { Interval } from "@/utils/interval.js";
 import { type Kysely, type SelectQueryBuilder, sql as ksql } from "kysely";
 import type { InsertObject } from "kysely";
 import {
@@ -47,14 +53,14 @@ import {
 export type SyncStore = {
   insertIntervals(args: {
     intervals: {
-      filter: Filter;
+      filter: FilterWithoutBlocks;
       interval: Interval;
     }[];
     chainId: number;
   }): Promise<void>;
   getIntervals(args: {
     filters: Filter[];
-  }): Promise<Map<Filter, Interval[]>>;
+  }): Promise<Map<Filter, { fragment: Fragment; intervals: Interval[] }[]>>;
   getChildAddresses(args: {
     filter: Factory;
     limit?: number;
@@ -168,12 +174,13 @@ export const createSyncStore = ({
       // dedupe and merge matching fragments
 
       for (const { filter, interval } of intervals) {
-        for (const fragment of getFragmentIds(filter)) {
-          if (perFragmentIntervals.has(fragment.id) === false) {
-            perFragmentIntervals.set(fragment.id, []);
+        for (const fragment of getFragments(filter)) {
+          const fragmentId = fragmentToId(fragment.fragment);
+          if (perFragmentIntervals.has(fragmentId) === false) {
+            perFragmentIntervals.set(fragmentId, []);
           }
 
-          perFragmentIntervals.get(fragment.id)!.push(interval);
+          perFragmentIntervals.get(fragmentId)!.push(interval);
         }
       }
 
@@ -213,25 +220,27 @@ export const createSyncStore = ({
         | SelectQueryBuilder<
             PonderSyncSchema,
             "intervals",
-            { merged_blocks: string | null; filter: string }
+            { merged_blocks: string | null; filter: string; fragment: string }
           >
         | undefined;
 
       for (let i = 0; i < filters.length; i++) {
         const filter = filters[i]!;
-        const fragments = getFragmentIds(filter);
-        for (const fragment of fragments) {
+        const fragments = getFragments(filter);
+        for (let j = 0; j < fragments.length; j++) {
+          const fragment = fragments[j]!;
           const _query = db
             .selectFrom(
               db
                 .selectFrom("intervals")
                 .select(ksql`unnest(blocks)`.as("blocks"))
-                .where("fragment_id", "in", fragment.adjacent)
+                .where("fragment_id", "in", fragment.adjacentIds)
                 .as("unnested"),
             )
             .select([
               ksql<string>`range_agg(unnested.blocks)`.as("merged_blocks"),
               ksql.raw(`'${i}'`).as("filter"),
+              ksql.raw(`'${j}'`).as("fragment"),
             ]);
           // @ts-ignore
           query = query === undefined ? _query : query.unionAll(_query);
@@ -240,28 +249,34 @@ export const createSyncStore = ({
 
       const rows = await query!.execute();
 
-      const result: Map<Filter, Interval[]> = new Map();
-
-      // intervals use "union" for the same fragment, and
-      // "intersection" for the same filter
+      const result = new Map<
+        Filter,
+        { fragment: Fragment; intervals: Interval[] }[]
+      >();
 
       // NOTE: `interval[1]` must be rounded down in order to offset the previous
       // rounding.
 
       for (let i = 0; i < filters.length; i++) {
         const filter = filters[i]!;
-        const intervals = rows
-          .filter((row) => row.filter === `${i}`)
-          .map((row) =>
-            (row.merged_blocks
-              ? (JSON.parse(
-                  `[${row.merged_blocks.slice(1, -1)}]`,
-                ) as Interval[])
-              : []
-            ).map((interval) => [interval[0], interval[1] - 1] as Interval),
-          );
+        const fragments = getFragments(filter);
+        result.set(filter, []);
+        for (let j = 0; j < fragments.length; j++) {
+          const fragment = fragments[j]!;
+          const intervals = rows
+            .filter((row) => row.filter === `${i}`)
+            .filter((row) => row.fragment === `${j}`)
+            .map((row) =>
+              (row.merged_blocks
+                ? (JSON.parse(
+                    `[${row.merged_blocks.slice(1, -1)}]`,
+                  ) as Interval[])
+                : []
+              ).map((interval) => [interval[0], interval[1] - 1] as Interval),
+            )[0]!;
 
-        result.set(filter, intervalIntersectionMany(intervals));
+          result.get(filter)!.push({ fragment: fragment.fragment, intervals });
+        }
       }
 
       return result;
diff --git a/packages/core/src/sync/fragments.ts b/packages/core/src/sync/fragments.ts
index 953a6ac9..4c051e2e 100644
--- a/packages/core/src/sync/fragments.ts
+++ b/packages/core/src/sync/fragments.ts
@@ -1,8 +1,10 @@
+import { dedupe } from "@ponder/common";
 import type { Address, Hex } from "viem";
 import {
   type BlockFilter,
   type Factory,
-  type Filter,
+  type FilterAddress,
+  type FilterWithoutBlocks,
   type LogFilter,
   type TraceFilter,
   type TransactionFilter,
@@ -12,66 +14,127 @@ import {
 } from "./source.js";
 
 type FragmentAddress =
+  | Address
+  | {
+      address: Address;
+      eventSelector: Factory["eventSelector"];
+      childAddressLocation: Factory["childAddressLocation"];
+    }
+  | null;
+
+type FragmentAddressId =
   | Address
   | `${Address}_${Factory["eventSelector"]}_${Factory["childAddressLocation"]}`
   | null;
 type FragmentTopic = Hex | null;
 
+export type Fragment =
+  | {
+      type: "block";
+      chainId: number;
+      interval: number;
+      offset: number;
+    }
+  | {
+      type: "transaction";
+      chainId: number;
+      fromAddress: FragmentAddress;
+      toAddress: FragmentAddress;
+    }
+  | {
+      type: "trace";
+      chainId: number;
+      fromAddress: FragmentAddress;
+      toAddress: FragmentAddress;
+      functionSelector: Hex | null;
+      includeTransactionReceipts: boolean;
+    }
+  | {
+      type: "log";
+      chainId: number;
+      address: FragmentAddress;
+      topic0: FragmentTopic;
+      topic1: FragmentTopic;
+      topic2: FragmentTopic;
+      topic3: FragmentTopic;
+      includeTransactionReceipts: boolean;
+    }
+  | {
+      type: "transfer";
+      chainId: number;
+      fromAddress: FragmentAddress;
+      toAddress: FragmentAddress;
+      includeTransactionReceipts: boolean;
+    };
+
 export type FragmentId =
   /** block_{chainId}_{interval}_{offset} */
   | `block_${number}_${number}_${number}`
   /** transaction_{chainId}_{fromAddress}_{toAddress} */
-  | `transaction_${number}_${FragmentAddress}_${FragmentAddress}`
+  | `transaction_${number}_${FragmentAddressId}_${FragmentAddressId}`
   /** trace_{chainId}_{fromAddress}_{toAddress}_{functionSelector}_{includeReceipts} */
-  | `trace_${number}_${FragmentAddress}_${FragmentAddress}_${Hex | null}_${0 | 1}`
+  | `trace_${number}_${FragmentAddressId}_${FragmentAddressId}_${Hex | null}_${0 | 1}`
   /** log_{chainId}_{address}_{topic0}_{topic1}_{topic2}_{topic3}_{includeReceipts} */
-  | `log_${number}_${FragmentAddress}_${FragmentTopic}_${FragmentTopic}_${FragmentTopic}_${FragmentTopic}_${0 | 1}`
+  | `log_${number}_${FragmentAddressId}_${FragmentTopic}_${FragmentTopic}_${FragmentTopic}_${FragmentTopic}_${0 | 1}`
   /** transfer_{chainId}_{fromAddress}_{toAddress}_{includeReceipts} */
-  | `transfer_${number}_${FragmentAddress}_${FragmentAddress}_${0 | 1}`;
+  | `transfer_${number}_${FragmentAddressId}_${FragmentAddressId}_${0 | 1}`;
 
-export const getFragmentIds = (
-  filter: Omit<Filter, "startBlock" | "endBlock">,
+export const getFragments = (
+  filter: FilterWithoutBlocks,
 ): FragmentReturnType => {
   switch (filter.type) {
     case "block":
-      return getBlockFilterFragmentId(filter as BlockFilter);
+      return getBlockFilterFragment(filter as BlockFilter);
     case "transaction":
-      return getTransactionFilterFragmentIds(filter as TransactionFilter);
+      return getTransactionFilterFragments(filter as TransactionFilter);
     case "trace":
-      return getTraceFilterFragmentIds(filter as TraceFilter);
+      return getTraceFilterFragments(filter as TraceFilter);
     case "log":
-      return getLogFilterFragmentIds(filter as LogFilter);
+      return getLogFilterFragments(filter as LogFilter);
     case "transfer":
-      return getTransferFilterFragmentIds(filter as TransferFilter);
+      return getTransferFilterFragments(filter as TransferFilter);
   }
 };
 
 type FragmentReturnType = {
-  id: FragmentId;
-  adjacent: FragmentId[];
+  fragment: Fragment;
+  adjacentIds: FragmentId[];
 }[];
 
-const getAddressFragmentIds = (
+const getAddressFragments = (
   address: Address | Address[] | Factory | undefined,
 ) => {
-  const fragments: { id: FragmentAddress; adjacent: FragmentAddress[] }[] = [];
+  const fragments: {
+    fragment: FragmentAddress;
+    adjacentIds: FragmentAddressId[];
+  }[] = [];
 
   if (isAddressFactory(address)) {
     for (const fragmentAddress of Array.isArray(address.address)
       ? address.address
       : [address.address]) {
-      const id =
-        `${fragmentAddress}_${address.eventSelector}_${address.childAddressLocation}` as const;
+      const fragment = {
+        address: fragmentAddress,
+        eventSelector: address.eventSelector,
+        childAddressLocation: address.childAddressLocation,
+      } satisfies FragmentAddress;
 
-      fragments.push({ id, adjacent: [id] });
+      fragments.push({
+        fragment,
+        adjacentIds: [
+          `${fragmentAddress}_${address.eventSelector}_${address.childAddressLocation}` as const,
+        ],
+      });
     }
   } else {
     for (const fragmentAddress of Array.isArray(address)
       ? address
       : [address ?? null]) {
       fragments.push({
-        id: fragmentAddress,
-        adjacent: fragmentAddress ? [fragmentAddress, null] : [fragmentAddress],
+        fragment: fragmentAddress,
+        adjacentIds: fragmentAddress
+          ? [fragmentAddress, null]
+          : [fragmentAddress],
       });
     }
   }
@@ -79,20 +142,25 @@ const getAddressFragmentIds = (
   return fragments;
 };
 
-export const getBlockFilterFragmentId = ({
+export const getBlockFilterFragment = ({
   chainId,
   interval,
   offset,
 }: Omit<BlockFilter, "fromBlock" | "toBlock">): FragmentReturnType => {
   return [
     {
-      id: `block_${chainId}_${interval}_${offset}`,
-      adjacent: [`block_${chainId}_${interval}_${offset}`],
+      fragment: {
+        type: "block",
+        chainId,
+        interval,
+        offset,
+      },
+      adjacentIds: [`block_${chainId}_${interval}_${offset}`],
     },
   ];
 };
 
-export const getTransactionFilterFragmentIds = ({
+export const getTransactionFilterFragments = ({
   chainId,
   fromAddress,
   toAddress,
@@ -100,32 +168,36 @@ export const getTransactionFilterFragmentIds = ({
   chainId: number;
 }): FragmentReturnType => {
   const fragments: FragmentReturnType = [];
-  const fromAddressFragmentIds = getAddressFragmentIds(fromAddress);
-  const toAddressFragmentIds = getAddressFragmentIds(toAddress);
-
-  for (const fragmentFromAddress of fromAddressFragmentIds) {
-    for (const fragmentToAddress of toAddressFragmentIds) {
-      const id =
-        `transaction_${chainId}_${fragmentFromAddress.id}_${fragmentToAddress.id}` as const;
-
-      const adjacent: FragmentId[] = [];
-
-      for (const adjacentFromAddress of fragmentFromAddress.adjacent) {
-        for (const adjacentToAddress of fragmentToAddress.adjacent) {
-          adjacent.push(
-            `transaction_${chainId}_${adjacentFromAddress}_${adjacentToAddress}`,
+  const fromAddressFragments = getAddressFragments(fromAddress);
+  const toAddressFragments = getAddressFragments(toAddress);
+
+  for (const fromAddressFragment of fromAddressFragments) {
+    for (const toAddressFragment of toAddressFragments) {
+      const fragment = {
+        type: "transaction",
+        chainId,
+        fromAddress: fromAddressFragment.fragment,
+        toAddress: toAddressFragment.fragment,
+      } satisfies Fragment;
+
+      const adjacentIds: FragmentId[] = [];
+
+      for (const fromAddressAdjacentId of fromAddressFragment.adjacentIds) {
+        for (const toAddressAdjacentId of toAddressFragment.adjacentIds) {
+          adjacentIds.push(
+            `transaction_${chainId}_${fromAddressAdjacentId}_${toAddressAdjacentId}`,
           );
         }
       }
 
-      fragments.push({ id, adjacent });
+      fragments.push({ fragment, adjacentIds });
     }
   }
 
   return fragments;
 };
 
-export const getTraceFilterFragmentIds = ({
+export const getTraceFilterFragments = ({
   chainId,
   fromAddress,
   toAddress,
@@ -136,37 +208,43 @@ export const getTraceFilterFragmentIds = ({
   chainId: number;
 }): FragmentReturnType => {
   const fragments: FragmentReturnType = [];
-  const fromAddressFragmentIds = getAddressFragmentIds(fromAddress);
-  const toAddressFragmentIds = getAddressFragmentIds(toAddress);
+  const fromAddressFragments = getAddressFragments(fromAddress);
+  const toAddressFragments = getAddressFragments(toAddress);
   const includeTransactionReceipts = shouldGetTransactionReceipt(filter);
 
-  for (const fragmentFromAddress of fromAddressFragmentIds) {
-    for (const fragmentToAddress of toAddressFragmentIds) {
+  for (const fromAddressFragment of fromAddressFragments) {
+    for (const toAddressFragment of toAddressFragments) {
       for (const fragmentFunctionSelector of Array.isArray(functionSelector)
         ? functionSelector
         : [functionSelector]) {
-        const id =
-          `trace_${chainId}_${fragmentFromAddress.id}_${fragmentToAddress.id}_${fragmentFunctionSelector ?? null}_${includeTransactionReceipts ? 1 : 0}` as const;
-
-        const adjacent: FragmentId[] = [];
-
-        for (const adjacentFromAddress of fragmentFromAddress.adjacent) {
-          for (const adjacentToAddress of fragmentToAddress.adjacent) {
+        const fragment = {
+          type: "trace",
+          chainId,
+          fromAddress: fromAddressFragment.fragment,
+          toAddress: toAddressFragment.fragment,
+          functionSelector: fragmentFunctionSelector ?? null,
+          includeTransactionReceipts,
+        } satisfies Fragment;
+
+        const adjacentIds: FragmentId[] = [];
+
+        for (const fromAddressAdjacentId of fromAddressFragment.adjacentIds) {
+          for (const toAddressAdjacentId of toAddressFragment.adjacentIds) {
             for (const adjacentFunctionSelector of fragmentFunctionSelector
               ? [fragmentFunctionSelector, null]
               : [null]) {
               for (const adjacentTxr of includeTransactionReceipts
                 ? [1]
                 : [0, 1]) {
-                adjacent.push(
-                  `trace_${chainId}_${adjacentFromAddress}_${adjacentToAddress}_${adjacentFunctionSelector}_${adjacentTxr as 0 | 1}`,
+                adjacentIds.push(
+                  `trace_${chainId}_${fromAddressAdjacentId}_${toAddressAdjacentId}_${adjacentFunctionSelector}_${adjacentTxr as 0 | 1}`,
                 );
               }
             }
           }
         }
 
-        fragments.push({ id, adjacent });
+        fragments.push({ fragment, adjacentIds });
       }
     }
   }
@@ -174,7 +252,7 @@ export const getTraceFilterFragmentIds = ({
   return fragments;
 };
 
-export const getLogFilterFragmentIds = ({
+export const getLogFilterFragments = ({
   chainId,
   address,
   topic0,
@@ -184,10 +262,10 @@ export const getLogFilterFragmentIds = ({
   ...filter
 }: Omit<LogFilter, "fromBlock" | "toBlock">): FragmentReturnType => {
   const fragments: FragmentReturnType = [];
-  const addressFragmentIds = getAddressFragmentIds(address);
+  const addressFragments = getAddressFragments(address);
   const includeTransactionReceipts = shouldGetTransactionReceipt(filter);
 
-  for (const fragmentAddress of addressFragmentIds) {
+  for (const addressFragment of addressFragments) {
     for (const fragmentTopic0 of Array.isArray(topic0) ? topic0 : [topic0]) {
       for (const fragmentTopic1 of Array.isArray(topic1) ? topic1 : [topic1]) {
         for (const fragmentTopic2 of Array.isArray(topic2)
@@ -196,12 +274,20 @@ export const getLogFilterFragmentIds = ({
           for (const fragmentTopic3 of Array.isArray(topic3)
             ? topic3
             : [topic3]) {
-            const id =
-              `log_${chainId}_${fragmentAddress.id}_${fragmentTopic0 ?? null}_${fragmentTopic1 ?? null}_${fragmentTopic2 ?? null}_${fragmentTopic3 ?? null}_${includeTransactionReceipts ? 1 : 0}` as const;
-
-            const adjacent: FragmentId[] = [];
-
-            for (const adjacentAddress of fragmentAddress.adjacent) {
+            const fragment = {
+              type: "log",
+              chainId,
+              address: addressFragment.fragment,
+              topic0: fragmentTopic0 ?? null,
+              topic1: fragmentTopic1 ?? null,
+              topic2: fragmentTopic2 ?? null,
+              topic3: fragmentTopic3 ?? null,
+              includeTransactionReceipts,
+            } satisfies Fragment;
+
+            const adjacentIds: FragmentId[] = [];
+
+            for (const addressAdjacentId of addressFragment.adjacentIds) {
               for (const adjacentTopic0 of fragmentTopic0
                 ? [fragmentTopic0, null]
                 : [null]) {
@@ -217,8 +303,8 @@ export const getLogFilterFragmentIds = ({
                       for (const adjacentTxr of includeTransactionReceipts
                         ? [1]
                         : [0, 1]) {
-                        adjacent.push(
-                          `log_${chainId}_${adjacentAddress}_${adjacentTopic0}_${adjacentTopic1}_${adjacentTopic2}_${adjacentTopic3}_${adjacentTxr as 0 | 1}`,
+                        adjacentIds.push(
+                          `log_${chainId}_${addressAdjacentId}_${adjacentTopic0}_${adjacentTopic1}_${adjacentTopic2}_${adjacentTopic3}_${adjacentTxr as 0 | 1}`,
                         );
                       }
                     }
@@ -227,7 +313,7 @@ export const getLogFilterFragmentIds = ({
               }
             }
 
-            fragments.push({ id, adjacent });
+            fragments.push({ fragment, adjacentIds });
           }
         }
       }
@@ -237,7 +323,7 @@ export const getLogFilterFragmentIds = ({
   return fragments;
 };
 
-export const getTransferFilterFragmentIds = ({
+export const getTransferFilterFragments = ({
   chainId,
   fromAddress,
   toAddress,
@@ -246,30 +332,211 @@ export const getTransferFilterFragmentIds = ({
   chainId: number;
 }): FragmentReturnType => {
   const fragments: FragmentReturnType = [];
-  const fromAddressFragmentIds = getAddressFragmentIds(fromAddress);
-  const toAddressFragmentIds = getAddressFragmentIds(toAddress);
+  const fromAddressFragments = getAddressFragments(fromAddress);
+  const toAddressFragments = getAddressFragments(toAddress);
   const includeTransactionReceipts = shouldGetTransactionReceipt(filter);
 
-  for (const fragmentFromAddress of fromAddressFragmentIds) {
-    for (const fragmentToAddress of toAddressFragmentIds) {
-      const id =
-        `transfer_${chainId}_${fragmentFromAddress.id}_${fragmentToAddress.id}_${includeTransactionReceipts ? 1 : 0}` as const;
+  for (const fromAddressFragment of fromAddressFragments) {
+    for (const toAddressFragment of toAddressFragments) {
+      const fragment = {
+        type: "transfer",
+        chainId,
+        fromAddress: fromAddressFragment.fragment,
+        toAddress: toAddressFragment.fragment,
+        includeTransactionReceipts,
+      } satisfies Fragment;
 
-      const adjacent: FragmentId[] = [];
+      const adjacentIds: FragmentId[] = [];
 
-      for (const adjacentFromAddress of fragmentFromAddress.adjacent) {
-        for (const adjacentToAddress of fragmentToAddress.adjacent) {
+      for (const fromAddressAdjacentId of fromAddressFragment.adjacentIds) {
+        for (const toAddressAdjacentId of toAddressFragment.adjacentIds) {
           for (const adjacentTxr of includeTransactionReceipts ? [1] : [0, 1]) {
-            adjacent.push(
-              `transfer_${chainId}_${adjacentFromAddress}_${adjacentToAddress}_${adjacentTxr as 0 | 1}`,
+            adjacentIds.push(
+              `transfer_${chainId}_${fromAddressAdjacentId}_${toAddressAdjacentId}_${adjacentTxr as 0 | 1}`,
             );
           }
         }
       }
 
-      fragments.push({ id, adjacent });
+      fragments.push({ fragment, adjacentIds });
     }
   }
 
   return fragments;
 };
+
+const fragmentAddressToId = (
+  fragmentAddress: FragmentAddress,
+): FragmentAddressId => {
+  if (fragmentAddress === null) return null;
+  if (typeof fragmentAddress === "string") return fragmentAddress;
+  return `${fragmentAddress.address}_${fragmentAddress.eventSelector}_${fragmentAddress.childAddressLocation}`;
+};
+
+export const fragmentToId = (fragment: Fragment): FragmentId => {
+  switch (fragment.type) {
+    case "block":
+      return `block_${fragment.chainId}_${fragment.interval}_${fragment.offset}`;
+    case "transaction":
+      return `transaction_${fragment.chainId}_${fragmentAddressToId(fragment.fromAddress)}_${fragmentAddressToId(fragment.toAddress)}`;
+    case "trace":
+      return `trace_${fragment.chainId}_${fragmentAddressToId(fragment.fromAddress)}_${fragmentAddressToId(fragment.toAddress)}_${fragment.functionSelector}_${fragment.includeTransactionReceipts ? 1 : 0}`;
+    case "log":
+      return `log_${fragment.chainId}_${fragmentAddressToId(fragment.address)}_${fragment.topic0}_${fragment.topic1}_${fragment.topic2}_${fragment.topic3}_${fragment.includeTransactionReceipts ? 1 : 0}`;
+    case "transfer":
+      return `transfer_${fragment.chainId}_${fragmentAddressToId(fragment.fromAddress)}_${fragmentAddressToId(fragment.toAddress)}_${fragment.includeTransactionReceipts ? 1 : 0}`;
+  }
+};
+
+const recoverAddress = (
+  baseAddress: FilterAddress,
+  fragmentAddresses: FragmentAddress[],
+): FilterAddress => {
+  if (baseAddress === undefined) return undefined;
+  if (typeof baseAddress === "string") return baseAddress;
+  if (Array.isArray(baseAddress)) return dedupe(fragmentAddresses) as Address[];
+  if (typeof baseAddress.address === "string") return baseAddress;
+
+  const address = {
+    type: "log",
+    chainId: baseAddress.chainId,
+    address: [] as Address[],
+    eventSelector: baseAddress.eventSelector,
+    childAddressLocation: baseAddress.childAddressLocation,
+  } satisfies Factory;
+
+  address.address = dedupe(
+    (fragmentAddresses as Extract<FragmentAddress, { address: Address }>[]).map(
+      ({ address }) => address,
+    ),
+  );
+
+  return address;
+};
+
+const recoverSelector = (
+  base: Hex | Hex[] | undefined,
+  fragments: (Hex | null)[],
+): Hex | Hex[] | undefined => {
+  if (base === undefined) return undefined;
+  if (typeof base === "string") return base;
+  return dedupe(fragments) as Hex[];
+};
+
+const recoverTopic = (
+  base: Hex | Hex[] | null,
+  fragments: (Hex | null)[],
+): Hex | Hex[] | null => {
+  if (base === null) return null;
+  if (typeof base === "string") return base;
+  return dedupe(fragments) as Hex[];
+};
+
+export const recoverFilter = (
+  baseFilter: FilterWithoutBlocks,
+  fragments: Fragment[],
+): FilterWithoutBlocks => {
+  switch (baseFilter.type) {
+    case "block": {
+      return baseFilter;
+    }
+    case "transaction": {
+      return {
+        ...baseFilter,
+        fromAddress: recoverAddress(
+          baseFilter.fromAddress,
+          (fragments as Extract<Fragment, { type: "transaction" }>[]).map(
+            (fragment) => fragment.fromAddress,
+          ),
+        ),
+        toAddress: recoverAddress(
+          baseFilter.toAddress,
+          (fragments as Extract<Fragment, { type: "transaction" }>[]).map(
+            (fragment) => fragment.toAddress,
+          ),
+        ),
+      };
+    }
+    case "trace": {
+      return {
+        ...baseFilter,
+        fromAddress: recoverAddress(
+          baseFilter.fromAddress,
+          (fragments as Extract<Fragment, { type: "transaction" }>[]).map(
+            (fragment) => fragment.fromAddress,
+          ),
+        ),
+        toAddress: recoverAddress(
+          baseFilter.toAddress,
+          (fragments as Extract<Fragment, { type: "transaction" }>[]).map(
+            (fragment) => fragment.toAddress,
+          ),
+        ),
+        functionSelector: recoverSelector(
+          baseFilter.functionSelector,
+          fragments.map(
+            (fragment) =>
+              (fragment as Extract<Fragment, { type: "trace" }>)
+                .functionSelector,
+          ),
+        ),
+      };
+    }
+    case "log": {
+      return {
+        ...baseFilter,
+        address: recoverAddress(
+          baseFilter.address,
+          (fragments as Extract<Fragment, { type: "log" }>[]).map(
+            (fragment) => fragment.address,
+          ),
+        ),
+        topic0: recoverTopic(
+          baseFilter.topic0,
+          fragments.map(
+            (fragment) =>
+              (fragment as Extract<Fragment, { type: "log" }>).topic0,
+          ),
+        ),
+        topic1: recoverTopic(
+          baseFilter.topic1,
+          fragments.map(
+            (fragment) =>
+              (fragment as Extract<Fragment, { type: "log" }>).topic1,
+          ),
+        ),
+        topic2: recoverTopic(
+          baseFilter.topic2,
+          fragments.map(
+            (fragment) =>
+              (fragment as Extract<Fragment, { type: "log" }>).topic2,
+          ),
+        ),
+        topic3: recoverTopic(
+          baseFilter.topic3,
+          fragments.map(
+            (fragment) =>
+              (fragment as Extract<Fragment, { type: "log" }>).topic3,
+          ),
+        ),
+      };
+    }
+    case "transfer": {
+      return {
+        ...baseFilter,
+        fromAddress: recoverAddress(
+          baseFilter.fromAddress,
+          (fragments as Extract<Fragment, { type: "transaction" }>[]).map(
+            (fragment) => fragment.fromAddress,
+          ),
+        ),
+        toAddress: recoverAddress(
+          baseFilter.toAddress,
+          (fragments as Extract<Fragment, { type: "transaction" }>[]).map(
+            (fragment) => fragment.toAddress,
+          ),
+        ),
+      };
+    }
+  }
+};
diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts
index 90ee5a3e..3564e97f 100644
--- a/packages/core/src/sync/index.ts
+++ b/packages/core/src/sync/index.ts
@@ -25,6 +25,7 @@ import {
   type Interval,
   intervalDifference,
   intervalIntersection,
+  intervalIntersectionMany,
   intervalSum,
   sortIntervals,
 } from "@/utils/interval.js";
@@ -679,6 +680,22 @@ export const createSync = async (args: CreateSyncParameters): Promise<Sync> => {
                   msg: `Indexed ${events.length} events`,
                 });
               }
+
+              // update `ponder_realtime_latency` metric
+              for (const network of args.networks) {
+                for (const { block, endClock } of perNetworkSync.get(network)!
+                  .unfinalizedBlocks) {
+                  const checkpoint = encodeCheckpoint(
+                    blockToCheckpoint(block, network.chainId, "up"),
+                  );
+                  if (checkpoint > from && checkpoint <= to && endClock) {
+                    args.common.metrics.ponder_realtime_latency.observe(
+                      { network: network.name },
+                      endClock(),
+                    );
+                  }
+                }
+              }
             });
         }
 
@@ -1043,10 +1060,15 @@ export const getCachedBlock = ({
       filter.fromBlock ?? 0,
       filter.toBlock ?? Number.POSITIVE_INFINITY,
     ] satisfies Interval;
-    const cachedIntervals = historicalSync.intervalsCache.get(filter)!;
+    const fragmentIntervals = historicalSync.intervalsCache.get(filter)!;
 
     const completedIntervals = sortIntervals(
-      intervalIntersection([requiredInterval], cachedIntervals),
+      intervalIntersection(
+        [requiredInterval],
+        intervalIntersectionMany(
+          fragmentIntervals.map(({ intervals }) => intervals),
+        ),
+      ),
     );
 
     if (completedIntervals.length === 0) return undefined;
@@ -1137,7 +1159,7 @@ export async function* localHistoricalSyncGenerator({
 
   const requiredIntervals = Array.from(
     historicalSync.intervalsCache.entries(),
-  ).flatMap(([filter, interval]) =>
+  ).flatMap(([filter, fragmentIntervals]) =>
     intervalDifference(
       [
         [
@@ -1148,7 +1170,9 @@ export async function* localHistoricalSyncGenerator({
           ),
         ],
       ],
-      interval,
+      intervalIntersectionMany(
+        fragmentIntervals.map(({ intervals }) => intervals),
+      ),
     ),
   );
 
@@ -1253,7 +1277,7 @@ export async function* localHistoricalSyncGenerator({
 
       // Update "ponder_sync_block" metric
       common.metrics.ponder_sync_block.set(
-        { network: network.name },
+        label,
         hexToNumber(syncProgress.current.number),
       );
 
diff --git a/packages/core/src/sync/source.ts b/packages/core/src/sync/source.ts
index ed067d4b..01c04fe2 100644
--- a/packages/core/src/sync/source.ts
+++ b/packages/core/src/sync/source.ts
@@ -40,7 +40,16 @@ export type Filter =
   | TransferFilter
   | TransactionFilter
   | TraceFilter;
+export type FilterWithoutBlocks =
+  | Omit<BlockFilter, "fromBlock" | "toBlock">
+  | Omit<TransactionFilter, "fromBlock" | "toBlock">
+  | Omit<TraceFilter, "fromBlock" | "toBlock">
+  | Omit<LogFilter, "fromBlock" | "toBlock">
+  | Omit<TransferFilter, "fromBlock" | "toBlock">;
 export type Factory = LogFactory;
+export type FilterAddress<
+  factory extends Factory | undefined = Factory | undefined,
+> = factory extends Factory ? factory : Address | Address[] | undefined;
 
 export type ContractMetadata = {
   type: "contract";
@@ -66,11 +75,11 @@ export type LogFilter<
 > = {
   type: "log";
   chainId: number;
-  address: factory extends Factory ? factory : Address | Address[] | undefined;
-  topic0: LogTopic | undefined;
-  topic1: LogTopic | undefined;
-  topic2: LogTopic | undefined;
-  topic3: LogTopic | undefined;
+  address: FilterAddress<factory>;
+  topic0: LogTopic;
+  topic1: LogTopic;
+  topic2: LogTopic;
+  topic3: LogTopic;
   fromBlock: number | undefined;
   toBlock: number | undefined;
   include:
@@ -99,12 +108,8 @@ export type TransferFilter<
 > = {
   type: "transfer";
   chainId: number;
-  fromAddress: fromFactory extends Factory
-    ? fromFactory
-    : Address | Address[] | undefined;
-  toAddress: toFactory extends Factory
-    ? fromFactory
-    : Address | Address[] | undefined;
+  fromAddress: FilterAddress<fromFactory>;
+  toAddress: FilterAddress<toFactory>;
   includeReverted: boolean;
   fromBlock: number | undefined;
   toBlock: number | undefined;
@@ -124,12 +129,8 @@ export type TransactionFilter<
 > = {
   type: "transaction";
   chainId: number;
-  fromAddress: fromFactory extends Factory
-    ? fromFactory
-    : Address | Address[] | undefined;
-  toAddress: toFactory extends Factory
-    ? toFactory
-    : Address | Address[] | undefined;
+  fromAddress: FilterAddress<fromFactory>;
+  toAddress: FilterAddress<toFactory>;
   includeReverted: boolean;
   fromBlock: number | undefined;
   toBlock: number | undefined;
@@ -148,12 +149,8 @@ export type TraceFilter<
 > = {
   type: "trace";
   chainId: number;
-  fromAddress: fromFactory extends Factory
-    ? fromFactory
-    : Address | Address[] | undefined;
-  toAddress: toFactory extends Factory
-    ? toFactory
-    : Address | Address[] | undefined;
+  fromAddress: FilterAddress<fromFactory>;
+  toAddress: FilterAddress<toFactory>;
   functionSelector: Hex | Hex[] | undefined;
   callType: Trace["result"]["type"] | undefined;
   includeReverted: boolean;
diff --git a/packages/core/src/utils/interval.ts b/packages/core/src/utils/interval.ts
index 62508a50..ff1722cc 100644
--- a/packages/core/src/utils/interval.ts
+++ b/packages/core/src/utils/interval.ts
@@ -166,6 +166,19 @@ export function intervalDifference(
   return result;
 }
 
+/**
+ * Return an interval that encompasses all the intervals in the list.
+ *
+ * @param intervals List of numeric intervals to find the bounds of.
+ * @returns Bounds of the intervals.
+ */
+export function intervalBounds(intervals: Interval[]): Interval {
+  const start = Math.min(...intervals.map((interval) => interval[0]));
+  const end = Math.max(...intervals.map((interval) => interval[1]));
+
+  return [start, end];
+}
+
 export function sortIntervals(intervals: Interval[]) {
   return intervals.sort((a, b) => (a[0] < b[0] ? -1 : 1));
 }
diff --git a/packages/create-ponder/CHANGELOG.md b/packages/create-ponder/CHANGELOG.md
index 42f76a87..a97b1f56 100644
--- a/packages/create-ponder/CHANGELOG.md
+++ b/packages/create-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # create-ponder
 
+## 0.8.25
+
 ## 0.8.24
 
 ## 0.8.23
diff --git a/packages/create-ponder/package.json b/packages/create-ponder/package.json
index cf1a5385..f26a8c22 100644
--- a/packages/create-ponder/package.json
+++ b/packages/create-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-ponder",
-  "version": "0.8.24",
+  "version": "0.8.25",
   "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 f8c4a01e..abe336af 100644
--- a/packages/eslint-config-ponder/CHANGELOG.md
+++ b/packages/eslint-config-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # eslint-config-ponder
 
+## 0.8.25
+
 ## 0.8.24
 
 ## 0.8.23
diff --git a/packages/eslint-config-ponder/package.json b/packages/eslint-config-ponder/package.json
index 4c88cf8a..a65378ed 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.8.24",
+  "version": "0.8.25",
   "description": "ESLint config for Ponder apps",
   "license": "MIT",
   "main": "./index.js",


Test patch:
diff --git a/packages/core/src/sync-historical/index.test.ts b/packages/core/src/sync-historical/index.test.ts
index bb4d8645..6b6170a3 100644
--- a/packages/core/src/sync-historical/index.test.ts
+++ b/packages/core/src/sync-historical/index.test.ts
@@ -25,7 +25,12 @@ import {
 } from "@/_test/utils.js";
 import { buildConfigAndIndexingFunctions } from "@/build/configAndIndexingFunctions.js";
 import { createRequestQueue } from "@/utils/requestQueue.js";
-import { encodeFunctionData, encodeFunctionResult, toHex } from "viem";
+import {
+  encodeFunctionData,
+  encodeFunctionResult,
+  toHex,
+  zeroAddress,
+} from "viem";
 import { parseEther } from "viem/utils";
 import { beforeEach, expect, test, vi } from "vitest";
 import { createHistoricalSync } from "./index.js";
@@ -568,7 +573,7 @@ test("sync() with many filters", async (context) => {
   await cleanup();
 });
 
-test("sync() with cache hit", async (context) => {
+test("sync() with cache", async (context) => {
   const { cleanup, syncStore } = await setupDatabaseServices(context);
 
   const network = getNetwork();
@@ -623,6 +628,114 @@ test("sync() with cache hit", async (context) => {
   await cleanup();
 });
 
+test("sync() with partial cache", async (context) => {
+  const { cleanup, syncStore } = await setupDatabaseServices(context);
+
+  const network = getNetwork();
+  const requestQueue = createRequestQueue({
+    network,
+    common: context.common,
+  });
+
+  const { address } = await deployErc20({ sender: ALICE });
+  await mintErc20({
+    erc20: address,
+    to: ALICE,
+    amount: parseEther("1"),
+    sender: ALICE,
+  });
+
+  const { config, rawIndexingFunctions } = getErc20ConfigAndIndexingFunctions({
+    address,
+  });
+  const { sources } = await buildConfigAndIndexingFunctions({
+    config,
+    rawIndexingFunctions,
+  });
+
+  let historicalSync = await createHistoricalSync({
+    common: context.common,
+    network,
+    sources,
+    syncStore,
+    requestQueue,
+    onFatalError: () => {},
+  });
+
+  await historicalSync.sync([1, 2]);
+
+  // re-instantiate `historicalSync` to reset the cached intervals
+
+  let spy = vi.spyOn(requestQueue, "request");
+
+  // @ts-ignore
+  sources[0]!.filter.address = [sources[0]!.filter.address, zeroAddress];
+
+  historicalSync = await createHistoricalSync({
+    common: context.common,
+    network,
+    sources,
+    syncStore,
+    requestQueue,
+    onFatalError: () => {},
+  });
+
+  await historicalSync.sync([1, 2]);
+  expect(spy).toHaveBeenCalledTimes(2);
+
+  expect(spy).toHaveBeenCalledWith({
+    method: "eth_getLogs",
+    params: [
+      {
+        address: [zeroAddress],
+        fromBlock: "0x1",
+        toBlock: "0x2",
+        topics: [
+          [
+            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+          ],
+        ],
+      },
+    ],
+  });
+
+  // re-instantiate `historicalSync` to reset the cached intervals
+
+  spy = vi.spyOn(requestQueue, "request");
+
+  historicalSync = await createHistoricalSync({
+    common: context.common,
+    network,
+    sources,
+    syncStore,
+    requestQueue,
+    onFatalError: () => {},
+  });
+
+  await testClient.mine({ blocks: 1 });
+
+  await historicalSync.sync([1, 3]);
+  expect(spy).toHaveBeenCalledTimes(2);
+
+  expect(spy).toHaveBeenCalledWith({
+    method: "eth_getLogs",
+    params: [
+      {
+        address: [address, zeroAddress],
+        fromBlock: "0x3",
+        toBlock: "0x3",
+        topics: [
+          [
+            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+          ],
+        ],
+      },
+    ],
+  });
+
+  await cleanup();
+});
+
 test("syncBlock() with cache", async (context) => {
   const { cleanup, syncStore } = await setupDatabaseServices(context);
 
diff --git a/packages/core/src/sync-realtime/index.test.ts b/packages/core/src/sync-realtime/index.test.ts
index 02d3a027..b0ab4741 100644
--- a/packages/core/src/sync-realtime/index.test.ts
+++ b/packages/core/src/sync-realtime/index.test.ts
@@ -371,6 +371,7 @@ test("handleBlock() block event with log", async (context) => {
   expect(onEvent).toHaveBeenCalledWith({
     type: "block",
     hasMatchedFilter: true,
+    endClock: expect.any(Function),
     block: expect.any(Object),
     logs: expect.any(Object),
     factoryLogs: expect.any(Object),
@@ -454,6 +455,7 @@ test("handleBlock() block event with log factory", async (context) => {
   expect(onEvent).toHaveBeenCalledWith({
     type: "block",
     hasMatchedFilter: true,
+    endClock: expect.any(Function),
     block: expect.any(Object),
     logs: expect.any(Object),
     factoryLogs: expect.any(Object),
@@ -533,6 +535,7 @@ test("handleBlock() block event with block", async (context) => {
   expect(onEvent).toHaveBeenCalledWith({
     type: "block",
     hasMatchedFilter: true,
+    endClock: expect.any(Function),
     block: expect.any(Object),
     logs: expect.any(Object),
     factoryLogs: expect.any(Object),
@@ -607,6 +610,7 @@ test("handleBlock() block event with transaction", async (context) => {
   expect(onEvent).toHaveBeenCalledWith({
     type: "block",
     hasMatchedFilter: true,
+    endClock: expect.any(Function),
     block: expect.any(Object),
     logs: expect.any(Object),
     factoryLogs: expect.any(Object),
@@ -708,6 +712,7 @@ test("handleBlock() block event with transfer", async (context) => {
   expect(onEvent).toHaveBeenCalledWith({
     type: "block",
     hasMatchedFilter: true,
+    endClock: expect.any(Function),
     block: expect.any(Object),
     logs: expect.any(Object),
     factoryLogs: expect.any(Object),
@@ -853,6 +858,7 @@ test("handleBlock() block event with trace", async (context) => {
   expect(onEvent).toHaveBeenCalledWith({
     type: "block",
     hasMatchedFilter: true,
+    endClock: expect.any(Function),
     block: expect.any(Object),
     logs: expect.any(Object),
     factoryLogs: expect.any(Object),
diff --git a/packages/core/src/sync-store/index.test.ts b/packages/core/src/sync-store/index.test.ts
index c2708874..0cde7691 100644
--- a/packages/core/src/sync-store/index.test.ts
+++ b/packages/core/src/sync-store/index.test.ts
@@ -89,7 +89,29 @@ test("getIntervals() empty", async (context) => {
     filters: [filter],
   });
 
-  expect(Array.from(intervals.values())[0]).toHaveLength(0);
+  expect(intervals).toMatchInlineSnapshot(`
+    Map {
+      {
+        "chainId": 1,
+        "fromBlock": undefined,
+        "include": [],
+        "interval": 1,
+        "offset": 0,
+        "toBlock": undefined,
+        "type": "block",
+      } => [
+        {
+          "fragment": {
+            "chainId": 1,
+            "interval": 1,
+            "offset": 0,
+            "type": "block",
+          },
+          "intervals": [],
+        },
+      ],
+    }
+  `);
 
   await cleanup();
 });
@@ -121,8 +143,34 @@ test("getIntervals() returns intervals", async (context) => {
     filters: [filter],
   });
 
-  expect(Array.from(intervals.values())[0]).toHaveLength(1);
-  expect(Array.from(intervals.values())[0]![0]).toStrictEqual([0, 4]);
+  expect(intervals).toMatchInlineSnapshot(`
+    Map {
+      {
+        "chainId": 1,
+        "fromBlock": undefined,
+        "include": [],
+        "interval": 1,
+        "offset": 0,
+        "toBlock": undefined,
+        "type": "block",
+      } => [
+        {
+          "fragment": {
+            "chainId": 1,
+            "interval": 1,
+            "offset": 0,
+            "type": "block",
+          },
+          "intervals": [
+            [
+              0,
+              4,
+            ],
+          ],
+        },
+      ],
+    }
+  `);
 
   await cleanup();
 });
@@ -163,8 +211,34 @@ test("getIntervals() merges intervals", async (context) => {
     filters: [filter],
   });
 
-  expect(Array.from(intervals.values())[0]).toHaveLength(1);
-  expect(Array.from(intervals.values())[0]![0]).toStrictEqual([0, 8]);
+  expect(intervals).toMatchInlineSnapshot(`
+    Map {
+      {
+        "chainId": 1,
+        "fromBlock": undefined,
+        "include": [],
+        "interval": 1,
+        "offset": 0,
+        "toBlock": undefined,
+        "type": "block",
+      } => [
+        {
+          "fragment": {
+            "chainId": 1,
+            "interval": 1,
+            "offset": 0,
+            "type": "block",
+          },
+          "intervals": [
+            [
+              0,
+              8,
+            ],
+          ],
+        },
+      ],
+    }
+  `);
 
   await cleanup();
 });
@@ -198,6 +272,7 @@ test("getIntervals() adjacent intervals", async (context) => {
   await syncStore.insertIntervals({
     intervals: [
       {
+        // @ts-ignore
         filter: { ...filter, address: undefined },
         interval: [5, 8],
       },
@@ -208,8 +283,43 @@ test("getIntervals() adjacent intervals", async (context) => {
     filters: [filter],
   });
 
-  expect(Array.from(intervals.values())[0]).toHaveLength(1);
-  expect(Array.from(intervals.values())[0]![0]).toStrictEqual([0, 8]);
+  expect(intervals).toMatchInlineSnapshot(`
+    Map {
+      {
+        "address": [
+          "0x0000000000000000000000000000000000000000",
+        ],
+        "chainId": 1,
+        "fromBlock": undefined,
+        "include": [],
+        "toBlock": undefined,
+        "topic0": null,
+        "topic1": null,
+        "topic2": null,
+        "topic3": null,
+        "type": "log",
+      } => [
+        {
+          "fragment": {
+            "address": "0x0000000000000000000000000000000000000000",
+            "chainId": 1,
+            "includeTransactionReceipts": false,
+            "topic0": null,
+            "topic1": null,
+            "topic2": null,
+            "topic3": null,
+            "type": "log",
+          },
+          "intervals": [
+            [
+              0,
+              8,
+            ],
+          ],
+        },
+      ],
+    }
+  `);
 
   await cleanup();
 });
@@ -255,8 +365,124 @@ test("insertIntervals() merges duplicates", async (context) => {
     filters: [filter],
   });
 
-  expect(Array.from(intervals.values())[0]).toHaveLength(1);
-  expect(Array.from(intervals.values())[0]![0]).toStrictEqual([0, 8]);
+  expect(intervals).toMatchInlineSnapshot(`
+    Map {
+      {
+        "chainId": 1,
+        "fromBlock": undefined,
+        "include": [],
+        "interval": 1,
+        "offset": 0,
+        "toBlock": undefined,
+        "type": "block",
+      } => [
+        {
+          "fragment": {
+            "chainId": 1,
+            "interval": 1,
+            "offset": 0,
+            "type": "block",
+          },
+          "intervals": [
+            [
+              0,
+              8,
+            ],
+          ],
+        },
+      ],
+    }
+  `);
+
+  await cleanup();
+});
+
+test("insertIntervals() preserves fragments", async (context) => {
+  const { cleanup, syncStore } = await setupDatabaseServices(context);
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    topic0: null,
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    address: [zeroAddress, ALICE],
+    fromBlock: undefined,
+    toBlock: undefined,
+    include: [],
+  } satisfies LogFilter;
+
+  await syncStore.insertIntervals({
+    intervals: [
+      {
+        filter,
+        interval: [0, 4],
+      },
+    ],
+    chainId: 1,
+  });
+
+  const intervals = await syncStore.getIntervals({
+    filters: [filter],
+  });
+
+  expect(intervals).toMatchInlineSnapshot(`
+    Map {
+      {
+        "address": [
+          "0x0000000000000000000000000000000000000000",
+          "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+        ],
+        "chainId": 1,
+        "fromBlock": undefined,
+        "include": [],
+        "toBlock": undefined,
+        "topic0": null,
+        "topic1": null,
+        "topic2": null,
+        "topic3": null,
+        "type": "log",
+      } => [
+        {
+          "fragment": {
+            "address": "0x0000000000000000000000000000000000000000",
+            "chainId": 1,
+            "includeTransactionReceipts": false,
+            "topic0": null,
+            "topic1": null,
+            "topic2": null,
+            "topic3": null,
+            "type": "log",
+          },
+          "intervals": [
+            [
+              0,
+              4,
+            ],
+          ],
+        },
+        {
+          "fragment": {
+            "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+            "chainId": 1,
+            "includeTransactionReceipts": false,
+            "topic0": null,
+            "topic1": null,
+            "topic2": null,
+            "topic3": null,
+            "type": "log",
+          },
+          "intervals": [
+            [
+              0,
+              4,
+            ],
+          ],
+        },
+      ],
+    }
+  `);
 
   await cleanup();
 });
diff --git a/packages/core/src/sync/fragments.test.ts b/packages/core/src/sync/fragments.test.ts
index da910817..4b7dc936 100644
--- a/packages/core/src/sync/fragments.test.ts
+++ b/packages/core/src/sync/fragments.test.ts
@@ -1,167 +1,610 @@
-import { buildLogFactory } from "@/build/factory.js";
-import { parseAbiItem } from "viem";
 import { expect, test } from "vitest";
-import { getLogFilterFragmentIds } from "./fragments.js";
+import { getFragments, recoverFilter } from "./fragments.js";
+import type { FilterWithoutBlocks } from "./source.js";
 
-const llamaFactoryEventAbiItem = parseAbiItem(
-  "event LlamaInstanceCreated(address indexed deployer, string indexed name, address llamaCore, address llamaExecutor, address llamaPolicy, uint256 chainId)",
-);
+test("getFragments() block filter", () => {
+  const fragments = getFragments({
+    type: "block",
+    chainId: 1,
+    interval: 100,
+    offset: 5,
+    include: [],
+  });
+
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "block_1_100_5",
+        ],
+        "fragment": {
+          "chainId": 1,
+          "interval": 100,
+          "offset": 5,
+          "type": "block",
+        },
+      },
+    ]
+  `);
+});
+
+test("getFragments() transaction filter", () => {
+  const fragments = getFragments({
+    type: "transaction",
+    chainId: 1,
+    fromAddress: "0xa",
+    toAddress: "0xb",
+    includeReverted: false,
+    include: [],
+  });
 
-test("getLogFilterFragmentIds generates 1 log filter fragment for null filter", () => {
-  const logFilterFragments = getLogFilterFragmentIds({
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "transaction_1_0xa_0xb",
+          "transaction_1_0xa_null",
+          "transaction_1_null_0xb",
+          "transaction_1_null_null",
+        ],
+        "fragment": {
+          "chainId": 1,
+          "fromAddress": "0xa",
+          "toAddress": "0xb",
+          "type": "transaction",
+        },
+      },
+    ]
+  `);
+});
+
+test("getFragments() log filter", () => {
+  const fragments = getFragments({
     type: "log",
     chainId: 1,
-    address: undefined,
-    topic0: null,
+    address: ["0xa", "0xb"],
+    topic0: ["0xc", "0xd"],
     topic1: null,
-    topic2: null,
+    topic2: "0xe",
     topic3: null,
     include: [],
   });
 
-  expect(logFilterFragments[0]!.id).toBe("log_1_null_null_null_null_null_0");
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "log_1_0xa_0xc_null_0xe_null_0",
+          "log_1_0xa_0xc_null_0xe_null_1",
+          "log_1_0xa_0xc_null_null_null_0",
+          "log_1_0xa_0xc_null_null_null_1",
+          "log_1_0xa_null_null_0xe_null_0",
+          "log_1_0xa_null_null_0xe_null_1",
+          "log_1_0xa_null_null_null_null_0",
+          "log_1_0xa_null_null_null_null_1",
+          "log_1_null_0xc_null_0xe_null_0",
+          "log_1_null_0xc_null_0xe_null_1",
+          "log_1_null_0xc_null_null_null_0",
+          "log_1_null_0xc_null_null_null_1",
+          "log_1_null_null_null_0xe_null_0",
+          "log_1_null_null_null_0xe_null_1",
+          "log_1_null_null_null_null_null_0",
+          "log_1_null_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": "0xa",
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": "0xc",
+          "topic1": null,
+          "topic2": "0xe",
+          "topic3": null,
+          "type": "log",
+        },
+      },
+      {
+        "adjacentIds": [
+          "log_1_0xa_0xd_null_0xe_null_0",
+          "log_1_0xa_0xd_null_0xe_null_1",
+          "log_1_0xa_0xd_null_null_null_0",
+          "log_1_0xa_0xd_null_null_null_1",
+          "log_1_0xa_null_null_0xe_null_0",
+          "log_1_0xa_null_null_0xe_null_1",
+          "log_1_0xa_null_null_null_null_0",
+          "log_1_0xa_null_null_null_null_1",
+          "log_1_null_0xd_null_0xe_null_0",
+          "log_1_null_0xd_null_0xe_null_1",
+          "log_1_null_0xd_null_null_null_0",
+          "log_1_null_0xd_null_null_null_1",
+          "log_1_null_null_null_0xe_null_0",
+          "log_1_null_null_null_0xe_null_1",
+          "log_1_null_null_null_null_null_0",
+          "log_1_null_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": "0xa",
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": "0xd",
+          "topic1": null,
+          "topic2": "0xe",
+          "topic3": null,
+          "type": "log",
+        },
+      },
+      {
+        "adjacentIds": [
+          "log_1_0xb_0xc_null_0xe_null_0",
+          "log_1_0xb_0xc_null_0xe_null_1",
+          "log_1_0xb_0xc_null_null_null_0",
+          "log_1_0xb_0xc_null_null_null_1",
+          "log_1_0xb_null_null_0xe_null_0",
+          "log_1_0xb_null_null_0xe_null_1",
+          "log_1_0xb_null_null_null_null_0",
+          "log_1_0xb_null_null_null_null_1",
+          "log_1_null_0xc_null_0xe_null_0",
+          "log_1_null_0xc_null_0xe_null_1",
+          "log_1_null_0xc_null_null_null_0",
+          "log_1_null_0xc_null_null_null_1",
+          "log_1_null_null_null_0xe_null_0",
+          "log_1_null_null_null_0xe_null_1",
+          "log_1_null_null_null_null_null_0",
+          "log_1_null_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": "0xb",
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": "0xc",
+          "topic1": null,
+          "topic2": "0xe",
+          "topic3": null,
+          "type": "log",
+        },
+      },
+      {
+        "adjacentIds": [
+          "log_1_0xb_0xd_null_0xe_null_0",
+          "log_1_0xb_0xd_null_0xe_null_1",
+          "log_1_0xb_0xd_null_null_null_0",
+          "log_1_0xb_0xd_null_null_null_1",
+          "log_1_0xb_null_null_0xe_null_0",
+          "log_1_0xb_null_null_0xe_null_1",
+          "log_1_0xb_null_null_null_null_0",
+          "log_1_0xb_null_null_null_null_1",
+          "log_1_null_0xd_null_0xe_null_0",
+          "log_1_null_0xd_null_0xe_null_1",
+          "log_1_null_0xd_null_null_null_0",
+          "log_1_null_0xd_null_null_null_1",
+          "log_1_null_null_null_0xe_null_0",
+          "log_1_null_null_null_0xe_null_1",
+          "log_1_null_null_null_null_null_0",
+          "log_1_null_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": "0xb",
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": "0xd",
+          "topic1": null,
+          "topic2": "0xe",
+          "topic3": null,
+          "type": "log",
+        },
+      },
+    ]
+  `);
 });
 
-test("getLogFilterFragmentIds generates 1 log filter fragment for simple filter", () => {
-  const logFilterFragments = getLogFilterFragmentIds({
+test("getFragments() log filter with transaction receipts", () => {
+  const fragments = getFragments({
     type: "log",
     chainId: 1,
-    address: "0xa",
+    address: undefined,
     topic0: null,
     topic1: null,
     topic2: null,
     topic3: null,
+    include: ["transactionReceipt.status"],
+  });
+
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "log_1_null_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": null,
+          "chainId": 1,
+          "includeTransactionReceipts": true,
+          "topic0": null,
+          "topic1": null,
+          "topic2": null,
+          "topic3": null,
+          "type": "log",
+        },
+      },
+    ]
+  `);
+});
+
+test("getFragments() trace filter", () => {
+  const fragments = getFragments({
+    type: "trace",
+    chainId: 1,
+    fromAddress: "0xa",
+    toAddress: undefined,
+    includeReverted: false,
+    functionSelector: "0xb",
+    callType: "CALL",
     include: [],
   });
 
-  expect(logFilterFragments[0]!.id).toBe("log_1_0xa_null_null_null_null_0");
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "trace_1_0xa_null_0xb_0",
+          "trace_1_0xa_null_0xb_1",
+          "trace_1_0xa_null_null_0",
+          "trace_1_0xa_null_null_1",
+          "trace_1_null_null_0xb_0",
+          "trace_1_null_null_0xb_1",
+          "trace_1_null_null_null_0",
+          "trace_1_null_null_null_1",
+        ],
+        "fragment": {
+          "chainId": 1,
+          "fromAddress": "0xa",
+          "functionSelector": "0xb",
+          "includeTransactionReceipts": false,
+          "toAddress": null,
+          "type": "trace",
+        },
+      },
+    ]
+  `);
 });
 
-test("getLogFilterFragmentIds generates 4 log filter fragment for 2x2 filter", () => {
-  const logFilterFragments = getLogFilterFragmentIds({
+test("getFragments() transfer filter", () => {
+  const fragments = getFragments({
+    type: "transfer",
+    chainId: 1,
+    fromAddress: "0xa",
+    toAddress: undefined,
+    includeReverted: false,
+    include: [],
+  });
+
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "transfer_1_0xa_null_0",
+          "transfer_1_0xa_null_1",
+          "transfer_1_null_null_0",
+          "transfer_1_null_null_1",
+        ],
+        "fragment": {
+          "chainId": 1,
+          "fromAddress": "0xa",
+          "includeTransactionReceipts": false,
+          "toAddress": null,
+          "type": "transfer",
+        },
+      },
+    ]
+  `);
+});
+
+test("getFragments() factory with topic", () => {
+  const fragments = getFragments({
     type: "log",
-    chainId: 115511,
-    address: ["0xa", "0xb"],
-    topic0: ["0xc", "0xd"],
+    chainId: 1,
+    topic0: null,
     topic1: null,
-    topic2: "0xe",
+    topic2: null,
     topic3: null,
+    address: {
+      type: "log",
+      chainId: 1,
+      address: "0xa",
+      eventSelector: "0xb",
+      childAddressLocation: "topic1",
+    },
     include: [],
   });
 
-  expect(logFilterFragments[0]!.id).toBe("log_115511_0xa_0xc_null_0xe_null_0");
-  expect(logFilterFragments[1]!.id).toBe("log_115511_0xa_0xd_null_0xe_null_0");
-  expect(logFilterFragments[2]!.id).toBe("log_115511_0xb_0xc_null_0xe_null_0");
-  expect(logFilterFragments[3]!.id).toBe("log_115511_0xb_0xd_null_0xe_null_0");
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "log_1_0xa_0xb_topic1_null_null_null_null_0",
+          "log_1_0xa_0xb_topic1_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": {
+            "address": "0xa",
+            "childAddressLocation": "topic1",
+            "eventSelector": "0xb",
+          },
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": null,
+          "topic1": null,
+          "topic2": null,
+          "topic3": null,
+          "type": "log",
+        },
+      },
+    ]
+  `);
 });
 
-test("getLogFilterFragmentIds generates 12 log filter fragment for 2x2x3 filter", () => {
-  const logFilterFragments = getLogFilterFragmentIds({
+test("getFragments() factory with offset", () => {
+  const fragments = getFragments({
     type: "log",
     chainId: 1,
-    address: ["0xa", "0xb"],
-    topic0: ["0xc", "0xd"],
+    topic0: null,
     topic1: null,
-    topic2: ["0xe", "0xf", "0x1"],
+    topic2: null,
     topic3: null,
+    address: {
+      type: "log",
+      chainId: 1,
+      address: "0xa",
+      eventSelector: "0xb",
+      childAddressLocation: "offset64",
+    },
     include: [],
   });
 
-  expect(logFilterFragments.length).toBe(12);
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "log_1_0xa_0xb_offset64_null_null_null_null_0",
+          "log_1_0xa_0xb_offset64_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": {
+            "address": "0xa",
+            "childAddressLocation": "offset64",
+            "eventSelector": "0xb",
+          },
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": null,
+          "topic1": null,
+          "topic2": null,
+          "topic3": null,
+          "type": "log",
+        },
+      },
+    ]
+  `);
 });
 
-test("getLogFilterFragmentIds includeTransactionReceipts", () => {
-  const logFilterFragments = getLogFilterFragmentIds({
+test("getFragments() multiple factories", () => {
+  const fragments = getFragments({
     type: "log",
     chainId: 1,
-    address: undefined,
     topic0: null,
     topic1: null,
     topic2: null,
     topic3: null,
-    include: ["transactionReceipt.status"],
+    address: {
+      type: "log",
+      chainId: 1,
+      address: ["0xa", "0xb"],
+      eventSelector: "0xc",
+      childAddressLocation: "topic1",
+    },
+    include: [],
   });
 
-  expect(logFilterFragments[0]!.id).toBe("log_1_null_null_null_null_null_1");
+  expect(fragments).toMatchInlineSnapshot(`
+    [
+      {
+        "adjacentIds": [
+          "log_1_0xa_0xc_topic1_null_null_null_null_0",
+          "log_1_0xa_0xc_topic1_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": {
+            "address": "0xa",
+            "childAddressLocation": "topic1",
+            "eventSelector": "0xc",
+          },
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": null,
+          "topic1": null,
+          "topic2": null,
+          "topic3": null,
+          "type": "log",
+        },
+      },
+      {
+        "adjacentIds": [
+          "log_1_0xb_0xc_topic1_null_null_null_null_0",
+          "log_1_0xb_0xc_topic1_null_null_null_null_1",
+        ],
+        "fragment": {
+          "address": {
+            "address": "0xb",
+            "childAddressLocation": "topic1",
+            "eventSelector": "0xc",
+          },
+          "chainId": 1,
+          "includeTransactionReceipts": false,
+          "topic0": null,
+          "topic1": null,
+          "topic2": null,
+          "topic3": null,
+          "type": "log",
+        },
+      },
+    ]
+  `);
 });
 
-test("getLogFilterFragmentIds builds id containing factory topic", () => {
-  const factory = buildLogFactory({
-    address: "0xa",
-    event: llamaFactoryEventAbiItem,
-    parameter: "deployer",
+test("recoverFilter() block filter", () => {
+  const filter = {
+    type: "block",
     chainId: 1,
-  });
+    interval: 100,
+    offset: 5,
+    include: [],
+  } satisfies FilterWithoutBlocks;
 
-  const fragments = getLogFilterFragmentIds({
+  const fragments = getFragments(filter);
+
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
+  );
+
+  expect(recovered).toStrictEqual(filter);
+});
+
+test("recoverFilter() transaction filter", () => {
+  const filter = {
+    type: "transaction",
+    chainId: 1,
+    fromAddress: "0xa",
+    toAddress: "0xb",
+    includeReverted: false,
+    include: [],
+  } satisfies FilterWithoutBlocks;
+
+  const fragments = getFragments(filter);
+
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
+  );
+
+  expect(recovered).toStrictEqual(filter);
+});
+
+test("recoverFilter() log filter", () => {
+  const filter = {
     type: "log",
     chainId: 1,
-    topic0: null,
+    address: ["0xa", "0xb"],
+    topic0: ["0xc", "0xd"],
     topic1: null,
-    topic2: null,
+    topic2: "0xe",
     topic3: null,
-    address: factory,
     include: [],
-  });
+  } satisfies FilterWithoutBlocks;
 
-  expect(fragments).toHaveLength(1);
+  const fragments = getFragments(filter);
 
-  expect(fragments[0]!.id).toBe(
-    "log_1_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_topic1_null_null_null_null_0",
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
   );
+
+  expect(recovered).toStrictEqual(filter);
 });
 
-test("getLogFilterFragmentIds builds id containing factory offset", () => {
-  const factory = buildLogFactory({
-    address: "0xa",
-    event: llamaFactoryEventAbiItem,
-    parameter: "llamaPolicy",
+test("recoverFilter() trace filter", () => {
+  const filter = {
+    type: "trace",
     chainId: 1,
-  });
+    fromAddress: "0xa",
+    toAddress: undefined,
+    includeReverted: false,
+    functionSelector: "0xb",
+    callType: "CALL",
+    include: [],
+  } satisfies FilterWithoutBlocks;
+
+  const fragments = getFragments(filter);
 
-  const fragments = getLogFilterFragmentIds({
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
+  );
+
+  expect(recovered).toStrictEqual(filter);
+});
+
+test("recoverFilter() transfer filter", () => {
+  const filter = {
+    type: "transfer",
+    chainId: 1,
+    fromAddress: "0xa",
+    toAddress: undefined,
+    includeReverted: false,
+    include: [],
+  } satisfies FilterWithoutBlocks;
+
+  const fragments = getFragments(filter);
+
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
+  );
+
+  expect(recovered).toStrictEqual(filter);
+});
+
+test("recoverFilter() factory", () => {
+  const filter = {
     type: "log",
-    chainId: 115511,
+    chainId: 1,
     topic0: null,
     topic1: null,
     topic2: null,
     topic3: null,
-    address: factory,
+    address: {
+      type: "log",
+      chainId: 1,
+      address: "0xa",
+      eventSelector: "0xb",
+      childAddressLocation: "topic1",
+    },
     include: [],
-  });
+  } satisfies FilterWithoutBlocks;
 
-  expect(fragments).toHaveLength(1);
+  const fragments = getFragments(filter);
 
-  expect(fragments[0]!.id).toBe(
-    "log_115511_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_offset64_null_null_null_null_0",
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
   );
-});
 
-test("getLogFilterFragmentIds builds id with multiple factories", () => {
-  const factory = buildLogFactory({
-    address: ["0xa", "0xb"],
-    event: llamaFactoryEventAbiItem,
-    parameter: "llamaPolicy",
-    chainId: 1,
-  });
+  expect(recovered).toStrictEqual(filter);
+});
 
-  const fragments = getLogFilterFragmentIds({
+test("recoverFilter() multiple factories", () => {
+  const filter = {
     type: "log",
     chainId: 1,
     topic0: null,
     topic1: null,
     topic2: null,
     topic3: null,
-    address: factory,
+    address: {
+      type: "log",
+      chainId: 1,
+      address: ["0xa", "0xb"],
+      eventSelector: "0xc",
+      childAddressLocation: "topic1",
+    },
     include: [],
-  });
+  } satisfies FilterWithoutBlocks;
 
-  expect(fragments).toHaveLength(2);
+  const fragments = getFragments(filter);
 
-  expect(fragments[0]!.id).toBe(
-    "log_1_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_offset64_null_null_null_null_0",
-  );
-  expect(fragments[1]!.id).toBe(
-    "log_1_0xb_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_offset64_null_null_null_null_0",
+  const recovered = recoverFilter(
+    filter,
+    fragments.map((f) => f.fragment),
   );
+
+  expect(recovered).toStrictEqual(filter);
 });