Skip to content

Comments

Integration of Indexing Status Builder into ENSIndexer API#1614

Open
tk-o wants to merge 24 commits intomainfrom
feat/indexing-status-builder-3
Open

Integration of Indexing Status Builder into ENSIndexer API#1614
tk-o wants to merge 24 commits intomainfrom
feat/indexing-status-builder-3

Conversation

@tk-o
Copy link
Contributor

@tk-o tk-o commented Feb 6, 2026

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • This PR includes an integration of Indexing Status Builder from PR feat(ensindexer): introduce Indexing Status Builder module #1613 with ENSIndexer API
    • ENSIndexer uses a singleton instance of LocalPonderClient to ensure initialization was executed exactly once during application startup.
    • Initializing LocalPonderClient may require retiries, as Ponder HTTP endpoints may not be available when the first client initialization is attempted.

Why

  • We need to properly contain layers of abstraction when it comes to integrating Ponder SDK into ENSIndexer. Three layers apply:
    • Layer 1: Ponder SDK which has 0 external dependencies (apart from the Prometheus Metrics parser dependency).
    • Layer 2: Indexing Status Builder which can build OmnichainIndexingStatusSnapshot based on abstractions from viem and @ensnode/ponder-sdk (and if really needed, for the time being, from @ensnode/ensnode-sdk ).
    • Layer 3: ENSIndexer powering Layer 2 with Ponder APIs.

Testing

  • I ran static code checks (lint, typecheck), and testing suite.
  • I used a local ENSIndexer instance to ensure the RPC calls fetching required block refs are executed exactly once, during application startup.

Notes for Reviewer (Optional)


Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

tk-o added 2 commits February 6, 2026 19:43
This module aims to host an abstraction layer that primarly relies on `viem` and `@ensnode/ponder-sdk` abstractions. It is still using `@ensnode/ensnode-sdk` for convenience, but leave the possiblity to iterate and remove `@ensnode/ensnode-sdk` dependency in the future.
Copilot AI review requested due to automatic review settings February 6, 2026 19:31
@vercel
Copy link
Contributor

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Feb 20, 2026 1:07pm
ensnode.io Ready Ready Preview, Comment Feb 20, 2026 1:07pm
ensrainbow.io Ready Ready Preview, Comment Feb 20, 2026 1:07pm

@changeset-bot
Copy link

changeset-bot bot commented Feb 6, 2026

⚠️ No Changeset found

Latest commit: c51d99e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a LocalPonderClient singleton with retry-based initialization; moves indexing-status handler to fetch per-chain metadata via that client; adds utilities for chain blockrange and block fetching; splits ChainIndexingMetadata into immutable and dynamic parts; and updates p-retry dependency to the workspace catalog.

Changes

Cohort / File(s) Summary
Dependency Management
pnpm-workspace.yaml, apps/ensapi/package.json, apps/ensindexer/package.json
Added p-retry to workspace catalog; replaced a direct version with catalog: in apps/ensapi; added p-retry dependency to ensindexer.
TypeScript config
apps/ensindexer/tsconfig.json
Added path alias "@/ponder/*": ["./ponder/src/*"].
Ponder client bootstrap & singleton
apps/ensindexer/src/lib/ponder-api-client.ts, apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts
New LocalPonderClient class and exported getLocalPonderClient() that lazily initializes the client with retry logic and caches the instance.
Handler refactor
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts
Refactored /indexing-status handler to use getLocalPonderClient() and chainsIndexingMetadata() instead of direct publicClients; updated imports and error/guard handling.
Blockrange & block fetch utilities
apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts, apps/ensindexer/ponder/src/api/lib/fetch-block-ref.ts
Added logic to derive per-chain block ranges from ponder config and to fetch/deserialize block refs via publicClient.
Indexing metadata types
apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts
Split ChainIndexingMetadata into ChainIndexingMetadataImmutable and ChainIndexingMetadataDynamic, then recompose with an intersection type.

Sequence Diagram(s)

