Repository: ponder-sh/ponder
Pull request #1685 (2025-04-20 19:16:29)

fix: block size undefined


Patch:
diff --git a/.changeset/five-hairs-jam.md b/.changeset/five-hairs-jam.md
new file mode 100644
index 00000000..0149293d
--- /dev/null
+++ b/.changeset/five-hairs-jam.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed `Cannot convert undefined to a BigInt` error by allowing `block.size` to be `undefined`. Fixes indexing on some chains including Somnia network.
diff --git a/.changeset/metal-cups-notice.md b/.changeset/metal-cups-notice.md
new file mode 100644
index 00000000..da5180fb
--- /dev/null
+++ b/.changeset/metal-cups-notice.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Increased statement timeout for `CREATE INDEX` statements from 2 minutes to 60 minutes.
diff --git a/examples/with-client/client/.env.local b/examples/with-client/client/.env.local
deleted file mode 100644
index 6c68e406..00000000
--- a/examples/with-client/client/.env.local
+++ /dev/null
@@ -1,5 +0,0 @@
-# Mainnet RPC URL used for fetching blockchain data. Alchemy is recommended.
-PONDER_RPC_URL_1=https://eth-mainnet.g.alchemy.com/v2/I96uJ_XZUY0peo6mhN8JSGYxoWehqunc
-
-# (Optional) Postgres database URL. If not provided, SQLite will be used. 
-DATABASE_URL=postgres://kylescott@localhost:5432/kylescott
diff --git a/examples/with-client/client/.gitignore b/examples/with-client/client/.gitignore
new file mode 100644
index 00000000..f0c7e117
--- /dev/null
+++ b/examples/with-client/client/.gitignore
@@ -0,0 +1,18 @@
+# Dependencies
+/node_modules
+
+# Debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# Misc
+.DS_Store
+
+# Env files
+.env*.local
+
+# Ponder
+/generated/
+/.ponder/
diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md
index 13cfcd91..66903fec 100644
--- a/packages/client/CHANGELOG.md
+++ b/packages/client/CHANGELOG.md
@@ -1,5 +1,7 @@
 # @ponder/client
 
+## 0.10.17
+
 ## 0.10.16
 
 ## 0.10.15
diff --git a/packages/client/package.json b/packages/client/package.json
index 8236672e..7cd64c12 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/client",
-  "version": "0.10.16",
+  "version": "0.10.17",
   "description": "",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 4f8f8496..93dd4606 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,13 @@
 # ponder
 
