Skip to content

Local Ponder Client & Indexing Status Builder to power ENSIndexer#1675

Merged
tk-o merged 30 commits intomainfrom
local-ponder-client-2.0
Mar 1, 2026
Merged

Local Ponder Client & Indexing Status Builder to power ENSIndexer#1675
tk-o merged 30 commits intomainfrom
local-ponder-client-2.0

Conversation

@tk-o
Copy link
Contributor

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

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • Introduces a simple Ponder SDK with LocalPonderClient class definition
  • Introduces a simple IndexingStatusBuilder class, wrapping LocalPonderClient instance and making RPC calls (only once, cached afterwards).
  • Integrates IndexingStatusBuilder into Indexing Status API for ENSIndexer.
  • Extends ENSNode SDK and Ponder SDK with functionality for working with blockranges.

Why


Testing

  • Tested Indexing Status API endpoint and it worked as expected.
  • Unit tests to be added.

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 5 commits February 24, 2026 20:54
The local client extends PonderClient, including override for the `metrics()` method to ensure the backfillEndBlock field is available in indexing metrics for each indexed chain.
…e SDK

This function uses well known types from ENSNode SDK, so it as well might live in ENSNode SDK module for Indexing Status
…ckrange map from ENSIndexer config object.
The builder can produce `OmnichainIndexingStatusSnapshot` with just `LocalPonderClient` instance and some cache RPC calls.
Copilot AI review requested due to automatic review settings February 24, 2026 20:02
@changeset-bot
Copy link

changeset-bot bot commented Feb 24, 2026

🦋 Changeset detected

Latest commit: 7e6d97a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@ensnode/ensnode-sdk Major
@ensnode/ponder-sdk Major
ensindexer Major
ensadmin Major
ensapi Major
ensrainbow Major
fallback-ensapi Major
@namehash/ens-referrals Major
@ensnode/ensnode-react Major
@ensnode/ensrainbow-sdk Major
@namehash/namehash-ui Major
@ensnode/datasources Major
@ensnode/ponder-metadata Major
@ensnode/ensnode-schema Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major

Not sure what this means? Click here to learn what changesets are.

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

@vercel
Copy link
Contributor

vercel bot commented Feb 24, 2026

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

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
admin.ensnode.io Skipped Skipped Mar 1, 2026 1:06pm
ensnode.io Skipped Skipped Mar 1, 2026 1:06pm
ensrainbow.io Skipped Skipped Mar 1, 2026 1:06pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 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

This PR introduces LocalPonderClient, a wrapper around PonderClient for local indexing with filtered chains, and IndexingStatusBuilder to construct omnichain indexing status snapshots. It adds utility functions for block range merging, new types for local metrics, and updates the ENS indexer API handler accordingly. Includes comprehensive tests and mocks.

Changes

Cohort / File(s) Summary
LocalPonderClient Core
packages/ponder-sdk/src/local-ponder-client.ts
New class extending PonderClient with filtered chain support, block range accessors, cached public client retrieval, and enriched local metrics via buildLocalPonderIndexingMetrics.
LocalPonderClient Mocks & Tests
packages/ponder-sdk/src/local-ponder-client.mock.ts, packages/ponder-sdk/src/local-ponder-client.test.ts
Mock factory function createLocalPonderClientMock with default overrides and comprehensive test suite validating constructor filtering, block range/client access, metrics enrichment, and error handling for missing chains.
LocalPonderClient Supporting Types
packages/ponder-sdk/src/local-indexing-metrics.ts, packages/ponder-sdk/src/indexing-blocks.ts, packages/ponder-sdk/src/cached-public-client.ts
New interfaces for local metrics (LocalChainIndexingMetricsHistorical, LocalPonderIndexingMetrics), chain block ranges, and cached public client type extension.
IndexingStatusBuilder Implementation
apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts
New class building omnichain indexing status snapshots by fetching metrics, caching block refs, and constructing per-chain snapshots with state-specific validation (Queued, Backfill, Completed, Following).
IndexingStatusBuilder Mocks & Tests
apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.mock.ts, apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.test.ts
Mock utilities and extensive test suite covering all omnichain/chain snapshot states, caching behavior, RPC call tracking, error handling, and retry semantics.
Omnichain Status & Block Range Utilities
packages/ensnode-sdk/src/indexing-status/omnichain-indexing-status-snapshot.ts, packages/ensnode-sdk/src/shared/blockrange.ts, packages/ensnode-sdk/src/shared/config/indexed-blockranges.ts
New buildOmnichainIndexingStatusSnapshot function, mergeBlockranges utility with type-safe overloads, and buildIndexedBlockranges for aggregating contract block ranges across plugins/datasources.
ENSNode SDK Tests
packages/ensnode-sdk/src/indexing-status/omnichain-indexing-status-snapshot.test.ts, packages/ensnode-sdk/src/shared/blockrange.test.ts, packages/ensnode-sdk/src/shared/config/indexed-blockranges.test.ts
Test coverage for omnichain snapshot building across all status states, block range merging logic, and indexed blockrange aggregation with error scenarios.
ENSNode SDK Types & Exports
packages/ensnode-sdk/src/shared/types.ts, packages/ensnode-sdk/src/index.ts
New BlockrangeWithStartBlock interface with required start and optional end block; re-exports indexed blockranges configuration module.
ENS Indexer Integration
apps/ensindexer/src/lib/clients.ts, apps/ensindexer/src/lib/plugin-helpers.ts, apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts
New localPonderClient instance initialization with filtered blockranges; new getPluginsRequiredDatasourceNames helper; handler refactor replacing buildOmnichainIndexingStatusSnapshot(publicClients) with IndexingStatusBuilder.getOmnichainIndexingStatusSnapshot().
Configuration & SDK Exports
apps/ensindexer/src/config/types.ts, packages/ponder-sdk/src/index.ts, packages/ponder-sdk/src/deserialize/chains.ts, packages/ponder-sdk/package.json
Rename ENSIndexerConfig to EnsIndexerConfig with backward-compatible deprecation alias; re-export new SDK modules (local-ponder-client, indexing-blocks, local-indexing-metrics); add deserializeChainId function; add viem to dev and peer dependencies.
Changesets
.changeset/great-ends-press.md, .changeset/khaki-pandas-reply.md, .changeset/orange-windows-say.md, .changeset/proud-teeth-fall.md, .changeset/fifty-dingos-send.md
Minor version bumps for @ensnode/ponder-sdk, @ensnode/ensnode-sdk, and ensindexer documenting new LocalPonderClient, buildOmnichainIndexingStatusSnapshot, buildIndexedBlockranges, IndexingStatusBuilder, and mergeBlockranges additions.

