Repository: ponder-sh/ponder
Pull request #1712 (2025-04-30 18:13:42)

fix migration for logical replication


Patch:
diff --git a/.changeset/blue-lions-talk.md b/.changeset/blue-lions-talk.md
deleted file mode 100644
index 11128791..00000000
--- a/.changeset/blue-lions-talk.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@ponder/utils": patch
----
-
-Updated eth_getLogs retry helper to support swell chain.
diff --git a/.changeset/curvy-elephants-flow.md b/.changeset/curvy-elephants-flow.md
new file mode 100644
index 00000000..ee54e41c
--- /dev/null
+++ b/.changeset/curvy-elephants-flow.md
@@ -0,0 +1,11 @@
+---
+"ponder": patch
+---
+
+Fixed a bug causing events from sources with factories to be missed.
+
+Any users that were affected by this bug can removed corrupted `ponder_sync` rows with the query:
+
+```sql
+DELETE FROM ponder_sync.intervals WHERE fragment_id like '%offset%' OR fragment_id like '%topic%';
+```
diff --git a/.changeset/fast-bees-rest.md b/.changeset/fast-bees-rest.md
new file mode 100644
index 00000000..f66e7e61
--- /dev/null
+++ b/.changeset/fast-bees-rest.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed an issue where the historical sync would sometimes fail with the error "RangeError: Invalid array length" when indexing very large factory contracts.
diff --git a/.changeset/nervous-adults-knock.md b/.changeset/nervous-adults-knock.md
new file mode 100644
index 00000000..2ce123e8
--- /dev/null
+++ b/.changeset/nervous-adults-knock.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed an issue where the `0.10` database migration to `ponder_sync` failed on databases where logical replication was enabled.
diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md
index 195cf19f..2ec3c061 100644
--- a/packages/client/CHANGELOG.md
+++ b/packages/client/CHANGELOG.md
@@ -1,5 +1,9 @@
 # @ponder/client
 
+## 0.10.22
+
+## 0.10.21
+
 ## 0.10.20
 
 ## 0.10.19
diff --git a/packages/client/package.json b/packages/client/package.json
index 9302e8f2..e5fe5822 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/client",
-  "version": "0.10.20",
+  "version": "0.10.22",
   "description": "",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 9c5fd323..413c6292 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,19 @@
 # ponder
 
