diff --git a/.changeset/quick-paws-attend.md b/.changeset/quick-paws-attend.md new file mode 100644 index 000000000..ede37f325 --- /dev/null +++ b/.changeset/quick-paws-attend.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": minor +--- + +Introduces `validateChainIndexingStatusSnapshot` which enables validating values against business-layer requirements. diff --git a/apps/ensindexer/src/lib/indexing-status/ponder-metadata/validations.ts b/apps/ensindexer/src/lib/indexing-status/ponder-metadata/validations.ts index dab8f7160..0394c58bd 100644 --- a/apps/ensindexer/src/lib/indexing-status/ponder-metadata/validations.ts +++ b/apps/ensindexer/src/lib/indexing-status/ponder-metadata/validations.ts @@ -1,13 +1,5 @@ -import { type ParsePayload, prettifyError } from "zod/v4/core"; +import { prettifyError } from "zod/v4/core"; -import { - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted, - OmnichainIndexingStatusIds, - type SerializedOmnichainIndexingStatusSnapshot, -} from "@ensnode/ensnode-sdk"; import type { PrometheusMetrics } from "@ensnode/ponder-metadata"; import { PonderAppSettingsSchema } from "./zod-schemas"; @@ -32,43 +24,3 @@ export function validatePonderMetrics(metrics: PrometheusMetrics) { ); } } - -/** - * Invariant: SerializedOmnichainSnapshot Has Valid Chains - * - * Validates that the `chains` property of a {@link SerializedOmnichainIndexingStatusSnapshot} - * is consistent with the reported `omnichainStatus`. - */ -export function invariant_serializedOmnichainSnapshotHasValidChains( - ctx: ParsePayload, -) { - const omnichainSnapshot = ctx.value; - const chains = Object.values(omnichainSnapshot.chains); - let hasValidChains = false; - - switch (omnichainSnapshot.omnichainStatus) { - case OmnichainIndexingStatusIds.Unstarted: - hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted(chains); - break; - - case OmnichainIndexingStatusIds.Backfill: - hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill(chains); - break; - - case OmnichainIndexingStatusIds.Completed: - hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted(chains); - break; - - case OmnichainIndexingStatusIds.Following: - hasValidChains = checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing(chains); - break; - } - - if (!hasValidChains) { - ctx.issues.push({ - code: "custom", - input: omnichainSnapshot, - message: `"chains" are not consistent with the reported '${omnichainSnapshot.omnichainStatus}' "omnichainStatus"`, - }); - } -} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/test-helpers.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/block-refs.mock.ts similarity index 100% rename from packages/ensnode-sdk/src/ensindexer/indexing-status/test-helpers.ts rename to packages/ensnode-sdk/src/ensindexer/indexing-status/block-refs.mock.ts diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.test.ts new file mode 100644 index 000000000..c28856bf7 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; + +import { earlierBlockRef, laterBlockRef } from "./block-refs.mock"; +import { + type ChainIndexingConfigDefinite, + type ChainIndexingConfigIndefinite, + ChainIndexingConfigTypeIds, + createIndexingConfig, +} from "./chain-indexing-status-snapshot"; + +describe("Chain Indexing Status Snapshot", () => { + describe("createIndexingConfig", () => { + it("returns 'definite' indexer config if the endBlock exists", () => { + // arrange + const startBlock = earlierBlockRef; + const endBlock = laterBlockRef; + + // act + const indexingConfig = createIndexingConfig(startBlock, endBlock); + + // assert + expect(indexingConfig).toStrictEqual({ + configType: ChainIndexingConfigTypeIds.Definite, + startBlock: earlierBlockRef, + endBlock: laterBlockRef, + } satisfies ChainIndexingConfigDefinite); + }); + + it("returns 'indefinite' indexer config if the endBlock does not exist", () => { + // arrange + const startBlock = earlierBlockRef; + const endBlock = null; + + // act + const indexingConfig = createIndexingConfig(startBlock, endBlock); + + // assert + expect(indexingConfig).toStrictEqual({ + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock: earlierBlockRef, + } satisfies ChainIndexingConfigIndefinite); + }); + }); +}); diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..659576b22 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/chain-indexing-status-snapshot.ts @@ -0,0 +1,395 @@ +import type { BlockRef, ChainId, UnixTimestamp } from "../../shared/types"; + +/** + * The type of indexing configuration for a chain. + */ +export const ChainIndexingConfigTypeIds = { + /** + * Represents that indexing of the chain should be performed for an indefinite range. + */ + Indefinite: "indefinite", + + /** + * Represents that indexing of the chain should be performed for a definite range. + */ + Definite: "definite", +} as const; + +/** + * The derived string union of possible {@link ChainIndexingConfigTypeIds}. + */ +export type ChainIndexingConfigTypeId = + (typeof ChainIndexingConfigTypeIds)[keyof typeof ChainIndexingConfigTypeIds]; + +/** + * Chain indexing config for a chain whose indexing config `configType` is + * {@link ChainIndexingConfigTypeIds.Indefinite}. + * + * Invariants: + * - `configType` is always `ChainIndexingConfigTypeIds.Indefinite`. + */ +export interface ChainIndexingConfigIndefinite { + /** + * The type of chain indexing config. + */ + configType: typeof ChainIndexingConfigTypeIds.Indefinite; + + /** + * A {@link BlockRef} to the block where indexing of the chain should start. + */ + startBlock: BlockRef; +} + +/** + * Chain indexing config for a chain whose indexing config `configType` is + * {@link ChainIndexingConfigTypeIds.Definite}. + * + * Invariants: + * - `configType` is always `ChainIndexingConfigTypeIds.Definite`. + * - `startBlock` is always before or the same as `endBlock`. + */ +export interface ChainIndexingConfigDefinite { + /** + * The type of chain indexing config. + */ + configType: typeof ChainIndexingConfigTypeIds.Definite; + + /** + * A {@link BlockRef} to the block where indexing of the chain should start. + */ + startBlock: BlockRef; + + /** + * A {@link BlockRef} to the block where indexing of the chain should end. + */ + endBlock: BlockRef; +} + +/** + * Indexing configuration for a chain. + * + * Use the `configType` field to determine the specific type interpretation + * at runtime. + */ +export type ChainIndexingConfig = ChainIndexingConfigIndefinite | ChainIndexingConfigDefinite; + +/** + * The status of indexing a chain at the time an indexing status snapshot + * is captured. + */ +export const ChainIndexingStatusIds = { + /** + * Represents that indexing of the chain is not ready to begin yet because: + * - ENSIndexer is in its initialization phase and the data to build a + * "true" {@link ChainIndexingStatusSnapshot} for the chain is still being loaded; or + * - ENSIndexer is using an omnichain indexing strategy and the + * `omnichainIndexingCursor` is <= `config.startBlock.timestamp` for the chain's + * {@link ChainIndexingStatusSnapshot}. + */ + Queued: "chain-queued", + + /** + * Represents that indexing of the chain is in progress and under a special + * "backfill" phase that optimizes for accelerated indexing until reaching the + * "fixed target" `backfillEndBlock`. + */ + Backfill: "chain-backfill", + + /** + * Represents that the "backfill" phase of indexing the chain is completed + * and that the chain is configured to be indexed for an indefinite range. + * Therefore, indexing of the chain remains indefinitely in progress where + * ENSIndexer will continuously work to discover and index new blocks as they + * are added to the chain across time. + */ + Following: "chain-following", + + /** + * Represents that indexing of the chain is completed as the chain is configured + * to be indexed for a definite range and the indexing of all blocks through + * that definite range is completed. + */ + Completed: "chain-completed", +} as const; + +/** + * The derived string union of possible {@link ChainIndexingStatusIds}. + */ +export type ChainIndexingStatusId = + (typeof ChainIndexingStatusIds)[keyof typeof ChainIndexingStatusIds]; + +/** + * Chain indexing status snapshot for a chain whose `chainStatus` is + * {@link ChainIndexingStatusIds.Queued}. + * + * Invariants: + * - `chainStatus` is always {@link ChainIndexingStatusIds.Queued}. + */ +export interface ChainIndexingStatusSnapshotQueued { + /** + * The status of indexing the chain at the time the indexing status snapshot + * was captured. + */ + chainStatus: typeof ChainIndexingStatusIds.Queued; + + /** + * The indexing configuration of the chain. + */ + config: ChainIndexingConfig; +} + +/** + * Chain indexing status snapshot for a chain whose `chainStatus` is + * {@link ChainIndexingStatusIds.Backfill}. + * + * During a backfill, special performance optimizations are applied to + * index all blocks between `config.startBlock` and `backfillEndBlock` + * as fast as possible. + * + * Note how `backfillEndBlock` is a "fixed target" that does not change during + * the lifetime of an ENSIndexer process instance: + * - If the `config` is {@link ChainIndexingConfigDefinite}: + * `backfillEndBlock` is always the same as `config.endBlock`. + * - If the `config` is {@link ChainIndexingConfigIndefinite}: + * `backfillEndBlock` is a {@link BlockRef} to what was the latest block on the + * chain when the ENSIndexer process was performing its initialization. Note how + * this means that if the backfill process takes X hours to complete, because the + * `backfillEndBlock` is a "fixed target", when `chainStatus` transitions to + * {@link ChainIndexingStatusIds.Following} the chain will be X hours behind + * "realtime" indexing. + * + * When `latestIndexedBlock` reaches `backfillEndBlock` the backfill is complete. + * The moment backfill is complete the `chainStatus` may not immediately transition. + * Instead, internal processing is completed for a period of time while + * `chainStatus` remains {@link ChainIndexingStatusIds.Backfill}. After this internal + * processing is completed `chainStatus` will transition: + * - to {@link ChainIndexingStatusIds.Following} if the `config` is + * {@link ChainIndexingConfigIndefinite}. + * - to {@link ChainIndexingStatusIds.Completed} if the `config` is + * {@link ChainIndexingConfigDefinite}. + * + * Invariants: + * - `chainStatus` is always {@link ChainIndexingStatusIds.Backfill}. + * - `config.startBlock` is always before or the same as `latestIndexedBlock` + * - `config.endBlock` is always the same as `backfillEndBlock` if and only if + * the config is {@link ChainIndexingConfigDefinite}. + * - `latestIndexedBlock` is always before or the same as `backfillEndBlock` + */ +export interface ChainIndexingStatusSnapshotBackfill { + /** + * The status of indexing the chain at the time the indexing status snapshot + * was captured. + */ + chainStatus: typeof ChainIndexingStatusIds.Backfill; + + /** + * The indexing configuration of the chain. + */ + config: ChainIndexingConfig; + + /** + * A {@link BlockRef} to the block that was most recently indexed as of the time the + * indexing status snapshot was captured. + */ + latestIndexedBlock: BlockRef; + + /** + * A {@link BlockRef} to the block where the backfill will end. + */ + backfillEndBlock: BlockRef; +} + +/** + * Chain indexing status snapshot for a chain whose `chainStatus` is + * {@link ChainIndexingStatusIds.Following}. + * + * Invariants: + * - `chainStatus` is always {@link ChainIndexingStatusIds.Following}. + * - `config` is always {@link ChainIndexingConfigIndefinite} + * - `config.startBlock` is always before or the same as `latestIndexedBlock` + * - `latestIndexedBlock` is always before or the same as `latestKnownBlock` + */ +export interface ChainIndexingStatusSnapshotFollowing { + /** + * The status of indexing the chain at the time the indexing status snapshot + * was captured. + */ + chainStatus: typeof ChainIndexingStatusIds.Following; + + /** + * The indexing configuration of the chain. + */ + config: ChainIndexingConfigIndefinite; + + /** + * A {@link BlockRef} to the block that was most recently indexed as of the time the + * indexing status snapshot was captured. + */ + latestIndexedBlock: BlockRef; + + /** + * A {@link BlockRef} to the "highest" block that has been discovered by RPCs + * and stored in the RPC cache as of the time the indexing status snapshot was + * captured. + */ + latestKnownBlock: BlockRef; +} + +/** + * Chain indexing status snapshot for a chain whose `chainStatus` is + * {@link ChainIndexingStatusIds.Completed}. + * + * After the backfill of a chain is completed, if the chain was configured + * to be indexed for a definite range, the chain indexing status will transition to + * {@link ChainIndexingStatusIds.Completed}. + * + * Invariants: + * - `chainStatus` is always {@link ChainIndexingStatusIds.Completed}. + * - `config` is always {@link ChainIndexingConfigDefinite} + * - `config.startBlock` is always before or the same as `latestIndexedBlock` + * - `latestIndexedBlock` is always the same as `config.endBlock`. + */ +export interface ChainIndexingStatusSnapshotCompleted { + /** + * The status of indexing the chain at the time the indexing status snapshot + * was captured. + */ + chainStatus: typeof ChainIndexingStatusIds.Completed; + + /** + * The indexing configuration of the chain. + */ + config: ChainIndexingConfigDefinite; + + /** + * A {@link BlockRef} to the block that was most recently indexed as of the time the + * indexing status snapshot was captured. + */ + latestIndexedBlock: BlockRef; +} + +/** + * Indexing status snapshot for a single chain. + * + * Use the `chainStatus` field to determine the specific type interpretation + * at runtime. + */ +export type ChainIndexingStatusSnapshot = + | ChainIndexingStatusSnapshotQueued + | ChainIndexingStatusSnapshotBackfill + | ChainIndexingStatusSnapshotFollowing + | ChainIndexingStatusSnapshotCompleted; + +/** + * Create {@link ChainIndexingConfig} for given block refs. + * + * @param startBlock required block ref + * @param endBlock optional block ref + */ +export function createIndexingConfig( + startBlock: BlockRef, + endBlock: BlockRef | null, +): ChainIndexingConfig { + if (endBlock) { + return { + configType: ChainIndexingConfigTypeIds.Definite, + startBlock, + endBlock, + } satisfies ChainIndexingConfigDefinite; + } + + return { + configType: ChainIndexingConfigTypeIds.Indefinite, + startBlock, + } satisfies ChainIndexingConfigIndefinite; +} + +/** + * Get the timestamp of the lowest `config.startBlock` across all chains + * in the provided array of {@link ChainIndexingStatusSnapshot}. + * + * Such timestamp is useful when presenting the "lowest" block + * to be indexed across all chains. + */ +export function getTimestampForLowestOmnichainStartBlock( + chains: ChainIndexingStatusSnapshot[], +): UnixTimestamp { + const earliestKnownBlockTimestamps: UnixTimestamp[] = chains.map( + (chain) => chain.config.startBlock.timestamp, + ); + + // Invariant: earliestKnownBlockTimestamps is guaranteed to have at least one element + if (earliestKnownBlockTimestamps.length === 0) { + throw new Error( + "Invariant violation: at least one chain is required to determine the lowest omnichain start block timestamp", + ); + } + + return Math.min(...earliestKnownBlockTimestamps); +} + +/** + * Get the timestamp of the "highest known block" across all chains + * in the provided array of {@link ChainIndexingStatusSnapshot}. + * + * Such timestamp is useful when presenting the "highest known block" + * to be indexed across all chains. + * + * The "highest known block" for a chain depends on its status: + * - `config.endBlock` for a "queued" chain (only if the config is `Definite`), + * - `backfillEndBlock` for a "backfill" chain, + * - `latestIndexedBlock` for a "completed" chain, + * - `latestKnownBlock` for a "following" chain. + */ +export function getTimestampForHighestOmnichainKnownBlock( + chains: ChainIndexingStatusSnapshot[], +): UnixTimestamp { + const latestKnownBlockTimestamps: UnixTimestamp[] = []; + + for (const chain of chains) { + switch (chain.chainStatus) { + case ChainIndexingStatusIds.Queued: + if (chain.config.configType === ChainIndexingConfigTypeIds.Definite) { + latestKnownBlockTimestamps.push(chain.config.endBlock.timestamp); + } + break; + + case ChainIndexingStatusIds.Backfill: + latestKnownBlockTimestamps.push(chain.backfillEndBlock.timestamp); + + break; + + case ChainIndexingStatusIds.Completed: + latestKnownBlockTimestamps.push(chain.latestIndexedBlock.timestamp); + break; + + case ChainIndexingStatusIds.Following: + latestKnownBlockTimestamps.push(chain.latestKnownBlock.timestamp); + break; + } + } + + // Invariant: at least one chain must contribute a known block timestamp + // (e.g., Queued chains with Indefinite config do not contribute) + if (latestKnownBlockTimestamps.length === 0) { + throw new Error( + "Invariant: at least one chain must contribute a known block timestamp to determine the highest omnichain known block timestamp", + ); + } + + return Math.max(...latestKnownBlockTimestamps); +} + +/** + * Sort a list of [{@link ChainId}, {@link ChainIndexingStatusSnapshot}] tuples + * by the omnichain start block timestamp in ascending order. + */ +export function sortChainStatusesByStartBlockAsc< + ChainStatusType extends ChainIndexingStatusSnapshot, +>(chains: [ChainId, ChainStatusType][]): [ChainId, ChainStatusType][] { + // Sort the chain statuses by the omnichain first block to index timestamp + return [...chains].sort( + ([, chainA], [, chainB]) => + chainA.config.startBlock.timestamp - chainB.config.startBlock.timestamp, + ); +} 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 3a7b265f0..8e65b716a 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/conversions.test.ts @@ -1,18 +1,22 @@ import { describe, expect, it } from "vitest"; -import { deserializeOmnichainIndexingStatusSnapshot } from "./deserialize"; -import { serializeOmnichainIndexingStatusSnapshot } from "./serialize"; -import type { SerializedOmnichainIndexingStatusSnapshot } from "./serialized-types"; -import { earlierBlockRef, earliestBlockRef, laterBlockRef, latestBlockRef } from "./test-helpers"; +import { + earlierBlockRef, + earliestBlockRef, + laterBlockRef, + latestBlockRef, +} from "./block-refs.mock"; import { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, type ChainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, - OmnichainIndexingStatusIds, - type OmnichainIndexingStatusSnapshot, -} from "./types"; +} 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"; 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 e2148deac..5864fecd5 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize.ts @@ -1,43 +1,21 @@ import { prettifyError } from "zod/v4"; import type { - SerializedChainIndexingStatusSnapshot, SerializedCrossChainIndexingStatusSnapshot, SerializedOmnichainIndexingStatusSnapshot, SerializedRealtimeIndexingStatusProjection, } from "./serialized-types"; import type { - ChainIndexingStatusSnapshot, CrossChainIndexingStatusSnapshot, OmnichainIndexingStatusSnapshot, RealtimeIndexingStatusProjection, } from "./types"; import { - makeChainIndexingStatusSnapshotSchema, makeCrossChainIndexingStatusSnapshotSchema, makeOmnichainIndexingStatusSnapshotSchema, makeRealtimeIndexingStatusProjectionSchema, } from "./zod-schemas"; -/** - * Deserialize into a {@link ChainIndexingSnapshot} object. - */ -export function deserializeChainIndexingStatusSnapshot( - maybeSnapshot: SerializedChainIndexingStatusSnapshot, - valueLabel?: string, -): ChainIndexingStatusSnapshot { - const schema = makeChainIndexingStatusSnapshotSchema(valueLabel); - const parsed = schema.safeParse(maybeSnapshot); - - if (parsed.error) { - throw new Error( - `Cannot deserialize into ChainIndexingStatusSnapshot:\n${prettifyError(parsed.error)}\n`, - ); - } - - return parsed.data; -} - /** * Deserialize an {@link OmnichainIndexingStatusSnapshot} object. */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/chain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..c7c07294d --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/deserialize/chain-indexing-status-snapshot.ts @@ -0,0 +1,24 @@ +import { prettifyError } from "zod/v4"; + +import type { ChainIndexingStatusSnapshot } from "../chain-indexing-status-snapshot"; +import type { SerializedChainIndexingStatusSnapshot } from "../serialize/chain-indexing-status-snapshot"; +import { makeSerializedChainIndexingStatusSnapshotSchema } from "../zod-schema/chain-indexing-status-snapshot"; + +/** + * Deserialize into a {@link ChainIndexingStatusSnapshot} object. + */ +export function deserializeChainIndexingStatusSnapshot( + maybeSnapshot: SerializedChainIndexingStatusSnapshot, + valueLabel?: string, +): ChainIndexingStatusSnapshot { + const schema = makeSerializedChainIndexingStatusSnapshotSchema(valueLabel); + const parsed = schema.safeParse(maybeSnapshot); + + if (parsed.error) { + throw new Error( + `Cannot deserialize into ChainIndexingStatusSnapshot:\n${prettifyError(parsed.error)}\n`, + ); + } + + return parsed.data; +} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts index 88343d2c2..f0d0e9c63 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.test.ts @@ -2,14 +2,12 @@ import { describe, expect, it } from "vitest"; import type { BlockRef } from "../../shared/types"; import { - createIndexingConfig, - getOmnichainIndexingCursor, - getOmnichainIndexingStatus, -} from "./helpers"; -import { earlierBlockRef, earliestBlockRef, laterBlockRef, latestBlockRef } from "./test-helpers"; + earlierBlockRef, + earliestBlockRef, + laterBlockRef, + latestBlockRef, +} from "./block-refs.mock"; import { - type ChainIndexingConfigDefinite, - type ChainIndexingConfigIndefinite, ChainIndexingConfigTypeIds, ChainIndexingStatusIds, type ChainIndexingStatusSnapshot, @@ -17,8 +15,9 @@ import { type ChainIndexingStatusSnapshotCompleted, type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, - OmnichainIndexingStatusIds, -} from "./types"; +} from "./chain-indexing-status-snapshot"; +import { getOmnichainIndexingCursor, getOmnichainIndexingStatus } from "./helpers"; +import { OmnichainIndexingStatusIds } from "./types"; describe("ENSIndexer: Indexing Snapshot helpers", () => { describe("getOmnichainIndexingStatus", () => { @@ -164,39 +163,6 @@ describe("ENSIndexer: Indexing Snapshot helpers", () => { expect(overallIndexingStatus).toStrictEqual(OmnichainIndexingStatusIds.Following); }); }); - - describe("createIndexingConfig", () => { - it("returns 'definite' indexer config if the endBlock exists", () => { - // arrange - const startBlock = earlierBlockRef; - const endBlock = laterBlockRef; - - // act - const indexingConfig = createIndexingConfig(startBlock, endBlock); - - // assert - expect(indexingConfig).toStrictEqual({ - configType: ChainIndexingConfigTypeIds.Definite, - startBlock: earlierBlockRef, - endBlock: laterBlockRef, - } satisfies ChainIndexingConfigDefinite); - }); - - it("returns 'indefinite' indexer config if the endBlock exists", () => { - // arrange - const startBlock = earlierBlockRef; - const endBlock = null; - - // act - const indexingConfig = createIndexingConfig(startBlock, endBlock); - - // assert - expect(indexingConfig).toStrictEqual({ - configType: ChainIndexingConfigTypeIds.Indefinite, - startBlock: earlierBlockRef, - } satisfies ChainIndexingConfigIndefinite); - }); - }); }); describe("getOmnichainIndexingCursor", () => { diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts index 085923944..7df076fed 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts @@ -1,14 +1,12 @@ import type { BlockRef, ChainId, UnixTimestamp } from "../../shared/types"; import { - type ChainIndexingConfig, - type ChainIndexingConfigDefinite, - type ChainIndexingConfigIndefinite, - ChainIndexingConfigTypeIds, ChainIndexingStatusIds, type ChainIndexingStatusSnapshot, type ChainIndexingStatusSnapshotCompleted, - type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, type ChainIndexingStatusSnapshotQueued, +} from "./chain-indexing-status-snapshot"; +import { + type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, type CrossChainIndexingStatusSnapshot, type OmnichainIndexingStatusId, OmnichainIndexingStatusIds, @@ -45,70 +43,6 @@ export function getOmnichainIndexingStatus( throw new Error(`Unable to determine omnichain indexing status for provided chains.`); } -/** - * Get the timestamp of the lowest `config.startBlock` across all chains - * in the provided array of {@link ChainIndexingStatusSnapshot}. - * - * Such timestamp is useful when presenting the "lowest" block - * to be indexed across all chains. - */ -export function getTimestampForLowestOmnichainStartBlock( - chains: ChainIndexingStatusSnapshot[], -): UnixTimestamp { - const earliestKnownBlockTimestamps: UnixTimestamp[] = chains.map( - (chain) => chain.config.startBlock.timestamp, - ); - - return Math.min(...earliestKnownBlockTimestamps); -} - -/** - * Get the timestamp of the "highest known block" across all chains - * in the provided array of {@link ChainIndexingStatusSnapshot}. - * - * Such timestamp is useful when presenting the "highest known block" - * to be indexed across all chains. - * - * The "highest known block" for a chain depends on its status: - * - `config.endBlock` for a "queued" chain, - * - `backfillEndBlock` for a "backfill" chain, - * - `latestIndexedBlock` for a "completed" chain, - * - `latestKnownBlock` for a "following" chain. - */ -export function getTimestampForHighestOmnichainKnownBlock( - chains: ChainIndexingStatusSnapshot[], -): UnixTimestamp { - const latestKnownBlockTimestamps: UnixTimestamp[] = []; - - for (const chain of chains) { - switch (chain.chainStatus) { - case ChainIndexingStatusIds.Queued: - if ( - chain.config.configType === ChainIndexingConfigTypeIds.Definite && - chain.config.endBlock - ) { - latestKnownBlockTimestamps.push(chain.config.endBlock.timestamp); - } - break; - - case ChainIndexingStatusIds.Backfill: - latestKnownBlockTimestamps.push(chain.backfillEndBlock.timestamp); - - break; - - case ChainIndexingStatusIds.Completed: - latestKnownBlockTimestamps.push(chain.latestIndexedBlock.timestamp); - break; - - case ChainIndexingStatusIds.Following: - latestKnownBlockTimestamps.push(chain.latestKnownBlock.timestamp); - break; - } - } - - return Math.max(...latestKnownBlockTimestamps); -} - /** * Get Omnichain Indexing Cursor * @@ -147,30 +81,6 @@ export function getOmnichainIndexingCursor(chains: ChainIndexingStatusSnapshot[] return Math.max(...latestIndexedBlockTimestamps); } -/** - * Create {@link ChainIndexingConfig} for given block refs. - * - * @param startBlock required block ref - * @param endBlock optional block ref - */ -export function createIndexingConfig( - startBlock: BlockRef, - endBlock: BlockRef | null, -): ChainIndexingConfig { - if (endBlock) { - return { - configType: ChainIndexingConfigTypeIds.Definite, - startBlock, - endBlock, - } satisfies ChainIndexingConfigDefinite; - } - - return { - configType: ChainIndexingConfigTypeIds.Indefinite, - startBlock, - } satisfies ChainIndexingConfigIndefinite; -} - /** * Check if Chain Indexing Status Snapshots fit the 'unstarted' overall status * snapshot requirements: @@ -245,22 +155,6 @@ export function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollo return allChainsHaveValidStatuses; } -/** - * Sort a list of [{@link ChainId}, {@link ChainIndexingStatusSnapshot}] tuples - * by the omnichain start block timestamp in ascending order. - */ -export function sortChainStatusesByStartBlockAsc< - ChainStatusType extends ChainIndexingStatusSnapshot, ->(chains: [ChainId, ChainStatusType][]): [ChainId, ChainStatusType][] { - // Sort the chain statuses by the omnichain first block to index timestamp - chains.sort( - ([, chainA], [, chainB]) => - chainA.config.startBlock.timestamp - chainB.config.startBlock.timestamp, - ); - - return chains; -} - /** * 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 62c1304c1..dc02d1811 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/index.ts @@ -1,6 +1,10 @@ +export * from "./chain-indexing-status-snapshot"; export * from "./deserialize"; +export * from "./deserialize/chain-indexing-status-snapshot"; export * from "./helpers"; export * from "./projection"; export * from "./serialize"; +export * from "./serialize/chain-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/projection.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts index e6a14a9d4..ace15062f 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/projection.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from "vitest"; -import { deserializeCrossChainIndexingStatusSnapshot } from "./deserialize"; -import { createRealtimeIndexingStatusProjection } from "./projection"; -import { earlierBlockRef, laterBlockRef } from "./test-helpers"; +import { earlierBlockRef, laterBlockRef } from "./block-refs.mock"; import { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, +} from "./chain-indexing-status-snapshot"; +import { deserializeCrossChainIndexingStatusSnapshot } from "./deserialize"; +import { createRealtimeIndexingStatusProjection } from "./projection"; +import { CrossChainIndexingStrategyIds, OmnichainIndexingStatusIds, type RealtimeIndexingStatusProjection, diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts index d35658505..e797d88c3 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts @@ -1,6 +1,4 @@ -import { serializeChainId } from "../../shared/serialize"; -import type { ChainIdString } from "../../shared/serialized-types"; -import type { ChainId } from "../../shared/types"; +import { serializeChainIndexingSnapshots } from "./serialize/chain-indexing-status-snapshot"; import type { SerializedCrossChainIndexingStatusSnapshot, SerializedOmnichainIndexingStatusSnapshot, @@ -11,7 +9,6 @@ import type { SerializedRealtimeIndexingStatusProjection, } from "./serialized-types"; import { - type ChainIndexingStatusSnapshot, type CrossChainIndexingStatusSnapshot, OmnichainIndexingStatusIds, type OmnichainIndexingStatusSnapshot, @@ -42,23 +39,6 @@ export function serializeRealtimeIndexingStatusProjection( } satisfies SerializedRealtimeIndexingStatusProjection; } -/** - * Serialize chain indexing snapshots. - */ -export function serializeChainIndexingSnapshots< - ChainIndexingStatusSnapshotType extends ChainIndexingStatusSnapshot, ->( - chains: Map, -): Record { - const serializedSnapshots: Record = {}; - - for (const [chainId, snapshot] of chains.entries()) { - serializedSnapshots[serializeChainId(chainId)] = snapshot; - } - - return serializedSnapshots; -} - /** * Serialize a {@link OmnichainIndexingStatusSnapshot} object. */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/chain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..bc9a34775 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize/chain-indexing-status-snapshot.ts @@ -0,0 +1,52 @@ +import { serializeChainId } from "../../../shared/serialize"; +import type { ChainIdString } from "../../../shared/serialized-types"; +import type { ChainId } from "../../../shared/types"; +import type { + ChainIndexingStatusSnapshot, + ChainIndexingStatusSnapshotBackfill, + ChainIndexingStatusSnapshotCompleted, + ChainIndexingStatusSnapshotFollowing, + ChainIndexingStatusSnapshotQueued, +} from "../chain-indexing-status-snapshot"; + +/** + * Serialized representation of {@link ChainIndexingStatusSnapshot} + */ +export type SerializedChainIndexingStatusSnapshot = ChainIndexingStatusSnapshot; + +/** + * Serialized representation of {@link ChainIndexingStatusSnapshotQueued} + */ +export type SerializedChainIndexingStatusSnapshotQueued = ChainIndexingStatusSnapshotQueued; + +/** + * Serialized representation of {@link ChainIndexingStatusSnapshotBackfill} + */ +export type SerializedChainIndexingStatusSnapshotBackfill = ChainIndexingStatusSnapshotBackfill; + +/** + * Serialized representation of {@link ChainIndexingStatusSnapshotCompleted} + */ +export type SerializedChainIndexingStatusSnapshotCompleted = ChainIndexingStatusSnapshotCompleted; + +/** + * Serialized representation of {@link ChainIndexingStatusSnapshotFollowing} + */ +export type SerializedChainIndexingStatusSnapshotFollowing = ChainIndexingStatusSnapshotFollowing; + +/** + * Serialize chain indexing status snapshots. + */ +export function serializeChainIndexingSnapshots< + ChainIndexingStatusSnapshotType extends ChainIndexingStatusSnapshot, +>( + chains: Map, +): Record { + const serializedSnapshots: Record = {}; + + for (const [chainId, snapshot] of chains.entries()) { + serializedSnapshots[serializeChainId(chainId)] = snapshot; + } + + return serializedSnapshots; +} 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 7ebacd7b7..9ad205592 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialized-types.ts @@ -1,11 +1,11 @@ import type { ChainIdString } from "../../shared/serialized-types"; import type { ChainIndexingStatusSnapshot, - ChainIndexingStatusSnapshotBackfill, ChainIndexingStatusSnapshotCompleted, - ChainIndexingStatusSnapshotFollowing, - ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, ChainIndexingStatusSnapshotQueued, +} from "./chain-indexing-status-snapshot"; +import type { + ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, CrossChainIndexingStatusSnapshot, CrossChainIndexingStatusSnapshotOmnichain, OmnichainIndexingStatusSnapshot, @@ -16,31 +16,6 @@ import type { RealtimeIndexingStatusProjection, } from "./types"; -/** - * Serialized representation of {@link ChainIndexingStatusSnapshot} - */ -export type SerializedChainIndexingStatusSnapshot = ChainIndexingStatusSnapshot; - -/** - * Serialized representation of {@link ChainIndexingStatusSnapshotQueued} - */ -export type SerializedChainIndexingStatusSnapshotQueued = ChainIndexingStatusSnapshotQueued; - -/** - * Serialized representation of {@link ChainIndexingStatusSnapshotBackfill} - */ -export type SerializedChainIndexingStatusSnapshotBackfill = ChainIndexingStatusSnapshotBackfill; - -/** - * Serialized representation of {@link ChainIndexingStatusSnapshotCompleted} - */ -export type SerializedChainIndexingStatusSnapshotCompleted = ChainIndexingStatusSnapshotCompleted; - -/** - * Serialized representation of {@link ChainIndexingStatusSnapshotFollowing} - */ -export type SerializedChainIndexingStatusSnapshotFollowing = ChainIndexingStatusSnapshotFollowing; - /** * Serialized representation of {@link OmnichainIndexingStatusSnapshotUnstarted} */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts index dc6f9c44d..c22be0e05 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts @@ -1,284 +1,12 @@ -import type { BlockRef, ChainId, Duration, UnixTimestamp } from "../../shared/types"; - -/** - * The type of indexing configuration for a chain. - */ -export const ChainIndexingConfigTypeIds = { - /** - * Represents that indexing of the chain should be performed for an indefinite range. - */ - Indefinite: "indefinite", - - /** - * Represents that indexing of the chain should be performed for a definite range. - */ - Definite: "definite", -} as const; - -/** - * The derived string union of possible {@link ChainIndexingConfigTypeIds}. - */ -export type ChainIndexingConfigTypeId = - (typeof ChainIndexingConfigTypeIds)[keyof typeof ChainIndexingConfigTypeIds]; - -/** - * Chain indexing config for a chain whose indexing config `configType` is - * {@link ChainIndexingConfigTypeIds.Indefinite}. - * - * Invariants: - * - `configType` is always `ChainIndexingConfigTypeIds.Indefinite`. - */ -export interface ChainIndexingConfigIndefinite { - /** - * The type of chain indexing config. - */ - configType: typeof ChainIndexingConfigTypeIds.Indefinite; - - /** - * A {@link BlockRef} to the block where indexing of the chain should start. - */ - startBlock: BlockRef; -} - -/** - * Chain indexing config for a chain whose indexing config `configType` is - * {@link ChainIndexingConfigTypeIds.Definite}. - * - * Invariants: - * - `configType` is always `ChainIndexingConfigTypeIds.Definite`. - * - `startBlock` is always before or the same as `endBlock`. - */ -export interface ChainIndexingConfigDefinite { - /** - * The type of chain indexing config. - */ - configType: typeof ChainIndexingConfigTypeIds.Definite; - - /** - * A {@link BlockRef} to the block where indexing of the chain should start. - */ - startBlock: BlockRef; - - /** - * A {@link BlockRef} to the block where indexing of the chain should end. - */ - endBlock: BlockRef; -} - -/** - * Indexing configuration for a chain. - * - * Use the `configType` field to determine the specific type interpretation - * at runtime. - */ -export type ChainIndexingConfig = ChainIndexingConfigIndefinite | ChainIndexingConfigDefinite; - -/** - * The status of indexing a chain at the time an indexing status snapshot - * is captured. - */ -export const ChainIndexingStatusIds = { - /** - * Represents that indexing of the chain is not ready to begin yet because: - * - ENSIndexer is in its initialization phase and the data to build a - * "true" {@link ChainIndexingSnapshot} for the chain is still being loaded; or - * - ENSIndexer is using an omnichain indexing strategy and the - * `omnichainIndexingCursor` is <= `config.startBlock.timestamp` for the chain's - * {@link ChainIndexingSnapshot}. - */ - Queued: "chain-queued", - - /** - * Represents that indexing of the chain is in progress and under a special - * "backfill" phase that optimizes for accelerated indexing until reaching the - * "fixed target" `backfillEndBlock`. - */ - Backfill: "chain-backfill", - - /** - * Represents that the "backfill" phase of indexing the chain is completed - * and that the chain is configured to be indexed for an indefinite range. - * Therefore, indexing of the chain remains indefinitely in progress where - * ENSIndexer will continuously work to discover and index new blocks as they - * are added to the chain across time. - */ - Following: "chain-following", - - /** - * Represents that indexing of the chain is completed as the chain is configured - * to be indexed for a definite range and the indexing of all blocks through - * that definite range is completed. - */ - Completed: "chain-completed", -} as const; - -/** - * The derived string union of possible {@link ChainIndexingStatusIds}. - */ -export type ChainIndexingStatusId = - (typeof ChainIndexingStatusIds)[keyof typeof ChainIndexingStatusIds]; - -/** - * Chain indexing status snapshot for a chain whose `chainStatus` is - * {@link ChainIndexingStatusIds.Queued}. - * - * Invariants: - * - `chainStatus` is always {@link ChainIndexingStatusIds.Queued}. - */ -export interface ChainIndexingStatusSnapshotQueued { - /** - * The status of indexing the chain at the time the indexing status snapshot - * was captured. - */ - chainStatus: typeof ChainIndexingStatusIds.Queued; - - /** - * The indexing configuration of the chain. - */ - config: ChainIndexingConfig; -} - -/** - * Chain indexing status snapshot for a chain whose `chainStatus` is - * {@link ChainIndexingStatusIds.Backfill}. - * - * During a backfill, special performance optimizations are applied to - * index all blocks between `config.startBlock` and `backfillEndBlock` - * as fast as possible. - * - * Note how `backfillEndBlock` is a "fixed target" that does not change during - * the lifetime of an ENSIndexer process instance: - * - If the `config` is {@link ChainIndexingConfigDefinite}: - * `backfillEndBlock` is always the same as `config.endBlock`. - * - If the `config` is {@link ChainIndexingConfigIndefinite}: - * `backfillEndBlock` is a {@link BlockRef} to what was the latest block on the - * chain when the ENSIndexer process was performing its initialization. Note how - * this means that if the backfill process takes X hours to complete, because the - * `backfillEndBlock` is a "fixed target", when `chainStatus` transitions to - * {@link ChainIndexingStatusIds.Following} the chain will be X hours behind - * "realtime" indexing. - * - * When `latestIndexedBlock` reaches `backfillEndBlock` the backfill is complete. - * The moment backfill is complete the `chainStatus` may not immediately transition. - * Instead, internal processing is completed for a period of time while - * `chainStatus` remains {@link ChainIndexingStatusIds.Backfill}. After this internal - * processing is completed `chainStatus` will transition: - * - to {@link ChainIndexingStatusIds.Following} if the `config` is - * {@link ChainIndexingConfigIndefinite}. - * - to {@link ChainIndexingStatusIds.Completed} if the `config` is - * {@link ChainIndexingConfigDefinite}. - * - * Invariants: - * - `chainStatus` is always {@link ChainIndexingStatusIds.Backfill}. - * - `config.startBlock` is always before or the same as `latestIndexedBlock` - * - `config.endBlock` is always the same as `backfillEndBlock` if and only if - * the config is {@link ChainIndexingConfigDefinite}. - * - `latestIndexedBlock` is always before or the same as `backfillEndBlock` - */ -export interface ChainIndexingStatusSnapshotBackfill { - /** - * The status of indexing the chain at the time the indexing status snapshot - * was captured. - */ - chainStatus: typeof ChainIndexingStatusIds.Backfill; - - /** - * The indexing configuration of the chain. - */ - config: ChainIndexingConfig; - - /** - * A {@link BlockRef} to the block that was most recently indexed as of the time the - * indexing status snapshot was captured. - */ - latestIndexedBlock: BlockRef; - - /** - * A {@link BlockRef} to the block where the backfill will end. - */ - backfillEndBlock: BlockRef; -} - -/** - * Chain indexing status snapshot for a chain whose `chainStatus` is - * {@link ChainIndexingStatusIds.Following}. - * - * Invariants: - * - `chainStatus` is always {@link ChainIndexingStatusIds.Following}. - * - `config` is always {@link ChainIndexingConfigIndefinite} - * - `config.startBlock` is always before or the same as `latestIndexedBlock` - * - `latestIndexedBlock` is always before or the same as `latestKnownBlock` - */ -export interface ChainIndexingStatusSnapshotFollowing { - /** - * The status of indexing the chain at the time the indexing status snapshot - * was captured. - */ - chainStatus: typeof ChainIndexingStatusIds.Following; - - /** - * The indexing configuration of the chain. - */ - config: ChainIndexingConfigIndefinite; - - /** - * A {@link BlockRef} to the block that was most recently indexed as of the time the - * indexing status snapshot was captured. - */ - latestIndexedBlock: BlockRef; - - /** - * A {@link BlockRef} to the "highest" block that has been discovered by RPCs - * and stored in the RPC cache as of the time the indexing status snapshot was - * captured. - */ - latestKnownBlock: BlockRef; -} - -/** - * Chain indexing status snapshot for a chain whose `chainStatus` is - * {@link ChainIndexingStatusIds.Completed}. - * - * After the backfill of a chain is completed, if the chain was configured - * to be indexed for a definite range, the chain indexing status will transition to - * {@link ChainIndexingStatusIds.Completed}. - * - * Invariants: - * - `chainStatus` is always {@link ChainIndexingStatusIds.Completed}. - * - `config` is always {@link ChainIndexingConfigDefinite} - * - `config.startBlock` is always before or the same as `latestIndexedBlock` - * - `latestIndexedBlock` is always the same as `config.endBlock`. - */ -export interface ChainIndexingStatusSnapshotCompleted { - /** - * The status of indexing the chain at the time the indexing status snapshot - * was captured. - */ - chainStatus: typeof ChainIndexingStatusIds.Completed; - - /** - * The indexing configuration of the chain. - */ - config: ChainIndexingConfigDefinite; - - /** - * A {@link BlockRef} to the block that was most recently indexed as of the time the - * indexing status snapshot was captured. - */ - latestIndexedBlock: BlockRef; -} - -/** - * Indexing status snapshot for a single chain. - * - * Use the `chainStatus` field to determine the specific type interpretation - * at runtime. - */ -export type ChainIndexingStatusSnapshot = - | ChainIndexingStatusSnapshotQueued - | ChainIndexingStatusSnapshotBackfill - | ChainIndexingStatusSnapshotFollowing - | ChainIndexingStatusSnapshotCompleted; +import type { ChainId, 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 diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..8c1628078 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts @@ -0,0 +1,24 @@ +import { prettifyError } from "zod/v4"; + +import type { Unvalidated } from "../../../shared/types"; +import type { ChainIndexingStatusSnapshot } from "../chain-indexing-status-snapshot"; +import { makeChainIndexingStatusSnapshotSchema } from "../zod-schema/chain-indexing-status-snapshot"; + +/** + * Validates a maybe {@link ChainIndexingStatusSnapshot} object. + * + * @throws Error if the provided object is not a valid {@link ChainIndexingStatusSnapshot}. + */ +export function validateChainIndexingStatusSnapshot( + unvalidatedSnapshot: Unvalidated, + valueLabel?: string, +): ChainIndexingStatusSnapshot { + const schema = makeChainIndexingStatusSnapshotSchema(valueLabel); + const parsed = schema.safeParse(unvalidatedSnapshot); + + if (parsed.error) { + throw new Error(`Invalid ChainIndexingStatusSnapshot:\n${prettifyError(parsed.error)}\n`); + } + + return parsed.data; +} diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts index 843a1d530..42f97e8b7 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/validations.ts @@ -1,7 +1,11 @@ import type { ParsePayload } from "zod/v4/core"; -import * as blockRef from "../../shared/block-ref"; import type { ChainId } from "../../shared/types"; +import { + ChainIndexingConfigTypeIds, + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshot, +} from "./chain-indexing-status-snapshot"; import { checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill, checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted, @@ -9,144 +13,13 @@ import { checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted, getOmnichainIndexingStatus, } from "./helpers"; -import { - ChainIndexingConfigTypeIds, - ChainIndexingStatusIds, - type ChainIndexingStatusSnapshot, - type ChainIndexingStatusSnapshotBackfill, - type ChainIndexingStatusSnapshotCompleted, - type ChainIndexingStatusSnapshotFollowing, - type ChainIndexingStatusSnapshotQueued, - type CrossChainIndexingStatusSnapshotOmnichain, - type OmnichainIndexingStatusSnapshot, - type OmnichainIndexingStatusSnapshotFollowing, - type RealtimeIndexingStatusProjection, +import type { + CrossChainIndexingStatusSnapshotOmnichain, + OmnichainIndexingStatusSnapshot, + OmnichainIndexingStatusSnapshotFollowing, + RealtimeIndexingStatusProjection, } from "./types"; -/** - * Invariants for {@link ChainIndexingSnapshot}. - */ - -/** - * Invariants for chain snapshot in 'queued' status: - * - `config.endBlock` (if set) is after `config.startBlock`. - */ -export function invariant_chainSnapshotQueuedBlocks( - ctx: ParsePayload, -) { - const { config } = ctx.value; - - // The `config.endBlock` does not exists for `indefinite` config type - if (config.configType === ChainIndexingConfigTypeIds.Indefinite) { - // invariant holds - return; - } - - if (config.endBlock && blockRef.isBeforeOrEqualTo(config.startBlock, config.endBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`config.startBlock` must be before or same as `config.endBlock`.", - }); - } -} - -/** - * Invariants for chain snapshot in 'backfill' status: - * - `config.startBlock` is before or same as `latestIndexedBlock`. - * - `latestIndexedBlock` is before or same as `backfillEndBlock`. - * - `backfillEndBlock` is the same as `config.endBlock` (if set). - */ -export function invariant_chainSnapshotBackfillBlocks( - ctx: ParsePayload, -) { - const { config, latestIndexedBlock, backfillEndBlock } = ctx.value; - - if (blockRef.isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`config.startBlock` must be before or same as `latestIndexedBlock`.", - }); - } - - if (blockRef.isBeforeOrEqualTo(latestIndexedBlock, backfillEndBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`latestIndexedBlock` must be before or same as `backfillEndBlock`.", - }); - } - - // The `config.endBlock` does not exists for `indefinite` config type - if (config.configType === ChainIndexingConfigTypeIds.Indefinite) { - // invariant holds - return; - } - - if (config.endBlock && blockRef.isEqualTo(backfillEndBlock, config.endBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`backfillEndBlock` must be the same as `config.endBlock`.", - }); - } -} - -/** - * Invariants for chain snapshot in 'completed' status: - * - `config.startBlock` is before or same as `latestIndexedBlock`. - * - `latestIndexedBlock` is before or same as `config.endBlock`. - */ -export function invariant_chainSnapshotCompletedBlocks( - ctx: ParsePayload, -) { - const { config, latestIndexedBlock } = ctx.value; - - if (blockRef.isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`config.startBlock` must be before or same as `latestIndexedBlock`.", - }); - } - - if (blockRef.isBeforeOrEqualTo(latestIndexedBlock, config.endBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`latestIndexedBlock` must be before or same as `config.endBlock`.", - }); - } -} - -/** - * Invariants for chain snapshot in 'following' status: - * - `config.startBlock` is before or same as `latestIndexedBlock`. - * - `latestIndexedBlock` is before or same as `latestKnownBlock`. - */ -export function invariant_chainSnapshotFollowingBlocks( - ctx: ParsePayload, -) { - const { config, latestIndexedBlock, latestKnownBlock } = ctx.value; - - if (blockRef.isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`config.startBlock` must be before or same as `latestIndexedBlock`.", - }); - } - - if (blockRef.isBeforeOrEqualTo(latestIndexedBlock, latestKnownBlock) === false) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "`latestIndexedBlock` must be before or same as `latestKnownBlock`.", - }); - } -} - /** * Invariants for {@link OmnichainIndexingSnapshot}. */ diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/chain-indexing-status-snapshot.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..737923a51 --- /dev/null +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schema/chain-indexing-status-snapshot.ts @@ -0,0 +1,224 @@ +import { z } from "zod/v4"; +import type { ParsePayload } from "zod/v4/core"; + +import * as blockRef from "../../../shared/block-ref"; +import { makeBlockRefSchema } from "../../../shared/zod-schemas"; +import { + ChainIndexingConfigTypeIds, + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshot, + type ChainIndexingStatusSnapshotBackfill, + type ChainIndexingStatusSnapshotCompleted, + type ChainIndexingStatusSnapshotFollowing, + type ChainIndexingStatusSnapshotQueued, +} from "../chain-indexing-status-snapshot"; +import type { SerializedChainIndexingStatusSnapshot } from "../serialize/chain-indexing-status-snapshot"; + +/** + * Invariants for chain snapshot in 'queued' status: + * - `config.endBlock` (if set) is after `config.startBlock`. + */ +export function invariant_chainSnapshotQueuedBlocks( + ctx: ParsePayload, +) { + const { config } = ctx.value; + + // The `config.endBlock` does not exist for `indefinite` config type + if (config.configType === ChainIndexingConfigTypeIds.Indefinite) { + // invariant holds + return; + } + + if (blockRef.isBeforeOrEqualTo(config.startBlock, config.endBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`config.startBlock` must be before or same as `config.endBlock`.", + }); + } +} + +/** + * Invariants for chain snapshot in 'backfill' status: + * - `config.startBlock` is before or same as `latestIndexedBlock`. + * - `latestIndexedBlock` is before or same as `backfillEndBlock`. + * - `backfillEndBlock` is the same as `config.endBlock` (if set). + */ +export function invariant_chainSnapshotBackfillBlocks( + ctx: ParsePayload, +) { + const { config, latestIndexedBlock, backfillEndBlock } = ctx.value; + + if (blockRef.isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`config.startBlock` must be before or same as `latestIndexedBlock`.", + }); + } + + if (blockRef.isBeforeOrEqualTo(latestIndexedBlock, backfillEndBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`latestIndexedBlock` must be before or same as `backfillEndBlock`.", + }); + } + + // The `config.endBlock` does not exist for `indefinite` config type + if (config.configType === ChainIndexingConfigTypeIds.Indefinite) { + // invariant holds + return; + } + + if (blockRef.isEqualTo(backfillEndBlock, config.endBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`backfillEndBlock` must be the same as `config.endBlock`.", + }); + } +} + +/** + * Invariants for chain snapshot in 'completed' status: + * - `config.startBlock` is before or same as `latestIndexedBlock`. + * - `latestIndexedBlock` is before or same as `config.endBlock`. + */ +export function invariant_chainSnapshotCompletedBlocks( + ctx: ParsePayload, +) { + const { config, latestIndexedBlock } = ctx.value; + + if (blockRef.isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`config.startBlock` must be before or same as `latestIndexedBlock`.", + }); + } + + if (blockRef.isBeforeOrEqualTo(latestIndexedBlock, config.endBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`latestIndexedBlock` must be before or same as `config.endBlock`.", + }); + } +} + +/** + * Invariants for chain snapshot in 'following' status: + * - `config.startBlock` is before or same as `latestIndexedBlock`. + * - `latestIndexedBlock` is before or same as `latestKnownBlock`. + */ +export function invariant_chainSnapshotFollowingBlocks( + ctx: ParsePayload, +) { + const { config, latestIndexedBlock, latestKnownBlock } = ctx.value; + + if (blockRef.isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`config.startBlock` must be before or same as `latestIndexedBlock`.", + }); + } + + if (blockRef.isBeforeOrEqualTo(latestIndexedBlock, latestKnownBlock) === false) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "`latestIndexedBlock` must be before or same as `latestKnownBlock`.", + }); + } +} + +/** + * Makes Zod schema for {@link ChainIndexingConfig} type. + */ +export const makeChainIndexingConfigSchema = (valueLabel: string = "Value") => + z.discriminatedUnion("configType", [ + z.object({ + configType: z.literal(ChainIndexingConfigTypeIds.Indefinite), + startBlock: makeBlockRefSchema(valueLabel), + }), + z.object({ + configType: z.literal(ChainIndexingConfigTypeIds.Definite), + startBlock: makeBlockRefSchema(valueLabel), + endBlock: makeBlockRefSchema(valueLabel), + }), + ]); + +/** + * Makes Zod schema for {@link ChainIndexingStatusSnapshotQueued} type. + */ +export const makeChainIndexingStatusSnapshotQueuedSchema = (valueLabel: string = "Value") => + z + .object({ + chainStatus: z.literal(ChainIndexingStatusIds.Queued), + config: makeChainIndexingConfigSchema(valueLabel), + }) + .check(invariant_chainSnapshotQueuedBlocks); + +/** + * Makes Zod schema for {@link ChainIndexingStatusSnapshotBackfill} type. + */ +export const makeChainIndexingStatusSnapshotBackfillSchema = (valueLabel: string = "Value") => + z + .object({ + chainStatus: z.literal(ChainIndexingStatusIds.Backfill), + config: makeChainIndexingConfigSchema(valueLabel), + latestIndexedBlock: makeBlockRefSchema(valueLabel), + backfillEndBlock: makeBlockRefSchema(valueLabel), + }) + .check(invariant_chainSnapshotBackfillBlocks); + +/** + * Makes Zod schema for {@link ChainIndexingStatusSnapshotCompleted} type. + */ +export const makeChainIndexingStatusSnapshotCompletedSchema = (valueLabel: string = "Value") => + z + .object({ + chainStatus: z.literal(ChainIndexingStatusIds.Completed), + config: z.object({ + configType: z.literal(ChainIndexingConfigTypeIds.Definite), + startBlock: makeBlockRefSchema(valueLabel), + endBlock: makeBlockRefSchema(valueLabel), + }), + latestIndexedBlock: makeBlockRefSchema(valueLabel), + }) + .check(invariant_chainSnapshotCompletedBlocks); + +/** + * Makes Zod schema for {@link ChainIndexingStatusSnapshotFollowing} type. + */ +export const makeChainIndexingStatusSnapshotFollowingSchema = (valueLabel: string = "Value") => + z + .object({ + chainStatus: z.literal(ChainIndexingStatusIds.Following), + config: z.object({ + configType: z.literal(ChainIndexingConfigTypeIds.Indefinite), + startBlock: makeBlockRefSchema(valueLabel), + }), + latestIndexedBlock: makeBlockRefSchema(valueLabel), + latestKnownBlock: makeBlockRefSchema(valueLabel), + }) + .check(invariant_chainSnapshotFollowingBlocks); + +/** + * Makes Zod schema for {@link ChainIndexingStatusSnapshot} + */ +export const makeChainIndexingStatusSnapshotSchema = (valueLabel: string = "Value") => + z.discriminatedUnion("chainStatus", [ + makeChainIndexingStatusSnapshotQueuedSchema(valueLabel), + makeChainIndexingStatusSnapshotBackfillSchema(valueLabel), + makeChainIndexingStatusSnapshotCompletedSchema(valueLabel), + makeChainIndexingStatusSnapshotFollowingSchema(valueLabel), + ]); + +/** + * Makes Zod schema for {@link SerializedChainIndexingStatusSnapshot} + */ +export const makeSerializedChainIndexingStatusSnapshotSchema = + makeChainIndexingStatusSnapshotSchema; diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.test.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.test.ts index 1bffa8c79..e58b6dd31 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from "vitest"; import { prettifyError, type ZodSafeParseResult } from "zod/v4"; -import { earlierBlockRef, earliestBlockRef, laterBlockRef, latestBlockRef } from "./test-helpers"; +import { + earlierBlockRef, + earliestBlockRef, + laterBlockRef, + latestBlockRef, +} from "./block-refs.mock"; import { ChainIndexingConfigTypeIds, ChainIndexingStatusIds, @@ -10,8 +15,8 @@ import { type ChainIndexingStatusSnapshotCompleted, type ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotQueued, -} from "./types"; -import { makeChainIndexingStatusSnapshotSchema } from "./zod-schemas"; +} from "./chain-indexing-status-snapshot"; +import { makeChainIndexingStatusSnapshotSchema } from "./zod-schema/chain-indexing-status-snapshot"; describe("ENSIndexer: Indexing Status", () => { describe("Zod Schemas", () => { 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 3fb80ab5a..a19f2cc1f 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts @@ -11,22 +11,17 @@ import { z } from "zod/v4"; import { deserializeChainId } from "../../shared/deserialize"; import type { ChainId } from "../../shared/types"; import { - makeBlockRefSchema, makeChainIdStringSchema, makeDurationSchema, makeUnixTimestampSchema, } from "../../shared/zod-schemas"; +import type { + ChainIndexingStatusSnapshot, + ChainIndexingStatusSnapshotCompleted, + ChainIndexingStatusSnapshotQueued, +} from "./chain-indexing-status-snapshot"; import { - ChainIndexingConfig, - ChainIndexingConfigTypeIds, - ChainIndexingStatusIds, - type ChainIndexingStatusSnapshot, - ChainIndexingStatusSnapshotBackfill, - type ChainIndexingStatusSnapshotCompleted, - ChainIndexingStatusSnapshotFollowing, type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, - type ChainIndexingStatusSnapshotQueued, - CrossChainIndexingStatusSnapshot, CrossChainIndexingStatusSnapshotOmnichain, CrossChainIndexingStrategyIds, OmnichainIndexingStatusIds, @@ -37,10 +32,6 @@ import { RealtimeIndexingStatusProjection, } from "./types"; import { - invariant_chainSnapshotBackfillBlocks, - invariant_chainSnapshotCompletedBlocks, - invariant_chainSnapshotFollowingBlocks, - invariant_chainSnapshotQueuedBlocks, invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain, invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains, invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains, @@ -53,89 +44,7 @@ import { invariant_slowestChainEqualsToOmnichainSnapshotTime, invariant_snapshotTimeIsTheHighestKnownBlockTimestamp, } from "./validations"; - -/** - * Makes Zod schema for {@link ChainIndexingConfig} type. - */ -const makeChainIndexingConfigSchema = (valueLabel: string = "Value") => - z.discriminatedUnion("configType", [ - z.strictObject({ - configType: z.literal(ChainIndexingConfigTypeIds.Indefinite), - startBlock: makeBlockRefSchema(valueLabel), - }), - z.strictObject({ - configType: z.literal(ChainIndexingConfigTypeIds.Definite), - startBlock: makeBlockRefSchema(valueLabel), - endBlock: makeBlockRefSchema(valueLabel), - }), - ]); - -/** - * Makes Zod schema for {@link ChainIndexingStatusSnapshotQueued} type. - */ -export const makeChainIndexingStatusSnapshotQueuedSchema = (valueLabel: string = "Value") => - z - .strictObject({ - chainStatus: z.literal(ChainIndexingStatusIds.Queued), - config: makeChainIndexingConfigSchema(valueLabel), - }) - .check(invariant_chainSnapshotQueuedBlocks); - -/** - * Makes Zod schema for {@link ChainIndexingStatusSnapshotBackfill} type. - */ -export const makeChainIndexingStatusSnapshotBackfillSchema = (valueLabel: string = "Value") => - z - .strictObject({ - chainStatus: z.literal(ChainIndexingStatusIds.Backfill), - config: makeChainIndexingConfigSchema(valueLabel), - latestIndexedBlock: makeBlockRefSchema(valueLabel), - backfillEndBlock: makeBlockRefSchema(valueLabel), - }) - .check(invariant_chainSnapshotBackfillBlocks); - -/** - * Makes Zod schema for {@link ChainIndexingStatusSnapshotCompleted} type. - */ -export const makeChainIndexingStatusSnapshotCompletedSchema = (valueLabel: string = "Value") => - z - .strictObject({ - chainStatus: z.literal(ChainIndexingStatusIds.Completed), - config: z.strictObject({ - configType: z.literal(ChainIndexingConfigTypeIds.Definite), - startBlock: makeBlockRefSchema(valueLabel), - endBlock: makeBlockRefSchema(valueLabel), - }), - latestIndexedBlock: makeBlockRefSchema(valueLabel), - }) - .check(invariant_chainSnapshotCompletedBlocks); - -/** - * Makes Zod schema for {@link ChainIndexingStatusSnapshotFollowing} type. - */ -export const makeChainIndexingStatusSnapshotFollowingSchema = (valueLabel: string = "Value") => - z - .strictObject({ - chainStatus: z.literal(ChainIndexingStatusIds.Following), - config: z.strictObject({ - configType: z.literal(ChainIndexingConfigTypeIds.Indefinite), - startBlock: makeBlockRefSchema(valueLabel), - }), - latestIndexedBlock: makeBlockRefSchema(valueLabel), - latestKnownBlock: makeBlockRefSchema(valueLabel), - }) - .check(invariant_chainSnapshotFollowingBlocks); - -/** - * Makes Zod schema for {@link ChainIndexingStatusSnapshot} - */ -export const makeChainIndexingStatusSnapshotSchema = (valueLabel: string = "Value") => - z.discriminatedUnion("chainStatus", [ - makeChainIndexingStatusSnapshotQueuedSchema(valueLabel), - makeChainIndexingStatusSnapshotBackfillSchema(valueLabel), - makeChainIndexingStatusSnapshotCompletedSchema(valueLabel), - makeChainIndexingStatusSnapshotFollowingSchema(valueLabel), - ]); +import { makeChainIndexingStatusSnapshotSchema } from "./zod-schema/chain-indexing-status-snapshot"; /** * Makes Zod schema for {@link ChainIndexingStatusSnapshot} per chain. diff --git a/packages/ensnode-sdk/src/shared/types.ts b/packages/ensnode-sdk/src/shared/types.ts index 7a35434fc..5121c5ce6 100644 --- a/packages/ensnode-sdk/src/shared/types.ts +++ b/packages/ensnode-sdk/src/shared/types.ts @@ -166,6 +166,37 @@ export type DeepPartial = { : T[P]; }; +/** + * Helper type to represent an unvalidated version of a business layer type `T`, + * where all properties are optional. + * + * This is useful for building a validated object `T` from partial input, + * where the input may be missing required fields or have fields that + * are not yet validated. + * + * For example, transforming serialized representation of type `T` into + * an unvalidated version of `T` that can be later validated against + * defined business rules and constraints. + * + * ```ts + * function buildUnvalidatedValue(serialized: SerializedChainId): Unvalidated { + * // transform serialized chainId into unvalidated number (e.g. parseInt) + * return parseInt(serialized, 10); + * } + * + * // Later, we can validate the unvalidated value against our business rules + * function validateChainId(unvalidatedChainId: Unvalidated): ChainId { + * if (typeof unvalidatedChainId !== "number" || unvalidatedChainId <= 0) { + * throw new Error("Invalid ChainId"); + * } + * + * return unvalidatedChainId as ChainId; + * } + * + * ``` + */ +export type Unvalidated = DeepPartial; + /** * Marks keys in K as required (not undefined) and not null. */