From a561135e160e231c355c9ded0970e7a9e40403bc Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 22 Dec 2025 17:24:54 +0100 Subject: [PATCH 1/2] refactor(ensindexer): use local ponder client Local Ponder Client is a point of integration between ENSIndexer and Ponder applications. It uses Ponder SDK to interact with Ponder APIs. --- .../chain-indexing-status-snapshot.ts | 167 ++++++ .../ponder => ponder/local-client}/config.ts | 0 apps/ensindexer/ponder/local-client/index.ts | 1 + .../local-client/local-ponder-client.ts | 93 ++++ .../omnichain-indexing-status-snapshot.ts | 70 +++ apps/ensindexer/ponder/ponder.config.ts | 3 +- .../ponder/src/api/handlers/ensnode-api.ts | 3 +- .../lib/indexing-status/build-index-status.ts | 103 +--- .../ensindexer/src/lib/ponder-local-client.ts | 27 - .../ensnode-sdk/src/shared/deserialize.ts | 5 +- packages/ensnode-sdk/src/shared/numbers.ts | 32 ++ packages/ponder-sdk/package.json | 7 +- packages/ponder-sdk/src/block-refs.ts | 94 +++- packages/ponder-sdk/src/chains.test.ts | 475 ------------------ packages/ponder-sdk/src/chains.ts | 236 +-------- packages/ponder-sdk/src/client.ts | 89 +--- packages/ponder-sdk/src/config.ts | 39 +- packages/ponder-sdk/src/index.ts | 2 + .../src/metrics/validate-ponder-metrics.ts | 19 +- packages/ponder-sdk/src/response.ts | 5 +- packages/ponder-sdk/src/rpc.ts | 10 +- packages/ponder-sdk/src/shared.ts | 45 ++ packages/ponder-sdk/src/time.ts | 10 + packages/ponder-sdk/src/validations.ts | 50 -- packages/ponder-sdk/src/zod-schemas.ts | 172 ++++--- pnpm-lock.yaml | 3 - 26 files changed, 685 insertions(+), 1075 deletions(-) create mode 100644 apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts rename apps/ensindexer/{src/ponder => ponder/local-client}/config.ts (100%) create mode 100644 apps/ensindexer/ponder/local-client/index.ts create mode 100644 apps/ensindexer/ponder/local-client/local-ponder-client.ts create mode 100644 apps/ensindexer/ponder/local-client/omnichain-indexing-status-snapshot.ts delete mode 100644 apps/ensindexer/src/lib/ponder-local-client.ts delete mode 100644 packages/ponder-sdk/src/chains.test.ts create mode 100644 packages/ponder-sdk/src/shared.ts create mode 100644 packages/ponder-sdk/src/time.ts delete mode 100644 packages/ponder-sdk/src/validations.ts diff --git a/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts b/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts new file mode 100644 index 000000000..e165a15f5 --- /dev/null +++ b/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts @@ -0,0 +1,167 @@ +import { + type ChainIdString, + ChainIndexingConfigTypeIds, + ChainIndexingStatusIds, + type ChainIndexingStatusSnapshot, + createIndexingConfig, + deserializeBlockRef, + deserializeChainId, + deserializeChainIndexingStatusSnapshot, + deserializeNonNegativeInteger, + type SerializedChainIndexingStatusSnapshot, + type SerializedChainIndexingStatusSnapshotBackfill, + type SerializedChainIndexingStatusSnapshotCompleted, + type SerializedChainIndexingStatusSnapshotFollowing, + type SerializedChainIndexingStatusSnapshotQueued, +} from "@ensnode/ensnode-sdk"; +import type { + ChainBlockRefs, + ChainMetadata, + ChainName, + PonderMetricsResponse, + PonderStatusResponse, + UnvalidatedChainMetadata, +} from "@ensnode/ponder-sdk"; + +/** + * Create {@link ChainIndexingStatusSnapshot} for the indexed chain metadata. + */ +export function createChainIndexingSnapshot( + chainMetadata: ChainMetadata, +): ChainIndexingStatusSnapshot { + const { + config: chainBlocksConfig, + backfillEndBlock: chainBackfillEndBlock, + isSyncComplete, + isSyncRealtime, + syncBlock: chainSyncBlock, + statusBlock: chainStatusBlock, + } = chainMetadata; + + const { startBlock, endBlock } = chainBlocksConfig; + const config = createIndexingConfig(startBlock, endBlock); + + // In omnichain ordering, if the startBlock is the same as the + // status block, the chain has not started yet. + if (chainBlocksConfig.startBlock.number === chainStatusBlock.number) { + return deserializeChainIndexingStatusSnapshot({ + chainStatus: ChainIndexingStatusIds.Queued, + config, + } satisfies SerializedChainIndexingStatusSnapshotQueued); + } + + if (isSyncComplete) { + if (config.configType !== ChainIndexingConfigTypeIds.Definite) { + throw new Error( + `The '${ChainIndexingStatusIds.Completed}' indexing status can be only created with the '${ChainIndexingConfigTypeIds.Definite}' indexing config type.`, + ); + } + + return deserializeChainIndexingStatusSnapshot({ + chainStatus: ChainIndexingStatusIds.Completed, + latestIndexedBlock: chainStatusBlock, + config, + } satisfies SerializedChainIndexingStatusSnapshotCompleted); + } + + if (isSyncRealtime) { + if (config.configType !== ChainIndexingConfigTypeIds.Indefinite) { + throw new Error( + `The '${ChainIndexingStatusIds.Following}' indexing status can be only created with the '${ChainIndexingConfigTypeIds.Indefinite}' indexing config type.`, + ); + } + + return deserializeChainIndexingStatusSnapshot({ + chainStatus: ChainIndexingStatusIds.Following, + latestIndexedBlock: chainStatusBlock, + latestKnownBlock: chainSyncBlock, + config: { + configType: config.configType, + startBlock: config.startBlock, + }, + } satisfies SerializedChainIndexingStatusSnapshotFollowing); + } + + return deserializeChainIndexingStatusSnapshot({ + chainStatus: ChainIndexingStatusIds.Backfill, + latestIndexedBlock: chainStatusBlock, + backfillEndBlock: chainBackfillEndBlock, + config, + } satisfies SerializedChainIndexingStatusSnapshotBackfill); +} + +/** + * Create serialized chain indexing snapshots. + * + * The output of this function is required for + * calling {@link createOmnichainIndexingSnapshot}. + */ +export function createSerializedChainSnapshots( + chainIds: ChainIdString[], + chainsBlockRefs: Map, + metrics: PonderMetricsResponse, + status: PonderStatusResponse, +): Record { + const serializedChainIndexingStatusSnapshots = {} as Record< + ChainIdString, + ChainIndexingStatusSnapshot + >; + + // collect unvalidated chain metadata for each indexed chain + for (const chainId of chainIds) { + const chainBlockRefs = chainsBlockRefs.get(chainId); + + const statusChainId = deserializeChainId(`${status[chainId]?.id}`); + + const backfillEndBlock = deserializeBlockRef(chainBlockRefs?.backfillEndBlock); + + const syncBlock = deserializeBlockRef({ + number: metrics.getValue("ponder_sync_block", { chain: chainId }), + timestamp: metrics.getValue("ponder_sync_block_timestamp", { chain: chainId }), + }); + + const statusBlock = deserializeBlockRef({ + number: status[chainId]?.block.number, + timestamp: status[chainId]?.block.timestamp, + }); + + const historicalTotalBlocks = deserializeNonNegativeInteger( + metrics.getValue("ponder_historical_total_blocks", { + chain: chainId, + }), + ); + + const isSyncComplete = metrics.getValue("ponder_sync_is_complete", { chain: chainId }); + + const isSyncRealtime = metrics.getValue("ponder_sync_is_realtime", { chain: chainId }); + + if (typeof isSyncRealtime === "string" && !["0", "1"].includes(isSyncRealtime)) { + throw new Error( + `The 'ponder_sync_is_realtime' metric for chain '${chainId}' must be a string with value "0" or "1".`, + ); + } + + const config = { + startBlock: deserializeBlockRef(chainBlockRefs?.config.startBlock), + endBlock: + chainBlockRefs?.config.endBlock === null + ? null + : deserializeBlockRef(chainBlockRefs?.config.endBlock), + }; + + const chainMetadata = { + chainId: statusChainId, + isSyncComplete: String(isSyncComplete) === "1", + isSyncRealtime: String(isSyncRealtime) === "1", + config, + backfillEndBlock, + historicalTotalBlocks, + syncBlock, + statusBlock, + } satisfies UnvalidatedChainMetadata; + + serializedChainIndexingStatusSnapshots[chainId] = createChainIndexingSnapshot(chainMetadata); + } + + return serializedChainIndexingStatusSnapshots; +} diff --git a/apps/ensindexer/src/ponder/config.ts b/apps/ensindexer/ponder/local-client/config.ts similarity index 100% rename from apps/ensindexer/src/ponder/config.ts rename to apps/ensindexer/ponder/local-client/config.ts diff --git a/apps/ensindexer/ponder/local-client/index.ts b/apps/ensindexer/ponder/local-client/index.ts new file mode 100644 index 000000000..bb0b47fdd --- /dev/null +++ b/apps/ensindexer/ponder/local-client/index.ts @@ -0,0 +1 @@ +export * from "./local-ponder-client"; diff --git a/apps/ensindexer/ponder/local-client/local-ponder-client.ts b/apps/ensindexer/ponder/local-client/local-ponder-client.ts new file mode 100644 index 000000000..3532e091a --- /dev/null +++ b/apps/ensindexer/ponder/local-client/local-ponder-client.ts @@ -0,0 +1,93 @@ +import { publicClients } from "ponder:api"; +import type { PublicClient } from "viem"; + +import { + deserializeOmnichainIndexingStatusSnapshot, + type OmnichainIndexingStatusSnapshot, +} from "@ensnode/ensnode-sdk"; +import { + buildHistoricalTotalBlocksForChains, + type ChainBlockRefs, + type ChainName, + getChainsBlockRefs, + getChainsBlockrange, + type PonderClient, + type PonderMetricsResponse, +} from "@ensnode/ponder-sdk"; + +import { createSerializedChainSnapshots } from "./chain-indexing-status-snapshot"; +import ponderConfig from "./config"; +import { createSerializedOmnichainIndexingStatusSnapshot } from "./omnichain-indexing-status-snapshot"; + +export class LocalPonderClient { + /** + * Cached Chain Block Refs + * + * {@link ChainBlockRefs} for each indexed chain. + */ + private chainsBlockRefs = new Map(); + + constructor(private readonly ponderClient: PonderClient) {} + + public async buildCrossChainIndexingStatusSnapshot(): Promise { + const [metrics, status] = await Promise.all([ + this.ponderClient.metrics(), + this.ponderClient.status(), + ]); + + const chainsBlockRefs = await this.getChainsBlockRefsCached(metrics); + + // create serialized chain indexing snapshot for each indexed chain + const serializedChainSnapshots = createSerializedChainSnapshots( + this.indexedChainNames, + chainsBlockRefs, + metrics, + status, + ); + + const serializedOmnichainSnapshot = + createSerializedOmnichainIndexingStatusSnapshot(serializedChainSnapshots); + + return deserializeOmnichainIndexingStatusSnapshot(serializedOmnichainSnapshot); + } + + /** + * Get cached {@link IndexedChainBlockRefs} for indexed chains. + * + * Guaranteed to include {@link ChainBlockRefs} for each indexed chain. + * + * Note: performs a network request only once and caches response to + * re-use it for further `getChainsBlockRefs` calls. + * + * @throws when RPC calls fail or data model invariants are not met. + */ + private async getChainsBlockRefsCached( + metrics: PonderMetricsResponse, + ): Promise> { + // early-return the cached chain block refs + if (this.chainsBlockRefs.size > 0) { + return this.chainsBlockRefs; + } + + this.chainsBlockRefs = await getChainsBlockRefs( + this.indexedChainNames, + getChainsBlockrange(this.ponderConfig), + buildHistoricalTotalBlocksForChains(this.indexedChainNames, metrics), + this.publicClients, + ); + + return this.chainsBlockRefs; + } + + private get ponderConfig() { + return ponderConfig; + } + + private get publicClients(): Record { + return publicClients; + } + + private get indexedChainNames(): ChainName[] { + return Object.keys(this.ponderConfig.chains) as ChainName[]; + } +} diff --git a/apps/ensindexer/ponder/local-client/omnichain-indexing-status-snapshot.ts b/apps/ensindexer/ponder/local-client/omnichain-indexing-status-snapshot.ts new file mode 100644 index 000000000..4a28f4c87 --- /dev/null +++ b/apps/ensindexer/ponder/local-client/omnichain-indexing-status-snapshot.ts @@ -0,0 +1,70 @@ +import { + type ChainIdString, + type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, + getOmnichainIndexingCursor, + getOmnichainIndexingStatus, + OmnichainIndexingStatusIds, + type SerializedChainIndexingStatusSnapshot, + type SerializedChainIndexingStatusSnapshotCompleted, + type SerializedChainIndexingStatusSnapshotQueued, + type SerializedOmnichainIndexingStatusSnapshot, + type SerializedOmnichainIndexingStatusSnapshotBackfill, + type SerializedOmnichainIndexingStatusSnapshotCompleted, + type SerializedOmnichainIndexingStatusSnapshotFollowing, + type SerializedOmnichainIndexingStatusSnapshotUnstarted, +} from "@ensnode/ensnode-sdk"; + +/** + * Create Serialized Omnichain Indexing Snapshot + * + * Creates {@link SerializedOmnichainIndexingStatusSnapshot} from serialized chain snapshots. + */ +export function createSerializedOmnichainIndexingStatusSnapshot( + serializedChainSnapshots: Record, +): SerializedOmnichainIndexingStatusSnapshot { + const chains = Object.values(serializedChainSnapshots); + const omnichainStatus = getOmnichainIndexingStatus(chains); + const omnichainIndexingCursor = getOmnichainIndexingCursor(chains); + + switch (omnichainStatus) { + case OmnichainIndexingStatusIds.Unstarted: { + return { + omnichainStatus: OmnichainIndexingStatusIds.Unstarted, + chains: serializedChainSnapshots as Record< + ChainIdString, + SerializedChainIndexingStatusSnapshotQueued + >, // forcing the type here, will be validated in the following 'check' step + omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotUnstarted; + } + + case OmnichainIndexingStatusIds.Backfill: { + return { + omnichainStatus: OmnichainIndexingStatusIds.Backfill, + chains: serializedChainSnapshots as Record< + ChainIdString, + ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill + >, // forcing the type here, will be validated in the following 'check' step + omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotBackfill; + } + + case OmnichainIndexingStatusIds.Completed: { + return { + omnichainStatus: OmnichainIndexingStatusIds.Completed, + chains: serializedChainSnapshots as Record< + ChainIdString, + SerializedChainIndexingStatusSnapshotCompleted + >, // forcing the type here, will be validated in the following 'check' step + omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotCompleted; + } + + case OmnichainIndexingStatusIds.Following: + return { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: serializedChainSnapshots, + omnichainIndexingCursor, + } satisfies SerializedOmnichainIndexingStatusSnapshotFollowing; + } +} diff --git a/apps/ensindexer/ponder/ponder.config.ts b/apps/ensindexer/ponder/ponder.config.ts index 017860f1b..ff9823430 100644 --- a/apps/ensindexer/ponder/ponder.config.ts +++ b/apps/ensindexer/ponder/ponder.config.ts @@ -3,7 +3,8 @@ import config from "@/config"; import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; import { redactENSIndexerConfig } from "@/config/redact"; -import ponderConfig from "@/ponder/config"; + +import ponderConfig from "./local-client/config"; //////// // Log redacted ENSIndexerConfig for debugging. diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index e45f9844f..c85aaa863 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -1,6 +1,5 @@ import config from "@/config"; -import { publicClients } from "ponder:api"; import { getUnixTime } from "date-fns"; import { Hono } from "hono"; @@ -38,7 +37,7 @@ app.get("/indexing-status", async (c) => { let omnichainSnapshot: OmnichainIndexingStatusSnapshot | undefined; try { - omnichainSnapshot = await buildOmnichainIndexingStatusSnapshot(publicClients); + omnichainSnapshot = await buildOmnichainIndexingStatusSnapshot(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; console.error(`Omnichain snapshot is currently not available: ${errorMessage}`); diff --git a/apps/ensindexer/src/lib/indexing-status/build-index-status.ts b/apps/ensindexer/src/lib/indexing-status/build-index-status.ts index 18beac466..270b7d72b 100644 --- a/apps/ensindexer/src/lib/indexing-status/build-index-status.ts +++ b/apps/ensindexer/src/lib/indexing-status/build-index-status.ts @@ -15,110 +15,17 @@ import config from "@/config"; import { type CrossChainIndexingStatusSnapshotOmnichain, CrossChainIndexingStrategyIds, - deserializeOmnichainIndexingStatusSnapshot, type OmnichainIndexingStatusSnapshot, type UnixTimestamp, } from "@ensnode/ensnode-sdk"; -import { - type ChainBlockRefs, - type ChainName, - createSerializedChainSnapshots, - createSerializedOmnichainIndexingStatusSnapshot, - getChainsBlockRefs, - getChainsBlockrange, - type PonderMetricsResponse, - type PonderStatusResponse, - type PublicClient, -} from "@ensnode/ponder-sdk"; - -import { ponderClient, waitForPonderApplicationToBecomeHealthy } from "@/lib/ponder-local-client"; -import ponderConfig from "@/ponder/config"; - -/** - * Stringified chain IDs for each indexed chain - */ -const chainIds = Object.keys(ponderConfig.chains) as string[]; - -/** - * A {@link Blockrange} for each indexed chain. - * - * Invariants: - * - every chain include a startBlock, - * - some chains may include an endBlock, - * - all present startBlock and endBlock values are valid {@link BlockNumber} values. - */ -const chainsBlockrange = getChainsBlockrange(ponderConfig); - -/** - * Chain Block Refs - * - * {@link ChainBlockRefs} for each indexed chain. - * - * Note: works as cache for {@link getChainsBlockRefs}. - */ -let chainsBlockRefs = new Map(); - -/** - * Get cached {@link IndexedChainBlockRefs} for indexed chains. - * - * Guaranteed to include {@link ChainBlockRefs} for each indexed chain. - * - * Note: performs a network request only once and caches response to - * re-use it for further `getChainsBlockRefs` calls. - */ -async function getChainsBlockRefsCached( - metrics: PonderMetricsResponse, - publicClients: Record, -): Promise> { - // early-return the cached chain block refs - if (chainsBlockRefs.size > 0) { - return chainsBlockRefs; - } - - chainsBlockRefs = await getChainsBlockRefs(chainIds, chainsBlockrange, metrics, publicClients); - - return chainsBlockRefs; -} - -export async function buildOmnichainIndexingStatusSnapshot( - publicClients: Record, -): Promise { - await waitForPonderApplicationToBecomeHealthy; - - let metrics: PonderMetricsResponse; - let status: PonderStatusResponse; - - try { - // Get current Ponder metadata (metrics, status) - const [ponderMetrics, ponderStatus] = await Promise.all([ - ponderClient.metrics(), - ponderClient.status(), - ]); - - metrics = ponderMetrics; - status = ponderStatus; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; - throw new Error( - `Could not fetch data from ENSIndexer at ${config.ensIndexerUrl.href}: ${errorMessage}.`, - ); - } - - // get BlockRefs for relevant blocks - const chainsBlockRefs = await getChainsBlockRefsCached(metrics, publicClients); +import { PonderClient } from "@ensnode/ponder-sdk"; - // create serialized chain indexing snapshot for each indexed chain - const serializedChainSnapshots = createSerializedChainSnapshots( - chainIds, - chainsBlockRefs, - metrics, - status, - ); +import { LocalPonderClient } from "../../../ponder/local-client"; - const serializedOmnichainSnapshot = - createSerializedOmnichainIndexingStatusSnapshot(serializedChainSnapshots); +const localPonderClient = new LocalPonderClient(new PonderClient(config.ensIndexerUrl)); - return deserializeOmnichainIndexingStatusSnapshot(serializedOmnichainSnapshot); +export async function buildOmnichainIndexingStatusSnapshot(): Promise { + return localPonderClient.buildCrossChainIndexingStatusSnapshot(); } export function createCrossChainIndexingStatusSnapshotOmnichain( diff --git a/apps/ensindexer/src/lib/ponder-local-client.ts b/apps/ensindexer/src/lib/ponder-local-client.ts deleted file mode 100644 index 81cba8e13..000000000 --- a/apps/ensindexer/src/lib/ponder-local-client.ts +++ /dev/null @@ -1,27 +0,0 @@ -import config from "@/config"; - -import pRetry from "p-retry"; - -import { PonderClient, PonderHealthCheckResults } from "@ensnode/ponder-sdk"; - -/** - * How many times retries should be attempted before - * {@link waitForPonderApplicationToBecomeHealthy} becomes - * a rejected promise. - */ -export const MAX_PONDER_APPLICATION_HEALTHCHECK_ATTEMPTS = 5; - -export const ponderClient = new PonderClient(config.ensIndexerUrl); - -export const waitForPonderApplicationToBecomeHealthy = pRetry( - async () => { - const response = await ponderClient.health(); - - if (response !== PonderHealthCheckResults.Ok) { - throw new Error("Ponder application is not healthy yet"); - } - }, - { - retries: MAX_PONDER_APPLICATION_HEALTHCHECK_ATTEMPTS, - }, -); diff --git a/packages/ensnode-sdk/src/shared/deserialize.ts b/packages/ensnode-sdk/src/shared/deserialize.ts index 44d73ed7f..d8d02b664 100644 --- a/packages/ensnode-sdk/src/shared/deserialize.ts +++ b/packages/ensnode-sdk/src/shared/deserialize.ts @@ -88,10 +88,7 @@ export function deserializeBlockrange(maybeBlockrange: Partial, valu return parsed.data; } -export function deserializeBlockRef( - maybeBlockRef: Partial, - valueLabel?: string, -): BlockRef { +export function deserializeBlockRef(maybeBlockRef: unknown, valueLabel?: string): BlockRef { const schema = makeBlockRefSchema(valueLabel); const parsed = schema.safeParse(maybeBlockRef); diff --git a/packages/ensnode-sdk/src/shared/numbers.ts b/packages/ensnode-sdk/src/shared/numbers.ts index b37f0a328..ee790e2d8 100644 --- a/packages/ensnode-sdk/src/shared/numbers.ts +++ b/packages/ensnode-sdk/src/shared/numbers.ts @@ -1,3 +1,7 @@ +import { prettifyError } from "zod/v4"; + +import { makeNonNegativeIntegerSchema, makePositiveIntegerSchema } from "./zod-schemas"; + /** * Converts a bigint value into a number value. * @@ -19,3 +23,31 @@ export function bigIntToNumber(n: bigint): number { return Number(n); } + +export function deserializeNonNegativeInteger( + maybePositiveInteger: unknown, + valueLabel?: string, +): number { + const schema = makeNonNegativeIntegerSchema(valueLabel); + const parsed = schema.safeParse(maybePositiveInteger); + + if (parsed.error) { + throw new Error(`Cannot deserialize Positive Integer:\n${prettifyError(parsed.error)}\n`); + } + + return parsed.data; +} + +export function deserializePositiveInteger( + maybePositiveInteger: unknown, + valueLabel?: string, +): number { + const schema = makePositiveIntegerSchema(valueLabel); + const parsed = schema.safeParse(maybePositiveInteger); + + if (parsed.error) { + throw new Error(`Cannot deserialize Positive Integer:\n${prettifyError(parsed.error)}\n`); + } + + return parsed.data; +} diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index da3be6773..5b005c1e7 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -7,11 +7,10 @@ "repository": { "type": "git", "url": "git+https://github.com/namehash/ensnode.git", - "directory": "packages/ponder-metadata-api" + "directory": "packages/ponder-sdk" }, - "homepage": "https://github.com/namehash/ensnode/tree/main/packages/ponder-metadata-api", + "homepage": "https://github.com/namehash/ensnode/tree/main/packages/ponder-sdk", "keywords": [ - "ENSNode", "Ponder" ], "files": [ @@ -49,7 +48,6 @@ "parse-prometheus-text-format": "^1.1.1" }, "devDependencies": { - "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/shared-configs": "workspace:*", "@types/node": "catalog:", "ponder": "catalog:", @@ -60,7 +58,6 @@ "zod": "catalog:" }, "peerDependencies": { - "@ensnode/ensnode-sdk": "workspace:*", "ponder": "catalog:", "viem": "catalog:", "zod": "catalog:" diff --git a/packages/ponder-sdk/src/block-refs.ts b/packages/ponder-sdk/src/block-refs.ts index 72b6f745e..8ca311616 100644 --- a/packages/ponder-sdk/src/block-refs.ts +++ b/packages/ponder-sdk/src/block-refs.ts @@ -5,11 +5,48 @@ * based on configured chain names, chains blockranges, and RPC calls. */ -import type { BlockRef, Blockrange, ChainIdString } from "@ensnode/ensnode-sdk"; +import type { PublicClient } from "viem"; -import type { ChainName } from "./config"; import type { PrometheusMetrics } from "./metrics"; -import { fetchBlockRef, type PublicClient } from "./rpc"; +import { fetchBlockRef } from "./rpc"; +import type { ChainName } from "./shared"; +import type { UnixTimestamp } from "./time"; + +/** + * Block Number + * + * Guaranteed to be a non-negative integer. + */ +export type BlockNumber = number; + +/** + * BlockRef + * + * Describes a block. + * + * We use parameter types to maintain fields layout and documentation across + * the domain model and its serialized counterpart. + */ +export interface BlockRef { + /** Block number (height) */ + number: BlockNumber; + + /** Block timestamp */ + timestamp: UnixTimestamp; +} + +/** + * Block range + * + * Represents a range of blocks + */ +export interface Blockrange { + /** Start block number */ + startBlock?: BlockType; + + /** End block number */ + endBlock?: BlockType; +} /** * Chain Block Refs @@ -36,36 +73,40 @@ export interface ChainBlockRefs { * Get {@link IndexedChainBlockRefs} for indexed chains. * * Guaranteed to include {@link ChainBlockRefs} for each indexed chain. + * + * @throws Error if prerequisites are not met: + * - no startBlock found for a chain, + * - no PublicClient found for a chain, + * - no historical total blocks metric found for a chain, + * - could not get BlockRefs for a chain. */ export async function getChainsBlockRefs( - chainIds: ChainIdString[], + chainNames: ChainName[], chainsBlockrange: Record, - metrics: PrometheusMetrics, + historicalTotalBlocksForChains: Record, publicClients: Record, ): Promise> { const chainsBlockRefs = new Map(); - for (const chainId of chainIds) { - const blockrange = chainsBlockrange[chainId]; + for (const chainName of chainNames) { + const blockrange = chainsBlockrange[chainName]; const startBlock = blockrange?.startBlock; const endBlock = blockrange?.endBlock; - const publicClient = publicClients[chainId]; + const publicClient = publicClients[chainName]; if (typeof startBlock !== "number") { - throw new Error(`startBlock not found for chain ${chainId}`); + throw new Error(`startBlock not found for chain ${chainName}`); } if (typeof publicClient === "undefined") { - throw new Error(`publicClient not found for chain ${chainId}`); + throw new Error(`publicClient not found for chain ${chainName}`); } - const historicalTotalBlocks = metrics.getValue("ponder_historical_total_blocks", { - chain: chainId, - }); + const historicalTotalBlocks = historicalTotalBlocksForChains[chainName]; if (typeof historicalTotalBlocks !== "number") { - throw new Error(`No historical total blocks metric found for chain ${chainId}`); + throw new Error(`No historical total blocks metric found for chain: ${chainName}`); } const backfillEndBlock = startBlock + historicalTotalBlocks - 1; @@ -86,11 +127,32 @@ export async function getChainsBlockRefs( backfillEndBlock: backfillEndBlockRef, } satisfies ChainBlockRefs; - chainsBlockRefs.set(chainId, chainBlockRef); + chainsBlockRefs.set(chainName, chainBlockRef); } catch { - throw new Error(`Could not get BlockRefs for chain ${chainId}`); + throw new Error(`Could not get BlockRefs for chain ${chainName}`); } } return chainsBlockRefs; } + +export function buildHistoricalTotalBlocksForChains( + indexedChainNames: ChainName[], + metrics: PrometheusMetrics, +): Record { + const historicalTotalBlocksForChains = {} as Record; + + for (const chainName of indexedChainNames) { + const historicalTotalBlocksMetric = metrics.getValue("ponder_historical_total_blocks", { + chain: chainName, + }); + + if (typeof historicalTotalBlocksMetric !== "number") { + throw new Error(`No historical total blocks metric found for chain: ${chainName}`); + } + + historicalTotalBlocksForChains[chainName] = historicalTotalBlocksMetric; + } + + return historicalTotalBlocksForChains; +} diff --git a/packages/ponder-sdk/src/chains.test.ts b/packages/ponder-sdk/src/chains.test.ts deleted file mode 100644 index 21e1de4ca..000000000 --- a/packages/ponder-sdk/src/chains.test.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - type BlockRef, - ChainIndexingConfigTypeIds, - ChainIndexingStatusIds, - type ChainIndexingStatusSnapshotBackfill, - type ChainIndexingStatusSnapshotCompleted, - type ChainIndexingStatusSnapshotFollowing, - type ChainIndexingStatusSnapshotQueued, -} from "@ensnode/ensnode-sdk"; - -import { type ChainMetadata, createChainIndexingSnapshot } from "./chains"; -import { getChainsBlockrange, type PonderConfigType } from "./config"; - -// Minimal helpers to simulate BlockRef -const blockRef = (number: number, timestamp: number = 0): BlockRef => ({ number, timestamp }); - -describe("getChainsBlockrange", () => { - it("allows endBlock if all datasources for a chain define their respective endBlock", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - }, - accounts: {}, - contracts: { - "subgraph/Registrar": { - chain: { - mainnet: { address: "0x1", startBlock: 444_444_444, endBlock: 999_999_990 }, - }, - }, - "subgraph/Registry": { - chain: { - mainnet: { address: "0x2", startBlock: 444_444_333, endBlock: 999_999_991 }, - }, - }, - "subgraph/UpgradableRegistry": { - chain: { - mainnet: { address: "0x2", startBlock: 444_555_333, endBlock: 999_999_999 }, - }, - }, - }, - blocks: {}, - } satisfies PonderConfigType; - - // act - const result = getChainsBlockrange(ponderConfig); - - // assert - expect(result).toStrictEqual({ - mainnet: { startBlock: 444_444_333, endBlock: 999_999_999 }, - }); - }); - - it("does not allow endBlock if any datasource for a chain does not define its respective endBlock", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - }, - accounts: {}, - contracts: { - "subgraph/Registrar": { - chain: { - mainnet: { address: "0x1", startBlock: 444_444_444, endBlock: 999_999_990 }, - }, - }, - "subgraph/Registry": { - chain: { - mainnet: { address: "0x2", startBlock: 444_444_333 }, - }, - }, - "subgraph/UpgradableRegistry": { - chain: { - mainnet: { address: "0x2", startBlock: 444_555_333, endBlock: 999_999_999 }, - }, - }, - }, - blocks: {}, - } satisfies PonderConfigType; - - // act - const result = getChainsBlockrange(ponderConfig); - - // assert - expect(result).toStrictEqual({ - mainnet: { startBlock: 444_444_333, endBlock: undefined }, - }); - }); - - it("picks lowest startBlock and highest endBlock across all datasources for each chain", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - }, - accounts: { - "vitalik.eth": { chain: "mainnet", address: "0x1", startBlock: 100, endBlock: 200 }, - "nick.eth": { chain: "mainnet", address: "0x2", startBlock: 50, endBlock: 300 }, - }, - contracts: { - "subgraph/Registrar": { - chain: { - mainnet: { address: "0x1", startBlock: 444, endBlock: 999 }, - }, - }, - "subgraph/Registry": { - chain: { - mainnet: { address: "0x2", startBlock: 111, endBlock: 211 }, - }, - }, - }, - blocks: { - "subgraph:InterestingBlocks": { - chain: "mainnet", - startBlock: 99, - endBlock: 123, - }, - }, - } satisfies PonderConfigType; - - // act - const result = getChainsBlockrange(ponderConfig); - - // assert - expect(result).toStrictEqual({ - mainnet: { startBlock: 50, endBlock: 999 }, - }); - }); - - it("throws if no startBlock is defined for a chain", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - }, - accounts: {}, - contracts: {}, - blocks: {}, - } satisfies PonderConfigType; - - // act & assert - expect(() => getChainsBlockrange(ponderConfig)).toThrow( - /No minimum start block found for chain 'mainnet'/, - ); - }); - - it("handles Ponder config with flat datasources", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - base: { - id: 8543, - rpc: "https://example.com/base", - }, - }, - accounts: {}, - contracts: { - "subgraph/Registrar": { - chain: "mainnet", - address: "0x1", - startBlock: 444_444_444, - }, - "subgraph/Registry": { - chain: "mainnet", - address: "0x2", - startBlock: 444_444_333, - }, - "basenames/Registrar": { - chain: "base", - address: "0x11", - startBlock: 1_799_433, - }, - "basenames/Registry": { - chain: "base", - address: "0x12", - startBlock: 1_799_430, - }, - }, - blocks: {}, - } satisfies PonderConfigType; - - // act - const result = getChainsBlockrange(ponderConfig); - - // assert - expect(result).toStrictEqual({ - mainnet: { startBlock: 444_444_333, endBlock: undefined }, - base: { startBlock: 1_799_430, endBlock: undefined }, - }); - }); - - it("handles Ponder config with nested datasources", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - base: { - id: 8543, - rpc: "https://example.com/base", - }, - }, - accounts: {}, - contracts: { - "subgraph/Registrar": { - chain: { - mainnet: { address: "0x1", startBlock: 444_444_444 }, - }, - }, - "subgraph/Registry": { - chain: { - mainnet: { address: "0x2", startBlock: 444_444_333 }, - }, - }, - "basenames/Registrar": { - chain: { - base: { address: "0x11", startBlock: 1_799_433 }, - }, - }, - "basenames/Registry": { - chain: { - base: { address: "0x12", startBlock: 1_799_430 }, - }, - }, - }, - blocks: {}, - } satisfies PonderConfigType; - - // act - const result = getChainsBlockrange(ponderConfig); - - // assert - expect(result).toStrictEqual({ - mainnet: { startBlock: 444_444_333, endBlock: undefined }, - base: { startBlock: 1_799_430, endBlock: undefined }, - }); - }); - - it("handles mix of flat and nested datasources", () => { - // arrange - const ponderConfig = { - chains: { - mainnet: { - id: 1, - rpc: "https://example.com/mainnet", - }, - base: { - id: 8543, - rpc: "https://example.com/base", - }, - }, - accounts: {}, - contracts: { - "subgraph/Registrar": { - chain: { - mainnet: { address: "0x1", startBlock: 444_444_444 }, - }, - }, - "subgraph/Registry": { - chain: { - mainnet: { address: "0x2", startBlock: 444_444_333 }, - }, - }, - "basenames/Registrar": { - chain: "base", - address: "0x11", - startBlock: 1_799_433, - }, - "basenames/Registry": { - chain: "base", - address: "0x12", - startBlock: 1_799_430, - }, - }, - blocks: {}, - } satisfies PonderConfigType; - - // act - const result = getChainsBlockrange(ponderConfig); - - // assert - expect(result).toStrictEqual({ - mainnet: { startBlock: 444_444_333, endBlock: undefined }, - base: { startBlock: 1_799_430, endBlock: undefined }, - }); - }); -}); - -describe("createChainIndexingSnapshot", () => { - it("returns 'queued' status if startBlock equals statusBlock (definite)", () => { - // arrange - const meta: ChainMetadata = { - chainId: 1, - historicalTotalBlocks: 100, - isSyncComplete: false, - isSyncRealtime: false, - config: { startBlock: blockRef(10, 1000), endBlock: blockRef(20, 2000) }, - backfillEndBlock: blockRef(20, 2000), - syncBlock: blockRef(15, 1500), - statusBlock: blockRef(10, 1000), - }; - - // act - const chainIndexingStatus = createChainIndexingSnapshot(meta); - - // assert - expect(chainIndexingStatus).toStrictEqual({ - chainStatus: ChainIndexingStatusIds.Queued, - config: { - configType: ChainIndexingConfigTypeIds.Definite, - startBlock: blockRef(10, 1000), - endBlock: blockRef(20, 2000), - }, - } satisfies ChainIndexingStatusSnapshotQueued); - }); - - it("returns 'queued' status if startBlock equals statusBlock (indefinite)", () => { - // arrange - const meta: ChainMetadata = { - chainId: 1, - historicalTotalBlocks: 100, - isSyncComplete: false, - isSyncRealtime: false, - config: { startBlock: blockRef(10, 1000), endBlock: null }, - backfillEndBlock: blockRef(20, 2000), - syncBlock: blockRef(20, 2000), - statusBlock: blockRef(10, 1000), - }; - - // act - const chainIndexingStatus = createChainIndexingSnapshot(meta); - - // assert - expect(chainIndexingStatus).toStrictEqual({ - chainStatus: ChainIndexingStatusIds.Queued, - config: { - configType: ChainIndexingConfigTypeIds.Indefinite, - startBlock: blockRef(10, 1000), - }, - } satisfies ChainIndexingStatusSnapshotQueued); - }); - - it("returns 'completed' status if isSyncComplete is true", () => { - // arrange - const meta: ChainMetadata = { - chainId: 1, - historicalTotalBlocks: 100, - isSyncComplete: true, - isSyncRealtime: false, - config: { startBlock: blockRef(10, 1000), endBlock: blockRef(20, 2000) }, - backfillEndBlock: blockRef(20, 2000), - syncBlock: blockRef(20, 2000), - statusBlock: blockRef(20, 2000), - }; - - // act - const chainIndexingStatus = createChainIndexingSnapshot(meta); - - // assert - expect(chainIndexingStatus).toStrictEqual({ - chainStatus: ChainIndexingStatusIds.Completed, - config: { - configType: ChainIndexingConfigTypeIds.Definite, - startBlock: blockRef(10, 1000), - endBlock: blockRef(20, 2000), - }, - latestIndexedBlock: blockRef(20, 2000), - } satisfies ChainIndexingStatusSnapshotCompleted); - }); - - it("returns 'following' status if isSyncRealtime is true", () => { - // arrange - const meta: ChainMetadata = { - chainId: 1, - historicalTotalBlocks: 100, - isSyncComplete: false, - isSyncRealtime: true, - config: { startBlock: blockRef(10, 1000), endBlock: null }, - backfillEndBlock: blockRef(20, 2000), - syncBlock: blockRef(30, 3000), - statusBlock: blockRef(25, 2500), - }; - - // act - const chainIndexingStatus = createChainIndexingSnapshot(meta); - - // assert - expect(chainIndexingStatus).toStrictEqual({ - chainStatus: ChainIndexingStatusIds.Following, - config: { - configType: ChainIndexingConfigTypeIds.Indefinite, - startBlock: blockRef(10, 1000), - }, - latestIndexedBlock: blockRef(25, 2500), - latestKnownBlock: blockRef(30, 3000), - } satisfies ChainIndexingStatusSnapshotFollowing); - }); - - it("returns Backfill status otherwise (definite config)", () => { - // arrange - const meta: ChainMetadata = { - chainId: 1, - historicalTotalBlocks: 100, - isSyncComplete: false, - isSyncRealtime: false, - config: { startBlock: blockRef(10, 1000), endBlock: blockRef(20, 2000) }, - backfillEndBlock: blockRef(20, 2000), - syncBlock: blockRef(18, 1800), - statusBlock: blockRef(15, 1500), - }; - - // act - const chainIndexingStatus = createChainIndexingSnapshot(meta); - - // assert - expect(chainIndexingStatus).toStrictEqual({ - chainStatus: ChainIndexingStatusIds.Backfill, - config: { - configType: ChainIndexingConfigTypeIds.Definite, - startBlock: blockRef(10, 1000), - endBlock: blockRef(20, 2000), - }, - latestIndexedBlock: blockRef(15, 1500), - backfillEndBlock: blockRef(20, 2000), - } satisfies ChainIndexingStatusSnapshotBackfill); - }); - - it("returns Backfill status otherwise (indefinite config)", () => { - // arrange - const meta: ChainMetadata = { - chainId: 1, - historicalTotalBlocks: 100, - isSyncComplete: false, - isSyncRealtime: false, - config: { startBlock: blockRef(10, 1000), endBlock: null }, - backfillEndBlock: blockRef(30, 3000), - syncBlock: blockRef(30, 3000), - statusBlock: blockRef(15, 1500), - }; - - // act - const chainIndexingStatus = createChainIndexingSnapshot(meta); - - // assert - expect(chainIndexingStatus).toStrictEqual({ - chainStatus: ChainIndexingStatusIds.Backfill, - config: { - configType: ChainIndexingConfigTypeIds.Indefinite, - startBlock: blockRef(10, 1000), - }, - latestIndexedBlock: blockRef(15, 1500), - backfillEndBlock: blockRef(30, 3000), - } satisfies ChainIndexingStatusSnapshotBackfill); - }); -}); diff --git a/packages/ponder-sdk/src/chains.ts b/packages/ponder-sdk/src/chains.ts index d74017221..2d0c0314f 100644 --- a/packages/ponder-sdk/src/chains.ts +++ b/packages/ponder-sdk/src/chains.ts @@ -2,47 +2,24 @@ * Ponder SDK: Chains * * This file describes ideas and functionality related to metadata about chains - * indexing status. In this module, ideas represented in other Ponder SDK - * modules, such as: - * - Config - * - Metrics - * - RPC - * - Status - * all come together to form a single view about a chain's indexing status. + * indexing status. */ -import { prettifyError } from "zod/v4/core"; +import type { BlockRef } from "./block-refs"; +import type { DeepPartial } from "./shared"; -import { - type BlockRef, - type ChainId, - type ChainIdString, - ChainIndexingConfigTypeIds, - ChainIndexingStatusIds, - type ChainIndexingStatusSnapshot, - type ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill, - createIndexingConfig, - type DeepPartial, - deserializeChainIndexingStatusSnapshot, - getOmnichainIndexingCursor, - getOmnichainIndexingStatus, - OmnichainIndexingStatusIds, - type SerializedChainIndexingStatusSnapshot, - type SerializedChainIndexingStatusSnapshotBackfill, - type SerializedChainIndexingStatusSnapshotCompleted, - type SerializedChainIndexingStatusSnapshotFollowing, - type SerializedChainIndexingStatusSnapshotQueued, - type SerializedOmnichainIndexingStatusSnapshot, - type SerializedOmnichainIndexingStatusSnapshotBackfill, - type SerializedOmnichainIndexingStatusSnapshotCompleted, - type SerializedOmnichainIndexingStatusSnapshotFollowing, - type SerializedOmnichainIndexingStatusSnapshotUnstarted, -} from "@ensnode/ensnode-sdk"; +/** + * Chain ID + * + * Represents a unique identifier for a chain. + * Guaranteed to be a positive integer. + **/ +export type ChainId = number; -import type { ChainBlockRefs } from "./block-refs"; -import type { ChainName } from "./config"; -import type { PonderMetricsResponse, PonderStatusResponse } from "./response"; -import { makePonderChainMetadataSchema } from "./zod-schemas"; +/** + * Serialized representation of {@link ChainId}. + **/ +export type ChainIdString = string; /** * Ponder Status Chain @@ -58,7 +35,7 @@ export interface PonderStatusChain { /** * Chain Metadata * - * Chain metadata, required to determine {@link ChainIndexingSnapshot}. + * Represents metadata about an indexed chain. */ export interface ChainMetadata { chainId: ChainId; @@ -123,185 +100,4 @@ export interface ChainMetadata { /** * Unvalidated representation of {@link ChainMetadata}. */ -export interface UnvalidatedChainMetadata - extends DeepPartial> { - isSyncComplete: number | undefined; - isSyncRealtime: number | undefined; -} - -/** - * Create {@link ChainIndexingStatusSnapshot} for the indexed chain metadata. - */ -export function createChainIndexingSnapshot( - chainMetadata: ChainMetadata, -): ChainIndexingStatusSnapshot { - const { - config: chainBlocksConfig, - backfillEndBlock: chainBackfillEndBlock, - isSyncComplete, - isSyncRealtime, - syncBlock: chainSyncBlock, - statusBlock: chainStatusBlock, - } = chainMetadata; - - const { startBlock, endBlock } = chainBlocksConfig; - const config = createIndexingConfig(startBlock, endBlock); - - // In omnichain ordering, if the startBlock is the same as the - // status block, the chain has not started yet. - if (chainBlocksConfig.startBlock.number === chainStatusBlock.number) { - return deserializeChainIndexingStatusSnapshot({ - chainStatus: ChainIndexingStatusIds.Queued, - config, - } satisfies SerializedChainIndexingStatusSnapshotQueued); - } - - if (isSyncComplete) { - if (config.configType !== ChainIndexingConfigTypeIds.Definite) { - throw new Error( - `The '${ChainIndexingStatusIds.Completed}' indexing status can be only created with the '${ChainIndexingConfigTypeIds.Definite}' indexing config type.`, - ); - } - - return deserializeChainIndexingStatusSnapshot({ - chainStatus: ChainIndexingStatusIds.Completed, - latestIndexedBlock: chainStatusBlock, - config, - } satisfies SerializedChainIndexingStatusSnapshotCompleted); - } - - if (isSyncRealtime) { - if (config.configType !== ChainIndexingConfigTypeIds.Indefinite) { - throw new Error( - `The '${ChainIndexingStatusIds.Following}' indexing status can be only created with the '${ChainIndexingConfigTypeIds.Indefinite}' indexing config type.`, - ); - } - - return deserializeChainIndexingStatusSnapshot({ - chainStatus: ChainIndexingStatusIds.Following, - latestIndexedBlock: chainStatusBlock, - latestKnownBlock: chainSyncBlock, - config: { - configType: config.configType, - startBlock: config.startBlock, - }, - } satisfies SerializedChainIndexingStatusSnapshotFollowing); - } - - return deserializeChainIndexingStatusSnapshot({ - chainStatus: ChainIndexingStatusIds.Backfill, - latestIndexedBlock: chainStatusBlock, - backfillEndBlock: chainBackfillEndBlock, - config, - } satisfies SerializedChainIndexingStatusSnapshotBackfill); -} - -/** - * Create Serialized Omnichain Indexing Snapshot - * - * Creates {@link SerializedOmnichainIndexingStatusSnapshot} from serialized chain snapshots. - */ -export function createSerializedOmnichainIndexingStatusSnapshot( - serializedChainSnapshots: Record, -): SerializedOmnichainIndexingStatusSnapshot { - const chains = Object.values(serializedChainSnapshots); - const omnichainStatus = getOmnichainIndexingStatus(chains); - const omnichainIndexingCursor = getOmnichainIndexingCursor(chains); - - switch (omnichainStatus) { - case OmnichainIndexingStatusIds.Unstarted: { - return { - omnichainStatus: OmnichainIndexingStatusIds.Unstarted, - chains: serializedChainSnapshots as Record< - ChainIdString, - SerializedChainIndexingStatusSnapshotQueued - >, // forcing the type here, will be validated in the following 'check' step - omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotUnstarted; - } - - case OmnichainIndexingStatusIds.Backfill: { - return { - omnichainStatus: OmnichainIndexingStatusIds.Backfill, - chains: serializedChainSnapshots as Record< - ChainIdString, - ChainIndexingStatusSnapshotForOmnichainIndexingStatusSnapshotBackfill - >, // forcing the type here, will be validated in the following 'check' step - omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotBackfill; - } - - case OmnichainIndexingStatusIds.Completed: { - return { - omnichainStatus: OmnichainIndexingStatusIds.Completed, - chains: serializedChainSnapshots as Record< - ChainIdString, - SerializedChainIndexingStatusSnapshotCompleted - >, // forcing the type here, will be validated in the following 'check' step - omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotCompleted; - } - - case OmnichainIndexingStatusIds.Following: - return { - omnichainStatus: OmnichainIndexingStatusIds.Following, - chains: serializedChainSnapshots, - omnichainIndexingCursor, - } satisfies SerializedOmnichainIndexingStatusSnapshotFollowing; - } -} - -/** - * Create serialized chain indexing snapshots. - * - * The output of this function is required for - * calling {@link createOmnichainIndexingSnapshot}. - */ -export function createSerializedChainSnapshots( - chainIds: ChainIdString[], - chainsBlockRefs: Map, - metrics: PonderMetricsResponse, - status: PonderStatusResponse, -): Record { - const chainsMetadata = new Map(); - - // collect unvalidated chain metadata for each indexed chain - for (const chainId of chainIds) { - const chainBlockRefs = chainsBlockRefs.get(chainId); - - const chainMetadata = { - chainId: status[chainId]?.id, - config: chainBlockRefs?.config, - backfillEndBlock: chainBlockRefs?.backfillEndBlock, - historicalTotalBlocks: metrics.getValue("ponder_historical_total_blocks", { - chain: chainId, - }), - isSyncComplete: metrics.getValue("ponder_sync_is_complete", { chain: chainId }), - isSyncRealtime: metrics.getValue("ponder_sync_is_realtime", { chain: chainId }), - syncBlock: { - number: metrics.getValue("ponder_sync_block", { chain: chainId }), - timestamp: metrics.getValue("ponder_sync_block_timestamp", { chain: chainId }), - }, - statusBlock: { - number: status[chainId]?.block.number, - timestamp: status[chainId]?.block.timestamp, - }, - } satisfies UnvalidatedChainMetadata; - - chainsMetadata.set(chainId, chainMetadata); - } - - // parse chain metadata for each indexed chain - const schema = makePonderChainMetadataSchema(chainIds); - const parsed = schema.safeParse(chainsMetadata); - - if (!parsed.success) { - throw new Error( - "Failed to build SerializedOmnichainIndexingStatusSnapshot object: \n" + - prettifyError(parsed.error) + - "\n", - ); - } - - return parsed.data; -} +export type UnvalidatedChainMetadata = DeepPartial; diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index 3fa318081..d18859106 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -5,64 +5,43 @@ import { type PonderStatusResponse, } from "./response"; -export const PonderHealthCheckResults = { - /** - * Ponder Health is unknown if the health check endpoint is unavailable. - */ - Unknown: "unknown", - - /** - * Ponder Health is not OK if the health check endpoint returned - * HTTP status other than `2xx`. - */ - NotOk: "not-ok", - - /** - * Ponder Health is OK if the health check endpoint returned - * `2xx` HTTP status. - */ - Ok: "ok", -} as const; - -export type PonderHealthCheckResult = - (typeof PonderHealthCheckResults)[keyof typeof PonderHealthCheckResults]; - export class PonderClient { - #healthCheckResult: PonderHealthCheckResult | undefined; - constructor(private ponderApplicationUrl: URL) {} /** * Ponder health check endpoint. * - * @returns Ponder health check result. + * @throws if the Ponder application request fails. + * @throws if the Ponder application returns an error response. */ - public async health(): Promise { - let response: Response; - - try { - response = await fetch(new URL("/health", this.ponderApplicationUrl)); + public async health(): Promise { + const response = await fetch(new URL("/status", this.ponderApplicationUrl)); - if (!response.ok) { - this.#healthCheckResult = PonderHealthCheckResults.NotOk; - } else { - this.#healthCheckResult = PonderHealthCheckResults.Ok; - } - } catch { - this.#healthCheckResult = PonderHealthCheckResults.Unknown; + if (!response.ok) { + throw new Error( + `Ponder application health check failed with status: ${response.status} ${response.statusText}`, + ); } - return this.#healthCheckResult; + return response; } /** - * Is Ponder app "ready"? + * Ponder readiness check endpoint. * - * @throws error about Ponder `/ready` endpoint not being supported. - * ENSNode makes no use of that endpoint. + * @throws if the Ponder application request fails. + * @throws if the Ponder application returns an error response. */ - public ready(): never { - throw new Error("Ponder `/ready` endpoint is not supported by this client."); + public async ready(): Promise { + const response = await fetch(new URL("/ready", this.ponderApplicationUrl)); + + if (!response.ok) { + throw new Error( + `Ponder application readiness check failed with status: ${response.status} ${response.statusText}`, + ); + } + + return response; } /** @@ -73,8 +52,6 @@ export class PonderClient { * @throws if the Ponder application response breaks required invariants */ public async status(): Promise { - this.validateHealthCheckResult(); - const response = await fetch(new URL("/status", this.ponderApplicationUrl)); const responseJson = await response.json(); @@ -89,31 +66,9 @@ export class PonderClient { * @throws if the Ponder application response breaks required invariants */ public async metrics(): Promise { - this.validateHealthCheckResult(); - const response = await fetch(new URL("/metrics", this.ponderApplicationUrl)); const responseText = await response.text(); return deserializePonderMetricsResponse(responseText); } - - /** - * Validate ENSIndexer health check result. - * - * @throws if the health check result is other than - * {@link PonderHealthCheckResults.Ok}. - */ - private validateHealthCheckResult(): void { - if (typeof this.#healthCheckResult === "undefined") { - throw new Error( - "Running health check for Ponder application is required. Call the 'health()' method first.", - ); - } - - if (this.#healthCheckResult !== PonderHealthCheckResults.Ok) { - throw new Error( - `Ponder application must be healthy. Current health check result is '${this.#healthCheckResult}'. You can keep calling the 'health()' method until it returns the 'ok' result.`, - ); - } - } } diff --git a/packages/ponder-sdk/src/config.ts b/packages/ponder-sdk/src/config.ts index 5f53c692f..9b58a6e26 100644 --- a/packages/ponder-sdk/src/config.ts +++ b/packages/ponder-sdk/src/config.ts @@ -14,20 +14,8 @@ import type { AddressConfig, ChainConfig, CreateConfigReturnType } from "ponder"; -import { - type BlockNumber, - type Blockrange, - deserializeBlockNumber, - deserializeBlockrange, -} from "@ensnode/ensnode-sdk"; - -/** - * Chain Name - * - * Often use as type for object keys expressing Ponder ideas, such as - * chain status, or chain metrics. - */ -export type ChainName = string; +import type { BlockNumber, Blockrange } from "./block-refs"; +import type { ChainName } from "./shared"; /** * Ponder config datasource with a flat `chain` value. @@ -132,12 +120,12 @@ export function getChainsBlockrange(ponderConfig: PonderConfigType): Record= 0) { + chainStartBlocks.push(startBlock); } - if (typeof endBlock === "number") { - chainEndBlocks.push(deserializeBlockNumber(endBlock)); + if (typeof endBlock === "number" && Number.isInteger(endBlock) && endBlock >= 0) { + chainEndBlocks.push(endBlock); } } @@ -164,12 +152,21 @@ export function getChainsBlockrange(ponderConfig: PonderConfigType): Record chainHighestEndBlock + ) { + throw new Error( + `For chain '${chainName}', the start block (${chainLowestStartBlock}) must be lower or equal to the end block (${chainHighestEndBlock}).`, + ); + } - chainsBlockrange[chainName] = deserializeBlockrange({ + // 5. Assign a valid blockrange to the chain + chainsBlockrange[chainName] = { startBlock: chainLowestStartBlock, endBlock: chainHighestEndBlock, - }); + }; } return chainsBlockrange; diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index 311b5ff41..8f7f5bdcc 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -5,3 +5,5 @@ export * from "./config"; export * from "./metrics"; export * from "./response"; export * from "./rpc"; +export * from "./shared"; +export * from "./time"; diff --git a/packages/ponder-sdk/src/metrics/validate-ponder-metrics.ts b/packages/ponder-sdk/src/metrics/validate-ponder-metrics.ts index f1acb7114..f441c4d66 100644 --- a/packages/ponder-sdk/src/metrics/validate-ponder-metrics.ts +++ b/packages/ponder-sdk/src/metrics/validate-ponder-metrics.ts @@ -1,6 +1,3 @@ -import { prettifyError } from "zod/v4"; - -import { PonderAppSettingsSchema } from "../zod-schemas"; import type { PrometheusMetrics } from "./prometheus-metrics"; /** @@ -12,14 +9,18 @@ import type { PrometheusMetrics } from "./prometheus-metrics"; */ export function validatePonderMetrics(metrics: PrometheusMetrics) { // Invariant: Ponder command & ordering are as expected - const parsedAppSettings = PonderAppSettingsSchema.safeParse({ - command: metrics.getLabel("ponder_settings_info", "command"), - ordering: metrics.getLabel("ponder_settings_info", "ordering"), - }); + const command = metrics.getLabel("ponder_settings_info", "command"); + const ordering = metrics.getLabel("ponder_settings_info", "ordering"); + + if (typeof command !== "string" || !["dev", "start"].includes(command)) { + throw new Error( + `Ponder settings_info command label is invalid: expected "dev" or "start", got "${command}"`, + ); + } - if (parsedAppSettings.error) { + if (ordering !== "omnichain") { throw new Error( - `Failed to build IndexingStatus object: \n${prettifyError(parsedAppSettings.error)}\n`, + `Ponder settings_info ordering label is invalid: expected "omnichain", got "${ordering}"`, ); } } diff --git a/packages/ponder-sdk/src/response.ts b/packages/ponder-sdk/src/response.ts index 3c03bd018..98dadc5a6 100644 --- a/packages/ponder-sdk/src/response.ts +++ b/packages/ponder-sdk/src/response.ts @@ -1,12 +1,11 @@ import { prettifyError } from "zod/v4"; -import type { ChainIdString } from "@ensnode/ensnode-sdk"; - import type { PonderStatusChain } from "./chains"; import { PrometheusMetrics, validatePonderMetrics } from "./metrics"; +import type { ChainName } from "./shared"; import { makePonderStatusResponseSchema } from "./zod-schemas"; -export type PonderStatusResponse = Record; +export type PonderStatusResponse = Record; export type PonderMetricsResponse = PrometheusMetrics; diff --git a/packages/ponder-sdk/src/rpc.ts b/packages/ponder-sdk/src/rpc.ts index 936d4efb0..cd3115c0c 100644 --- a/packages/ponder-sdk/src/rpc.ts +++ b/packages/ponder-sdk/src/rpc.ts @@ -8,9 +8,7 @@ import type { PublicClient } from "viem"; -import { type BlockNumber, type BlockRef, deserializeBlockRef } from "@ensnode/ensnode-sdk"; - -export type { PublicClient } from "viem"; +import type { BlockNumber, BlockRef } from "./block-refs"; /** * Fetch block ref from RPC. @@ -25,11 +23,13 @@ export async function fetchBlockRef( blockNumber: BlockNumber, ): Promise { const block = await publicClient.getBlock({ blockNumber: BigInt(blockNumber) }); + if (!block) { throw new Error(`Could not fetch block ${blockNumber}`); } - return deserializeBlockRef({ + + return { number: Number(block.number), timestamp: Number(block.timestamp), - }); + }; } diff --git a/packages/ponder-sdk/src/shared.ts b/packages/ponder-sdk/src/shared.ts new file mode 100644 index 000000000..4aad5815e --- /dev/null +++ b/packages/ponder-sdk/src/shared.ts @@ -0,0 +1,45 @@ +/** + * Chain Name + * + * Used as a type for object keys expressing Ponder ideas, such as + * chain status, or chain metrics. + */ +export type ChainName = string; + +/** + * A utility type that makes all properties of a type optional recursively, + * including nested objects and arrays. + * + * @example + * ```typescript + * type Config = { + * a: string; + * b: { + * x: number; + * y: { z: boolean }; + * }; + * c: { id: string }[]; + * } + * + * type PartialConfig = DeepPartial; + * // Results in: + * // { + * // a?: string; + * // b?: { + * // x?: number; + * // y?: { z?: boolean }; + * // }; + * // c?: { id?: string }[]; + * // } + * + * // Usage: + * const update: PartialConfig = { b: { y: { z: true } } }; + * ``` + */ +export type DeepPartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? DeepPartial[] + : T[P] extends object + ? DeepPartial + : T[P]; +}; diff --git a/packages/ponder-sdk/src/time.ts b/packages/ponder-sdk/src/time.ts new file mode 100644 index 000000000..e0ea2408f --- /dev/null +++ b/packages/ponder-sdk/src/time.ts @@ -0,0 +1,10 @@ +/** + * Unix timestamp value + * + * Represents the number of seconds that have elapsed + * since January 1, 1970 (midnight UTC/GMT). + * + * Guaranteed to be an integer. May be zero or negative to represent a time at or + * before Jan 1, 1970. + */ +export type UnixTimestamp = number; diff --git a/packages/ponder-sdk/src/validations.ts b/packages/ponder-sdk/src/validations.ts deleted file mode 100644 index 12bcf3234..000000000 --- a/packages/ponder-sdk/src/validations.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ParsePayload } from "zod/v4/core"; - -import { - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing, - checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted, - OmnichainIndexingStatusIds, - type SerializedOmnichainIndexingStatusSnapshot, -} from "@ensnode/ensnode-sdk"; - -/** - * 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/ponder-sdk/src/zod-schemas.ts b/packages/ponder-sdk/src/zod-schemas.ts index 7ce4038b3..dea8a39b6 100644 --- a/packages/ponder-sdk/src/zod-schemas.ts +++ b/packages/ponder-sdk/src/zod-schemas.ts @@ -13,74 +13,108 @@ import z from "zod/v4"; -import type { ChainIdString, ChainIndexingStatusSnapshot } from "@ensnode/ensnode-sdk"; -import { - makeBlockRefSchema, - makeChainIdSchema, - makeChainIdStringSchema, - makeNonNegativeIntegerSchema, -} from "@ensnode/ensnode-sdk/internal"; - -import { createChainIndexingSnapshot } from "./chains"; - -const makeChainIdsSchema = (chainIds: string[]) => z.enum(chainIds); - -const PonderBlockRefSchema = makeBlockRefSchema(); - -const PonderCommandSchema = z.enum(["dev", "start"]); - -const PonderOrderingSchema = z.literal("omnichain"); - -export const PonderAppSettingsSchema = z.strictObject({ - command: PonderCommandSchema, - ordering: PonderOrderingSchema, -}); - -const PonderMetricBooleanSchema = z.coerce.string().transform((v) => v === "1"); - -const PonderChainMetadataSchema = z.strictObject({ - chainId: makeChainIdSchema(), - config: z.object({ - startBlock: PonderBlockRefSchema, - endBlock: PonderBlockRefSchema.nullable(), - }), - backfillEndBlock: PonderBlockRefSchema, - historicalTotalBlocks: makeNonNegativeIntegerSchema(), - isSyncComplete: PonderMetricBooleanSchema, - isSyncRealtime: PonderMetricBooleanSchema, - syncBlock: PonderBlockRefSchema, - statusBlock: PonderBlockRefSchema, -}); - -export const makePonderChainMetadataSchema = (chainIds: ChainIdString[]) => { - const ChainIdsSchema = makeChainIdsSchema(chainIds); - - const invariant_definedEntryForEachIndexedChain = (v: Map) => - chainIds.every((chainIds) => Array.from(v.keys()).includes(chainIds)); - - return z - .map(ChainIdsSchema, PonderChainMetadataSchema) - .refine(invariant_definedEntryForEachIndexedChain, { - error: "All `chainIds` must be represented by Ponder Chains Block Refs object.", - }) - - .transform((chains) => { - const serializedChainIndexingStatusSnapshots = {} as Record< - ChainIdString, - ChainIndexingStatusSnapshot - >; - - for (const chainId of chainIds) { - // biome-ignore lint/style/noNonNullAssertion: guaranteed to exist - const indexedChain = chains.get(chainId)!; - - serializedChainIndexingStatusSnapshots[indexedChain.chainId] = - createChainIndexingSnapshot(indexedChain); - } - - return serializedChainIndexingStatusSnapshots; - }); -}; +import { BlockNumber, BlockRef, Blockrange } from "./block-refs"; +import type { ChainId } from "./chains"; +import { ChainName } from "./shared"; +import type { UnixTimestamp } from "./time"; + +/** + * Parses a numeric value as an integer. + */ +export const makeIntegerSchema = (valueLabel: string = "Value") => + z.int({ + error: `${valueLabel} must be an integer.`, + }); + +/** + * Parses a numeric value as a positive integer. + */ +export const makePositiveIntegerSchema = (valueLabel: string = "Value") => + makeIntegerSchema(valueLabel).positive({ + error: `${valueLabel} must be a positive integer (>0).`, + }); + +/** + * Parses a numeric value as a non-negative integer. + */ +export const makeNonNegativeIntegerSchema = (valueLabel: string = "Value") => + makeIntegerSchema(valueLabel).nonnegative({ + error: `${valueLabel} must be a non-negative integer (>=0).`, + }); + +/** + * Parses value as {@link UnixTimestamp}. + */ +export const makeUnixTimestampSchema = (valueLabel: string = "Timestamp") => + makeIntegerSchema(valueLabel); + +/** + * Parses a serialized representation of {@link ChainName}. + */ +export const makeChainNameSchema = (valueLabel: string = "Chain Name") => + z.string({ error: `${valueLabel} must be a string representing a chain name.` }); + +/** + * Parses Chain ID + * + * {@link ChainId} + */ +export const makeChainIdSchema = (valueLabel: string = "Chain ID") => + makePositiveIntegerSchema(valueLabel).transform((val) => val as ChainId); + +/** + * Parses a serialized representation of {@link ChainId}. + */ +export const makeChainIdStringSchema = (valueLabel: string = "Chain ID String") => + z + .string({ error: `${valueLabel} must be a string representing a chain ID.` }) + .pipe(z.coerce.number({ error: `${valueLabel} must represent a positive integer (>0).` })) + .pipe(makeChainIdSchema(`The numeric value represented by ${valueLabel}`)); + +/** + * Parses a serialized representation of {@link BlockNumber}. + */ +export const makeBlockNumberSchema = (valueLabel: string = "Block number") => + makeNonNegativeIntegerSchema(valueLabel); + +/** + * Parses an object value as the {@link Blockrange} object. + */ +export const makeBlockrangeSchema = (valueLabel: string = "Value") => + z + .strictObject( + { + startBlock: makeBlockNumberSchema(`${valueLabel}.startBlock`).optional(), + endBlock: makeBlockNumberSchema(`${valueLabel}.endBlock`).optional(), + }, + { + error: `${valueLabel} must be a valid Blockrange object.`, + }, + ) + .refine( + (v) => { + if (v.startBlock && v.endBlock) { + return v.startBlock <= v.endBlock; + } + + return true; + }, + { error: `${valueLabel}: startBlock must be before or equal to endBlock` }, + ); + +/** + * Parses an object value as the {@link BlockRef} object. + */ +export const makeBlockRefSchema = (valueLabel: string = "Value") => + z.strictObject( + { + timestamp: makeUnixTimestampSchema(`${valueLabel}.timestamp`), + number: makeBlockNumberSchema(`${valueLabel}.number`), + }, + { + error: `${valueLabel} must be a valid BlockRef object.`, + }, + ); export const makePonderStatusChainSchema = (valueLabel = "Ponder Status Chain") => { const chainIdSchema = makeChainIdSchema(valueLabel); @@ -94,7 +128,7 @@ export const makePonderStatusChainSchema = (valueLabel = "Ponder Status Chain") export const makePonderStatusResponseSchema = (valueLabel = "Ponder Status Response") => z.record( - makeChainIdStringSchema(`${valueLabel}.key`), + z.string({ error: `${valueLabel}.key must be a string.` }), makePonderStatusChainSchema(`${valueLabel}.value`), { error: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfe0de61c..67ba1bf17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,9 +822,6 @@ importers: specifier: ^1.1.1 version: 1.1.1 devDependencies: - '@ensnode/ensnode-sdk': - specifier: workspace:* - version: link:../ensnode-sdk '@ensnode/shared-configs': specifier: workspace:* version: link:../shared-configs From b8b0d8e6e259c7021ad475e96d8bad6771daa4dc Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Mon, 22 Dec 2025 17:30:12 +0100 Subject: [PATCH 2/2] copy updates --- README.md | 4 ++++ .../ponder/local-client/chain-indexing-status-snapshot.ts | 6 ------ .../ensindexer/ponder/local-client/local-ponder-client.ts | 8 ++++++-- .../src/lib/indexing-status/build-index-status.ts | 3 +-- packages/ponder-sdk/README.md | 2 +- packages/ponder-sdk/package.json | 2 +- packages/ponder-sdk/tsup.config.ts | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f7dfab802..7278ba217 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,10 @@ TypeScript library for interacting with the [ENSRainbow API](apps/ensrainbow). Shared Drizzle schema definitions used by ENSNode +### [`packages/ponder-sdk`](packages/ponder-sdk) + +A set of utilities for interacting with a Ponder app. + ### [`packages/ponder-subgraph`](packages/ponder-subgraph) Subgraph-compatible GraphQL API diff --git a/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts b/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts index e165a15f5..a610d10f6 100644 --- a/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts +++ b/apps/ensindexer/ponder/local-client/chain-indexing-status-snapshot.ts @@ -135,12 +135,6 @@ export function createSerializedChainSnapshots( const isSyncRealtime = metrics.getValue("ponder_sync_is_realtime", { chain: chainId }); - if (typeof isSyncRealtime === "string" && !["0", "1"].includes(isSyncRealtime)) { - throw new Error( - `The 'ponder_sync_is_realtime' metric for chain '${chainId}' must be a string with value "0" or "1".`, - ); - } - const config = { startBlock: deserializeBlockRef(chainBlockRefs?.config.startBlock), endBlock: diff --git a/apps/ensindexer/ponder/local-client/local-ponder-client.ts b/apps/ensindexer/ponder/local-client/local-ponder-client.ts index 3532e091a..4c9633f8f 100644 --- a/apps/ensindexer/ponder/local-client/local-ponder-client.ts +++ b/apps/ensindexer/ponder/local-client/local-ponder-client.ts @@ -11,7 +11,7 @@ import { type ChainName, getChainsBlockRefs, getChainsBlockrange, - type PonderClient, + PonderClient, type PonderMetricsResponse, } from "@ensnode/ponder-sdk"; @@ -27,7 +27,11 @@ export class LocalPonderClient { */ private chainsBlockRefs = new Map(); - constructor(private readonly ponderClient: PonderClient) {} + private readonly ponderClient: PonderClient; + + constructor(ponderApplicationUrl: URL) { + this.ponderClient = new PonderClient(ponderApplicationUrl); + } public async buildCrossChainIndexingStatusSnapshot(): Promise { const [metrics, status] = await Promise.all([ diff --git a/apps/ensindexer/src/lib/indexing-status/build-index-status.ts b/apps/ensindexer/src/lib/indexing-status/build-index-status.ts index 270b7d72b..3ff0fcce6 100644 --- a/apps/ensindexer/src/lib/indexing-status/build-index-status.ts +++ b/apps/ensindexer/src/lib/indexing-status/build-index-status.ts @@ -18,11 +18,10 @@ import { type OmnichainIndexingStatusSnapshot, type UnixTimestamp, } from "@ensnode/ensnode-sdk"; -import { PonderClient } from "@ensnode/ponder-sdk"; import { LocalPonderClient } from "../../../ponder/local-client"; -const localPonderClient = new LocalPonderClient(new PonderClient(config.ensIndexerUrl)); +const localPonderClient = new LocalPonderClient(config.ensIndexerUrl); export async function buildOmnichainIndexingStatusSnapshot(): Promise { return localPonderClient.buildCrossChainIndexingStatusSnapshot(); diff --git a/packages/ponder-sdk/README.md b/packages/ponder-sdk/README.md index 1921e6521..ee1b7100d 100644 --- a/packages/ponder-sdk/README.md +++ b/packages/ponder-sdk/README.md @@ -1,3 +1,3 @@ # Ponder SDK -This package is a set of libraries enabling smooth interaction with Ponder application and data, including shared types, data processing (such as validating data and enforcing invariants), and Ponder-oriented helper functions. +This package is a set of utilities for interacting with a Ponder app. diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index 5b005c1e7..5c93959a8 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -2,7 +2,7 @@ "name": "@ensnode/ponder-sdk", "version": "1.3.1", "type": "module", - "description": "A utility library for interacting with Ponder application and data", + "description": "A library of utilities for interacting with Ponder apps.", "license": "MIT", "repository": { "type": "git", diff --git a/packages/ponder-sdk/tsup.config.ts b/packages/ponder-sdk/tsup.config.ts index 5e48f2e9d..cf9614061 100644 --- a/packages/ponder-sdk/tsup.config.ts +++ b/packages/ponder-sdk/tsup.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ sourcemap: true, dts: true, clean: true, - external: ["@ensnode/ensnode-sdk", "ponder", "viem", "zod"], + external: ["ponder", "viem", "zod"], noExternal: ["parse-prometheus-text-format"], outDir: "./dist", });