Sequence Diagram(s)

sequenceDiagram
    participant Handler as ENS API Handler
    participant Builder as IndexingStatusBuilder
    participant LocalClient as LocalPonderClient
    participant PonderClient as PonderClient
    participant PublicClient as PublicClient(s)
    
    Handler->>Builder: getOmnichainIndexingStatusSnapshot()
    
    Builder->>LocalClient: metrics()
    LocalClient->>PonderClient: metrics()
    PonderClient-->>LocalClient: PonderIndexingMetrics
    LocalClient->>LocalClient: enrichMetrics (add backfillEndBlock)
    LocalClient-->>Builder: LocalPonderIndexingMetrics
    
    Builder->>Builder: fetchChainsIndexingBlockRefs()
    Builder->>PublicClient: getBlock(startBlock)
    PublicClient-->>Builder: BlockRef
    Builder->>PublicClient: getBlock(backfillEndBlock)
    PublicClient-->>Builder: BlockRef
    Builder->>Builder: cache block refs
    
    Builder->>Builder: buildChainIndexingStatusSnapshots()
    Builder->>Builder: For each chain: buildChainIndexingStatusSnapshot()
    Builder->>Builder: Determine state: Queued/Backfill/Completed/Following
    
    Builder->>Builder: buildOmnichainIndexingStatusSnapshot(chainSnapshots)
    Builder->>Builder: Validate & construct omnichain snapshot
    
    Builder-->>Handler: OmnichainIndexingStatusSnapshot
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • PR #1604: Adds PonderClient.metrics() API and PonderIndexingMetrics types that LocalPonderClient directly wraps and enriches; foundational to this PR's metrics handling.
  • PR #1612: Modifies the Ponder SDK's indexing data model (chain metrics variants, checkpoint blocks) which LocalPonderClient and IndexingStatusBuilder consume and build upon.
  • PR #1629: Refactors omnichain indexing status validators and schemas that buildOmnichainIndexingStatusSnapshot and IndexingStatusBuilder depend on for snapshot construction/validation.

Poem

🐰 A LocalClient hops through blocks so fine,
With builders merging ranges in a line,
Snapshots bloom across each indexed chain,
From metrics gathered, omnichain's domain!
Cache and build with structured grace so bright,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main changes: introducing a LocalPonderClient and IndexingStatusBuilder to power ENSIndexer, which aligns with the core objective.
Description check ✅ Passed The PR description follows the required template structure with all major sections completed: Summary, Why, Testing, Notes for Reviewer, and Pre-Review Checklist.

✏️ 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 local-ponder-client-2.0

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

This PR introduces a new “local Ponder client” + “indexing status builder” layer and wires it into the ENSIndexer /indexing-status API, aiming to simplify/standardize how omnichain indexing snapshots are built using cached RPC lookups.