sequenceDiagram
    participant App as ENSIndexer App
    participant Loader as getLocalPonderClient()
    participant LocalClient as LocalPonderClient
    participant PonderApp as Ponder App (HTTP)
    participant PublicClient as Chain PublicClient
    participant Handler as /indexing-status Handler

    App->>Loader: request LocalPonderClient
    Loader->>LocalClient: init(ponderAppUrl, indexedChainIds) [with p-retry]
    LocalClient->>PonderApp: fetch ponderConfig, public clients, metrics/status
    PonderApp-->>LocalClient: ponderConfig, ponderPublicClients, metrics/status
    LocalClient->>PublicClient: build per-chain PublicClient map
    LocalClient-->>Loader: initialized LocalPonderClient

    Handler->>Loader: getLocalPonderClient()
    Loader-->>Handler: LocalPonderClient instance
    Handler->>LocalClient: chainsIndexingMetadata()
    LocalClient->>PonderApp: fetch dynamic metrics/status
    PonderApp-->>LocalClient: metrics/status
    LocalClient->>PublicClient: fetch blocks (via fetchBlockRef) as needed
    LocalClient-->>Handler: merged ChainIndexingMetadata per chain
    Handler->>Handler: buildOmnichainIndexingStatusSnapshot()
    Handler-->>Client: return snapshot
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A client wakes with nimble paws and cheer,
It retries thrice until the Ponder's near.
Fetching blocks, configs, metrics in a hop—
Merging snapshots until the queries stop.
ENSIndexer hums; the rabbits clap—hip hop!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective of the PR: integrating the Indexing Status Builder into the ENSIndexer API. It is specific, concise, and directly related to the changeset.
Description check ✅ Passed The PR description follows the repository template with all required sections: Summary, Why, Testing, Notes for Reviewer, and Pre-Review Checklist. All sections are populated with relevant details.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/indexing-status-builder-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Integrates the new “Indexing Status Builder” layer into the ENSIndexer (Ponder) HTTP API by moving /indexing-status snapshot construction onto builder + PonderClient-fetched metadata.

Changes:

  • Added fetchAndBuildCrossChainIndexingStatusSnapshot() to fetch Ponder /metrics + /status, compute chain block refs, and build an omnichain cross-chain snapshot.
  • Added a Ponder-config parsing helper to derive per-chain blockranges from datasource start/end blocks.
  • Updated /indexing-status handler to use the new snapshot builder flow.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
apps/ensindexer/ponder/src/lib/cross-chain-indexing-status-snapshot.ts New builder integration: fetch Ponder metadata, compute block refs, build cross-chain snapshot.
apps/ensindexer/ponder/src/lib/chains-config-blockrange.ts New helper to derive per-chain blockranges from ponder.config.ts datasources.
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts Switches /indexing-status to the new fetch+build snapshot function.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tk-o tk-o force-pushed the feat/indexing-status-builder-3 branch from 17e224c to 8ac1c11 Compare February 9, 2026 20:40
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 9, 2026 20:40 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 9, 2026 20:40 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 9, 2026 20:40 Inactive
@tk-o tk-o linked an issue Feb 10, 2026 that may be closed by this pull request
Copilot AI review requested due to automatic review settings February 10, 2026 18:32
@tk-o tk-o force-pushed the feat/indexing-status-builder-3 branch from 8ac1c11 to 6303636 Compare February 10, 2026 18:32
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 10, 2026 18:32 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 10, 2026 18:32 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 10, 2026 18:32 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tk-o added 4 commits February 11, 2026 15:42
Simplify code, merge input params for `buildOmnichainIndexingStatusSnapshot` into a single input object with chain metadata.
@tk-o tk-o force-pushed the feat/indexing-status-builder-3 branch from 6303636 to f5ecab1 Compare February 18, 2026 07:30
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 18, 2026

Additional Comments (1)

apps/ensindexer/src/lib/indexing-status/build-index-status.ts
Dead code: unused export

The old buildOmnichainIndexingStatusSnapshot function in this file is no longer imported anywhere — ensnode-api.ts now uses the new implementation from @/lib/indexing-status-builder/omnichain-indexing-status-snapshot. Consider removing this function and its associated helpers (getChainsBlockRefsCached, chainsBlockRefs, chainsBlockrange, chainNames) to avoid confusion about which implementation is active.

@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 18, 2026 16:24 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 18, 2026 16:24 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 18, 2026 16:24 Inactive
Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Thanks. Shared a few questions appreciate your advice.

