Skip to content

Conversation

@tk-o
Copy link
Contributor

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

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • Review commit-by-commit is encouraged.
  • The update organizes ideas around Chain Indexing Status Snapshot of ENSNode SDK into granular layers of responsibility.
    • Deserialization layer uses "serialized" schema from zod-schema layer.
      • In Chain Indexing Status Snapshot case, "serialized" schema is an alias of "unserialized" schema in order to keep the semantic explicit.
    • Validation layer can be used without deserialization layer, which is a huge unlock for client side use cases.
  • Consumers using ENSNode SDK will now be able to
    a. Deserialize a serialized representation of business-layer data model, including validation.
    b. Validate an unvalidated object with shape matching business-layer data model.

Why

  • Before this change, deserialization into ENSIndexer data model required building serialized representation of business-layer data model. Such approach prevented validation of unvalidated objects that already had a shape matching business-layer data model. All validation did required mixing serialized data model with buinsess-layer data model.
  • As per Merge /api/config into /api/indexing-status #1405, Indexing Status API will soon be different for ENSApi and ENSIndexer. Changes in this PR enable re-using ENSIndexer data model in the future ENSApi data model for Indexing Status.

Testing

  • Ran lint, typecheck, and test.
  • Tested /api/indexing-status on local ENSIndexer instance.
  • Tested /api/indexing-status on local ENSApi instance connected to ENSIndexer Alpha in the Green env.

pn

Notes for Reviewer (Optional)

  • Please focus on the WHYs, this PR won't be optimising any business logic. If any of such updates are required, please share feedback and let's log it in issue tracker.
  • This change represents a slice of a larger updated initially proposed in PR refactor(ensnode-sdk): improve Indexing Status data model #1615.
    • Hoping this smaller PR approach with granular commits enables more straightforward review process.
  • If changes proposed in this PR for Chain Indexing Status Snapshot data model are approved, I'll create another three PRs, just like this one, for the remaining data models in ENSIndexer Indexing Status module:
    • OmnichainIndexingStatusSnapshot
    • CrossChainIndexingStatusSnapshot
    • RealtimeIndexingStatusProjection

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 9, 2026 09:45
@changeset-bot
Copy link

changeset-bot bot commented Feb 9, 2026

🦋 Changeset detected

Latest commit: 974335e

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
ensadmin Major
ensapi Major
ensindexer 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-sdk 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 9, 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 10, 2026 2:49pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ensnode.io Skipped Skipped Feb 10, 2026 2:49pm
ensrainbow.io Skipped Skipped Feb 10, 2026 2:49pm

@coderabbitai
Copy link

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

A comprehensive refactoring of indexing status infrastructure that reorganizes type definitions and validation logic, introduces new dedicated schema modules, removes legacy omnichaining validators, and modernizes the ponder-sdk type system by replacing zod-inferred types with plain types and Unvalidated utilities.

Changes

