From 9bc4fc516862c3316a8433679e82b3f9f7f3805e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 6 Feb 2026 19:43:36 +0100 Subject: [PATCH 1/8] feat(ensindexer): introduce `Indexing Status Builder` module This module aims to host an abstraction layer that primarly relies on `viem` and `@ensnode/ponder-sdk` abstractions. It is still using `@ensnode/ensnode-sdk` for convenience, but leave the possiblity to iterate and remove `@ensnode/ensnode-sdk` dependency in the future. --- apps/ensindexer/package.json | 1 + .../chain-block-refs.ts | 114 +++++++++++++ .../chain-indexing-status-snapshot.ts | 154 ++++++++++++++++++ .../omnichain-indexing-status-snapshot.ts | 68 ++++++++ .../chain-indexing-status-snapshot.ts | 7 + .../omnichain-indexing-status-snapshot.ts | 7 + packages/ponder-sdk/src/blocks.ts | 67 ++++++++ .../src/deserialize/indexing-status.ts | 7 +- packages/ponder-sdk/src/index.ts | 2 + packages/ponder-sdk/src/indexing-status.ts | 22 ++- pnpm-lock.yaml | 3 + 11 files changed, 444 insertions(+), 8 deletions(-) create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index 1db3a2711..0f22cb35c 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -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", diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts new file mode 100644 index 000000000..6726a2948 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts @@ -0,0 +1,114 @@ +import type { PublicClient } from "viem"; + +import { bigIntToNumber, type ChainIdString, deserializeBlockRef } from "@ensnode/ensnode-sdk"; +import type { BlockNumber, BlockRef, Blockrange, 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 { + const block = await publicClient.getBlock({ blockNumber: BigInt(blockNumber) }); + + if (!block) { + throw new Error(`Could not fetch block ${blockNumber}`); + } + + return deserializeBlockRef({ + number: bigIntToNumber(block.number), + timestamp: bigIntToNumber(block.timestamp), + }); +} + +/** + * 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: ChainIdString[], + chainsConfigBlockrange: Record, + chainsIndexingMetrics: Map, + publicClients: Record, +): Promise> { + const chainsBlockRefs = new Map(); + + 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; + + 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}`); + } + } + + return chainsBlockRefs; +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..376ac0115 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts @@ -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; + 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)) { + 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); +} + +/** + * 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, + chainsIndexingMetrics: Map, + chainsIndexingStatus: Map, +): Map { + const chainStatusSnapshots = new Map(); + + // 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; +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..ef167d000 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts @@ -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, + 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, // 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, // 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, // 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); + } +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..0e4fd1e9c --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts @@ -0,0 +1,7 @@ +import type { ChainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk"; + +export function validateChainIndexingStatusSnapshot( + unvalidatedSnapshot: ChainIndexingStatusSnapshot, +): ChainIndexingStatusSnapshot { + return unvalidatedSnapshot; +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..20fd23a92 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts @@ -0,0 +1,7 @@ +import type { OmnichainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk"; + +export function validateOmnichainIndexingStatusSnapshot( + unvalidatedSnapshot: OmnichainIndexingStatusSnapshot, +): OmnichainIndexingStatusSnapshot { + return unvalidatedSnapshot; +} diff --git a/packages/ponder-sdk/src/blocks.ts b/packages/ponder-sdk/src/blocks.ts index 05d985073..7f072c855 100644 --- a/packages/ponder-sdk/src/blocks.ts +++ b/packages/ponder-sdk/src/blocks.ts @@ -25,3 +25,70 @@ export const schemaBlockRef = z.object({ * Reference to a block. */ export type BlockRef = z.infer; + +/** + * Compare two {@link BlockRef} object to check + * if blockA is before blockB. + */ +export function isBlockRefBefore(blockA: BlockRef, blockB: BlockRef) { + return blockA.number < blockB.number && blockA.timestamp < blockB.timestamp; +} + +/** + * Compare two {@link BlockRef} object to check + * if blockA is equal to blockB. + */ +export function isBlockRefEqualTo(blockA: BlockRef, blockB: BlockRef) { + return blockA.number === blockB.number && blockA.timestamp === blockB.timestamp; +} + +/** + * Compare two {@link BlockRef} object to check + * if blockA is before or equal to blockB. + */ +export function isBlockRefBeforeOrEqualTo(blockA: BlockRef, blockB: BlockRef) { + return isBlockRefBefore(blockA, blockB) || isBlockRefEqualTo(blockA, blockB); +} + +/** + * Block range + * + * Represents a range of blocks + */ +export interface Blockrange { + /** + * Start block number + * + * Guaranteed to be lower than `endBlock` when both are present. + */ + startBlock?: BlockNumber; + + /** + * End block number + * + * Guaranteed to be greater than `startBlock` when both are present. + */ + endBlock?: BlockNumber; +} + +/** + * Block range with required start block + * + * Represents a range of blocks where the start block is required and the end + * block is optional. + */ +export interface BlockrangeWithStartBlock { + /** + * Start block number + * + * Guaranteed to be lower than `endBlock` when both are present. + */ + startBlock: BlockNumber; + + /** + * End block number + * + * Guaranteed to be greater than `startBlock` when both are present. + */ + endBlock?: BlockNumber; +} diff --git a/packages/ponder-sdk/src/deserialize/indexing-status.ts b/packages/ponder-sdk/src/deserialize/indexing-status.ts index 9f998c160..fa85060e2 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-status.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-status.ts @@ -9,11 +9,10 @@ import { prettifyError, z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; -import type { BlockRef } from "../blocks"; import { schemaBlockRef } from "../blocks"; import type { ChainId } from "../chains"; import { schemaChainId } from "../chains"; -import type { PonderIndexingStatus } from "../indexing-status"; +import type { ChainIndexingStatus, PonderIndexingStatus } from "../indexing-status"; const schemaSerializedChainName = z.string(); @@ -54,10 +53,10 @@ export type SerializedPonderIndexingStatus = z.infer(); + const chains = new Map(); for (const [, chainData] of Object.entries(data)) { - chains.set(chainData.id, chainData.block); + chains.set(chainData.id, { checkpointBlock: chainData.block }); } return { diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index 683a3b2e3..f1e54688b 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -1,3 +1,5 @@ +export * from "./blocks"; +export * from "./chains"; export * from "./client"; export * from "./indexing-metrics"; export * from "./indexing-status"; diff --git a/packages/ponder-sdk/src/indexing-status.ts b/packages/ponder-sdk/src/indexing-status.ts index 8bd444b4d..8305e930c 100644 --- a/packages/ponder-sdk/src/indexing-status.ts +++ b/packages/ponder-sdk/src/indexing-status.ts @@ -1,6 +1,23 @@ import type { BlockRef } from "./blocks"; import type { ChainId } from "./chains"; +/** + * Chain Indexing Status + * + * Represents the indexing status for a specific chain in a Ponder app. + */ +export interface ChainIndexingStatus { + /** + * Checkpoint Block + * + * During omnichain indexing, a Ponder app indexes the chain and keeps track of the latest indexed block for each chain. + * This is represented by the `checkpointBlock` property, which is a reference to either: + * - the first block to be indexed for the chain (if indexing is queued), or + * - the last indexed block for the chain (if indexing is in progress). + */ + checkpointBlock: BlockRef; +} + /** * Ponder Indexing Status * @@ -12,9 +29,6 @@ export interface PonderIndexingStatus { * * Guarantees: * - Includes entry for at least one indexed chain. - * - BlockRef corresponds to either: - * - The first block to be indexed (when chain indexing is currently queued). - * - The last indexed block (when chain indexing is currently in progress). */ - chains: Map; + chains: Map; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04a45864e..e85c8012e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -459,6 +459,9 @@ importers: '@ensnode/ponder-metadata': specifier: workspace:* version: link:../../packages/ponder-metadata + '@ensnode/ponder-sdk': + specifier: workspace:* + version: link:../../packages/ponder-sdk caip: specifier: 'catalog:' version: 1.1.1 From cb6057d05cbf8b4741f202c7aabf3ff4ccc842bd Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 6 Feb 2026 20:28:09 +0100 Subject: [PATCH 2/8] Add cross-chain indexing status snapshot to Indexing Status Builder --- .../chain-block-refs.ts | 41 +++++++++++-------- .../corss-chain-indexing-status-snapshot.ts | 18 ++++++++ packages/ponder-sdk/src/index.ts | 2 + 3 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts index 6726a2948..b4d54e595 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts @@ -1,7 +1,13 @@ import type { PublicClient } from "viem"; -import { bigIntToNumber, type ChainIdString, deserializeBlockRef } from "@ensnode/ensnode-sdk"; -import type { BlockNumber, BlockRef, Blockrange, ChainIndexingMetrics } from "@ensnode/ponder-sdk"; +import { bigIntToNumber, deserializeBlockRef } from "@ensnode/ensnode-sdk"; +import type { + BlockNumber, + BlockRef, + Blockrange, + ChainId, + ChainIndexingMetrics, +} from "@ensnode/ponder-sdk"; /** * Fetch block ref from RPC. @@ -15,16 +21,17 @@ async function fetchBlockRef( publicClient: PublicClient, blockNumber: BlockNumber, ): Promise { - const block = await publicClient.getBlock({ blockNumber: BigInt(blockNumber) }); - - if (!block) { - throw new Error(`Could not fetch block ${blockNumber}`); + 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}`); } - - return deserializeBlockRef({ - number: bigIntToNumber(block.number), - timestamp: bigIntToNumber(block.timestamp), - }); } /** @@ -54,12 +61,12 @@ export interface ChainBlockRefs { * Guaranteed to include {@link ChainBlockRefs} for each indexed chain. */ export async function getChainsBlockRefs( - chainIds: ChainIdString[], - chainsConfigBlockrange: Record, - chainsIndexingMetrics: Map, - publicClients: Record, -): Promise> { - const chainsBlockRefs = new Map(); + chainIds: ChainId[], + chainsConfigBlockrange: Record, + chainsIndexingMetrics: Map, + publicClients: Record, +): Promise> { + const chainsBlockRefs = new Map(); for (const chainId of chainIds) { const blockrange = chainsConfigBlockrange[chainId]; diff --git a/apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..6a06db9cd --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts @@ -0,0 +1,18 @@ +import { + type CrossChainIndexingStatusSnapshotOmnichain, + CrossChainIndexingStrategyIds, + type OmnichainIndexingStatusSnapshot, +} from "@ensnode/ensnode-sdk"; +import type { UnixTimestamp } from "@ensnode/ponder-sdk"; + +export function buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot: OmnichainIndexingStatusSnapshot, + snapshotTime: UnixTimestamp, +): CrossChainIndexingStatusSnapshotOmnichain { + return { + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: omnichainSnapshot.omnichainIndexingCursor, + snapshotTime, + omnichainSnapshot, + }; +} diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index f1e54688b..6c161e332 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -3,3 +3,5 @@ export * from "./chains"; export * from "./client"; export * from "./indexing-metrics"; export * from "./indexing-status"; +export * from "./numbers"; +export * from "./time"; From 008c0fa9926e01df05e82443e118b64594891cd8 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 11 Feb 2026 16:34:02 +0100 Subject: [PATCH 3/8] wip --- .../chain-indexing-status-snapshot.ts | 63 +++++++----------- .../omnichain-indexing-status-snapshot.ts | 65 +++++++++++++++---- .../chain-indexing-status-snapshot.ts | 7 -- .../omnichain-indexing-status-snapshot.ts | 7 -- 4 files changed, 76 insertions(+), 66 deletions(-) delete mode 100644 apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts delete mode 100644 apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts index 376ac0115..8f5ddccb2 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts @@ -7,52 +7,45 @@ import { type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, createIndexingConfig, + type Unvalidated, + validateChainIndexingStatusSnapshot, } from "@ensnode/ensnode-sdk"; import { type ChainId, type ChainIndexingMetrics, + ChainIndexingStates, 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. + * Build Unvalidated Chain Indexing Status Snapshot */ -export function buildChainIndexingStatusSnapshot( +export function buildUnvalidatedChainIndexingStatusSnapshot( chainId: ChainId, chainBlockRefs: ChainBlockRefs, chainIndexingMetrics: ChainIndexingMetrics, chainIndexingStatus: ChainIndexingStatus, -): ChainIndexingStatusSnapshot { +): Unvalidated { const { checkpointBlock } = chainIndexingStatus; + 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)) { - return validateChainIndexingStatusSnapshot({ + return { chainStatus: ChainIndexingStatusIds.Queued, config, - } satisfies ChainIndexingStatusSnapshotQueued); + } satisfies Unvalidated; } - if (chainIndexingMetrics.indexingCompleted) { + if (chainIndexingMetrics.state === ChainIndexingStates.Completed) { // TODO: move that invariant to validation schema if (config.configType !== ChainIndexingConfigTypeIds.Definite) { throw new Error( @@ -60,14 +53,14 @@ export function buildChainIndexingStatusSnapshot( ); } - return validateChainIndexingStatusSnapshot({ + return { chainStatus: ChainIndexingStatusIds.Completed, latestIndexedBlock: checkpointBlock, config, - } satisfies ChainIndexingStatusSnapshotCompleted); + } satisfies Unvalidated; } - if (chainIndexingMetrics.indexingRealtime) { + if (chainIndexingMetrics.state === ChainIndexingStates.Realtime) { // TODO: move that invariant to validation schema if (config.configType !== ChainIndexingConfigTypeIds.Indefinite) { throw new Error( @@ -75,49 +68,41 @@ export function buildChainIndexingStatusSnapshot( ); } - return validateChainIndexingStatusSnapshot({ + return { chainStatus: ChainIndexingStatusIds.Following, latestIndexedBlock: checkpointBlock, latestKnownBlock: chainIndexingMetrics.latestSyncedBlock, - config: { - configType: config.configType, - startBlock: config.startBlock, - }, - } satisfies ChainIndexingStatusSnapshotFollowing); + config, + } satisfies Unvalidated; } - return validateChainIndexingStatusSnapshot({ + return { chainStatus: ChainIndexingStatusIds.Backfill, latestIndexedBlock: checkpointBlock, backfillEndBlock: chainBlockRefs.backfillEndBlock, config, - } satisfies ChainIndexingStatusSnapshotBackfill); + } satisfies Unvalidated; } /** - * 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. + * Build Unvalidated Chain Indexing Status Snapshots * * @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. + * @returns record of {@link Unvalidated} keyed by chain ID. * * @throws error if any of the required data is missing or if data validation fails. */ -export function buildChainIndexingStatusSnapshots( +export function buildUnvalidatedChainIndexingStatusSnapshots( indexedChainIds: ChainId[], chainsBlockRefs: Map, chainsIndexingMetrics: Map, chainsIndexingStatus: Map, -): Map { - const chainStatusSnapshots = new Map(); +): Map> { + const chainStatusSnapshots = new Map>(); // Build chain indexing status snapshot for each indexed chain. for (const chainId of indexedChainIds) { @@ -140,7 +125,7 @@ export function buildChainIndexingStatusSnapshots( throw new Error(`Indexing metrics must be defined for chain ID ${chainId}`); } - const chainStatusSnapshot = buildChainIndexingStatusSnapshot( + const chainStatusSnapshot = buildUnvalidatedChainIndexingStatusSnapshot( chainId, chainBlockRefs, chainIndexingMetrics, diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts index ef167d000..d48d5c3a8 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts @@ -1,4 +1,5 @@ import { + type ChainIndexingStatusSnapshot, type ChainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotCompleted, type ChainIndexingStatusSnapshotQueued, @@ -10,12 +11,40 @@ import { type OmnichainIndexingStatusSnapshotCompleted, type OmnichainIndexingStatusSnapshotFollowing, type OmnichainIndexingStatusSnapshotUnstarted, + type Unvalidated, } from "@ensnode/ensnode-sdk"; -import type { ChainId, PonderIndexingMetrics, PonderIndexingStatus } from "@ensnode/ponder-sdk"; +import type { + ChainId, + ChainIndexingMetrics, + ChainIndexingStatus, + 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"; +import { buildUnvalidatedChainIndexingStatusSnapshots } from "./chain-indexing-status-snapshot"; + +function validateOmnichainIndexingStatusSnapshot( + snapshot: Unvalidated, +): OmnichainIndexingStatusSnapshot { + return snapshot as OmnichainIndexingStatusSnapshot; // TODO: implement actual validation logic in ENSNode SDK +} + +function buildChainStatusSnapshots( + indexedChainIds: ChainId[], + chainsBlockRefs: Map, + chainsIndexingMetrics: Map, + chainsIndexingStatus: Map, +): Map> { + const chainStatusSnapshots = buildUnvalidatedChainIndexingStatusSnapshots( + indexedChainIds, + chainsBlockRefs, + chainsIndexingMetrics, + chainsIndexingStatus, + ); + + return chainStatusSnapshots; +} export function buildOmnichainIndexingStatusSnapshot( indexedChainIds: ChainId[], @@ -23,39 +52,49 @@ export function buildOmnichainIndexingStatusSnapshot( ponderIndexingMetrics: PonderIndexingMetrics, ponderIndexingStatus: PonderIndexingStatus, ): OmnichainIndexingStatusSnapshot { - const chainStatusSnapshots = buildChainIndexingStatusSnapshots( + const chainStatusSnapshots = buildChainStatusSnapshots( 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, // narrowing the type here, will be validated in the following 'check' step + return { + omnichainStatus, + chains: chainStatusSnapshots as Map< + ChainId, + Unvalidated + >, // narrowing the type here, will be validated in the following 'check' step omnichainIndexingCursor, - } satisfies OmnichainIndexingStatusSnapshotUnstarted); + } satisfies Unvalidated; } case OmnichainIndexingStatusIds.Backfill: { return validateOmnichainIndexingStatusSnapshot({ omnichainStatus: OmnichainIndexingStatusIds.Backfill, - chains: chainStatusSnapshots as Map, // narrowing the type here, will be validated in the following 'check' step + chains: chainStatusSnapshots as Map< + ChainId, + Unvalidated + >, // narrowing the type here, will be validated in the following 'check' step omnichainIndexingCursor, - } satisfies OmnichainIndexingStatusSnapshotBackfill); + } satisfies Unvalidated); } case OmnichainIndexingStatusIds.Completed: { return validateOmnichainIndexingStatusSnapshot({ omnichainStatus: OmnichainIndexingStatusIds.Completed, - chains: chainStatusSnapshots as Map, // narrowing the type here, will be validated in the following 'check' step + chains: chainStatusSnapshots as Map< + ChainId, + Unvalidated + >, // narrowing the type here, will be validated in the following 'check' step omnichainIndexingCursor, - } satisfies OmnichainIndexingStatusSnapshotCompleted); + } satisfies Unvalidated); } case OmnichainIndexingStatusIds.Following: @@ -63,6 +102,6 @@ export function buildOmnichainIndexingStatusSnapshot( omnichainStatus: OmnichainIndexingStatusIds.Following, chains: chainStatusSnapshots, omnichainIndexingCursor, - } satisfies OmnichainIndexingStatusSnapshotFollowing); + } satisfies Unvalidated); } } diff --git a/apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts deleted file mode 100644 index 0e4fd1e9c..000000000 --- a/apps/ensindexer/src/lib/indexing-status-builder/validate/chain-indexing-status-snapshot.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ChainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk"; - -export function validateChainIndexingStatusSnapshot( - unvalidatedSnapshot: ChainIndexingStatusSnapshot, -): ChainIndexingStatusSnapshot { - return unvalidatedSnapshot; -} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts deleted file mode 100644 index 20fd23a92..000000000 --- a/apps/ensindexer/src/lib/indexing-status-builder/validate/omnichain-indexing-status-snapshot.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { OmnichainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk"; - -export function validateOmnichainIndexingStatusSnapshot( - unvalidatedSnapshot: OmnichainIndexingStatusSnapshot, -): OmnichainIndexingStatusSnapshot { - return unvalidatedSnapshot; -} From 63275b604021adc23e79a117b6c1ee86f3b2485a Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Feb 2026 06:21:38 +0100 Subject: [PATCH 4/8] Improve data model for Indexing Status Builder Simplify code, merge input params for `buildOmnichainIndexingStatusSnapshot` into a single input object with chain metadata. --- .../indexing-status-builder/backfill-scope.ts | 24 +++ .../chain-block-refs.ts | 121 ---------------- .../chain-indexing-metadata.ts | 43 ++++++ .../chain-indexing-status-snapshot.ts | 137 ++++++------------ .../omnichain-indexing-status-snapshot.ts | 63 +++----- 5 files changed, 127 insertions(+), 261 deletions(-) create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/backfill-scope.ts delete mode 100644 apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts diff --git a/apps/ensindexer/src/lib/indexing-status-builder/backfill-scope.ts b/apps/ensindexer/src/lib/indexing-status-builder/backfill-scope.ts new file mode 100644 index 000000000..29cf4ff61 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/backfill-scope.ts @@ -0,0 +1,24 @@ +import type { BlockRef } from "@ensnode/ponder-sdk"; + +/** + * Backfill scope for a chain. + * + * Defines the range of blocks to be indexed during the backfill phase. + * The backfill phase progresses from `startBlock` toward `endBlock`, and ends + * when the indexed block reaches `endBlock`. + */ +export interface BackfillScope { + /** + * The starting block of the backfill range (inclusive). + * This is derived from the chain's indexing configuration. + */ + startBlock: BlockRef; + + /** + * The ending block of the backfill range (inclusive). + * This is the "fixed target" that backfill progresses toward. + * + * Guaranteed to be greater than `startBlock`. + */ + endBlock: BlockRef; +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts deleted file mode 100644 index b4d54e595..000000000 --- a/apps/ensindexer/src/lib/indexing-status-builder/chain-block-refs.ts +++ /dev/null @@ -1,121 +0,0 @@ -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 { - 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}`); - } -} - -/** - * 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, - chainsIndexingMetrics: Map, - publicClients: Record, -): Promise> { - const chainsBlockRefs = new Map(); - - 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; - - 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}`); - } - } - - return chainsBlockRefs; -} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts new file mode 100644 index 000000000..e37c1459b --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.ts @@ -0,0 +1,43 @@ +import type { ChainIndexingConfig } from "@ensnode/ensnode-sdk"; +import type { ChainIndexingMetrics, ChainIndexingStatus } from "@ensnode/ponder-sdk"; + +import type { BackfillScope } from "./backfill-scope"; + +/** + * Complete indexing metadata for a single chain. + * + * Bundles all data sources describing the chain indexing that are needed to + * build a ChainIndexingStatusSnapshot: + * - The configured backfill scope (what blocks we need to index) + * - Ponder's indexing metrics (current progress/performance) + * - Ponder's indexing status (checkpoint state) + */ +export interface ChainIndexingMetadata { + /** + * Backfill scope for the chain + * + * Defines the range of blocks to be indexed during the backfill phase. + */ + backfillScope: BackfillScope; + + /** + * Indexing config for the chain + * + * Defines the range of blocks to be indexed for the chain. + */ + indexingConfig: ChainIndexingConfig; + + /** + * Indexing metrics for the chain + * + * References current indexing metrics from Ponder Metrics. + */ + indexingMetrics: ChainIndexingMetrics; + + /** + * Indexing status for this chain + * + * References the checkpoint block from Ponder Status. + */ + indexingStatus: ChainIndexingStatus; +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts index 8f5ddccb2..bd50c3539 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts @@ -1,136 +1,83 @@ import { - ChainIndexingConfigTypeIds, + type ChainIndexingConfigDefinite, + type ChainIndexingConfigIndefinite, ChainIndexingStatusIds, type ChainIndexingStatusSnapshot, type ChainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotCompleted, type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, - createIndexingConfig, type Unvalidated, validateChainIndexingStatusSnapshot, } from "@ensnode/ensnode-sdk"; -import { - type ChainId, - type ChainIndexingMetrics, - ChainIndexingStates, - type ChainIndexingStatus, - isBlockRefEqualTo, -} from "@ensnode/ponder-sdk"; +import { type ChainId, ChainIndexingStates, isBlockRefEqualTo } from "@ensnode/ponder-sdk"; -import type { ChainBlockRefs } from "./chain-block-refs"; +import type { ChainIndexingMetadata } from "./chain-indexing-metadata"; /** - * Build Unvalidated Chain Indexing Status Snapshot + * Build Chain Indexing Status Snapshot from metadata. + * + * @param metadata - Complete indexing metadata including backfill scope, + * Ponder config, metrics, and status needed to determine the chain's state. + * + * @returns The chain indexing status snapshot. */ -export function buildUnvalidatedChainIndexingStatusSnapshot( - chainId: ChainId, - chainBlockRefs: ChainBlockRefs, - chainIndexingMetrics: ChainIndexingMetrics, - chainIndexingStatus: ChainIndexingStatus, -): Unvalidated { - const { checkpointBlock } = chainIndexingStatus; - - const config = createIndexingConfig( - chainBlockRefs.config.startBlock, - chainBlockRefs.config.endBlock, - ); +function buildChainIndexingStatusSnapshot( + metadata: ChainIndexingMetadata, +): ChainIndexingStatusSnapshot { + const { backfillScope, indexingConfig, indexingMetrics, indexingStatus } = metadata; + const { checkpointBlock } = indexingStatus; // 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)) { - return { + if (isBlockRefEqualTo(backfillScope.startBlock, checkpointBlock)) { + return validateChainIndexingStatusSnapshot({ chainStatus: ChainIndexingStatusIds.Queued, - config, - } satisfies Unvalidated; + config: indexingConfig, + } satisfies Unvalidated); } - if (chainIndexingMetrics.state === ChainIndexingStates.Completed) { - // 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 { + if (indexingMetrics.state === ChainIndexingStates.Completed) { + return validateChainIndexingStatusSnapshot({ chainStatus: ChainIndexingStatusIds.Completed, latestIndexedBlock: checkpointBlock, - config, - } satisfies Unvalidated; + config: indexingConfig as Unvalidated, + } satisfies Unvalidated); } - if (chainIndexingMetrics.state === ChainIndexingStates.Realtime) { - // 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 { + if (indexingMetrics.state === ChainIndexingStates.Realtime) { + return validateChainIndexingStatusSnapshot({ chainStatus: ChainIndexingStatusIds.Following, latestIndexedBlock: checkpointBlock, - latestKnownBlock: chainIndexingMetrics.latestSyncedBlock, - config, - } satisfies Unvalidated; + latestKnownBlock: indexingMetrics.latestSyncedBlock, + config: indexingConfig as Unvalidated, + } satisfies Unvalidated); } - return { + return validateChainIndexingStatusSnapshot({ chainStatus: ChainIndexingStatusIds.Backfill, latestIndexedBlock: checkpointBlock, - backfillEndBlock: chainBlockRefs.backfillEndBlock, - config, - } satisfies Unvalidated; + backfillEndBlock: backfillScope.endBlock, + config: indexingConfig, + } satisfies Unvalidated); } /** - * Build Unvalidated Chain Indexing Status Snapshots + * Build Chain Indexing Status Snapshot from metadata. * - * @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. + * @param metadata - Complete indexing metadata including backfill scope, + * Ponder config, metrics, and status needed to determine the chain's state. * - * @returns record of {@link Unvalidated} keyed by chain ID. - * - * @throws error if any of the required data is missing or if data validation fails. + * @returns The chain indexing status snapshot. */ -export function buildUnvalidatedChainIndexingStatusSnapshots( - indexedChainIds: ChainId[], - chainsBlockRefs: Map, - chainsIndexingMetrics: Map, - chainsIndexingStatus: Map, -): Map> { - const chainStatusSnapshots = new Map>(); +export function buildChainStatusSnapshots( + chainsIndexingMetadata: Map, +): Map { + const chainStatusSnapshots = new Map(); // 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 = buildUnvalidatedChainIndexingStatusSnapshot( - chainId, - chainBlockRefs, - chainIndexingMetrics, - chainIndexingStatus, - ); + for (const [chainId, metadata] of chainsIndexingMetadata) { + const chainStatusSnapshot = buildChainIndexingStatusSnapshot(metadata); chainStatusSnapshots.set(chainId, chainStatusSnapshot); } diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts index d48d5c3a8..9593c7ba5 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts @@ -1,5 +1,4 @@ import { - type ChainIndexingStatusSnapshot, type ChainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotCompleted, type ChainIndexingStatusSnapshotQueued, @@ -12,67 +11,41 @@ import { type OmnichainIndexingStatusSnapshotFollowing, type OmnichainIndexingStatusSnapshotUnstarted, type Unvalidated, + validateOmnichainIndexingStatusSnapshot, } from "@ensnode/ensnode-sdk"; -import type { - ChainId, - ChainIndexingMetrics, - ChainIndexingStatus, - PonderIndexingMetrics, - PonderIndexingStatus, -} from "@ensnode/ponder-sdk"; +import type { ChainId } from "@ensnode/ponder-sdk"; -import type { ChainBlockRefs } from "./chain-block-refs"; -import { buildUnvalidatedChainIndexingStatusSnapshots } from "./chain-indexing-status-snapshot"; - -function validateOmnichainIndexingStatusSnapshot( - snapshot: Unvalidated, -): OmnichainIndexingStatusSnapshot { - return snapshot as OmnichainIndexingStatusSnapshot; // TODO: implement actual validation logic in ENSNode SDK -} - -function buildChainStatusSnapshots( - indexedChainIds: ChainId[], - chainsBlockRefs: Map, - chainsIndexingMetrics: Map, - chainsIndexingStatus: Map, -): Map> { - const chainStatusSnapshots = buildUnvalidatedChainIndexingStatusSnapshots( - indexedChainIds, - chainsBlockRefs, - chainsIndexingMetrics, - chainsIndexingStatus, - ); - - return chainStatusSnapshots; -} +import type { ChainIndexingMetadata } from "./chain-indexing-metadata"; +import { buildChainStatusSnapshots } from "./chain-indexing-status-snapshot"; +/** + * Build an omnichain indexing status snapshot from per-chain metadata. + * + * @param chainsIndexingMetadata - A map of chain IDs to their complete indexing + * metadata. Each entry contains the backfill scope, Ponder metrics, and status + * needed to determine that chain's indexing state. + * + * @returns The validated omnichain indexing status snapshot. + */ export function buildOmnichainIndexingStatusSnapshot( - indexedChainIds: ChainId[], - chainsBlockRefs: Map, - ponderIndexingMetrics: PonderIndexingMetrics, - ponderIndexingStatus: PonderIndexingStatus, + chainsIndexingMetadata: Map, ): OmnichainIndexingStatusSnapshot { - const chainStatusSnapshots = buildChainStatusSnapshots( - indexedChainIds, - chainsBlockRefs, - ponderIndexingMetrics.chains, - ponderIndexingStatus.chains, - ); + const chainStatusSnapshots = buildChainStatusSnapshots(chainsIndexingMetadata); - const chains = Array.from(chainStatusSnapshots.values()); + const chains = Array.from(chainStatusSnapshots.values()).filter(Boolean); const omnichainStatus = getOmnichainIndexingStatus(chains); const omnichainIndexingCursor = getOmnichainIndexingCursor(chains); switch (omnichainStatus) { case OmnichainIndexingStatusIds.Unstarted: { - return { + return validateOmnichainIndexingStatusSnapshot({ omnichainStatus, chains: chainStatusSnapshots as Map< ChainId, Unvalidated >, // narrowing the type here, will be validated in the following 'check' step omnichainIndexingCursor, - } satisfies Unvalidated; + } satisfies Unvalidated); } case OmnichainIndexingStatusIds.Backfill: { From 9bad3422cc06c7352f2ec75bcd1e24e4d75f3ba0 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Feb 2026 08:39:39 +0100 Subject: [PATCH 5/8] Apply AI PR feedback --- .../chain-indexing-status-snapshot.ts | 8 ++++---- ...napshot.ts => cross-chain-indexing-status-snapshot.ts} | 0 .../omnichain-indexing-status-snapshot.ts | 2 +- packages/ponder-sdk/src/blocks.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename apps/ensindexer/src/lib/indexing-status-builder/{corss-chain-indexing-status-snapshot.ts => cross-chain-indexing-status-snapshot.ts} (100%) diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts index bd50c3539..7704a90b3 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.ts @@ -63,12 +63,12 @@ function buildChainIndexingStatusSnapshot( } /** - * Build Chain Indexing Status Snapshot from metadata. + * Build Chain Indexing Status Snapshots for all indexed chains. * - * @param metadata - Complete indexing metadata including backfill scope, - * Ponder config, metrics, and status needed to determine the chain's state. + * @param chainsIndexingMetadata - A map of chain IDs to their complete + * indexing metadata. * - * @returns The chain indexing status snapshot. + * @returns A map of chain IDs to their chain indexing status snapshots. */ export function buildChainStatusSnapshots( chainsIndexingMetadata: Map, diff --git a/apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.ts similarity index 100% rename from apps/ensindexer/src/lib/indexing-status-builder/corss-chain-indexing-status-snapshot.ts rename to apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.ts diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts index 9593c7ba5..2bbe11016 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts @@ -32,7 +32,7 @@ export function buildOmnichainIndexingStatusSnapshot( ): OmnichainIndexingStatusSnapshot { const chainStatusSnapshots = buildChainStatusSnapshots(chainsIndexingMetadata); - const chains = Array.from(chainStatusSnapshots.values()).filter(Boolean); + const chains = Array.from(chainStatusSnapshots.values()); const omnichainStatus = getOmnichainIndexingStatus(chains); const omnichainIndexingCursor = getOmnichainIndexingCursor(chains); diff --git a/packages/ponder-sdk/src/blocks.ts b/packages/ponder-sdk/src/blocks.ts index 7f072c855..609c4e9f2 100644 --- a/packages/ponder-sdk/src/blocks.ts +++ b/packages/ponder-sdk/src/blocks.ts @@ -27,7 +27,7 @@ export const schemaBlockRef = z.object({ export type BlockRef = z.infer; /** - * Compare two {@link BlockRef} object to check + * Compare two {@link BlockRef} objects to check * if blockA is before blockB. */ export function isBlockRefBefore(blockA: BlockRef, blockB: BlockRef) { @@ -35,7 +35,7 @@ export function isBlockRefBefore(blockA: BlockRef, blockB: BlockRef) { } /** - * Compare two {@link BlockRef} object to check + * Compare two {@link BlockRef} objects to check * if blockA is equal to blockB. */ export function isBlockRefEqualTo(blockA: BlockRef, blockB: BlockRef) { @@ -43,7 +43,7 @@ export function isBlockRefEqualTo(blockA: BlockRef, blockB: BlockRef) { } /** - * Compare two {@link BlockRef} object to check + * Compare two {@link BlockRef} objects to check * if blockA is before or equal to blockB. */ export function isBlockRefBeforeOrEqualTo(blockA: BlockRef, blockB: BlockRef) { From afe61a4460f14aba17dc1c13854885f23e7add2f Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Feb 2026 08:54:38 +0100 Subject: [PATCH 6/8] Apply AI PR feedback --- .../cross-chain-indexing-status-snapshot.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.ts index 6a06db9cd..eecac3bf2 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.ts @@ -2,6 +2,8 @@ import { type CrossChainIndexingStatusSnapshotOmnichain, CrossChainIndexingStrategyIds, type OmnichainIndexingStatusSnapshot, + type Unvalidated, + validateCrossChainIndexingStatusSnapshot, } from "@ensnode/ensnode-sdk"; import type { UnixTimestamp } from "@ensnode/ponder-sdk"; @@ -9,10 +11,10 @@ export function buildCrossChainIndexingStatusSnapshotOmnichain( omnichainSnapshot: OmnichainIndexingStatusSnapshot, snapshotTime: UnixTimestamp, ): CrossChainIndexingStatusSnapshotOmnichain { - return { + return validateCrossChainIndexingStatusSnapshot({ strategy: CrossChainIndexingStrategyIds.Omnichain, slowestChainIndexingCursor: omnichainSnapshot.omnichainIndexingCursor, snapshotTime, omnichainSnapshot, - }; + } satisfies Unvalidated); } From 34d792073cf701d62c528f0871f73b3515e5035f Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Feb 2026 13:36:29 +0100 Subject: [PATCH 7/8] Add testing suite cases --- .../block-refs.mock.ts | 21 + .../chain-indexing-metadata.mock.ts | 87 ++++ .../chain-indexing-status-snapshot.test.ts | 172 ++++++++ ...oss-chain-indexing-status-snapshot.test.ts | 400 ++++++++++++++++++ ...omnichain-indexing-status-snapshot.test.ts | 290 +++++++++++++ .../omnichain-indexing-status-snapshot.ts | 6 +- 6 files changed, 973 insertions(+), 3 deletions(-) create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/block-refs.mock.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.mock.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.test.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.test.ts create mode 100644 apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts diff --git a/apps/ensindexer/src/lib/indexing-status-builder/block-refs.mock.ts b/apps/ensindexer/src/lib/indexing-status-builder/block-refs.mock.ts new file mode 100644 index 000000000..7fe06113e --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/block-refs.mock.ts @@ -0,0 +1,21 @@ +import type { BlockRef } from "@ensnode/ponder-sdk"; + +export const earliestBlockRef = { + timestamp: 1667260799, + number: 999, +} as const satisfies BlockRef; + +export const earlierBlockRef = { + timestamp: 1672531199, + number: 1024, +} as const satisfies BlockRef; + +export const laterBlockRef = { + timestamp: 1672531200, + number: 1025, +} as const satisfies BlockRef; + +export const latestBlockRef = { + timestamp: 1677104542, + number: 1222, +} as const satisfies BlockRef; diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.mock.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.mock.ts new file mode 100644 index 000000000..7317abf7a --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-metadata.mock.ts @@ -0,0 +1,87 @@ +import { type ChainIndexingConfig, ChainIndexingConfigTypeIds } from "@ensnode/ensnode-sdk"; +import type { + BlockRef, + ChainIndexingMetrics, + ChainIndexingMetricsCompleted, + ChainIndexingMetricsHistorical, + ChainIndexingMetricsRealtime, + ChainIndexingState, +} from "@ensnode/ponder-sdk"; +import { ChainIndexingStates } from "@ensnode/ponder-sdk"; + +import type { BackfillScope } from "./backfill-scope"; +import type { ChainIndexingMetadata } from "./chain-indexing-metadata"; + +/** + * Build complete indexing metadata for a chain, used for testing the indexing + * status snapshot builders. + * + * @param config - The chain indexing configuration, including backfill scope. + * @param checkpointBlock - The block ref representing the current checkpoint of + * indexing. + * @param state - The current state of indexing (Historical, Realtime, Completed). + * @param latestSyncedBlock - The latest block ref that has been synced (used for + * Realtime state). + * @param backfillEndBlock - The block ref representing the end of the backfill + * scope (used for Historical state). + * + * @returns Complete indexing metadata for the chain. + */ +export function buildChainIndexingMetadataMock({ + config, + checkpointBlock, + state, + latestSyncedBlock, + backfillEndBlock, +}: { + config: ChainIndexingConfig; + checkpointBlock: BlockRef; + state: ChainIndexingState; + latestSyncedBlock: BlockRef; + backfillEndBlock: BlockRef; +}): ChainIndexingMetadata { + // assuming historical total blocks as + // `latestBlockRef.number - config.startBlock.number + 1` for the sake of + // this mock, since we don't have real metrics to determine + // the actual end block for historical indexing. + const historicalTotalBlocks = backfillEndBlock.number - config.startBlock.number + 1; + + const backfillScope = { + startBlock: config.startBlock, + endBlock: + config.configType === ChainIndexingConfigTypeIds.Definite + ? config.endBlock + : backfillEndBlock, + } satisfies BackfillScope; + + let indexingMetrics: ChainIndexingMetrics; + + switch (state) { + case ChainIndexingStates.Historical: + indexingMetrics = { + state: ChainIndexingStates.Historical, + latestSyncedBlock, + historicalTotalBlocks, + } satisfies ChainIndexingMetricsHistorical; + break; + case ChainIndexingStates.Realtime: + indexingMetrics = { + state: ChainIndexingStates.Realtime, + latestSyncedBlock: latestSyncedBlock, + } satisfies ChainIndexingMetricsRealtime; + break; + case ChainIndexingStates.Completed: + indexingMetrics = { + state: ChainIndexingStates.Completed, + finalIndexedBlock: checkpointBlock, + } satisfies ChainIndexingMetricsCompleted; + break; + } + + return { + backfillScope, + indexingConfig: config, + indexingMetrics, + indexingStatus: { checkpointBlock }, + }; +} diff --git a/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.test.ts b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.test.ts new file mode 100644 index 000000000..d520bf270 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/chain-indexing-status-snapshot.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from "vitest"; + +import { + ChainIndexingConfigTypeIds, + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshotBackfill, + type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotFollowing, + type ChainIndexingStatusSnapshotQueued, + createIndexingConfig, +} from "@ensnode/ensnode-sdk"; +import { ChainIndexingStates } from "@ensnode/ponder-sdk"; + +import { + earlierBlockRef, + earliestBlockRef, + laterBlockRef, + latestBlockRef, +} from "./block-refs.mock"; +import type { ChainIndexingMetadata } from "./chain-indexing-metadata"; +import { buildChainIndexingMetadataMock } from "./chain-indexing-metadata.mock"; +import { buildChainStatusSnapshots } from "./chain-indexing-status-snapshot"; + +describe("ChainIndexingStatusSnapshot", () => { + describe("buildChainStatusSnapshots", () => { + it("returns Queued status when checkpointBlock equals startBlock", () => { + // arrange + const metadata = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, latestBlockRef), + checkpointBlock: earliestBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: earlierBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadata]]); + + // act + const result = buildChainStatusSnapshots(chainsMetadata); + + // assert + expect(result.get(1)).toStrictEqual({ + chainStatus: ChainIndexingStatusIds.Queued, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + } satisfies ChainIndexingStatusSnapshotQueued); + }); + + it("returns Completed status when indexingMetrics.state is Completed", () => { + // arrange + const metadata = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, latestBlockRef), + checkpointBlock: latestBlockRef, + state: ChainIndexingStates.Completed, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadata]]); + + // act + const result = buildChainStatusSnapshots(chainsMetadata); + + // assert + expect(result.get(1)).toStrictEqual({ + chainStatus: ChainIndexingStatusIds.Completed, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotCompleted); + }); + + it("returns Following status when indexingMetrics.state is Realtime", () => { + // arrange + const metadata = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, null), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Realtime, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadata]]); + + // act + const result = buildChainStatusSnapshots(chainsMetadata); + + // assert + expect(result.get(1)).toStrictEqual({ + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + latestKnownBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotFollowing); + }); + + it("returns Backfill status for Historical state with definite config", () => { + // arrange + const metadata = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, latestBlockRef), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadata]]); + + // act + const result = buildChainStatusSnapshots(chainsMetadata); + + // assert + expect(result.get(1)).toStrictEqual({ + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotBackfill); + }); + + it("returns Backfill status for Historical state with indefinite config", () => { + // arrange + const metadata = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, null), + checkpointBlock: earlierBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadata]]); + + // act + const result = buildChainStatusSnapshots(chainsMetadata); + + // assert + expect(result.get(1)).toStrictEqual({ + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + latestIndexedBlock: earlierBlockRef, + backfillEndBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotBackfill); + }); + + it("returns empty map when no chains provided", () => { + // arrange + const chainsMetadata = new Map(); + + // act + const result = buildChainStatusSnapshots(chainsMetadata); + + // assert + expect(result.size).toBe(0); + }); + }); +}); diff --git a/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.test.ts b/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.test.ts new file mode 100644 index 000000000..7f14ab657 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/cross-chain-indexing-status-snapshot.test.ts @@ -0,0 +1,400 @@ +import { describe, expect, it } from "vitest"; + +import { + ChainIndexingConfigTypeIds, + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshotBackfill, + type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotFollowing, + type ChainIndexingStatusSnapshotQueued, + CrossChainIndexingStrategyIds, + OmnichainIndexingStatusIds, + type OmnichainIndexingStatusSnapshotBackfill, + type OmnichainIndexingStatusSnapshotCompleted, + type OmnichainIndexingStatusSnapshotFollowing, + type OmnichainIndexingStatusSnapshotUnstarted, +} from "@ensnode/ensnode-sdk"; +import type { UnixTimestamp } from "@ensnode/ponder-sdk"; + +import { earliestBlockRef, laterBlockRef, latestBlockRef } from "./block-refs.mock"; +import { buildCrossChainIndexingStatusSnapshotOmnichain } from "./cross-chain-indexing-status-snapshot"; + +describe("cross-chain-indexing-status-snapshot", () => { + describe("buildCrossChainIndexingStatusSnapshotOmnichain", () => { + it("builds snapshot from omnichain backfill snapshot", () => { + // arrange + const snapshotTime = latestBlockRef.timestamp as UnixTimestamp; + const cursor = laterBlockRef.timestamp; + + const chains = new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotBackfill, + ], + ]); + + const omnichainSnapshot = { + omnichainStatus: OmnichainIndexingStatusIds.Backfill, + chains, + omnichainIndexingCursor: cursor, + } satisfies OmnichainIndexingStatusSnapshotBackfill; + + // act + const result = buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot, + snapshotTime, + ); + + // assert + expect(result).toStrictEqual({ + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: cursor, + snapshotTime, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Backfill, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotBackfill, + ], + ]), + omnichainIndexingCursor: cursor, + }, + }); + }); + + it("builds snapshot from omnichain completed snapshot", () => { + // arrange + const snapshotTime = latestBlockRef.timestamp as UnixTimestamp; + const cursor = latestBlockRef.timestamp; + + const chains = new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Completed, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotCompleted, + ], + ]); + + const omnichainSnapshot = { + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains, + omnichainIndexingCursor: cursor, + } satisfies OmnichainIndexingStatusSnapshotCompleted; + + // act + const result = buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot, + snapshotTime, + ); + + // assert + expect(result).toStrictEqual({ + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: cursor, + snapshotTime, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Completed, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: latestBlockRef, + }, + ], + ]), + omnichainIndexingCursor: cursor, + }, + }); + }); + + it("builds snapshot from omnichain following snapshot", () => { + // arrange + const snapshotTime = latestBlockRef.timestamp as UnixTimestamp; + const cursor = laterBlockRef.timestamp; + + const chains = new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + latestKnownBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotFollowing, + ], + ]); + + const omnichainSnapshot = { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains, + omnichainIndexingCursor: cursor, + } satisfies OmnichainIndexingStatusSnapshotFollowing; + + // act + const result = buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot, + snapshotTime, + ); + + // assert + expect(result).toStrictEqual({ + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: cursor, + snapshotTime, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + latestKnownBlock: latestBlockRef, + }, + ], + ]), + omnichainIndexingCursor: cursor, + }, + }); + }); + + it("builds snapshot from omnichain unstarted snapshot", () => { + // arrange + const cursor = earliestBlockRef.timestamp - 1; + // snapshotTime must be >= highest known block timestamp + // for unstarted with queued chain, this is endBlock timestamp + const snapshotTime = latestBlockRef.timestamp as UnixTimestamp; + + const chains = new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Queued, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + } satisfies ChainIndexingStatusSnapshotQueued, + ], + ]); + + const omnichainSnapshot = { + omnichainStatus: OmnichainIndexingStatusIds.Unstarted, + chains, + omnichainIndexingCursor: cursor, + } satisfies OmnichainIndexingStatusSnapshotUnstarted; + + // act + const result = buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot, + snapshotTime, + ); + + // assert + expect(result).toStrictEqual({ + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: cursor, + snapshotTime, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Unstarted, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Queued, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + }, + ], + ]), + omnichainIndexingCursor: cursor, + }, + }); + }); + + it("includes correct slowestChainIndexingCursor value", () => { + // arrange + const cursor = latestBlockRef.timestamp; + const snapshotTime = latestBlockRef.timestamp as UnixTimestamp; + + const chains = new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Completed, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotCompleted, + ], + ]); + + const omnichainSnapshot = { + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains, + omnichainIndexingCursor: cursor, + } satisfies OmnichainIndexingStatusSnapshotCompleted; + + // act + const result = buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot, + snapshotTime, + ); + + // assert + expect(result).toStrictEqual({ + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: cursor, + snapshotTime, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Completed, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: latestBlockRef, + }, + ], + ]), + omnichainIndexingCursor: cursor, + }, + }); + }); + + it("preserves nested omnichainSnapshot structure", () => { + // arrange + const cursor = laterBlockRef.timestamp; + const snapshotTime = latestBlockRef.timestamp as UnixTimestamp; + + const chains = new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + latestKnownBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotFollowing, + ], + [ + 8453, + { + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotBackfill, + ], + ]); + + const omnichainSnapshot = { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains, + omnichainIndexingCursor: cursor, + } satisfies OmnichainIndexingStatusSnapshotFollowing; + + // act + const result = buildCrossChainIndexingStatusSnapshotOmnichain( + omnichainSnapshot, + snapshotTime, + ); + + // assert + expect(result).toStrictEqual({ + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: cursor, + snapshotTime, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + latestKnownBlock: latestBlockRef, + }, + ], + [ + 8453, + { + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + }, + ], + ]), + omnichainIndexingCursor: cursor, + }, + }); + }); + }); +}); diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts new file mode 100644 index 000000000..c7fd6fbd3 --- /dev/null +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts @@ -0,0 +1,290 @@ +import { assert, describe, expect, it } from "vitest"; + +import { + ChainIndexingConfigTypeIds, + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshotBackfill, + type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotFollowing, + type ChainIndexingStatusSnapshotQueued, + createIndexingConfig, + OmnichainIndexingStatusIds, +} from "@ensnode/ensnode-sdk"; +import { type BlockRef, ChainIndexingStates } from "@ensnode/ponder-sdk"; + +import { + earlierBlockRef, + earliestBlockRef, + laterBlockRef, + latestBlockRef, +} from "./block-refs.mock"; +import { buildChainIndexingMetadataMock } from "./chain-indexing-metadata.mock"; +import { buildOmnichainIndexingStatusSnapshot } from "./omnichain-indexing-status-snapshot"; + +describe("OmnichainIndexingStatusSnapshot", () => { + describe("buildOmnichainIndexingStatusSnapshot", () => { + it("returns Unstarted status when all chains are Queued", () => { + // arrange + const metadata = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, null), + checkpointBlock: earliestBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadata]]); + + // act + const result = buildOmnichainIndexingStatusSnapshot(chainsMetadata); + + // assert + expect(result).toStrictEqual({ + omnichainStatus: OmnichainIndexingStatusIds.Unstarted, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Queued, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earliestBlockRef, + }, + } satisfies ChainIndexingStatusSnapshotQueued, + ], + ]), + omnichainIndexingCursor: earliestBlockRef.timestamp - 1, + }); + }); + + it("returns Backfill status when at least one chain is Backfill and none are Following", () => { + // arrange + const metadataQueued = buildChainIndexingMetadataMock({ + config: createIndexingConfig(laterBlockRef, null), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const metadataBackfill = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, null), + checkpointBlock: earlierBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: laterBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([ + [1, metadataQueued], + [8453, metadataBackfill], + ]); + + // act + const result = buildOmnichainIndexingStatusSnapshot(chainsMetadata); + + // assert + expect(result).toStrictEqual({ + omnichainStatus: OmnichainIndexingStatusIds.Backfill, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Queued, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: metadataQueued.indexingConfig.startBlock, + }, + } satisfies ChainIndexingStatusSnapshotQueued, + ], + [ + 8453, + { + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: metadataBackfill.indexingConfig.startBlock, + }, + latestIndexedBlock: metadataBackfill.indexingStatus.checkpointBlock, + backfillEndBlock: metadataBackfill.backfillScope.endBlock, + } satisfies ChainIndexingStatusSnapshotBackfill, + ], + ]), + omnichainIndexingCursor: metadataBackfill.indexingStatus.checkpointBlock.timestamp, + }); + }); + + it("returns Following status when at least one chain is Following", () => { + // arrange + + const metadataBackfill = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earlierBlockRef, null), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Historical, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const metadataFollowing = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, null), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Realtime, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: earlierBlockRef, + }); + + const chainsMetadata = new Map([ + [1, metadataBackfill], + [8453, metadataFollowing], + ]); + + // act + const result = buildOmnichainIndexingStatusSnapshot(chainsMetadata); + + // assert + assert( + metadataFollowing.indexingMetrics.state === ChainIndexingStates.Realtime, + "Expected Realtime state for Following chain in test setup", + ); + + expect(result).toStrictEqual({ + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Backfill, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: metadataBackfill.indexingConfig.startBlock, + }, + latestIndexedBlock: metadataBackfill.indexingStatus.checkpointBlock, + backfillEndBlock: metadataBackfill.backfillScope.endBlock, + } satisfies ChainIndexingStatusSnapshotBackfill, + ], + [ + 8453, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: metadataFollowing.indexingConfig.startBlock, + }, + latestIndexedBlock: metadataFollowing.indexingStatus.checkpointBlock, + latestKnownBlock: metadataFollowing.indexingMetrics.latestSyncedBlock, + } satisfies ChainIndexingStatusSnapshotFollowing, + ], + ]), + omnichainIndexingCursor: metadataFollowing.indexingStatus.checkpointBlock.timestamp, + }); + }); + + it("returns Completed status when all chains are Completed", () => { + // arrange + const metadataCompleted = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, latestBlockRef), + checkpointBlock: latestBlockRef, + state: ChainIndexingStates.Completed, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: latestBlockRef, + }); + + const chainsMetadata = new Map([[1, metadataCompleted]]); + + // act + const result = buildOmnichainIndexingStatusSnapshot(chainsMetadata); + + // assert + expect(result).toStrictEqual({ + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Completed, + config: { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earliestBlockRef, + endBlock: latestBlockRef, + }, + latestIndexedBlock: latestBlockRef, + } satisfies ChainIndexingStatusSnapshotCompleted, + ], + ]), + omnichainIndexingCursor: latestBlockRef.timestamp, + }); + }); + + it("correctly calculates omnichainIndexingCursor with mixed status chains", () => { + // arrange + const evenLaterBlockRef: BlockRef = { + timestamp: latestBlockRef.timestamp + 1000, + number: latestBlockRef.number + 1000, + }; + + const metadata1 = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earliestBlockRef, null), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Realtime, + latestSyncedBlock: evenLaterBlockRef, + backfillEndBlock: earlierBlockRef, + }); + + const metadata2 = buildChainIndexingMetadataMock({ + config: createIndexingConfig(earlierBlockRef, null), + checkpointBlock: laterBlockRef, + state: ChainIndexingStates.Realtime, + latestSyncedBlock: latestBlockRef, + backfillEndBlock: laterBlockRef, + }); + + const chainsMetadata = new Map([ + [1, metadata1], + [8453, metadata2], + ]); + + // act + const result = buildOmnichainIndexingStatusSnapshot(chainsMetadata); + + // assert + assert( + metadata1.indexingMetrics.state === ChainIndexingStates.Realtime, + "Expected Realtime state for Following chain in test setup", + ); + assert( + metadata2.indexingMetrics.state === ChainIndexingStates.Realtime, + "Expected Realtime state for Following chain in test setup", + ); + expect(result).toStrictEqual({ + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: new Map([ + [ + 1, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: metadata1.indexingConfig.startBlock, + }, + latestIndexedBlock: metadata1.indexingStatus.checkpointBlock, + latestKnownBlock: metadata1.indexingMetrics.latestSyncedBlock, + } satisfies ChainIndexingStatusSnapshotFollowing, + ], + [ + 8453, + { + chainStatus: ChainIndexingStatusIds.Following, + config: { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: metadata2.indexingConfig.startBlock, + }, + latestIndexedBlock: metadata2.indexingStatus.checkpointBlock, + latestKnownBlock: metadata2.indexingMetrics.latestSyncedBlock, + } satisfies ChainIndexingStatusSnapshotFollowing, + ], + ]), + omnichainIndexingCursor: laterBlockRef.timestamp, + }); + }); + }); +}); diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts index 2bbe11016..580d4b2c6 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts @@ -1,6 +1,6 @@ import { - type ChainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotQueued, getOmnichainIndexingCursor, getOmnichainIndexingStatus, @@ -39,7 +39,7 @@ export function buildOmnichainIndexingStatusSnapshot( switch (omnichainStatus) { case OmnichainIndexingStatusIds.Unstarted: { return validateOmnichainIndexingStatusSnapshot({ - omnichainStatus, + omnichainStatus: OmnichainIndexingStatusIds.Unstarted, chains: chainStatusSnapshots as Map< ChainId, Unvalidated @@ -53,7 +53,7 @@ export function buildOmnichainIndexingStatusSnapshot( omnichainStatus: OmnichainIndexingStatusIds.Backfill, chains: chainStatusSnapshots as Map< ChainId, - Unvalidated + Unvalidated >, // narrowing the type here, will be validated in the following 'check' step omnichainIndexingCursor, } satisfies Unvalidated); From e9db330755b6a17cf745682cbe8d20dc70a60bb7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Feb 2026 14:51:17 +0100 Subject: [PATCH 8/8] Add invariant to buildOmnichainIndexingStatusSnapshot function` --- .../omnichain-indexing-status-snapshot.test.ts | 10 ++++++++++ .../omnichain-indexing-status-snapshot.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts index c7fd6fbd3..42889db9c 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.test.ts @@ -286,5 +286,15 @@ describe("OmnichainIndexingStatusSnapshot", () => { omnichainIndexingCursor: laterBlockRef.timestamp, }); }); + + it("throws an error when no chains metadata is provided", () => { + // arrange + const chainsMetadata = new Map(); + + // act & assert + expect(() => buildOmnichainIndexingStatusSnapshot(chainsMetadata)).toThrowError( + /At least one chain's indexing metadata is required to build an OmnichainIndexingStatusSnapshot/, + ); + }); }); }); diff --git a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts index 580d4b2c6..81712062a 100644 --- a/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts +++ b/apps/ensindexer/src/lib/indexing-status-builder/omnichain-indexing-status-snapshot.ts @@ -26,10 +26,18 @@ import { buildChainStatusSnapshots } from "./chain-indexing-status-snapshot"; * needed to determine that chain's indexing state. * * @returns The validated omnichain indexing status snapshot. + * @throws Error if the provided metadata is insufficient to determine the status + * snapshot for any chain, or if the resulting snapshot is invalid. */ export function buildOmnichainIndexingStatusSnapshot( chainsIndexingMetadata: Map, ): OmnichainIndexingStatusSnapshot { + if (chainsIndexingMetadata.size === 0) { + throw new Error( + "At least one chain's indexing metadata is required to build an OmnichainIndexingStatusSnapshot", + ); + } + const chainStatusSnapshots = buildChainStatusSnapshots(chainsIndexingMetadata); const chains = Array.from(chainStatusSnapshots.values());