"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@/ponder/*": ["./ponder/src/*"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Appreciate background info that motivated this update. Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update was required so we could use the following style of imports:

import { LocalPonderClient } from "@/ponder/api/lib/local-ponder-client";
import ponderConfig from "@/ponder/config";

Without this update, we'd need to use relative paths, which would also work. However, this @/ponder/* alias is quite nice to apply. Please feel free to share your thoughts on that motivation 👍

import { createCrossChainIndexingStatusSnapshotOmnichain } from "@/lib/indexing-status/build-index-status";
import { fetchAndBuildOmnichainIndexingStatusSnapshot } from "@/ponder/lib/omnichian-indexing-status-snapshot";

const publicClients = new Map<ChainId, PublicClient>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be a great utility function to move into the "local ponder client"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created publicClient(chainId: ChainId): PublicClient method on LocalPonderClient to allow access to Ponder cached RPC clients 👍


// Ponder API dependencies: Ponder config, and PonderClient
// TODO: move into a separate file
const indexedChainIds = Object.keys(ponderConfig.chains).map((maybeChainId) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a special reason we read this from the ponder config and not the ENSIndexer config object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, I think we could use just ENSIndexer config here 👍

/**
* Build a list of indexed chain IDs.
*/
export function buildIndexedChainIds(publicClients: Map<ChainId, PublicClient>): ChainId[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear on some things that are happening here.

I thought I saw some other code that was getting the set of indexed chain ids from the ponder config?

But now this is another function that seems to be getting the set of indexed chain ids from a different source: from a map of ChainId / PublicClient.

Are we doing things here right?

Is there potentially a mixup here caused by how there's a distinction in Ponder between:

  1. The set of RPCs (PublicClients) that are configured.
  2. The set of chains configured for indexing.

Where these two might not be exactly the same? If so we need more precision with our terminology and how we communicate these ideas.

It also seems strange to me to see this function all alone in this file operating on such a specific data structure. Maybe it should live somewhere else?

Appreciate your advice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right, we don't have to build a list of indexed chain IDs based on any Ponder API. All we need is to pass the set of indexed chain IDs from ENSIndexer config into the Local Ponder Client.

* @throws Error if any of the required data cannot be fetched or is invalid,
* or if invariants are violated.
*/
async function buildChainsIndexingMetadataFixed(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "fixed" mean? Does it mean "immutable"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it means "immutable", which I think suits here better than "fixed". Will apply renames accordingly 👍

* @returns The initialized LocalPonderClient instance.
* @throws Error if the client fails to initialize after the specified number of retries.
*/
localPonderClientPromise = pRetry(() => LocalPonderClient.init(config.ensIndexerUrl), {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Why are we trying to force the local ponder client to init this way upon process startup?

I assume we're doing this because of a strategy to eagerly load some state into the local ponder client that requires network fetches? But why do we need such an eager strategy? Why wouldn't a lazy strategy work nice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we'll change the approach and will rely on data fetching done by SWR caches so that the client initialization could remain sync. More in PR #1652.

Updates LocalPonderClient dependency to favour Ponder SDK types
Copilot AI review requested due to automatic review settings February 19, 2026 12:06
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 19, 2026 12:06 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 19, 2026 12:06 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 19, 2026 12:06 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 12 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts`:
- Around line 43-53: The JSDoc for PonderConfigDatasources duplicates the
singular comment used for PonderConfigDatasource; update the comment above the
type PonderConfigDatasources to a distinct, plural description (e.g., "Ponder
config datasources" or "Collection of Ponder config datasources") so it
accurately documents the map type instead of repeating the singular description
for PonderConfigDatasource.
- Line 152: Fix the typo in the inline comment that currently reads "Get the
highest endBLock for the chain." — change "endBLock" to "endBlock" so the
comment correctly references the endBlock concept used in the
chains-config-blockrange logic; ensure any other nearby occurrences of the same
misspelling in the same file are corrected to "endBlock".
- Around line 175-181: The second guard checking typeof blockrange.startBlock
=== "undefined" is unreachable; remove that throw and instead assert the type of
the returned value from deserializeBlockrange so TypeScript knows startBlock is
present. Replace the runtime guard with a type assertion/cast to
BlockrangeWithStartBlock (or add an inline assertion comment) when calling
chainsBlockrange.set(chainId, ...) after
deserializeBlockrange(chainLowestStartBlock, ...), referencing
deserializeBlockrange, blockrange, chainLowestStartBlock,
BlockrangeWithStartBlock, chainsBlockrange.set and serializedChainId to locate
the code.
- Around line 88-92: The type guard isPonderDatasourceNested incorrectly treats
null as an object; update it to explicitly exclude null (and arrays if
applicable) before returning true. Change the implementation to check
ponderDatasource.chain !== null && typeof ponderDatasource.chain === "object"
(and add && !Array.isArray(ponderDatasource.chain) if chain should be a plain
object) so downstream access like ponderSource.chain[serializedChainId] is safe.

In `@apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts`:
- Around line 53-60: The JSDoc for the exported type ChainIndexingMetadata still
uses the outdated term "fixed metadata"; update that comment to say "immutable
metadata" to match the renamed type ChainIndexingMetadataImmutable and ensure
consistency — e.g., replace "fixed metadata (backfill scope and indexing
config)" with "immutable metadata (backfill scope and indexing config)" in the
block above the ChainIndexingMetadata declaration.

---

Duplicate comments:
In `@apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts`:
- Around line 110-114: The inline cast on ponderSources hides type errors —
remove the blanket "as PonderConfigDatasource[]" and ensure each source is
correctly typed or filtered: either change
ponderConfig.accounts/blocks/contracts to have values of PonderConfigDatasource,
or create a type‑guard function (e.g. isPonderConfigDatasource) and build
ponderSources by concatenating Object.values(...) and filtering with that guard
so only valid PonderConfigDatasource items remain; reference symbols:
ponderSources, ponderConfig, PonderConfigDatasource, and the type‑guard you add.

In `@apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts`:
- Around line 30-51: No changes required—the JSDoc title was corrected and the
interface ChainIndexingMetadataDynamic with non-optional fields indexingMetrics
and indexingStatus is correct; leave the interface and its doc-comment as-is.

// ponderSource for that chain has its respective `endBlock` defined.
const isEndBlockForChainAllowed = chainEndBlocks.length === chainStartBlocks.length;

// 3.b) Get the highest endBLock for the chain.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Typo: "endBLock" → "endBlock".

-    // 3.b) Get the highest endBLock for the chain.
+    // 3.b) Get the highest endBlock for the chain.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 3.b) Get the highest endBLock for the chain.
// 3.b) Get the highest endBlock for the chain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts` at line 152,
Fix the typo in the inline comment that currently reads "Get the highest
endBLock for the chain." — change "endBLock" to "endBlock" so the comment
correctly references the endBlock concept used in the chains-config-blockrange
logic; ensure any other nearby occurrences of the same misspelling in the same
file are corrected to "endBlock".

Comment on lines +175 to +181
if (typeof blockrange.startBlock === "undefined") {
throw new Error(
`Invalid blockrange for chain '${serializedChainId}'. The blockrange must include a valid startBlock number.`,
);
}

chainsBlockrange.set(chainId, blockrange as BlockrangeWithStartBlock);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

The second startBlock guard at lines 175–179 is unreachable at runtime.

After the throw at lines 161–165, chainLowestStartBlock is a valid BlockNumber, so deserializeBlockrange will either throw on schema failure or return with startBlock present. The if (typeof blockrange.startBlock === "undefined") branch can never execute.

The guard exists only to satisfy TypeScript's inferred return type of deserializeBlockrange (Blockrange with optional startBlock) before the cast on line 181. Consider using an assertion comment or a type assertion directly to make the intent clearer without the misleading dead throw:

♻️ Proposed simplification
-    // Invariant: the blockrange must include a valid startBlock number.
-    if (typeof blockrange.startBlock === "undefined") {
-      throw new Error(
-        `Invalid blockrange for chain '${serializedChainId}'. The blockrange must include a valid startBlock number.`,
-      );
-    }
-
-    chainsBlockrange.set(chainId, blockrange as BlockrangeWithStartBlock);
+    // startBlock is guaranteed non-undefined: chainLowestStartBlock was validated above.
+    chainsBlockrange.set(chainId, blockrange as BlockrangeWithStartBlock);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof blockrange.startBlock === "undefined") {
throw new Error(
`Invalid blockrange for chain '${serializedChainId}'. The blockrange must include a valid startBlock number.`,
);
}
chainsBlockrange.set(chainId, blockrange as BlockrangeWithStartBlock);
// startBlock is guaranteed non-undefined: chainLowestStartBlock was validated above.
chainsBlockrange.set(chainId, blockrange as BlockrangeWithStartBlock);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/ponder/src/api/lib/chains-config-blockrange.ts` around lines
175 - 181, The second guard checking typeof blockrange.startBlock ===
"undefined" is unreachable; remove that throw and instead assert the type of the
returned value from deserializeBlockrange so TypeScript knows startBlock is
present. Replace the runtime guard with a type assertion/cast to
BlockrangeWithStartBlock (or add an inline assertion comment) when calling
chainsBlockrange.set(chainId, ...) after
deserializeBlockrange(chainLowestStartBlock, ...), referencing
deserializeBlockrange, blockrange, chainLowestStartBlock,
BlockrangeWithStartBlock, chainsBlockrange.set and serializedChainId to locate
the code.

@tk-o tk-o force-pushed the feat/indexing-status-builder-3 branch from 34db972 to b8b2f19 Compare February 20, 2026 10:39
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 20, 2026 10:40 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 20, 2026 10:40 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 20, 2026 10:40 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts`:
- Around line 195-211: The init path (LocalPonderClient.init) currently builds
publicClients (via buildPublicClientsMap) and chainIndexingMetadataImmutable
(via buildChainsIndexingMetadataImmutable) but does not validate that the
provided indexedChainIds are present in those maps, causing runtime errors in
chainsIndexingMetadata; update init to validate that every id in indexedChainIds
exists in the publicClients (or chainIndexingMetadataImmutable) keys and, if any
are missing, throw a clear startup error listing the missing ChainId(s) so the
app fails fast with an actionable message.

---

Duplicate comments:
In `@apps/ensindexer/ponder/src/api/lib/local-ponder-client.ts`:
- Around line 76-80: The current check in local-ponder-client.ts uses if
(!chainConfigBlockrange.startBlock) which erroneously treats a valid genesis
block 0 as missing; update the conditional in the function handling
chainConfigBlockrange (referencing chainConfigBlockrange.startBlock) to use an
explicit nullish check (e.g., if (chainConfigBlockrange.startBlock == null) or
=== undefined) so that 0 is accepted, and keep the same error throw when truly
null/undefined.

In `@apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts`:
- Around line 53-60: Update the JSDoc for the ChainIndexingMetadata type to use
"immutable metadata" instead of "fixed metadata" to match the renamed interface;
edit the comment block above the exported type (referencing
ChainIndexingMetadata, ChainIndexingMetadataImmutable and
ChainIndexingMetadataDynamic) so the description reads "...Combines both the
immutable metadata (backfill scope and indexing config) and the dynamic
metadata..." for consistency.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 12 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


if (chainIndexingMetrics.state !== ChainIndexingStates.Historical) {
throw new Error(
`In order to build 'ChainsIndexingMetadataFixed', chain indexing state must be "historical" for indexed chain ID ${chainId}, but got "${chainIndexingMetrics.state}"`,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message references 'ChainsIndexingMetadataFixed', but the actual type name is 'ChainIndexingMetadataImmutable' (singular 'Chain' and 'Immutable' instead of 'Chains' and 'Fixed'). This inconsistency could confuse developers debugging issues. The error message should reference the correct type name.

Suggested change
`In order to build 'ChainsIndexingMetadataFixed', chain indexing state must be "historical" for indexed chain ID ${chainId}, but got "${chainIndexingMetrics.state}"`,
`In order to build 'ChainIndexingMetadataImmutable', chain indexing state must be "historical" for indexed chain ID ${chainId}, but got "${chainIndexingMetrics.state}"`,

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +38
/**
* Initialize the LocalPonderClient by connecting to the local Ponder app and
* fetching necessary data to build the client's state. This operation is
* retried up to 3 times in case of failure, with a warning logged on each
* failed attempt.
*
* @returns The initialized LocalPonderClient instance.
* @throws Error if the client fails to initialize after the specified number of retries.
*/
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This JSDoc comment block (lines 30-38) is redundant and appears to be leftover documentation that should have been removed. It duplicates the function-level JSDoc comment above (lines 9-20) and is placed incorrectly within the function body rather than before a function declaration. The inline comments on lines 27-29 already explain what's happening at this point in the code.

Suggested change
/**
* Initialize the LocalPonderClient by connecting to the local Ponder app and
* fetching necessary data to build the client's state. This operation is
* retried up to 3 times in case of failure, with a warning logged on each
* failed attempt.
*
* @returns The initialized LocalPonderClient instance.
* @throws Error if the client fails to initialize after the specified number of retries.
*/

Copilot uses AI. Check for mistakes.
// ponderSource for that chain has its respective `endBlock` defined.
const isEndBlockForChainAllowed = chainEndBlocks.length === chainStartBlocks.length;

// 3.b) Get the highest endBLock for the chain.
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: "endBLock" should be "endBlock" (lowercase 'l' in "Block").

Suggested change
// 3.b) Get the highest endBLock for the chain.
// 3.b) Get the highest endBlock for the chain.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built Ponder Client and rename ponder-metadata to ponder-sdk

2 participants