Skip to content

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 presents an initial integration of Indexing Status Builder with ENSIndexer API

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

  • How this was tested.
  • If you didn't test it, say why.

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)

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.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
admin.ensnode.io Skipped Skipped Feb 10, 2026 6:32pm
ensnode.io Skipped Skipped Feb 10, 2026 6:32pm
ensrainbow.io Skipped Skipped Feb 10, 2026 6:32pm

@changeset-bot
Copy link

changeset-bot bot commented Feb 6, 2026

⚠️ No Changeset found

Latest commit: 6303636

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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ 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.

* Get a {@link Blockrange} for each indexed chain.
*
* Invariants:
* - every chain include a startBlock,
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Grammar in the invariants list: “every chain include a startBlock” should be “every chain includes a startBlock”.

Suggested change
* - every chain include a startBlock,
* - every chain includes a startBlock,

Copilot uses AI. Check for mistakes.
Comment on lines 24 to 54
/**
* Chain Name
*
* Often use as type for object keys expressing Ponder ideas, such as
* chain status, or chain metrics.
*/
export type ChainName = string;

/**
* Ponder config datasource with a flat `chain` value.
*/
export type PonderConfigDatasourceFlat = {
chain: ChainName;
} & AddressConfig &
Blockrange;

/**
* Ponder config datasource with a nested `chain` value.
*/
export type PonderConfigDatasourceNested = {
chain: Record<ChainName, AddressConfig & Blockrange>;
};

/**
* Ponder config datasource
*/
export type PonderConfigDatasource = PonderConfigDatasourceFlat | PonderConfigDatasourceNested;

/**
* Ponder config datasource
*/
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This file appears to duplicate the existing Ponder-config parsing logic in apps/ensindexer/src/lib/indexing-status/ponder-metadata/config.ts (including identical types and error messages). Duplicating this logic across two locations increases the risk of them drifting apart; consider re-exporting/reusing the existing implementation (or moving it to a shared module) instead of maintaining two copies.

Suggested change
/**
* Chain Name
*
* Often use as type for object keys expressing Ponder ideas, such as
* chain status, or chain metrics.
*/
export type ChainName = string;
/**
* Ponder config datasource with a flat `chain` value.
*/
export type PonderConfigDatasourceFlat = {
chain: ChainName;
} & AddressConfig &
Blockrange;
/**
* Ponder config datasource with a nested `chain` value.
*/
export type PonderConfigDatasourceNested = {
chain: Record<ChainName, AddressConfig & Blockrange>;
};
/**
* Ponder config datasource
*/
export type PonderConfigDatasource = PonderConfigDatasourceFlat | PonderConfigDatasourceNested;
/**
* Ponder config datasource
*/
export type {
ChainName,
PonderConfigDatasourceFlat,
PonderConfigDatasourceNested,
PonderConfigDatasource,
} from "../../../src/lib/indexing-status/ponder-metadata/config";
/**
* Ponder config datasource
*/

Copilot uses AI. Check for mistakes.
type ChainBlockRefs,
getChainsBlockRefs,
} from "@/lib/indexing-status-builder/chain-block-refs";
import { buildCrossChainIndexingStatusSnapshotOmnichain } from "@/lib/indexing-status-builder/corss-chain-indexing-status-snapshot";
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Import path contains a typo (corss-chain-indexing-status-snapshot). Even if the underlying file currently uses that spelling, propagating the typo into new code makes it harder to discover/maintain. Consider renaming the module to cross-chain-indexing-status-snapshot (and updating imports) or adding a correctly spelled re-export to avoid baking in the misspelling.

Suggested change
import { buildCrossChainIndexingStatusSnapshotOmnichain } from "@/lib/indexing-status-builder/corss-chain-indexing-status-snapshot";
import { buildCrossChainIndexingStatusSnapshotOmnichain } from "@/lib/indexing-status-builder/cross-chain-indexing-status-snapshot";

Copilot uses AI. Check for mistakes.
Comment on lines 33 to 36
const crossChainIndexingSnapshot = await fetchAndBuildCrossChainIndexingStatusSnapshot(
publicClients,
snapshotTime,
);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

/indexing-status no longer handles failures from fetchAndBuildCrossChainIndexingStatusSnapshot(). Any thrown error will be caught by the global app.onError handler and returned as { message: "Internal Server Error" }, which breaks the ENSNode API contract that callers can always discriminate on responseCode (should return IndexingStatusResponseCodes.Error with HTTP 500). Wrap this call in a try/catch and return a serialized error response on failure (optionally log the underlying error).

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 46
// TODO: make it a cached call, so the RPC requests are performed just once, at the application startup
const chainsBlockRefs: Map<number, ChainBlockRefs> = await getChainsBlockRefs(
indexedChainIds,
chainsConfigBlockrange,
ponderIndexingMetrics.chains,
publicClients,
);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This implementation performs RPC calls on every /indexing-status request via getChainsBlockRefs(...) (which fetches multiple block refs per chain). Previously this endpoint used a cached block-ref lookup; without caching this can add significant latency/load. Consider memoizing the computed chainsBlockRefs (and/or the chainsConfigBlockrange) similarly to the existing cached approach so the RPC calls happen once per process/startup, not per request.

Copilot uses AI. Check for mistakes.
]);