Changes:

  • Add @ensnode/ponder-sdk LocalPonderClient + “local” indexing metrics/blocks types, and export them from the SDK.
  • Add IndexingStatusBuilder in ENSIndexer to build OmnichainIndexingStatusSnapshot, including cached block ref fetching via viem PublicClient.
  • Wire the builder into the Ponder API handler; add config helpers/types for chain block ranges.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pnpm-lock.yaml Locks viem addition for the workspace.
packages/ponder-sdk/package.json Adds viem as dev + peer dependency for the new client integration.
packages/ponder-sdk/src/index.ts Exports new local client/metrics/blocks APIs.
packages/ponder-sdk/src/indexing-blocks.ts Defines ChainIndexingBlocks input for block ref fetching.
packages/ponder-sdk/src/local-indexing-metrics.ts Defines “local” metrics types (notably backfill end block enrichment).
packages/ponder-sdk/src/local-ponder-client.ts Implements LocalPonderClient that enriches/filters Ponder metrics and holds cached PublicClients and block ranges.
packages/ensnode-sdk/src/shared/types.ts Adds BlockrangeWithStartBlock in ensnode-sdk shared types.
packages/ensnode-sdk/src/indexing-status/omnichain-indexing-status-snapshot.ts Adds buildOmnichainIndexingStatusSnapshot(Map<...>) helper with validation.
apps/ensindexer/src/config/types.ts Renames ENSIndexerConfig interface to EnsIndexerConfig + provides deprecated alias.
apps/ensindexer/src/config/chains-blockrange.ts Builds per-chain block ranges derived from plugins/datasources.
apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts New builder that fetches block refs (cached) and builds chain + omnichain snapshots.
apps/ensindexer/ponder/src/api/local-ponder-client.ts Constructs singleton LocalPonderClient from Ponder publicClients + derived block ranges.
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts Switches /indexing-status to use IndexingStatusBuilder.
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
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

BlockrangeWithStartBlock interface is duplicated in both packages/ponder-sdk/src/blocks.ts and packages/ensnode-sdk/src/shared/types.ts, violating DRY principle and creating maintenance burden

Fix on Vercel

Copy link
Contributor

@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: 7

