Repository: ponder-sh/ponder
Pull request #1227 (2024-11-05 22:42:24)

feat: `context.client` supports all rpc methods
Adds support for caching all available rpc methods. Several new methods are available off of `context.client`, including `simulateCall`, `getTransactionCount`, `getTransactionReceipt`, and `getBlockTransactionCount`.

cicadaweb3: Would be awesome to have this!

Patch:
diff --git a/.changeset/rotten-socks-agree.md b/.changeset/rotten-socks-agree.md
new file mode 100644
index 00000000..0fe5135b
--- /dev/null
+++ b/.changeset/rotten-socks-agree.md
@@ -0,0 +1,5 @@
+---
+"@ponder/core": patch
+---
+
+Added support for caching all available rpc methods. Several new client actions are available, including `context.client.simulateCall()`, `context.client.getTransactionCount()`, `context.client.getTransactionReceipt()`, and `context.client.getBlockTransactionCount()`. See [docs](https://ponder.sh/docs/indexing/read-contract-data#supported-actions) for all actions.
diff --git a/docs/pages/docs/indexing/read-contract-data.mdx b/docs/pages/docs/indexing/read-contract-data.mdx
index e2132b3b..aeb2a9ae 100644
--- a/docs/pages/docs/indexing/read-contract-data.mdx
+++ b/docs/pages/docs/indexing/read-contract-data.mdx
@@ -124,13 +124,29 @@ The custom client uses the transport you specify in `ponder.config.ts`.
 
 ### Supported actions
 
-| name         | description                                               | Viem docs                                                         |
-| :----------- | :-------------------------------------------------------- | :---------------------------------------------------------------- |
-| readContract | Returns the result of a read-only function on a contract. | [readContract](https://viem.sh/docs/contract/readContract.html)   |
-| multicall    | Similar to readContract, but batches requests.            | [multicall](https://viem.sh/docs/contract/multicall.html)         |
-| getBalance   | Returns the balance of an address in wei.                 | [getBalance](https://viem.sh/docs/actions/public/getBalance.html) |
-| getBytecode  | Returns the bytecode at an address.                       | [getBytecode](https://viem.sh/docs/contract/getBytecode.html)     |
-| getStorageAt | Returns the value from a storage slot at a given address. | [getStorageAt](https://viem.sh/docs/contract/getStorageAt.html)   |
+| name                        | description                                                                                         | Viem docs                                                                                        |
+| :-------------------------- | :-------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
+| readContract                | Returns the result of a read-only function on a contract.                                           | [readContract](https://viem.sh/docs/contract/readContract.html)                                  |
+| multicall                   | Similar to readContract, but batches requests.                                                      | [multicall](https://viem.sh/docs/contract/multicall.html)                                        |
+| simulateContract            | Simulates & validates a contract interaction.                                                       | [simulateContract](https://viem.sh/docs/contract/simulateContract)                               |
+| getBalance                  | Returns the balance of an address in wei.                                                           | [getBalance](https://viem.sh/docs/actions/public/getBalance.html)                                |
+| getBytecode                 | Returns the bytecode at an address.                                                                 | [getBytecode](https://viem.sh/docs/contract/getBytecode.html)                                    |
+| getStorageAt                | Returns the value from a storage slot at a given address.                                           | [getStorageAt](https://viem.sh/docs/contract/getStorageAt.html)                                  |
+| getBlock                    | Returns information about a block at a block number, hash or tag.                                   | [getBlock](https://viem.sh/docs/actions/public/getBlock)                                         |
+| getTransactionCount         | Returns the number of transactions an account has broadcast / sent.                                 | [getTransactionCount](https://viem.sh/docs/actions/public/getTransactionCount)                   |
+| getBlockTransactionCount    | Returns the number of Transactions at a block number, hash or tag.                                  | [getBlockTransactionCount](https://viem.sh/docs/actions/public/getBlockTransactionCount)         |
+| getTransaction              | Returns information about a transaction given a hash or block identifier.                           | [getTransaction](https://viem.sh/docs/actions/public/getTransaction)                             |
+| getTransactionReceipt       | Returns the transaction receipt given a transaction hash.                                           | [getTransactionReceipt](https://viem.sh/docs/actions/public/getTransactionReceipt)               |
+| getTransactionConfirmations | Returns the number of blocks passed (confirmations) since the transaction was processed on a block. | [getTransactionConfirmations](https://viem.sh/docs/actions/public/getTransactionConfirmations)   |
+| call                        | An Action for executing a new message call.                                                         | [call](https://viem.sh/docs/actions/public/call)                                                 |
+| estimateGas                 | An Action for estimating gas for a transaction.                                                     | [estimateGas](https://viem.sh/docs/actions/public/estimateGas)                                   |
+| getFeeHistory               | Returns a collection of historical gas information.                                                 | [getFeeHistory](https://viem.sh/docs/actions/public/getFeeHistory)                               |
+| getProof                    | Returns the account and storage values of the specified account including the Merkle-proof.         | [getProof](https://viem.sh/docs/actions/public/getProof)                                         |
+| getEnsAddress               | Gets address for ENS name.                                                                          | [getEnsAddress](https://viem.sh/docs/ens/actions/getEnsAddress)                                  |
+| getEnsAvatar                | Gets the avatar of an ENS name.                                                                     | [getEnsAvatar](https://viem.sh/docs/ens/actions/getEnsAvatar)                                    |
+| getEnsName                  | Gets primary name for specified address.                                                            | [getEnsName](https://viem.sh/docs/ens/actions/getEnsName)                                        |
+| getEnsResolver              | Gets resolver for ENS name.                                                                         | [getEnsResolver](https://viem.sh/docs/ens/actions/getEnsResolver)                                |
+| getEnsText                  | Gets a text record for specified ENS name.                                                          | [getEnsText](https://viem.sh/docs/ens/actions/getEnsText)                                        |
 
 ### Block number
 
diff --git a/packages/core/src/indexing/ponderActions.ts b/packages/core/src/indexing/ponderActions.ts
index 9b5b31a6..3dd6be22 100644
--- a/packages/core/src/indexing/ponderActions.ts
+++ b/packages/core/src/indexing/ponderActions.ts
@@ -1,37 +1,53 @@
 import type { Prettify } from "@/types/utils.js";
-import type {
-  Abi,
-  Account,
-  Chain,
-  Client,
-  ContractFunctionArgs,
-  ContractFunctionName,
-  GetBalanceParameters,
-  GetBalanceReturnType,
-  GetCodeParameters,
-  GetCodeReturnType,
-  GetEnsNameParameters,
-  GetEnsNameReturnType,
-  GetStorageAtParameters,
-  GetStorageAtReturnType,
-  MulticallParameters,
-  MulticallReturnType,
-  PublicRpcSchema,
-  ReadContractParameters,
-  ReadContractReturnType,
-  Transport,
-} from "viem";
 import {
-  getBalance as viemGetBalance,
-  getCode as viemGetCode,
-  getEnsName as viemGetEnsName,
-  getStorageAt as viemGetStorageAt,
-  multicall as viemMulticall,
-  readContract as viemReadContract,
-} from "viem/actions";
+  type Abi,
+  type Account,
+  type Address,
+  type Chain,
+  type Client,
+  type ContractFunctionArgs,
+  type ContractFunctionName,
+  type GetBlockReturnType,
+  type GetBlockTransactionCountReturnType,
+  type GetTransactionCountReturnType,
+  type Hash,
+  type MulticallParameters,
+  type MulticallReturnType,
+  type PublicActions,
+  type PublicRpcSchema,
+  type ReadContractParameters,
+  type ReadContractReturnType,
+  type SimulateContractParameters,
+  type SimulateContractReturnType,
+  type Transport,
+  publicActions,
+} from "viem";
 import type { Service } from "./service.js";
 
-export type BlockOptions =
+/** Viem actions where the `block` property is optional and implicit. */
+const blockDependentActions = [
+  "getBalance",
+  "call",
+  "estimateGas",
+  "getFeeHistory",
+  "getProof",
+  "getCode",
+  "getStorageAt",
+  "getEnsAddress",
+  "getEnsAvatar",
+  "getEnsName",
+  "getEnsResolver",
+  "getEnsText",
+] as const satisfies readonly (keyof ReturnType<typeof publicActions>)[];
+
+/** Viem actions where the `block` property is non-existent. */
+const nonBlockDependentActions = [
+  "getTransaction",
+  "getTransactionReceipt",
+  "getTransactionConfirmations",
+] as const satisfies readonly (keyof ReturnType<typeof publicActions>)[];
+
+type BlockOptions =
   | {
       cache?: undefined;
       blockNumber?: undefined;
@@ -45,17 +61,32 @@ export type BlockOptions =
       blockNumber: bigint;
     };
 
+type RequiredBlockOptions =
+  | {
+      /** Hash of the block. */
+      blockHash: Hash;
+      blockNumber?: undefined;
+    }
+  | {
+      blockHash?: undefined;
+      /** The block number. */
+      blockNumber: bigint;
+    };
+
+type BlockDependentAction<
+  fn extends (client: any, args: any) => unknown,
+  ///
+  params = Parameters<fn>[0],
+  returnType = ReturnType<fn>,
+> = (
+  args: Omit<params, "blockTag" | "blockNumber"> & BlockOptions,
+) => returnType;
+
 export type PonderActions = {
-  getBalance: (
-    args: Omit<GetBalanceParameters, "blockTag" | "blockNumber"> & BlockOptions,
-  ) => Promise<GetBalanceReturnType>;
-  getCode: (
-    args: Omit<GetCodeParameters, "blockTag" | "blockNumber"> & BlockOptions,
-  ) => Promise<GetCodeReturnType>;
-  getStorageAt: (
-    args: Omit<GetStorageAtParameters, "blockTag" | "blockNumber"> &
-      BlockOptions,
-  ) => Promise<GetStorageAtReturnType>;
+  [action in (typeof blockDependentActions)[number]]: BlockDependentAction<
+    ReturnType<typeof publicActions>[action]
+  >;
+} & {
   multicall: <
     const contracts extends readonly unknown[],
     allowFailure extends boolean = true,
@@ -77,19 +108,60 @@ export type PonderActions = {
     > &
       BlockOptions,
   ) => Promise<ReadContractReturnType<abi, functionName, args>>;
-  getEnsName: (
-    args: Omit<GetEnsNameParameters, "blockTag" | "blockNumber"> & BlockOptions,
-  ) => Promise<GetEnsNameReturnType>;
-};
+  simulateContract: <
+    const abi extends Abi | readonly unknown[],
+    functionName extends ContractFunctionName<abi, "nonpayable" | "payable">,
+    const args extends ContractFunctionArgs<
+      abi,
+      "nonpayable" | "payable",
+      functionName
+    >,
+  >(
+    args: Omit<
+      SimulateContractParameters<abi, functionName, args>,
+      "blockTag" | "blockNumber"
+    > &
+      BlockOptions,
+  ) => Promise<SimulateContractReturnType<abi, functionName, args>>;
+  getBlock: <includeTransactions extends boolean = false>(
+    args: {
+      /** Whether or not to include transaction data in the response. */
+      includeTransactions?: includeTransactions | undefined;
+    } & RequiredBlockOptions,
+  ) => Promise<GetBlockReturnType<Chain | undefined, includeTransactions>>;
+  getTransactionCount: (
+    args: {
+      /** The account address. */
+      address: Address;
+    } & RequiredBlockOptions,
+  ) => Promise<GetTransactionCountReturnType>;
+  getBlockTransactionCount: (
+    args: RequiredBlockOptions,
+  ) => Promise<GetBlockTransactionCountReturnType>;
+} & Pick<PublicActions, (typeof nonBlockDependentActions)[number]>;
 
 export type ReadOnlyClient<
   transport extends Transport = Transport,
   chain extends Chain | undefined = Chain | undefined,
 > = Prettify<
-  Client<transport, chain, undefined, PublicRpcSchema, PonderActions>
+  Omit<
+    Client<transport, chain, undefined, PublicRpcSchema, PonderActions>,
+    | "extend"
+    | "key"
+    | "batch"
+    | "cacheTime"
+    | "account"
+    | "type"
+    | "uid"
+    | "chain"
+    | "name"
+    | "pollingInterval"
+    | "transport"
+    | "ccipRead"
+  >
 >;
 
-export const buildCachedActions = (
+export const getPonderActions = (
   contextState: Pick<Service["currentEvent"]["contextState"], "blockNumber">,
 ) => {
   return <
@@ -98,96 +170,58 @@ export const buildCachedActions = (
     TAccount extends Account | undefined = Account | undefined,
   >(
     client: Client<TTransport, TChain, TAccount>,
-  ): PonderActions => ({
-    getBalance: ({
-      cache,
-      blockNumber: userBlockNumber,
-      ...args
-    }: Omit<GetBalanceParameters, "blockTag" | "blockNumber"> &
-      BlockOptions): Promise<GetBalanceReturnType> =>
-      viemGetBalance(client, {
-        ...args,
-        ...(cache === "immutable"
-          ? { blockTag: "latest" }
-          : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
-      }),
-    getCode: ({
-      cache,
-      blockNumber: userBlockNumber,
-      ...args
-    }: Omit<GetCodeParameters, "blockTag" | "blockNumber"> &
-      BlockOptions): Promise<GetCodeReturnType> =>
-      viemGetCode(client, {
-        ...args,
-        ...(cache === "immutable"
-          ? { blockTag: "latest" }
-          : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
-      }),
-    getStorageAt: ({
-      cache,
-      blockNumber: userBlockNumber,
-      ...args
-    }: Omit<GetStorageAtParameters, "blockTag" | "blockNumber"> &
-      BlockOptions): Promise<GetStorageAtReturnType> =>
-      viemGetStorageAt(client, {
-        ...args,
-        ...(cache === "immutable"
-          ? { blockTag: "latest" }
-          : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
-      }),
-    multicall: <
-      const contracts extends readonly unknown[],
-      allowFailure extends boolean = true,
-    >({
-      cache,
-      blockNumber: userBlockNumber,
-      ...args
-    }: Omit<
-      MulticallParameters<contracts, allowFailure>,
-      "blockTag" | "blockNumber"
-    > &
-      BlockOptions): Promise<MulticallReturnType<contracts, allowFailure>> =>
-      viemMulticall(client, {
-        ...args,
-        ...(cache === "immutable"
-          ? { blockTag: "latest" }
-          : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
-      }),
-    // @ts-ignore
-    readContract: <
-      const abi extends Abi | readonly unknown[],
-      functionName extends ContractFunctionName<abi, "pure" | "view">,
-      const args extends ContractFunctionArgs<
-        abi,
-        "pure" | "view",
-        functionName
-      >,
-    >({
-      cache,
-      blockNumber: userBlockNumber,
-      ...args
-    }: Omit<
-      ReadContractParameters<abi, functionName, args>,
-      "blockTag" | "blockNumber"
-    > &
-      BlockOptions): Promise<ReadContractReturnType<abi, functionName, args>> =>
-      viemReadContract(client, {
-        ...args,
-        ...(cache === "immutable"
-          ? { blockTag: "latest" }
-          : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
-      } as ReadContractParameters<abi, functionName, args>),
-    getEnsName: ({
-      cache,
-      blockNumber: userBlockNumber,
-      ...args
-    }: Omit<GetEnsNameParameters, "blockTag" | "blockNumber"> &
-      BlockOptions): Promise<GetEnsNameReturnType> =>
-      viemGetEnsName(client, {
-        ...args,
-        ...(cache === "immutable"
-          ? { blockTag: "latest" }
-          : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
-      }),
-  });
+  ): PonderActions => {
+    const actions = {} as PonderActions;
+    const _publicActions = publicActions(client);
+
+    const addAction = <
+      action extends
+        | (typeof blockDependentActions)[number]
+        | "multicall"
+        | "readContract"
+        | "simulateContract",
+    >(
+      action: action,
+    ) => {
+      // @ts-ignore
+      actions[action] = ({
+        cache,
+        blockNumber: userBlockNumber,
+        ...args
+      }: Parameters<PonderActions[action]>[0]) =>
+        // @ts-ignore
+        _publicActions[action]({
+          ...args,
+          ...(cache === "immutable"
+            ? { blockTag: "latest" }
+            : { blockNumber: userBlockNumber ?? contextState.blockNumber }),
+        } as Parameters<ReturnType<typeof publicActions>[action]>[0]);
+    };
+
+    for (const action of blockDependentActions) {
+      addAction(action);
+    }
+
+    addAction("multicall");
+    addAction("readContract");
+    addAction("simulateContract");
+
+    for (const action of nonBlockDependentActions) {
+      // @ts-ignore
+      actions[action] = _publicActions[action];
+    }
+
+    // required block actions
+
+    for (const action of [
+      "getBlock",
+      "getBlockTransactionCount",
+      "getTransactionCount",
+    ]) {
+      // @ts-ignore
+      actions[action] = _publicActions[action];
+    }
+
+    return actions;
+  };
 };
diff --git a/packages/core/src/indexing/service.ts b/packages/core/src/indexing/service.ts
index f427ec37..501d0922 100644
--- a/packages/core/src/indexing/service.ts
+++ b/packages/core/src/indexing/service.ts
@@ -29,7 +29,7 @@ import type {
   SetupEvent,
 } from "../sync/events.js";
 import { addStackTrace } from "./addStackTrace.js";
-import { type ReadOnlyClient, buildCachedActions } from "./ponderActions.js";
+import { type ReadOnlyClient, getPonderActions } from "./ponderActions.js";
 
 export type Context = {
   network: { chainId: number; name: string };
@@ -142,9 +142,6 @@ export const create = ({
     };
   }
 
-  // build cachedActions
-  const cachedActions = buildCachedActions(contextState);
-
   // build clientByChainId
   for (const network of networks) {
     const transport = sync.getCachedTransport(network);
@@ -152,7 +149,7 @@ export const create = ({
       transport,
       chain: network.chain,
       // @ts-ignore
-    }).extend(cachedActions);
+    }).extend(getPonderActions(contextState));
   }
 
   // build eventCount
diff --git a/packages/core/src/sync-store/encoding.ts b/packages/core/src/sync-store/encoding.ts
index 23e665df..1b15ad12 100644
--- a/packages/core/src/sync-store/encoding.ts
+++ b/packages/core/src/sync-store/encoding.ts
@@ -299,8 +299,12 @@ export function encodeCallTrace({
 
 type RpcRequestResultsTable = {
   request: string;
-  chainId: number;
-  blockNumber: ColumnType<string, string | bigint, string | bigint>;
+  chain_id: number;
+  block_number: ColumnType<
+    string | undefined,
+    string | bigint | undefined,
+    string | bigint | undefined
+  >;
   result: string;
 };
 
@@ -393,7 +397,7 @@ export type PonderSyncSchema = {
   transactionReceipts: TransactionReceiptsTable;
   callTraces: CallTracesTable;
 
-  rpcRequestResults: RpcRequestResultsTable;
+  rpc_request_results: RpcRequestResultsTable;
 
   logFilters: LogFiltersTable;
   logFilterIntervals: LogFilterIntervalsTable;
diff --git a/packages/core/src/sync-store/index.ts b/packages/core/src/sync-store/index.ts
index f86be92b..8116e29b 100644
--- a/packages/core/src/sync-store/index.ts
+++ b/packages/core/src/sync-store/index.ts
@@ -108,15 +108,14 @@ export type SyncStore = {
   }): Promise<{ events: RawEvent[]; cursor: string }>;
   insertRpcRequestResult(args: {
     request: string;
-    blockNumber: bigint;
     chainId: number;
+    blockNumber: bigint | undefined;
     result: string;
   }): Promise<void>;
   getRpcRequestResult(args: {
     request: string;
-    blockNumber: bigint;
     chainId: number;
-  }): Promise<string | null>;
+  }): Promise<string | undefined>;
   pruneRpcRequestResult(args: {
     blocks: Pick<LightBlock, "number">[];
     chainId: number;
@@ -1229,31 +1228,28 @@ export const createSyncStore = ({
   insertRpcRequestResult: async ({ request, blockNumber, chainId, result }) =>
     db.wrap({ method: "insertRpcRequestResult" }, async () => {
       await db
-        .insertInto("rpcRequestResults")
+        .insertInto("rpc_request_results")
         .values({
           request,
-          blockNumber,
-          chainId,
+          block_number: blockNumber,
+          chain_id: chainId,
           result,
         })
         .onConflict((oc) =>
-          oc
-            .columns(["request", "chainId", "blockNumber"])
-            .doUpdateSet({ result }),
+          oc.columns(["request", "chain_id"]).doUpdateSet({ result }),
         )
         .execute();
     }),
-  getRpcRequestResult: async ({ request, blockNumber, chainId }) =>
+  getRpcRequestResult: async ({ request, chainId }) =>
     db.wrap({ method: "getRpcRequestResult" }, async () => {
       const result = await db
-        .selectFrom("rpcRequestResults")
+        .selectFrom("rpc_request_results")
         .select("result")
         .where("request", "=", request)
-        .where("chainId", "=", chainId)
-        .where("blockNumber", "=", blockNumber.toString())
+        .where("chain_id", "=", chainId)
         .executeTakeFirst();
 
-      return result?.result ?? null;
+      return result?.result;
     }),
   pruneRpcRequestResult: async ({ blocks, chainId }) =>
     db.wrap({ method: "pruneRpcRequestResult" }, async () => {
@@ -1264,9 +1260,9 @@ export const createSyncStore = ({
       );
 
       await db
-        .deleteFrom("rpcRequestResults")
-        .where("chainId", "=", chainId)
-        .where("blockNumber", "in", numbers)
+        .deleteFrom("rpc_request_results")
+        .where("chain_id", "=", chainId)
+        .where("block_number", "in", numbers)
         .execute();
     }),
   pruneByChain: async ({ fromBlock, chainId }) =>
@@ -1492,9 +1488,9 @@ export const createSyncStore = ({
           .where("number", ">=", fromBlock.toString())
           .execute();
         await tx
-          .deleteFrom("rpcRequestResults")
-          .where("chainId", "=", chainId)
-          .where("blockNumber", ">=", fromBlock.toString())
+          .deleteFrom("rpc_request_results")
+          .where("chain_id", "=", chainId)
+          .where("block_number", ">=", fromBlock.toString())
           .execute();
         await tx
           .deleteFrom("callTraces")
diff --git a/packages/core/src/sync-store/migrations.ts b/packages/core/src/sync-store/migrations.ts
index 16317575..b8d6c871 100644
--- a/packages/core/src/sync-store/migrations.ts
+++ b/packages/core/src/sync-store/migrations.ts
@@ -840,6 +840,70 @@ const migrations: Record<string, Migration> = {
         .execute();
     },
   },
+  "2024_11_04_0_request_cache": {
+    async up(db: Kysely<any>) {
+      await db.schema
+        .createTable("rpc_request_results")
+        .addColumn("request", "text", (col) => col.notNull())
+        .addColumn("block_number", "numeric(78, 0)")
+        .addColumn("chain_id", "integer", (col) => col.notNull())
+        .addColumn("result", "text", (col) => col.notNull())
+        .addPrimaryKeyConstraint("rpc_request_result_primary_key", [
+          "request",
+          "chain_id",
+        ])
+        .execute();
+
+      await db.executeQuery(
+        sql`
+INSERT INTO ponder_sync.rpc_request_results (request, block_number, chain_id, result)
+SELECT 
+  CONCAT (
+    '{"method":"eth_getbalance","params":["',
+    LOWER(SUBSTRING(request, 16)),
+    '","0x',
+    to_hex("blockNumber"::bigint),
+    '"]}'
+  ) as request,
+  "blockNumber" as block_number,
+  "chainId" as chain_id,
+  result
+FROM ponder_sync."rpcRequestResults"
+WHERE ponder_sync."rpcRequestResults".request LIKE 'eth_getBalance_%'
+AND ponder_sync."rpcRequestResults"."blockNumber" <= 9223372036854775807;
+`.compile(db),
+      );
+
+      await db.executeQuery(
+        sql`
+INSERT INTO ponder_sync.rpc_request_results (request, block_number, chain_id, result)
+SELECT 
+  CONCAT (
+    '{"method":"eth_call","params":[{"address":"',
+    LOWER(SUBSTRING(request, 10, 42)),
+    '","data":"',
+    LOWER(SUBSTRING(request, 53)),
+    '"},"0x',
+    to_hex("blockNumber"::bigint),
+    '"]}'
+  ) as request,
+  "blockNumber" as block_number,
+  "chainId" as chain_id,
+  result
+FROM ponder_sync."rpcRequestResults"
+WHERE ponder_sync."rpcRequestResults".request LIKE 'eth_call_%'
+AND ponder_sync."rpcRequestResults"."blockNumber" <= 9223372036854775807;
+`.compile(db),
+      );
+    },
+    async down(db: Kysely<any>) {
+      await db.schema
+        .dropTable("rpc_request_results")
+        .ifExists()
+        .cascade()
+        .execute();
+    },
+  },
 };
 
 class StaticMigrationProvider implements MigrationProvider {
diff --git a/packages/core/src/sync/transport.ts b/packages/core/src/sync/transport.ts
index a993a912..2bd24829 100644
--- a/packages/core/src/sync/transport.ts
+++ b/packages/core/src/sync/transport.ts
@@ -1,15 +1,37 @@
 import type { SyncStore } from "@/sync-store/index.js";
 import { toLowerCase } from "@/utils/lowercase.js";
+import { orderObject } from "@/utils/order.js";
 import type { RequestQueue } from "@/utils/requestQueue.js";
-import type { Address, Hex, Transport } from "viem";
+import type { Hex, Transport } from "viem";
 import { custom, hexToBigInt, maxUint256 } from "viem";
 
-const cachedMethods = [
-  "eth_call",
+/** RPC methods that reference a block. */
+const blockDependentMethods = new Set([
   "eth_getBalance",
+  "eth_getTransactionCount",
+  "eth_getBlockByNumber",
+  "eth_getBlockTransactionCountByNumber",
+  "eth_getTransactionByBlockNumberAndIndex",
+  "eth_call",
+  "eth_estimateGas",
+  "eth_feeHistory",
+  "eth_getProof",
   "eth_getCode",
   "eth_getStorageAt",
-] as const;
+  "eth_getUncleByBlockNumberAndIndex",
+]);
+
+/** RPC methods that don't reference a block. */
+const nonBlockDependentMethods = new Set([
+  "eth_getBlockByHash",
+  "eth_getTransactionByHash",
+  "eth_getBlockTransactionCountByHash",
+  "eth_getTransactionByBlockHashAndIndex",
+  "eth_getTransactionConfirmations",
+  "eth_getTransactionReceipt",
+  "eth_getUncleByBlockHashAndIndex",
+  "eth_getUncleCountByBlockHash",
+]);
 
 export const cachedTransport = ({
   requestQueue,
@@ -23,56 +45,54 @@ export const cachedTransport = ({
       async request({ method, params }) {
         const body = { method, params };
 
-        if (cachedMethods.includes(method)) {
-          let request: string = undefined!;
-          let blockNumber: Hex | "latest" = undefined!;
-
-          if (method === "eth_call") {
-            const [{ data, to }, _blockNumber] = params as [
-              { data: Hex; to: Hex },
-              Hex | "latest",
-            ];
+        if (
+          blockDependentMethods.has(method) ||
+          nonBlockDependentMethods.has(method)
+        ) {
+          const request = toLowerCase(JSON.stringify(orderObject(body)));
+          let blockNumber: Hex | "latest" | undefined = undefined;
 
-            request = `${method as string}_${toLowerCase(to)}_${toLowerCase(data)}`;
-            blockNumber = _blockNumber;
-          } else if (method === "eth_getBalance") {
-            const [address, _blockNumber] = params as [Address, Hex | "latest"];
+          switch (method) {
+            case "eth_getBlockByNumber":
+            case "eth_getBlockTransactionCountByNumber":
+            case "eth_getTransactionByBlockNumberAndIndex":
+            case "eth_getUncleByBlockNumberAndIndex":
+              [blockNumber] = params;
+              break;
+            case "eth_getBalance":
+            case "eth_call":
+            case "eth_getCode":
+            case "eth_estimateGas":
+            case "eth_feeHistory":
+            case "eth_getTransactionCount":
+              [, blockNumber] = params;
+              break;
 
-            request = `${method as string}_${toLowerCase(address)}`;
-            blockNumber = _blockNumber;
-          } else if (method === "eth_getCode") {
-            const [address, _blockNumber] = params as [Address, Hex | "latest"];
-
-            request = `${method as string}_${toLowerCase(address)}`;
-            blockNumber = _blockNumber;
-          } else if (method === "eth_getStorageAt") {
-            const [address, slot, _blockNumber] = params as [
-              Address,
-              Hex,
-              Hex | "latest",
-            ];
-
-            request = `${method as string}_${toLowerCase(address)}_${toLowerCase(slot)}`;
-            blockNumber = _blockNumber;
+            case "eth_getProof":
+            case "eth_getStorageAt":
+              [, , blockNumber] = params;
+              break;
           }
 
-          const blockNumberBigInt =
-            blockNumber === "latest" ? maxUint256 : hexToBigInt(blockNumber);
-
-          const cachedResult = await syncStore.getRpcRequestResult({
-            blockNumber: blockNumberBigInt,
+          const cacheKey = {
             chainId: chain!.id,
             request,
-          });
+            blockNumber:
+              blockNumber === undefined
+                ? undefined
+                : blockNumber === "latest"
+                  ? maxUint256
+                  : hexToBigInt(blockNumber),
+          };
+
+          const cachedResult = await syncStore.getRpcRequestResult(cacheKey);
 
-          if (cachedResult !== null) return cachedResult;
+          if (cachedResult !== undefined) return JSON.parse(cachedResult);
           else {
             const response = await requestQueue.request(body);
             await syncStore.insertRpcRequestResult({
-              blockNumber: blockNumberBigInt,
-              chainId: chain!.id,
-              request,
-              result: response as string,
+              ...cacheKey,
+              result: JSON.stringify(response),
             });
             return response;
           }
diff --git a/packages/core/src/types/virtual.ts b/packages/core/src/types/virtual.ts
index 8dfc0194..f3096b30 100644
--- a/packages/core/src/types/virtual.ts
+++ b/packages/core/src/types/virtual.ts
@@ -202,23 +202,7 @@ export namespace Virtual {
               keyof config["networks"]]["chainId"];
           };
         }[keyof sourceNetwork];
-    client: Prettify<
-      Omit<
-        ReadOnlyClient,
-        | "extend"
-        | "key"
-        | "batch"
-        | "cacheTime"
-        | "account"
-        | "type"
-        | "uid"
-        | "chain"
-        | "name"
-        | "pollingInterval"
-        | "transport"
-        | "ccipRead"
-      >
-    >;
+    client: Prettify<ReadOnlyClient>;
     db: Db<schema>;
   };
 
diff --git a/packages/core/src/utils/order.ts b/packages/core/src/utils/order.ts
new file mode 100644
index 00000000..61d7ce75
--- /dev/null
+++ b/packages/core/src/utils/order.ts
@@ -0,0 +1,17 @@
+export const orderObject = <T extends { [key: string]: any } | any[]>(
+  obj: T,
+): T => {
+  if (Array.isArray(obj)) return obj.map((x) => orderObject(x)) as T;
+
+  const newObj = {} as T;
+  for (const key of Object.keys(obj).sort()) {
+    const val = obj[key];
+    if (typeof val === "object") {
+      newObj[key as keyof T] = orderObject(obj[key]);
+    } else {
+      newObj[key as keyof T] = obj[key];
+    }
+  }
+
+  return newObj;
+};


Test patch:
diff --git a/packages/core/src/sync-store/index.test.ts b/packages/core/src/sync-store/index.test.ts
index 1e676c9a..049e6146 100644
--- a/packages/core/src/sync-store/index.test.ts
+++ b/packages/core/src/sync-store/index.test.ts
@@ -1043,7 +1043,7 @@ test("pruneRpcRequestResult", async (context) => {
   });
 
   const requestResults = await database.qb.sync
-    .selectFrom("rpcRequestResults")
+    .selectFrom("rpc_request_results")
     .selectAll()
     .execute();
 
diff --git a/packages/core/src/sync/transport.test.ts b/packages/core/src/sync/transport.test.ts
index 9a48d439..d76a3a4f 100644
--- a/packages/core/src/sync/transport.test.ts
+++ b/packages/core/src/sync/transport.test.ts
@@ -6,7 +6,7 @@ import {
 } from "@/_test/setup.js";
 import { anvil, publicClient } from "@/_test/utils.js";
 import type { Transport } from "viem";
-import { getFunctionSelector, toHex } from "viem";
+import { toHex } from "viem";
 import { assertType, beforeEach, expect, test, vi } from "vitest";
 import { cachedTransport } from "./transport.js";
 
@@ -44,8 +44,8 @@ test("default", async (context) => {
   await cleanup();
 });
 
-test("eth_call", async (context) => {
-  const { erc20, requestQueues } = context;
+test("request() block dependent method", async (context) => {
+  const { requestQueues } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
   const blockNumber = await publicClient.getBlockNumber();
 
@@ -57,103 +57,32 @@ test("eth_call", async (context) => {
   });
 
   const response1 = await transport.request({
-    method: "eth_call",
-    params: [
-      {
-        data: getFunctionSelector("totalSupply()"),
-        to: erc20.address,
-      },
-      toHex(blockNumber),
-    ],
+    method: "eth_getBlockByNumber",
+    params: [toHex(blockNumber), false],
   });
 
   expect(response1).toBeDefined();
 
   const insertSpy = vi.spyOn(syncStore, "insertRpcRequestResult");
-
-  const response2 = await transport.request({
-    method: "eth_call",
-    params: [
-      {
-        data: getFunctionSelector("totalSupply()"),
-        to: erc20.address,
-      },
-      toHex(blockNumber),
-    ],
-  });
-
-  expect(response1).toBe(response2);
-
-  expect(insertSpy).toHaveBeenCalledTimes(0);
-
   const getSpy = vi.spyOn(syncStore, "getRpcRequestResult");
 
-  const response3 = await transport.request({
-    method: "eth_call",
-    params: [
-      {
-        data: getFunctionSelector("totalSupply()"),
-        to: erc20.address,
-      },
-      "latest",
-    ],
-  });
-
-  expect(response3).toBeDefined();
-
-  expect(getSpy).toHaveBeenCalledTimes(1);
-
-  await cleanup();
-});
-
-test("eth_getBalance", async (context) => {
-  const { erc20, requestQueues } = context;
-  const { syncStore, cleanup } = await setupDatabaseServices(context);
-  const blockNumber = await publicClient.getBlockNumber();
-
-  const transport = cachedTransport({
-    requestQueue: requestQueues[0],
-    syncStore,
-  })({
-    chain: anvil,
-  });
-
-  const response1 = await transport.request({
-    method: "eth_getBalance",
-    params: [erc20.address, toHex(blockNumber)],
-  });
-
-  expect(response1).toBeDefined();
-
-  const insertSpy = vi.spyOn(syncStore, "insertRpcRequestResult");
-
   const response2 = await transport.request({
-    method: "eth_getBalance",
-    params: [erc20.address, toHex(blockNumber)],
+    method: "eth_getBlockByNumber",
+    params: [toHex(blockNumber), false],
   });
 
-  expect(response1).toBe(response2);
+  expect(response1).toStrictEqual(response2);
 
   expect(insertSpy).toHaveBeenCalledTimes(0);
-
-  const getSpy = vi.spyOn(syncStore, "getRpcRequestResult");
-
-  const response3 = await transport.request({
-    method: "eth_getBalance",
-    params: [erc20.address, "latest"],
-  });
-
-  expect(response3).toBeDefined();
-
   expect(getSpy).toHaveBeenCalledTimes(1);
 
   await cleanup();
 });
 
-test("eth_getStorageAt", async (context) => {
-  const { erc20, requestQueues } = context;
+test("request() non-block dependent method", async (context) => {
+  const { requestQueues } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
-  const blockNumber = await publicClient.getBlockNumber();
+  const block = await publicClient.getBlock({ blockNumber: 2n });
 
   const transport = cachedTransport({
     requestQueue: requestQueues[0],
@@ -163,42 +92,31 @@ test("eth_getStorageAt", async (context) => {
   });
 
   const response1 = await transport.request({
-    method: "eth_getStorageAt",
-    params: [erc20.address, toHex(3), toHex(blockNumber)],
+    method: "eth_getTransactionByHash",
+    params: [block.transactions[0]!],
   });
 
-  expect(response1).toBeDefined();
+  expect(response1).toBeDefined;
 
   const insertSpy = vi.spyOn(syncStore, "insertRpcRequestResult");
+  const getSpy = vi.spyOn(syncStore, "getRpcRequestResult");
 
   const response2 = await transport.request({
-    method: "eth_getStorageAt",
-    params: [erc20.address, toHex(3), toHex(blockNumber)],
+    method: "eth_getTransactionByHash",
+    params: [block.transactions[0]!],
   });
 
-  expect(response1).toBe(response2);
+  expect(response1).toStrictEqual(response2);
 
   expect(insertSpy).toHaveBeenCalledTimes(0);
-
-  const getSpy = vi.spyOn(syncStore, "getRpcRequestResult");
-
-  const response3 = await transport.request({
-    method: "eth_getStorageAt",
-    params: [erc20.address, toHex(3), "latest"],
-  });
-
-  expect(response3).toBeDefined();
-
   expect(getSpy).toHaveBeenCalledTimes(1);
 
   await cleanup();
 });
 
-test("eth_getCode", async (context) => {
-  const { erc20, requestQueues } = context;
+test("request() non-cached method", async (context) => {
+  const { requestQueues } = context;
   const { syncStore, cleanup } = await setupDatabaseServices(context);
-  const blockNumber = await publicClient.getBlockNumber();
-
   const transport = cachedTransport({
     requestQueue: requestQueues[0],
     syncStore,
@@ -206,49 +124,13 @@ test("eth_getCode", async (context) => {
     chain: anvil,
   });
 
-  const response1 = await transport.request({
-    method: "eth_getCode",
-    params: [erc20.address, toHex(blockNumber)],
-  });
-
-  expect(response1).toBeDefined();
-
   const insertSpy = vi.spyOn(syncStore, "insertRpcRequestResult");
-
-  const response2 = await transport.request({
-    method: "eth_getCode",
-    params: [erc20.address, toHex(blockNumber)],
-  });
-
-  expect(response1).toBe(response2);
-
-  expect(insertSpy).toHaveBeenCalledTimes(0);
-
   const getSpy = vi.spyOn(syncStore, "getRpcRequestResult");
 
-  const response3 = await transport.request({
-    method: "eth_getCode",
-    params: [erc20.address, "latest"],
-  });
-
-  expect(response3).toBeDefined();
-
-  expect(getSpy).toHaveBeenCalledTimes(1);
-
-  await cleanup();
-});
-
-test("fallback method", async (context) => {
-  const { requestQueues } = context;
-  const { syncStore, cleanup } = await setupDatabaseServices(context);
-  const transport = cachedTransport({
-    requestQueue: requestQueues[0],
-    syncStore,
-  })({
-    chain: anvil,
-  });
-
   expect(await transport.request({ method: "eth_blockNumber" })).toBeDefined();
 
+  expect(insertSpy).toHaveBeenCalledTimes(0);
+  expect(getSpy).toHaveBeenCalledTimes(0);
+
   await cleanup();
 });
diff --git a/packages/core/src/types/virtual.test-d.ts b/packages/core/src/types/virtual.test-d.ts
index 4aae4300..160d8bef 100644
--- a/packages/core/src/types/virtual.test-d.ts
+++ b/packages/core/src/types/virtual.test-d.ts
@@ -272,7 +272,6 @@ test("Context client", () => {
     | "getEnsName";
 
   assertType<keyof a>({} as any as expectedFunctions);
-  assertType<expectedFunctions>({} as any as keyof a);
 });
 
 test("Context contracts", () => {