Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ensindexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@ensnode/ensnode-sdk": "workspace:*",
"@ensnode/ensrainbow-sdk": "workspace:*",
"@ensnode/ponder-metadata": "workspace:*",
"@ensnode/ponder-sdk": "workspace:*",
"caip": "catalog:",
"date-fns": "catalog:",
"deepmerge-ts": "^7.1.5",
Expand Down
121 changes: 121 additions & 0 deletions apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { PublicClient } from "viem";

import { bigIntToNumber, deserializeBlockRef } from "@ensnode/ensnode-sdk";
import type {
BlockNumber,
BlockRef,
Blockrange,
ChainId,
ChainIndexingMetrics,
} from "@ensnode/ponder-sdk";

/**
* Fetch block ref from RPC.
*
* @param publicClient for a chain
* @param blockNumber
*
* @throws error if data validation fails.
*/
async function fetchBlockRef(
publicClient: PublicClient,
blockNumber: BlockNumber,
): Promise<BlockRef> {
try {
const block = await publicClient.getBlock({ blockNumber: BigInt(blockNumber) });

return deserializeBlockRef({
number: bigIntToNumber(block.number),
timestamp: bigIntToNumber(block.timestamp),
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to fetch block ref for block number ${blockNumber}: ${errorMessage}`);
Comment on lines +31 to +33
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Preserve the original error as cause for proper error chaining.

The message is extracted, but the full stack trace and error identity are discarded. Using the cause option preserves the entire chain for debuggability while still providing your contextual message.

♻️ Proposed fix
   } catch (error) {
-    const errorMessage = error instanceof Error ? error.message : "Unknown error";
-    throw new Error(`Failed to fetch block ref for block number ${blockNumber}: ${errorMessage}`);
+    throw new Error(`Failed to fetch block ref for block number ${blockNumber}`, { cause: error });
   }
🤖 Prompt for AI Agents
In `@apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts` around
lines 31 - 33, The catch block in chain-block-refs.ts currently creates a new
Error with the message but drops the original error; update the throw to
preserve the original error via the Error cause option (i.e., throw new
Error(`Failed to fetch block ref for block number ${blockNumber}:
${errorMessage}`, { cause: error })) so the original stack and identity are
retained for proper error chaining; keep the existing errorMessage logic (error
instanceof Error ? error.message : "Unknown error") and pass the original error
variable as the cause when constructing the new Error.

}
}

/**
* Chain Block Refs
*
* Represents information about indexing scope for an indexed chain.
*/
export interface ChainBlockRefs {
/**
* Based on Ponder Configuration
*/
config: {
startBlock: BlockRef;

endBlock: BlockRef | null;
};

/**
* Based on Ponder runtime metrics
*/
backfillEndBlock: BlockRef;
}

/**
* Get {@link IndexedChainBlockRefs} for indexed chains.
*
* Guaranteed to include {@link ChainBlockRefs} for each indexed chain.
*/
export async function getChainsBlockRefs(
chainIds: ChainId[],
chainsConfigBlockrange: Record<ChainId, Blockrange>,
chainsIndexingMetrics: Map<ChainId, ChainIndexingMetrics>,
publicClients: Record<ChainId, PublicClient>,
): Promise<Map<ChainId, ChainBlockRefs>> {
const chainsBlockRefs = new Map<ChainId, ChainBlockRefs>();

for (const chainId of chainIds) {
const blockrange = chainsConfigBlockrange[chainId];
const startBlock = blockrange?.startBlock;
const endBlock = blockrange?.endBlock;
const publicClient = publicClients[chainId];
const indexingMetrics = chainsIndexingMetrics.get(chainId);

if (typeof startBlock !== "number") {
throw new Error(`startBlock not found for chain ${chainId}`);
}

if (typeof publicClient === "undefined") {
throw new Error(`publicClient not found for chain ${chainId}`);
}

if (typeof indexingMetrics === "undefined") {
throw new Error(`indexingMetrics not found for chain ${chainId}`);
}

const historicalTotalBlocks = indexingMetrics.backfillSyncBlocksTotal;

if (typeof historicalTotalBlocks !== "number") {
throw new Error(`No historical total blocks metric found for chain ${chainId}`);
}

const backfillEndBlock = startBlock + historicalTotalBlocks - 1;
Comment on lines +90 to +96
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate that historicalTotalBlocks is a positive integer.

The check at Line 92 only verifies the value is a number, but 0 or negative values would produce a nonsensical backfillEndBlock (before startBlock). Zero blocks for backfill indicates an error condition or misconfiguration, not a valid state.

🛡️ Proposed fix
     const historicalTotalBlocks = indexingMetrics.backfillSyncBlocksTotal;

-    if (typeof historicalTotalBlocks !== "number") {
+    if (typeof historicalTotalBlocks !== "number" || historicalTotalBlocks < 1) {
       throw new Error(`No historical total blocks metric found for chain ${chainId}`);
     }

Based on learnings: "backfillSyncBlocksTotal field in Ponder Indexing Metrics must always be a positive integer (at least 1). Zero blocks for backfill indicates an error condition or misconfiguration, not a valid state."

📝 Committable suggestion

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

Suggested change
const historicalTotalBlocks = indexingMetrics.backfillSyncBlocksTotal;
if (typeof historicalTotalBlocks !== "number") {
throw new Error(`No historical total blocks metric found for chain ${chainId}`);
}
const backfillEndBlock = startBlock + historicalTotalBlocks - 1;
const historicalTotalBlocks = indexingMetrics.backfillSyncBlocksTotal;
if (typeof historicalTotalBlocks !== "number" || historicalTotalBlocks < 1) {
throw new Error(`No historical total blocks metric found for chain ${chainId}`);
}
const backfillEndBlock = startBlock + historicalTotalBlocks - 1;
🤖 Prompt for AI Agents
In `@apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts` around
lines 90 - 96, The code currently only checks that
indexingMetrics.backfillSyncBlocksTotal is a number; update validation so
historicalTotalBlocks (the value assigned from
indexingMetrics.backfillSyncBlocksTotal) is a positive integer (use
Number.isInteger and > 0) before computing backfillEndBlock = startBlock +
historicalTotalBlocks - 1; if the check fails throw an Error that includes
chainId and the invalid value to make the misconfiguration obvious.


try {
// fetch relevant block refs using RPC
const [startBlockRef, endBlockRef, backfillEndBlockRef] = await Promise.all([
fetchBlockRef(publicClient, startBlock),
endBlock ? fetchBlockRef(publicClient, endBlock) : null,
fetchBlockRef(publicClient, backfillEndBlock),
]);

const chainBlockRef = {
config: {
startBlock: startBlockRef,
endBlock: endBlockRef,
},
backfillEndBlock: backfillEndBlockRef,
} satisfies ChainBlockRefs;

chainsBlockRefs.set(chainId, chainBlockRef);
} catch {
throw new Error(`Could not get BlockRefs for chain ${chainId}`);
}
Comment on lines +114 to +117
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 catch { ... } here discards the original error, losing the underlying RPC/validation failure details. Capture the error and rethrow with additional context (or use new Error(message, { cause })) so callers can diagnose which fetch failed and why.

Copilot uses AI. Check for mistakes.
}
Comment on lines +71 to +118
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Chains are processed sequentially; consider parallelizing across chains.

The for...of loop with await processes each chain serially, meaning N chains result in N sequential rounds of RPC calls. Since each chain's block refs are independent, they could be fetched concurrently.

♻️ Sketch: parallel processing across chains
- const chainsBlockRefs = new Map<ChainIdString, ChainBlockRefs>();
-
- for (const chainId of chainIds) {
-   // ... validation and fetch per chain ...
-   chainsBlockRefs.set(chainId, chainBlockRef);
- }
-
- return chainsBlockRefs;
+ const entries = await Promise.all(
+   chainIds.map(async (chainId) => {
+     // ... validation and fetch per chain ...
+     return [chainId, chainBlockRef] as const;
+   }),
+ );
+
+ return new Map(entries);
🤖 Prompt for AI Agents
In `@apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts` around
lines 64 - 111, The loop over chainIds is performing RPCs serially; change it to
run per-chain work in parallel by mapping chainIds to an array of async tasks
and awaiting Promise.all (or Promise.allSettled with error aggregation). For
each chainId task, read chainsConfigBlockrange, publicClients, and
chainsIndexingMetrics, compute startBlock/endBlock/backfillEndBlock, call
fetchBlockRef for the three refs, construct the ChainBlockRefs object, and
return [chainId, chainBlockRef]; after Promise.all resolve, populate
chainsBlockRefs from the results (or throw a combined error if any task failed).
Ensure you keep the same validation checks
(startBlock/publicClient/indexingMetrics/historicalTotalBlocks) inside each task
and include chainId in any thrown errors to aid debugging.


return chainsBlockRefs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
ChainIndexingConfigTypeIds,
ChainIndexingStatusIds,
type ChainIndexingStatusSnapshot,
type ChainIndexingStatusSnapshotBackfill,
type ChainIndexingStatusSnapshotCompleted,
type ChainIndexingStatusSnapshotFollowing,
type ChainIndexingStatusSnapshotQueued,
createIndexingConfig,
} from "@ensnode/ensnode-sdk";
import {
type ChainId,
type ChainIndexingMetrics,
type ChainIndexingStatus,
isBlockRefEqualTo,
} from "@ensnode/ponder-sdk";

import type { ChainBlockRefs } from "./chain-block-refs";
import { validateChainIndexingStatusSnapshot } from "./validate/chain-indexing-status-snapshot";

/**
* Build Chain Indexing Status Snapshot
*
* Builds {@link ChainIndexingStatusSnapshot} for a chain based on:
* - block refs based on chain configuration and RPC data,
* - current indexing status,
* - current indexing metrics.
*/
export function buildChainIndexingStatusSnapshot(
chainId: ChainId,
chainBlockRefs: ChainBlockRefs,
chainIndexingMetrics: ChainIndexingMetrics,
chainIndexingStatus: ChainIndexingStatus,
): ChainIndexingStatusSnapshot {
const { checkpointBlock } = chainIndexingStatus;
Comment on lines +29 to +35
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 new module introduces non-trivial status derivation logic (Queued/Backfill/Following/Completed + omnichain aggregation) but currently has no tests. There is existing test coverage for similar logic under apps/ensindexer/src/lib/indexing-status/ponder-metadata/*.test.ts; adding analogous unit tests for these builder functions would help prevent regressions during the planned SDK abstraction refactors.

Copilot uses AI. Check for mistakes.
const config = createIndexingConfig(
chainBlockRefs.config.startBlock,
chainBlockRefs.config.endBlock,
);

// TODO: Use `ChainIndexingMetrics` data model from PR #1612.
// This updated data model includes `type` field to distinguish
// between different chain indexing phases, for example:
// Queued, Backfill, Realtime, Completed.

// In omnichain ordering, if the startBlock is the same as the
// status block, the chain has not started yet.
if (isBlockRefEqualTo(chainBlockRefs.config.startBlock, checkpointBlock)) {
Comment on lines +46 to +48
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.

Queued detection is based on isBlockRefEqualTo(startBlock, checkpointBlock), which requires both number and timestamp equality. The previous implementation in apps/ensindexer/src/lib/indexing-status/ponder-metadata/chains.ts used only startBlock.number === statusBlock.number; if timestamps can differ between RPC-fetched startBlock and Ponder’s status response, this will misclassify queued vs started. Consider comparing block numbers only for this specific queued check.

Suggested change
// In omnichain ordering, if the startBlock is the same as the
// status block, the chain has not started yet.
if (isBlockRefEqualTo(chainBlockRefs.config.startBlock, checkpointBlock)) {
// In omnichain ordering, if the start block number is the same as the
// checkpoint block number, the chain has not started yet.
if (chainBlockRefs.config.startBlock.number === checkpointBlock.number) {

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (isBlockRefEqualTo(chainBlockRefs.config.startBlock, checkpointBlock)) {
if (chainBlockRefs.config.startBlock.number === checkpointBlock.number) {

Queued status detection fails when block numbers match but timestamps differ, preventing correct detection of "not started" state

Fix on Vercel

return validateChainIndexingStatusSnapshot({
chainStatus: ChainIndexingStatusIds.Queued,
config,
} satisfies ChainIndexingStatusSnapshotQueued);
}

if (chainIndexingMetrics.indexingCompleted) {
// TODO: move that invariant to validation schema
if (config.configType !== ChainIndexingConfigTypeIds.Definite) {
throw new Error(
`The '${ChainIndexingStatusIds.Completed}' indexing status for chain ID '${chainId}' can be only created with the '${ChainIndexingConfigTypeIds.Definite}' indexing config type.`,
);
}

return validateChainIndexingStatusSnapshot({
chainStatus: ChainIndexingStatusIds.Completed,
latestIndexedBlock: checkpointBlock,
config,
} satisfies ChainIndexingStatusSnapshotCompleted);
}

if (chainIndexingMetrics.indexingRealtime) {
// TODO: move that invariant to validation schema
if (config.configType !== ChainIndexingConfigTypeIds.Indefinite) {
throw new Error(
`The '${ChainIndexingStatusIds.Following}' indexing status for chain ID '${chainId}' can be only created with the '${ChainIndexingConfigTypeIds.Indefinite}' indexing config type.`,
);
}

return validateChainIndexingStatusSnapshot({
chainStatus: ChainIndexingStatusIds.Following,
latestIndexedBlock: checkpointBlock,
latestKnownBlock: chainIndexingMetrics.latestSyncedBlock,
config: {
configType: config.configType,
startBlock: config.startBlock,
},
} satisfies ChainIndexingStatusSnapshotFollowing);
}

return validateChainIndexingStatusSnapshot({
chainStatus: ChainIndexingStatusIds.Backfill,
latestIndexedBlock: checkpointBlock,
backfillEndBlock: chainBlockRefs.backfillEndBlock,
config,
} satisfies ChainIndexingStatusSnapshotBackfill);
Comment on lines +46 to +94
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Status determination order: Queued check may mask contradictory states.

If checkpointBlock equals startBlock and indexingCompleted is simultaneously true (a contradictory state), the Queued branch on line 48 fires first and the inconsistency is silently swallowed. I see the TODO on lines 41-44 acknowledges the need for a proper type discriminator from PR #1612, which should address this. Until then, consider adding a defensive log/warning when the Queued condition is met but metrics indicate completion or realtime, so contradictory states don't go unnoticed.

🤖 Prompt for AI Agents
In
`@apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts`
around lines 46 - 94, The Queued branch (when
isBlockRefEqualTo(chainBlockRefs.config.startBlock, checkpointBlock) is true)
can mask contradictory states if chainIndexingMetrics.indexingCompleted or
indexingRealtime are also true; update the queued branch in
chain-indexing-status-snapshot to emit a defensive warning (e.g., logger.warn)
including chainId, checkpointBlock and which metric(s) are set before returning
the Queued snapshot so these inconsistencies are visible in logs; keep the
existing return value (validateChainIndexingStatusSnapshot with
ChainIndexingStatusIds.Queued) but do not change downstream logic.

}

/**
* Build Chain Indexing Status Snapshots
*
* Builds {@link ChainIndexingStatusSnapshot} for each indexed chain based on:
* - block refs based on chain configuration and RPC data,
* - current indexing status,
* - current indexing metrics.
*
* @param indexedChainIds list of indexed chain IDs to build snapshots for.
* @param chainsBlockRefs block refs for indexed chains.
* @param chainsIndexingMetrics indexing metrics for indexed chains.
* @param chainsIndexingStatus indexing status for indexed chains.
*
* @returns record of {@link ChainIndexingStatusSnapshot} keyed by chain ID.
*
* @throws error if any of the required data is missing or if data validation fails.
*/
export function buildChainIndexingStatusSnapshots(
indexedChainIds: ChainId[],
chainsBlockRefs: Map<ChainId, ChainBlockRefs>,
chainsIndexingMetrics: Map<ChainId, ChainIndexingMetrics>,
chainsIndexingStatus: Map<ChainId, ChainIndexingStatus>,
): Map<ChainId, ChainIndexingStatusSnapshot> {
const chainStatusSnapshots = new Map<ChainId, ChainIndexingStatusSnapshot>();

// Build chain indexing status snapshot for each indexed chain.
for (const chainId of indexedChainIds) {
const chainBlockRefs = chainsBlockRefs.get(chainId);
const chainIndexingStatus = chainsIndexingStatus.get(chainId);
const chainIndexingMetrics = chainsIndexingMetrics.get(chainId);

// Invariant: block refs must be defined for the chain
if (!chainBlockRefs) {
throw new Error(`Block refs must be defined for chain ID ${chainId}`);
}

// Invariant: chainIndexingStatus must be defined for the chain
if (!chainIndexingStatus) {
throw new Error(`Indexing status must be defined for chain ID ${chainId}`);
}

// Invariant: chainIndexingMetrics must be defined for the chain
if (!chainIndexingMetrics) {
throw new Error(`Indexing metrics must be defined for chain ID ${chainId}`);
}

const chainStatusSnapshot = buildChainIndexingStatusSnapshot(
chainId,
chainBlockRefs,
chainIndexingMetrics,
chainIndexingStatus,
);

chainStatusSnapshots.set(chainId, chainStatusSnapshot);
}

return chainStatusSnapshots;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

Filename contains typo: 'corss' should be 'cross' in indexing status builder module

Fix on Vercel

type CrossChainIndexingStatusSnapshotOmnichain,
CrossChainIndexingStrategyIds,
type OmnichainIndexingStatusSnapshot,
} from "@ensnode/ensnode-sdk";
import type { UnixTimestamp } from "@ensnode/ponder-sdk";
Comment on lines +1 to +6
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the actual filename in the repository
find . -type f -name "*cross*chain*indexing*" -o -name "*corss*chain*indexing*" | grep -E "\.(ts|tsx|js)$"

# Check for any imports referencing either spelling
rg "from.*corss-chain-indexing-status-snapshot" --type ts
rg "from.*cross-chain-indexing-status-snapshot" --type ts

# Check if the file exists at the path mentioned in the review
ls -la "apps/ensindexer/src/lib/indexing-status-builder/" 2>/dev/null | grep -i "chain"

Repository: namehash/ensnode

Length of output: 477


🏁 Script executed:

# Check for barrel/index exports in the directory
cat "apps/ensindexer/src/lib/indexing-status-builder/index.ts" 2>/dev/null

# Search for imports of the specific function name
rg "buildCrossChainIndexingStatusSnapshotOmnichain" --type ts

# Check for wildcard imports from this directory
rg "from.*indexing-status-builder['\"]" --type ts

Repository: namehash/ensnode

Length of output: 211


Filename typo: corss-chaincross-chain.

The file is named corss-chain-indexing-status-snapshot.ts — "corss" is a typo for "cross". Rename the file to cross-chain-indexing-status-snapshot.ts to prevent confusion and ensure correct import paths for future consumers.

🤖 Prompt for AI Agents
In
`@apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts`
around lines 1 - 6, The file name has a typo
("corss-chain-indexing-status-snapshot.ts"); rename it to
"cross-chain-indexing-status-snapshot.ts" and update every import/reference that
points to the old name so modules can resolve correctly; check for usages
referencing CrossChainIndexingStatusSnapshotOmnichain,
CrossChainIndexingStrategyIds, and OmnichainIndexingStatusSnapshot and adjust
their import paths to the new file name, then run TypeScript/IDE imports fix to
ensure no broken references remain.


export function buildCrossChainIndexingStatusSnapshotOmnichain(
omnichainSnapshot: OmnichainIndexingStatusSnapshot,
snapshotTime: UnixTimestamp,
): CrossChainIndexingStatusSnapshotOmnichain {
return {
strategy: CrossChainIndexingStrategyIds.Omnichain,
slowestChainIndexingCursor: omnichainSnapshot.omnichainIndexingCursor,
snapshotTime,
omnichainSnapshot,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
type ChainIndexingStatusSnapshotBackfill,
type ChainIndexingStatusSnapshotCompleted,
type ChainIndexingStatusSnapshotQueued,
getOmnichainIndexingCursor,
getOmnichainIndexingStatus,
OmnichainIndexingStatusIds,
type OmnichainIndexingStatusSnapshot,
type OmnichainIndexingStatusSnapshotBackfill,
type OmnichainIndexingStatusSnapshotCompleted,
type OmnichainIndexingStatusSnapshotFollowing,
type OmnichainIndexingStatusSnapshotUnstarted,
} from "@ensnode/ensnode-sdk";
import type { ChainId, PonderIndexingMetrics, PonderIndexingStatus } from "@ensnode/ponder-sdk";

import type { ChainBlockRefs } from "./chain-block-refs";
import { buildChainIndexingStatusSnapshots } from "./chain-indexing-status-snapshot";
import { validateOmnichainIndexingStatusSnapshot } from "./validate/omnichain-indexing-status-snapshot";

export function buildOmnichainIndexingStatusSnapshot(
indexedChainIds: ChainId[],
chainsBlockRefs: Map<ChainId, ChainBlockRefs>,
ponderIndexingMetrics: PonderIndexingMetrics,
ponderIndexingStatus: PonderIndexingStatus,
): OmnichainIndexingStatusSnapshot {
const chainStatusSnapshots = buildChainIndexingStatusSnapshots(
indexedChainIds,
chainsBlockRefs,
ponderIndexingMetrics.chains,
ponderIndexingStatus.chains,
);
const chains = Array.from(chainStatusSnapshots.values());
const omnichainStatus = getOmnichainIndexingStatus(chains);
const omnichainIndexingCursor = getOmnichainIndexingCursor(chains);

switch (omnichainStatus) {
case OmnichainIndexingStatusIds.Unstarted: {
return validateOmnichainIndexingStatusSnapshot({
omnichainStatus: OmnichainIndexingStatusIds.Unstarted,
chains: chainStatusSnapshots as Map<ChainId, ChainIndexingStatusSnapshotQueued>, // narrowing the type here, will be validated in the following 'check' step
omnichainIndexingCursor,
} satisfies OmnichainIndexingStatusSnapshotUnstarted);
}

case OmnichainIndexingStatusIds.Backfill: {
return validateOmnichainIndexingStatusSnapshot({
omnichainStatus: OmnichainIndexingStatusIds.Backfill,
chains: chainStatusSnapshots as Map<ChainId, ChainIndexingStatusSnapshotBackfill>, // narrowing the type here, will be validated in the following 'check' step
omnichainIndexingCursor,
} satisfies OmnichainIndexingStatusSnapshotBackfill);
}

case OmnichainIndexingStatusIds.Completed: {
return validateOmnichainIndexingStatusSnapshot({
omnichainStatus: OmnichainIndexingStatusIds.Completed,
chains: chainStatusSnapshots as Map<ChainId, ChainIndexingStatusSnapshotCompleted>, // narrowing the type here, will be validated in the following 'check' step
omnichainIndexingCursor,
} satisfies OmnichainIndexingStatusSnapshotCompleted);
}

case OmnichainIndexingStatusIds.Following:
return validateOmnichainIndexingStatusSnapshot({
omnichainStatus: OmnichainIndexingStatusIds.Following,
chains: chainStatusSnapshots,
omnichainIndexingCursor,
} satisfies OmnichainIndexingStatusSnapshotFollowing);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ChainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk";

export function validateChainIndexingStatusSnapshot(
unvalidatedSnapshot: ChainIndexingStatusSnapshot,
): ChainIndexingStatusSnapshot {
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.

validateChainIndexingStatusSnapshot is a no-op (returns the input unchanged). Given the name and how it’s used, this is misleading and provides no runtime safety. Either implement validation (e.g., via @ensnode/ensnode-sdk zod schemas/deserializers) or rename it to reflect that it’s an identity function.

Suggested change
): ChainIndexingStatusSnapshot {
): ChainIndexingStatusSnapshot {
if (unvalidatedSnapshot === null || typeof unvalidatedSnapshot !== "object") {
throw new TypeError("Invalid ChainIndexingStatusSnapshot: expected a non-null object.");
}
// Additional structural validation can be added here if the shape of
// ChainIndexingStatusSnapshot is available at runtime.

Copilot uses AI. Check for mistakes.
return unvalidatedSnapshot;
}
Comment on lines +1 to +7
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

No-op validator provides false confidence to callers.

validateChainIndexingStatusSnapshot performs no validation—it returns the input unchanged. Callers will assume the snapshot has been validated. Since ChainIndexingStatusSnapshot is a discriminated union of 4 variants (Queued, Backfill, Following, Completed), consider either:

  1. Implementing actual invariant checks (e.g., verifying the discriminant and variant-specific fields).
  2. Adding a clear TODO comment so this doesn't ship to production unnoticed.
🤖 Prompt for AI Agents
In
`@apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts`
around lines 1 - 7, validateChainIndexingStatusSnapshot currently returns its
input without checks; replace the no-op with real validation of the
discriminated union ChainIndexingStatusSnapshot by switching on the discriminant
(the variant tag) and asserting each variant's required fields and types (e.g.,
Queued, Backfill, Following, Completed-specific invariants such as required
numeric offsets, timestamps, or nullable fields), throwing a descriptive Error
on failure; if you cannot implement full checks now, replace the body with a
clear TODO comment mentioning validateChainIndexingStatusSnapshot and
short-circuit with a runtime assertion (e.g., throw new Error("TODO:
validateChainIndexingStatusSnapshot not implemented")) so callers don't get
silent false confidence.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { OmnichainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk";

export function validateOmnichainIndexingStatusSnapshot(
unvalidatedSnapshot: OmnichainIndexingStatusSnapshot,
): OmnichainIndexingStatusSnapshot {
return unvalidatedSnapshot;
}
Loading