Repository: ponder-sh/ponder
Pull request #823 (2024-04-22 18:06:37)

Receipts


Patch:
diff --git a/.changeset/tricky-lies-attack.md b/.changeset/tricky-lies-attack.md
new file mode 100644
index 00000000..1d3e8b7d
--- /dev/null
+++ b/.changeset/tricky-lies-attack.md
@@ -0,0 +1,5 @@
+---
+"@ponder/core": patch
+---
+
+Added support for including transaction receipts (`event.transactionReceipt`). To enable transaction receipts for all events on a contract, set `includeTransactionReceipts: true` on the contract config. Receipts can be specified/overriden on a per-network basis. Note that including receipts may slow down the historical sync due to additional `eth_getTransactionReceipt` RPC requests.
diff --git a/benchmarks/src/ponder.ts b/benchmarks/src/ponder.ts
index 67518afe..2473610b 100644
--- a/benchmarks/src/ponder.ts
+++ b/benchmarks/src/ponder.ts
@@ -9,7 +9,7 @@ import { fetchWithTimeout, startClock } from "./utils";
 const fetchPonderMetrics = async () => {
   try {
     const metricsResponse = await fetchWithTimeout(
-      "http://localhost:42069/metrics",
+      "http://0.0.0.0:42069/metrics",
     );
     const metricsRaw = await metricsResponse.text();
     return metricsRaw.split("\n");
@@ -19,9 +19,7 @@ const fetchPonderMetrics = async () => {
 };
 
 const fetchHealth = async () => {
-  const healthResponse = await fetchWithTimeout(
-    "http://localhost:42069/health",
-  );
+  const healthResponse = await fetchWithTimeout("http://0.0.0.0:42069/health");
 
   if (healthResponse.status === 200) return true;
   return false;
diff --git a/packages/core/src/build/configAndIndexingFunctions.ts b/packages/core/src/build/configAndIndexingFunctions.ts
index 070b692a..01ccaead 100644
--- a/packages/core/src/build/configAndIndexingFunctions.ts
+++ b/packages/core/src/build/configAndIndexingFunctions.ts
@@ -328,6 +328,9 @@ export async function buildConfigAndIndexingFunctions({
           factory: "factory" in contract ? contract.factory : undefined,
           filter: contract.filter,
 
+          includeTransactionReceipts:
+            contract.includeTransactionReceipts ?? false,
+
           startBlock,
           endBlock,
           maxBlockRange: contract.maxBlockRange,
@@ -366,6 +369,11 @@ export async function buildConfigAndIndexingFunctions({
               ("factory" in contract ? contract.factory : undefined),
             filter: overrides.filter ?? contract.filter,
 
+            includeTransactionReceipts:
+              overrides.includeTransactionReceipts ??
+              contract.includeTransactionReceipts ??
+              false,
+
             startBlock,
             endBlock,
             maxBlockRange: overrides.maxBlockRange ?? contract.maxBlockRange,
@@ -506,12 +514,18 @@ export async function buildConfigAndIndexingFunctions({
 
       if (resolvedFactory) {
         // Note that this can throw.
-        const factoryCriteria = buildFactoryCriteria(resolvedFactory);
+        const factoryCriteria = buildFactoryCriteria({
+          ...resolvedFactory,
+          includeTransactionReceipts: rawContract.includeTransactionReceipts,
+        });
 
         return {
           ...baseContract,
           type: "factory",
-          criteria: { ...factoryCriteria, topics },
+          criteria: {
+            ...factoryCriteria,
+            topics,
+          },
         } satisfies FactorySource;
       }
 
@@ -545,6 +559,7 @@ export async function buildConfigAndIndexingFunctions({
         criteria: {
           address: validatedAddress,
           topics,
+          includeTransactionReceipts: rawContract.includeTransactionReceipts,
         },
       } satisfies LogSource;
     })
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 53fbd609..0471f33e 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -68,6 +68,10 @@ type AbiConfig<abi extends Abi | readonly unknown[]> = {
   abi: abi;
 };
 
+type TransactionReceiptConfig = {
+  includeTransactionReceipts?: boolean;
+};
+
 type GetNetwork<
   networks,
   contract,
@@ -89,6 +93,7 @@ type GetNetwork<
             [name in allNetworkNames]?: Prettify<
               GetAddress<NonStrictPick<network, "factory" | "address">> &
                 GetEventFilter<abi, NonStrictPick<contract, "filter">> &
+                TransactionReceiptConfig &
                 BlockConfig
             >;
           };
@@ -103,7 +108,10 @@ type GetNetwork<
         | allNetworkNames
         | {
             [name in allNetworkNames]?: Prettify<
-              GetAddress<unknown> & GetEventFilter<abi, unknown> & BlockConfig
+              GetAddress<unknown> &
+                GetEventFilter<abi, unknown> &
+                TransactionReceiptConfig &
+                BlockConfig
             >;
           };
     };
@@ -113,6 +121,7 @@ type ContractConfig<networks, contract, abi extends Abi> = Prettify<
     GetNetwork<networks, NonStrictPick<contract, "network">, abi> &
     GetAddress<NonStrictPick<contract, "factory" | "address">> &
     GetEventFilter<abi, NonStrictPick<contract, "filter">> &
+    TransactionReceiptConfig &
     BlockConfig
 >;
 
diff --git a/packages/core/src/config/factories.ts b/packages/core/src/config/factories.ts
index 7154e0b2..06c1dd42 100644
--- a/packages/core/src/config/factories.ts
+++ b/packages/core/src/config/factories.ts
@@ -8,10 +8,12 @@ export function buildFactoryCriteria({
   address: _address,
   event,
   parameter,
+  includeTransactionReceipts,
 }: {
   address: `0x${string}`;
   event: AbiEvent;
   parameter: string;
+  includeTransactionReceipts: boolean;
 }) {
   const address = toLowerCase(_address);
   const eventSelector = getEventSelector(event);
@@ -27,6 +29,7 @@ export function buildFactoryCriteria({
       eventSelector,
       // Add 1 because inputs will not contain an element for topic0 (the signature).
       childAddressLocation: `topic${(indexedInputPosition + 1) as 1 | 2 | 3}`,
+      includeTransactionReceipts,
     } satisfies Omit<FactoryCriteria, "topics">;
   }
 
@@ -54,5 +57,6 @@ export function buildFactoryCriteria({
     address,
     eventSelector,
     childAddressLocation: `offset${offset}`,
+    includeTransactionReceipts,
   } satisfies Omit<FactoryCriteria, "topics">;
 }
diff --git a/packages/core/src/config/sources.ts b/packages/core/src/config/sources.ts
index d4f8f86e..dacffe72 100644
--- a/packages/core/src/config/sources.ts
+++ b/packages/core/src/config/sources.ts
@@ -4,6 +4,7 @@ import type { AbiEvents } from "./abi.js";
 export type LogFilterCriteria = {
   address?: Address | Address[];
   topics: LogTopic[];
+  includeTransactionReceipts: boolean;
 };
 
 export type FactoryCriteria = {
@@ -11,6 +12,7 @@ export type FactoryCriteria = {
   eventSelector: Hex;
   childAddressLocation: "topic1" | "topic2" | "topic3" | `offset${number}`;
   topics: LogTopic[];
+  includeTransactionReceipts: boolean;
 };
 
 type BaseSource = {
diff --git a/packages/core/src/indexing/service.ts b/packages/core/src/indexing/service.ts
index 4bd650a1..faf6a3b2 100644
--- a/packages/core/src/indexing/service.ts
+++ b/packages/core/src/indexing/service.ts
@@ -492,6 +492,7 @@ const executeLog = async (
           log: event.event.log,
           block: event.event.block,
           transaction: event.event.transaction,
+          transactionReceipt: event.event.transactionReceipt,
         },
         context: currentEvent.context,
       });
diff --git a/packages/core/src/sync-historical/service.ts b/packages/core/src/sync-historical/service.ts
index 0bf515bf..b67e8f38 100644
--- a/packages/core/src/sync-historical/service.ts
+++ b/packages/core/src/sync-historical/service.ts
@@ -38,6 +38,9 @@ import {
   type RpcBlock,
   RpcError,
   type RpcLog,
+  type RpcTransaction,
+  type RpcTransactionReceipt,
+  TransactionReceiptNotFoundError,
   hexToBigInt,
   hexToNumber,
   numberToHex,
@@ -94,12 +97,12 @@ type HistoricalSyncTask =
   | BlockTask;
 
 type HistoricalBlock = RpcBlock<"finalized", true>;
+type HistoricalTransaction = RpcTransaction<false>;
 
 type LogInterval = {
   startBlock: number;
   endBlock: number;
   logs: RpcLog[];
-  transactionHashes: Set<Hash>;
 };
 
 export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
@@ -209,10 +212,7 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
           const completedLogFilterIntervals =
             await this.syncStore.getLogFilterIntervals({
               chainId: source.chainId,
-              logFilter: {
-                address: source.criteria.address,
-                topics: source.criteria.topics,
-              },
+              logFilter: source.criteria,
             });
           const logFilterProgressTracker = new ProgressTracker({
             target: [startBlock, endBlock],
@@ -301,6 +301,7 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
               logFilter: {
                 address: source.criteria.address,
                 topics: [source.criteria.eventSelector],
+                includeTransactionReceipts: false,
               },
             });
           const factoryChildAddressProgressTracker = new ProgressTracker({
@@ -628,11 +629,26 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
         this.blockCallbacks[endBlock] = [];
 
       this.blockCallbacks[endBlock].push(async (block) => {
+        const { transactionHashes } = logInterval;
+        const transactions = block.transactions.filter((tx) =>
+          transactionHashes.has(tx.hash),
+        );
+        const transactionReceipts =
+          logFilter.criteria.includeTransactionReceipts === true
+            ? await Promise.all(
+                transactions.map((tx) =>
+                  this._eth_getTransactionReceipt({ hash: tx.hash }),
+                ),
+              )
+            : [];
+
         await this._insertLogFilterInterval({
           logInterval,
           logFilter: logFilter.criteria,
           chainId: logFilter.chainId,
           block,
+          transactions,
+          transactionReceipts,
         });
 
         this.common.metrics.ponder_historical_completed_blocks.inc(
@@ -687,13 +703,24 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
         this.blockCallbacks[endBlock] = [];
 
       this.blockCallbacks[endBlock].push(async (block) => {
+        const transactions = block.transactions.filter((tx) =>
+          transactionHashes.has(tx.hash),
+        );
+        const transactionReceipts =
+          factory.criteria.includeTransactionReceipts === true
+            ? await Promise.all(
+                transactions.map((tx) =>
+                  this._eth_getTransactionReceipt({ hash: tx.hash }),
+                ),
+              )
+            : [];
+
         await this.syncStore.insertFactoryLogFilterInterval({
           chainId: factory.chainId,
           factory: factory.criteria,
           block,
-          transactions: block.transactions.filter((tx) =>
-            transactionHashes.has(tx.hash),
-          ),
+          transactions,
+          transactionReceipts,
           logs,
           interval: {
             startBlock: BigInt(startBlock),
@@ -748,14 +775,23 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
         this.blockCallbacks[logInterval.endBlock] = [];
 
       this.blockCallbacks[logInterval.endBlock].push(async (block) => {
+        const { transactionHashes } = logInterval;
+
+        const transactions = block.transactions.filter((tx) =>
+          transactionHashes.has(tx.hash),
+        );
+
         await this._insertLogFilterInterval({
           logInterval,
           logFilter: {
             address: factory.criteria.address,
             topics: [factory.criteria.eventSelector],
+            includeTransactionReceipts: false,
           },
           chainId: factory.chainId,
           block,
+          transactions,
+          transactionReceipts: [],
         });
       });
     }
@@ -805,9 +841,8 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
       blockNumber,
     });
 
-    while (callbacks.length !== 0) {
-      await callbacks[callbacks.length - 1](block);
-      callbacks.pop()!;
+    for (const callback of callbacks) {
+      await callback(block);
     }
 
     const newBlockCheckpoint = this.blockProgressTracker.addCompletedBlock({
@@ -1010,27 +1045,51 @@ export class HistoricalSyncService extends Emittery<HistoricalSyncEvents> {
         return block as HistoricalBlock;
       });
 
+  /**
+   * Helper function for "eth_getTransactionReceipt" request.
+   */
+  private _eth_getTransactionReceipt = ({
+    hash,
+  }: {
+    hash: Hex;
+  }): Promise<RpcTransactionReceipt> =>
+    this.requestQueue
+      .request({
+        method: "eth_getTransactionReceipt",
+        params: [hash],
+      })
+      .then((receipt) => {
+        if (!receipt)
+          throw new TransactionReceiptNotFoundError({
+            hash,
+          });
+        return receipt as RpcTransactionReceipt;
+      });
+
   /**
    * Helper function for "insertLogFilterInterval"
    */
   private _insertLogFilterInterval = ({
-    logInterval: { transactionHashes, logs, startBlock, endBlock },
+    logInterval: { logs, startBlock, endBlock },
     logFilter,
     block,
+    transactions,
+    transactionReceipts,
     chainId,
   }: {
     logInterval: LogInterval;
     logFilter: LogFilterCriteria;
     block: HistoricalBlock;
+    transactions: HistoricalTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     chainId: number;
   }) =>
     this.syncStore.insertLogFilterInterval({
       chainId,
       logFilter,
       block,
-      transactions: block.transactions.filter((tx) =>
-        transactionHashes.has(tx.hash),
-      ),
+      transactions,
+      transactionReceipts,
       logs,
       interval: {
         startBlock: BigInt(startBlock),
diff --git a/packages/core/src/sync-realtime/service.ts b/packages/core/src/sync-realtime/service.ts
index 857c5a2c..3b5f2221 100644
--- a/packages/core/src/sync-realtime/service.ts
+++ b/packages/core/src/sync-realtime/service.ts
@@ -14,6 +14,7 @@ import {
   _eth_getBlockByHash,
   _eth_getBlockByNumber,
   _eth_getLogs,
+  _eth_getTransactionReceipt,
 } from "@/sync/index.js";
 import { type Checkpoint, maxCheckpoint } from "@/utils/checkpoint.js";
 import { range } from "@/utils/range.js";
@@ -59,6 +60,7 @@ export type Service = {
   /** List of all possible event selectors based on the provided `sources`. */
   eventSelectors: Hex[];
   hasFactorySource: boolean;
+  hasTransactionReceiptSource: boolean;
   logFilterSources: LogSource[];
   factorySources: FactorySource[];
 };
@@ -129,6 +131,9 @@ export const create = ({
     onFatalError,
     eventSelectors,
     hasFactorySource: sources.some(sourceIsFactory),
+    hasTransactionReceiptSource: sources.some(
+      (s) => s.criteria.includeTransactionReceipts,
+    ),
     logFilterSources: sources.filter(sourceIsLog),
     factorySources: sources.filter(sourceIsFactory),
   };
@@ -339,16 +344,26 @@ export const handleBlock = async (
 
   // Add pending event data to sync store and local event data. Ordering is
   // important because the sync store operation may throw an error, causing a retry.
+
   const transactionHashes = new Set(newLogs.map((l) => l.transactionHash));
   const transactions = newHeadBlock.transactions.filter((t) =>
     transactionHashes.has(t.hash),
   );
 
+  const newTransactionReceipts = service.hasTransactionReceiptSource
+    ? await Promise.all(
+        transactions.map(({ hash }) =>
+          _eth_getTransactionReceipt(service, { hash }),
+        ),
+      )
+    : [];
+
   if (newLogs.length > 0) {
     await service.syncStore.insertRealtimeBlock({
       chainId: service.network.chainId,
       block: newHeadBlock,
       transactions,
+      transactionReceipts: newTransactionReceipts,
       logs: newLogs,
     });
 
diff --git a/packages/core/src/sync-store/postgres/encoding.ts b/packages/core/src/sync-store/postgres/encoding.ts
index 17058924..3e45b87d 100644
--- a/packages/core/src/sync-store/postgres/encoding.ts
+++ b/packages/core/src/sync-store/postgres/encoding.ts
@@ -1,5 +1,5 @@
 import type { Generated, Insertable } from "kysely";
-import type { Address, Hash, Hex } from "viem";
+import type { Address, Hash, Hex, RpcTransactionReceipt } from "viem";
 import {
   type RpcBlock,
   type RpcLog,
@@ -120,6 +120,51 @@ export function rpcToPostgresTransaction(
   };
 }
 
+type TransactionReceiptsTable = {
+  blockHash: Hash;
+  blockNumber: bigint;
+  contractAddress: Address | null;
+  cumulativeGasUsed: bigint;
+  effectiveGasPrice: bigint;
+  from: Address;
+  gasUsed: bigint;
+  logs: string;
+  logsBloom: Hex;
+  status: Hex;
+  to: Address | null;
+  transactionHash: Hash;
+  transactionIndex: number;
+  type: Hex;
+
+  chainId: number;
+};
+
+export type InsertableTransactionReceipts =
+  Insertable<TransactionReceiptsTable>;
+
+export function rpcToPostgresTransactionReceipt(
+  transactionReceipt: RpcTransactionReceipt,
+): Omit<TransactionReceiptsTable, "chainId"> {
+  return {
+    blockHash: transactionReceipt.blockHash,
+    blockNumber: BigInt(transactionReceipt.blockNumber),
+    contractAddress: transactionReceipt.contractAddress
+      ? toLowerCase(transactionReceipt.contractAddress)
+      : null,
+    cumulativeGasUsed: BigInt(transactionReceipt.cumulativeGasUsed),
+    effectiveGasPrice: BigInt(transactionReceipt.effectiveGasPrice),
+    from: toLowerCase(transactionReceipt.from),
+    gasUsed: BigInt(transactionReceipt.gasUsed),
+    logs: JSON.stringify(transactionReceipt.logs),
+    logsBloom: transactionReceipt.logsBloom,
+    status: transactionReceipt.status,
+    to: transactionReceipt.to ? toLowerCase(transactionReceipt.to) : null,
+    transactionHash: transactionReceipt.transactionHash,
+    transactionIndex: Number(transactionReceipt.transactionIndex),
+    type: transactionReceipt.type as Hex,
+  };
+}
+
 type LogsTable = {
   id: string;
   address: Address;
@@ -173,6 +218,7 @@ type LogFiltersTable = {
   topic1: Hex | null;
   topic2: Hex | null;
   topic3: Hex | null;
+  includeTransactionReceipts: 0 | 1;
 };
 
 type LogFilterIntervalsTable = {
@@ -192,6 +238,7 @@ type FactoriesTable = {
   topic1: Hex | null;
   topic2: Hex | null;
   topic3: Hex | null;
+  includeTransactionReceipts: 0 | 1;
 };
 
 type FactoryLogFilterIntervalsTable = {
@@ -204,6 +251,7 @@ type FactoryLogFilterIntervalsTable = {
 export type SyncStoreTables = {
   blocks: BlocksTable;
   transactions: TransactionsTable;
+  transactionReceipts: TransactionReceiptsTable;
   logs: LogsTable;
   rpcRequestResults: RpcRequestResultsTable;
 
diff --git a/packages/core/src/sync-store/postgres/migrations.ts b/packages/core/src/sync-store/postgres/migrations.ts
index c2e68585..1fe511ce 100644
--- a/packages/core/src/sync-store/postgres/migrations.ts
+++ b/packages/core/src/sync-store/postgres/migrations.ts
@@ -536,6 +536,86 @@ const migrations: Record<string, Migration> = {
         .execute();
     },
   },
+  "2024_04_22_0_transaction_receipts": {
+    async up(db: Kysely<any>) {
+      // Update the log filter ID keys to include the integer includeTransactionReceipts value.
+      // Note that we have to remove the FK constraint, which is fine given our app logic.
+      await db.schema
+        .alterTable("logFilterIntervals")
+        .dropConstraint("logFilterIntervals_logFilterId_fkey")
+        .execute();
+      await db
+        .updateTable("logFilters")
+        .set({ id: sql`"id" || '_0'` })
+        .execute();
+      await db
+        .updateTable("logFilterIntervals")
+        .set({ logFilterId: sql`"logFilterId" || '_0'` })
+        .execute();
+      // Add the includeTransactionReceipts column. By setting a default in the ADD COLUMN statement,
+      // Postgres will automatically populate all existing rows with the default value. But, we don't
+      // actually want a default (want to require a value on insertion), so immediately drop the default.
+      await db.schema
+        .alterTable("logFilters")
+        .addColumn("includeTransactionReceipts", "integer", (col) =>
+          col.notNull().defaultTo(0),
+        )
+        .execute();
+      await db.schema
+        .alterTable("logFilters")
+        .alterColumn("includeTransactionReceipts", (col) => col.dropDefault())
+        .execute();
+
+      // Repeat the same 2 steps for the factory tables.
+      await db.schema
+        .alterTable("factoryLogFilterIntervals")
+        .dropConstraint("factoryLogFilterIntervals_factoryId_fkey")
+        .execute();
+      await db
+        .updateTable("factories")
+        .set({ id: sql`"id" || '_0'` })
+        .execute();
+      await db
+        .updateTable("factoryLogFilterIntervals")
+        .set({ factoryId: sql`"factoryId" || '_0'` })
+        .execute();
+      await db.schema
+        .alterTable("factories")
+        .addColumn("includeTransactionReceipts", "integer", (col) =>
+          col.notNull().defaultTo(0),
+        )
+        .execute();
+      await db.schema
+        .alterTable("factories")
+        .alterColumn("includeTransactionReceipts", (col) => col.dropDefault())
+        .execute();
+
+      await db.schema
+        .createTable("transactionReceipts")
+        .addColumn("blockHash", "varchar(66)", (col) => col.notNull())
+        .addColumn("blockNumber", "numeric(78, 0)", (col) => col.notNull())
+        .addColumn("chainId", "integer", (col) => col.notNull())
+        .addColumn("contractAddress", "varchar(66)")
+        .addColumn("cumulativeGasUsed", "numeric(78, 0)", (col) =>
+          col.notNull(),
+        )
+        .addColumn("effectiveGasPrice", "numeric(78, 0)", (col) =>
+          col.notNull(),
+        )
+        .addColumn("from", "varchar(42)", (col) => col.notNull())
+        .addColumn("gasUsed", "numeric(78, 0)", (col) => col.notNull())
+        .addColumn("logs", "text", (col) => col.notNull())
+        .addColumn("logsBloom", "varchar(514)", (col) => col.notNull())
+        .addColumn("status", "text", (col) => col.notNull())
+        .addColumn("to", "varchar(42)")
+        .addColumn("transactionHash", "varchar(66)", (col) =>
+          col.notNull().primaryKey(),
+        )
+        .addColumn("transactionIndex", "integer", (col) => col.notNull())
+        .addColumn("type", "text", (col) => col.notNull())
+        .execute();
+    },
+  },
 };
 
 class StaticMigrationProvider implements MigrationProvider {
diff --git a/packages/core/src/sync-store/postgres/store.ts b/packages/core/src/sync-store/postgres/store.ts
index b2429c5e..4c4aa392 100644
--- a/packages/core/src/sync-store/postgres/store.ts
+++ b/packages/core/src/sync-store/postgres/store.ts
@@ -8,7 +8,8 @@ import {
   sourceIsLog,
 } from "@/config/sources.js";
 import type { HeadlessKysely } from "@/database/kysely.js";
-import type { Block, Log, Transaction } from "@/types/eth.js";
+import type { SyncLog } from "@/sync/index.js";
+import type { Log, TransactionReceipt } from "@/types/eth.js";
 import type { NonNull } from "@/types/utils.js";
 import {
   type Checkpoint,
@@ -32,15 +33,18 @@ import {
   type RpcBlock,
   type RpcLog,
   type RpcTransaction,
+  type RpcTransactionReceipt,
   checksumAddress,
+  hexToBigInt,
   hexToNumber,
 } from "viem";
-import type { SyncStore } from "../store.js";
+import type { RawEvent, SyncStore } from "../store.js";
 import {
   type SyncStoreTables,
   rpcToPostgresBlock,
   rpcToPostgresLog,
   rpcToPostgresTransaction,
+  rpcToPostgresTransactionReceipt,
 } from "./encoding.js";
 
 export class PostgresSyncStore implements SyncStore {
@@ -56,6 +60,7 @@ export class PostgresSyncStore implements SyncStore {
     logFilter,
     block: rpcBlock,
     transactions: rpcTransactions,
+    transactionReceipts: rpcTransactionReceipts,
     logs: rpcLogs,
     interval,
   }: {
@@ -63,6 +68,7 @@ export class PostgresSyncStore implements SyncStore {
     logFilter: LogFilterCriteria;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
     interval: { startBlock: bigint; endBlock: bigint };
   }) => {
@@ -86,6 +92,20 @@ export class PostgresSyncStore implements SyncStore {
             .execute();
         }
 
+        if (rpcTransactionReceipts.length > 0) {
+          const transactionReceipts = rpcTransactionReceipts.map(
+            (rpcTransactionReceipt) => ({
+              ...rpcToPostgresTransactionReceipt(rpcTransactionReceipt),
+              chainId,
+            }),
+          );
+          await tx
+            .insertInto("transactionReceipts")
+            .values(transactionReceipts)
+            .onConflict((oc) => oc.column("transactionHash").doNothing())
+            .execute();
+        }
+
         if (rpcLogs.length > 0) {
           const logs = rpcLogs.map((rpcLog) => ({
             ...rpcToPostgresLog(rpcLog),
@@ -169,7 +189,7 @@ export class PostgresSyncStore implements SyncStore {
 
       const intervals = await this.db
         .with(
-          "logFilterFragments(fragmentId, fragmentAddress, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3)",
+          "logFilterFragments(fragmentId, fragmentAddress, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3, fragmentIncludeTransactionReceipts)",
           () =>
             sql`( values ${sql.join(
               fragments.map(
@@ -178,25 +198,32 @@ export class PostgresSyncStore implements SyncStore {
                     f.topic0,
                   )}, ${sql.val(f.topic1)}, ${sql.val(f.topic2)}, ${sql.val(
                     f.topic3,
-                  )} )`,
+                  )}, ${sql.lit(f.includeTransactionReceipts)} )`,
               ),
             )} )`,
         )
         .selectFrom("logFilterIntervals")
-        .leftJoin("logFilters", "logFilterId", "logFilters.id")
+        .innerJoin("logFilters", "logFilterId", "logFilters.id")
         .innerJoin("logFilterFragments", (join) => {
-          let baseJoin = join.on(({ or, cmpr }) =>
-            or([
-              cmpr("address", "is", null),
-              cmpr("fragmentAddress", "=", sql.ref("address")),
+          let baseJoin = join.on((eb) =>
+            eb.or([
+              eb("address", "is", null),
+              eb("fragmentAddress", "=", sql.ref("address")),
             ]),
           );
+          baseJoin = baseJoin.on((eb) =>
+            eb(
+              "fragmentIncludeTransactionReceipts",
+              "<=",
+              sql.ref("includeTransactionReceipts"),
+            ),
+          );
           for (const idx_ of range(0, 4)) {
-            baseJoin = baseJoin.on(({ or, cmpr }) => {
+            baseJoin = baseJoin.on((eb) => {
               const idx = idx_ as 0 | 1 | 2 | 3;
-              return or([
-                cmpr(`topic${idx}`, "is", null),
-                cmpr(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
+              return eb.or([
+                eb(`topic${idx}`, "is", null),
+                eb(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
               ]);
             });
           }
@@ -303,6 +330,7 @@ export class PostgresSyncStore implements SyncStore {
     factory,
     block: rpcBlock,
     transactions: rpcTransactions,
+    transactionReceipts: rpcTransactionReceipts,
     logs: rpcLogs,
     interval,
   }: {
@@ -310,6 +338,7 @@ export class PostgresSyncStore implements SyncStore {
     factory: FactoryCriteria;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
     interval: { startBlock: bigint; endBlock: bigint };
   }) => {
@@ -335,6 +364,20 @@ export class PostgresSyncStore implements SyncStore {
               .execute();
           }
 
+          if (rpcTransactionReceipts.length > 0) {
+            const transactionReceipts = rpcTransactionReceipts.map(
+              (rpcTransactionReceipt) => ({
+                ...rpcToPostgresTransactionReceipt(rpcTransactionReceipt),
+                chainId,
+              }),
+            );
+            await tx
+              .insertInto("transactionReceipts")
+              .values(transactionReceipts)
+              .onConflict((oc) => oc.column("transactionHash").doNothing())
+              .execute();
+          }
+
           if (rpcLogs.length > 0) {
             const logs = rpcLogs.map((rpcLog) => ({
               ...rpcToPostgresLog(rpcLog),
@@ -424,7 +467,7 @@ export class PostgresSyncStore implements SyncStore {
 
         const intervals = await this.db
           .with(
-            "factoryFilterFragments(fragmentId, fragmentAddress, fragmentEventSelector, fragmentChildAddressLocation, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3)",
+            "factoryFilterFragments(fragmentId, fragmentAddress, fragmentEventSelector, fragmentChildAddressLocation, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3, fragmentIncludeTransactionReceipts)",
             () =>
               sql`( values ${sql.join(
                 fragments.map(
@@ -435,30 +478,37 @@ export class PostgresSyncStore implements SyncStore {
                       f.topic0,
                     )}, ${sql.val(f.topic1)}, ${sql.val(f.topic2)}, ${sql.val(
                       f.topic3,
-                    )} )`,
+                    )}, ${sql.lit(f.includeTransactionReceipts)} )`,
                 ),
               )} )`,
           )
           .selectFrom("factoryLogFilterIntervals")
-          .leftJoin("factories", "factoryId", "factories.id")
+          .innerJoin("factories", "factoryId", "factories.id")
           .innerJoin("factoryFilterFragments", (join) => {
-            let baseJoin = join.on(({ and, cmpr }) =>
-              and([
-                cmpr("fragmentAddress", "=", sql.ref("address")),
-                cmpr("fragmentEventSelector", "=", sql.ref("eventSelector")),
-                cmpr(
+            let baseJoin = join.on((eb) =>
+              eb.and([
+                eb("fragmentAddress", "=", sql.ref("address")),
+                eb("fragmentEventSelector", "=", sql.ref("eventSelector")),
+                eb(
                   "fragmentChildAddressLocation",
                   "=",
                   sql.ref("childAddressLocation"),
                 ),
               ]),
             );
+            baseJoin = baseJoin.on((eb) =>
+              eb(
+                "fragmentIncludeTransactionReceipts",
+                "<=",
+                sql.ref("includeTransactionReceipts"),
+              ),
+            );
             for (const idx_ of range(0, 4)) {
-              baseJoin = baseJoin.on(({ or, cmpr }) => {
+              baseJoin = baseJoin.on((eb) => {
                 const idx = idx_ as 0 | 1 | 2 | 3;
-                return or([
-                  cmpr(`topic${idx}`, "is", null),
-                  cmpr(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
+                return eb.or([
+                  eb(`topic${idx}`, "is", null),
+                  eb(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
                 ]);
               });
             }
@@ -513,11 +563,13 @@ export class PostgresSyncStore implements SyncStore {
     chainId,
     block: rpcBlock,
     transactions: rpcTransactions,
+    transactionReceipts: rpcTransactionReceipts,
     logs: rpcLogs,
   }: {
     chainId: number;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
   }) => {
     return this.db.wrap({ method: "insertRealtimeBlock" }, async () => {
@@ -546,6 +598,20 @@ export class PostgresSyncStore implements SyncStore {
             .execute();
         }
 
+        if (rpcTransactionReceipts.length > 0) {
+          const transactionReceipts = rpcTransactionReceipts.map(
+            (rpcTransactionReceipt) => ({
+              ...rpcToPostgresTransactionReceipt(rpcTransactionReceipt),
+              chainId,
+            }),
+          );
+          await tx
+            .insertInto("transactionReceipts")
+            .values(transactionReceipts)
+            .onConflict((oc) => oc.column("transactionHash").doNothing())
+            .execute();
+        }
+
         if (rpcLogs.length > 0) {
           const logs = rpcLogs.map((rpcLog) => ({
             ...rpcToPostgresLog(rpcLog),
@@ -587,6 +653,7 @@ export class PostgresSyncStore implements SyncStore {
             ...factories.map((f) => ({
               address: f.address,
               topics: [f.eventSelector],
+              includeTransactionReceipts: f.includeTransactionReceipts,
             })),
           ],
           interval,
@@ -752,6 +819,16 @@ export class PostgresSyncStore implements SyncStore {
     let cursor = encodeCheckpoint(fromCheckpoint);
     const encodedToCheckpoint = encodeCheckpoint(toCheckpoint);
 
+    const shouldJoinReceipts = sources.some(
+      (source) => source.criteria.includeTransactionReceipts,
+    );
+    const sourcesById = sources.reduce<{
+      [sourceId: EventSource["id"]]: (typeof sources)[number];
+    }>((acc, cur) => {
+      acc[cur.id] = cur;
+      return acc;
+    }, {});
+
     while (true) {
       const events = await this.db.wrap(
         { method: "getLogEvents" },
@@ -792,6 +869,30 @@ export class PostgresSyncStore implements SyncStore {
 
               return eb.or([...logFilterCmprs, ...factoryCmprs]);
             })
+            .$if(shouldJoinReceipts, (qb) =>
+              qb
+                .innerJoin(
+                  "transactionReceipts",
+                  "transactionReceipts.transactionHash",
+                  "logs.transactionHash",
+                )
+                .select([
+                  "transactionReceipts.blockHash as txr_blockHash",
+                  "transactionReceipts.blockNumber as txr_blockNumber",
+                  "transactionReceipts.contractAddress as txr_contractAddress",
+                  "transactionReceipts.cumulativeGasUsed as txr_cumulativeGasUsed",
+                  "transactionReceipts.effectiveGasPrice as txr_effectiveGasPrice",
+                  "transactionReceipts.from as txr_from",
+                  "transactionReceipts.gasUsed as txr_gasUsed",
+                  "transactionReceipts.logs as txr_logs",
+                  "transactionReceipts.logsBloom as txr_logsBloom",
+                  "transactionReceipts.status as txr_status",
+                  "transactionReceipts.to as txr_to",
+                  "transactionReceipts.transactionHash as txr_transactionHash",
+                  "transactionReceipts.transactionIndex as txr_transactionIndex",
+                  "transactionReceipts.type as txr_type",
+                ]),
+            )
             .select([
               "source_id",
 
@@ -940,14 +1041,59 @@ export class PostgresSyncStore implements SyncStore {
                           }
                         : { type: row.tx_type }),
               },
-            } satisfies {
-              chainId: number;
-              sourceId: string;
-              log: Log;
-              block: Block;
-              transaction: Transaction;
-              encodedCheckpoint: string;
-            };
+              transactionReceipt: sourcesById[row.source_id].criteria
+                .includeTransactionReceipts
+                ? {
+                    blockHash: row.txr_blockHash,
+                    blockNumber: row.txr_blockNumber,
+                    contractAddress: row.txr_contractAddress
+                      ? checksumAddress(row.txr_contractAddress)
+                      : null,
+                    cumulativeGasUsed: row.txr_cumulativeGasUsed,
+                    effectiveGasPrice: row.txr_effectiveGasPrice,
+                    from: checksumAddress(row.txr_from),
+                    gasUsed: row.txr_gasUsed,
+                    logs: JSON.parse(row.txr_logs).map((log: SyncLog) => ({
+                      address: checksumAddress(log.address),
+                      blockHash: log.blockHash,
+                      blockNumber: hexToBigInt(log.blockNumber),
+                      data: log.data,
+                      logIndex: hexToNumber(log.logIndex),
+                      removed: false,
+                      topics: [
+                        log.topics[0] ?? null,
+                        log.topics[1] ?? null,
+                        log.topics[2] ?? null,
+                        log.topics[3] ?? null,
+                      ].filter((t): t is Hex => t !== null) as
+                        | [Hex, ...Hex[]]
+                        | [],
+                      transactionHash: log.transactionHash,
+                      transactionIndex: hexToNumber(log.transactionIndex),
+                    })),
+                    logsBloom: row.txr_logsBloom,
+                    status:
+                      row.txr_status === "0x1"
+                        ? "success"
+                        : row.txr_status === "0x0"
+                          ? "reverted"
+                          : (row.txr_status as TransactionReceipt["status"]),
+                    to: row.txr_to ? checksumAddress(row.txr_to) : null,
+                    transactionHash: row.txr_transactionHash,
+                    transactionIndex: Number(row.txr_transactionIndex),
+                    type:
+                      row.txr_type === "0x0"
+                        ? "legacy"
+                        : row.txr_type === "0x1"
+                          ? "eip2930"
+                          : row.tx_type === "0x2"
+                            ? "eip1559"
+                            : row.tx_type === "0x7e"
+                              ? "deposit"
+                              : row.tx_type,
+                  }
+                : undefined,
+            } satisfies RawEvent;
           });
         },
       );
diff --git a/packages/core/src/sync-store/sqlite/encoding.ts b/packages/core/src/sync-store/sqlite/encoding.ts
index 4132d609..3026d809 100644
--- a/packages/core/src/sync-store/sqlite/encoding.ts
+++ b/packages/core/src/sync-store/sqlite/encoding.ts
@@ -1,5 +1,5 @@
 import type { Generated, Insertable } from "kysely";
-import type { Address, Hash, Hex } from "viem";
+import type { Address, Hash, Hex, RpcTransactionReceipt } from "viem";
 import {
   type RpcBlock,
   type RpcLog,
@@ -125,6 +125,51 @@ export function rpcToSqliteTransaction(
   };
 }
 
+type TransactionReceiptsTable = {
+  blockHash: Hash;
+  blockNumber: BigIntText;
+  contractAddress: Address | null;
+  cumulativeGasUsed: BigIntText;
+  effectiveGasPrice: BigIntText;
+  from: Address;
+  gasUsed: BigIntText;
+  logs: string;
+  logsBloom: Hex;
+  status: Hex;
+  to: Address | null;
+  transactionHash: Hash;
+  transactionIndex: number;
+  type: Hex;
+
+  chainId: number;
+};
+
+export type InsertableTransactionReceipts =
+  Insertable<TransactionReceiptsTable>;
+
+export function rpcToSqliteTransactionReceipt(
+  transactionReceipt: RpcTransactionReceipt,
+): Omit<TransactionReceiptsTable, "chainId"> {
+  return {
+    blockHash: transactionReceipt.blockHash,
+    blockNumber: encodeAsText(transactionReceipt.blockNumber),
+    contractAddress: transactionReceipt.contractAddress
+      ? toLowerCase(transactionReceipt.contractAddress)
+      : null,
+    cumulativeGasUsed: encodeAsText(transactionReceipt.cumulativeGasUsed),
+    effectiveGasPrice: encodeAsText(transactionReceipt.effectiveGasPrice),
+    from: toLowerCase(transactionReceipt.from),
+    gasUsed: encodeAsText(transactionReceipt.gasUsed),
+    logs: JSON.stringify(transactionReceipt.logs),
+    logsBloom: transactionReceipt.logsBloom,
+    status: transactionReceipt.status,
+    to: transactionReceipt.to ? toLowerCase(transactionReceipt.to) : null,
+    transactionHash: transactionReceipt.transactionHash,
+    transactionIndex: Number(transactionReceipt.transactionIndex),
+    type: transactionReceipt.type as Hex,
+  };
+}
+
 type LogsTable = {
   id: string;
   address: Address;
@@ -178,6 +223,7 @@ type LogFiltersTable = {
   topic1: Hex | null;
   topic2: Hex | null;
   topic3: Hex | null;
+  includeTransactionReceipts: 0 | 1;
 };
 
 type LogFilterIntervalsTable = {
@@ -197,6 +243,7 @@ type FactoriesTable = {
   topic1: Hex | null;
   topic2: Hex | null;
   topic3: Hex | null;
+  includeTransactionReceipts: 0 | 1;
 };
 
 type FactoryLogFilterIntervalsTable = {
@@ -209,6 +256,7 @@ type FactoryLogFilterIntervalsTable = {
 export type SyncStoreTables = {
   blocks: BlocksTable;
   transactions: TransactionsTable;
+  transactionReceipts: TransactionReceiptsTable;
   logs: LogsTable;
   rpcRequestResults: RpcRequestResultsTable;
 
diff --git a/packages/core/src/sync-store/sqlite/migrations.ts b/packages/core/src/sync-store/sqlite/migrations.ts
index 5435e4f4..5034c430 100644
--- a/packages/core/src/sync-store/sqlite/migrations.ts
+++ b/packages/core/src/sync-store/sqlite/migrations.ts
@@ -537,6 +537,166 @@ const migrations: Record<string, Migration> = {
         .execute();
     },
   },
+  "2024_04_22_0_transaction_receipts": {
+    async up(db: Kysely<any>) {
+      // Disable foriegn keys for the duration of this transaction.
+      await db.executeQuery(sql`PRAGMA foreign_keys = 0`.compile(db));
+
+      // Rename and update the existing tables to include the data we want. Note that
+      // these tables have constraints that we do NOT want. They won't get copied over.
+      await db.schema
+        .alterTable("logFilters")
+        .renameTo("logFilters_temp")
+        .execute();
+      await db
+        .updateTable("logFilters_temp")
+        .set({ id: sql`"id" || '_0'` })
+        .execute();
+      await db.schema
+        .alterTable("logFilters_temp")
+        .addColumn("includeTransactionReceipts", "integer", (col) =>
+          col.notNull().defaultTo(0),
+        )
+        .execute();
+      await db.schema
+        .alterTable("logFilterIntervals")
+        .renameTo("logFilterIntervals_temp")
+        .execute();
+      await db
+        .updateTable("logFilterIntervals_temp")
+        .set({ logFilterId: sql`"logFilterId" || '_0'` })
+        .execute();
+
+      await db.schema
+        .createTable("logFilters")
+        // `${chainId}_${address}_${topic0}_${topic1}_${topic2}_${topic3}_${includeTransactionReceipts}`
+        .addColumn("id", "text", (col) => col.notNull().primaryKey())
+        .addColumn("chainId", "integer", (col) => col.notNull())
+        .addColumn("address", "varchar(66)")
+        .addColumn("topic0", "varchar(66)")
+        .addColumn("topic1", "varchar(66)")
+        .addColumn("topic2", "varchar(66)")
+        .addColumn("topic3", "varchar(66)")
+        .addColumn("includeTransactionReceipts", "integer", (col) =>
+          col.notNull(),
+        )
+        .execute();
+      await db.schema
+        .createTable("logFilterIntervals")
+        .addColumn("id", "integer", (col) => col.notNull().primaryKey()) // Auto-increment
+        // Note that we removed the foreign key constraint here.
+        .addColumn("logFilterId", "text", (col) => col.notNull())
+        .addColumn("startBlock", "varchar(79)", (col) => col.notNull())
+        .addColumn("endBlock", "varchar(79)", (col) => col.notNull())
+        .execute();
+      // Copy data from temp tables to new tables.
+      await db.executeQuery(
+        sql`INSERT INTO "logFilters" SELECT * FROM "logFilters_temp"`.compile(
+          db,
+        ),
+      );
+      await db.executeQuery(
+        sql`INSERT INTO "logFilterIntervals" SELECT * FROM "logFilterIntervals_temp"`.compile(
+          db,
+        ),
+      );
+      // Drop the temp tables.
+      await db.schema.dropTable("logFilters_temp").execute();
+      await db.schema.dropTable("logFilterIntervals_temp").execute();
+      // Add back the index.
+      await db.schema
+        .createIndex("logFilterIntervalsLogFilterId")
+        .on("logFilterIntervals")
+        .column("logFilterId")
+        .execute();
+
+      // Repeat the same process for factories.
+      await db.schema
+        .alterTable("factories")
+        .renameTo("factories_temp")
+        .execute();
+      await db
+        .updateTable("factories_temp")
+        .set({ id: sql`"id" || '_0'` })
+        .execute();
+      await db.schema
+        .alterTable("factories_temp")
+        .addColumn("includeTransactionReceipts", "integer", (col) =>
+          col.notNull().defaultTo(0),
+        )
+        .execute();
+      await db.schema
+        .alterTable("factoryLogFilterIntervals")
+        .renameTo("factoryLogFilterIntervals_temp")
+        .execute();
+      await db
+        .updateTable("factoryLogFilterIntervals_temp")
+        .set({ factoryId: sql`"factoryId" || '_0'` })
+        .execute();
+      await db.schema
+        .createTable("factories")
+        // `${chainId}_${address}_${eventSelector}_${childAddressLocation}_${includeTransactionReceipts}`
+        .addColumn("id", "text", (col) => col.notNull().primaryKey())
+        .addColumn("chainId", "integer", (col) => col.notNull())
+        .addColumn("address", "varchar(42)", (col) => col.notNull())
+        .addColumn("eventSelector", "varchar(66)", (col) => col.notNull())
+        .addColumn("childAddressLocation", "text", (col) => col.notNull()) // `topic${number}` or `offset${number}`
+        .addColumn("topic0", "varchar(66)")
+        .addColumn("topic1", "varchar(66)")
+        .addColumn("topic2", "varchar(66)")
+        .addColumn("topic3", "varchar(66)")
+        .addColumn("includeTransactionReceipts", "integer", (col) =>
+          col.notNull(),
+        )
+        .execute();
+      await db.schema
+        .createTable("factoryLogFilterIntervals")
+        .addColumn("id", "integer", (col) => col.notNull().primaryKey()) // Auto-increment
+        // Note that we removed the foreign key constraint here.
+        .addColumn("factoryId", "text", (col) => col.notNull())
+        .addColumn("startBlock", "varchar(79)", (col) => col.notNull())
+        .addColumn("endBlock", "varchar(79)", (col) => col.notNull())
+        .execute();
+      await db.executeQuery(
+        sql`INSERT INTO "factories" SELECT * FROM "factories_temp"`.compile(db),
+      );
+      await db.executeQuery(
+        sql`INSERT INTO "factoryLogFilterIntervals" SELECT * FROM "factoryLogFilterIntervals_temp"`.compile(
+          db,
+        ),
+      );
+      await db.schema.dropTable("factories_temp").execute();
+      await db.schema.dropTable("factoryLogFilterIntervals_temp").execute();
+      await db.schema
+        .createIndex("factoryLogFilterIntervalsFactoryId")
+        .on("factoryLogFilterIntervals")
+        .column("factoryId")
+        .execute();
+
+      await db.schema
+        .createTable("transactionReceipts")
+        .addColumn("blockHash", "varchar(66)", (col) => col.notNull())
+        .addColumn("blockNumber", "varchar(79)", (col) => col.notNull())
+        .addColumn("chainId", "integer", (col) => col.notNull())
+        .addColumn("contractAddress", "varchar(66)")
+        .addColumn("cumulativeGasUsed", "varchar(79)", (col) => col.notNull())
+        .addColumn("effectiveGasPrice", "varchar(79)", (col) => col.notNull())
+        .addColumn("from", "varchar(42)", (col) => col.notNull())
+        .addColumn("gasUsed", "varchar(79)", (col) => col.notNull())
+        .addColumn("logs", "text", (col) => col.notNull())
+        .addColumn("logsBloom", "varchar(514)", (col) => col.notNull())
+        .addColumn("status", "text", (col) => col.notNull())
+        .addColumn("to", "varchar(42)")
+        .addColumn("transactionHash", "varchar(66)", (col) =>
+          col.notNull().primaryKey(),
+        )
+        .addColumn("transactionIndex", "integer", (col) => col.notNull())
+        .addColumn("type", "text", (col) => col.notNull())
+        .execute();
+
+      await db.executeQuery(sql`PRAGMA foreign_keys = 1`.compile(db));
+    },
+  },
 };
 
 async function hasCheckpointCol(db: Kysely<any>) {
diff --git a/packages/core/src/sync-store/sqlite/store.ts b/packages/core/src/sync-store/sqlite/store.ts
index 739c880f..7da4c41c 100644
--- a/packages/core/src/sync-store/sqlite/store.ts
+++ b/packages/core/src/sync-store/sqlite/store.ts
@@ -8,7 +8,8 @@ import {
   sourceIsLog,
 } from "@/config/sources.js";
 import type { HeadlessKysely } from "@/database/kysely.js";
-import type { Block, Log, Transaction } from "@/types/eth.js";
+import type { SyncLog } from "@/sync/index.js";
+import type { Log } from "@/types/eth.js";
 import type { NonNull } from "@/types/utils.js";
 import {
   type Checkpoint,
@@ -33,9 +34,14 @@ import {
   type RpcBlock,
   type RpcLog,
   type RpcTransaction,
+  type RpcTransactionReceipt,
+  type TransactionReceipt,
   checksumAddress,
+  hexToBigInt,
+  hexToNumber,
 } from "viem";
-import type { SyncStore } from "../store.js";
+import type { RawEvent, SyncStore } from "../store.js";
+import { rpcToSqliteTransactionReceipt } from "./encoding.js";
 import {
   type SyncStoreTables,
   rpcToSqliteBlock,
@@ -56,6 +62,7 @@ export class SqliteSyncStore implements SyncStore {
     logFilter,
     block: rpcBlock,
     transactions: rpcTransactions,
+    transactionReceipts: rpcTransactionReceipts,
     logs: rpcLogs,
     interval,
   }: {
@@ -63,6 +70,7 @@ export class SqliteSyncStore implements SyncStore {
     logFilter: LogFilterCriteria;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
     interval: { startBlock: bigint; endBlock: bigint };
   }) => {
@@ -86,6 +94,20 @@ export class SqliteSyncStore implements SyncStore {
             .execute();
         }
 
+        if (rpcTransactionReceipts.length > 0) {
+          const transactionReceipts = rpcTransactionReceipts.map(
+            (rpcTransactionReceipt) => ({
+              ...rpcToSqliteTransactionReceipt(rpcTransactionReceipt),
+              chainId,
+            }),
+          );
+          await tx
+            .insertInto("transactionReceipts")
+            .values(transactionReceipts)
+            .onConflict((oc) => oc.column("transactionHash").doNothing())
+            .execute();
+        }
+
         if (rpcLogs.length > 0) {
           const logs = rpcLogs.map((rpcLog) => ({
             ...rpcToSqliteLog(rpcLog),
@@ -169,7 +191,7 @@ export class SqliteSyncStore implements SyncStore {
 
       const intervals = await this.db
         .with(
-          "logFilterFragments(fragmentId, fragmentAddress, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3)",
+          "logFilterFragments(fragmentId, fragmentAddress, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3, fragmentIncludeTransactionReceipts)",
           () =>
             sql`( values ${sql.join(
               fragments.map(
@@ -178,25 +200,32 @@ export class SqliteSyncStore implements SyncStore {
                     f.topic0,
                   )}, ${sql.val(f.topic1)}, ${sql.val(f.topic2)}, ${sql.val(
                     f.topic3,
-                  )} )`,
+                  )}, ${sql.lit(f.includeTransactionReceipts)} )`,
               ),
             )} )`,
         )
         .selectFrom("logFilterIntervals")
-        .leftJoin("logFilters", "logFilterId", "logFilters.id")
+        .innerJoin("logFilters", "logFilterId", "logFilters.id")
         .innerJoin("logFilterFragments", (join) => {
-          let baseJoin = join.on(({ or, cmpr }) =>
-            or([
-              cmpr("address", "is", null),
-              cmpr("fragmentAddress", "=", sql.ref("address")),
+          let baseJoin = join.on((eb) =>
+            eb.or([
+              eb("address", "is", null),
+              eb("fragmentAddress", "=", sql.ref("address")),
             ]),
           );
+          baseJoin = baseJoin.on((eb) =>
+            eb(
+              "fragmentIncludeTransactionReceipts",
+              "<=",
+              sql.ref("includeTransactionReceipts"),
+            ),
+          );
           for (const idx_ of range(0, 4)) {
-            baseJoin = baseJoin.on(({ or, cmpr }) => {
+            baseJoin = baseJoin.on((eb) => {
               const idx = idx_ as 0 | 1 | 2 | 3;
-              return or([
-                cmpr(`topic${idx}`, "is", null),
-                cmpr(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
+              return eb.or([
+                eb(`topic${idx}`, "is", null),
+                eb(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
               ]);
             });
           }
@@ -306,6 +335,7 @@ export class SqliteSyncStore implements SyncStore {
     factory,
     block: rpcBlock,
     transactions: rpcTransactions,
+    transactionReceipts: rpcTransactionReceipts,
     logs: rpcLogs,
     interval,
   }: {
@@ -313,6 +343,7 @@ export class SqliteSyncStore implements SyncStore {
     factory: FactoryCriteria;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
     interval: { startBlock: bigint; endBlock: bigint };
   }) => {
@@ -338,6 +369,20 @@ export class SqliteSyncStore implements SyncStore {
               .execute();
           }
 
+          if (rpcTransactionReceipts.length > 0) {
+            const transactionReceipts = rpcTransactionReceipts.map(
+              (rpcTransactionReceipt) => ({
+                ...rpcToSqliteTransactionReceipt(rpcTransactionReceipt),
+                chainId,
+              }),
+            );
+            await tx
+              .insertInto("transactionReceipts")
+              .values(transactionReceipts)
+              .onConflict((oc) => oc.column("transactionHash").doNothing())
+              .execute();
+          }
+
           if (rpcLogs.length > 0) {
             const logs = rpcLogs.map((rpcLog) => ({
               ...rpcToSqliteLog(rpcLog),
@@ -423,7 +468,7 @@ export class SqliteSyncStore implements SyncStore {
 
         const intervals = await this.db
           .with(
-            "factoryFilterFragments(fragmentId, fragmentAddress, fragmentEventSelector, fragmentChildAddressLocation, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3)",
+            "factoryFilterFragments(fragmentId, fragmentAddress, fragmentEventSelector, fragmentChildAddressLocation, fragmentTopic0, fragmentTopic1, fragmentTopic2, fragmentTopic3, fragmentIncludeTransactionReceipts)",
             () =>
               sql`( values ${sql.join(
                 fragments.map(
@@ -434,30 +479,37 @@ export class SqliteSyncStore implements SyncStore {
                       f.topic0,
                     )}, ${sql.val(f.topic1)}, ${sql.val(f.topic2)}, ${sql.val(
                       f.topic3,
-                    )} )`,
+                    )}, ${sql.lit(f.includeTransactionReceipts)} )`,
                 ),
               )} )`,
           )
           .selectFrom("factoryLogFilterIntervals")
-          .leftJoin("factories", "factoryId", "factories.id")
+          .innerJoin("factories", "factoryId", "factories.id")
           .innerJoin("factoryFilterFragments", (join) => {
-            let baseJoin = join.on(({ and, cmpr }) =>
-              and([
-                cmpr("fragmentAddress", "=", sql.ref("address")),
-                cmpr("fragmentEventSelector", "=", sql.ref("eventSelector")),
-                cmpr(
+            let baseJoin = join.on((eb) =>
+              eb.and([
+                eb("fragmentAddress", "=", sql.ref("address")),
+                eb("fragmentEventSelector", "=", sql.ref("eventSelector")),
+                eb(
                   "fragmentChildAddressLocation",
                   "=",
                   sql.ref("childAddressLocation"),
                 ),
               ]),
             );
+            baseJoin = baseJoin.on((eb) =>
+              eb(
+                "fragmentIncludeTransactionReceipts",
+                "<=",
+                sql.ref("includeTransactionReceipts"),
+              ),
+            );
             for (const idx_ of range(0, 4)) {
-              baseJoin = baseJoin.on(({ or, cmpr }) => {
+              baseJoin = baseJoin.on((eb) => {
                 const idx = idx_ as 0 | 1 | 2 | 3;
-                return or([
-                  cmpr(`topic${idx}`, "is", null),
-                  cmpr(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
+                return eb.or([
+                  eb(`topic${idx}`, "is", null),
+                  eb(`fragmentTopic${idx}`, "=", sql.ref(`topic${idx}`)),
                 ]);
               });
             }
@@ -492,11 +544,13 @@ export class SqliteSyncStore implements SyncStore {
     chainId,
     block: rpcBlock,
     transactions: rpcTransactions,
+    transactionReceipts: rpcTransactionReceipts,
     logs: rpcLogs,
   }: {
     chainId: number;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
   }) => {
     return this.db.wrap({ method: "insertRealtimeBlock" }, async () => {
@@ -525,6 +579,20 @@ export class SqliteSyncStore implements SyncStore {
             .execute();
         }
 
+        if (rpcTransactionReceipts.length > 0) {
+          const transactionReceipts = rpcTransactionReceipts.map(
+            (rpcTransactionReceipt) => ({
+              ...rpcToSqliteTransactionReceipt(rpcTransactionReceipt),
+              chainId,
+            }),
+          );
+          await tx
+            .insertInto("transactionReceipts")
+            .values(transactionReceipts)
+            .onConflict((oc) => oc.column("transactionHash").doNothing())
+            .execute();
+        }
+
         if (rpcLogs.length > 0) {
           const logs = rpcLogs.map((rpcLog) => ({
             ...rpcToSqliteLog(rpcLog),
@@ -590,6 +658,7 @@ export class SqliteSyncStore implements SyncStore {
             ...factories.map((f) => ({
               address: f.address,
               topics: [f.eventSelector],
+              includeTransactionReceipts: f.includeTransactionReceipts,
             })),
           ],
           interval,
@@ -775,6 +844,16 @@ export class SqliteSyncStore implements SyncStore {
     let cursor = encodeCheckpoint(fromCheckpoint);
     const encodedToCheckpoint = encodeCheckpoint(toCheckpoint);
 
+    const shouldJoinReceipts = sources.some(
+      (source) => source.criteria.includeTransactionReceipts,
+    );
+    const sourcesById = sources.reduce<{
+      [sourceId: EventSource["id"]]: (typeof sources)[number];
+    }>((acc, cur) => {
+      acc[cur.id] = cur;
+      return acc;
+    }, {});
+
     while (true) {
       const events = await this.db.wrap(
         { method: "getLogEvents" },
@@ -815,6 +894,30 @@ export class SqliteSyncStore implements SyncStore {
 
               return eb.or([...logFilterCmprs, ...factoryCmprs]);
             })
+            .$if(shouldJoinReceipts, (qb) =>
+              qb
+                .innerJoin(
+                  "transactionReceipts",
+                  "transactionReceipts.transactionHash",
+                  "logs.transactionHash",
+                )
+                .select([
+                  "transactionReceipts.blockHash as txr_blockHash",
+                  "transactionReceipts.blockNumber as txr_blockNumber",
+                  "transactionReceipts.contractAddress as txr_contractAddress",
+                  "transactionReceipts.cumulativeGasUsed as txr_cumulativeGasUsed",
+                  "transactionReceipts.effectiveGasPrice as txr_effectiveGasPrice",
+                  "transactionReceipts.from as txr_from",
+                  "transactionReceipts.gasUsed as txr_gasUsed",
+                  "transactionReceipts.logs as txr_logs",
+                  "transactionReceipts.logsBloom as txr_logsBloom",
+                  "transactionReceipts.status as txr_status",
+                  "transactionReceipts.to as txr_to",
+                  "transactionReceipts.transactionHash as txr_transactionHash",
+                  "transactionReceipts.transactionIndex as txr_transactionIndex",
+                  "transactionReceipts.type as txr_type",
+                ]),
+            )
             .select([
               "source_id",
 
@@ -976,14 +1079,63 @@ export class SqliteSyncStore implements SyncStore {
                             type: row.tx_type,
                           }),
               },
-            } satisfies {
-              chainId: number;
-              sourceId: string;
-              log: Log;
-              block: Block;
-              transaction: Transaction;
-              encodedCheckpoint: string;
-            };
+              transactionReceipt: sourcesById[row.source_id].criteria
+                .includeTransactionReceipts
+                ? {
+                    blockHash: row.txr_blockHash,
+                    blockNumber: decodeToBigInt(row.txr_blockNumber),
+                    contractAddress: row.txr_contractAddress
+                      ? checksumAddress(row.txr_contractAddress)
+                      : null,
+                    cumulativeGasUsed: decodeToBigInt(
+                      row.txr_cumulativeGasUsed,
+                    ),
+                    effectiveGasPrice: decodeToBigInt(
+                      row.txr_effectiveGasPrice,
+                    ),
+                    from: checksumAddress(row.txr_from),
+                    gasUsed: decodeToBigInt(row.txr_gasUsed),
+                    logs: JSON.parse(row.txr_logs).map((log: SyncLog) => ({
+                      address: checksumAddress(log.address),
+                      blockHash: log.blockHash,
+                      blockNumber: hexToBigInt(log.blockNumber),
+                      data: log.data,
+                      logIndex: hexToNumber(log.logIndex),
+                      removed: false,
+                      topics: [
+                        log.topics[0] ?? null,
+                        log.topics[1] ?? null,
+                        log.topics[2] ?? null,
+                        log.topics[3] ?? null,
+                      ].filter((t): t is Hex => t !== null) as
+                        | [Hex, ...Hex[]]
+                        | [],
+                      transactionHash: log.transactionHash,
+                      transactionIndex: hexToNumber(log.transactionIndex),
+                    })),
+                    logsBloom: row.txr_logsBloom,
+                    status:
+                      row.txr_status === "0x1"
+                        ? "success"
+                        : row.txr_status === "0x0"
+                          ? "reverted"
+                          : (row.txr_status as TransactionReceipt["status"]),
+                    to: row.txr_to ? checksumAddress(row.txr_to) : null,
+                    transactionHash: row.txr_transactionHash,
+                    transactionIndex: Number(row.txr_transactionIndex),
+                    type:
+                      row.txr_type === "0x0"
+                        ? "legacy"
+                        : row.txr_type === "0x1"
+                          ? "eip2930"
+                          : row.tx_type === "0x2"
+                            ? "eip1559"
+                            : row.tx_type === "0x7e"
+                              ? "deposit"
+                              : row.tx_type,
+                  }
+                : undefined,
+            } satisfies RawEvent;
           });
         },
       );
diff --git a/packages/core/src/sync-store/store.bench.ts b/packages/core/src/sync-store/store.bench.ts
index b15d69c8..ffb3935d 100644
--- a/packages/core/src/sync-store/store.bench.ts
+++ b/packages/core/src/sync-store/store.bench.ts
@@ -10,7 +10,12 @@ import type { LogFilterCriteria } from "@/config/sources.js";
 import { type Checkpoint, maxCheckpoint } from "@/utils/checkpoint.js";
 import { drainAsyncGenerator } from "@/utils/drainAsyncGenerator.js";
 import { range } from "@/utils/range.js";
-import { type RpcBlock, type RpcLog, type RpcTransaction } from "viem";
+import {
+  type RpcBlock,
+  type RpcLog,
+  type RpcTransaction,
+  type RpcTransactionReceipt,
+} from "viem";
 import { type TestContext, bench } from "vitest";
 import type { SyncStore } from "./store.js";
 
@@ -53,6 +58,7 @@ const generateBlock = (
   logFilter: LogFilterCriteria;
   block: RpcBlock;
   transactions: RpcTransaction[];
+  transactionReceipts: RpcTransactionReceipt[];
   logs: RpcLog[];
   interval: { startBlock: bigint; endBlock: bigint };
 } => {
@@ -127,6 +133,7 @@ const generateBlock = (
     block,
     logs,
     transactions,
+    transactionReceipts: [],
     interval: {
       startBlock: opts.block,
       endBlock: opts.block,
@@ -135,6 +142,7 @@ const generateBlock = (
     logFilter: {
       address: opts.addr,
       topics: opts.topics,
+      includeTransactionReceipts: false,
     },
   };
 };
@@ -325,6 +333,7 @@ for (const c of LOG_FILTER_CASES) {
               criteria: {
                 address: CONTRACT_ADDR,
                 topics: [TOPIC_0],
+                includeTransactionReceipts: false,
               },
             },
           ],
@@ -366,6 +375,7 @@ for (const c of LOG_FILTER_CASES) {
               criteria: {
                 address: CONTRACT_ADDR,
                 topics: [TOPIC_0],
+                includeTransactionReceipts: false,
               },
             },
           ],
@@ -407,6 +417,7 @@ for (const c of LOG_FILTER_CASES) {
               criteria: {
                 address: CONTRACT_ADDR,
                 topics: [TOPIC_0],
+                includeTransactionReceipts: false,
               },
             },
           ],
diff --git a/packages/core/src/sync-store/store.ts b/packages/core/src/sync-store/store.ts
index 8a3ef204..5437eaba 100644
--- a/packages/core/src/sync-store/store.ts
+++ b/packages/core/src/sync-store/store.ts
@@ -4,9 +4,20 @@ import type {
   LogFilterCriteria,
 } from "@/config/sources.js";
 import type { HeadlessKysely } from "@/database/kysely.js";
-import type { Block, Log, Transaction } from "@/types/eth.js";
+import type {
+  Block,
+  Log,
+  Transaction,
+  TransactionReceipt,
+} from "@/types/eth.js";
 import type { Checkpoint } from "@/utils/checkpoint.js";
-import type { Address, RpcBlock, RpcLog, RpcTransaction } from "viem";
+import type {
+  Address,
+  RpcBlock,
+  RpcLog,
+  RpcTransaction,
+  RpcTransactionReceipt,
+} from "viem";
 
 export type RawEvent = {
   chainId: number;
@@ -14,6 +25,7 @@ export type RawEvent = {
   log: Log;
   block: Block;
   transaction: Transaction;
+  transactionReceipt?: TransactionReceipt;
   encodedCheckpoint: string;
 };
 
@@ -33,6 +45,7 @@ export interface SyncStore {
     logFilter: LogFilterCriteria;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
     interval: { startBlock: bigint; endBlock: bigint };
   }): Promise<void>;
@@ -85,6 +98,7 @@ export interface SyncStore {
     factory: FactoryCriteria;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
     interval: { startBlock: bigint; endBlock: bigint };
   }): Promise<void>;
@@ -108,6 +122,7 @@ export interface SyncStore {
     chainId: number;
     block: RpcBlock;
     transactions: RpcTransaction[];
+    transactionReceipts: RpcTransactionReceipt[];
     logs: RpcLog[];
   }): Promise<void>;
 
diff --git a/packages/core/src/sync/events.ts b/packages/core/src/sync/events.ts
index 38e908bd..878f7292 100644
--- a/packages/core/src/sync/events.ts
+++ b/packages/core/src/sync/events.ts
@@ -1,5 +1,10 @@
 import type { RawEvent } from "@/sync-store/store.js";
-import type { Block, Log, Transaction } from "@/types/eth.js";
+import type {
+  Block,
+  Log,
+  Transaction,
+  TransactionReceipt,
+} from "@/types/eth.js";
 import { decodeEventLog } from "viem";
 import type { Service } from "./service.js";
 
@@ -21,6 +26,7 @@ export type LogEvent = {
     log: Log;
     block: Block;
     transaction: Transaction;
+    transactionReceipt?: TransactionReceipt;
   };
   encodedCheckpoint: string;
 };
@@ -57,6 +63,7 @@ export const decodeEvents = (
           log: event.log,
           block: event.block,
           transaction: event.transaction,
+          transactionReceipt: event.transactionReceipt,
         },
         encodedCheckpoint: event.encodedCheckpoint,
       });
diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts
index e368cd04..c19d9efe 100644
--- a/packages/core/src/sync/index.ts
+++ b/packages/core/src/sync/index.ts
@@ -17,6 +17,8 @@ import {
   type LogTopic,
   type RpcBlock,
   RpcError,
+  type RpcTransactionReceipt,
+  TransactionReceiptNotFoundError,
   hexToBigInt,
   numberToHex,
 } from "viem";
@@ -50,6 +52,7 @@ export type BaseSyncService = {
 
 export type SyncBlock = RpcBlock<Exclude<BlockTag, "pending">, true>;
 export type SyncLog = Log<Hex, Hex, false>;
+export type SyncTransactionReceipt = RpcTransactionReceipt;
 
 /**
  * Helper function for "eth_getBlockByNumber" request.
@@ -197,3 +200,23 @@ export const _eth_getLogs = async (
     ).then((l) => l.flat());
   }
 };
+
+/**
+ * Helper function for "eth_getTransactionReceipt" request.
+ */
+export const _eth_getTransactionReceipt = (
+  { requestQueue }: Pick<BaseSyncService, "requestQueue">,
+  { hash }: { hash: Hex },
+): Promise<SyncTransactionReceipt> =>
+  requestQueue
+    .request({
+      method: "eth_getTransactionReceipt",
+      params: [hash],
+    })
+    .then((receipt) => {
+      if (!receipt)
+        throw new TransactionReceiptNotFoundError({
+          hash,
+        });
+      return receipt as SyncTransactionReceipt;
+    });
diff --git a/packages/core/src/types/eth.ts b/packages/core/src/types/eth.ts
index 2e8b9e7e..abc2cc3e 100644
--- a/packages/core/src/types/eth.ts
+++ b/packages/core/src/types/eth.ts
@@ -1,4 +1,4 @@
-import type { AccessList, Address, Hash, Hex } from "viem";
+import type { AccessList, Address, Hash, Hex, TransactionType } from "viem";
 import type { Prettify } from "./utils.js";
 
 /**
@@ -158,3 +158,37 @@ export type Log = {
   /** Index of the transaction that created this log */
   transactionIndex: number;
 };
+
+/**
+ * A confirmed Ethereum transaction receipt.
+ */
+export type TransactionReceipt = {
+  /** Hash of block containing this transaction */
+  blockHash: Hash;
+  /** Number of block containing this transaction */
+  blockNumber: bigint;
+  /** Address of new contract or `null` if no contract was created */
+  contractAddress: Address | null;
+  /** Gas used by this and all preceding transactions in this block */
+  cumulativeGasUsed: bigint;
+  /** Pre-London, it is equal to the transaction's gasPrice. Post-London, it is equal to the actual gas price paid for inclusion. */
+  effectiveGasPrice: bigint;
+  /** Transaction sender */
+  from: Address;
+  /** Gas used by this transaction */
+  gasUsed: bigint;
+  /** List of log objects generated by this transaction */
+  logs: Log[];
+  /** Logs bloom filter */
+  logsBloom: Hex;
+  /** `success` if this transaction was successful or `reverted` if it failed */
+  status: "success" | "reverted";
+  /** Transaction recipient or `null` if deploying a contract */
+  to: Address | null;
+  /** Hash of this transaction */
+  transactionHash: Hash;
+  /** Index of this transaction in the block */
+  transactionIndex: number;
+  /** Transaction type */
+  type: TransactionType;
+};
diff --git a/packages/core/src/types/virtual.ts b/packages/core/src/types/virtual.ts
index 5cdfee09..97fc01a7 100644
--- a/packages/core/src/types/virtual.ts
+++ b/packages/core/src/types/virtual.ts
@@ -2,7 +2,12 @@ import type { Config } from "@/config/config.js";
 import type { ParseAbiEvent, SafeEventNames } from "@/config/utilityTypes.js";
 import type { ReadOnlyClient } from "@/indexing/ponderActions.js";
 import type { Infer, Schema as _Schema } from "@/schema/types.js";
-import type { Block, Log, Transaction } from "@/types/eth.js";
+import type {
+  Block,
+  Log,
+  Transaction,
+  TransactionReceipt,
+} from "@/types/eth.js";
 import type { DatabaseModel } from "@/types/model.js";
 import type { Abi, GetEventArgs } from "viem";
 import type { Prettify } from "./utils.js";
@@ -70,7 +75,20 @@ export namespace Virtual {
         log: Prettify<Log>;
         block: Prettify<Block>;
         transaction: Prettify<Transaction>;
-      };
+      } & (ExtractOverridenProperty<
+        config["contracts"][contractName],
+        "includeTransactionReceipts"
+      > extends infer includeTxr
+        ? includeTxr extends includeTxr
+          ? includeTxr extends true
+            ? {
+                transactionReceipt: Prettify<TransactionReceipt>;
+              }
+            : {
+                transactionReceipt?: never;
+              }
+          : never
+        : never);
 
   type ContextContractProperty = Exclude<
     keyof Config["contracts"][string],
diff --git a/packages/core/src/utils/fragments.ts b/packages/core/src/utils/fragments.ts
index 5af1e810..bc6e0e7b 100644
--- a/packages/core/src/utils/fragments.ts
+++ b/packages/core/src/utils/fragments.ts
@@ -1,6 +1,5 @@
-import type { Address, Hex } from "viem";
-
 import type { FactoryCriteria, LogFilterCriteria } from "@/config/sources.js";
+import type { Address, Hex } from "viem";
 
 /**
  * Generates log filter fragments from a log filter.
@@ -11,6 +10,7 @@ import type { FactoryCriteria, LogFilterCriteria } from "@/config/sources.js";
 export function buildLogFilterFragments({
   address,
   topics,
+  includeTransactionReceipts,
   chainId,
 }: LogFilterCriteria & {
   chainId: number;
@@ -18,9 +18,12 @@ export function buildLogFilterFragments({
   return buildFragments({
     address,
     topics,
+    includeTransactionReceipts,
     chainId,
     idCallback: (address_, topic0_, topic1_, topic2_, topic3_) =>
-      `${chainId}_${address_}_${topic0_}_${topic1_}_${topic2_}_${topic3_}`,
+      `${chainId}_${address_}_${topic0_}_${topic1_}_${topic2_}_${topic3_}_${
+        includeTransactionReceipts ? 1 : 0
+      }`,
   });
 }
 
@@ -33,6 +36,7 @@ export function buildLogFilterFragments({
 export function buildFactoryFragments({
   address,
   topics,
+  includeTransactionReceipts,
   childAddressLocation,
   eventSelector,
   chainId,
@@ -42,11 +46,14 @@ export function buildFactoryFragments({
   const fragments = buildFragments({
     address,
     topics,
+    includeTransactionReceipts,
     chainId,
     childAddressLocation,
     eventSelector,
     idCallback: (address_, topic0_, topic1_, topic2_, topic3_) =>
-      `${chainId}_${address_}_${eventSelector}_${childAddressLocation}_${topic0_}_${topic1_}_${topic2_}_${topic3_}`,
+      `${chainId}_${address_}_${eventSelector}_${childAddressLocation}_${topic0_}_${topic1_}_${topic2_}_${topic3_}_${
+        includeTransactionReceipts ? 1 : 0
+      }`,
   });
 
   return fragments as ((typeof fragments)[number] &
@@ -61,6 +68,7 @@ function buildFragments({
   topics,
   chainId,
   idCallback,
+  includeTransactionReceipts,
   ...rest
 }: (LogFilterCriteria | FactoryCriteria) & {
   idCallback: (
@@ -80,6 +88,7 @@ function buildFragments({
     topic1: Hex | null;
     topic2: Hex | null;
     topic3: Hex | null;
+    includeTransactionReceipts: 0 | 1;
   }[] = [];
 
   const { topic0, topic1, topic2, topic3 } = parseTopics(topics);
@@ -98,6 +107,7 @@ function buildFragments({
               topic1: topic1_,
               topic2: topic2_,
               topic3: topic3_,
+              includeTransactionReceipts: includeTransactionReceipts ? 1 : 0,
             });
           }
         }
diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts
index 883a1b17..f261292a 100644
--- a/packages/core/tsup.config.ts
+++ b/packages/core/tsup.config.ts
@@ -11,6 +11,8 @@ export default defineConfig({
   clean: true,
   splitting: true,
   onSuccess: async () => {
-    await execa("pnpm", ["wagmi", "generate"]);
+    try {
+      await execa("pnpm", ["wagmi", "generate"]);
+    } catch {}
   },
 });


Test patch:
diff --git a/packages/core/src/_test/utils.ts b/packages/core/src/_test/utils.ts
index 32f14a0d..d8f3e75b 100644
--- a/packages/core/src/_test/utils.ts
+++ b/packages/core/src/_test/utils.ts
@@ -6,14 +6,16 @@ import { type EventSource } from "@/config/sources.js";
 import type { RawEvent } from "@/sync-store/store.js";
 import { encodeCheckpoint } from "@/utils/checkpoint.js";
 import { createRequestQueue } from "@/utils/requestQueue.js";
-import type {
-  BlockTag,
-  Chain,
-  Hash,
-  Hex,
-  RpcBlock,
-  RpcLog,
-  RpcTransaction,
+import {
+  type BlockTag,
+  type Chain,
+  type Hash,
+  type Hex,
+  type RpcBlock,
+  type RpcLog,
+  type RpcTransaction,
+  type RpcTransactionReceipt,
+  formatTransactionReceipt,
 } from "viem";
 import {
   http,
@@ -192,38 +194,58 @@ export const getRawRPCData = async (sources: EventSource[]) => {
       }),
     ),
   );
+  const transactionReceipts = await Promise.all(
+    [...txHashes].map((tx) =>
+      publicClient.request({
+        method: "eth_getTransactionReceipt",
+        params: [tx],
+      }),
+    ),
+  );
 
   return {
     block1: {
       logs: [logs[0]!, logs[1]!],
       block: blocks[0]!,
       transactions: blocks[0]!.transactions,
+      transactionReceipts: transactionReceipts.filter(
+        (tr) => tr?.blockNumber === blocks[0]?.number,
+      ),
     },
     block2: {
       logs: [logs[2]],
       block: blocks[1],
       transactions: blocks[1]!.transactions,
+      transactionReceipts: transactionReceipts.filter(
+        (tr) => tr?.blockNumber === blocks[1]?.number,
+      ),
     },
     block3: {
       logs: [logs[3]],
       block: blocks[2],
       transactions: blocks[2]!.transactions,
+      transactionReceipts: transactionReceipts.filter(
+        (tr) => tr?.blockNumber === blocks[2]?.number,
+      ),
     },
   } as {
     block1: {
       logs: [RpcLog, RpcLog];
       block: RpcBlock<Exclude<BlockTag, "pending">, true>;
       transactions: [RpcTransaction, RpcTransaction];
+      transactionReceipts: [RpcTransactionReceipt, RpcTransactionReceipt];
     };
     block2: {
       logs: [RpcLog];
       block: RpcBlock<Exclude<BlockTag, "pending">, true>;
       transactions: [RpcTransaction];
+      transactionReceipts: [RpcTransactionReceipt];
     };
     block3: {
       logs: [RpcLog];
       block: RpcBlock<Exclude<BlockTag, "pending">, true>;
       transactions: [RpcTransaction];
+      transactionReceipts: [RpcTransactionReceipt];
     };
   };
 };
@@ -241,19 +263,22 @@ export const getEventsErc20 = async (
       log: rpcData.block1.logs[0],
       block: rpcData.block1.block,
       transaction: rpcData.block1.transactions[0]!,
+      transactionReceipt: rpcData.block1.transactionReceipts[0]!,
     },
     {
       log: rpcData.block1.logs[1],
       block: rpcData.block1.block,
       transaction: rpcData.block1.transactions[1]!,
+      transactionReceipt: rpcData.block1.transactionReceipts[1]!,
     },
   ]
     .map((e) => ({
       log: formatLog(e.log),
       block: formatBlock(e.block),
       transaction: formatTransaction(e.transaction),
+      transactionReceipt: formatTransactionReceipt(e.transactionReceipt),
     }))
-    .map(({ log, block, transaction }) => ({
+    .map(({ log, block, transaction, transactionReceipt }) => ({
       sourceId: sources[0].id,
       chainId: sources[0].chainId,
       log: {
@@ -267,6 +292,17 @@ export const getEventsErc20 = async (
         from: checksumAddress(transaction.from),
         to: transaction.to ? checksumAddress(transaction.to) : transaction.to,
       },
+      transactionReceipt: {
+        ...transactionReceipt,
+        from: checksumAddress(transactionReceipt.from),
+        to: transactionReceipt.to
+          ? checksumAddress(transactionReceipt.to)
+          : transactionReceipt.to,
+        logs: transactionReceipt.logs.map((l) => ({
+          ...l,
+          id: `${l.blockHash}-${toHex(l.logIndex!)}`,
+        })),
+      },
       encodedCheckpoint: encodeCheckpoint({
         blockTimestamp: Number(block.timestamp),
         chainId: sources[0].chainId,
diff --git a/packages/core/src/build/configAndIndexingFunctions.test.ts b/packages/core/src/build/configAndIndexingFunctions.test.ts
index cb3d79b8..4e3f2655 100644
--- a/packages/core/src/build/configAndIndexingFunctions.test.ts
+++ b/packages/core/src/build/configAndIndexingFunctions.test.ts
@@ -462,6 +462,34 @@ test("buildConfigAndIndexingFunctions() coerces NaN startBlock to 0", async () =
   expect(sources[0].startBlock).toBe(0);
 });
 
+test("buildConfigAndIndexingFunctions() includeTransactionReceipts", async () => {
+  const config = createConfig({
+    networks: {
+      mainnet: { chainId: 1, transport: http("http://127.0.0.1:8545") },
+      optimism: { chainId: 10, transport: http("http://127.0.0.1:8545") },
+    },
+    contracts: {
+      a: {
+        includeTransactionReceipts: true,
+        network: {
+          mainnet: {},
+          optimism: { includeTransactionReceipts: false },
+        },
+        abi: [event0],
+      },
+    },
+  });
+
+  const { sources } = await buildConfigAndIndexingFunctions({
+    config,
+    rawIndexingFunctions: [{ name: "a:Event0", fn: () => {} }],
+    options,
+  });
+
+  expect(sources[0].criteria.includeTransactionReceipts).toBe(true);
+  expect(sources[1].criteria.includeTransactionReceipts).toBe(false);
+});
+
 test("buildConfigAndIndexingFunctions() coerces NaN endBlock to undefined", async () => {
   const config = createConfig({
     networks: {
diff --git a/packages/core/src/config/factories.test.ts b/packages/core/src/config/factories.test.ts
index 6ae8d36a..5e907b1c 100644
--- a/packages/core/src/config/factories.test.ts
+++ b/packages/core/src/config/factories.test.ts
@@ -13,6 +13,7 @@ test("buildFactoryCriteria throws if provided parameter not found in inputs", ()
       address: "0xa",
       event: llamaFactoryEventAbiItem,
       parameter: "fakeParameter",
+      includeTransactionReceipts: false,
     }),
   ).toThrowError(
     "Factory event parameter not found in factory event signature. Got 'fakeParameter', expected one of ['deployer', 'name', 'llamaCore', 'llamaExecutor', 'llamaPolicy', 'chainId'].",
@@ -24,6 +25,7 @@ test("buildFactoryCriteria handles LlamaInstanceCreated llamaCore", () => {
     address: "0xa",
     event: llamaFactoryEventAbiItem,
     parameter: "llamaCore",
+    includeTransactionReceipts: false,
   });
 
   expect(criteria).toMatchObject({
@@ -38,6 +40,7 @@ test("buildFactoryCriteria handles LlamaInstanceCreated llamaPolicy", () => {
     address: "0xa",
     event: llamaFactoryEventAbiItem,
     parameter: "llamaPolicy",
+    includeTransactionReceipts: false,
   });
 
   expect(criteria).toMatchObject({
diff --git a/packages/core/src/indexing/service.test.ts b/packages/core/src/indexing/service.test.ts
index 31aee100..616212f5 100644
--- a/packages/core/src/indexing/service.test.ts
+++ b/packages/core/src/indexing/service.test.ts
@@ -224,6 +224,7 @@ test("processEvent() log events", async (context) => {
       log: expect.any(Object),
       block: expect.any(Object),
       transaction: expect.any(Object),
+      transactionReceipt: expect.any(Object),
     },
     context: {
       network: { chainId: 1, name: "mainnet" },
diff --git a/packages/core/src/sync-historical/service.test.ts b/packages/core/src/sync-historical/service.test.ts
index 331bb143..03fc9e44 100644
--- a/packages/core/src/sync-historical/service.test.ts
+++ b/packages/core/src/sync-historical/service.test.ts
@@ -52,6 +52,38 @@ test("start() with log filter inserts log filter interval records", async (conte
   await cleanup();
 });
 
+test("start() inserts transaction receipts", async (context) => {
+  const { common, networks, requestQueues, sources } = context;
+  const { syncStore, cleanup } = await setupDatabaseServices(context);
+
+  const blockNumbers = await getBlockNumbers();
+  const service = new HistoricalSyncService({
+    common,
+    syncStore,
+    network: networks[0],
+    requestQueue: requestQueues[0],
+    sources: [
+      {
+        ...sources[0],
+        criteria: { ...sources[0].criteria, includeTransactionReceipts: true },
+      },
+    ],
+  });
+  await service.setup(blockNumbers);
+  service.start();
+  await service.onIdle();
+
+  const transactionReceipts = await syncStore.db
+    .selectFrom("transactionReceipts")
+    .selectAll()
+    .execute();
+  expect(transactionReceipts).toHaveLength(2);
+
+  service.kill();
+  await service.onIdle();
+  await cleanup();
+});
+
 test("start() with factory contract inserts log filter and factory log filter interval records", async (context) => {
   const { common, networks, requestQueues, sources } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
@@ -73,6 +105,7 @@ test("start() with factory contract inserts log filter and factory log filter in
     logFilter: {
       address: sources[1].criteria.address,
       topics: [sources[1].criteria.eventSelector],
+      includeTransactionReceipts: false,
     },
   });
 
@@ -294,6 +327,7 @@ test("start() retries unexpected error in log filter task", async (context) => {
   expect(logFilterIntervals).toMatchObject([
     [0, blockNumbers.finalizedBlockNumber],
   ]);
+  // 2 logs + 2 blocks
   expect(rpcRequestSpy).toHaveBeenCalledTimes(4);
 
   service.kill();
diff --git a/packages/core/src/sync-realtime/service.test.ts b/packages/core/src/sync-realtime/service.test.ts
index 410f2a19..4c15e2b2 100644
--- a/packages/core/src/sync-realtime/service.test.ts
+++ b/packages/core/src/sync-realtime/service.test.ts
@@ -6,6 +6,7 @@ import {
   setupIsolatedDatabase,
 } from "@/_test/setup.js";
 import { testClient } from "@/_test/utils.js";
+import type { EventSource } from "@/config/sources.js";
 import { type SyncBlock, _eth_getBlockByNumber } from "@/sync/index.js";
 import { maxCheckpoint } from "@/utils/checkpoint.js";
 import { getAbiItem, getEventSelector } from "viem";
@@ -410,6 +411,56 @@ test("handleBlock() ingests block and logs", async (context) => {
   await cleanup();
 });
 
+test("handleBlock() gets receipts", async (context) => {
+  const { common, networks, requestQueues, sources, erc20, factory } = context;
+  const { syncStore, cleanup } = await setupDatabaseServices(context);
+
+  const finalizedBlock = await requestQueues[0].request({
+    method: "eth_getBlockByNumber",
+    params: ["0x0", false],
+  });
+
+  const realtimeSyncService = create({
+    common,
+    syncStore,
+    network: networks[0],
+    requestQueue: requestQueues[0],
+    sources: sources.map(
+      (source) =>
+        ({
+          ...source,
+          criteria: { ...source.criteria, includeTransactionReceipts: true },
+        }) as EventSource,
+    ),
+    finalizedBlock: finalizedBlock as SyncBlock,
+    onEvent: vi.fn(),
+    onFatalError: vi.fn(),
+  });
+
+  for (let i = 1; i <= 4; i++) {
+    await handleBlock(realtimeSyncService, {
+      newHeadBlock: await _eth_getBlockByNumber(
+        { requestQueue: requestQueues[0] },
+        { blockNumber: i },
+      ),
+    });
+  }
+
+  const transactionReceipts = await syncStore.db
+    .selectFrom("transactionReceipts")
+    .selectAll()
+    .execute();
+
+  expect(transactionReceipts).toHaveLength(3);
+  expect(transactionReceipts[0].to).toBe(erc20.address);
+  expect(transactionReceipts[1].to).toBe(erc20.address);
+  expect(transactionReceipts[2].to).toBe(factory.pair);
+
+  await kill(realtimeSyncService);
+
+  await cleanup();
+});
+
 test("handleBlock() skips eth_getLogs request", async (context) => {
   const { common, networks, requestQueues, sources } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
diff --git a/packages/core/src/sync-store/store.test.ts b/packages/core/src/sync-store/store.test.ts
index 3770d8b0..01fdbd88 100644
--- a/packages/core/src/sync-store/store.test.ts
+++ b/packages/core/src/sync-store/store.test.ts
@@ -48,7 +48,7 @@ test("setup creates tables", async (context) => {
   await cleanup();
 });
 
-test("insertLogFilterInterval inserts block, transactions, and logs", async (context) => {
+test("insertLogFilterInterval inserts block, transactions, receipts, and logs", async (context) => {
   const { erc20, sources } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
   const rpcData = await getRawRPCData(sources);
@@ -56,7 +56,11 @@ test("insertLogFilterInterval inserts block, transactions, and logs", async (con
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block1,
     interval: {
       startBlock: blockNumber - 3n,
@@ -73,6 +77,12 @@ test("insertLogFilterInterval inserts block, transactions, and logs", async (con
     .execute();
   expect(transactions).toHaveLength(2);
 
+  const transactionReceipts = await syncStore.db
+    .selectFrom("transactionReceipts")
+    .selectAll()
+    .execute();
+  expect(transactionReceipts).toHaveLength(2);
+
   const logs = await syncStore.db.selectFrom("logs").selectAll().execute();
   expect(logs).toHaveLength(2);
   await cleanup();
@@ -86,7 +96,11 @@ test("insertLogFilterInterval updates sync db metrics", async (context) => {
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block1,
     interval: {
       startBlock: blockNumber - 3n,
@@ -116,6 +130,7 @@ test("insertLogFilterInterval inserts log filter intervals", async (context) =>
     logFilter: {
       address: ["0xa", "0xb"],
       topics: [["0xc", "0xd"], null, "0xe", null],
+      includeTransactionReceipts: false,
     },
     ...rpcData.block1,
     interval: { startBlock: 0n, endBlock: 100n },
@@ -126,6 +141,7 @@ test("insertLogFilterInterval inserts log filter intervals", async (context) =>
     logFilter: {
       address: ["0xa", "0xb"],
       topics: [["0xc", "0xd"], null, "0xe", null],
+      includeTransactionReceipts: false,
     },
   });
 
@@ -140,7 +156,11 @@ test("insertLogFilterInterval merges ranges on insertion", async (context) => {
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block1,
     interval: {
       startBlock: hexToBigInt(rpcData.block1.block.number!),
@@ -150,7 +170,11 @@ test("insertLogFilterInterval merges ranges on insertion", async (context) => {
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block3,
     interval: {
       startBlock: hexToBigInt(rpcData.block3.block.number!),
@@ -160,7 +184,11 @@ test("insertLogFilterInterval merges ranges on insertion", async (context) => {
 
   let logFilterRanges = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
   });
 
   expect(logFilterRanges).toMatchObject([
@@ -176,7 +204,11 @@ test("insertLogFilterInterval merges ranges on insertion", async (context) => {
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block2,
     interval: {
       startBlock: hexToBigInt(rpcData.block2.block.number!),
@@ -186,7 +218,11 @@ test("insertLogFilterInterval merges ranges on insertion", async (context) => {
 
   logFilterRanges = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
   });
 
   expect(logFilterRanges).toMatchObject([
@@ -206,7 +242,11 @@ test("insertLogFilterInterval merges log intervals inserted concurrently", async
   await Promise.all([
     syncStore.insertLogFilterInterval({
       chainId: 1,
-      logFilter: { address: erc20.address, topics: [] },
+      logFilter: {
+        address: erc20.address,
+        topics: [],
+        includeTransactionReceipts: false,
+      },
       ...rpcData.block1,
       interval: {
         startBlock: hexToBigInt(rpcData.block1.block.number!),
@@ -215,7 +255,11 @@ test("insertLogFilterInterval merges log intervals inserted concurrently", async
     }),
     syncStore.insertLogFilterInterval({
       chainId: 1,
-      logFilter: { address: erc20.address, topics: [] },
+      logFilter: {
+        address: erc20.address,
+        topics: [],
+        includeTransactionReceipts: false,
+      },
       ...rpcData.block2,
       interval: {
         startBlock: hexToBigInt(rpcData.block2.block.number!),
@@ -224,7 +268,11 @@ test("insertLogFilterInterval merges log intervals inserted concurrently", async
     }),
     syncStore.insertLogFilterInterval({
       chainId: 1,
-      logFilter: { address: erc20.address, topics: [] },
+      logFilter: {
+        address: erc20.address,
+        topics: [],
+        includeTransactionReceipts: false,
+      },
       ...rpcData.block3,
       interval: {
         startBlock: hexToBigInt(rpcData.block3.block.number!),
@@ -235,7 +283,11 @@ test("insertLogFilterInterval merges log intervals inserted concurrently", async
 
   const logFilterRanges = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
   });
 
   expect(logFilterRanges).toMatchObject([
@@ -263,7 +315,11 @@ test("insertLogFilterInterval updates log checkpoints on conflict", async (conte
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block2,
     interval: {
       startBlock: hexToBigInt(rpcData.block2.block.number!),
@@ -288,6 +344,7 @@ test("getLogFilterIntervals respects log filter inclusivity rules", async (conte
     logFilter: {
       address: ["0xa", "0xb"],
       topics: [["0xc", "0xd"], null, "0xe", null],
+      includeTransactionReceipts: false,
     },
     ...rpcData.block1,
     interval: { startBlock: 0n, endBlock: 100n },
@@ -296,7 +353,11 @@ test("getLogFilterIntervals respects log filter inclusivity rules", async (conte
   // This is a narrower inclusion criteria on `address` and `topic0`. Full range is available.
   let logFilterRanges = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { address: ["0xa"], topics: [["0xc"], null, "0xe", null] },
+    logFilter: {
+      address: ["0xa"],
+      topics: [["0xc"], null, "0xe", null],
+      includeTransactionReceipts: false,
+    },
   });
 
   expect(logFilterRanges).toMatchObject([[0, 100]]);
@@ -304,7 +365,11 @@ test("getLogFilterIntervals respects log filter inclusivity rules", async (conte
   // This is a broader inclusion criteria on `address`. No ranges available.
   logFilterRanges = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { address: undefined, topics: [["0xc"], null, "0xe", null] },
+    logFilter: {
+      address: undefined,
+      topics: [["0xc"], null, "0xe", null],
+      includeTransactionReceipts: false,
+    },
   });
 
   expect(logFilterRanges).toMatchObject([]);
@@ -312,7 +377,11 @@ test("getLogFilterIntervals respects log filter inclusivity rules", async (conte
   // This is a narrower inclusion criteria on `topic1`. Full range available.
   logFilterRanges = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { address: ["0xa"], topics: [["0xc"], "0xd", "0xe", null] },
+    logFilter: {
+      address: ["0xa"],
+      topics: [["0xc"], "0xd", "0xe", null],
+      includeTransactionReceipts: false,
+    },
   });
 
   expect(logFilterRanges).toMatchObject([[0, 100]]);
@@ -326,14 +395,17 @@ test("getLogFilterRanges handles complex log filter inclusivity rules", async (c
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { topics: [] },
+    logFilter: { topics: [], includeTransactionReceipts: false },
     ...rpcData.block1,
     interval: { startBlock: 0n, endBlock: 100n },
   });
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { topics: [null, ["0xc", "0xd"], null, null] },
+    logFilter: {
+      topics: [null, ["0xc", "0xd"], null, null],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block1,
     interval: { startBlock: 150n, endBlock: 250n },
   });
@@ -341,14 +413,17 @@ test("getLogFilterRanges handles complex log filter inclusivity rules", async (c
   // Broad criteria only includes broad intervals.
   let logFilterIntervals = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { topics: [] },
+    logFilter: { topics: [], includeTransactionReceipts: false },
   });
   expect(logFilterIntervals).toMatchObject([[0, 100]]);
 
   // Narrower criteria includes both broad and specific intervals.
   logFilterIntervals = await syncStore.getLogFilterIntervals({
     chainId: 1,
-    logFilter: { topics: [null, "0xc", null, null] },
+    logFilter: {
+      topics: [null, "0xc", null, null],
+      includeTransactionReceipts: false,
+    },
   });
   expect(logFilterIntervals).toMatchObject([
     [0, 100],
@@ -364,7 +439,10 @@ test("getLogFilterIntervals merges overlapping intervals that both match a filte
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { topics: [["0xc", "0xd"], null, null, null] },
+    logFilter: {
+      topics: [["0xc", "0xd"], null, null, null],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block1,
     interval: { startBlock: 0n, endBlock: 50n },
   });
@@ -375,6 +453,7 @@ test("getLogFilterIntervals merges overlapping intervals that both match a filte
     logFilter: {
       address: "0xaddress",
       topics: [["0xc"], null, null, null],
+      includeTransactionReceipts: false,
     },
   });
   expect(logFilterIntervals).toMatchObject([[0, 50]]);
@@ -384,6 +463,7 @@ test("getLogFilterIntervals merges overlapping intervals that both match a filte
     logFilter: {
       address: "0xaddress",
       topics: [["0xc"], null, null, null],
+      includeTransactionReceipts: false,
     },
     ...rpcData.block1,
     interval: { startBlock: 0n, endBlock: 100n },
@@ -394,6 +474,7 @@ test("getLogFilterIntervals merges overlapping intervals that both match a filte
     logFilter: {
       address: "0xaddress",
       topics: [["0xc"], null, null, null],
+      includeTransactionReceipts: false,
     },
   });
 
@@ -402,6 +483,78 @@ test("getLogFilterIntervals merges overlapping intervals that both match a filte
   await cleanup();
 });
 
+test("getLogFilterIntervals handles includeTransactionReceipts", async (context) => {
+  const { sources } = context;
+  const { syncStore, cleanup } = await setupDatabaseServices(context);
+  const rpcData = await getRawRPCData(sources);
+
+  await syncStore.insertLogFilterInterval({
+    chainId: 1,
+    logFilter: {
+      topics: ["0x0"],
+      includeTransactionReceipts: false,
+    },
+    ...rpcData.block1,
+    interval: { startBlock: 0n, endBlock: 100n },
+  });
+
+  // This is an exact match on `includeTransactionReceipts`. Full range is available.
+  let logFilterRanges = await syncStore.getLogFilterIntervals({
+    chainId: 1,
+    logFilter: {
+      topics: ["0x0"],
+      includeTransactionReceipts: false,
+    },
+  });
+
+  expect(logFilterRanges).toMatchObject([[0, 100]]);
+
+  // This is a broader inclusion criteria on `includeTransactionReceipts`. No ranges available.
+  logFilterRanges = await syncStore.getLogFilterIntervals({
+    chainId: 1,
+    logFilter: {
+      topics: ["0x0"],
+      includeTransactionReceipts: true,
+    },
+  });
+
+  expect(logFilterRanges).toMatchObject([]);
+
+  await syncStore.insertLogFilterInterval({
+    chainId: 1,
+    logFilter: {
+      topics: ["0x1"],
+      includeTransactionReceipts: true,
+    },
+    ...rpcData.block1,
+    interval: { startBlock: 0n, endBlock: 100n },
+  });
+
+  // This is an exact match on `includeTransactionReceipts`. Full range is available.
+  logFilterRanges = await syncStore.getLogFilterIntervals({
+    chainId: 1,
+    logFilter: {
+      topics: ["0x1"],
+      includeTransactionReceipts: true,
+    },
+  });
+
+  expect(logFilterRanges).toMatchObject([[0, 100]]);
+
+  // This is a broader inclusion criteria on `includeTransactionReceipts`. Full range is available.
+  logFilterRanges = await syncStore.getLogFilterIntervals({
+    chainId: 1,
+    logFilter: {
+      topics: ["0x1"],
+      includeTransactionReceipts: false,
+    },
+  });
+
+  expect(logFilterRanges).toMatchObject([[0, 100]]);
+
+  await cleanup();
+});
+
 test("insertFactoryChildAddressLogs inserts logs", async (context) => {
   const { sources } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
@@ -428,6 +581,7 @@ test("getFactoryChildAddresses gets child addresses for topic location", async (
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryChildAddressLogs({
@@ -499,6 +653,7 @@ test("getFactoryChildAddresses gets child addresses for offset location", async
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "offset32",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryChildAddressLogs({
@@ -553,6 +708,7 @@ test("getFactoryChildAddresses respects toBlock argument", async (context) => {
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryChildAddressLogs({
@@ -619,6 +775,7 @@ test("getFactoryChildAddresses paginates correctly", async (context) => {
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryChildAddressLogs({
@@ -733,6 +890,7 @@ test("getFactoryChildAddresses does not yield empty list", async (context) => {
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   const iterator = syncStore.getFactoryChildAddresses({
@@ -751,7 +909,7 @@ test("getFactoryChildAddresses does not yield empty list", async (context) => {
   await cleanup();
 });
 
-test("insertFactoryLogFilterInterval inserts block, transactions, and logs", async (context) => {
+test("insertFactoryLogFilterInterval inserts block, transactions, receipts and logs", async (context) => {
   const { sources } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
   const rpcData = await getRawRPCData(sources);
@@ -762,6 +920,7 @@ test("insertFactoryLogFilterInterval inserts block, transactions, and logs", asy
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryLogFilterInterval({
@@ -780,6 +939,12 @@ test("insertFactoryLogFilterInterval inserts block, transactions, and logs", asy
     .execute();
   expect(transactions).toHaveLength(2);
 
+  const transactionReceipts = await syncStore.db
+    .selectFrom("transactionReceipts")
+    .selectAll()
+    .execute();
+  expect(transactionReceipts).toHaveLength(2);
+
   const logs = await syncStore.db.selectFrom("logs").selectAll().execute();
   expect(logs).toHaveLength(2);
   await cleanup();
@@ -796,6 +961,7 @@ test("insertFactoryLogFilterInterval inserts and merges child contract intervals
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryLogFilterInterval({
@@ -849,6 +1015,7 @@ test("getFactoryLogFilterIntervals handles topic filtering rules", async (contex
       "0x0000000000000000000000000000000000000000000factoryeventsignature",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertFactoryLogFilterInterval({
@@ -882,6 +1049,74 @@ test("getFactoryLogFilterIntervals handles topic filtering rules", async (contex
   await cleanup();
 });
 
+test("getFactoryLogFilterIntervals handles includeTransactionReceipts", async (context) => {
+  const { sources } = context;
+  const { syncStore, cleanup } = await setupDatabaseServices(context);
+  const rpcData = await getRawRPCData(sources);
+
+  const factoryCriteria: FactoryCriteria = {
+    address: "0xfactory",
+    eventSelector:
+      "0x0000000000000000000000000000000000000000000factoryeventsignature",
+    childAddressLocation: "topic1",
+    topics: ["0x0"],
+    includeTransactionReceipts: false,
+  };
+
+  await syncStore.insertFactoryLogFilterInterval({
+    chainId: 1,
+    factory: factoryCriteria,
+    ...rpcData.block1,
+    interval: { startBlock: 0n, endBlock: 500n },
+  });
+
+  let intervals = await syncStore.getFactoryLogFilterIntervals({
+    chainId: 1,
+    factory: factoryCriteria,
+  });
+
+  expect(intervals).toMatchObject([[0, 500]]);
+
+  intervals = await syncStore.getFactoryLogFilterIntervals({
+    chainId: 1,
+    factory: {
+      ...factoryCriteria,
+      includeTransactionReceipts: true,
+    } as FactoryCriteria,
+  });
+
+  expect(intervals).toMatchObject([]);
+
+  factoryCriteria.includeTransactionReceipts = true;
+  factoryCriteria.topics = ["0x1"];
+
+  await syncStore.insertFactoryLogFilterInterval({
+    chainId: 1,
+    factory: factoryCriteria,
+    ...rpcData.block1,
+    interval: { startBlock: 0n, endBlock: 500n },
+  });
+
+  intervals = await syncStore.getFactoryLogFilterIntervals({
+    chainId: 1,
+    factory: factoryCriteria,
+  });
+
+  expect(intervals).toMatchObject([[0, 500]]);
+
+  intervals = await syncStore.getFactoryLogFilterIntervals({
+    chainId: 1,
+    factory: {
+      ...factoryCriteria,
+      includeTransactionReceipts: false,
+    } as FactoryCriteria,
+  });
+
+  expect(intervals).toMatchObject([[0, 500]]);
+
+  await cleanup();
+});
+
 test("insertRealtimeBlock inserts data", async (context) => {
   const { sources } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
@@ -901,6 +1136,12 @@ test("insertRealtimeBlock inserts data", async (context) => {
     .execute();
   expect(transactions).toHaveLength(2);
 
+  const transactionReceipts = await syncStore.db
+    .selectFrom("logs")
+    .selectAll()
+    .execute();
+  expect(transactionReceipts).toHaveLength(2);
+
   const logs = await syncStore.db.selectFrom("logs").selectAll().execute();
   expect(logs).toHaveLength(2);
   await cleanup();
@@ -952,6 +1193,7 @@ test("insertRealtimeInterval inserts log filter intervals", async (context) => {
   const logFilterCriteria = {
     address: erc20.address,
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies LogFilterCriteria;
 
   const factoryCriteriaOne = {
@@ -959,6 +1201,7 @@ test("insertRealtimeInterval inserts log filter intervals", async (context) => {
     eventSelector: "0xa",
     childAddressLocation: "topic1",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   const factoryCriteriaTwo = {
@@ -966,6 +1209,7 @@ test("insertRealtimeInterval inserts log filter intervals", async (context) => {
     eventSelector: "0xa",
     childAddressLocation: "offset64",
     topics: [],
+    includeTransactionReceipts: false,
   } satisfies FactoryCriteria;
 
   await syncStore.insertRealtimeInterval({
@@ -989,6 +1233,7 @@ test("insertRealtimeInterval inserts log filter intervals", async (context) => {
       logFilter: {
         address: factoryCriteriaOne.address,
         topics: [factoryCriteriaOne.eventSelector, null, null, null],
+        includeTransactionReceipts: false,
       },
     }),
   ).toMatchObject([[500, 550]]);
@@ -998,6 +1243,7 @@ test("insertRealtimeInterval inserts log filter intervals", async (context) => {
       logFilter: {
         address: factoryCriteriaOne.address,
         topics: [factoryCriteriaOne.eventSelector, null, null, null],
+        includeTransactionReceipts: false,
       },
     }),
   ).toMatchObject([[500, 550]]);
@@ -1025,7 +1271,11 @@ test("deleteRealtimeData deletes logs", async (context) => {
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block1,
     interval: {
       startBlock: hexToBigInt(rpcData.block1.block.number!),
@@ -1035,7 +1285,11 @@ test("deleteRealtimeData deletes logs", async (context) => {
 
   await syncStore.insertLogFilterInterval({
     chainId: 1,
-    logFilter: { address: erc20.address, topics: [] },
+    logFilter: {
+      address: erc20.address,
+      topics: [],
+      includeTransactionReceipts: false,
+    },
     ...rpcData.block2,
     interval: {
       startBlock: hexToBigInt(rpcData.block2.block.number!),
@@ -1197,14 +1451,72 @@ test("getLogEvents returns log events", async (context) => {
   expect(events[0].log.address).toBe(checksumAddress(erc20.address));
   expect(events[0].block.hash).toBe(rpcData.block1.block.hash);
   expect(events[0].transaction.hash).toBe(rpcData.block1.transactions[0].hash);
+  expect(events[0].transactionReceipt).toBeUndefined();
 
   expect(events[1].log.address).toBe(checksumAddress(erc20.address));
   expect(events[1].block.hash).toBe(rpcData.block1.block.hash);
   expect(events[1].transaction.hash).toBe(rpcData.block1.transactions[1].hash);
+  expect(events[1].transactionReceipt).toBeUndefined();
 
   expect(events[2].log.address).toBe(checksumAddress(factory.pair));
   expect(events[2].block.hash).toBe(rpcData.block3.block.hash);
   expect(events[2].transaction.hash).toBe(rpcData.block3.transactions[0].hash);
+  expect(events[2].transactionReceipt).toBeUndefined();
+
+  await cleanup();
+});
+
+test("getLogEvents returns log events with receipts", async (context) => {
+  const { erc20, sources, factory } = context;
+  const { syncStore, cleanup } = await setupDatabaseServices(context);
+  const rpcData = await getRawRPCData(sources);
+
+  await syncStore.insertRealtimeBlock({
+    chainId: 1,
+    ...rpcData.block1,
+  });
+  await syncStore.insertRealtimeBlock({
+    chainId: 1,
+    ...rpcData.block2,
+  });
+  await syncStore.insertRealtimeBlock({
+    chainId: 1,
+    ...rpcData.block3,
+  });
+
+  const ag = syncStore.getLogEvents({
+    sources: sources.map((s) => ({
+      ...s,
+      criteria: { ...s.criteria, includeTransactionReceipts: true },
+    })),
+    fromCheckpoint: zeroCheckpoint,
+    toCheckpoint: maxCheckpoint,
+    limit: 100,
+  });
+  const events = await drainAsyncGenerator(ag);
+
+  expect(events).toHaveLength(3);
+
+  expect(events[0].log.address).toBe(checksumAddress(erc20.address));
+  expect(events[0].block.hash).toBe(rpcData.block1.block.hash);
+  expect(events[0].transaction.hash).toBe(rpcData.block1.transactions[0].hash);
+  expect(events[0].transactionReceipt?.transactionHash).toBe(
+    rpcData.block1.transactionReceipts[0].transactionHash,
+  );
+
+  expect(events[1].log.address).toBe(checksumAddress(erc20.address));
+  expect(events[1].block.hash).toBe(rpcData.block1.block.hash);
+  expect(events[1].transaction.hash).toBe(rpcData.block1.transactions[1].hash);
+  expect(events[1].transactionReceipt?.transactionHash).toBe(
+    rpcData.block1.transactionReceipts[1].transactionHash,
+  );
+
+  expect(events[2].log.address).toBe(checksumAddress(factory.pair));
+  expect(events[2].block.hash).toBe(rpcData.block3.block.hash);
+  expect(events[2].transaction.hash).toBe(rpcData.block3.transactions[0].hash);
+  expect(events[2].transactionReceipt?.transactionHash).toBe(
+    rpcData.block3.transactionReceipts[0].transactionHash,
+  );
 
   await cleanup();
 });
@@ -1231,6 +1543,7 @@ test("getLogEvents filters on log filter with multiple addresses", async (contex
         criteria: {
           address: [erc20.address, factory.pair],
           topics: [],
+          includeTransactionReceipts: false,
         },
       },
     ],
@@ -1279,6 +1592,7 @@ test("getLogEvents filters on log filter with single topic", async (context) =>
         ...sources[0],
         criteria: {
           topics: [transferSelector, null, null, null],
+          includeTransactionReceipts: false,
         },
       },
     ],
@@ -1332,6 +1646,7 @@ test("getLogEvents filters on log filter with multiple topics", async (context)
             null,
             null,
           ],
+          includeTransactionReceipts: false,
         },
       },
     ],
@@ -1369,6 +1684,7 @@ test("getLogEvents filters on simple factory", async (context) => {
     block: rpcData.block3.block,
     transactions: rpcData.block3.transactions,
     logs: rpcData.block3.logs,
+    transactionReceipts: rpcData.block3.transactionReceipts,
   });
 
   const ag = syncStore.getLogEvents({
@@ -1586,6 +1902,7 @@ test("getLogEvents event filter on factory", async (context) => {
     block: rpcData.block3.block,
     transactions: rpcData.block3.transactions,
     logs: rpcData.block3.logs,
+    transactionReceipts: [],
   });
 
   const ag = syncStore.getLogEvents({
diff --git a/packages/core/src/types/virtual.test-d.ts b/packages/core/src/types/virtual.test-d.ts
index 623466b5..3f738fb3 100644
--- a/packages/core/src/types/virtual.test-d.ts
+++ b/packages/core/src/types/virtual.test-d.ts
@@ -2,7 +2,7 @@ import { createConfig } from "@/config/config.js";
 import { createSchema, createTable } from "@/schema/schema.js";
 import { http, type Abi, type Hex, parseAbiItem } from "viem";
 import { assertType, test } from "vitest";
-import type { Block, Log, Transaction } from "./eth.js";
+import type { Block, Log, Transaction, TransactionReceipt } from "./eth.js";
 import type { DatabaseModel } from "./model.js";
 import type { Virtual } from "./virtual.js";
 
@@ -37,6 +37,7 @@ const config = createConfig({
       network: "mainnet",
       address: "0x",
       startBlock: 0,
+      includeTransactionReceipts: false,
     },
     c2: {
       abi: [event1, event1Overloaded],
@@ -44,6 +45,7 @@ const config = createConfig({
       network: {
         mainnet: {
           startBlock: 1,
+          includeTransactionReceipts: true,
         },
         optimism: {},
       },
@@ -233,6 +235,23 @@ test("Event", () => {
   assertType<expectedEvent>({} as any as a);
 });
 
+test("Event transaction receipt", () => {
+  type a = Virtual.Event<typeof config, "c2:Event1()">;
+  //   ^?
+
+  type expectedEvent = {
+    name: "Event1()";
+    args: readonly [];
+    log: Log;
+    block: Block;
+    transaction: Transaction;
+    transactionReceipt?: TransactionReceipt;
+  };
+
+  assertType<a>({} as any as expectedEvent);
+  assertType<expectedEvent>({} as any as a);
+});
+
 test("Event with unnamed parameters", () => {
   type a = Virtual.Event<typeof config, "c2:Event1(bytes32)">;
   //   ^?
diff --git a/packages/core/src/utils/fragments.test.ts b/packages/core/src/utils/fragments.test.ts
index af16258b..2e72f915 100644
--- a/packages/core/src/utils/fragments.test.ts
+++ b/packages/core/src/utils/fragments.test.ts
@@ -1,8 +1,6 @@
+import { buildFactoryCriteria } from "@/config/factories.js";
 import { parseAbiItem } from "viem";
 import { expect, test } from "vitest";
-
-import { buildFactoryCriteria } from "@/config/factories.js";
-
 import { buildFactoryFragments, buildLogFilterFragments } from "./fragments.js";
 
 const llamaFactoryEventAbiItem = parseAbiItem(
@@ -13,16 +11,18 @@ test("buildLogFilterFragments generates 1 log filter fragment for null filter",
   const logFilterFragments = buildLogFilterFragments({
     chainId: 1,
     topics: [null, null, null, null],
+    includeTransactionReceipts: false,
   });
 
   expect(logFilterFragments).toMatchObject([
     {
-      id: "1_null_null_null_null_null",
+      id: "1_null_null_null_null_null_0",
       address: null,
       topic0: null,
       topic1: null,
       topic2: null,
       topic3: null,
+      includeTransactionReceipts: 0,
     },
   ]);
 });
@@ -32,16 +32,18 @@ test("buildLogFilterFragments generates 1 log filter fragment for simple filter"
     chainId: 1,
     address: "0xa",
     topics: [null, null, null, null],
+    includeTransactionReceipts: false,
   });
 
   expect(logFilterFragments).toMatchObject([
     {
-      id: "1_0xa_null_null_null_null",
+      id: "1_0xa_null_null_null_null_0",
       address: "0xa",
       topic0: null,
       topic1: null,
       topic2: null,
       topic3: null,
+      includeTransactionReceipts: 0,
     },
   ]);
 });
@@ -51,40 +53,45 @@ test("buildLogFilterFragments generates 4 log filter fragment for 2x2 filter", (
     chainId: 115511,
     address: ["0xa", "0xb"],
     topics: [["0xc", "0xd"], null, "0xe", null],
+    includeTransactionReceipts: false,
   });
 
   expect(logFilterFragments).toMatchObject([
     {
-      id: "115511_0xa_0xc_null_0xe_null",
+      id: "115511_0xa_0xc_null_0xe_null_0",
       address: "0xa",
       topic0: "0xc",
       topic1: null,
       topic2: "0xe",
       topic3: null,
+      includeTransactionReceipts: 0,
     },
     {
-      id: "115511_0xa_0xd_null_0xe_null",
+      id: "115511_0xa_0xd_null_0xe_null_0",
       address: "0xa",
       topic0: "0xd",
       topic1: null,
       topic2: "0xe",
       topic3: null,
+      includeTransactionReceipts: 0,
     },
     {
-      id: "115511_0xb_0xc_null_0xe_null",
+      id: "115511_0xb_0xc_null_0xe_null_0",
       address: "0xb",
       topic0: "0xc",
       topic1: null,
       topic2: "0xe",
       topic3: null,
+      includeTransactionReceipts: 0,
     },
     {
-      id: "115511_0xb_0xd_null_0xe_null",
+      id: "115511_0xb_0xd_null_0xe_null_0",
       address: "0xb",
       topic0: "0xd",
       topic1: null,
       topic2: "0xe",
       topic3: null,
+      includeTransactionReceipts: 0,
     },
   ]);
 });
@@ -94,16 +101,38 @@ test("buildLogFilterFragments generates 12 log filter fragment for 2x2x3 filter"
     chainId: 1,
     address: ["0xa", "0xb"],
     topics: [["0xc", "0xd"], null, ["0xe", "0xf", "0x1"], null],
+    includeTransactionReceipts: false,
   });
 
   expect(logFilterFragments.length).toBe(12);
 });
 
+test("buildLogFilterFragments includeTransactionReceipts", () => {
+  const logFilterFragments = buildLogFilterFragments({
+    chainId: 1,
+    topics: [null, null, null, null],
+    includeTransactionReceipts: true,
+  });
+
+  expect(logFilterFragments).toMatchObject([
+    {
+      id: "1_null_null_null_null_null_1",
+      address: null,
+      topic0: null,
+      topic1: null,
+      topic2: null,
+      topic3: null,
+      includeTransactionReceipts: 1,
+    },
+  ]);
+});
+
 test("buildFactoryFragments builds id containing topic", () => {
   const criteria = buildFactoryCriteria({
     address: "0xa",
     event: llamaFactoryEventAbiItem,
     parameter: "deployer",
+    includeTransactionReceipts: false,
   });
 
   expect(
@@ -113,7 +142,7 @@ test("buildFactoryFragments builds id containing topic", () => {
       ...criteria,
     })[0].id,
   ).toBe(
-    "1_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_topic1_null_null_null_null",
+    "1_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_topic1_null_null_null_null_0",
   );
 });
 
@@ -122,6 +151,7 @@ test("buildFactoryFragments builds id containing offset", () => {
     address: "0xa",
     event: llamaFactoryEventAbiItem,
     parameter: "llamaPolicy",
+    includeTransactionReceipts: false,
   });
 
   expect(
@@ -131,6 +161,6 @@ test("buildFactoryFragments builds id containing offset", () => {
       ...criteria,
     })[0].id,
   ).toBe(
-    "115511_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_offset64_null_null_null_null",
+    "115511_0xa_0x00fef2d461a2fabbb523f9f42752c61336f03b17a602af52cc6c83cb8b110599_offset64_null_null_null_null_0",
   );
 });