+## 0.10.17
+
+### Patch Changes
+
+- [#1680](https://github.com/ponder-sh/ponder/pull/1680) [`c1b6a4de54d33cb229f2248b7b41e37cbf57d449`](https://github.com/ponder-sh/ponder/commit/c1b6a4de54d33cb229f2248b7b41e37cbf57d449) Thanks [@kyscott18](https://github.com/kyscott18)! - Improved import rules between config, schema, indexing functions, and api function files.
+
+- [#1684](https://github.com/ponder-sh/ponder/pull/1684) [`92e1358cbf8817aea425a8c5e683ddfb1a9bc02d`](https://github.com/ponder-sh/ponder/commit/92e1358cbf8817aea425a8c5e683ddfb1a9bc02d) Thanks [@kyscott18](https://github.com/kyscott18)! - Fixed a bug affecting logs and traces with no args that would cause the error `TypeError: Cannot convert undefined or null to object`.
+
 ## 0.10.16
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index 2c622758..7b41bb9e 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ponder",
-  "version": "0.10.16",
+  "version": "0.10.17",
   "description": "An open-source framework for crypto application backends",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/src/bin/commands/dev.ts b/packages/core/src/bin/commands/dev.ts
index c77dcf1e..08016216 100644
--- a/packages/core/src/bin/commands/dev.ts
+++ b/packages/core/src/bin/commands/dev.ts
@@ -115,7 +115,7 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) {
           return;
         }
 
-        const schemaResult = await build.executeSchema({ namespace });
+        const schemaResult = await build.executeSchema();
         if (schemaResult.status === "error") {
           buildQueue.add({
             status: "error",
@@ -292,6 +292,8 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) {
   const namespace =
     cliOptions.schema ?? process.env.DATABASE_SCHEMA ?? "public";
 
+  globalThis.PONDER_NAMESPACE_BUILD = namespace;
+
   build.startDev({
     onReload: (kind) => {
       buildQueue.clear();
diff --git a/packages/core/src/bin/commands/serve.ts b/packages/core/src/bin/commands/serve.ts
index 8181c3d2..3dc0dc79 100644
--- a/packages/core/src/bin/commands/serve.ts
+++ b/packages/core/src/bin/commands/serve.ts
@@ -69,9 +69,7 @@ export async function serve({ cliOptions }: { cliOptions: CliOptions }) {
     return;
   }
 
-  const schemaResult = await build.executeSchema({
-    namespace: namespaceResult.result,
-  });
+  const schemaResult = await build.executeSchema();
   if (schemaResult.status === "error") {
     await exit({ reason: "Failed intial build", code: 1 });
     return;
diff --git a/packages/core/src/bin/commands/start.ts b/packages/core/src/bin/commands/start.ts
index 6449da94..1e97e436 100644
--- a/packages/core/src/bin/commands/start.ts
+++ b/packages/core/src/bin/commands/start.ts
@@ -72,9 +72,7 @@ export async function start({ cliOptions }: { cliOptions: CliOptions }) {
     return;
   }
 
-  const schemaResult = await build.executeSchema({
-    namespace: namespaceResult.result,
-  });
+  const schemaResult = await build.executeSchema();
   if (schemaResult.status === "error") {
     await exit({ reason: "Failed intial build", code: 1 });
     return;
diff --git a/packages/core/src/build/index.ts b/packages/core/src/build/index.ts
index 6c979a35..84ae1f72 100644
--- a/packages/core/src/build/index.ts
+++ b/packages/core/src/build/index.ts
@@ -51,9 +51,7 @@ type ApiResult = Result<{ app: Hono }>;
 
 export type Build = {
   executeConfig: () => Promise<ConfigResult>;
-  executeSchema: (params: {
-    namespace: NamespaceBuild;
-  }) => Promise<SchemaResult>;
+  executeSchema: () => Promise<SchemaResult>;
   executeIndexingFunctions: () => Promise<IndexingResult>;
   executeApi: (params: {
     indexingBuild: IndexingBuild;
@@ -193,8 +191,7 @@ export const createBuild = async ({
         result: { config, contentHash },
       } as const;
     },
-    async executeSchema({ namespace }): Promise<SchemaResult> {
-      globalThis.PONDER_NAMESPACE_BUILD = namespace;
+    async executeSchema(): Promise<SchemaResult> {
       const executeResult = await executeFile({
         file: common.options.schemaFile,
       });
@@ -224,6 +221,7 @@ export const createBuild = async ({
       const files = glob.sync(indexingPattern, {
         ignore: apiPattern,
       });
+
       const executeResults = await Promise.all(
         files.map(async (file) => ({
           ...(await executeFile({ file })),
@@ -291,9 +289,6 @@ export const createBuild = async ({
         return { status: "error", error };
       }
 
-      viteNodeRunner.moduleCache.invalidateDepTree(glob.sync(apiPattern));
-      viteNodeRunner.moduleCache.deleteByModuleId("ponder:api");
-
       const executeResult = await executeFile({
         file: common.options.apiFile,
       });
@@ -348,9 +343,14 @@ export const createBuild = async ({
         });
         return { status: "error", error } as const;
       }
+
+      const namespace = cliOptions.schema ?? process.env.DATABASE_SCHEMA!;
+
+      globalThis.PONDER_NAMESPACE_BUILD = namespace;
+
       return {
         status: "success",
-        result: cliOptions.schema ?? process.env.DATABASE_SCHEMA!,
+        result: namespace,
       } as const;
     },
     preCompile({ config }): Result<PreBuild> {
diff --git a/packages/core/src/build/plugin.ts b/packages/core/src/build/plugin.ts
index 5f67bf79..644bd7e9 100644
--- a/packages/core/src/build/plugin.ts
+++ b/packages/core/src/build/plugin.ts
@@ -18,6 +18,10 @@ export default schema;
 
 const apiModule = () => `import { createPublicClient } from "viem";
 
+if (globalThis.PONDER_INDEXING_BUILD === undefined || globalThis.PONDER_DATABASE === undefined) {
+  throw new Error('Invalid dependency graph. Config, schema, and indexing function files cannot import objects from the API function file "src/api/index.ts".')
+}
+
 const publicClients = {};
 
 for (const network of globalThis.PONDER_INDEXING_BUILD.networks) {
diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts
index 86d17fad..8ab92a3f 100644
--- a/packages/core/src/database/index.ts
+++ b/packages/core/src/database/index.ts
@@ -1182,7 +1182,12 @@ EXECUTE PROCEDURE "${namespace}".${notification};`),
     },
     async createIndexes() {
       for (const statement of schemaBuild.statements.indexes.sql) {
-        await qb.drizzle.execute(statement);
+        await this.wrap({ method: "createIndexes" }, async () => {
+          await qb.drizzle.transaction(async (tx) => {
+            await tx.execute("SET statement_timeout = 3600000;"); // 60 minutes
+            await tx.execute(statement);
+          });
+        });
       }
     },
     async createTriggers() {
diff --git a/packages/core/src/indexing-store/profile.ts b/packages/core/src/indexing-store/profile.ts
index b69f8f8d..ac39fb79 100644
--- a/packages/core/src/indexing-store/profile.ts
+++ b/packages/core/src/indexing-store/profile.ts
@@ -132,18 +132,26 @@ export const recordProfilePattern = (
       }
 
       case "log": {
-        let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
-
-          if (typeof argValue !== "object" && eq(argValue, value)) {
-            result[js] = ["args", argKey];
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          let hasMatch = false;
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, value)) {
+              result[js] = ["args", argKey];
+              hasMatch = true;
+              break;
+            }
           }
-        }
 
-        if (hasMatch) continue;
+          if (hasMatch) continue;
+        }
 
         if (eq(event.event.log.address, value)) {
           result[js] = ["log", "address"];
@@ -211,13 +219,40 @@ export const recordProfilePattern = (
 
       case "trace": {
         let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
 
-          if (typeof argValue !== "object" && eq(argValue, value)) {
-            result[js] = ["args", argKey];
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, value)) {
+              result[js] = ["args", argKey];
+              hasMatch = true;
+              break;
+            }
+          }
+        }
+
+        // Note: explicitly skip profiling result if it is an array
+        if (
+          event.event.result !== undefined &&
+          Array.isArray(event.event.result) === false
+        ) {
+          for (const argKey of Object.keys(event.event.result)) {
+            const argValue = (event.event.result as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, value)) {
+              result[js] = ["result", argKey];
+              hasMatch = true;
+              break;
+            }
           }
         }
 
diff --git a/packages/core/src/indexing/index.ts b/packages/core/src/indexing/index.ts
index 32e53fe8..c7c2acae 100644
--- a/packages/core/src/indexing/index.ts
+++ b/packages/core/src/indexing/index.ts
@@ -341,7 +341,7 @@ export const toErrorMeta = (
 
     case "log": {
       return [
-        `Event arguments:\n${prettyPrint(event?.event?.args)}`,
+        `Event arguments:\n${prettyPrint(Array.isArray(event.event?.args) ? undefined : event.event?.args)}`,
         logText(event?.event?.log),
         transactionText(event?.event?.transaction),
         blockText(event?.event?.block),
@@ -350,7 +350,7 @@ export const toErrorMeta = (
 
     case "trace": {
       return [
-        `Call trace arguments:\n${prettyPrint(event?.event?.args)}`,
+        `Call trace arguments:\n${prettyPrint(Array.isArray(event.event?.args) ? undefined : event.event?.args)}`,
         traceText(event?.event?.trace),
         transactionText(event?.event?.transaction),
         blockText(event?.event?.block),
diff --git a/packages/core/src/indexing/profile.ts b/packages/core/src/indexing/profile.ts
index e7dc33ad..66f184a9 100644
--- a/packages/core/src/indexing/profile.ts
+++ b/packages/core/src/indexing/profile.ts
@@ -102,18 +102,27 @@ export const recordProfilePattern = ({
     }
 
     case "log": {
-      let hasMatch = false;
-      for (const argKey of Object.keys(event.event.args)) {
-        const argValue = event.event.args[argKey];
+      // Note: explicitly skip profiling args if they are an array
+      if (
+        event.event.args !== undefined &&
+        Array.isArray(event.event.args) === false
+      ) {
+        let hasMatch = false;
+
+        for (const argKey of Object.keys(event.event.args)) {
+          const argValue = (event.event.args as { [key: string]: unknown })[
+            argKey
+          ] as string | bigint | number | boolean;
 
-        if (typeof argValue !== "object" && eq(argValue, args.address)) {
-          resultAddress = { type: "derived", value: ["args", argKey] };
-          hasMatch = true;
-          break;
+          if (typeof argValue !== "object" && eq(argValue, args.address)) {
+            resultAddress = { type: "derived", value: ["args", argKey] };
+            hasMatch = true;
+            break;
+          }
         }
-      }
 
-      if (hasMatch) break;
+        if (hasMatch) break;
+      }
 
       if (eq(event.event.log.address, args.address)) {
         resultAddress = { type: "derived", value: ["log", "address"] };
@@ -154,13 +163,40 @@ export const recordProfilePattern = ({
 
     case "trace": {
       let hasMatch = false;
-      for (const argKey of Object.keys(event.event.args)) {
-        const argValue = event.event.args[argKey];
 
-        if (typeof argValue !== "object" && eq(argValue, args.address)) {
-          resultAddress = { type: "derived", value: ["args", argKey] };
-          hasMatch = true;
-          break;
+      // Note: explicitly skip profiling args if they are an array
+      if (
+        event.event.args !== undefined &&
+        Array.isArray(event.event.args) === false
+      ) {
+        for (const argKey of Object.keys(event.event.args)) {
+          const argValue = (event.event.args as { [key: string]: unknown })[
+            argKey
+          ] as string | bigint | number | boolean;
+
+          if (typeof argValue !== "object" && eq(argValue, args.address)) {
+            resultAddress = { type: "derived", value: ["args", argKey] };
+            hasMatch = true;
+            break;
+          }
+        }
+      }
+
+      // Note: explicitly skip profiling result if it is an array
+      if (
+        event.event.result !== undefined &&
+        Array.isArray(event.event.result) === false
+      ) {
+        for (const argKey of Object.keys(event.event.result)) {
+          const argValue = (event.event.result as { [key: string]: unknown })[
+            argKey
+          ] as string | bigint | number | boolean;
+
+          if (typeof argValue !== "object" && eq(argValue, args.address)) {
+            resultAddress = { type: "derived", value: ["result", argKey] };
+            hasMatch = true;
+            break;
+          }
         }
       }
 
@@ -372,18 +408,27 @@ export const recordProfilePattern = ({
       }
 
       case "log": {
-        let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
-
-          if (typeof argValue !== "object" && eq(argValue, arg)) {
-            resultArgs.push({ type: "derived", value: ["args", argKey] });
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          let hasMatch = false;
+
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, arg)) {
+              resultArgs.push({ type: "derived", value: ["args", argKey] });
+              hasMatch = true;
+              break;
+            }
           }
-        }
 
-        if (hasMatch) continue;
+          if (hasMatch) continue;
+        }
 
         if (eq(event.event.log.address, arg)) {
           resultArgs.push({ type: "derived", value: ["log", "address"] });
@@ -454,13 +499,40 @@ export const recordProfilePattern = ({
 
       case "trace": {
         let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
 
-          if (typeof argValue !== "object" && eq(argValue, arg)) {
-            resultArgs.push({ type: "derived", value: ["args", argKey] });
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, arg)) {
+              resultArgs.push({ type: "derived", value: ["args", argKey] });
+              hasMatch = true;
+              break;
+            }
+          }
+        }
+
+        // Note: explicitly skip profiling result if it is an array
+        if (
+          event.event.result !== undefined &&
+          Array.isArray(event.event.result) === false
+        ) {
+          for (const argKey of Object.keys(event.event.result)) {
+            const argValue = (event.event.result as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, arg)) {
+              resultArgs.push({ type: "derived", value: ["result", argKey] });
+              hasMatch = true;
+              break;
+            }
           }
         }
 
diff --git a/packages/core/src/internal/types.ts b/packages/core/src/internal/types.ts
index 8066a537..ab46538b 100644
--- a/packages/core/src/internal/types.ts
+++ b/packages/core/src/internal/types.ts
@@ -8,7 +8,7 @@ import type {
   TransactionReceipt,
   Transfer,
 } from "@/types/eth.js";
-import type { Prettify } from "@/types/utils.js";
+import type { MakeOptional, Prettify } from "@/types/utils.js";
 import type { Trace as DebugTrace } from "@/utils/debug.js";
 import type { PGliteOptions } from "@/utils/pglite.js";
 import type { PGlite } from "@electric-sql/pglite";
@@ -369,7 +369,9 @@ export type Seconds = {
 
 // Blockchain data
 
-export type SyncBlock = RpcBlock<Exclude<BlockTag, "pending">, true>;
+export type SyncBlock = Prettify<
+  MakeOptional<RpcBlock<Exclude<BlockTag, "pending">, true>, "size">
+>;
 export type SyncLog = ViemLog<Hex, Hex, false>;
 export type SyncTransaction = RpcTransaction<false>;
 export type SyncTransactionReceipt = RpcTransactionReceipt;
@@ -441,7 +443,7 @@ export type LogEvent = {
 
   event: {
     id: string;
-    args: any;
+    args: { [key: string]: unknown } | readonly unknown[] | undefined;
     log: Log;
     block: Block;
     transaction: Transaction;
@@ -507,8 +509,8 @@ export type TraceEvent = {
 
   event: {
     id: string;
-    args: any;
-    result: any;
+    args: { [key: string]: unknown } | readonly unknown[] | undefined;
+    result: { [key: string]: unknown } | readonly unknown[] | undefined;
     trace: Trace;
     block: Block;
     transaction: Transaction;
diff --git a/packages/core/src/sync-store/encoding.ts b/packages/core/src/sync-store/encoding.ts
index 2d21ffaf..098f451c 100644
--- a/packages/core/src/sync-store/encoding.ts
+++ b/packages/core/src/sync-store/encoding.ts
@@ -60,7 +60,7 @@ export const encodeBlock = ({
   receipts_root: block.receiptsRoot,
   transactions_root: block.transactionsRoot,
   sha3_uncles: block.sha3Uncles ?? null,
-  size: hexToBigInt(block.size),
+  size: block.size ? hexToBigInt(block.size) : 0n,
   difficulty: hexToBigInt(block.difficulty),
   total_difficulty: block.totalDifficulty
     ? hexToBigInt(block.totalDifficulty)
diff --git a/packages/core/src/sync/events.ts b/packages/core/src/sync/events.ts
index f0821ada..c2372c24 100644
--- a/packages/core/src/sync/events.ts
+++ b/packages/core/src/sync/events.ts
@@ -618,7 +618,7 @@ export const syncBlockToInternal = ({
   parentHash: block.parentHash,
   receiptsRoot: block.receiptsRoot,
   sha3Uncles: block.sha3Uncles,
-  size: hexToBigInt(block.size),
+  size: block.size ? hexToBigInt(block.size) : 0n,
   stateRoot: block.stateRoot,
   timestamp: hexToBigInt(block.timestamp),
   totalDifficulty: block.totalDifficulty
diff --git a/packages/core/src/types/utils.ts b/packages/core/src/types/utils.ts
index fce082c2..afa3cf74 100644
--- a/packages/core/src/types/utils.ts
+++ b/packages/core/src/types/utils.ts
@@ -26,3 +26,9 @@ export type DeepPartial<T> = {
       ? DeepPartial<T[P]>
       : T[P];
 };
+
+/**
+ * @description Marks a property of an object as optional.
+ */
+export type MakeOptional<T, K extends keyof T> = Omit<T, K> &
+  Partial<Pick<T, K>>;
diff --git a/packages/create-ponder/CHANGELOG.md b/packages/create-ponder/CHANGELOG.md
index d2e88e48..278a81a6 100644
--- a/packages/create-ponder/CHANGELOG.md
+++ b/packages/create-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # create-ponder
 
+## 0.10.17
+
 ## 0.10.16
 
 ## 0.10.15
diff --git a/packages/create-ponder/package.json b/packages/create-ponder/package.json
index 58f5537b..259979c1 100644
--- a/packages/create-ponder/package.json
+++ b/packages/create-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-ponder",
-  "version": "0.10.16",
+  "version": "0.10.17",
   "type": "module",
   "description": "A CLI tool to create Ponder apps",
   "license": "MIT",
diff --git a/packages/eslint-config-ponder/CHANGELOG.md b/packages/eslint-config-ponder/CHANGELOG.md
index e9a873ba..f79660dc 100644
--- a/packages/eslint-config-ponder/CHANGELOG.md
+++ b/packages/eslint-config-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # eslint-config-ponder
 
+## 0.10.17
+
 ## 0.10.16
 
 ## 0.10.15
diff --git a/packages/eslint-config-ponder/package.json b/packages/eslint-config-ponder/package.json
index 29157276..b49c748e 100644
--- a/packages/eslint-config-ponder/package.json
+++ b/packages/eslint-config-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-config-ponder",
-  "version": "0.10.16",
+  "version": "0.10.17",
   "description": "ESLint config for Ponder apps",
   "license": "MIT",
   "main": "./index.js",
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index f503860b..24d273db 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,12 @@
 # @ponder/react
 
+## 0.10.17
+
+### Patch Changes
+
+- Updated dependencies []:
+  - @ponder/client@0.10.17
+
 ## 0.10.16
 
 ### Patch Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 1f222892..1fe7fcc3 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/react",
-  "version": "0.10.16",
+  "version": "0.10.17",
   "description": "React hooks for Ponder",
   "license": "MIT",
   "repository": {
@@ -37,7 +37,7 @@
     "superjson": "^2.2.2"
   },
   "peerDependencies": {
-    "@ponder/client": ">=0.10.16",
+    "@ponder/client": ">=0.10.17",
     "@tanstack/react-query": ">=5.0.0",
     "react": ">=18",
     "typescript": ">=5.0.4"


Test patch:
diff --git a/packages/core/src/indexing-store/profile.test.ts b/packages/core/src/indexing-store/profile.test.ts
index 126dd32f..918f8333 100644
--- a/packages/core/src/indexing-store/profile.test.ts
+++ b/packages/core/src/indexing-store/profile.test.ts
@@ -1,5 +1,5 @@
 import { onchainTable } from "@/drizzle/onchain.js";
-import type { BlockEvent, LogEvent } from "@/internal/types.js";
+import type { BlockEvent, LogEvent, TraceEvent } from "@/internal/types.js";
 import { ZERO_CHECKPOINT_STRING } from "@/utils/checkpoint.js";
 import type { Column, Table } from "drizzle-orm";
 import { zeroAddress } from "viem";
@@ -40,6 +40,156 @@ test("recordProfilePattern() no pattern", () => {
   expect(pattern).toBeUndefined();
 });
 
+test("recordProfilePattern() with undefined log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
+test("recordProfilePattern() with undefined trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      result: undefined,
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
+test("recordProfilePattern() with array log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
+test("recordProfilePattern() with array trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      result: [],
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
 test("recordProfilePattern() chainId", () => {
   const event = {
     type: "block",
diff --git a/packages/core/src/indexing/profile.test.ts b/packages/core/src/indexing/profile.test.ts
index 835813fb..e986a22f 100644
--- a/packages/core/src/indexing/profile.test.ts
+++ b/packages/core/src/indexing/profile.test.ts
@@ -1,11 +1,413 @@
 import { ALICE } from "@/_test/constants.js";
 import { erc20ABI } from "@/_test/generated.js";
-import type { BlockEvent, LogEvent } from "@/internal/types.js";
+import type { BlockEvent, LogEvent, TraceEvent } from "@/internal/types.js";
 import { ZERO_CHECKPOINT_STRING } from "@/utils/checkpoint.js";
 import { getAbiItem, zeroAddress } from "viem";
 import { expect, test } from "vitest";
 import { recordProfilePattern, recoverProfilePattern } from "./profile.js";
 
+test("recordProfilePattern() with undefined log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
+test("recordProfilePattern() with undefined trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      result: undefined,
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
+test("recordProfilePattern() with array log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
+test("recordProfilePattern() with array trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      result: [],
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
 test("recordProfilePattern() address", () => {
   const event = {
     type: "log",