+## 0.10.22
+
+### Patch Changes
+
+- Updated dependencies [[`27886fef0788e7ee1c25221087ecd6af05ea6197`](https://github.com/ponder-sh/ponder/commit/27886fef0788e7ee1c25221087ecd6af05ea6197)]:
+  - @ponder/utils@0.2.6
+
+## 0.10.21
+
+### Patch Changes
+
+- Updated dependencies [[`e9b0fb99772baff7d3008a9dd1c8383e6182df59`](https://github.com/ponder-sh/ponder/commit/e9b0fb99772baff7d3008a9dd1c8383e6182df59)]:
+  - @ponder/utils@0.2.5
+
 ## 0.10.20
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index 3e533bbb..7e7e3ad9 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ponder",
-  "version": "0.10.20",
+  "version": "0.10.22",
   "description": "An open-source framework for crypto application backends",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/src/sync-store/index.ts b/packages/core/src/sync-store/index.ts
index 75975e96..2046cd86 100644
--- a/packages/core/src/sync-store/index.ts
+++ b/packages/core/src/sync-store/index.ts
@@ -253,12 +253,14 @@ export const createSyncStore = ({
       },
     ),
   insertChildAddresses: async ({ factory, childAddresses, chainId }) => {
-    if (childAddresses.size === 0) {
-      return;
-    }
+    if (childAddresses.size === 0) return;
     await database.wrap(
       { method: "insertChildAddresses", includeTraceLogs: true },
       async () => {
+        const batchSize = Math.floor(
+          common.options.databaseMaxQueryParameters / 3,
+        );
+
         const values: InsertObject<PonderSyncSchema, "factory_addresses">[] =
           [];
         for (const [address, blockNumber] of childAddresses) {
@@ -270,21 +272,23 @@ export const createSyncStore = ({
           });
         }
 
-        await database.qb.sync
-          .with("factory_insert", (qb) =>
-            qb
-              .insertInto("factories")
-              .values({ factory })
-              .returning("id")
-              .onConflict((oc) =>
-                oc
-                  .column("factory")
-                  .doUpdateSet({ factory: sql`excluded.factory` }),
-              ),
-          )
-          .insertInto("factory_addresses")
-          .values(values)
-          .execute();
+        for (let i = 0; i < values.length; i += batchSize) {
+          await database.qb.sync
+            .with("factory_insert", (qb) =>
+              qb
+                .insertInto("factories")
+                .values({ factory })
+                .returning("id")
+                .onConflict((oc) =>
+                  oc
+                    .column("factory")
+                    .doUpdateSet({ factory: sql`excluded.factory` }),
+                ),
+            )
+            .insertInto("factory_addresses")
+            .values(values.slice(i, i + batchSize))
+            .execute();
+        }
       },
     );
   },
diff --git a/packages/core/src/sync-store/migrations.ts b/packages/core/src/sync-store/migrations.ts
index 33f5bfea..3e15b824 100644
--- a/packages/core/src/sync-store/migrations.ts
+++ b/packages/core/src/sync-store/migrations.ts
@@ -1278,8 +1278,8 @@ GROUP BY fragment_id, chain_id
   "2025_02_19_0_primary_key": {
     async up(db) {
       // 1. drop unused indexes
-      // 2. drop primary key
-      // 3. update column types
+      // 2. update column types
+      // 3. drop primary key
       // 4. drop unused columns
       // 5. rename tables and columns
       // 6. create new primary key
@@ -1330,24 +1330,6 @@ GROUP BY fragment_id, chain_id
         `${new Date().toISOString()} [ponder_sync migration] dropped indexes`,
       );
 
-      await db.schema.alterTable("logs").dropConstraint("logs_pkey").execute();
-      await db.schema
-        .alterTable("blocks")
-        .dropConstraint("blocks_pkey")
-        .execute();
-      await db.schema
-        .alterTable("transactions")
-        .dropConstraint("transactions_pkey")
-        .execute();
-      await db.schema
-        .alterTable("transactionReceipts")
-        .dropConstraint("transactionReceipts_pkey")
-        .execute();
-      await db.schema
-        .alterTable("traces")
-        .dropConstraint("traces_pkey")
-        .execute();
-
       await db.schema
         .alterTable("logs")
         .alterColumn("blockNumber", (qb) => qb.setDataType("bigint"))
@@ -1397,11 +1379,30 @@ GROUP BY fragment_id, chain_id
         .alterColumn("chain_id", (qb) => qb.setDataType("bigint"))
         .execute();
 
+      await db.deleteFrom("logs").where("checkpoint", "=", null).execute();
+
       console.log(
         `${new Date().toISOString()} [ponder_sync migration] updated column types`,
       );
 
-      await db.deleteFrom("logs").where("checkpoint", "=", null).execute();
+      await db.schema.alterTable("logs").dropConstraint("logs_pkey").execute();
+      await db.schema
+        .alterTable("blocks")
+        .dropConstraint("blocks_pkey")
+        .execute();
+      await db.schema
+        .alterTable("transactions")
+        .dropConstraint("transactions_pkey")
+        .execute();
+      await db.schema
+        .alterTable("transactionReceipts")
+        .dropConstraint("transactionReceipts_pkey")
+        .execute();
+      await db.schema
+        .alterTable("traces")
+        .dropConstraint("traces_pkey")
+        .execute();
+
       await db.schema.alterTable("logs").dropColumn("checkpoint").execute();
       await db.schema.alterTable("logs").dropColumn("id").execute();
       await db.schema.alterTable("blocks").dropColumn("checkpoint").execute();
@@ -1723,10 +1724,6 @@ GROUP BY fragment_id, chain_id
       );
       await db.executeQuery(sql`SET statement_timeout = 3600000;`.compile(db));
 
-      await db.schema
-        .alterTable("rpc_request_results")
-        .dropConstraint("rpc_request_result_primary_key")
-        .execute();
       await db.schema
         .alterTable("rpc_request_results")
         .addColumn("request_hash_temp", "text")
@@ -1735,6 +1732,10 @@ GROUP BY fragment_id, chain_id
         .updateTable("rpc_request_results")
         .set({ request_hash_temp: sql`request_hash` })
         .execute();
+      await db.schema
+        .alterTable("rpc_request_results")
+        .dropConstraint("rpc_request_result_primary_key")
+        .execute();
       await db.schema
         .alterTable("rpc_request_results")
         .dropColumn("request_hash")
diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts
index 730e6d57..4da93c23 100644
--- a/packages/core/src/sync/index.ts
+++ b/packages/core/src/sync/index.ts
@@ -1010,7 +1010,9 @@ export const getPerChainOnRealtimeSyncEvent = ({
 
         for (const block of finalizedBlocks) {
           for (const [factory, addresses] of block.childAddresses) {
-            childAddresses.set(factory, new Map());
+            if (childAddresses.has(factory) === false) {
+              childAddresses.set(factory, new Map());
+            }
             for (const address of addresses) {
               if (childAddresses.get(factory)!.has(address) === false) {
                 childAddresses
diff --git a/packages/create-ponder/CHANGELOG.md b/packages/create-ponder/CHANGELOG.md
index 0f9ac623..2d0e444e 100644
--- a/packages/create-ponder/CHANGELOG.md
+++ b/packages/create-ponder/CHANGELOG.md
@@ -1,5 +1,9 @@
 # create-ponder
 
+## 0.10.22
+
+## 0.10.21
+
 ## 0.10.20
 
 ## 0.10.19
diff --git a/packages/create-ponder/package.json b/packages/create-ponder/package.json
index eb1dc748..db73230a 100644
--- a/packages/create-ponder/package.json
+++ b/packages/create-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-ponder",
-  "version": "0.10.20",
+  "version": "0.10.22",
   "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 d8a3cdce..217827db 100644
--- a/packages/eslint-config-ponder/CHANGELOG.md
+++ b/packages/eslint-config-ponder/CHANGELOG.md
@@ -1,5 +1,9 @@
 # eslint-config-ponder
 
+## 0.10.22
+
+## 0.10.21
+
 ## 0.10.20
 
 ## 0.10.19
diff --git a/packages/eslint-config-ponder/package.json b/packages/eslint-config-ponder/package.json
index 087b3708..75b6765b 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.20",
+  "version": "0.10.22",
   "description": "ESLint config for Ponder apps",
   "license": "MIT",
   "main": "./index.js",
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 562f7fcf..524847f3 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,19 @@
 # @ponder/react
 
+## 0.10.22
+
+### Patch Changes
+
+- Updated dependencies []:
+  - @ponder/client@0.10.22
+
+## 0.10.21
+
+### Patch Changes
+
+- Updated dependencies []:
+  - @ponder/client@0.10.21
+
 ## 0.10.20
 
 ### Patch Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 61476afd..37001870 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/react",
-  "version": "0.10.20",
+  "version": "0.10.22",
   "description": "React hooks for Ponder",
   "license": "MIT",
   "repository": {
@@ -37,7 +37,7 @@
     "superjson": "^2.2.2"
   },
   "peerDependencies": {
-    "@ponder/client": ">=0.10.20",
+    "@ponder/client": ">=0.10.22",
     "@tanstack/react-query": ">=5.0.0",
     "react": ">=18",
     "typescript": ">=5.0.4"
diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md
index e4b88c11..e62c0a3a 100644
--- a/packages/utils/CHANGELOG.md
+++ b/packages/utils/CHANGELOG.md
@@ -1,5 +1,17 @@
 # @ponder/utils
 
+## 0.2.6
+
+### Patch Changes
+
+- [#1714](https://github.com/ponder-sh/ponder/pull/1714) [`27886fef0788e7ee1c25221087ecd6af05ea6197`](https://github.com/ponder-sh/ponder/commit/27886fef0788e7ee1c25221087ecd6af05ea6197) Thanks [@kyscott18](https://github.com/kyscott18)! - Updated eth_getLogs retry helper to support somnia chain.
+
+## 0.2.5
+
+### Patch Changes
+
+- [#1710](https://github.com/ponder-sh/ponder/pull/1710) [`e9b0fb99772baff7d3008a9dd1c8383e6182df59`](https://github.com/ponder-sh/ponder/commit/e9b0fb99772baff7d3008a9dd1c8383e6182df59) Thanks [@kyscott18](https://github.com/kyscott18)! - Updated eth_getLogs retry helper to support swell chain.
+
 ## 0.2.4
 
 ### Patch Changes
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 140c1bd2..879b2657 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/utils",
-  "version": "0.2.4",
+  "version": "0.2.6",
   "description": "",
   "license": "MIT",
   "type": "module",
diff --git a/packages/utils/src/getLogsRetryHelper.ts b/packages/utils/src/getLogsRetryHelper.ts
index 641779fa..ab7fb4d8 100644
--- a/packages/utils/src/getLogsRetryHelper.ts
+++ b/packages/utils/src/getLogsRetryHelper.ts
@@ -319,7 +319,7 @@ export const getLogsRetryHelper = ({
   }
 
   // arbitrum
-  match = sError.match(/logs matched by query exceeds limit of 10000/);
+  match = sError.match(/logs matched by query exceeds limit of \d+/);
   if (match !== null) {
     const ranges = chunk({
       params,
@@ -456,6 +456,25 @@ export const getLogsRetryHelper = ({
     } as const;
   }
 
+  // somnia
+  match = sError.match(/block range exceeds ([\d,.]+)/);
+  if (match !== null) {
+    const ranges = chunk({
+      params,
+      range: BigInt(match[1]!.replace(/[,.]/g, "")),
+    });
+
+    if (isRangeUnchanged(params, ranges)) {
+      return { shouldRetry: false } as const;
+    }
+
+    return {
+      shouldRetry: true,
+      ranges,
+      isSuggestedRange: true,
+    } as const;
+  }
+
   // No match found
   return {
     shouldRetry: false,


Test patch:
diff --git a/packages/core/src/sync-realtime/bloom.test.ts b/packages/core/src/sync-realtime/bloom.test.ts
index 5ab1b699..b8edbc89 100644
--- a/packages/core/src/sync-realtime/bloom.test.ts
+++ b/packages/core/src/sync-realtime/bloom.test.ts
@@ -1,8 +1,9 @@
+import type { LogFactory, LogFilter } from "@/internal/types.js";
 import type { Hex } from "viem";
 import { expect, test } from "vitest";
-import { isInBloom } from "./bloom.js";
+import { isFilterInBloom, isInBloom } from "./bloom.js";
 
-test("bloom", () => {
+test("isInBloom", () => {
   let bloom =
     "0x00000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020000000000000000000000000000000000000000000008000000001000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" as Hex;
 
@@ -24,3 +25,161 @@ test("bloom", () => {
 
   expect(isInBloom(bloom, topic)).toBe(true);
 });
+
+test("isFilterInBloom returns false for out of range blocks", () => {
+  const block = {
+    number: "0x5",
+    logsBloom:
+      "0x00000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020000000000000000000000000000000000000000000008000000001000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+  } as const;
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    address: undefined,
+    topic0: null,
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    fromBlock: 10,
+    toBlock: 20,
+    include: undefined,
+  } satisfies LogFilter;
+
+  expect(isFilterInBloom({ block, filter })).toBe(false);
+});
+
+test("isFilterInBloom returns false for missing topics", () => {
+  const block = {
+    number: "0x5",
+    logsBloom:
+      "0x00000000000000000000008000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000044000200000000000000000002000000000000000000000040000000000000000000000000000020000000000000000000800000000000800000000000800000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808002000000000400000000000000000000000060000000000000000000000000000000000000000000000100000000000002000000",
+  } as const;
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    address: undefined,
+    topic0:
+      "0x02c69be41d0b7e40352fc85be1cd65eb03d40ef8427a0ca4596b1ead9a00e9fc",
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    fromBlock: undefined,
+    toBlock: undefined,
+    include: undefined,
+  } satisfies LogFilter;
+
+  expect(isFilterInBloom({ block, filter })).toBe(false);
+});
+
+test("isFilterInBloom returns true for undefined address", () => {
+  const block = {
+    number: "0x5",
+    logsBloom:
+      "0x00000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020000000000000000000000000000000000000000000008000000001000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+  } as const;
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    address: undefined,
+    topic0: null,
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    fromBlock: undefined,
+    toBlock: undefined,
+    include: undefined,
+  } satisfies LogFilter;
+
+  expect(isFilterInBloom({ block, filter })).toBe(true);
+});
+
+test("isFilterInBloom returns true for factory with new child address", () => {
+  const block = {
+    number: "0x5",
+    logsBloom:
+      "0x00000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020000000000000000000000000000000000000000000008000000001000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+  } as const;
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    address: {
+      type: "log",
+      chainId: 1,
+      address: "0xef2d6d194084c2de36e0dabfce45d046b37d1106",
+      eventSelector:
+        "0x02c69be41d0b7e40352fc85be1cd65eb03d40ef8427a0ca4596b1ead9a00e9fc",
+      childAddressLocation: "topic1",
+      fromBlock: undefined,
+      toBlock: undefined,
+    } satisfies LogFactory,
+    topic0: null,
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    fromBlock: undefined,
+    toBlock: undefined,
+    include: undefined,
+  } satisfies LogFilter;
+
+  expect(isFilterInBloom({ block, filter })).toBe(true);
+});
+
+test("isFilterInBloom returns true for factory without new child address", () => {
+  const block = {
+    number: "0x5",
+    logsBloom:
+      "0x00000000000000000000008000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000044000200000000000000000002000000000000000000000040000000000000000000000000000020000000000000000000800000000000800000000000800000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808002000000000400000000000000000000000060000000000000000000000000000000000000000000000100000000000002000000",
+  } as const;
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    address: {
+      type: "log",
+      chainId: 1,
+      address: "0xef2d6d194084c2de36e0dabfce45d046b37d1106",
+      eventSelector:
+        "0x02c69be41d0b7e40352fc85be1cd65eb03d40ef8427a0ca4596b1ead9a00e9fc",
+      childAddressLocation: "topic1",
+      fromBlock: undefined,
+      toBlock: undefined,
+    } satisfies LogFactory,
+    topic0:
+      "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb",
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    fromBlock: undefined,
+    toBlock: undefined,
+    include: undefined,
+  } satisfies LogFilter;
+
+  expect(isFilterInBloom({ block, filter })).toBe(true);
+});
+
+test("isFilterInBloom returns true for array of addresses", () => {
+  const block = {
+    number: "0x5",
+    logsBloom:
+      "0x00000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020000000000000000000000000000000000000000000008000000001000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+  } as const;
+
+  const filter = {
+    type: "log",
+    chainId: 1,
+    address: ["0xef2d6d194084c2de36e0dabfce45d046b37d1106"],
+    topic0: null,
+    topic1: null,
+    topic2: null,
+    topic3: null,
+    fromBlock: undefined,
+    toBlock: undefined,
+    include: undefined,
+  } satisfies LogFilter;
+
+  expect(isFilterInBloom({ block, filter })).toBe(true);
+});
diff --git a/packages/utils/src/_test/somnia.test.ts b/packages/utils/src/_test/somnia.test.ts
new file mode 100644
index 00000000..bd42f863
--- /dev/null
+++ b/packages/utils/src/_test/somnia.test.ts
@@ -0,0 +1,67 @@
+import { RpcRequestError, numberToHex } from "viem";
+import { expect, test } from "vitest";
+import { getLogsRetryHelper } from "../getLogsRetryHelper.js";
+import { type Params, getRequest } from "./utils.js";
+
+const request = getRequest("https://dream-rpc.somnia.network");
+const fromBlock = 53_580_000n;
+const maxBlockRange = 1_000n;
+
+test(
+  "somnia success",
+  async () => {
+    const logs = await request({
+      method: "eth_getLogs",
+      params: [
+        {
+          fromBlock: numberToHex(fromBlock),
+          toBlock: numberToHex(fromBlock + maxBlockRange),
+        },
+      ],
+    });
+
+    expect(logs).toHaveLength(5616);
+  },
+  { timeout: 15_000 },
+);
+
+test(
+  "somnia block range",
+  async () => {
+    const params: Params = [
+      {
+        fromBlock: numberToHex(fromBlock),
+        toBlock: numberToHex(fromBlock + maxBlockRange + 1n),
+      },
+    ];
+
+    const error = await request({
+      method: "eth_getLogs",
+      params,
+    }).catch((error) => error);
+
+    expect(error).toBeInstanceOf(RpcRequestError);
+    expect(JSON.stringify(error)).includes("block range exceeds 1000");
+
+    const retry = getLogsRetryHelper({
+      params,
+      error,
+    });
+
+    expect(retry).toStrictEqual({
+      shouldRetry: true,
+      isSuggestedRange: true,
+      ranges: [
+        {
+          fromBlock: numberToHex(fromBlock),
+          toBlock: numberToHex(fromBlock + maxBlockRange),
+        },
+        {
+          fromBlock: numberToHex(fromBlock + maxBlockRange + 1n),
+          toBlock: numberToHex(fromBlock + maxBlockRange + 1n),
+        },
+      ],
+    });
+  },
+  { timeout: 15_000 },
+);