From 922db1b21df57fcad9fc19c10fa8957814bdf774 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 11:34:42 +0100 Subject: [PATCH 01/18] Create `ponder-sdk` package --- README.md | 10 ++-- packages/ponder-sdk/LICENSE | 21 +++++++++ packages/ponder-sdk/README.md | 3 ++ packages/ponder-sdk/package.json | 68 ++++++++++++++++++++++++++++ packages/ponder-sdk/src/index.ts | 0 packages/ponder-sdk/tsconfig.json | 4 ++ packages/ponder-sdk/tsup.config.ts | 16 +++++++ packages/ponder-sdk/vitest.config.ts | 11 +++++ pnpm-lock.yaml | 34 ++++++++++++++ 9 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 packages/ponder-sdk/LICENSE create mode 100644 packages/ponder-sdk/README.md create mode 100644 packages/ponder-sdk/package.json create mode 100644 packages/ponder-sdk/src/index.ts create mode 100644 packages/ponder-sdk/tsconfig.json create mode 100644 packages/ponder-sdk/tsup.config.ts create mode 100644 packages/ponder-sdk/vitest.config.ts diff --git a/README.md b/README.md index 99b35f564..ae59062d9 100644 --- a/README.md +++ b/README.md @@ -173,15 +173,19 @@ TypeScript library for interacting with the [ENSRainbow API](apps/ensrainbow). ### [`packages/ensnode-schema`](packages/ensnode-schema) -Shared Drizzle schema definitions used by ENSNode +Shared Drizzle schema definitions used by ENSNode. + +### [`packages/ponder-sdk`](packages/ponder-subgraph) + +A utility library for interacting with Ponder application and data. ### [`packages/ponder-subgraph`](packages/ponder-subgraph) -Subgraph-compatible GraphQL API +Subgraph-compatible GraphQL API. ### [`packages/shared-configs`](packages/shared-configs) -Shared internal configuration files +Shared internal configuration files. ## Docs diff --git a/packages/ponder-sdk/LICENSE b/packages/ponder-sdk/LICENSE new file mode 100644 index 000000000..24d66814d --- /dev/null +++ b/packages/ponder-sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 NameHash + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/ponder-sdk/README.md b/packages/ponder-sdk/README.md new file mode 100644 index 000000000..1921e6521 --- /dev/null +++ b/packages/ponder-sdk/README.md @@ -0,0 +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. diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json new file mode 100644 index 000000000..734f4aae1 --- /dev/null +++ b/packages/ponder-sdk/package.json @@ -0,0 +1,68 @@ +{ + "name": "@ensnode/ponder-sdk", + "version": "1.5.1", + "type": "module", + "description": "A utility library for interacting with Ponder application and data", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/namehash/ensnode.git", + "directory": "packages/ponder-metadata-api" + }, + "homepage": "https://github.com/namehash/ensnode/tree/main/packages/ponder-metadata-api", + "keywords": [ + "ENSNode", + "Ponder" + ], + "files": [ + "dist" + ], + "exports": { + ".": "./src/index.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "scripts": { + "prepublish": "tsup", + "typecheck": "tsc --noEmit", + "test": "vitest", + "lint": "biome check --write .", + "lint:ci": "biome ci" + }, + "dependencies": { + "parse-prometheus-text-format": "^1.1.1" + }, + "devDependencies": { + "@ensnode/ensnode-sdk": "workspace:*", + "@ensnode/shared-configs": "workspace:*", + "@types/node": "catalog:", + "ponder": "catalog:", + "tsup": "catalog:", + "typescript": "catalog:", + "viem": "catalog:", + "vitest": "catalog:", + "zod": "catalog:" + }, + "peerDependencies": { + "@ensnode/ensnode-sdk": "workspace:*", + "ponder": "catalog:", + "viem": "catalog:", + "zod": "catalog:" + } +} diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/ponder-sdk/tsconfig.json b/packages/ponder-sdk/tsconfig.json new file mode 100644 index 000000000..8eddfe35c --- /dev/null +++ b/packages/ponder-sdk/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@ensnode/shared-configs/tsconfig.ponder.json", + "include": ["src/**/*"] +} diff --git a/packages/ponder-sdk/tsup.config.ts b/packages/ponder-sdk/tsup.config.ts new file mode 100644 index 000000000..5e48f2e9d --- /dev/null +++ b/packages/ponder-sdk/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + platform: "neutral", + format: ["esm", "cjs"], + target: "es2022", + bundle: true, + splitting: false, + sourcemap: true, + dts: true, + clean: true, + external: ["@ensnode/ensnode-sdk", "ponder", "viem", "zod"], + noExternal: ["parse-prometheus-text-format"], + outDir: "./dist", +}); diff --git a/packages/ponder-sdk/vitest.config.ts b/packages/ponder-sdk/vitest.config.ts new file mode 100644 index 000000000..806cbcc17 --- /dev/null +++ b/packages/ponder-sdk/vitest.config.ts @@ -0,0 +1,11 @@ +import { resolve } from "node:path"; + +import { defineProject } from "vitest/config"; + +export default defineProject({ + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6c709764..ed4739277 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1048,6 +1048,40 @@ importers: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + packages/ponder-sdk: + dependencies: + parse-prometheus-text-format: + 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 + '@types/node': + specifier: 'catalog:' + version: 24.10.9 + ponder: + specifier: 'catalog:' + version: 0.16.2(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(hono@4.11.7)(lightningcss@1.30.2)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) + tsup: + specifier: 'catalog:' + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: 'catalog:' + version: 5.9.3 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@4.3.6) + vitest: + specifier: 'catalog:' + version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + zod: + specifier: 'catalog:' + version: 4.3.6 + packages/ponder-subgraph: dependencies: '@escape.tech/graphql-armor-max-aliases': From e3347d2530878032de0f373a64278cf4fde0405a Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 12:03:07 +0100 Subject: [PATCH 02/18] Create `PonderClient` in `ponder-sdk` --- packages/ponder-sdk/src/block-ref.ts | 16 +++++++ packages/ponder-sdk/src/chain.ts | 28 ++++++++++++ packages/ponder-sdk/src/client.test.ts | 33 ++++++++++++++ packages/ponder-sdk/src/client.ts | 19 ++++++++ packages/ponder-sdk/src/index.ts | 5 +++ packages/ponder-sdk/src/mocks.ts | 56 ++++++++++++++++++++++++ packages/ponder-sdk/src/ponder-status.ts | 39 +++++++++++++++++ packages/ponder-sdk/src/shared.ts | 35 +++++++++++++++ 8 files changed, 231 insertions(+) create mode 100644 packages/ponder-sdk/src/block-ref.ts create mode 100644 packages/ponder-sdk/src/chain.ts create mode 100644 packages/ponder-sdk/src/client.test.ts create mode 100644 packages/ponder-sdk/src/client.ts create mode 100644 packages/ponder-sdk/src/mocks.ts create mode 100644 packages/ponder-sdk/src/ponder-status.ts create mode 100644 packages/ponder-sdk/src/shared.ts diff --git a/packages/ponder-sdk/src/block-ref.ts b/packages/ponder-sdk/src/block-ref.ts new file mode 100644 index 000000000..40089cb69 --- /dev/null +++ b/packages/ponder-sdk/src/block-ref.ts @@ -0,0 +1,16 @@ +import { z } from "zod/v4"; + +import { blockNumberSchema } from "./chain"; +import { unixTimestampSchema } from "./shared"; + +export const blockRefSchema = z.object({ + number: blockNumberSchema, + timestamp: unixTimestampSchema, +}); + +/** + * BlockRef + * + * Describes a single block. + */ +export type BlockRef = z.infer; diff --git a/packages/ponder-sdk/src/chain.ts b/packages/ponder-sdk/src/chain.ts new file mode 100644 index 000000000..c3cd6055b --- /dev/null +++ b/packages/ponder-sdk/src/chain.ts @@ -0,0 +1,28 @@ +import type { z } from "zod/v4"; + +import { nonnegativeIntegerSchema } from "./shared"; + +//// Block Number + +export const blockNumberSchema = nonnegativeIntegerSchema; + +/** + * Block Number + * + * Guaranteed to be a non-negative integer. + */ +export type BlockNumber = z.infer; + +// Chain ID + +export const chainIdSchema = nonnegativeIntegerSchema; + +/** + * Chain ID + * + * Represents a unique identifier for a chain. + * Guaranteed to be a positive integer. + * + * Chain id standards are organized by the Ethereum Community @ https://github.com/ethereum-lists/chains + **/ +export type ChainId = z.infer; diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts new file mode 100644 index 000000000..6457ec9ce --- /dev/null +++ b/packages/ponder-sdk/src/client.test.ts @@ -0,0 +1,33 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { PonderClient } from "./client"; +import { invalidPonderStatusResponse, validPonderStatusResponse } from "./mocks"; + +// Mock fetch globally +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +describe("Ponder Client", () => { + let ponderClient: PonderClient; + + beforeEach(() => { + mockFetch.mockClear(); + ponderClient = new PonderClient(new URL("https://example.com")); + }); + + describe("status()", () => { + it("should handle valid Ponder status response", async () => { + mockFetch.mockResolvedValueOnce({ ok: true, json: async () => validPonderStatusResponse }); + + const status = await ponderClient.status(); + + expect(status).toEqual(validPonderStatusResponse); + }); + + it("should handle invalid Ponder status response", async () => { + mockFetch.mockResolvedValueOnce({ ok: true, json: async () => invalidPonderStatusResponse }); + + expect(ponderClient.status()).rejects.toThrowError(/Invalid Ponder status response/); + }); + }); +}); diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts new file mode 100644 index 000000000..a5beab7c3 --- /dev/null +++ b/packages/ponder-sdk/src/client.ts @@ -0,0 +1,19 @@ +import { type PonderStatusResponse, parsePonderStatusResponse } from "./ponder-status"; + +export class PonderClient { + constructor(private baseUrl: URL) {} + + /** + * Get Ponder Status + * + * @returns Validated Ponder Status response + * @throws Error if the response is invalid + */ + async status(): Promise { + const requestUrl = new URL("/status", this.baseUrl); + const response = await fetch(requestUrl); + const data = await response.json(); + + return parsePonderStatusResponse(data); + } +} diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index e69de29bb..bf38a864d 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -0,0 +1,5 @@ +export type { BlockRef } from "./block-ref"; +export type { BlockNumber, ChainId } from "./chain"; +export * from "./client"; +export type { PonderStatusResponse } from "./ponder-status"; +export type { UnixTimestamp } from "./shared"; diff --git a/packages/ponder-sdk/src/mocks.ts b/packages/ponder-sdk/src/mocks.ts new file mode 100644 index 000000000..aed76720e --- /dev/null +++ b/packages/ponder-sdk/src/mocks.ts @@ -0,0 +1,56 @@ +import type { PonderStatusResponse } from "./ponder-status"; + +export const validPonderStatusResponse = { + "1": { + id: 1, + block: { + number: 24375715, + timestamp: 1770114251, + }, + }, + "10": { + id: 10, + block: { + number: 147257736, + timestamp: 1770114249, + }, + }, + "8453": { + id: 8453, + block: { + number: 41662451, + timestamp: 1770114249, + }, + }, + "42161": { + id: 42161, + block: { + number: 428158835, + timestamp: 1770114250, + }, + }, + "59144": { + id: 59144, + block: { + number: 28577755, + timestamp: 1770114244, + }, + }, + "534352": { + id: 534352, + block: { + number: 29352537, + timestamp: 1770114250, + }, + }, +} satisfies PonderStatusResponse; + +export const invalidPonderStatusResponse = { + "1": { + id: 1, + block: { + number: -24375715, // Invalid negative block number + timestamp: 1770114251, + }, + }, +}; diff --git a/packages/ponder-sdk/src/ponder-status.ts b/packages/ponder-sdk/src/ponder-status.ts new file mode 100644 index 000000000..92310594e --- /dev/null +++ b/packages/ponder-sdk/src/ponder-status.ts @@ -0,0 +1,39 @@ +/** + * Ponder Status + * + * Defines the structure and validation for the Ponder status response. + * @see https://ponder.sh/docs/advanced/observability#indexing-status + */ + +import { prettifyError, z } from "zod/v4"; + +import { blockRefSchema } from "./block-ref"; +import { chainIdSchema } from "./chain"; + +const ponderStatusChainNameSchema = z.string(); + +const ponderStatusChainSchema = z.object({ + id: chainIdSchema, + block: blockRefSchema, +}); + +const ponderStatusResponseSchema = z.record(ponderStatusChainNameSchema, ponderStatusChainSchema); + +/** + * Get validated Ponder status response. + * + * @param response Unvalidated Ponder status response. + * @returns Validated Ponder status response. + * @throws Error if the response is invalid. + */ +export function parsePonderStatusResponse(response: unknown) { + const parsedResponse = ponderStatusResponseSchema.safeParse(response); + + if (!parsedResponse.success) { + throw new Error(`Invalid Ponder status response: ${prettifyError(parsedResponse.error)}`); + } + + return parsedResponse.data; +} + +export type PonderStatusResponse = z.infer; diff --git a/packages/ponder-sdk/src/shared.ts b/packages/ponder-sdk/src/shared.ts new file mode 100644 index 000000000..5445ed826 --- /dev/null +++ b/packages/ponder-sdk/src/shared.ts @@ -0,0 +1,35 @@ +import { z } from "zod/v4"; + +// Numbers + +export const numberSchema = z.number({ error: `Value must be a number` }); + +export const integerSchema = numberSchema.int({ error: `Value must be an integer` }); + +export const nonnegativeNumberSchema = numberSchema.nonnegative({ + error: `Value must be non-negative`, +}); + +export const positiveNumberSchema = numberSchema.positive({ error: `Value must be positive` }); + +export const nonnegativeIntegerSchema = integerSchema.nonnegative({ + error: `Value must be a non-negative integer`, +}); + +export const positiveIntegerSchema = integerSchema.positive({ + error: `Value must be a positive integer`, +}); + +//// Unix Timestamp +export const unixTimestampSchema = integerSchema; + +/** + * 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 = z.infer; From eb4cb1786bcedd10254e70a149c3b1a88ae40d2b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 14:33:00 +0100 Subject: [PATCH 03/18] Add `status()`method to `PonderClient` in `ponder-sdk` --- packages/ponder-sdk/package.json | 1 - packages/ponder-sdk/src/api/status.ts | 31 +++++++++++++ packages/ponder-sdk/src/client.test.ts | 57 ++++++++++++++++++++---- packages/ponder-sdk/src/client.ts | 23 +++++++--- packages/ponder-sdk/src/index.ts | 5 +-- packages/ponder-sdk/src/mocks.ts | 12 ++++- packages/ponder-sdk/src/ponder-status.ts | 53 +++++++++++++++++----- pnpm-lock.yaml | 6 +-- 8 files changed, 156 insertions(+), 32 deletions(-) create mode 100644 packages/ponder-sdk/src/api/status.ts diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index 734f4aae1..69ebd1834 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -49,7 +49,6 @@ "parse-prometheus-text-format": "^1.1.1" }, "devDependencies": { - "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/shared-configs": "workspace:*", "@types/node": "catalog:", "ponder": "catalog:", diff --git a/packages/ponder-sdk/src/api/status.ts b/packages/ponder-sdk/src/api/status.ts new file mode 100644 index 000000000..3a0d4b8eb --- /dev/null +++ b/packages/ponder-sdk/src/api/status.ts @@ -0,0 +1,31 @@ +import type { BlockRef } from "../block-ref"; +import type { ChainId } from "../chain"; +import type { PonderStatusResponse } from "../ponder-status"; + +/** + * Ponder Status for each indexed chain. + * + * Guarantees: + * - Contains status for all indexed chain IDs. + */ +export interface PonderStatus { + chains: Map; +} + +/** + * Build PonderStatus from validated PonderStatusResponse. + * + * @param data Validated PonderStatusResponse. + * @returns PonderStatus built from the response data. + */ +export function buildPonderStatus(data: PonderStatusResponse): PonderStatus { + const chains = new Map(); + + for (const [, chainData] of Object.entries(data)) { + chains.set(chainData.id, chainData.block); + } + + return { + chains, + }; +} diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 6457ec9ce..5596749c8 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -1,33 +1,74 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { buildPonderStatus } from "./api/status"; +import type { ChainId } from "./chain"; import { PonderClient } from "./client"; -import { invalidPonderStatusResponse, validPonderStatusResponse } from "./mocks"; +import { + invalidPonderStatusResponseNegativeBlockNumber, + validPonderStatusResponse, + validPonderStatusResponseMinimal, +} from "./mocks"; +import type { PonderStatusResponse } from "./ponder-status"; // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch; -describe("Ponder Client", () => { - let ponderClient: PonderClient; +// Helper function to extract indexed chain IDs from a mocked PonderStatusResponse +const getIndexedChainIds = (response: PonderStatusResponse): Set => + new Set(Object.values(response).map((chain) => chain.id)); +describe("Ponder Client", () => { beforeEach(() => { mockFetch.mockClear(); - ponderClient = new PonderClient(new URL("https://example.com")); }); describe("status()", () => { it("should handle valid Ponder status response", async () => { + // Arrange mockFetch.mockResolvedValueOnce({ ok: true, json: async () => validPonderStatusResponse }); + const indexedChainIds = getIndexedChainIds(validPonderStatusResponse); + const ponderClient = new PonderClient(new URL("http://localhost:3000"), indexedChainIds); + + // Act const status = await ponderClient.status(); - expect(status).toEqual(validPonderStatusResponse); + // Assert + expect(status).toEqual(buildPonderStatus(validPonderStatusResponse)); }); - it("should handle invalid Ponder status response", async () => { - mockFetch.mockResolvedValueOnce({ ok: true, json: async () => invalidPonderStatusResponse }); + describe("Invalid response handling", () => { + it("should handle invalid block numbers in the response", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => invalidPonderStatusResponseNegativeBlockNumber, + }); + + const indexedChainIds = getIndexedChainIds(invalidPonderStatusResponseNegativeBlockNumber); + const ponderClient = new PonderClient(new URL("http://localhost:3000"), indexedChainIds); + + expect(ponderClient.status()).rejects.toThrowError( + /Invalid Ponder status response.*Value must be a non-negative integer/, + ); + }); + + it("should handle indexed chain IDs that are not present in the response", async () => { + // Arrange + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => validPonderStatusResponseMinimal, + }); + + // Set indexed chain IDs to be a wider set than the chains present in the response + const indexedChainIds = getIndexedChainIds(validPonderStatusResponse); + const ponderClient = new PonderClient(new URL("http://localhost:3000"), indexedChainIds); - expect(ponderClient.status()).rejects.toThrowError(/Invalid Ponder status response/); + // Act & Assert + await expect(ponderClient.status()).rejects.toThrowError( + /Ponder Status response is missing status for indexed chain IDs: 10, 8453, 42161, 59144, 534352/, + ); + }); }); }); }); diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index a5beab7c3..791b0dfb4 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -1,7 +1,18 @@ -import { type PonderStatusResponse, parsePonderStatusResponse } from "./ponder-status"; +import { buildPonderStatus, type PonderStatus } from "./api/status"; +import type { ChainId } from "./chain"; +import { validatePonderStatusResponse } from "./ponder-status"; +/** + * PonderClient for interacting with Ponder app endpoints. + * + * Requires the set of indexed chain IDs to validate the status response against the expected chains. + * This ensures that the client can detect if the Ponder instance is missing status for any of the indexed chains. + */ export class PonderClient { - constructor(private baseUrl: URL) {} + constructor( + private baseUrl: URL, + private indexedChainIds: Set, + ) {} /** * Get Ponder Status @@ -9,11 +20,13 @@ export class PonderClient { * @returns Validated Ponder Status response * @throws Error if the response is invalid */ - async status(): Promise { + async status(): Promise { const requestUrl = new URL("/status", this.baseUrl); const response = await fetch(requestUrl); - const data = await response.json(); + const responseData = await response.json(); - return parsePonderStatusResponse(data); + const validatedData = validatePonderStatusResponse(responseData, this.indexedChainIds); + + return buildPonderStatus(validatedData); } } diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index bf38a864d..cc30b0c03 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -1,5 +1,2 @@ -export type { BlockRef } from "./block-ref"; -export type { BlockNumber, ChainId } from "./chain"; +export type { PonderStatus } from "./api/status"; export * from "./client"; -export type { PonderStatusResponse } from "./ponder-status"; -export type { UnixTimestamp } from "./shared"; diff --git a/packages/ponder-sdk/src/mocks.ts b/packages/ponder-sdk/src/mocks.ts index aed76720e..d4fc59c51 100644 --- a/packages/ponder-sdk/src/mocks.ts +++ b/packages/ponder-sdk/src/mocks.ts @@ -45,7 +45,17 @@ export const validPonderStatusResponse = { }, } satisfies PonderStatusResponse; -export const invalidPonderStatusResponse = { +export const validPonderStatusResponseMinimal = { + "1": { + id: 1, + block: { + number: 4375715, + timestamp: 1770114251, + }, + }, +}; + +export const invalidPonderStatusResponseNegativeBlockNumber = { "1": { id: 1, block: { diff --git a/packages/ponder-sdk/src/ponder-status.ts b/packages/ponder-sdk/src/ponder-status.ts index 92310594e..b420bb6e7 100644 --- a/packages/ponder-sdk/src/ponder-status.ts +++ b/packages/ponder-sdk/src/ponder-status.ts @@ -6,9 +6,10 @@ */ import { prettifyError, z } from "zod/v4"; +import type { ParsePayload } from "zod/v4/core"; import { blockRefSchema } from "./block-ref"; -import { chainIdSchema } from "./chain"; +import { type ChainId, chainIdSchema } from "./chain"; const ponderStatusChainNameSchema = z.string(); @@ -17,23 +18,55 @@ const ponderStatusChainSchema = z.object({ block: blockRefSchema, }); +export type PonderStatusChain = z.infer; + +/** + * Schema for validating raw response from Ponder Status endpoint at `GET /status`. + */ const ponderStatusResponseSchema = z.record(ponderStatusChainNameSchema, ponderStatusChainSchema); /** - * Get validated Ponder status response. + * Validated Ponder Status response. + */ +export type PonderStatusResponse = z.infer; + +function invariant_includesStatusForEachIndexedChainId( + ctx: ParsePayload, + indexedChainIds: Set, +) { + const ponderStatusChainIds = new Set(Object.values(ctx.value).map((chain) => chain.id)); + + if (!indexedChainIds.isSubsetOf(ponderStatusChainIds)) { + const missingIndexedChainIds = [...indexedChainIds].filter( + (id) => !ponderStatusChainIds.has(id), + ); + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `Ponder Status response is missing status for indexed chain IDs: ${missingIndexedChainIds.join(", ")}`, + }); + } +} + +/** + * Validate Ponder Status response. * * @param response Unvalidated Ponder status response. + * @param indexedChainIds Set of chain IDs that are indexed by the Ponder instance. * @returns Validated Ponder status response. - * @throws Error if the response is invalid. + * @throws Error if response is invalid or invariants check fails. */ -export function parsePonderStatusResponse(response: unknown) { - const parsedResponse = ponderStatusResponseSchema.safeParse(response); +export function validatePonderStatusResponse( + response: unknown, + indexedChainIds: Set, +): PonderStatusResponse { + const validation = ponderStatusResponseSchema + .check((ctx) => invariant_includesStatusForEachIndexedChainId(ctx, indexedChainIds)) + .safeParse(response); - if (!parsedResponse.success) { - throw new Error(`Invalid Ponder status response: ${prettifyError(parsedResponse.error)}`); + if (!validation.success) { + throw new Error(`Invalid Ponder status response: ${prettifyError(validation.error)}`); } - return parsedResponse.data; + return validation.data; } - -export type PonderStatusResponse = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed4739277..68fe761d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1050,13 +1050,13 @@ importers: packages/ponder-sdk: dependencies: + '@ensnode/ensnode-sdk': + specifier: workspace:* + version: link:../ensnode-sdk parse-prometheus-text-format: 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 9af69921f91b8240e690ebb7434694c27000d16e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 14:42:16 +0100 Subject: [PATCH 04/18] docs(changeset): Introduce `ponder-sdk` package, including `PonderClient` implementation. --- .changeset/cozy-turkeys-fix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cozy-turkeys-fix.md diff --git a/.changeset/cozy-turkeys-fix.md b/.changeset/cozy-turkeys-fix.md new file mode 100644 index 000000000..dd6a89942 --- /dev/null +++ b/.changeset/cozy-turkeys-fix.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ponder-sdk": minor +--- + +Introduce `ponder-sdk` package, including `PonderClient` implementation. From f034c560abe8cad09093cc8ecc0fae2069275eb8 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:13:41 +0400 Subject: [PATCH 05/18] Update README.md Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae59062d9..48f1364c1 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ TypeScript library for interacting with the [ENSRainbow API](apps/ensrainbow). Shared Drizzle schema definitions used by ENSNode. -### [`packages/ponder-sdk`](packages/ponder-subgraph) +### [`packages/ponder-sdk`](packages/ponder-sdk) A utility library for interacting with Ponder application and data. From 2b1d283af1af7b00a5726f5255835cb66e759b62 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:16:24 +0400 Subject: [PATCH 06/18] Update packages/ponder-sdk/package.json Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- packages/ponder-sdk/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index 69ebd1834..099868144 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -7,9 +7,9 @@ "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" From ecf47f3423ea02168f0df03fd2d1063160ec993d Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:05:52 +0100 Subject: [PATCH 07/18] Apply PR feedback --- .changeset/config.json | 3 +- .github/workflows/release.yml | 1 + .github/workflows/release_preview.yml | 3 +- README.md | 4 +- packages/ponder-sdk/README.md | 2 +- packages/ponder-sdk/package.json | 12 +-- packages/ponder-sdk/src/api/status.ts | 31 ------- packages/ponder-sdk/src/block-ref.ts | 16 ---- packages/ponder-sdk/src/block.ts | 27 ++++++ packages/ponder-sdk/src/chain.ts | 15 +--- packages/ponder-sdk/src/client.test.ts | 83 +++++++++-------- packages/ponder-sdk/src/client.ts | 32 ++++--- .../src/deserialize/indexing-status.ts | 90 +++++++++++++++++++ packages/ponder-sdk/src/index.ts | 1 - packages/ponder-sdk/src/mocks.ts | 36 ++++---- .../ponder-sdk/src/{shared.ts => numbers.ts} | 14 --- packages/ponder-sdk/src/ponder-status.ts | 72 --------------- packages/ponder-sdk/src/time.ts | 17 ++++ packages/ponder-sdk/tsup.config.ts | 3 +- packages/ponder-sdk/vitest.config.ts | 7 +- 20 files changed, 228 insertions(+), 241 deletions(-) delete mode 100644 packages/ponder-sdk/src/api/status.ts delete mode 100644 packages/ponder-sdk/src/block-ref.ts create mode 100644 packages/ponder-sdk/src/block.ts create mode 100644 packages/ponder-sdk/src/deserialize/indexing-status.ts rename packages/ponder-sdk/src/{shared.ts => numbers.ts} (62%) delete mode 100644 packages/ponder-sdk/src/ponder-status.ts create mode 100644 packages/ponder-sdk/src/time.ts diff --git a/.changeset/config.json b/.changeset/config.json index 4638171c0..e6b4359c8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -16,8 +16,9 @@ "@ensnode/ponder-metadata", "@ensnode/ensnode-schema", "@ensnode/ensnode-react", - "@ensnode/ponder-subgraph", "@ensnode/ensnode-sdk", + "@ensnode/ponder-sdk", + "@ensnode/ponder-subgraph", "@ensnode/shared-configs", "@docs/ensnode", "@docs/ensrainbow", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b676d0294..f901fce25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,6 +104,7 @@ jobs: or .name == "@ensnode/ensrainbow-sdk" or .name == "@ensnode/namehash-ui" or .name == "@ensnode/ponder-metadata" + or .name == "@ensnode/ponder-sdk" or .name == "@ensnode/ponder-subgraph" )) diff --git a/.github/workflows/release_preview.yml b/.github/workflows/release_preview.yml index 5647dcd62..1063751ea 100644 --- a/.github/workflows/release_preview.yml +++ b/.github/workflows/release_preview.yml @@ -305,8 +305,9 @@ jobs: pnpm add @ensnode/ensrainbow-sdk@${{ needs.validate-and-prepare.outputs.dist-tag }} pnpm add @ensnode/ensnode-schema@${{ needs.validate-and-prepare.outputs.dist-tag }} pnpm add @ensnode/ensnode-sdk@${{ needs.validate-and-prepare.outputs.dist-tag }} - pnpm add @ensnode/ponder-subgraph@${{ needs.validate-and-prepare.outputs.dist-tag }} pnpm add @ensnode/ponder-metadata@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ponder-sdk@${{ needs.validate-and-prepare.outputs.dist-tag }} + pnpm add @ensnode/ponder-subgraph@${{ needs.validate-and-prepare.outputs.dist-tag }} pnpm add @ensnode/ens-referrals@${{ needs.validate-and-prepare.outputs.dist-tag }} pnpm add @ensnode/namehash-ui@${{ needs.validate-and-prepare.outputs.dist-tag }} diff --git a/README.md b/README.md index ae59062d9..2f4ffa580 100644 --- a/README.md +++ b/README.md @@ -175,9 +175,9 @@ TypeScript library for interacting with the [ENSRainbow API](apps/ensrainbow). Shared Drizzle schema definitions used by ENSNode. -### [`packages/ponder-sdk`](packages/ponder-subgraph) +### [`packages/ponder-sdk`](packages/ponder-sdk) -A utility library for interacting with Ponder application and data. +A utility library for interacting with Ponder apps and data. ### [`packages/ponder-subgraph`](packages/ponder-subgraph) diff --git a/packages/ponder-sdk/README.md b/packages/ponder-sdk/README.md index 1921e6521..8755219fe 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 libraries enabling smooth interaction with Ponder apps and data, including shared types, data processing (such as validating data and enforcing invariants), and Ponder-oriented helper functions. diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index 69ebd1834..9b8489fdf 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -2,14 +2,14 @@ "name": "@ensnode/ponder-sdk", "version": "1.5.1", "type": "module", - "description": "A utility library for interacting with Ponder application and data", + "description": "A utility library for interacting with Ponder apps and data.", "license": "MIT", "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" @@ -39,15 +39,12 @@ "types": "./dist/index.d.ts" }, "scripts": { - "prepublish": "tsup", + "prepublishOnly": "tsup", "typecheck": "tsc --noEmit", "test": "vitest", "lint": "biome check --write .", "lint:ci": "biome ci" }, - "dependencies": { - "parse-prometheus-text-format": "^1.1.1" - }, "devDependencies": { "@ensnode/shared-configs": "workspace:*", "@types/node": "catalog:", @@ -59,7 +56,6 @@ "zod": "catalog:" }, "peerDependencies": { - "@ensnode/ensnode-sdk": "workspace:*", "ponder": "catalog:", "viem": "catalog:", "zod": "catalog:" diff --git a/packages/ponder-sdk/src/api/status.ts b/packages/ponder-sdk/src/api/status.ts deleted file mode 100644 index 3a0d4b8eb..000000000 --- a/packages/ponder-sdk/src/api/status.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BlockRef } from "../block-ref"; -import type { ChainId } from "../chain"; -import type { PonderStatusResponse } from "../ponder-status"; - -/** - * Ponder Status for each indexed chain. - * - * Guarantees: - * - Contains status for all indexed chain IDs. - */ -export interface PonderStatus { - chains: Map; -} - -/** - * Build PonderStatus from validated PonderStatusResponse. - * - * @param data Validated PonderStatusResponse. - * @returns PonderStatus built from the response data. - */ -export function buildPonderStatus(data: PonderStatusResponse): PonderStatus { - const chains = new Map(); - - for (const [, chainData] of Object.entries(data)) { - chains.set(chainData.id, chainData.block); - } - - return { - chains, - }; -} diff --git a/packages/ponder-sdk/src/block-ref.ts b/packages/ponder-sdk/src/block-ref.ts deleted file mode 100644 index 40089cb69..000000000 --- a/packages/ponder-sdk/src/block-ref.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod/v4"; - -import { blockNumberSchema } from "./chain"; -import { unixTimestampSchema } from "./shared"; - -export const blockRefSchema = z.object({ - number: blockNumberSchema, - timestamp: unixTimestampSchema, -}); - -/** - * BlockRef - * - * Describes a single block. - */ -export type BlockRef = z.infer; diff --git a/packages/ponder-sdk/src/block.ts b/packages/ponder-sdk/src/block.ts new file mode 100644 index 000000000..d121c87d9 --- /dev/null +++ b/packages/ponder-sdk/src/block.ts @@ -0,0 +1,27 @@ +import { z } from "zod/v4"; + +import { nonnegativeIntegerSchema } from "./numbers"; +import { unixTimestampSchema } from "./time"; + +//// Block Number + +export const blockNumberSchema = nonnegativeIntegerSchema; + +/** + * Block Number + * + * Guaranteed to be a non-negative integer. + */ +export type BlockNumber = z.infer; + +export const blockRefSchema = z.object({ + number: blockNumberSchema, + timestamp: unixTimestampSchema, +}); + +/** + * BlockRef + * + * Describes a single block. + */ +export type BlockRef = z.infer; diff --git a/packages/ponder-sdk/src/chain.ts b/packages/ponder-sdk/src/chain.ts index c3cd6055b..d27e6c18e 100644 --- a/packages/ponder-sdk/src/chain.ts +++ b/packages/ponder-sdk/src/chain.ts @@ -1,21 +1,10 @@ import type { z } from "zod/v4"; -import { nonnegativeIntegerSchema } from "./shared"; - -//// Block Number - -export const blockNumberSchema = nonnegativeIntegerSchema; - -/** - * Block Number - * - * Guaranteed to be a non-negative integer. - */ -export type BlockNumber = z.infer; +import { positiveIntegerSchema } from "./numbers"; // Chain ID -export const chainIdSchema = nonnegativeIntegerSchema; +export const chainIdSchema = positiveIntegerSchema; /** * Chain ID diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 5596749c8..3f1dbc6cc 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -1,72 +1,71 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { buildPonderStatus } from "./api/status"; -import type { ChainId } from "./chain"; import { PonderClient } from "./client"; +import { deserializePonderIndexingStatus } from "./deserialize/indexing-status"; import { - invalidPonderStatusResponseNegativeBlockNumber, - validPonderStatusResponse, - validPonderStatusResponseMinimal, + mockSerializedPonderIndexingStatusInvalidBlockNumber, + mockSerializedPonderIndexingStatusInvalidChainId, + mockSerializedPonderIndexingStatusValid, } from "./mocks"; -import type { PonderStatusResponse } from "./ponder-status"; -// Mock fetch globally -const mockFetch = vi.fn(); -global.fetch = mockFetch; - -// Helper function to extract indexed chain IDs from a mocked PonderStatusResponse -const getIndexedChainIds = (response: PonderStatusResponse): Set => - new Set(Object.values(response).map((chain) => chain.id)); +// Mock Fetch API +const mockFetch = vi.fn(); describe("Ponder Client", () => { beforeEach(() => { - mockFetch.mockClear(); + vi.stubGlobal("fetch", mockFetch); + }); + + afterAll(() => { + vi.unstubAllGlobals(); }); describe("status()", () => { it("should handle valid Ponder status response", async () => { // Arrange - mockFetch.mockResolvedValueOnce({ ok: true, json: async () => validPonderStatusResponse }); - - const indexedChainIds = getIndexedChainIds(validPonderStatusResponse); - const ponderClient = new PonderClient(new URL("http://localhost:3000"), indexedChainIds); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockSerializedPonderIndexingStatusValid), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); - // Act - const status = await ponderClient.status(); + const ponderClient = new PonderClient(new URL("http://localhost:3000")); - // Assert - expect(status).toEqual(buildPonderStatus(validPonderStatusResponse)); + // Act & Assert + await expect(ponderClient.status()).resolves.toStrictEqual( + deserializePonderIndexingStatus(mockSerializedPonderIndexingStatusValid), + ); }); describe("Invalid response handling", () => { it("should handle invalid block numbers in the response", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => invalidPonderStatusResponseNegativeBlockNumber, - }); + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockSerializedPonderIndexingStatusInvalidBlockNumber), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); - const indexedChainIds = getIndexedChainIds(invalidPonderStatusResponseNegativeBlockNumber); - const ponderClient = new PonderClient(new URL("http://localhost:3000"), indexedChainIds); + const ponderClient = new PonderClient(new URL("http://localhost:3000")); - expect(ponderClient.status()).rejects.toThrowError( - /Invalid Ponder status response.*Value must be a non-negative integer/, + await expect(ponderClient.status()).rejects.toThrowError( + /Invalid serialized Ponder Indexing Status.*Value must be a non-negative integer/, ); }); - it("should handle indexed chain IDs that are not present in the response", async () => { - // Arrange - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => validPonderStatusResponseMinimal, - }); + it("should handle invalid chain IDs in the response", async () => { + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockSerializedPonderIndexingStatusInvalidChainId), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); - // Set indexed chain IDs to be a wider set than the chains present in the response - const indexedChainIds = getIndexedChainIds(validPonderStatusResponse); - const ponderClient = new PonderClient(new URL("http://localhost:3000"), indexedChainIds); + const ponderClient = new PonderClient(new URL("http://localhost:3000")); - // Act & Assert await expect(ponderClient.status()).rejects.toThrowError( - /Ponder Status response is missing status for indexed chain IDs: 10, 8453, 42161, 59144, 534352/, + /Invalid serialized Ponder Indexing Status.*Value must be a positive integer/, ); }); }); diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index 791b0dfb4..3606dc129 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -1,32 +1,30 @@ -import { buildPonderStatus, type PonderStatus } from "./api/status"; -import type { ChainId } from "./chain"; -import { validatePonderStatusResponse } from "./ponder-status"; +import { + deserializePonderIndexingStatus, + type PonderIndexingStatus, +} from "./deserialize/indexing-status"; /** * PonderClient for interacting with Ponder app endpoints. - * - * Requires the set of indexed chain IDs to validate the status response against the expected chains. - * This ensures that the client can detect if the Ponder instance is missing status for any of the indexed chains. */ export class PonderClient { - constructor( - private baseUrl: URL, - private indexedChainIds: Set, - ) {} + constructor(private baseUrl: URL) {} /** - * Get Ponder Status + * Get Ponder Indexing Status * - * @returns Validated Ponder Status response - * @throws Error if the response is invalid + * @returns Ponder Indexing Status. + * @throws Error if the response could not be fetched or was invalid. */ - async status(): Promise { + async status(): Promise { const requestUrl = new URL("/status", this.baseUrl); const response = await fetch(requestUrl); - const responseData = await response.json(); - const validatedData = validatePonderStatusResponse(responseData, this.indexedChainIds); + if (!response.ok) { + throw new Error(`Failed to fetch Ponder status: ${response.status} ${response.statusText}`); + } + + const responseData = await response.json(); - return buildPonderStatus(validatedData); + return deserializePonderIndexingStatus(responseData); } } diff --git a/packages/ponder-sdk/src/deserialize/indexing-status.ts b/packages/ponder-sdk/src/deserialize/indexing-status.ts new file mode 100644 index 000000000..a19df3892 --- /dev/null +++ b/packages/ponder-sdk/src/deserialize/indexing-status.ts @@ -0,0 +1,90 @@ +/** + * Ponder Indexing Status + * + * Defines the structure and validation for the Ponder Indexing Status response. + * @see https://ponder.sh/docs/advanced/observability#indexing-status + */ + +import { prettifyError, z } from "zod/v4"; + +import type { BlockRef } from "../block"; +import { blockRefSchema } from "../block"; +import type { ChainId } from "../chain"; +import { chainIdSchema } from "../chain"; + +const schemaSerializedChainName = z.string(); + +const schemaSerializedChainBlockRef = z.object({ + id: chainIdSchema, + block: blockRefSchema, +}); + +export type SerializedChainBlockRef = z.infer; + +/** + * Schema describing response at `GET /status`. + */ +export const schemaSerializedPonderIndexingStatus = z.record( + schemaSerializedChainName, + schemaSerializedChainBlockRef, +); + +/** + * Serialized Ponder Indexing Status. + */ +export type SerializedPonderIndexingStatus = z.infer; + +/** + * Ponder Indexing Status + * + * Represents chains indexing status in Ponder application. + */ +export interface PonderIndexingStatus { + /** + * Map of indexed chain IDs to their block reference. + * + * Guarantees: + * - Includes entry for at least one indexed chain. + * - BlockRef corresponds to either: + * - The first block to be indexed (when chain indexing is currently queued). + * - The last indexed block (when chain indexing is currently in progress). + */ + chains: Map; +} + +/** + * Build Ponder Indexing Status + * + * @param data Validated serialized Ponder Indexing Status. + * @returns Ponder Indexing Status. + */ +function buildPonderIndexingStatus(data: SerializedPonderIndexingStatus): PonderIndexingStatus { + const chains = new Map(); + + for (const [, chainData] of Object.entries(data)) { + chains.set(chainData.id, chainData.block); + } + + return { + chains, + }; +} + +/** + * Deserialize Ponder Indexing Status. + * + * @param response Maybe unvalidated Ponder Indexing Status. + * @returns Ponder Indexing Status. + * @throws Error if response is invalid. + */ +export function deserializePonderIndexingStatus(response: unknown): PonderIndexingStatus { + const validation = schemaSerializedPonderIndexingStatus.safeParse(response); + + if (!validation.success) { + throw new Error( + `Invalid serialized Ponder Indexing Status: ${prettifyError(validation.error)}`, + ); + } + + return buildPonderIndexingStatus(validation.data); +} diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index cc30b0c03..5ec76921e 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -1,2 +1 @@ -export type { PonderStatus } from "./api/status"; export * from "./client"; diff --git a/packages/ponder-sdk/src/mocks.ts b/packages/ponder-sdk/src/mocks.ts index d4fc59c51..38926232a 100644 --- a/packages/ponder-sdk/src/mocks.ts +++ b/packages/ponder-sdk/src/mocks.ts @@ -1,66 +1,66 @@ -import type { PonderStatusResponse } from "./ponder-status"; +import type { SerializedPonderIndexingStatus } from "./deserialize/indexing-status"; -export const validPonderStatusResponse = { - "1": { +export const mockSerializedPonderIndexingStatusValid = { + mainnet: { id: 1, block: { number: 24375715, timestamp: 1770114251, }, }, - "10": { + optimism: { id: 10, block: { number: 147257736, timestamp: 1770114249, }, }, - "8453": { + base: { id: 8453, block: { number: 41662451, timestamp: 1770114249, }, }, - "42161": { + arbitrum: { id: 42161, block: { number: 428158835, timestamp: 1770114250, }, }, - "59144": { + linea: { id: 59144, block: { number: 28577755, timestamp: 1770114244, }, }, - "534352": { + scroll: { id: 534352, block: { number: 29352537, timestamp: 1770114250, }, }, -} satisfies PonderStatusResponse; +} satisfies SerializedPonderIndexingStatus; -export const validPonderStatusResponseMinimal = { - "1": { +export const mockSerializedPonderIndexingStatusInvalidBlockNumber = { + mainnet: { id: 1, block: { - number: 4375715, + number: -24375715, // Invalid negative block number timestamp: 1770114251, }, }, -}; +} satisfies SerializedPonderIndexingStatus; -export const invalidPonderStatusResponseNegativeBlockNumber = { - "1": { - id: 1, +export const mockSerializedPonderIndexingStatusInvalidChainId = { + mainnet: { + id: 0, // Invalid non-positive chain ID block: { - number: -24375715, // Invalid negative block number + number: 24375715, timestamp: 1770114251, }, }, -}; +} satisfies SerializedPonderIndexingStatus; diff --git a/packages/ponder-sdk/src/shared.ts b/packages/ponder-sdk/src/numbers.ts similarity index 62% rename from packages/ponder-sdk/src/shared.ts rename to packages/ponder-sdk/src/numbers.ts index 5445ed826..5c0208a8f 100644 --- a/packages/ponder-sdk/src/shared.ts +++ b/packages/ponder-sdk/src/numbers.ts @@ -19,17 +19,3 @@ export const nonnegativeIntegerSchema = integerSchema.nonnegative({ export const positiveIntegerSchema = integerSchema.positive({ error: `Value must be a positive integer`, }); - -//// Unix Timestamp -export const unixTimestampSchema = integerSchema; - -/** - * 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 = z.infer; diff --git a/packages/ponder-sdk/src/ponder-status.ts b/packages/ponder-sdk/src/ponder-status.ts deleted file mode 100644 index b420bb6e7..000000000 --- a/packages/ponder-sdk/src/ponder-status.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Ponder Status - * - * Defines the structure and validation for the Ponder status response. - * @see https://ponder.sh/docs/advanced/observability#indexing-status - */ - -import { prettifyError, z } from "zod/v4"; -import type { ParsePayload } from "zod/v4/core"; - -import { blockRefSchema } from "./block-ref"; -import { type ChainId, chainIdSchema } from "./chain"; - -const ponderStatusChainNameSchema = z.string(); - -const ponderStatusChainSchema = z.object({ - id: chainIdSchema, - block: blockRefSchema, -}); - -export type PonderStatusChain = z.infer; - -/** - * Schema for validating raw response from Ponder Status endpoint at `GET /status`. - */ -const ponderStatusResponseSchema = z.record(ponderStatusChainNameSchema, ponderStatusChainSchema); - -/** - * Validated Ponder Status response. - */ -export type PonderStatusResponse = z.infer; - -function invariant_includesStatusForEachIndexedChainId( - ctx: ParsePayload, - indexedChainIds: Set, -) { - const ponderStatusChainIds = new Set(Object.values(ctx.value).map((chain) => chain.id)); - - if (!indexedChainIds.isSubsetOf(ponderStatusChainIds)) { - const missingIndexedChainIds = [...indexedChainIds].filter( - (id) => !ponderStatusChainIds.has(id), - ); - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `Ponder Status response is missing status for indexed chain IDs: ${missingIndexedChainIds.join(", ")}`, - }); - } -} - -/** - * Validate Ponder Status response. - * - * @param response Unvalidated Ponder status response. - * @param indexedChainIds Set of chain IDs that are indexed by the Ponder instance. - * @returns Validated Ponder status response. - * @throws Error if response is invalid or invariants check fails. - */ -export function validatePonderStatusResponse( - response: unknown, - indexedChainIds: Set, -): PonderStatusResponse { - const validation = ponderStatusResponseSchema - .check((ctx) => invariant_includesStatusForEachIndexedChainId(ctx, indexedChainIds)) - .safeParse(response); - - if (!validation.success) { - throw new Error(`Invalid Ponder status response: ${prettifyError(validation.error)}`); - } - - return validation.data; -} diff --git a/packages/ponder-sdk/src/time.ts b/packages/ponder-sdk/src/time.ts new file mode 100644 index 000000000..bc130264c --- /dev/null +++ b/packages/ponder-sdk/src/time.ts @@ -0,0 +1,17 @@ +import type { z } from "zod/v4"; + +import { integerSchema } from "./numbers"; + +//// Unix Timestamp +export const unixTimestampSchema = integerSchema; + +/** + * 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 = z.infer; diff --git a/packages/ponder-sdk/tsup.config.ts b/packages/ponder-sdk/tsup.config.ts index 5e48f2e9d..34e76bce8 100644 --- a/packages/ponder-sdk/tsup.config.ts +++ b/packages/ponder-sdk/tsup.config.ts @@ -10,7 +10,6 @@ export default defineConfig({ sourcemap: true, dts: true, clean: true, - external: ["@ensnode/ensnode-sdk", "ponder", "viem", "zod"], - noExternal: ["parse-prometheus-text-format"], + external: ["ponder", "viem", "zod"], // Mark peer dependencies as external outDir: "./dist", }); diff --git a/packages/ponder-sdk/vitest.config.ts b/packages/ponder-sdk/vitest.config.ts index 806cbcc17..0207a7b67 100644 --- a/packages/ponder-sdk/vitest.config.ts +++ b/packages/ponder-sdk/vitest.config.ts @@ -1,11 +1,14 @@ -import { resolve } from "node:path"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { defineProject } from "vitest/config"; +const cwd = dirname(fileURLToPath(import.meta.url)); + export default defineProject({ resolve: { alias: { - "@": resolve(__dirname, "./src"), + "@": resolve(cwd, "./src"), }, }, }); From fd420a5da600c97bc32b8a0be6174c33b82b5b91 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:12:18 +0100 Subject: [PATCH 08/18] Test case: HTTP response not OK --- packages/ponder-sdk/src/client.test.ts | 19 +++++++++++++++++++ packages/ponder-sdk/src/client.ts | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 3f1dbc6cc..81499f39c 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -69,5 +69,24 @@ describe("Ponder Client", () => { ); }); }); + + describe("HTTP error handling", () => { + it("should handle non-OK HTTP responses", async () => { + // Arrange + mockFetch.mockResolvedValueOnce( + new Response("Internal Server Error", { + status: 500, + statusText: "Internal Server Error", + }), + ); + + const ponderClient = new PonderClient(new URL("http://localhost:3000")); + + // Act & Assert + await expect(ponderClient.status()).rejects.toThrowError( + /Failed to fetch Ponder Indexing Status/, + ); + }); + }); }); }); diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index 3606dc129..3969dc9c8 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -20,7 +20,9 @@ export class PonderClient { const response = await fetch(requestUrl); if (!response.ok) { - throw new Error(`Failed to fetch Ponder status: ${response.status} ${response.statusText}`); + throw new Error( + `Failed to fetch Ponder Indexing Status: ${response.status} ${response.statusText}`, + ); } const responseData = await response.json(); From 9487dae7950fbd51e014dbe542df4ad5083e0bb6 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:23:56 +0100 Subject: [PATCH 09/18] Enforce invariant --- packages/ponder-sdk/src/client.test.ts | 15 +++++++++++++ .../src/deserialize/indexing-status.ts | 21 +++++++++++++++---- pnpm-lock.yaml | 7 ------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 81499f39c..a46562af4 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -68,6 +68,21 @@ describe("Ponder Client", () => { /Invalid serialized Ponder Indexing Status.*Value must be a positive integer/, ); }); + + it("should handle zero indexed chains in the response", async () => { + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify({}), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const ponderClient = new PonderClient(new URL("http://localhost:3000")); + + await expect(ponderClient.status()).rejects.toThrowError( + /Invalid serialized Ponder Indexing Status.*Ponder Indexing Status must include at least one indexed chai/, + ); + }); }); describe("HTTP error handling", () => { diff --git a/packages/ponder-sdk/src/deserialize/indexing-status.ts b/packages/ponder-sdk/src/deserialize/indexing-status.ts index a19df3892..046869560 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-status.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-status.ts @@ -6,6 +6,7 @@ */ import { prettifyError, z } from "zod/v4"; +import type { ParsePayload } from "zod/v4/core"; import type { BlockRef } from "../block"; import { blockRefSchema } from "../block"; @@ -21,13 +22,25 @@ const schemaSerializedChainBlockRef = z.object({ export type SerializedChainBlockRef = z.infer; +function invariant_includesAtLeastOneIndexedChain( + ctx: ParsePayload, +) { + const records = ctx.value; + if (Object.keys(records).length === 0) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "Ponder Indexing Status must include at least one indexed chain.", + }); + } +} + /** * Schema describing response at `GET /status`. */ -export const schemaSerializedPonderIndexingStatus = z.record( - schemaSerializedChainName, - schemaSerializedChainBlockRef, -); +export const schemaSerializedPonderIndexingStatus = z + .record(schemaSerializedChainName, schemaSerializedChainBlockRef) + .check(invariant_includesAtLeastOneIndexedChain); /** * Serialized Ponder Indexing Status. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68fe761d9..aec36d7fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1049,13 +1049,6 @@ importers: version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) packages/ponder-sdk: - dependencies: - '@ensnode/ensnode-sdk': - specifier: workspace:* - version: link:../ensnode-sdk - parse-prometheus-text-format: - specifier: ^1.1.1 - version: 1.1.1 devDependencies: '@ensnode/shared-configs': specifier: workspace:* From cc00089b90b4cde3a8ed6fd1fc842728b8743a37 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:28:29 +0100 Subject: [PATCH 10/18] Apply AI agent feedback --- packages/ponder-sdk/package.json | 2 +- packages/ponder-sdk/src/client.ts | 8 +++++++- packages/ponder-sdk/tsup.config.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index 9b8489fdf..403b8f100 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -39,7 +39,7 @@ "types": "./dist/index.d.ts" }, "scripts": { - "prepublishOnly": "tsup", + "prepublish": "tsup", "typecheck": "tsc --noEmit", "test": "vitest", "lint": "biome check --write .", diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index 3969dc9c8..e0450537e 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -25,7 +25,13 @@ export class PonderClient { ); } - const responseData = await response.json(); + let responseData: unknown; + + try { + responseData = await response.json(); + } catch { + throw new Error("Failed to parse Ponder status response as JSON"); + } return deserializePonderIndexingStatus(responseData); } diff --git a/packages/ponder-sdk/tsup.config.ts b/packages/ponder-sdk/tsup.config.ts index 34e76bce8..a7f5f3808 100644 --- a/packages/ponder-sdk/tsup.config.ts +++ b/packages/ponder-sdk/tsup.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ sourcemap: true, dts: true, clean: true, - external: ["ponder", "viem", "zod"], // Mark peer dependencies as external + external: ["ponder", "viem", "zod/*"], // Mark peer dependencies as external outDir: "./dist", }); From 344f9c768a1da7ab32876f53a7abd75dedbe6939 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:34:23 +0100 Subject: [PATCH 11/18] Improve unit tests handling --- packages/ponder-sdk/src/client.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index a46562af4..e4b54c595 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -13,6 +13,7 @@ const mockFetch = vi.fn(); describe("Ponder Client", () => { beforeEach(() => { + mockFetch.mockReset(); vi.stubGlobal("fetch", mockFetch); }); From fa953017d6d39a01ca004ece508a99235744cad0 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:39:41 +0100 Subject: [PATCH 12/18] Test case: malformed json data --- packages/ponder-sdk/src/client.test.ts | 19 +++++++++++++++---- packages/ponder-sdk/src/client.ts | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index e4b54c595..593b6e267 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -81,7 +81,7 @@ describe("Ponder Client", () => { const ponderClient = new PonderClient(new URL("http://localhost:3000")); await expect(ponderClient.status()).rejects.toThrowError( - /Invalid serialized Ponder Indexing Status.*Ponder Indexing Status must include at least one indexed chai/, + /Invalid serialized Ponder Indexing Status.*Ponder Indexing Status must include at least one indexed chain/, ); }); }); @@ -95,12 +95,23 @@ describe("Ponder Client", () => { statusText: "Internal Server Error", }), ); - const ponderClient = new PonderClient(new URL("http://localhost:3000")); - // Act & Assert await expect(ponderClient.status()).rejects.toThrowError( - /Failed to fetch Ponder Indexing Status/, + /Failed to fetch Ponder Indexing Status response/, + ); + }); + + it("should handle JSON parsing errors", async () => { + mockFetch.mockResolvedValueOnce( + new Response("not valid json", { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + const ponderClient = new PonderClient(new URL("http://localhost:3000")); + await expect(ponderClient.status()).rejects.toThrowError( + /Failed to parse Ponder Indexing Status response as JSON/, ); }); }); diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index e0450537e..41017c019 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -21,7 +21,7 @@ export class PonderClient { if (!response.ok) { throw new Error( - `Failed to fetch Ponder Indexing Status: ${response.status} ${response.statusText}`, + `Failed to fetch Ponder Indexing Status response: ${response.status} ${response.statusText}`, ); } @@ -30,7 +30,7 @@ export class PonderClient { try { responseData = await response.json(); } catch { - throw new Error("Failed to parse Ponder status response as JSON"); + throw new Error("Failed to parse Ponder Indexing Status response as JSON"); } return deserializePonderIndexingStatus(responseData); From a940a975c633199edf3197186a866997ab420a76 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:41:17 +0100 Subject: [PATCH 13/18] Improve unit tests handling --- packages/ponder-sdk/src/client.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 593b6e267..49137cfd1 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -1,4 +1,6 @@ -import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach } from "node:test"; + +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { PonderClient } from "./client"; import { deserializePonderIndexingStatus } from "./deserialize/indexing-status"; @@ -12,11 +14,14 @@ import { const mockFetch = vi.fn(); describe("Ponder Client", () => { - beforeEach(() => { - mockFetch.mockReset(); + beforeAll(() => { vi.stubGlobal("fetch", mockFetch); }); + afterEach(() => { + mockFetch.mockReset(); + }); + afterAll(() => { vi.unstubAllGlobals(); }); From 6d4f65c6aa763d93934e1e43db52a4dcc4a4f21d Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:42:49 +0100 Subject: [PATCH 14/18] Clean up peer deps --- packages/ponder-sdk/package.json | 4 ---- packages/ponder-sdk/tsup.config.ts | 2 +- pnpm-lock.yaml | 6 ------ 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/ponder-sdk/package.json b/packages/ponder-sdk/package.json index 403b8f100..4309c6646 100644 --- a/packages/ponder-sdk/package.json +++ b/packages/ponder-sdk/package.json @@ -48,16 +48,12 @@ "devDependencies": { "@ensnode/shared-configs": "workspace:*", "@types/node": "catalog:", - "ponder": "catalog:", "tsup": "catalog:", "typescript": "catalog:", - "viem": "catalog:", "vitest": "catalog:", "zod": "catalog:" }, "peerDependencies": { - "ponder": "catalog:", - "viem": "catalog:", "zod": "catalog:" } } diff --git a/packages/ponder-sdk/tsup.config.ts b/packages/ponder-sdk/tsup.config.ts index a7f5f3808..a97448258 100644 --- a/packages/ponder-sdk/tsup.config.ts +++ b/packages/ponder-sdk/tsup.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ sourcemap: true, dts: true, clean: true, - external: ["ponder", "viem", "zod/*"], // Mark peer dependencies as external + external: ["zod/*"], // Mark peer dependencies as external outDir: "./dist", }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aec36d7fa..1b159c5c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1056,18 +1056,12 @@ importers: '@types/node': specifier: 'catalog:' version: 24.10.9 - ponder: - specifier: 'catalog:' - version: 0.16.2(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(hono@4.11.7)(lightningcss@1.30.2)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6))(zod@4.3.6) tsup: specifier: 'catalog:' version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: 'catalog:' version: 5.9.3 - viem: - specifier: 'catalog:' - version: 2.38.5(typescript@5.9.3)(zod@4.3.6) vitest: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) From 03df55cb20defa139fb0fd42578c03c8e5ebae05 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 21:53:10 +0100 Subject: [PATCH 15/18] Improve unit tests handling --- packages/ponder-sdk/src/client.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 49137cfd1..3bca5ca1e 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -1,6 +1,4 @@ -import { afterEach } from "node:test"; - -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { PonderClient } from "./client"; import { deserializePonderIndexingStatus } from "./deserialize/indexing-status"; From 7051307cfe6cbd332f0fb13ac8cd2c5fc1f97d12 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Tue, 3 Feb 2026 22:04:55 +0100 Subject: [PATCH 16/18] Export useful types --- packages/ponder-sdk/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index 5ec76921e..c3ccb5a65 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -1 +1,4 @@ +export type { BlockNumber, BlockRef } from "./block"; +export type { ChainId } from "./chain"; export * from "./client"; +export type { PonderIndexingStatus } from "./deserialize/indexing-status"; From dcdbee2626baca88df211ac3d81774cf885070d8 Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Wed, 4 Feb 2026 09:49:43 +0100 Subject: [PATCH 17/18] Apply suggestions from code review Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- .changeset/cozy-turkeys-fix.md | 2 +- packages/ponder-sdk/README.md | 2 +- packages/ponder-sdk/src/block.ts | 2 +- packages/ponder-sdk/src/client.ts | 2 +- .../ponder-sdk/src/deserialize/indexing-status.ts | 14 +++++++------- packages/ponder-sdk/src/numbers.ts | 3 +++ 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.changeset/cozy-turkeys-fix.md b/.changeset/cozy-turkeys-fix.md index dd6a89942..fa560eb0f 100644 --- a/.changeset/cozy-turkeys-fix.md +++ b/.changeset/cozy-turkeys-fix.md @@ -2,4 +2,4 @@ "@ensnode/ponder-sdk": minor --- -Introduce `ponder-sdk` package, including `PonderClient` implementation. +Introduce the `ponder-sdk` package, including an initial `PonderClient` implementation. diff --git a/packages/ponder-sdk/README.md b/packages/ponder-sdk/README.md index 8755219fe..939aa9844 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 apps and data, including shared types, data processing (such as validating data and enforcing invariants), and Ponder-oriented helper functions. +This package is a utility library for interacting with Ponder apps and data. diff --git a/packages/ponder-sdk/src/block.ts b/packages/ponder-sdk/src/block.ts index d121c87d9..e80169be2 100644 --- a/packages/ponder-sdk/src/block.ts +++ b/packages/ponder-sdk/src/block.ts @@ -22,6 +22,6 @@ export const blockRefSchema = z.object({ /** * BlockRef * - * Describes a single block. + * Reference to a block. */ export type BlockRef = z.infer; diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index 41017c019..a1c2d045e 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -4,7 +4,7 @@ import { } from "./deserialize/indexing-status"; /** - * PonderClient for interacting with Ponder app endpoints. + * PonderClient for fetching data from Ponder apps. */ export class PonderClient { constructor(private baseUrl: URL) {} diff --git a/packages/ponder-sdk/src/deserialize/indexing-status.ts b/packages/ponder-sdk/src/deserialize/indexing-status.ts index 046869560..cb689cdd4 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-status.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-status.ts @@ -36,7 +36,7 @@ function invariant_includesAtLeastOneIndexedChain( } /** - * Schema describing response at `GET /status`. + * Schema describing the response of fetching `GET /status` from a Ponder app. */ export const schemaSerializedPonderIndexingStatus = z .record(schemaSerializedChainName, schemaSerializedChainBlockRef) @@ -50,7 +50,7 @@ export type SerializedPonderIndexingStatus = z.infer Date: Wed, 4 Feb 2026 11:46:09 +0100 Subject: [PATCH 18/18] Apply PR feedback --- .../ponder-sdk/src/{block.ts => blocks.ts} | 0 .../ponder-sdk/src/{chain.ts => chains.ts} | 0 packages/ponder-sdk/src/client.test.ts | 2 +- packages/ponder-sdk/src/client.ts | 6 +-- .../indexing-status.mock.ts} | 2 +- .../src/deserialize/indexing-status.ts | 40 ++++++------------- packages/ponder-sdk/src/index.ts | 3 -- packages/ponder-sdk/src/indexing-status.ts | 20 ++++++++++ 8 files changed, 36 insertions(+), 37 deletions(-) rename packages/ponder-sdk/src/{block.ts => blocks.ts} (100%) rename packages/ponder-sdk/src/{chain.ts => chains.ts} (100%) rename packages/ponder-sdk/src/{mocks.ts => deserialize/indexing-status.mock.ts} (93%) create mode 100644 packages/ponder-sdk/src/indexing-status.ts diff --git a/packages/ponder-sdk/src/block.ts b/packages/ponder-sdk/src/blocks.ts similarity index 100% rename from packages/ponder-sdk/src/block.ts rename to packages/ponder-sdk/src/blocks.ts diff --git a/packages/ponder-sdk/src/chain.ts b/packages/ponder-sdk/src/chains.ts similarity index 100% rename from packages/ponder-sdk/src/chain.ts rename to packages/ponder-sdk/src/chains.ts diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index 3bca5ca1e..f509a61af 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -6,7 +6,7 @@ import { mockSerializedPonderIndexingStatusInvalidBlockNumber, mockSerializedPonderIndexingStatusInvalidChainId, mockSerializedPonderIndexingStatusValid, -} from "./mocks"; +} from "./deserialize/indexing-status.mock"; // Mock Fetch API const mockFetch = vi.fn(); diff --git a/packages/ponder-sdk/src/client.ts b/packages/ponder-sdk/src/client.ts index a1c2d045e..56ec23074 100644 --- a/packages/ponder-sdk/src/client.ts +++ b/packages/ponder-sdk/src/client.ts @@ -1,7 +1,5 @@ -import { - deserializePonderIndexingStatus, - type PonderIndexingStatus, -} from "./deserialize/indexing-status"; +import { deserializePonderIndexingStatus } from "./deserialize/indexing-status"; +import type { PonderIndexingStatus } from "./indexing-status"; /** * PonderClient for fetching data from Ponder apps. diff --git a/packages/ponder-sdk/src/mocks.ts b/packages/ponder-sdk/src/deserialize/indexing-status.mock.ts similarity index 93% rename from packages/ponder-sdk/src/mocks.ts rename to packages/ponder-sdk/src/deserialize/indexing-status.mock.ts index 38926232a..de591373f 100644 --- a/packages/ponder-sdk/src/mocks.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-status.mock.ts @@ -1,4 +1,4 @@ -import type { SerializedPonderIndexingStatus } from "./deserialize/indexing-status"; +import type { SerializedPonderIndexingStatus } from "./indexing-status"; export const mockSerializedPonderIndexingStatusValid = { mainnet: { diff --git a/packages/ponder-sdk/src/deserialize/indexing-status.ts b/packages/ponder-sdk/src/deserialize/indexing-status.ts index cb689cdd4..68eba16ce 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-status.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-status.ts @@ -1,17 +1,19 @@ /** * Ponder Indexing Status * - * Defines the structure and validation for the Ponder Indexing Status response. + * Defines the structure and validation for the Ponder Indexing Status response + * from `GET /status` endpoint. * @see https://ponder.sh/docs/advanced/observability#indexing-status */ import { prettifyError, z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; -import type { BlockRef } from "../block"; -import { blockRefSchema } from "../block"; -import type { ChainId } from "../chain"; -import { chainIdSchema } from "../chain"; +import type { BlockRef } from "../blocks"; +import { blockRefSchema } from "../blocks"; +import type { ChainId } from "../chains"; +import { chainIdSchema } from "../chains"; +import type { PonderIndexingStatus } from "../indexing-status"; const schemaSerializedChainName = z.string(); @@ -20,8 +22,6 @@ const schemaSerializedChainBlockRef = z.object({ block: blockRefSchema, }); -export type SerializedChainBlockRef = z.infer; - function invariant_includesAtLeastOneIndexedChain( ctx: ParsePayload, ) { @@ -38,7 +38,7 @@ function invariant_includesAtLeastOneIndexedChain( /** * Schema describing the response of fetching `GET /status` from a Ponder app. */ -export const schemaSerializedPonderIndexingStatus = z +const schemaSerializedPonderIndexingStatus = z .record(schemaSerializedChainName, schemaSerializedChainBlockRef) .check(invariant_includesAtLeastOneIndexedChain); @@ -47,24 +47,6 @@ export const schemaSerializedPonderIndexingStatus = z */ export type SerializedPonderIndexingStatus = z.infer; -/** - * Ponder Indexing Status - * - * Represents the chain indexing status in a Ponder application. - */ -export interface PonderIndexingStatus { - /** - * Map of indexed chain IDs to their block reference. - * - * Guarantees: - * - Includes entry for at least one indexed chain. - * - BlockRef corresponds to either: - * - The first block to be indexed (when chain indexing is currently queued). - * - The last indexed block (when chain indexing is currently in progress). - */ - chains: Map; -} - /** * Build Ponder Indexing Status * @@ -90,8 +72,10 @@ function buildPonderIndexingStatus(data: SerializedPonderIndexingStatus): Ponder * @returns Deserialized and validated Ponder Indexing Status. * @throws Error if data cannot be deserialized into a valid Ponder Indexing Status. */ -export function deserializePonderIndexingStatus(response: SerializedPonderIndexingStatus | unknown): PonderIndexingStatus { - const validation = schemaSerializedPonderIndexingStatus.safeParse(response); +export function deserializePonderIndexingStatus( + data: SerializedPonderIndexingStatus | unknown, +): PonderIndexingStatus { + const validation = schemaSerializedPonderIndexingStatus.safeParse(data); if (!validation.success) { throw new Error( diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index c3ccb5a65..5ec76921e 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -1,4 +1 @@ -export type { BlockNumber, BlockRef } from "./block"; -export type { ChainId } from "./chain"; export * from "./client"; -export type { PonderIndexingStatus } from "./deserialize/indexing-status"; diff --git a/packages/ponder-sdk/src/indexing-status.ts b/packages/ponder-sdk/src/indexing-status.ts new file mode 100644 index 000000000..8bd444b4d --- /dev/null +++ b/packages/ponder-sdk/src/indexing-status.ts @@ -0,0 +1,20 @@ +import type { BlockRef } from "./blocks"; +import type { ChainId } from "./chains"; + +/** + * Ponder Indexing Status + * + * Represents the chain indexing status in a Ponder application. + */ +export interface PonderIndexingStatus { + /** + * Map of indexed chain IDs to their block reference. + * + * Guarantees: + * - Includes entry for at least one indexed chain. + * - BlockRef corresponds to either: + * - The first block to be indexed (when chain indexing is currently queued). + * - The last indexed block (when chain indexing is currently in progress). + */ + chains: Map; +}