🤖 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/src/config/chains-blockrange.ts`:
- Around line 11-18: Remove the redundant `@returns` JSDoc tags that merely
restate the summary in the JSDoc for the function that "Build[s] a map of
indexed chains to their corresponding blockranges" (the comment block above the
blockrange builder) and the similar JSDoc at lines 48-55; keep the summary and
`@param` tags but delete the duplicate `@returns` entries so the doc follows the
guideline against restating the method summary.
- Around line 64-69: The current endBlock calculation can drop a previously
computed currentBlockrange.endBlock when the new contract lacks an endBlock;
change the logic so that if contract.endBlock is present use
Math.max(currentBlockrange.endBlock, contract.endBlock) (when
currentBlockrange.endBlock exists) but if contract.endBlock is absent preserve
currentBlockrange.endBlock (and only fall back to contract.endBlock when
currentBlockrange.endBlock is undefined). Update the expression that assigns
endBlock (referencing currentBlockrange, contract.endBlock, and endBlock) to
prefer currentBlockrange.endBlock when contract.endBlock is missing, ensuring
the chain’s blockrange never widens due to a missing contract endBlock.

In `@apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts`:
- Around line 41-47: Remove the redundant `@returns` JSDoc tags that simply
restate each method summary in the new JSDoc blocks; for example, edit the JSDoc
for getOmnichainIndexingStatusSnapshot() to delete the redundant `@returns` line
(or replace it with a more informative return description if needed). Apply the
same change to the other JSDoc blocks flagged in this review (the ones at the
other ranges in this file) so JSDoc only contains meaningful `@returns` content or
none at all.
- Around line 181-194: The current loop throws when a chain's metric isn't
Historical; instead, remove the throw and handle non‑historical states by only
using backfillEndBlock when present: for each entry in
localChainsIndexingMetrics check chainIndexingMetric.state ===
ChainIndexingStates.Historical and set a local backfillEndBlock variable (or
leave it undefined) accordingly, then call
this.fetchChainIndexingBlockRefs(chainId, { startBlock, endBlock,
backfillEndBlock }) using that variable; ensure you still call
this.localPonderClient.getChainBlockrange(chainId) and process results for all
chains rather than aborting on non‑historical states.

In
`@packages/ensnode-sdk/src/indexing-status/omnichain-indexing-status-snapshot.ts`:
- Around line 344-349: Remove the redundant `@returns` JSDoc in the comment block
above the function that "Build[s] an Omnichain Indexing Status Snapshot" (the
JSDoc immediately preceding the omnichain snapshot builder function); either
delete the `@returns` line or replace it with a more specific semantic description
if additional return detail is needed, leaving only the summary and any
non-redundant tags.

In `@packages/ensnode-sdk/src/shared/types.ts`:
- Around line 122-141: Replace the duplicated invariant comments by documenting
the invariant once on the type alias instead of on each field: convert or
replace the current interface BlockrangeWithStartBlock with a type alias
(keeping the same name) that has a single doc comment stating the invariant that
startBlock is required and, when endBlock is present, it is greater than
startBlock; then simplify the field docs for startBlock and endBlock to minimal
single-line descriptions (or remove them) and remove the duplicate invariant
text from startBlock and endBlock; reference BlockNumber for types as before.

In `@packages/ponder-sdk/src/local-ponder-client.ts`:
- Around line 40-45: Remove the redundant JSDoc `@returns` lines that merely
restate the summary: edit the JSDoc block that begins "Get the block range for a
specific chain ID" (the getter for the block range) and the other similar JSDoc
blocks flagged in the comment; delete the duplicate `@returns` tags and leave
either no `@returns` or a single concise `@returns` only if it adds new information
(e.g., describes the shape/type), ensuring the remaining comments still describe
thrown errors and parameters (retain `@param` and `@throws` as appropriate).

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e772ab and f7e4c30.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts
  • apps/ensindexer/ponder/src/api/local-ponder-client.ts
  • apps/ensindexer/src/config/chains-blockrange.ts
  • apps/ensindexer/src/config/types.ts
  • apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts
  • packages/ensnode-sdk/src/indexing-status/omnichain-indexing-status-snapshot.ts
  • packages/ensnode-sdk/src/shared/types.ts
  • packages/ponder-sdk/package.json
  • packages/ponder-sdk/src/index.ts
  • packages/ponder-sdk/src/indexing-blocks.ts
  • packages/ponder-sdk/src/local-indexing-metrics.ts
  • packages/ponder-sdk/src/local-ponder-client.ts

@tk-o
Copy link
Contributor Author

tk-o commented Feb 24, 2026

@greptile review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 24, 2026

Greptile Summary

This PR introduces a well-architected Ponder SDK with LocalPonderClient and IndexingStatusBuilder to enhance the ENSIndexer's Indexing Status API. The implementation follows clean separation of concerns with proper abstraction layers.

Key Changes:

  • New LocalPonderClient class extends PonderClient with filtering and enrichment for indexed chains only
  • IndexingStatusBuilder wraps LocalPonderClient to construct omnichain indexing status snapshots
  • Added buildIndexedBlockranges to aggregate blockranges across datasources and contracts
  • Introduced mergeBlockNumberRanges helper for blockrange aggregation
  • Comprehensive unit test coverage for new components

Architecture:
The PR establishes a three-layer architecture: (1) LocalPonderClient provides enhanced Ponder metadata access, (2) IndexingStatusBuilder orchestrates RPC calls and builds status snapshots, and (3) API handlers consume the builder. Both client and builder are module-level singletons with appropriate caching.

Code Quality:
The implementation demonstrates strong type safety with proper TypeScript types, discriminated unions, and validation. Error handling is thorough with descriptive messages. The caching strategy for immutable indexing config is sound and prevents redundant RPC calls after initial fetch.

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - well-tested architectural refactoring
  • Score reflects solid implementation with comprehensive tests and good design patterns. Minor deductions for previously identified performance considerations (sequential chain processing) and documentation issues (docstring clarifications needed) that don't affect correctness but could be optimized.
  • No files require special attention - all implementations are well-structured with proper error handling and validation

Important Files Changed

Filename Overview
packages/ponder-sdk/src/local-ponder-client.ts New LocalPonderClient class that extends PonderClient with filtering, validation, and enrichment of Ponder app metadata for indexed chains only. Includes proper error handling and type safety.
apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts New IndexingStatusBuilder class that integrates LocalPonderClient to build omnichain indexing status snapshots. Caches immutable config to avoid redundant RPC calls. Some known performance considerations with sequential chain processing.
apps/ensindexer/src/lib/clients.ts Module-level singleton creation of localPonderClient using indexed blockranges built from plugin requirements. Clean integration point.
packages/ensnode-sdk/src/shared/config/indexed-blockranges.ts New buildIndexedBlockranges function that aggregates blockranges across datasources and contracts with proper validation ensuring all ranges have start blocks.
apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts Simplified API handler integration using new IndexingStatusBuilder. Replaced previous implementation with cleaner abstraction.
packages/ensnode-sdk/src/indexing-status/omnichain-indexing-status-snapshot.ts Added buildOmnichainIndexingStatusSnapshot function that validates and builds omnichain snapshots from chain status snapshots with proper type narrowing.

Last reviewed commit: 6c47611

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

34 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 25, 2026 15:07 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 25, 2026 15:07 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 25, 2026 15:07 Inactive
@tk-o
Copy link
Contributor Author

tk-o commented Feb 25, 2026

@greptile review

Copy link
Contributor

@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

♻️ Duplicate comments (6)
packages/ponder-sdk/src/local-ponder-client.ts (1)

40-47: Remove redundant @returns tags across multiple methods.

The @returns tags on getChainBlockrange, getCachedPublicClient, metrics, buildLocalPonderIndexingMetrics, and selectEntriesForIndexedChainsOnly restate their respective summaries. As per coding guidelines, "Do not add JSDoc @returns tags that merely restate the method summary; remove redundancy during PR review."

Also applies to lines 58-63, 75-80, 96-105, 167-174.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ponder-sdk/src/local-ponder-client.ts` around lines 40 - 47, Remove
the redundant JSDoc `@returns` tags that merely restate the method summary for the
listed methods: getChainBlockrange, getCachedPublicClient, metrics,
buildLocalPonderIndexingMetrics, and selectEntriesForIndexedChainsOnly; edit
each function's JSDoc to delete the unnecessary `@returns` entry (but retain the
summary and any `@returns` content that provides additional, non-redundant detail
such as error conditions or value shape).
packages/ponder-sdk/src/local-indexing-metrics.ts (2)