// TODO: make it a cached call, so the RPC requests are performed just once, at the application startup
const chainsBlockRefs: Map<number, ChainBlockRefs> = await getChainsBlockRefs(
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

chainsBlockRefs is typed as Map<number, ChainBlockRefs>, but getChainsBlockRefs() returns Map<ChainId, ChainBlockRefs>. Using number here weakens type safety and can hide key-type mismatches; prefer Map<ChainId, ChainBlockRefs> to keep the type consistent end-to-end.

Suggested change
const chainsBlockRefs: Map<number, ChainBlockRefs> = await getChainsBlockRefs(
const chainsBlockRefs: Map<ChainId, ChainBlockRefs> = await getChainsBlockRefs(

Copilot uses AI. Check for mistakes.
Comment on lines 24 to 31
/**
* Chain Name
*
* Often use as type for object keys expressing Ponder ideas, such as
* chain status, or chain metrics.
*/
export type ChainName = string;

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The code/documentation refers to ChainName, but in this codebase ponderConfig.chains keys are chain IDs stringified (see chainsConnectionConfig() using [chainId.toString()]). Using ChainName = string here is misleading and makes it easier to accidentally pass non-chain-id keys. Consider renaming this to something like ChainIdString (or reusing the SDK’s ChainIdString type) and updating the related comments accordingly.

Copilot uses AI. Check for mistakes.
@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
* Build {@link Blockrange} for each indexed chain.
*
* Invariants:
* - every chain include a startBlock,
Copy link
Contributor

@vercel vercel bot Feb 9, 2026

Choose a reason for hiding this comment

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

Grammar error in comment: "every chain include a startBlock" should use singular verb "includes"

Fix on Vercel

deserializeChainId,
} from "@ensnode/ensnode-sdk";

/**
Copy link
Contributor

@vercel vercel bot Feb 9, 2026

Choose a reason for hiding this comment

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

Code duplication in Ponder config parsing logic across two files creates maintenance burden and risk of divergence

Fix on Vercel

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.

import { deserializeChainId } from "@ensnode/ensnode-sdk";
import { type ChainId, PonderClient } from "@ensnode/ponder-sdk";

import type { ChainBlockRefs } from "@/lib/indexing-status/ponder-metadata";
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

ChainBlockRefs is imported from @/lib/indexing-status/ponder-metadata, but fetchChainsBlockRefs returns ChainBlockRefs from @/lib/indexing-status-builder/chain-block-refs. These are different types (ensnode-sdk vs ponder-sdk BlockRef) and will likely make the returned Map<ChainId, ChainBlockRefs> not typecheck. Import ChainBlockRefs from the same module as fetchChainsBlockRefs (or re-export a single canonical type) so the map value type is consistent.

Suggested change
import type { ChainBlockRefs } from "@/lib/indexing-status/ponder-metadata";
import type { ChainBlockRefs } from "@/lib/indexing-status-builder/chain-block-refs";

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +55
// TODO: this operation may fail, so it should be wrapped in auto-retry logic.
// pRetry could be used, with a retry strategy that includes
// exponential backoff and jitter, to avoid overwhelming the RPC endpoints
// in case of transient errors or rate limits.
export const cachedChainsBlockRefs = await initializeCachedChainsBlockRefs();

async function initializeCachedChainsBlockRefs(): Promise<Readonly<Map<ChainId, ChainBlockRefs>>> {
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

cachedChainsBlockRefs is initialized using a top-level await, which performs network requests (Ponder metrics + RPC block fetches) at module import time. If any of these calls fail, the whole module import fails and the API server may not start. Consider lazy initialization (e.g., an async getter with memoization) and/or moving initialization into the server startup sequence with explicit retry/backoff and a clear failure mode.

Copilot uses AI. Check for mistakes.
throw new Error("Failed to fetch chainsBlockRefs: no block refs found");
}

return Object.freeze(chainsBlockRefs);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Object.freeze(chainsBlockRefs) does not make a Map immutable (callers can still mutate via .set()/.delete()). If immutability is required, avoid exporting a mutable Map instance (e.g., return a ReadonlyMap reference that isn’t shared, or wrap access behind functions and keep the mutable map private).

Suggested change
return Object.freeze(chainsBlockRefs);
return chainsBlockRefs as Readonly<Map<ChainId, ChainBlockRefs>>;

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +144
// 3.a) The endBlock can only be set for a chain if and only if every
// ponderSource for that chain has its respective `endBlock` defined.
const isEndBlockForChainAllowed = chainEndBlocks.length === chainStartBlocks.length;

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

isEndBlockForChainAllowed is computed as chainEndBlocks.length === chainStartBlocks.length, but chainStartBlocks only counts sources where startBlock is a number. This can incorrectly allow an endBlock when some datasources for the chain use non-numeric startBlock (e.g. "latest") or otherwise weren’t counted. Track the total number of datasources that match the chain separately and only allow endBlock if all matching datasources provide a numeric endBlock (and consider failing fast if any matching datasource has a non-numeric startBlock, since it’s described as unsupported).

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 10, 2026

Choose a reason for hiding this comment

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

Typo in comment: endBLock should be endBlock.

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.
Comment on lines +99 to +101
export function buildChainsBlockrange(ponderConfig: PonderConfigType): Map<ChainId, Blockrange> {
const chainsBlockrange = new Map<ChainId, Blockrange>();

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

buildChainsBlockrange introduces non-trivial parsing/invariant logic (flat vs nested datasources, min start block, conditional end block). There are existing Vitest suites for indexing-status/ponder-metadata; adding focused unit tests for this function (including mixed flat/nested configs and partial endBlock definitions) would help prevent regressions.

Copilot uses AI. Check for mistakes.
*/
// TODO: this operation may fail, so it should be wrapped in auto-retry logic.
// pRetry could be used, with a retry strategy that includes
// exponential backoff and jitter, to avoid overwhelming the RPC endpoints
Copy link
Contributor

Choose a reason for hiding this comment

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

Top-level await on module-level exports causes entire API to fail if initialization fails, with no error handling or recovery mechanism

Fix on Vercel

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

1 participant