diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts index 8e65b716a..6f731860e 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts @@ -13,10 +13,15 @@ import { type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, } from "./chain-indexing-status-snapshot"; -import { deserializeOmnichainIndexingStatusSnapshot } from "./deserialize"; -import { serializeOmnichainIndexingStatusSnapshot } from "./serialize"; -import type { SerializedOmnichainIndexingStatusSnapshot } from "./serialized-types"; -import { OmnichainIndexingStatusIds, type OmnichainIndexingStatusSnapshot } from "./types"; +import { deserializeOmnichainIndexingStatusSnapshot } from "./deserialize/omnichain-indexing-status-snapshot"; +import { + OmnichainIndexingStatusIds, + type OmnichainIndexingStatusSnapshot, +} from "./omnichain-indexing-status-snapshot"; +import { + type SerializedOmnichainIndexingStatusSnapshot, + serializeOmnichainIndexingStatusSnapshot, +} from "./serialize/omnichain-indexing-status-snapshot"; describe("ENSIndexer: Indexing Status", () => { describe("Omnichain Indexing Status Snapshot", () => { diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts index 5864fecd5..ea49239ff 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts @@ -2,39 +2,14 @@ import { prettifyError } from "zod/v4"; import type { SerializedCrossChainIndexingStatusSnapshot, - SerializedOmnichainIndexingStatusSnapshot, SerializedRealtimeIndexingStatusProjection, } from "./serialized-types"; -import type { - CrossChainIndexingStatusSnapshot, - OmnichainIndexingStatusSnapshot, - RealtimeIndexingStatusProjection, -} from "./types"; +import type { CrossChainIndexingStatusSnapshot, RealtimeIndexingStatusProjection } from "./types"; import { makeCrossChainIndexingStatusSnapshotSchema, - makeOmnichainIndexingStatusSnapshotSchema, makeRealtimeIndexingStatusProjectionSchema, } from "./zod-schemas"; -/** - * Deserialize an {@link OmnichainIndexingStatusSnapshot} object. - */ -export function deserializeOmnichainIndexingStatusSnapshot( - maybeSnapshot: SerializedOmnichainIndexingStatusSnapshot, - valueLabel?: string, -): OmnichainIndexingStatusSnapshot { - const schema = makeOmnichainIndexingStatusSnapshotSchema(valueLabel); - const parsed = schema.safeParse(maybeSnapshot); - - if (parsed.error) { - throw new Error( - `Cannot deserialize into OmnichainIndexingStatusSnapshot:\n${prettifyError(parsed.error)}\n`, - ); - } - - return parsed.data; -} - /** * Deserialize an {@link CrossChainIndexingStatusSnapshot} object. */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/omnichain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..d29601add --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/omnichain-indexing-status-snapshot.ts @@ -0,0 +1,24 @@ +import { prettifyError } from "zod/v4"; + +import type { OmnichainIndexingStatusSnapshot } from "../omnichain-indexing-status-snapshot"; +import type { SerializedOmnichainIndexingStatusSnapshot } from "../serialize/omnichain-indexing-status-snapshot"; +import { makeOmnichainIndexingStatusSnapshotSchema } from "../zod-schema/omnichain-indexing-status-snapshot"; + +/** + * Deserialize an {@link OmnichainIndexingStatusSnapshot} object. + */ +export function deserializeOmnichainIndexingStatusSnapshot( + maybeSnapshot: SerializedOmnichainIndexingStatusSnapshot, + valueLabel?: string, +): OmnichainIndexingStatusSnapshot { + const schema = makeOmnichainIndexingStatusSnapshotSchema(valueLabel); + const parsed = schema.safeParse(maybeSnapshot); + + if (parsed.error) { + throw new Error( + `Cannot deserialize into OmnichainIndexingStatusSnapshot:\n${prettifyError(parsed.error)}\n`, + ); + } + + return parsed.data; +} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts index 7df076fed..911676896 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts @@ -1,159 +1,6 @@ -import type { BlockRef, ChainId, UnixTimestamp } from "../../shared/types"; -import { - ChainIndexingStatusIds, - type ChainIndexingStatusSnapshot, - type ChainIndexingStatusSnapshotCompleted, - type ChainIndexingStatusSnapshotQueued, -} from "./chain-indexing-status-snapshot"; -import { - type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, - type CrossChainIndexingStatusSnapshot, - type OmnichainIndexingStatusId, - OmnichainIndexingStatusIds, -} from "./types"; - -/** - * Get {@link OmnichainIndexingStatusId} based on indexed chains' statuses. - * - * This function decides what is the `OmnichainIndexingStatusId` is, - * based on provided chain indexing statuses. - * - * @throws an error if unable to determine overall indexing status - */ -export function getOmnichainIndexingStatus( - chains: ChainIndexingStatusSnapshot[], -): OmnichainIndexingStatusId { - if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing(chains)) { - return OmnichainIndexingStatusIds.Following; - } - - if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill(chains)) { - return OmnichainIndexingStatusIds.Backfill; - } - - if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted(chains)) { - return OmnichainIndexingStatusIds.Unstarted; - } - - if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted(chains)) { - return OmnichainIndexingStatusIds.Completed; - } - - // if none of the chain statuses matched, throw an error - throw new Error(`Unable to determine omnichain indexing status for provided chains.`); -} - -/** - * Get Omnichain Indexing Cursor - * - * The cursor tracks the "highest" latest indexed block timestamp across - * all indexed chains. If all chains are queued, the cursor tracks the moment - * just before the earliest start block timestamp across those chains. - * - * @throws an error if no chains are provided - */ -export function getOmnichainIndexingCursor(chains: ChainIndexingStatusSnapshot[]): UnixTimestamp { - if (chains.length === 0) { - throw new Error(`Unable to determine omnichain indexing cursor when no chains were provided.`); - } - - // for omnichain indexing status snapshot 'unstarted', the cursor tracks - // the moment just before the indexing would start from. - if (getOmnichainIndexingStatus(chains) === OmnichainIndexingStatusIds.Unstarted) { - const earliestStartBlockTimestamps = chains.map((chain) => chain.config.startBlock.timestamp); - - return Math.min(...earliestStartBlockTimestamps) - 1; - } - - // otherwise, the cursor tracks the "highest" latest indexed block timestamp - // across all indexed chains - const latestIndexedBlockTimestamps = chains - .filter((chain) => chain.chainStatus !== ChainIndexingStatusIds.Queued) - .map((chain) => chain.latestIndexedBlock.timestamp); - - // Invariant: there's at least one element in `latestIndexedBlockTimestamps` array - // This is theoretically impossible based on the 2 checks above, - // but the invariant is explicitly added here as a formality. - if (latestIndexedBlockTimestamps.length < 1) { - throw new Error("latestIndexedBlockTimestamps array must include at least one element"); - } - - return Math.max(...latestIndexedBlockTimestamps); -} - -/** - * Check if Chain Indexing Status Snapshots fit the 'unstarted' overall status - * snapshot requirements: - * - All chains are guaranteed to have a status of "queued". - * - * Note: This function narrows the {@link ChainIndexingStatusSnapshot} type to - * {@link ChainIndexingStatusSnapshotQueued}. - */ -export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted( - chains: ChainIndexingStatusSnapshot[], -): chains is ChainIndexingStatusSnapshotQueued[] { - return chains.every((chain) => chain.chainStatus === ChainIndexingStatusIds.Queued); -} - -/** - * Check if Chain Indexing Status Snapshots fit the 'backfill' overall status - * snapshot requirements: - * - At least one chain is guaranteed to be in the "backfill" status. - * - Each chain is guaranteed to have a status of either "queued", - * "backfill" or "completed". - * - * Note: This function narrows the {@link ChainIndexingStatusSnapshot} type to - * {@link ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill}. - */ -export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill( - chains: ChainIndexingStatusSnapshot[], -): chains is ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill[] { - const atLeastOneChainInTargetStatus = chains.some( - (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill, - ); - const otherChainsHaveValidStatuses = chains.every( - (chain) => - chain.chainStatus === ChainIndexingStatusIds.Queued || - chain.chainStatus === ChainIndexingStatusIds.Backfill || - chain.chainStatus === ChainIndexingStatusIds.Completed, - ); - - return atLeastOneChainInTargetStatus && otherChainsHaveValidStatuses; -} - -/** - * Checks if Chain Indexing Status Snapshots fit the 'completed' overall status - * snapshot requirements: - * - All chains are guaranteed to have a status of "completed". - * - * Note: This function narrows the {@link ChainIndexingStatusSnapshot} type to - * {@link ChainIndexingStatusSnapshotCompleted}. - */ -export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted( - chains: ChainIndexingStatusSnapshot[], -): chains is ChainIndexingStatusSnapshotCompleted[] { - const allChainsHaveValidStatuses = chains.every( - (chain) => chain.chainStatus === ChainIndexingStatusIds.Completed, - ); - - return allChainsHaveValidStatuses; -} - -/** - * Checks Chain Indexing Status Snapshots fit the 'following' overall status - * snapshot requirements: - * - At least one chain is guaranteed to be in the "following" status. - * - Any other chain can have any status. - */ -export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing( - chains: ChainIndexingStatusSnapshot[], -): chains is ChainIndexingStatusSnapshot[] { - const allChainsHaveValidStatuses = chains.some( - (chain) => chain.chainStatus === ChainIndexingStatusIds.Following, - ); - - return allChainsHaveValidStatuses; -} +import type { BlockRef, ChainId } from "../../shared/types"; +import { ChainIndexingStatusIds } from "./chain-indexing-status-snapshot"; +import type { CrossChainIndexingStatusSnapshot } from "./types"; /** * Gets the latest indexed {@link BlockRef} for the given {@link ChainId}. diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts index dc02d1811..d9ba4f01a 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts @@ -1,10 +1,13 @@ export * from "./chain-indexing-status-snapshot"; export * from "./deserialize"; export * from "./deserialize/chain-indexing-status-snapshot"; +export * from "./deserialize/omnichain-indexing-status-snapshot"; export * from "./helpers"; +export * from "./omnichain-indexing-status-snapshot"; export * from "./projection"; export * from "./serialize"; export * from "./serialize/chain-indexing-status-snapshot"; +export * from "./serialize/omnichain-indexing-status-snapshot"; export * from "./serialized-types"; export * from "./types"; export * from "./validate/chain-indexing-status-snapshot"; diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/omnichain-indexing-status-snapshot.test.ts similarity index 98% rename from packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts rename to packages/ensnode-sdk/src/ensindexer/indexing-status/omnichain-indexing-status-snapshot.test.ts index f0d0e9c63..4086e33ec 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/omnichain-indexing-status-snapshot.test.ts @@ -16,8 +16,11 @@ import { type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, } from "./chain-indexing-status-snapshot"; -import { getOmnichainIndexingCursor, getOmnichainIndexingStatus } from "./helpers"; -import { OmnichainIndexingStatusIds } from "./types"; +import { + getOmnichainIndexingCursor, + getOmnichainIndexingStatus, + OmnichainIndexingStatusIds, +} from "./omnichain-indexing-status-snapshot"; describe("ENSIndexer: Indexing Snapshot helpers", () => { describe("getOmnichainIndexingStatus", () => { diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/omnichain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..a5a1c308f --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/omnichain-indexing-status-snapshot.ts @@ -0,0 +1,341 @@ +import type { ChainId, UnixTimestamp } from "../../shared/types"; +import { + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshot, + type ChainIndexingStatusSnapshotBackfill, + type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotQueued, +} from "./chain-indexing-status-snapshot"; + +/** + * The status of omnichain indexing at the time an omnichain indexing status + * snapshot is captured. + */ +export const OmnichainIndexingStatusIds = { + /** + * Represents that omnichain indexing is not ready to begin yet because + * ENSIndexer is in its initialization phase and the data to build a "true" + * {@link OmnichainIndexingStatusSnapshot} is still being loaded. + */ + Unstarted: "omnichain-unstarted", + + /** + * Represents that omnichain indexing is in an overall "backfill" status because + * - At least one indexed chain has a `chainStatus` of + * {@link ChainIndexingStatusIds.Backfill}; and + * - No indexed chain has a `chainStatus` of {@link ChainIndexingStatusIds.Following}. + */ + Backfill: "omnichain-backfill", + + /** + * Represents that omnichain indexing is in an overall "following" status because + * at least one indexed chain has a `chainStatus` of + * {@link ChainIndexingStatusIds.Following}. + */ + Following: "omnichain-following", + + /** + * Represents that omnichain indexing has completed because all indexed chains have + * a `chainStatus` of {@link ChainIndexingStatusIds.Completed}. + */ + Completed: "omnichain-completed", +} as const; + +/** + * The derived string union of possible {@link OmnichainIndexingStatusIds}. + */ +export type OmnichainIndexingStatusId = + (typeof OmnichainIndexingStatusIds)[keyof typeof OmnichainIndexingStatusIds]; + +/** + * Omnichain indexing status snapshot when the overall `omnichainStatus` is + * {@link OmnichainIndexingStatusIds.Unstarted}. + * + * Invariants: + * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Unstarted}. + * - `chains` is always a map to {@link ChainIndexingStatusSnapshotQueued} values exclusively. + * - `omnichainIndexingCursor` is always < the `config.startBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Queued}. + */ +export interface OmnichainIndexingStatusSnapshotUnstarted { + /** + * The status of omnichain indexing. + */ + omnichainStatus: typeof OmnichainIndexingStatusIds.Unstarted; + + /** + * The indexing status snapshot for each indexed chain. + */ + chains: Map; + + /** + * The timestamp of omnichain indexing progress across all indexed chains. + */ + omnichainIndexingCursor: UnixTimestamp; +} + +/** + * The range of {@link ChainIndexingSnapshot} types allowed when the + * overall omnichain indexing status is {@link OmnichainIndexingStatusIds.Backfill}. + * + * Note that this is all of the {@link ChainIndexingSnapshot} types with the exception + * of {@link ChainIndexingStatusSnapshotFollowing}. + */ +export type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill = + | ChainIndexingStatusSnapshotQueued + | ChainIndexingStatusSnapshotBackfill + | ChainIndexingStatusSnapshotCompleted; + +/** + * Omnichain indexing status snapshot when the `omnichainStatus` is + * {@link OmnichainIndexingStatusIds.Backfill}. + * + * Invariants: + * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Backfill}. + * - `chains` is guaranteed to contain at least one chain with a `chainStatus` of + * {@link ChainIndexingStatusIds.Backfill}. + * - `chains` is guaranteed to not to contain any chain with a `chainStatus` of + * {@link ChainIndexingStatusIds.Following} + * - `omnichainIndexingCursor` is always < the `config.startBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Queued}. + * - `omnichainIndexingCursor` is always <= the `backfillEndBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Backfill}. + * - `omnichainIndexingCursor` is always >= the `latestIndexedBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Completed}. + * - `omnichainIndexingCursor` is always equal to the timestamp of the highest + * `latestIndexedBlock` across all chains that have started indexing + * (`chainStatus` is not {@link ChainIndexingStatusIds.Queued}). + */ +export interface OmnichainIndexingStatusSnapshotBackfill { + /** + * The status of omnichain indexing. + */ + omnichainStatus: typeof OmnichainIndexingStatusIds.Backfill; + + /** + * The indexing status snapshot for each indexed chain. + */ + chains: Map; + + /** + * The timestamp of omnichain indexing progress across all indexed chains. + */ + omnichainIndexingCursor: UnixTimestamp; +} + +/** + * Omnichain indexing status snapshot when the overall `omnichainStatus` is + * {@link OmnichainIndexingStatusIds.Following}. + * + * Invariants: + * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Following}. + * - `chains` is guaranteed to contain at least one chain with a `status` of + * {@link ChainIndexingStatusIds.Following}. + * - `omnichainIndexingCursor` is always < the `config.startBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Queued}. + * - `omnichainIndexingCursor` is always <= the `backfillEndBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Backfill}. + * - `omnichainIndexingCursor` is always >= the `latestIndexedBlock.timestamp` for all + * chains with `chainStatus` of {@link ChainIndexingStatusIds.Completed}. + * - `omnichainIndexingCursor` is always equal to the timestamp of the highest + * `latestIndexedBlock` across all chains that have started indexing + * (`chainStatus` is not {@link ChainIndexingStatusIds.Queued}). + */ +export interface OmnichainIndexingStatusSnapshotFollowing { + /** + * The status of omnichain indexing. + */ + omnichainStatus: typeof OmnichainIndexingStatusIds.Following; + + /** + * The indexing status snapshot for each indexed chain. + */ + chains: Map; + + /** + * The timestamp of omnichain indexing progress across all indexed chains. + */ + omnichainIndexingCursor: UnixTimestamp; +} + +/** + * Omnichain indexing status snapshot when the overall `omnichainStatus` is + * {@link OmnichainIndexingStatusIds.Completed}. + * + * Invariants: + * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Completed}. + * - `chains` is always a map to {@link ChainIndexingStatusSnapshotCompleted} values exclusively. + * - `omnichainIndexingCursor` is always equal to the highest + * `latestIndexedBlock.timestamp` for all chains. + */ +export interface OmnichainIndexingStatusSnapshotCompleted { + /** + * The status of omnichain indexing. + */ + omnichainStatus: typeof OmnichainIndexingStatusIds.Completed; + + /** + * The indexing status snapshot for each indexed chain. + */ + chains: Map; + + /** + * The timestamp of omnichain indexing progress across all indexed chains. + */ + omnichainIndexingCursor: UnixTimestamp; +} + +/** + * Omnichain indexing status snapshot for one or more chains. + * + * Use the `omnichainStatus` field to determine the specific type interpretation + * at runtime. + */ +export type OmnichainIndexingStatusSnapshot = + | OmnichainIndexingStatusSnapshotUnstarted + | OmnichainIndexingStatusSnapshotBackfill + | OmnichainIndexingStatusSnapshotCompleted + | OmnichainIndexingStatusSnapshotFollowing; + +/** + * Check if Chain Indexing Status Snapshots fit the 'unstarted' overall status + * snapshot requirements: + * - All chains are guaranteed to have a status of "queued". + * + * Note: This function narrows the {@link ChainIndexingStatusSnapshot} type to + * {@link ChainIndexingStatusSnapshotQueued}. + */ +export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted( + chains: ChainIndexingStatusSnapshot[], +): chains is ChainIndexingStatusSnapshotQueued[] { + return chains.every((chain) => chain.chainStatus === ChainIndexingStatusIds.Queued); +} + +/** + * Check if Chain Indexing Status Snapshots fit the 'backfill' overall status + * snapshot requirements: + * - At least one chain is guaranteed to be in the "backfill" status. + * - Each chain is guaranteed to have a status of either "queued", + * "backfill" or "completed". + * + * Note: This function narrows the {@link ChainIndexingStatusSnapshot} type to + * {@link ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill}. + */ +export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill( + chains: ChainIndexingStatusSnapshot[], +): chains is ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill[] { + const atLeastOneChainInTargetStatus = chains.some( + (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill, + ); + const otherChainsHaveValidStatuses = chains.every( + (chain) => + chain.chainStatus === ChainIndexingStatusIds.Queued || + chain.chainStatus === ChainIndexingStatusIds.Backfill || + chain.chainStatus === ChainIndexingStatusIds.Completed, + ); + + return atLeastOneChainInTargetStatus && otherChainsHaveValidStatuses; +} + +/** + * Checks if Chain Indexing Status Snapshots fit the 'completed' overall status + * snapshot requirements: + * - All chains are guaranteed to have a status of "completed". + * + * Note: This function narrows the {@link ChainIndexingStatusSnapshot} type to + * {@link ChainIndexingStatusSnapshotCompleted}. + */ +export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted( + chains: ChainIndexingStatusSnapshot[], +): chains is ChainIndexingStatusSnapshotCompleted[] { + const allChainsHaveValidStatuses = chains.every( + (chain) => chain.chainStatus === ChainIndexingStatusIds.Completed, + ); + + return allChainsHaveValidStatuses; +} + +/** + * Checks Chain Indexing Status Snapshots fit the 'following' overall status + * snapshot requirements: + * - At least one chain is guaranteed to be in the "following" status. + * - Any other chain can have any status. + */ +export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing( + chains: ChainIndexingStatusSnapshot[], +): boolean { + const allChainsHaveValidStatuses = chains.some( + (chain) => chain.chainStatus === ChainIndexingStatusIds.Following, + ); + + return allChainsHaveValidStatuses; +} + +/** + * Get {@link OmnichainIndexingStatusId} based on indexed chains' statuses. + * + * This function decides what is the `OmnichainIndexingStatusId` is, + * based on provided chain indexing statuses. + * + * @throws an error if unable to determine overall indexing status + */ +export function getOmnichainIndexingStatus( + chains: ChainIndexingStatusSnapshot[], +): OmnichainIndexingStatusId { + if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing(chains)) { + return OmnichainIndexingStatusIds.Following; + } + + if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill(chains)) { + return OmnichainIndexingStatusIds.Backfill; + } + + if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted(chains)) { + return OmnichainIndexingStatusIds.Unstarted; + } + + if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted(chains)) { + return OmnichainIndexingStatusIds.Completed; + } + + // if none of the chain statuses matched, throw an error + throw new Error(`Unable to determine omnichain indexing status for provided chains.`); +} + +/** + * Get Omnichain Indexing Cursor + * + * The cursor tracks the "highest" latest indexed block timestamp across + * all indexed chains. If all chains are queued, the cursor tracks the moment + * just before the earliest start block timestamp across those chains. + * + * @throws an error if no chains are provided + */ +export function getOmnichainIndexingCursor(chains: ChainIndexingStatusSnapshot[]): UnixTimestamp { + if (chains.length === 0) { + throw new Error(`Unable to determine omnichain indexing cursor when no chains were provided.`); + } + + // for omnichain indexing status snapshot 'unstarted', the cursor tracks + // the moment just before the indexing would start from. + if (getOmnichainIndexingStatus(chains) === OmnichainIndexingStatusIds.Unstarted) { + const earliestStartBlockTimestamps = chains.map((chain) => chain.config.startBlock.timestamp); + + return Math.min(...earliestStartBlockTimestamps) - 1; + } + + // otherwise, the cursor tracks the "highest" latest indexed block timestamp + // across all indexed chains + const latestIndexedBlockTimestamps = chains + .filter((chain) => chain.chainStatus !== ChainIndexingStatusIds.Queued) + .map((chain) => chain.latestIndexedBlock.timestamp); + + // Invariant: there's at least one element in `latestIndexedBlockTimestamps` array + // This is theoretically impossible based on the 2 checks above, + // but the invariant is explicitly added here as a formality. + if (latestIndexedBlockTimestamps.length < 1) { + throw new Error("latestIndexedBlockTimestamps array must include at least one element"); + } + + return Math.max(...latestIndexedBlockTimestamps); +} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts index ace15062f..e8b2c33fd 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts @@ -6,12 +6,9 @@ import { ChainIndexingStatusIds, } from "./chain-indexing-status-snapshot"; import { deserializeCrossChainIndexingStatusSnapshot } from "./deserialize"; +import { OmnichainIndexingStatusIds } from "./omnichain-indexing-status-snapshot"; import { createRealtimeIndexingStatusProjection } from "./projection"; -import { - CrossChainIndexingStrategyIds, - OmnichainIndexingStatusIds, - type RealtimeIndexingStatusProjection, -} from "./types"; +import { CrossChainIndexingStrategyIds, type RealtimeIndexingStatusProjection } from "./types"; describe("Realtime Indexing Status Projection", () => { it("can be created from existing omnichain snapshot", () => { diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts index e797d88c3..fd7b82448 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts @@ -1,19 +1,9 @@ -import { serializeChainIndexingSnapshots } from "./serialize/chain-indexing-status-snapshot"; +import { serializeOmnichainIndexingStatusSnapshot } from "./serialize/omnichain-indexing-status-snapshot"; import type { SerializedCrossChainIndexingStatusSnapshot, - SerializedOmnichainIndexingStatusSnapshot, - SerializedOmnichainIndexingStatusSnapshotBackfill, - SerializedOmnichainIndexingStatusSnapshotCompleted, - SerializedOmnichainIndexingStatusSnapshotFollowing, - SerializedOmnichainIndexingStatusSnapshotUnstarted, SerializedRealtimeIndexingStatusProjection, } from "./serialized-types"; -import { - type CrossChainIndexingStatusSnapshot, - OmnichainIndexingStatusIds, - type OmnichainIndexingStatusSnapshot, - type RealtimeIndexingStatusProjection, -} from "./types"; +import type { CrossChainIndexingStatusSnapshot, RealtimeIndexingStatusProjection } from "./types"; export function serializeCrossChainIndexingStatusSnapshotOmnichain({ strategy, @@ -38,41 +28,3 @@ export function serializeRealtimeIndexingStatusProjection( snapshot: serializeCrossChainIndexingStatusSnapshotOmnichain(indexingProjection.snapshot), } satisfies SerializedRealtimeIndexingStatusProjection; } - -/** - * Serialize a {@link OmnichainIndexingStatusSnapshot} object. - */ -export function serializeOmnichainIndexingStatusSnapshot( - indexingStatus: OmnichainIndexingStatusSnapshot, -): SerializedOmnichainIndexingStatusSnapshot { - switch (indexingStatus.omnichainStatus) { - case OmnichainIndexingStatusIds.Unstarted: - return { - omnichainStatus: OmnichainIndexingStatusIds.Unstarted, - chains: serializeChainIndexingSnapshots(indexingStatus.chains), - omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotUnstarted; - - case OmnichainIndexingStatusIds.Backfill: - return { - omnichainStatus: OmnichainIndexingStatusIds.Backfill, - chains: serializeChainIndexingSnapshots(indexingStatus.chains), - omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotBackfill; - - case OmnichainIndexingStatusIds.Completed: { - return { - omnichainStatus: OmnichainIndexingStatusIds.Completed, - chains: serializeChainIndexingSnapshots(indexingStatus.chains), - omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotCompleted; - } - - case OmnichainIndexingStatusIds.Following: - return { - omnichainStatus: OmnichainIndexingStatusIds.Following, - chains: serializeChainIndexingSnapshots(indexingStatus.chains), - omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotFollowing; - } -} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/omnichain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..e3668eef2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/omnichain-indexing-status-snapshot.ts @@ -0,0 +1,98 @@ +import type { ChainIdString } from "../../../shared/serialized-types"; +import type { + ChainIndexingStatusSnapshot, + ChainIndexingStatusSnapshotCompleted, + ChainIndexingStatusSnapshotQueued, +} from "../chain-indexing-status-snapshot"; +import { + type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, + OmnichainIndexingStatusIds, + type OmnichainIndexingStatusSnapshot, + type OmnichainIndexingStatusSnapshotBackfill, + type OmnichainIndexingStatusSnapshotCompleted, + type OmnichainIndexingStatusSnapshotFollowing, + type OmnichainIndexingStatusSnapshotUnstarted, +} from "../omnichain-indexing-status-snapshot"; +import { serializeChainIndexingSnapshots } from "./chain-indexing-status-snapshot"; + +/** + * Serialized representation of {@link OmnichainIndexingStatusSnapshotUnstarted} + */ +export interface SerializedOmnichainIndexingStatusSnapshotUnstarted + extends Omit { + chains: Record; +} + +/** + * Serialized representation of {@link OmnichainIndexingStatusSnapshotBackfill} + */ +export interface SerializedOmnichainIndexingStatusSnapshotBackfill + extends Omit { + chains: Record< + ChainIdString, + ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill + >; +} + +/** + * Serialized representation of {@link OmnichainIndexingStatusSnapshotCompleted} + */ +export interface SerializedOmnichainIndexingStatusSnapshotCompleted + extends Omit { + chains: Record; +} + +/** + * Serialized representation of {@link OmnichainIndexingStatusSnapshotFollowing} + */ +export interface SerializedOmnichainIndexingStatusSnapshotFollowing + extends Omit { + chains: Record; +} + +/** + * Serialized representation of {@link OmnichainIndexingStatusSnapshot} + */ +export type SerializedOmnichainIndexingStatusSnapshot = + | SerializedOmnichainIndexingStatusSnapshotUnstarted + | SerializedOmnichainIndexingStatusSnapshotBackfill + | SerializedOmnichainIndexingStatusSnapshotCompleted + | SerializedOmnichainIndexingStatusSnapshotFollowing; + +/** + * Serialize a {@link OmnichainIndexingStatusSnapshot} object. + */ +export function serializeOmnichainIndexingStatusSnapshot( + indexingStatus: OmnichainIndexingStatusSnapshot, +): SerializedOmnichainIndexingStatusSnapshot { + switch (indexingStatus.omnichainStatus) { + case OmnichainIndexingStatusIds.Unstarted: + return { + omnichainStatus: OmnichainIndexingStatusIds.Unstarted, + chains: serializeChainIndexingSnapshots(indexingStatus.chains), + omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotUnstarted; + + case OmnichainIndexingStatusIds.Backfill: + return { + omnichainStatus: OmnichainIndexingStatusIds.Backfill, + chains: serializeChainIndexingSnapshots(indexingStatus.chains), + omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotBackfill; + + case OmnichainIndexingStatusIds.Completed: { + return { + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains: serializeChainIndexingSnapshots(indexingStatus.chains), + omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotCompleted; + } + + case OmnichainIndexingStatusIds.Following: + return { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: serializeChainIndexingSnapshots(indexingStatus.chains), + omnichainIndexingCursor: indexingStatus.omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotFollowing; + } +} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts index 9ad205592..e866d06df 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts @@ -1,65 +1,10 @@ -import type { ChainIdString } from "../../shared/serialized-types"; +import type { SerializedOmnichainIndexingStatusSnapshot } from "./serialize/omnichain-indexing-status-snapshot"; import type { - ChainIndexingStatusSnapshot, - ChainIndexingStatusSnapshotCompleted, - ChainIndexingStatusSnapshotQueued, -} from "./chain-indexing-status-snapshot"; -import type { - ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, CrossChainIndexingStatusSnapshot, CrossChainIndexingStatusSnapshotOmnichain, - OmnichainIndexingStatusSnapshot, - OmnichainIndexingStatusSnapshotBackfill, - OmnichainIndexingStatusSnapshotCompleted, - OmnichainIndexingStatusSnapshotFollowing, - OmnichainIndexingStatusSnapshotUnstarted, RealtimeIndexingStatusProjection, } from "./types"; -/** - * Serialized representation of {@link OmnichainIndexingStatusSnapshotUnstarted} - */ -export interface SerializedOmnichainIndexingStatusSnapshotUnstarted - extends Omit { - chains: Record; -} - -/** - * Serialized representation of {@link OmnichainIndexingStatusSnapshotBackfill} - */ -export interface SerializedOmnichainIndexingStatusSnapshotBackfill - extends Omit { - chains: Record< - ChainIdString, - ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill - >; -} - -/** - * Serialized representation of {@link OmnichainIndexingStatusSnapshotCompleted} - */ -export interface SerializedOmnichainIndexingStatusSnapshotCompleted - extends Omit { - chains: Record; -} - -/** - * Serialized representation of {@link OmnichainIndexingStatusSnapshotFollowing} - */ -export interface SerializedOmnichainIndexingStatusSnapshotFollowing - extends Omit { - chains: Record; -} - -/** - * Serialized representation of {@link OmnichainIndexingStatusSnapshot} - */ -export type SerializedOmnichainIndexingStatusSnapshot = - | SerializedOmnichainIndexingStatusSnapshotUnstarted - | SerializedOmnichainIndexingStatusSnapshotBackfill - | SerializedOmnichainIndexingStatusSnapshotCompleted - | SerializedOmnichainIndexingStatusSnapshotFollowing; - /** * Serialized representation of {@link CrossChainIndexingStatusSnapshotOmnichain} */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts index c22be0e05..8165b0732 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts @@ -1,202 +1,9 @@ -import type { ChainId, Duration, UnixTimestamp } from "../../shared/types"; +import type { Duration, UnixTimestamp } from "../../shared/types"; import type { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, - ChainIndexingStatusSnapshot, - ChainIndexingStatusSnapshotBackfill, - ChainIndexingStatusSnapshotCompleted, - ChainIndexingStatusSnapshotQueued, } from "./chain-indexing-status-snapshot"; - -/** - * The status of omnichain indexing at the time an omnichain indexing status - * snapshot is captured. - */ -export const OmnichainIndexingStatusIds = { - /** - * Represents that omnichain indexing is not ready to begin yet because - * ENSIndexer is in its initialization phase and the data to build a "true" - * {@link OmnichainIndexingStatusSnapshot} is still being loaded. - */ - Unstarted: "omnichain-unstarted", - - /** - * Represents that omnichain indexing is in an overall "backfill" status because - * - At least one indexed chain has a `chainStatus` of - * {@link ChainIndexingStatusIds.Backfill}; and - * - No indexed chain has a `chainStatus` of {@link ChainIndexingStatusIds.Following}. - */ - Backfill: "omnichain-backfill", - - /** - * Represents that omnichain indexing is in an overall "following" status because - * at least one indexed chain has a `chainStatus` of - * {@link ChainIndexingStatusIds.Following}. - */ - Following: "omnichain-following", - - /** - * Represents that omnichain indexing has completed because all indexed chains have - * a `chainStatus` of {@link ChainIndexingStatusIds.Completed}. - */ - Completed: "omnichain-completed", -} as const; - -/** - * The derived string union of possible {@link OmnichainIndexingStatusIds}. - */ -export type OmnichainIndexingStatusId = - (typeof OmnichainIndexingStatusIds)[keyof typeof OmnichainIndexingStatusIds]; - -/** - * Omnichain indexing status snapshot when the overall `omnichainStatus` is - * {@link OmnichainIndexingStatusIds.Unstarted}. - * - * Invariants: - * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Unstarted}. - * - `chains` is always a map to {@link ChainIndexingStatusSnapshotQueued} values exclusively. - * - `omnichainIndexingCursor` is always < the `config.startBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Queued}. - */ -export interface OmnichainIndexingStatusSnapshotUnstarted { - /** - * The status of omnichain indexing. - */ - omnichainStatus: typeof OmnichainIndexingStatusIds.Unstarted; - - /** - * The indexing status snapshot for each indexed chain. - */ - chains: Map; - - /** - * The timestamp of omnichain indexing progress across all indexed chains. - */ - omnichainIndexingCursor: UnixTimestamp; -} - -/** - * The range of {@link ChainIndexingSnapshot} types allowed when the - * overall omnichain indexing status is {@link OmnichainIndexingStatusIds.Backfill}. - * - * Note that this is all of the {@link ChainIndexingSnapshot} types with the exception - * of {@link ChainIndexingStatusSnapshotFollowing}. - */ -export type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill = - | ChainIndexingStatusSnapshotQueued - | ChainIndexingStatusSnapshotBackfill - | ChainIndexingStatusSnapshotCompleted; - -/** - * Omnichain indexing status snapshot when the `omnichainStatus` is - * {@link OmnichainIndexingStatusIds.Backfill}. - * - * Invariants: - * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Backfill}. - * - `chains` is guaranteed to contain at least one chain with a `chainStatus` of - * {@link ChainIndexingStatusIds.Backfill}. - * - `chains` is guaranteed to not to contain any chain with a `chainStatus` of - * {@link ChainIndexingStatusIds.Following} - * - `omnichainIndexingCursor` is always < the `config.startBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Queued}. - * - `omnichainIndexingCursor` is always <= the `backfillEndBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Backfill}. - * - `omnichainIndexingCursor` is always >= the `latestIndexedBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Completed}. - * - `omnichainIndexingCursor` is always equal to the timestamp of the highest - * `latestIndexedBlock` across all chains that have started indexing - * (`chainStatus` is not {@link ChainIndexingStatusIds.Queued}). - */ -export interface OmnichainIndexingStatusSnapshotBackfill { - /** - * The status of omnichain indexing. - */ - omnichainStatus: typeof OmnichainIndexingStatusIds.Backfill; - - /** - * The indexing status snapshot for each indexed chain. - */ - chains: Map; - - /** - * The timestamp of omnichain indexing progress across all indexed chains. - */ - omnichainIndexingCursor: UnixTimestamp; -} - -/** - * Omnichain indexing status snapshot when the overall `omnichainStatus` is - * {@link OmnichainIndexingStatusIds.Following}. - * - * Invariants: - * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Following}. - * - `chains` is guaranteed to contain at least one chain with a `status` of - * {@link ChainIndexingStatusIds.Following}. - * - `omnichainIndexingCursor` is always < the `config.startBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Queued}. - * - `omnichainIndexingCursor` is always <= the `backfillEndBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Backfill}. - * - `omnichainIndexingCursor` is always >= the `latestIndexedBlock.timestamp` for all - * chains with `chainStatus` of {@link ChainIndexingStatusIds.Completed}. - * - `omnichainIndexingCursor` is always equal to the timestamp of the highest - * `latestIndexedBlock` across all chains that have started indexing - * (`chainStatus` is not {@link ChainIndexingStatusIds.Queued}). - */ -export interface OmnichainIndexingStatusSnapshotFollowing { - /** - * The status of omnichain indexing. - */ - omnichainStatus: typeof OmnichainIndexingStatusIds.Following; - - /** - * The indexing status snapshot for each indexed chain. - */ - chains: Map; - - /** - * The timestamp of omnichain indexing progress across all indexed chains. - */ - omnichainIndexingCursor: UnixTimestamp; -} - -/** - * Omnichain indexing status snapshot when the overall `omnichainStatus` is - * {@link OmnichainIndexingStatusIds.Completed}. - * - * Invariants: - * - `omnichainStatus` is always {@link OmnichainIndexingStatusIds.Completed}. - * - `chains` is always a map to {@link ChainIndexingStatusSnapshotCompleted} values exclusively. - * - `omnichainIndexingCursor` is always equal to the highest - * `latestIndexedBlock.timestamp` for all chains. - */ -export interface OmnichainIndexingStatusSnapshotCompleted { - /** - * The status of omnichain indexing. - */ - omnichainStatus: typeof OmnichainIndexingStatusIds.Completed; - - /** - * The indexing status snapshot for each indexed chain. - */ - chains: Map; - - /** - * The timestamp of omnichain indexing progress across all indexed chains. - */ - omnichainIndexingCursor: UnixTimestamp; -} - -/** - * Omnichain indexing status snapshot for one or more chains. - * - * Use the `omnichainStatus` field to determine the specific type interpretation - * at runtime. - */ -export type OmnichainIndexingStatusSnapshot = - | OmnichainIndexingStatusSnapshotUnstarted - | OmnichainIndexingStatusSnapshotBackfill - | OmnichainIndexingStatusSnapshotCompleted - | OmnichainIndexingStatusSnapshotFollowing; +import type { OmnichainIndexingStatusSnapshot } from "./omnichain-indexing-status-snapshot"; /** * The strategy used for indexing one or more chains. diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts index 42f97e8b7..94f86b126 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts @@ -1,22 +1,11 @@ import type { ParsePayload } from "zod/v4/core"; -import type { ChainId } from "../../shared/types"; import { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, - type ChainIndexingStatusSnapshot, } from "./chain-indexing-status-snapshot"; -import { - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted, - getOmnichainIndexingStatus, -} from "./helpers"; import type { CrossChainIndexingStatusSnapshotOmnichain, - OmnichainIndexingStatusSnapshot, - OmnichainIndexingStatusSnapshotFollowing, RealtimeIndexingStatusProjection, } from "./types"; @@ -24,229 +13,6 @@ import type { * Invariants for {@link OmnichainIndexingSnapshot}. */ -/** - * Invariant: For omnichain snapshot, - * `omnichainStatus` is set based on the snapshots of individual chains. - */ -export function invariant_omnichainSnapshotStatusIsConsistentWithChainSnapshot( - ctx: ParsePayload, -) { - const snapshot = ctx.value; - const chains = Array.from(snapshot.chains.values()); - const expectedOmnichainStatus = getOmnichainIndexingStatus(chains); - const actualOmnichainStatus = snapshot.omnichainStatus; - - if (expectedOmnichainStatus !== actualOmnichainStatus) { - ctx.issues.push({ - code: "custom", - input: snapshot, - message: `'${actualOmnichainStatus}' is an invalid omnichainStatus. Expected '${expectedOmnichainStatus}' based on the statuses of individual chains.`, - }); - } -} - -/** - * Invariant: For omnichain status snapshot, - * `omnichainIndexingCursor` is lower than the earliest start block - * across all queued chains. - * - * Note: if there are no queued chains, the invariant holds. - */ -export function invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains( - ctx: ParsePayload, -) { - const snapshot = ctx.value; - const queuedChains = Array.from(snapshot.chains.values()).filter( - (chain) => chain.chainStatus === ChainIndexingStatusIds.Queued, - ); - - // there are no queued chains - if (queuedChains.length === 0) { - // the invariant holds - return; - } - - const queuedChainStartBlocks = queuedChains.map((chain) => chain.config.startBlock.timestamp); - const queuedChainEarliestStartBlock = Math.min(...queuedChainStartBlocks); - - // there are queued chains - // the invariant holds if the omnichain indexing cursor is lower than - // the earliest start block across all queued chains - if (snapshot.omnichainIndexingCursor >= queuedChainEarliestStartBlock) { - ctx.issues.push({ - code: "custom", - input: snapshot, - message: - "`omnichainIndexingCursor` must be lower than the earliest start block across all queued chains.", - }); - } -} - -/** - * Invariant: For omnichain status snapshot, - * `omnichainIndexingCursor` is lower than or equal to - * the highest `backfillEndBlock` across all backfill chains. - * - * Note: if there are no backfill chains, the invariant holds. - */ -export function invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains( - ctx: ParsePayload, -) { - const snapshot = ctx.value; - const backfillChains = Array.from(snapshot.chains.values()).filter( - (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill, - ); - - // there are no backfill chains - if (backfillChains.length === 0) { - // the invariant holds - return; - } - - const backfillEndBlocks = backfillChains.map((chain) => chain.backfillEndBlock.timestamp); - const highestBackfillEndBlock = Math.max(...backfillEndBlocks); - - // there are backfill chains - // the invariant holds if the omnichainIndexingCursor is lower than or - // equal to the highest backfillEndBlock across all backfill chains. - if (snapshot.omnichainIndexingCursor > highestBackfillEndBlock) { - ctx.issues.push({ - code: "custom", - input: snapshot, - message: - "`omnichainIndexingCursor` must be lower than or equal to the highest `backfillEndBlock` across all backfill chains.", - }); - } -} - -/** - * Invariant: For omnichain status snapshot, - * `omnichainIndexingCursor` is same as the highest latestIndexedBlock - * across all indexed chains. - * - * Note: if there are no indexed chains, the invariant holds. - */ -export function invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain( - ctx: ParsePayload, -) { - const snapshot = ctx.value; - const indexedChains = Array.from(snapshot.chains.values()).filter( - (chain) => - chain.chainStatus === ChainIndexingStatusIds.Backfill || - chain.chainStatus === ChainIndexingStatusIds.Completed || - chain.chainStatus === ChainIndexingStatusIds.Following, - ); - - // there are no indexed chains - if (indexedChains.length === 0) { - // the invariant holds - return; - } - - const indexedChainLatestIndexedBlocks = indexedChains.map( - (chain) => chain.latestIndexedBlock.timestamp, - ); - const indexedChainHighestLatestIndexedBlock = Math.max(...indexedChainLatestIndexedBlocks); - - // there are indexed chains - // the invariant holds if the omnichain indexing cursor is same as - // the highest latestIndexedBlock across all indexed chains - if (snapshot.omnichainIndexingCursor !== indexedChainHighestLatestIndexedBlock) { - ctx.issues.push({ - code: "custom", - input: snapshot, - message: - "`omnichainIndexingCursor` must be same as the highest `latestIndexedBlock` across all indexed chains.", - }); - } -} - -/** - * Invariant: For omnichain status snapshot 'unstarted', - * all chains must have "queued" status. - */ -export function invariant_omnichainSnapshotUnstartedHasValidChains( - ctx: ParsePayload>, -) { - const chains = ctx.value; - const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted( - Array.from(chains.values()), - ); - - if (hasValidChains === false) { - ctx.issues.push({ - code: "custom", - input: chains, - message: `For omnichain status snapshot 'unstarted', all chains must have "queued" status.`, - }); - } -} - -/** - * Invariant: For omnichain status snapshot 'backfill', - * at least one chain must be in "backfill" status and - * each chain has to have a status of either "queued", "backfill" - * or "completed". - */ -export function invariant_omnichainStatusSnapshotBackfillHasValidChains( - ctx: ParsePayload>, -) { - const chains = ctx.value; - const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill( - Array.from(chains.values()), - ); - - if (hasValidChains === false) { - ctx.issues.push({ - code: "custom", - input: chains, - message: `For omnichain status snapshot 'backfill', at least one chain must be in "backfill" status and each chain has to have a status of either "queued", "backfill" or "completed".`, - }); - } -} - -/** - * Invariant: For omnichain status snapshot 'completed', - * all chains must have "completed" status. - */ -export function invariant_omnichainStatusSnapshotCompletedHasValidChains( - ctx: ParsePayload>, -) { - const chains = ctx.value; - const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted( - Array.from(chains.values()), - ); - - if (hasValidChains === false) { - ctx.issues.push({ - code: "custom", - input: chains, - message: `For omnichain status snapshot 'completed', all chains must have "completed" status.`, - }); - } -} - -/** - * Invariant: For omnichain status snapshot 'following', - * at least one chain must be in 'following' status. - */ -export function invariant_omnichainStatusSnapshotFollowingHasValidChains( - ctx: ParsePayload, -) { - const snapshot = ctx.value; - const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing( - Array.from(snapshot.chains.values()), - ); - - if (hasValidChains === false) { - ctx.issues.push({ - code: "custom", - input: snapshot, - message: "For omnichainStatus 'following', at least one chain must be in 'following' status.", - }); - } -} - /** * Invariants for {@link CrossChainIndexingStatusSnapshotOmnichain}. */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/omnichain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..543e3b0e2 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/omnichain-indexing-status-snapshot.ts @@ -0,0 +1,341 @@ +import { z } from "zod/v4"; +import type { ParsePayload } from "zod/v4/core"; + +import { deserializeChainId } from "../../../shared/deserialize"; +import type { ChainId } from "../../../shared/types"; +import { makeChainIdStringSchema, makeUnixTimestampSchema } from "../../../shared/zod-schemas"; +import { + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshot, + type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotQueued, +} from "../chain-indexing-status-snapshot"; +import { + type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, + checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill, + checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted, + checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing, + checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted, + getOmnichainIndexingStatus, + OmnichainIndexingStatusIds, + type OmnichainIndexingStatusSnapshot, + type OmnichainIndexingStatusSnapshotFollowing, +} from "../omnichain-indexing-status-snapshot"; +import { makeChainIndexingStatusSnapshotSchema } from "./chain-indexing-status-snapshot"; + +/** + * Invariant: For omnichain snapshot, + * `omnichainStatus` is set based on the snapshots of individual chains. + */ +export function invariant_omnichainSnapshotStatusIsConsistentWithChainSnapshot( + ctx: ParsePayload, +) { + const snapshot = ctx.value; + const chains = Array.from(snapshot.chains.values()); + const expectedOmnichainStatus = getOmnichainIndexingStatus(chains); + const actualOmnichainStatus = snapshot.omnichainStatus; + + if (expectedOmnichainStatus !== actualOmnichainStatus) { + ctx.issues.push({ + code: "custom", + input: snapshot, + message: `'${actualOmnichainStatus}' is an invalid omnichainStatus. Expected '${expectedOmnichainStatus}' based on the statuses of individual chains.`, + }); + } +} + +/** + * Invariant: For omnichain status snapshot, + * `omnichainIndexingCursor` is lower than the earliest start block + * across all queued chains. + * + * Note: if there are no queued chains, the invariant holds. + */ +export function invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains( + ctx: ParsePayload, +) { + const snapshot = ctx.value; + const queuedChains = Array.from(snapshot.chains.values()).filter( + (chain) => chain.chainStatus === ChainIndexingStatusIds.Queued, + ); + + // there are no queued chains + if (queuedChains.length === 0) { + // the invariant holds + return; + } + + const queuedChainStartBlocks = queuedChains.map((chain) => chain.config.startBlock.timestamp); + const queuedChainEarliestStartBlock = Math.min(...queuedChainStartBlocks); + + // there are queued chains + // the invariant holds if the omnichain indexing cursor is lower than + // the earliest start block across all queued chains + if (snapshot.omnichainIndexingCursor >= queuedChainEarliestStartBlock) { + ctx.issues.push({ + code: "custom", + input: snapshot, + message: + "`omnichainIndexingCursor` must be lower than the earliest start block across all queued chains.", + }); + } +} + +/** + * Invariant: For omnichain status snapshot, + * `omnichainIndexingCursor` is lower than or equal to + * the highest `backfillEndBlock` across all backfill chains. + * + * Note: if there are no backfill chains, the invariant holds. + */ +export function invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains( + ctx: ParsePayload, +) { + const snapshot = ctx.value; + const backfillChains = Array.from(snapshot.chains.values()).filter( + (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill, + ); + + // there are no backfill chains + if (backfillChains.length === 0) { + // the invariant holds + return; + } + + const backfillEndBlocks = backfillChains.map((chain) => chain.backfillEndBlock.timestamp); + const highestBackfillEndBlock = Math.max(...backfillEndBlocks); + + // there are backfill chains + // the invariant holds if the omnichainIndexingCursor is lower than or + // equal to the highest backfillEndBlock across all backfill chains. + if (snapshot.omnichainIndexingCursor > highestBackfillEndBlock) { + ctx.issues.push({ + code: "custom", + input: snapshot, + message: + "`omnichainIndexingCursor` must be lower than or equal to the highest `backfillEndBlock` across all backfill chains.", + }); + } +} + +/** + * Invariant: For omnichain status snapshot, + * `omnichainIndexingCursor` is same as the highest latestIndexedBlock + * across all indexed chains. + * + * Note: if there are no indexed chains, the invariant holds. + */ +export function invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain( + ctx: ParsePayload, +) { + const snapshot = ctx.value; + const indexedChains = Array.from(snapshot.chains.values()).filter( + (chain) => + chain.chainStatus === ChainIndexingStatusIds.Backfill || + chain.chainStatus === ChainIndexingStatusIds.Completed || + chain.chainStatus === ChainIndexingStatusIds.Following, + ); + + // there are no indexed chains + if (indexedChains.length === 0) { + // the invariant holds + return; + } + + const indexedChainLatestIndexedBlocks = indexedChains.map( + (chain) => chain.latestIndexedBlock.timestamp, + ); + const indexedChainHighestLatestIndexedBlock = Math.max(...indexedChainLatestIndexedBlocks); + + // there are indexed chains + // the invariant holds if the omnichain indexing cursor is same as + // the highest latestIndexedBlock across all indexed chains + if (snapshot.omnichainIndexingCursor !== indexedChainHighestLatestIndexedBlock) { + ctx.issues.push({ + code: "custom", + input: snapshot, + message: + "`omnichainIndexingCursor` must be same as the highest `latestIndexedBlock` across all indexed chains.", + }); + } +} + +/** + * Invariant: For omnichain status snapshot 'unstarted', + * all chains must have "queued" status. + */ +export function invariant_omnichainSnapshotUnstartedHasValidChains( + ctx: ParsePayload>, +) { + const chains = ctx.value; + const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted( + Array.from(chains.values()), + ); + + if (hasValidChains === false) { + ctx.issues.push({ + code: "custom", + input: chains, + message: `For omnichain status snapshot 'unstarted', all chains must have "queued" status.`, + }); + } +} + +/** + * Invariant: For omnichain status snapshot 'backfill', + * at least one chain must be in "backfill" status and + * each chain has to have a status of either "queued", "backfill" + * or "completed". + */ +export function invariant_omnichainStatusSnapshotBackfillHasValidChains( + ctx: ParsePayload>, +) { + const chains = ctx.value; + const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill( + Array.from(chains.values()), + ); + + if (hasValidChains === false) { + ctx.issues.push({ + code: "custom", + input: chains, + message: `For omnichain status snapshot 'backfill', at least one chain must be in "backfill" status and each chain has to have a status of either "queued", "backfill" or "completed".`, + }); + } +} + +/** + * Invariant: For omnichain status snapshot 'completed', + * all chains must have "completed" status. + */ +export function invariant_omnichainStatusSnapshotCompletedHasValidChains( + ctx: ParsePayload>, +) { + const chains = ctx.value; + const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted( + Array.from(chains.values()), + ); + + if (hasValidChains === false) { + ctx.issues.push({ + code: "custom", + input: chains, + message: `For omnichain status snapshot 'completed', all chains must have "completed" status.`, + }); + } +} + +/** + * Invariant: For omnichain status snapshot 'following', + * at least one chain must be in 'following' status. + */ +export function invariant_omnichainStatusSnapshotFollowingHasValidChains( + ctx: ParsePayload, +) { + const snapshot = ctx.value; + const hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing( + Array.from(snapshot.chains.values()), + ); + + if (hasValidChains === false) { + ctx.issues.push({ + code: "custom", + input: snapshot, + message: "For omnichainStatus 'following', at least one chain must be in 'following' status.", + }); + } +} + +/** + * Makes Zod schema for {@link ChainIndexingStatusSnapshot} per chain. + */ +export const makeChainIndexingStatusesSchema = (valueLabel: string = "Value") => + z + .record(makeChainIdStringSchema(), makeChainIndexingStatusSnapshotSchema(valueLabel), { + error: + "Chains indexing statuses must be an object mapping valid chain IDs to their indexing status snapshots.", + }) + .transform((serializedChainsIndexingStatus) => { + const chainsIndexingStatus = new Map(); + + for (const [chainIdString, chainStatus] of Object.entries(serializedChainsIndexingStatus)) { + chainsIndexingStatus.set(deserializeChainId(chainIdString), chainStatus); + } + + return chainsIndexingStatus; + }); + +/** + * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotUnstarted} + */ +const makeOmnichainIndexingStatusSnapshotUnstartedSchema = (valueLabel?: string) => + z.strictObject({ + omnichainStatus: z.literal(OmnichainIndexingStatusIds.Unstarted), + chains: makeChainIndexingStatusesSchema(valueLabel) + .check(invariant_omnichainSnapshotUnstartedHasValidChains) + .transform((chains) => chains as Map), + omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), + }); + +/** + * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotBackfill} + */ +const makeOmnichainIndexingStatusSnapshotBackfillSchema = (valueLabel?: string) => + z.strictObject({ + omnichainStatus: z.literal(OmnichainIndexingStatusIds.Backfill), + chains: makeChainIndexingStatusesSchema(valueLabel) + .check(invariant_omnichainStatusSnapshotBackfillHasValidChains) + .transform( + (chains) => + chains as Map< + ChainId, + ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill + >, + ), + omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), + }); + +/** + * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotCompleted} + */ +const makeOmnichainIndexingStatusSnapshotCompletedSchema = (valueLabel?: string) => + z.strictObject({ + omnichainStatus: z.literal(OmnichainIndexingStatusIds.Completed), + chains: makeChainIndexingStatusesSchema(valueLabel) + .check(invariant_omnichainStatusSnapshotCompletedHasValidChains) + .transform((chains) => chains as Map), + omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), + }); + +/** + * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotFollowing} + */ +const makeOmnichainIndexingStatusSnapshotFollowingSchema = (valueLabel?: string) => + z.strictObject({ + omnichainStatus: z.literal(OmnichainIndexingStatusIds.Following), + chains: makeChainIndexingStatusesSchema(valueLabel), + omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), + }); + +/** + * Omnichain Indexing Snapshot Schema + * + * Makes a Zod schema definition for validating indexing snapshot + * across all chains indexed by ENSIndexer instance. + */ +export const makeOmnichainIndexingStatusSnapshotSchema = ( + valueLabel: string = "Omnichain Indexing Snapshot", +) => + z + .discriminatedUnion("omnichainStatus", [ + makeOmnichainIndexingStatusSnapshotUnstartedSchema(valueLabel), + makeOmnichainIndexingStatusSnapshotBackfillSchema(valueLabel), + makeOmnichainIndexingStatusSnapshotCompletedSchema(valueLabel), + makeOmnichainIndexingStatusSnapshotFollowingSchema(valueLabel), + ]) + .check(invariant_omnichainSnapshotStatusIsConsistentWithChainSnapshot) + .check(invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains) + .check( + invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains, + ) + .check(invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain); diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts index a19f2cc1f..a93463abb 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts @@ -8,137 +8,15 @@ */ import { z } from "zod/v4"; -import { deserializeChainId } from "../../shared/deserialize"; -import type { ChainId } from "../../shared/types"; +import { makeDurationSchema, makeUnixTimestampSchema } from "../../shared/zod-schemas"; +import { CrossChainIndexingStatusSnapshotOmnichain, CrossChainIndexingStrategyIds } from "./types"; import { - makeChainIdStringSchema, - makeDurationSchema, - makeUnixTimestampSchema, -} from "../../shared/zod-schemas"; -import type { - ChainIndexingStatusSnapshot, - ChainIndexingStatusSnapshotCompleted, - ChainIndexingStatusSnapshotQueued, -} from "./chain-indexing-status-snapshot"; -import { - type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, - CrossChainIndexingStatusSnapshotOmnichain, - CrossChainIndexingStrategyIds, - OmnichainIndexingStatusIds, - OmnichainIndexingStatusSnapshotBackfill, - OmnichainIndexingStatusSnapshotCompleted, - OmnichainIndexingStatusSnapshotFollowing, - OmnichainIndexingStatusSnapshotUnstarted, - RealtimeIndexingStatusProjection, -} from "./types"; -import { - invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain, - invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains, - invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains, - invariant_omnichainSnapshotStatusIsConsistentWithChainSnapshot, - invariant_omnichainSnapshotUnstartedHasValidChains, - invariant_omnichainStatusSnapshotBackfillHasValidChains, - invariant_omnichainStatusSnapshotCompletedHasValidChains, invariant_realtimeIndexingStatusProjectionProjectedAtIsAfterOrEqualToSnapshotTime, invariant_realtimeIndexingStatusProjectionWorstCaseDistanceIsCorrect, invariant_slowestChainEqualsToOmnichainSnapshotTime, invariant_snapshotTimeIsTheHighestKnownBlockTimestamp, } from "./validations"; -import { makeChainIndexingStatusSnapshotSchema } from "./zod-schema/chain-indexing-status-snapshot"; - -/** - * Makes Zod schema for {@link ChainIndexingStatusSnapshot} per chain. - */ -export const makeChainIndexingStatusesSchema = (valueLabel: string = "Value") => - z - .record(makeChainIdStringSchema(), makeChainIndexingStatusSnapshotSchema(valueLabel), { - error: - "Chains indexing statuses must be an object mapping valid chain IDs to their indexing status snapshots.", - }) - .transform((serializedChainsIndexingStatus) => { - const chainsIndexingStatus = new Map(); - - for (const [chainIdString, chainStatus] of Object.entries(serializedChainsIndexingStatus)) { - chainsIndexingStatus.set(deserializeChainId(chainIdString), chainStatus); - } - - return chainsIndexingStatus; - }); - -/** - * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotUnstarted} - */ -const makeOmnichainIndexingStatusSnapshotUnstartedSchema = (valueLabel?: string) => - z.strictObject({ - omnichainStatus: z.literal(OmnichainIndexingStatusIds.Unstarted), - chains: makeChainIndexingStatusesSchema(valueLabel) - .check(invariant_omnichainSnapshotUnstartedHasValidChains) - .transform((chains) => chains as Map), - omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), - }); - -/** - * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotBackfill} - */ -const makeOmnichainIndexingStatusSnapshotBackfillSchema = (valueLabel?: string) => - z.strictObject({ - omnichainStatus: z.literal(OmnichainIndexingStatusIds.Backfill), - chains: makeChainIndexingStatusesSchema(valueLabel) - .check(invariant_omnichainStatusSnapshotBackfillHasValidChains) - .transform( - (chains) => - chains as Map< - ChainId, - ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill - >, - ), - omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), - }); - -/** - * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotCompleted} - */ -const makeOmnichainIndexingStatusSnapshotCompletedSchema = (valueLabel?: string) => - z.strictObject({ - omnichainStatus: z.literal(OmnichainIndexingStatusIds.Completed), - chains: makeChainIndexingStatusesSchema(valueLabel) - .check(invariant_omnichainStatusSnapshotCompletedHasValidChains) - .transform((chains) => chains as Map), - omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), - }); - -/** - * Makes Zod schema for {@link OmnichainIndexingStatusSnapshotFollowing} - */ -const makeOmnichainIndexingStatusSnapshotFollowingSchema = (valueLabel?: string) => - z.strictObject({ - omnichainStatus: z.literal(OmnichainIndexingStatusIds.Following), - chains: makeChainIndexingStatusesSchema(valueLabel), - omnichainIndexingCursor: makeUnixTimestampSchema(valueLabel), - }); - -/** - * Omnichain Indexing Snapshot Schema - * - * Makes a Zod schema definition for validating indexing snapshot - * across all chains indexed by ENSIndexer instance. - */ -export const makeOmnichainIndexingStatusSnapshotSchema = ( - valueLabel: string = "Omnichain Indexing Snapshot", -) => - z - .discriminatedUnion("omnichainStatus", [ - makeOmnichainIndexingStatusSnapshotUnstartedSchema(valueLabel), - makeOmnichainIndexingStatusSnapshotBackfillSchema(valueLabel), - makeOmnichainIndexingStatusSnapshotCompletedSchema(valueLabel), - makeOmnichainIndexingStatusSnapshotFollowingSchema(valueLabel), - ]) - .check(invariant_omnichainSnapshotStatusIsConsistentWithChainSnapshot) - .check(invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains) - .check( - invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains, - ) - .check(invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain); +import { makeOmnichainIndexingStatusSnapshotSchema } from "./zod-schema/omnichain-indexing-status-snapshot"; /** * Makes Zod schema for {@link CrossChainIndexingStatusSnapshotOmnichain}