10-10: Remove unused import LocalPonderClient.

LocalPonderClient is only referenced in a JSDoc {@link} comment (line 42), which doesn't require a runtime or type-level import. The {@link} will still render as text without the import.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ponder-sdk/src/local-indexing-metrics.ts` at line 10, Remove the
unused type import LocalPonderClient from the top of the file; it’s only
referenced in a JSDoc {`@link`} and doesn’t require a runtime or type import, so
delete the line importing LocalPonderClient to eliminate the unused-import
lint/error and keep the JSDoc link as-is.

4-4: Remove unused import ChainIndexingMetrics.

ChainIndexingMetrics is imported but not referenced in any type definition or code in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ponder-sdk/src/local-indexing-metrics.ts` at line 4, Remove the
unused import ChainIndexingMetrics from the import list in this module; locate
the import statement that includes "ChainIndexingMetrics" (alongside other
imports) and delete just that symbol so the file no longer references an unused
identifier, then run the linter or TypeScript build to ensure no remaining
references need cleanup.
apps/ensindexer/src/config/chains-blockrange.ts (1)

54-54: Remove redundant @returns tag.

The @returns The blockrange for the contract. merely restates the method summary. As per coding guidelines, "Do not add JSDoc @returns tags that merely restate the method summary; remove redundancy during PR review."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/src/config/chains-blockrange.ts` at line 54, Remove the
redundant JSDoc `@returns` tag that restates the summary: delete the line
"@returns The blockrange for the contract." from the JSDoc block above the
function that returns the contract block range (the JSDoc describing the
blockrange for the contract), leaving the descriptive summary without the
duplicate `@returns` entry.
apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts (2)

45-67: First-call Historical-only gate: be aware of the restart-after-transition edge case.

The caching logic at Lines 52–58 requires all chains to be in Historical state on the very first invocation (via assertChainsIndexingMetricsHistorical). If the process restarts after any chain has already transitioned to Completed or Realtime, the first call will throw and block refs will never be cached, making the endpoint permanently broken until a full re-index.