Cohort / File(s) Summary
Chain Indexing Status Snapshot Foundation
packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.test.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/block-refs.mock.ts
New module establishing core types and utilities for chain indexing status snapshots: ChainIndexingConfig variants (Indefinite/Definite), four snapshot states (Queued/Backfill/Following/Completed), helper functions (createIndexingConfig, getTimestampForLowest/HighestOmnichainBlock, sortChainStatusesByStartBlockAsc), and mock block reference data.
Zod Schema Reorganization
packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/chain-indexing-status-snapshot.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.test.ts
Moves schema and invariant definitions from zod-schemas.ts into a dedicated zod-schema subdirectory. New module contains per-status invariant validators and polymorphic schema builders (makeChainIndexingConfigSchema, makeChainIndexingStatusSnapshotSchema variants). zod-schemas.ts now imports and delegates to the external schema module, significantly reducing in-file logic.
Type Definitions Migration
packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts
Removes ChainIndexingConfig and ChainIndexingStatus type definitions from types.ts (now imported from chain-indexing-status-snapshot), and removes serialized snapshot aliases from serialized-types.ts (relocated to serialize/chain-indexing-status-snapshot.ts). Consolidates type surface area by delegating to specialized modules.
Serialization/Deserialization Refactoring
packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/chain-indexing-status-snapshot.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/chain-indexing-status-snapshot.ts
Extracts serializeChainIndexingSnapshots from serialize.ts into dedicated submodule. Removes deserializeChainIndexingStatusSnapshot from deserialize.ts (re-implemented in submodule). Creates new serialize/deserialize modules for chain snapshots with proper type aliases and composition patterns.
Validation and Helper Consolidation
packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts
Removes per-status invariant functions (invariant_chainSnapshot{Queued/Backfill/Completed/Following}Blocks) from validations.ts and helper functions (getTimestampForLowest/Highest, createIndexingConfig, sortChainStatusesByStartBlockAsc) from helpers.ts. Creates new validate/chain-indexing-status-snapshot module with validateChainIndexingStatusSnapshot function.
Public API Expansion
packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts
Adds four new public exports for chain-indexing-status-snapshot module and its serialize/deserialize/validate submodules, expanding the indexing-status surface to include chain snapshot functionality.
Test Import Refactoring
packages/ensnode-sdk/src/ensindexer/indexing-status/*.test.ts, packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts
Updates import paths across test files: block references now source from block-refs.mock instead of test-helpers; type imports redirect from types to chain-indexing-status-snapshot; removes createIndexingConfig test and adjusts remaining test logic to reflect new module boundaries.
Changeset and Omnichaining Cleanup
.changeset/quick-paws-attend.md, apps/ensindexer/src/lib/indexing-status/ponder-metadata/validations.ts
Adds changeset documenting validateChainIndexingStatusSnapshot introduction. Removes invariant_serializedOmnichainSnapshotHasValidChains from indexer validations, eliminating omnichaining chain validation from the indexer layer.
Ponder-SDK Type System Modernization
packages/ponder-sdk/src/blocks.ts, packages/ponder-sdk/src/chains.ts, packages/ponder-sdk/src/time.ts, packages/ponder-sdk/src/utils.ts
Removes zod schema dependencies and converts types to plain definitions: BlockNumber → number, ChainId → number, BlockRef → interface, UnixTimestamp → number. Adds ChainIdString type. Introduces DeepPartial and Unvalidated utility types.
Ponder-SDK Schema Foundation
packages/ponder-sdk/src/schema/blocks.ts, packages/ponder-sdk/src/schema/chains.ts, packages/ponder-sdk/src/schema/time.ts
Creates schema modules that define Zod validation layers: schemaBlockNumber, schemaBlockRef, schemaChainId, schemaUnixTimestamp. Separates runtime validation concerns from type definitions.
Ponder-SDK Complex Validation
packages/ponder-sdk/src/schema/chain-indexing-metrics.ts, packages/ponder-sdk/src/schema/chain-indexing-status.ts
Adds comprehensive metrics validation (schemaPonderIndexingMetrics, schemaDecodedPonderIndexingMetrics) with Prometheus parsing and invariants, and status validation (schemaPonderIndexingStatus, schemaSerializedPonderIndexingStatus) with chain presence checks.
Ponder-SDK Deserialization Updates
packages/ponder-sdk/src/deserialize/indexing-metrics.ts, packages/ponder-sdk/src/deserialize/indexing-status.ts, packages/ponder-sdk/src/deserialize/indexing-status.mock.ts
Restructures deserialization to use externalized schemas and builders: deserializePonderIndexingMetrics composes schemaDecodedPonderIndexingMetrics and schemaPonderIndexingMetrics; buildUnvalidatedPonderIndexingStatus returns Unvalidated. Updates mock data keys from network names to numeric chain IDs.
Ponder-SDK Type Exports
packages/ponder-sdk/src/indexing-metrics.ts, packages/ponder-sdk/src/indexing-status.ts
Adds SerializedPonderIndexingMetrics as string type alias. Introduces ChainBlockRef interface and SerializedPonderIndexingStatus record type to formalize serialized status structure.
Ponder-SDK Client Tests
packages/ponder-sdk/src/client.test.ts
Updates error message expectations from exact strings to regex patterns for ChainId validation errors, accommodating multiple metric label scenarios without hard-coded metric names.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat(ponder-sdk): create Ponder Client #1602: Directly overlaps with extensive ponder-sdk additions and refactors (schemas, deserialize/indexing-status, client updates, type definitions) affecting the same modules extended in this PR.
  • feat(ponder-sdk): Ponder Indexing Metrics #1604: Related through ponder-sdk indexing-metrics functionality changes, including PonderIndexingMetrics schemas, deserializePonderIndexingMetrics modifications, and metrics health endpoint integration.

Poem

🐰 Snapshots reorganized with care and grace,
Schemas moved to their rightful place,
Types unpacked from zod's embrace,
Validation flows now validate and trace,
A cleaner indexing status space!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.07% 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 title clearly and concisely summarizes the main change: a refactor of the Chain Indexing Status Snapshot data model in ensnode-sdk, reflecting the primary objective of reorganizing this model into layered responsibilities.
Description check ✅ Passed The PR description follows the template structure with Summary, Why, Testing, Notes for Reviewer, and Pre-Review Checklist sections. All required sections are present and substantively filled with relevant context about the changes and their motivation.

✏️ 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/ensnode-validate-chain-indexing-status-snapshot

Tip

We've launched Issue Planner and it is currently in beta. Please try it out and share your feedback on Discord!


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 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.

Comment on lines +321 to +326
// Invariant: earliestKnownBlockTimestamps is guaranteed to have at least one element
if (earliestKnownBlockTimestamps.length === 0) {
throw new Error(
"Invariant violation: at least one chain is required to determine the lowest omnichain start block timestamp",
);
}
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 is additional change to just moving the code from helpers.ts.

Comment on lines 375 to 380
// Invariant: latestKnownBlockTimestamps is guaranteed to have at least one element
if (latestKnownBlockTimestamps.length === 0) {
throw new Error(
"Invariant violation: at least one chain is required to determine the highest omnichain known block timestamp",
);
}
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 is additional change to just moving the code from helpers.ts.

ChainStatusType extends ChainIndexingStatusSnapshot,
>(chains: [ChainId, ChainStatusType][]): [ChainId, ChainStatusType][] {
// Sort the chain statuses by the omnichain first block to index timestamp
return [...chains].sort(
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 is additional change to just moving the code from helpers.ts.

Before it was modifying the input object:

chains.sort(

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts (1)

148-156: 🧹 Nitpick | 🔵 Trivial

Misleading variable name: allChainsHaveValidStatuses uses .some(), not .every().

The variable name suggests all chains are checked, but .some() only checks for at least one match. Consider renaming to something like atLeastOneChainFollowing for consistency with the naming pattern used in checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill (line 111).

♻️ Suggested rename
 export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing(
   chains: ChainIndexingStatusSnapshot[],
 ): chains is ChainIndexingStatusSnapshot[] {
-  const allChainsHaveValidStatuses = chains.some(
+  const atLeastOneChainInTargetStatus = chains.some(
     (chain) => chain.chainStatus === ChainIndexingStatusIds.Following,
   );
 
-  return allChainsHaveValidStatuses;
+  return atLeastOneChainInTargetStatus;
 }
packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts (1)

264-268: ⚠️ Potential issue | 🟡 Minor

Remove debug console.log from invariant function.

This console.log in invariant_slowestChainEqualsToOmnichainSnapshotTime appears to be a leftover debug statement. It will emit internal state to the console on every validation failure in production.

🧹 Proposed fix
   if (slowestChainIndexingCursor !== omnichainIndexingCursor) {
-    console.log("invariant_slowestChainEqualsToOmnichainSnapshotTime", {
-      slowestChainIndexingCursor,
-      omnichainIndexingCursor,
-    });
     ctx.issues.push({
🤖 Fix all issues with AI agents
In
`@packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts`:
- Around line 344-383: Update the documentation and error message to reflect
that queued chains only contribute a timestamp when their config is Definite: in
getTimestampForHighestOmnichainKnownBlock, clarify in the JSDoc that
ChainIndexingStatusIds.Queued contributes config.endBlock.timestamp only for
ChainIndexingConfigTypeIds.Definite and that Queued+Indefinite chains are
intentionally skipped (they do not add to latestKnownBlockTimestamps); also
change the thrown Error text to state the actual precondition — e.g., "Invariant
violation: at least one chain must contribute a known block timestamp" — so the
invariant refers to contributing chains rather than the input chains array.

In `@packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts`:
- Around line 1-7: Add imports for the missing types ChainIndexingStatusIds and
ChainIndexingConfigTypeIds to the top of this file (in the same import block
that currently brings in ChainIndexingStatusSnapshot,
ChainIndexingStatusSnapshotBackfill, ChainIndexingStatusSnapshotCompleted,
ChainIndexingStatusSnapshotQueued) so JSDoc {`@link` ...} references resolve;
import them from the module where those IDs are defined and ensure they are
exported as types (i.e., use "import type { ChainIndexingStatusIds,
ChainIndexingConfigTypeIds } from '...'").

In
`@packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts`:
- Around line 7-12: The JSDoc for validateChainIndexingStatusSnapshot is
misleading: it says it validates a "maybe ChainIndexingStatusSnapshot" while the
function parameter unvalidatedSnapshot is already typed as
ChainIndexingStatusSnapshot; either update the JSDoc to remove "maybe" and state
it performs runtime invariant checks on an already-typed object, or change the
parameter type to a looser type (e.g., unknown) so the function performs full
structural validation before asserting/returning
ChainIndexingStatusSnapshot—update the function signature or the comment
accordingly and ensure references to unvalidatedSnapshot and
validateChainIndexingStatusSnapshot remain consistent.

In
`@packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/chain-indexing-status-snapshot.ts`:
- Around line 21-39: Remove the redundant truthiness check for config.endBlock
after the Indefinite guard in invariant_chainSnapshotQueuedBlocks: once you
return when config.configType === ChainIndexingConfigTypeIds.Indefinite the type
is narrowed to the definite variant (ChainIndexingConfigDefinite) so drop the
`config.endBlock &&` condition and directly call
blockRef.isBeforeOrEqualTo(config.startBlock, config.endBlock); apply the same
cleanup to the similar check around line 74 in the other invariant where the
Indefinite guard already narrows the type.

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

Refactors the ENSNode SDK’s Chain Indexing Status Snapshot model into clearer layers (business types, schema/validation, serialization/deserialization) so consumers can validate/deserialise snapshots more flexibly in preparation for upcoming indexing-status API divergence.

Changes:

  • Extracts ChainIndexingStatusSnapshot business types + related helpers into a dedicated module.
  • Moves chain snapshot Zod schemas/invariants into zod-schema/ and adds a dedicated runtime validator (validateChainIndexingStatusSnapshot).
  • Introduces a dedicated serialize/deserialize entry for chain snapshots and updates tests/imports accordingly.

Reviewed changes

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

Show a summary per file
File Description
packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts Removes inline chain snapshot schema definitions and reuses new zod-schema/ module.
packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.test.ts Updates test imports to use new chain snapshot module and schema location.
packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/chain-indexing-status-snapshot.ts New home for chain snapshot schemas + invariants (and “serialized schema” alias).
packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts Removes chain-level invariants from the shared validations module.
packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts Adds a runtime validator for the business-layer chain snapshot type.
packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts Removes chain snapshot business types from types.ts (now imported from new module).
packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts Adjusts serialized omnichain/cross-chain types to depend on new chain snapshot module.
packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/chain-indexing-status-snapshot.ts New serialize helper + serialized chain snapshot type aliases.
packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts Uses extracted serializeChainIndexingSnapshots helper.
packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts Updates imports for chain snapshot constants/types.
packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts Re-exports new modules (business types, serialize/deserialize, validator).
packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts Moves chain snapshot–specific helpers out of general helpers module.
packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts Removes tests for helpers moved to chain snapshot module.
packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/chain-indexing-status.ts Adds dedicated deserializer for a chain snapshot.
packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts Removes chain snapshot deserializer from the aggregate deserialize module.
packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts Updates imports due to type/module refactor.
packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts New module defining chain snapshot business types + moved helpers.
packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.test.ts Adds tests for createIndexingConfig in its new location.
packages/ensnode-sdk/src/ensindexer/indexing-status/block-refs.mock.ts Adds block ref fixtures used by new chain snapshot tests.
apps/ensindexer/src/lib/indexing-status/ponder-metadata/validations.ts Removes now-unneeded invariant and simplifies imports.
.changeset/quick-paws-attend.md Declares a minor bump for the new validator API.

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

Copilot AI review requested due to automatic review settings February 9, 2026 10:11
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 9, 2026 10:11 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 9, 2026 10:11 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 for updates 👍 Shared a few suggestions please take the lead to merge when ready.

* Validates a maybe {@link ChainIndexingStatusSnapshot} object.
*/
export function validateChainIndexingStatusSnapshot(
unvalidatedSnapshot: ChainIndexingStatusSnapshot,
Copy link
Member

Choose a reason for hiding this comment

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

Let's find a different way to type unvalidated objects so that it's always explicit that they might not be what you think they are. I don't feel comfortable ever typing something as an X that we cannot rely on as being an X. Let's support the type system so that it can support us.

Appreciate your advice on how we solve this.

Copy link
Contributor Author

@tk-o tk-o Feb 10, 2026

Choose a reason for hiding this comment

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

@lightwalker-eth What if our buildInvalidatedX functions would all return respective Unvalidated<X> value so that it could be validated as X?

The Unvalidated wrapper type would signal that data is of a business-level data model shape, but lacks guarantees of that model. For example:

unvalidatedSnapshot: Unvalidated<ChainIndexingStatusSnapshot>,

where Unvalidated is defined as:

type Unvalidated<T> = DeepPartial<T>;

We have DeepPartial defined in both ENSNode SDK and can use it the same way in Ponder SDK:

/**
* A utility type that makes all properties of a type optional recursively,
* including nested objects and arrays.
*
* @example
* ```typescript
* type Config = {
* a: string;
* b: {
* x: number;
* y: { z: boolean };
* };
* c: { id: string }[];
* }
*
* type PartialConfig = DeepPartial<Config>;
* // Results in:
* // {
* // a?: string;
* // b?: {
* // x?: number;
* // y?: { z?: boolean };
* // };
* // c?: { id?: string }[];
* // }
*
* // Usage:
* const update: PartialConfig = { b: { y: { z: true } } };
* ```
*/
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? DeepPartial<U>[]
: T[P] extends object
? DeepPartial<T[P]>
: T[P];

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 I like this idea 👍 Thanks!

unvalidatedSnapshot: ChainIndexingStatusSnapshot,
unvalidatedSnapshot: ChainIndexingStatusSnapshot | unknown,
valueLabel?: string,
): ChainIndexingStatusSnapshot {
Copy link
Member

Choose a reason for hiding this comment

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

The JSDoc should make it explicit what happens if it's not valid.

Additionally, what is the motivation to have this return the parsed object? If this is purely about validation then there should be zero transformations, right?

And assuming zero transformations, and considering how this function can also take an input an unknown, shouldn't this return the type hint to the type system identifying that unvalidatedSnapshot truly is a ChainIndexingStatusSnapshot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, the final suggestion that I'll apply is with using the Unvalidated<T> type to signal that validation happens on object that is shaped like T but has not been validated against business rules required for T.

unvalidatedSnapshot: Unvalidated<ChainIndexingStatusSnapshot>,

Copy link
Collaborator

@shrugs shrugs left a comment

Choose a reason for hiding this comment

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

LGTM!

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 36 out of 38 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

packages/ponder-sdk/src/schema/chains.ts:22

  • schemaChainIdString currently only checks that String(Number(value)) === value. This allows invalid inputs like "NaN", "Infinity", "-1", or "0" to pass, which can let invalid chain ID strings through (e.g., as record keys) without being caught. Tighten the check to require a finite, safe integer > 0 (and still reject formats like "01"), e.g., parse to a number and validate Number.isSafeInteger(n) && n > 0 && String(n) === value.
    packages/ponder-sdk/src/deserialize/indexing-status.ts:33
  • deserializePonderIndexingStatus builds the chains Map using chainData.id and ignores the record key. This means inconsistent payloads like { "1": { id: 10, ... } } will be accepted and silently remapped to chain 10. Consider enforcing that the record key matches chainData.id (e.g., via a .check on the schema or by comparing inside buildUnvalidatedPonderIndexingStatus and surfacing a validation issue), or simplify the serialized shape to Record<ChainIdString, BlockRef> to avoid duplicated IDs.
  const chains = new Map<ChainId, BlockRef>();

  for (const [, chainData] of Object.entries(data)) {
    chains.set(chainData.id, chainData.block);
  }

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ponder-sdk/src/client.test.ts (1)

142-160: ⚠️ Potential issue | 🟡 Minor

Test silently passes if no error is thrown.

The try/catch block has no guard ensuring the error path is actually reached. If ponderClient.metrics() resolves successfully, no assertions execute and the test passes silently. Add expect.assertions(n) at the top or use the rejects pattern (as other tests in this file already do).

Proposed fix
       // Act
+      expect.assertions(5);
       try {
         await ponderClient.metrics();
       } catch (error) {

Or alternatively, refactor to use rejects.toThrowError like the other test cases.

packages/ponder-sdk/src/schema/chains.ts (1)

8-22: ⚠️ Potential issue | 🟠 Major

schemaChainIdString now accepts invalid chain-ID strings (e.g. "0", "-1", "1.5", "Infinity", "NaN").

The removed preprocessing/cross-check previously validated that the parsed numeric value was a positive integer. The remaining invariant only checks that the string round-trips through Number() → template literal, which passes for any string that Number() can parse — including "0", "-1", "1.5", "Infinity", and "NaN".

Consider adding an integer + positive check inside the invariant:

Proposed fix
 function invariant_chainIdStringRepresentsValidChainId(ctx: ParsePayload<string>) {
   const maybeChainId = ctx.value;
 
-  if (`${Number(maybeChainId)}` !== maybeChainId) {
+  const num = Number(maybeChainId);
+  if (!Number.isInteger(num) || num <= 0 || `${num}` !== maybeChainId) {
     ctx.issues.push({
       code: "custom",
       input: ctx.value,
       message: `'${maybeChainId}' must be a string representing a chain ID.`,
     });
   }
 }
🤖 Fix all issues with AI agents
In `@packages/ponder-sdk/src/deserialize/indexing-metrics.ts`:
- Around line 100-106: The function deserializePonderIndexingMetrics narrows its
parameter to SerializedPonderIndexingMetrics while the sibling
deserializePonderIndexingStatus accepts SerializedPonderIndexingStatus |
unknown; change the parameter type of deserializePonderIndexingMetrics to
SerializedPonderIndexingMetrics | unknown so it matches the sibling and accepts
untyped external input, leaving the existing
schemaDecodedPonderIndexingMetrics.transform(...).pipe(...).safeParse(data)
logic intact; update the function signature for deserializePonderIndexingMetrics
to use the union type to avoid breaking callers and ensure consistent behavior
with deserializePonderIndexingStatus.
- Around line 28-37: The JSDoc for buildUnvalidatedPonderIndexingMetrics
incorrectly references schemaSerializedPonderIndexingMetrics; update the comment
to state the returned Unvalidated<PonderIndexingMetrics> should be validated
with schemaPonderIndexingMetrics (the schema that validates the builder's
output), i.e., replace the {`@link` schemaSerializedPonderIndexingMetrics}
reference with {`@link` schemaPonderIndexingMetrics} in the function's docblock so
it matches the actual validation used in the code.

In `@packages/ponder-sdk/src/schema/chain-indexing-metrics.ts`:
- Around line 80-124: The function invariant_includesRequiredMetrics currently
always runs label validation for requiredChainMetricNames even if the metric was
already reported missing; update invariant_includesRequiredMetrics to
short-circuit per-metric label checks by first verifying the metric exists in
metricNames before calling prometheusMetrics.getLabels or validating chain label
values. Specifically, after computing requiredChainMetricNames and metricNames,
skip the whole getLabels / schemaChainIdString.safeParse block for any
requiredChainMetricName that is not present (i.e., only call
prometheusMetrics.getLabels and validate maybeChainId when
metricNames.includes(requiredChainMetricName) is true) so you don't emit
redundant "must include a 'chain' label" or parsing errors when the metric was
already flagged missing.

In `@packages/ponder-sdk/src/schema/chain-indexing-status.ts`:
- Around line 8-11: The schemaChainBlockRef constant is defined but is used only
internally by schemaSerializedPonderIndexingStatus; add a short inline comment
above schemaChainBlockRef (or rename it with an internal prefix like
_schemaChainBlockRef) to indicate it is intentionally module-private and not
exported, so future contributors know it's for internal use only and avoid
exporting or reusing it elsewhere; update any nearby JSDoc or comment to
reference schemaSerializedPonderIndexingStatus as its sole consumer.
- Line 2: Remove the import of the internal ParsePayload type from zod/v4/core
and stop using it in the .check() callback signatures; instead, have the
callback accept a safe public type (e.g. use unknown or any) or declare a local
typedef for the payload parameter, and keep using Zod's public APIs (.check(),
.refine()) for validation. Specifically, in chain-indexing-status.ts (and the
other affected files chains.ts and chain-indexing-metrics.ts) remove "import
type { ParsePayload } ..." and update the .check(...) / callback parameter types
in the schema definitions (look for the schema variable/function names in those
files) to use a locally defined type or unknown/any so you no longer rely on the
undocumented ParsePayload symbol.

In `@packages/ponder-sdk/src/utils.ts`:
- Around line 1-68: DeepPartial and Unvalidated are duplicated across packages
(DeepPartial and Unvalidated types); extract these types into a shared module
(e.g., a new types/shared file or an existing common utils package) and have
both packages import/re-export from that single location. Update all references
to use the shared export (replace local DeepPartial and Unvalidated definitions
with imports) and add a tests/build check to ensure both packages compile
against the shared type to prevent future divergence.
- Around line 31-37: DeepPartial<T> currently recurses into all non-primitives
(the "extends object" branch), which breaks built-in types like Date/Map/Set and
affects places such as PonderIndexingStatus.chains; change the type guard so
only plain record-like objects are deep-partial'd (for example use T[P] extends
Record<string, unknown> or explicitly exclude built-ins/Function types) and
leave built-ins (Date, Map, Set, WeakMap, WeakSet, Function, etc.) as-is; update
the DeepPartial<T> definition accordingly so arrays still recurse but built-in
objects do not get their methods turned into optionals.

@@ -0,0 +1,48 @@
import { z } from "zod/v4";
import type { ParsePayload } from "zod/v4/core";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Zod v4 ParsePayload type public API documentation

💡 Result:

ParsePayload in Zod v4 is not part of the documented public API.

  • In the v4 architecture it’s an internal parsing “carrier” object (holds the current value, accumulated issues, and an optional aborted flag) used by the core parser pipeline; it shows up in the generated type declarations as interface ParsePayload<T = unknown> { aborted?: boolean; issues: ...; value: T } and is defined in node_modules/zod/v4/core/schemas.d.cts. [3]
  • Zod’s own docs describe zod/v4/core as internals-oriented and “not intended to be used directly”, designed for other packages to build on. [1]
  • The v4 migration guide explicitly warns that undocumented quasi-internal types/functions are not considered public API and changes to them may be undocumented. [2]

So: there isn’t official “public API documentation” for ParsePayload; if you depend on it, treat it as internal/unstable, and prefer the supported parse APIs (parse, safeParse, etc.) and their return types instead. [1][2]

Sources: [1] [2] [3]


🏁 Script executed:

# First, let's examine the file to understand how ParsePayload is being used
cat -n packages/ponder-sdk/src/schema/chain-indexing-status.ts

Repository: namehash/ensnode

Length of output: 1890


🏁 Script executed:

# Check if there are any other uses of ParsePayload in the codebase
rg "ParsePayload" --type ts --type tsx

Repository: namehash/ensnode

Length of output: 87


🏁 Script executed:

# Check the package.json to see the Zod version being used
cat packages/ponder-sdk/package.json | grep -A 2 -B 2 "zod"

Repository: namehash/ensnode

Length of output: 194


🌐 Web query:

Zod v4 .check() method vs .refine() .superRefine() public API validation

💡 Result:

Zod v4: .check() vs .refine() vs .superRefine() (and what to use for public API validation)

.refine(predicate, options?) — “simple custom rule”

  • Best for one boolean condition (“valid / invalid”).
  • Produces a single issue with code "custom".
  • Still the most readable choice for typical business rules on public inputs. [3]
  • In v4, type predicates no longer narrow the inferred type, and some older overloads were removed. [4]

.superRefine((value, ctx) => …) — legacy “multi-issue with ctx”

  • Lets you add multiple issues and use non-"custom" issue codes. [2]
  • Deprecated in Zod v4 in favor of .check(). [1]
  • Also, in v4 the refinement ctx no longer exposes ctx.path. [4]

.check(checkOrFn) — v4’s low-level “checks” API (preferred over superRefine)

  • This is the more general primitive underneath refinements in Zod v4; schemas hold an array of “checks” that run post-parse. [5]
  • Use it when you need:
    • multiple issues in one pass
    • full control over issue objects (codes like "too_big", etc., not just "custom")
    • advanced behavior like continuable issues (continue: true) [3]
  • Zod docs note it’s more complex/verbose than superRefine, but can be faster in performance-sensitive paths. [2]

Practical guidance for “public API validation”

  • Prefer built-in validators (.min, .email, etc.) for standard constraints.
  • Add .refine() for single-rule business logic (clean error message + optional path/conditional execution). [6]
  • Use .check() when you need multiple field-specific errors, precise issue codes, or you’re building reusable validation “rules” at scale. [3]
  • Avoid starting new code with .superRefine() (deprecated). [1]

Sources:
[1] Zod package docs (superRefine deprecated; check exists) https://zod.dev/packages/zod
[2] Zod API docs (superRefine + note on check) https://zod.dev/api?id=preprocess
[3] Zod API docs (check() is the versatile API; example with multiple issues and issue objects) https://zod.dev/api?id=sets
[4] Zod v4 changelog/migration notes (refine changes; ctx.path removed) https://zod.dev/v4/changelog
[5] Zod Core docs (checks concept; .check(...) composition) https://zod.dev/packages/core
[6] Zod docs (refine usage, including path/conditional execution example) https://zod.dev/?id=dates-1


🏁 Script executed:

# Check what Zod types/methods are imported elsewhere in the codebase
rg "from [\"']zod" packages/ponder-sdk/src --type ts -A 1 | head -30

Repository: namehash/ensnode

Length of output: 1301


Remove import of ParsePayload from internal Zod module.

ParsePayload is not a public API in Zod v4—it's an internal implementation detail defined in zod/v4/core and explicitly marked as "not intended to be used directly" per Zod's v4 migration guide. Zod warns that changes to undocumented types may be made without notice.

While .check() is the correct public method for this type of validation (replacing deprecated .superRefine()), the ParsePayload type should not be imported from internals. Either define the check function's parameter type locally, use the public .refine() method if possible, or find the documented way to type check callbacks in Zod v4.

This pattern appears in multiple schema files (chains.ts, chain-indexing-status.ts, chain-indexing-metrics.ts) and should be fixed across all of them.

🤖 Prompt for AI Agents
In `@packages/ponder-sdk/src/schema/chain-indexing-status.ts` at line 2, Remove
the import of the internal ParsePayload type from zod/v4/core and stop using it
in the .check() callback signatures; instead, have the callback accept a safe
public type (e.g. use unknown or any) or declare a local typedef for the payload
parameter, and keep using Zod's public APIs (.check(), .refine()) for
validation. Specifically, in chain-indexing-status.ts (and the other affected
files chains.ts and chain-indexing-metrics.ts) remove "import type {
ParsePayload } ..." and update the .check(...) / callback parameter types in the
schema definitions (look for the schema variable/function names in those files)
to use a locally defined type or unknown/any so you no longer rely on the
undocumented ParsePayload symbol.

@tk-o tk-o force-pushed the feat/ensnode-validate-chain-indexing-status-snapshot branch from 8758c1d to 974335e Compare February 10, 2026 14:48
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 10, 2026 14:48 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 10, 2026 14:48 Inactive
@tk-o
Copy link
Contributor Author

tk-o commented Feb 10, 2026

All changes affecting files in packages/ponder-sdk directory were extracted from this PR 👍

@tk-o tk-o merged commit 1bc599f into main Feb 10, 2026
16 checks passed
@tk-o tk-o deleted the feat/ensnode-validate-chain-indexing-status-snapshot branch February 10, 2026 14:54
@github-actions github-actions bot mentioned this pull request Feb 10, 2026
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