This was flagged in a previous review cycle. If this is intentional (i.e., the builder is always constructed early in the lifecycle before any chain transitions), please add an inline comment documenting that assumption.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts`
around lines 45 - 67, The current getOmnichainIndexingStatusSnapshot() uses
assertChainsIndexingMetricsHistorical() before caching _chainsIndexingBlockRefs,
which throws on first call if any chain has already transitioned and thus breaks
restarts; either remove/relax that strict assertion and instead proceed to call
fetchChainsIndexingBlockRefs() unconditionally (or only validate per-chain
without throwing) so the cache is populated even when some chains are
Completed/Realtime, or if the strict historical-only requirement is intentional,
add a clear inline comment on the constructor/builder and above the
assertChainsIndexingMetricsHistorical() call documenting the lifecycle
assumption; reference getOmnichainIndexingStatusSnapshot,
_chainsIndexingBlockRefs, assertChainsIndexingMetricsHistorical, and
fetchChainsIndexingBlockRefs when making the change.

42-44: Remove redundant @returns tags that merely restate the method summary.

Lines 76, 119–120, 231, and 257 contain @returns tags that restate the method summary without adding new information. As per coding guidelines, Do not add JSDoc @returns tags that merely restate the method summary; remove redundancy during PR review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts`
around lines 42 - 44, Remove redundant JSDoc `@returns` tags that simply restate
the method summary in the IndexingStatusBuilder file: specifically remove the
duplicate `@returns` entries from the JSDoc for getOmnichainIndexingStatusSnapshot
(the "Get Omnichain Indexing Status Snapshot" comment) and the other functions
in the same file whose `@returns` lines merely repeat the summary; keep meaningful
`@returns` only when they add details about the return shape or semantics,
otherwise delete the `@returns` tag.
🤖 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/src/lib/indexing-status-builder/indexing-status-builder.test.ts`:
- Around line 249-261: The test's Realtime mock includes Historical-only fields;
remove `historicalTotalBlocks` and `backfillEndBlock` from the
`localMetricsRealtime` data passed to buildLocalChainsIndexingMetrics so the
Realtime variant only contains Realtime-relevant properties (e.g., `state` and
`latestSyncedBlock`); locate the object keyed by `chainId` in the test where
`ChainIndexingStates.Realtime` is used and drop those two fields to avoid
misleading excess properties.
- Around line 407-447: Add a test in the Error handling suite that simulates an
RPC failure by making the public client throw when fetching a block and asserts
the builder wraps that error with chain and block context: use
buildPublicClientMock() and set publicClientMock.getBlock to
mockRejectedValue(new Error("RPC failure")), construct localMetrics and
localStatus so the chain exists, create localPonderClientMock with
getCachedPublicClient returning the mocked public client and status/metrics as
before, instantiate IndexingStatusBuilder and assert await
expect(builder.getOmnichainIndexingStatusSnapshot()).rejects.toThrowError(/chain
ID 1.*block.*\d+|block.*\d+.*chain ID 1/) (or a regex matching the chain ID and
block number in the wrapped message) to verify the fetchBlockRef catch path
wraps RPC errors with chain ID and block number context.

In `@apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts`:
- Around line 201-220: The current fetchChainsIndexingBlockRefs iterates
localChainsIndexingMetrics and awaits fetchChainIndexingBlockRefs inside a
for-loop, causing sequential RPCs; change it to build an array of promises by
mapping over localChainsIndexingMetrics (use
this.localPonderClient.getChainBlockrange(chainId) and call
this.fetchChainIndexingBlockRefs(chainId, {...}) for each entry), await
Promise.all on that array, then populate and return chainsIndexingBlockRefs Map
from the resolved results so per-chain fetches run in parallel; optionally limit
concurrency if needed.

In `@packages/ponder-sdk/src/local-ponder-client.test.ts`:
- Around line 89-102: The test uses a placeholder {} as PublicClient but only
asserts toBeDefined(); change the assertion to check referential equality so we
validate the exact cached instance is returned: in the test that calls
createLocalPonderClientMock(...) with cachedPublicClients and then
client.getCachedPublicClient(chainIds.Optimism), replace the
expect(...).toBeDefined() with an assertion like expect(clientRef).toBe(the same
map entry value) to confirm getCachedPublicClient returns the exact object
placed into cachedPublicClients.

In `@packages/ponder-sdk/src/local-ponder-client.ts`:
- Around line 17-19: The three private fields indexedChainIds, chainsBlockrange,
and cachedPublicClients in the LocalPonderClient class are only assigned in the
constructor and should be marked readonly to express immutability and prevent
reassignment; update their declarations to "private readonly indexedChainIds:
Set<ChainId>", "private readonly chainsBlockrange: Map<ChainId,
BlockrangeWithStartBlock>", and "private readonly cachedPublicClients:
Map<ChainId, PublicClient>" (constructor assignments remain valid for readonly
fields).

---

Duplicate comments:
In `@apps/ensindexer/src/config/chains-blockrange.ts`:
- Line 54: Remove the redundant JSDoc `@returns` tag that restates the summary:
delete the line "@returns The blockrange for the contract." from the JSDoc block
above the function that returns the contract block range (the JSDoc describing
the blockrange for the contract), leaving the descriptive summary without the
duplicate `@returns` entry.

In `@apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts`:
- Around line 45-67: The current getOmnichainIndexingStatusSnapshot() uses
assertChainsIndexingMetricsHistorical() before caching _chainsIndexingBlockRefs,
which throws on first call if any chain has already transitioned and thus breaks
restarts; either remove/relax that strict assertion and instead proceed to call
fetchChainsIndexingBlockRefs() unconditionally (or only validate per-chain
without throwing) so the cache is populated even when some chains are
Completed/Realtime, or if the strict historical-only requirement is intentional,
add a clear inline comment on the constructor/builder and above the
assertChainsIndexingMetricsHistorical() call documenting the lifecycle
assumption; reference getOmnichainIndexingStatusSnapshot,
_chainsIndexingBlockRefs, assertChainsIndexingMetricsHistorical, and
fetchChainsIndexingBlockRefs when making the change.
- Around line 42-44: Remove redundant JSDoc `@returns` tags that simply restate
the method summary in the IndexingStatusBuilder file: specifically remove the
duplicate `@returns` entries from the JSDoc for getOmnichainIndexingStatusSnapshot
(the "Get Omnichain Indexing Status Snapshot" comment) and the other functions
in the same file whose `@returns` lines merely repeat the summary; keep meaningful
`@returns` only when they add details about the return shape or semantics,
otherwise delete the `@returns` tag.

In `@packages/ponder-sdk/src/local-indexing-metrics.ts`:
- Line 10: Remove the unused type import LocalPonderClient from the top of the
file; it’s only referenced in a JSDoc {`@link`} and doesn’t require a runtime or
type import, so delete the line importing LocalPonderClient to eliminate the
unused-import lint/error and keep the JSDoc link as-is.
- Line 4: Remove the unused import ChainIndexingMetrics from the import list in
this module; locate the import statement that includes "ChainIndexingMetrics"
(alongside other imports) and delete just that symbol so the file no longer
references an unused identifier, then run the linter or TypeScript build to
ensure no remaining references need cleanup.

In `@packages/ponder-sdk/src/local-ponder-client.ts`:
- Around line 40-47: Remove the redundant JSDoc `@returns` tags that merely
restate the method summary for the listed methods: getChainBlockrange,
getCachedPublicClient, metrics, buildLocalPonderIndexingMetrics, and
selectEntriesForIndexedChainsOnly; edit each function's JSDoc to delete the
unnecessary `@returns` entry (but retain the summary and any `@returns` content that
provides additional, non-redundant detail such as error conditions or value
shape).

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7e4c30 and c8765b3.

📒 Files selected for processing (11)
  • apps/ensindexer/ponder/src/api/local-ponder-client.ts
  • apps/ensindexer/src/config/chains-blockrange.test.ts
  • apps/ensindexer/src/config/chains-blockrange.ts
  • apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.mock.ts
  • apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.test.ts
  • apps/ensindexer/src/lib/indexing-status-builder/indexing-status-builder.ts
  • packages/ponder-sdk/src/block-refs.mock.ts
  • packages/ponder-sdk/src/local-indexing-metrics.ts
  • packages/ponder-sdk/src/local-ponder-client.mock.ts
  • packages/ponder-sdk/src/local-ponder-client.test.ts
  • packages/ponder-sdk/src/local-ponder-client.ts

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

19 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copilot AI review requested due to automatic review settings February 25, 2026 15:25
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 25, 2026 15:26 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 25, 2026 15:26 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 28, 2026 20:04 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 28, 2026 20:04 Inactive
Copilot AI review requested due to automatic review settings February 28, 2026 20:05
@tk-o tk-o force-pushed the local-ponder-client-2.0 branch from a4c5c86 to 2b55ec5 Compare February 28, 2026 20:05
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 28, 2026 20:05 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 28, 2026 20:05 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 28, 2026 20:05 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 33 out of 34 changed files in this pull request and generated 4 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.

@tk-o
Copy link
Contributor Author

tk-o commented Feb 28, 2026

@greptile review

Copy link
Contributor Author

@tk-o tk-o left a comment

Choose a reason for hiding this comment

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

Self-review completed.


try {
omnichainSnapshot = await buildOmnichainIndexingStatusSnapshot(publicClients);
omnichainSnapshot = await indexingStatusBuilder.getOmnichainIndexingStatusSnapshot();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Soon, only the ENSDb Writer Worker will call the IndexingStatusBuilder instance.

*
* @param pluginNames - Names of the plugins to retrieve required datasource names for.
*/
export function getPluginsRequiredDatasourceNames(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Useful for creating constructor params for LocalPonderClient class.

Comment on lines +24 to +27
export function buildIndexedBlockranges(
namespace: ENSNamespaceId,
pluginsRequiredDatasourceNames: Map<PluginName, DatasourceName[]>,
): Map<ChainId, BlockNumberRangeWithStartBlock> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uses only ENSNode SDK and datasources abstractions now.

/**
* Deserialize an unvalidated string representation of a chain ID.
*/
export function deserializeChainId(unvalidatedData: Unvalidated<ChainIdString>): ChainId {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used when parsing raw input returned from Ponder API.

*
* References config applied when indexing a given chain.
*/
export interface ChainIndexingConfig {
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 interface includes indexed block reg range for the chain, and also, if applicable, the block ref for the backfill end block.

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 Looks good 👍 Happy with the improved execution. Shared some small comments. Please take the lead to merge when ready

pluginsRequiredDatasourceNames,
);

export const localPonderClient = new LocalPonderClient(
Copy link
Member

Choose a reason for hiding this comment

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

Should this file be renamed from clients.ts to something like local-ponder-client.ts?

/**
* Local Ponder Client
*
* It is a specialized client for interacting with a local Ponder app instance.
Copy link
Member

Choose a reason for hiding this comment

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

The big idea isn't being communicated here correctly yet. What makes a Local Ponder Client "local" is because it has access to state through in-memory Ponder library imports in addition to Ponder's external APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for sharing that example 👍

Copilot AI review requested due to automatic review settings March 1, 2026 13:06
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 1, 2026 13:06 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 1, 2026 13:06 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 1, 2026 13:06 Inactive
@tk-o tk-o merged commit a13e206 into main Mar 1, 2026
18 checks passed
@tk-o tk-o deleted the local-ponder-client-2.0 branch March 1, 2026 13:09
@github-actions github-actions bot mentioned this pull request Mar 1, 2026
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 33 out of 34 changed files in this pull request and generated 5 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.

Comment on lines 20 to +22
export const schemaChainIdString = z
.string({ error: `Value must be a string representing a chain ID.` })
.check(invariant_chainIdStringRepresentsValidChainId)
.pipe(z.preprocess((v) => Number(v), schemaChainId));
.check(invariant_chainIdStringRepresentsValidChainId);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

schemaChainIdString no longer validates that the string represents a valid ChainId per schemaChainId (positive integer). As written, values like "0", "-1", "1.5", or "NaN" will pass the .check(...), which weakens validation in other call sites (e.g. metrics label validation). Consider adding an additional check/refinement that Number(value) successfully parses and passes schemaChainId, while still keeping deserializeChainId() as the conversion helper.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +188
export function mergeBlockNumberRanges(...ranges: BlockNumberRange[]): BlockNumberRange {
if (ranges.length === 0) {
return buildBlockNumberRange(undefined, undefined);
}

let minStartBlock: BlockNumber | undefined;
let maxEndBlock: BlockNumber | undefined;

for (const range of ranges) {
// Update min start block (lower values win, undefined is ignored)
if (range.startBlock !== undefined) {
if (minStartBlock === undefined || range.startBlock < minStartBlock) {
minStartBlock = range.startBlock;
}
}

// Update max end block (higher values win, undefined is ignored)
if (range.endBlock !== undefined) {
if (maxEndBlock === undefined || range.endBlock > maxEndBlock) {
maxEndBlock = range.endBlock;
}
}
}

return buildBlockNumberRange(minStartBlock, maxEndBlock);
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

mergeBlockNumberRanges can throw via buildBlockNumberRange(minStartBlock, maxEndBlock) when combining a left-bounded range with a right-bounded range (or any mix that yields minStartBlock > maxEndBlock), despite the docstring saying it returns a merged range that covers all inputs. Consider treating an undefined start/end as unbounded in that direction (e.g., if any input has startBlock === undefined, keep merged startBlock undefined; similarly for endBlock) or explicitly handling the minStartBlock > maxEndBlock case with a well-defined outcome (return unbounded or throw with a clearer error + docs update).

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +184
export function mergeBlockNumberRanges(...ranges: BlockNumberRange[]): BlockNumberRange {
if (ranges.length === 0) {
return buildBlockNumberRange(undefined, undefined);
}

let minStartBlock: BlockNumber | undefined;
let maxEndBlock: BlockNumber | undefined;

for (const range of ranges) {
// Update min start block (lower values win, undefined is ignored)
if (range.startBlock !== undefined) {
if (minStartBlock === undefined || range.startBlock < minStartBlock) {
minStartBlock = range.startBlock;
}
}

// Update max end block (higher values win, undefined is ignored)
if (range.endBlock !== undefined) {
if (maxEndBlock === undefined || range.endBlock > maxEndBlock) {
maxEndBlock = range.endBlock;
}
}
}

return buildBlockNumberRange(minStartBlock, maxEndBlock);
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

Same issue as in ponder-sdk: mergeBlockNumberRanges can throw if called with a left-bounded range and a right-bounded range (or any mix producing minStartBlock > maxEndBlock), which contradicts the function docs that imply it always returns a covering merged range. Consider treating undefined bounds as unbounded (if any input is unbounded on a side, the merged range must be too) or explicitly handling the minStartBlock > maxEndBlock case with a defined behavior and matching docs/tests.

Copilot uses AI. Check for mistakes.
@@ -1,3 +1,4 @@
import type { RangeTypeIds } from "../shared/blockrange";
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

RangeTypeIds is imported but only referenced in JSDoc. With Biome's recommended rules (including unused import checks), this is likely to be reported as an unused import. Consider removing the import, or (if you need the symbol for docs) adding a targeted Biome ignore comment for unused imports on this line.

Copilot uses AI. Check for mistakes.
expect(publicClientMock.getBlock).toHaveBeenCalledTimes(2); // RPC calls for startBlock, and backfillEndBlock
});

it("retries fetching block refs when RPC call fails", async () => {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This test name suggests the builder performs an internal retry, but the behavior under test appears to be: first call fails, second call succeeds because _immutableIndexingConfig wasn't cached and the whole fetch is attempted again. Consider renaming the test to reflect the actual behavior (e.g. "does not cache partial results and allows a subsequent call to retry").

Suggested change
it("retries fetching block refs when RPC call fails", async () => {
it("does not cache partial results and allows a subsequent call to retry", async () => {

Copilot uses AI. Check for mistakes.
@tk-o tk-o mentioned this pull request Mar 1, 2026
2 tasks
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.

3 participants