From d3acb6237b7500c78a96cdb3a8479a6085799e72 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 2 Dec 2025 09:48:42 -0500 Subject: [PATCH 01/33] feat: started OpenAPI implementation - Installs `hono-openapi` - Updates `validate.ts` to use `validator` from `hono-openapi` - Adds response zod schemas to ensnode-sdk - Adds `describeRoute` to resolution endpoints - Adds `GET /openapi.json` route --- apps/ensapi/package.json | 4 +- apps/ensapi/src/handlers/resolution-api.ts | 56 +++++++- apps/ensapi/src/index.ts | 19 +++ apps/ensapi/src/lib/handlers/validate.ts | 4 +- packages/ensnode-sdk/src/api/index.ts | 1 + packages/ensnode-sdk/src/api/zod-schemas.ts | 52 ++++++++ pnpm-lock.yaml | 140 +++++++++++++++++++- 7 files changed, 267 insertions(+), 9 deletions(-) diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index ae2f420c7..74165ace0 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -25,10 +25,11 @@ "@ensnode/ensnode-schema": "workspace:*", "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/ponder-subgraph": "workspace:*", - "@namehash/ens-referrals": "workspace:*", "@hono/node-server": "^1.19.5", "@hono/otel": "^0.2.2", + "@hono/standard-validator": "^0.2.0", "@hono/zod-validator": "^0.7.2", + "@namehash/ens-referrals": "workspace:*", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-metrics-otlp-proto": "^0.202.0", @@ -42,6 +43,7 @@ "date-fns": "catalog:", "drizzle-orm": "catalog:", "hono": "catalog:", + "hono-openapi": "^1.1.1", "p-memoize": "^8.0.0", "p-reflect": "^3.1.0", "p-retry": "^7.1.0", diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index 23ac4915b..fe42035fc 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -1,10 +1,13 @@ import { z } from "zod/v4"; -import type { - Duration, - ResolvePrimaryNameResponse, - ResolvePrimaryNamesResponse, - ResolveRecordsResponse, +import { + makeResolvePrimaryNameResponseSchema, + makeResolvePrimaryNamesResponseSchema, + makeResolveRecordsResponseSchema, + type Duration, + type ResolvePrimaryNameResponse, + type ResolvePrimaryNamesResponse, + type ResolveRecordsResponse, } from "@ensnode/ensnode-sdk"; import { params } from "@/lib/handlers/params.schema"; @@ -16,6 +19,7 @@ import { resolveReverse } from "@/lib/resolution/reverse-resolution"; import { captureTrace } from "@/lib/tracing/protocol-tracing"; import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware"; import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware"; +import { describeRoute, resolver } from "hono-openapi"; /** * The effective distance for acceleration is indexing status cache time plus @@ -44,6 +48,20 @@ app.use(canAccelerateMiddleware); */ app.get( "/records/:name", + describeRoute({ + summary: "Resolve ENS Records", + description: "Resolves ENS records for a given name", + responses: { + 200: { + description: "Successfully resolved records", + content: { + "application/json": { + schema: resolver(makeResolveRecordsResponseSchema()), + }, + }, + }, + }, + }), validate("param", z.object({ name: params.name })), validate( "query", @@ -99,6 +117,20 @@ app.get( */ app.get( "/primary-name/:address/:chainId", + describeRoute({ + summary: "Resolve Primary Name", + description: "Resolves a primary name for a given `address` and `chainId`", + responses: { + 200: { + description: "Successfully resolved name", + content: { + "application/json": { + schema: resolver(makeResolvePrimaryNameResponseSchema()), + }, + }, + }, + }, + }), validate("param", z.object({ address: params.address, chainId: params.defaultableChainId })), validate( "query", @@ -144,6 +176,20 @@ app.get( */ app.get( "/primary-names/:address", + describeRoute({ + summary: "Resolve Primary Names", + description: "Resolves all primary names for a given address across multiple chains", + responses: { + 200: { + description: "Successfully resolved records", + content: { + "application/json": { + schema: resolver(makeResolvePrimaryNamesResponseSchema()), + }, + }, + }, + }, + }), validate("param", z.object({ address: params.address })), validate( "query", diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index 5abaae064..d07546233 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -3,6 +3,7 @@ import config from "@/config"; import { serve } from "@hono/node-server"; import { otel } from "@hono/otel"; +import { openAPIRouteHandler } from "hono-openapi"; import { cors } from "hono/cors"; import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; @@ -45,6 +46,24 @@ app.route("/subgraph", subgraphApi); // use ENSAnalytics API at /ensanalytics app.route("/ensanalytics", ensanalyticsApi); +// use OpenAPI Schema +app.get( + "/openapi.json", + openAPIRouteHandler(app, { + documentation: { + info: { + title: "ENSApi", + version: packageJson.version, + description: "ENS resolution and analytics API", + }, + servers: [ + { url: `http://localhost:${config.port}`, description: "Local Development" }, + // Add your production servers here + ], + }, + }), +); + // will automatically 500 if config is not available due to ensIndexerPublicConfigMiddleware app.get("/health", async (c) => { return c.json({ ok: true }); diff --git a/apps/ensapi/src/lib/handlers/validate.ts b/apps/ensapi/src/lib/handlers/validate.ts index 4aa4d33f2..83246bbf2 100644 --- a/apps/ensapi/src/lib/handlers/validate.ts +++ b/apps/ensapi/src/lib/handlers/validate.ts @@ -1,4 +1,4 @@ -import { zValidator } from "@hono/zod-validator"; +import { validator } from "hono-openapi"; import type { ValidationTargets } from "hono"; import type { ZodType } from "zod/v4"; @@ -18,7 +18,7 @@ export const validate = - zValidator(target, schema, (result, c) => { + validator(target, schema, (result, c) => { // if validation failed, return our custom-formatted ErrorResponse instead of default if (!result.success) return errorResponse(c, result.error); }); diff --git a/packages/ensnode-sdk/src/api/index.ts b/packages/ensnode-sdk/src/api/index.ts index 282cf2e14..1c29bdcf0 100644 --- a/packages/ensnode-sdk/src/api/index.ts +++ b/packages/ensnode-sdk/src/api/index.ts @@ -3,3 +3,4 @@ export * from "./registrar-actions"; export * from "./serialize"; export * from "./serialized-types"; export * from "./types"; +export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/api/zod-schemas.ts b/packages/ensnode-sdk/src/api/zod-schemas.ts index da0963245..b959c12a4 100644 --- a/packages/ensnode-sdk/src/api/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/zod-schemas.ts @@ -15,6 +15,9 @@ import { RegistrarActionsResponseCodes, RegistrarActionsResponseError, RegistrarActionsResponseOk, + type ResolvePrimaryNameResponse, + type ResolvePrimaryNamesResponse, + type ResolveRecordsResponse, } from "./types"; export const ErrorResponseSchema = z.object({ @@ -113,3 +116,52 @@ export const makeRegistrarActionsResponseSchema = ( makeRegistrarActionsResponseOkSchema(valueLabel), makeRegistrarActionsResponseErrorSchema(valueLabel), ]); + +// Resolution API + +/** + * Schema for resolver records response (addresses, texts, name) + */ +const makeResolverRecordsResponseSchema = (_valueLabel: string = "Resolver Records Response") => + z.object({ + name: z.string().nullable().optional(), + addresses: z.record(z.string(), z.string().nullable()).optional(), + texts: z.record(z.string(), z.string().nullable()).optional(), + }); + +/** + * Schema for {@link ResolveRecordsResponse} + */ +export const makeResolveRecordsResponseSchema = (valueLabel: string = "Resolve Records Response") => + z.object({ + records: makeResolverRecordsResponseSchema(valueLabel), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.any().optional(), + }); + +/** + * Schema for {@link ResolvePrimaryNameResponse} + */ +export const makeResolvePrimaryNameResponseSchema = ( + _valueLabel: string = "Resolve Primary Name Response", +) => + z.object({ + name: z.string().nullable(), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.any().optional(), + }); + +/** + * Schema for {@link ResolvePrimaryNamesResponse} + */ +export const makeResolvePrimaryNamesResponseSchema = ( + _valueLabel: string = "Resolve Primary Names Response", +) => + z.object({ + names: z.record(z.number(), z.string().nullable()), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.any().optional(), + }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ba22da63..ddba76423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: '@hono/otel': specifier: ^0.2.2 version: 0.2.2(hono@4.10.3) + '@hono/standard-validator': + specifier: ^0.2.0 + version: 0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3) '@hono/zod-validator': specifier: ^0.7.2 version: 0.7.4(hono@4.10.3)(zod@3.25.76) @@ -334,6 +337,9 @@ importers: hono: specifier: 'catalog:' version: 4.10.3 + hono-openapi: + specifier: ^1.1.1 + version: 1.1.1(@hono/standard-validator@0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(hono@4.10.3)(openapi-types@12.1.3) p-memoize: specifier: ^8.0.0 version: 8.0.0 @@ -1705,6 +1711,12 @@ packages: peerDependencies: hono: '*' + '@hono/standard-validator@0.2.0': + resolution: {integrity: sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ==} + peerDependencies: + '@standard-schema/spec': 1.0.0 + hono: '>=3.9.0' + '@hono/zod-validator@0.7.4': resolution: {integrity: sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q==} peerDependencies: @@ -3103,6 +3115,67 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-community/standard-json@0.3.5': + resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==} + peerDependencies: + '@standard-schema/spec': ^1.0.0 + '@types/json-schema': ^7.0.15 + '@valibot/to-json-schema': ^1.3.0 + arktype: ^2.1.20 + effect: ^3.16.8 + quansync: ^0.2.11 + sury: ^10.0.0 + typebox: ^1.0.17 + valibot: ^1.1.0 + zod: ^3.25.0 || ^4.0.0 + zod-to-json-schema: ^3.24.5 + peerDependenciesMeta: + '@valibot/to-json-schema': + optional: true + arktype: + optional: true + effect: + optional: true + sury: + optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true + zod-to-json-schema: + optional: true + + '@standard-community/standard-openapi@0.2.9': + resolution: {integrity: sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==} + peerDependencies: + '@standard-community/standard-json': ^0.3.5 + '@standard-schema/spec': ^1.0.0 + arktype: ^2.1.20 + effect: ^3.17.14 + openapi-types: ^12.1.3 + sury: ^10.0.0 + typebox: ^1.0.0 + valibot: ^1.1.0 + zod: ^3.25.0 || ^4.0.0 + zod-openapi: ^4 + peerDependenciesMeta: + arktype: + optional: true + effect: + optional: true + sury: + optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true + zod-openapi: + optional: true + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -3393,6 +3466,9 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5075,6 +5151,21 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hono-openapi@1.1.1: + resolution: {integrity: sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q==} + peerDependencies: + '@hono/standard-validator': ^0.1.2 + '@standard-community/standard-json': ^0.3.5 + '@standard-community/standard-openapi': ^0.2.8 + '@types/json-schema': ^7.0.15 + hono: ^4.8.3 + openapi-types: ^12.1.3 + peerDependenciesMeta: + '@hono/standard-validator': + optional: true + hono: + optional: true + hono@4.10.3: resolution: {integrity: sha512-2LOYWUbnhdxdL8MNbNg9XZig6k+cZXm5IjHn2Aviv7honhBMOHb+jxrKIeJRZJRmn+htUCKhaicxwXuUDlchRA==} engines: {node: '>=16.9.0'} @@ -5992,6 +6083,9 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -8980,6 +9074,11 @@ snapshots: '@opentelemetry/semantic-conventions': 1.37.0 hono: 4.10.3 + '@hono/standard-validator@0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3)': + dependencies: + '@standard-schema/spec': 1.0.0 + hono: 4.10.3 + '@hono/zod-validator@0.7.4(hono@4.10.3)(zod@3.25.76)': dependencies: hono: 4.10.3 @@ -10473,6 +10572,23 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/json-schema': 7.0.15 + quansync: 0.2.11 + optionalDependencies: + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + + '@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76)': + dependencies: + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76) + '@standard-schema/spec': 1.0.0 + openapi-types: 12.1.3 + optionalDependencies: + zod: 3.25.76 + '@standard-schema/spec@1.0.0': {} '@swc/helpers@0.5.15': @@ -10783,6 +10899,8 @@ snapshots: '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -10884,6 +11002,14 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 + '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.5 @@ -12723,6 +12849,16 @@ snapshots: help-me@5.0.0: {} + hono-openapi@1.1.1(@hono/standard-validator@0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3))(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(hono@4.10.3)(openapi-types@12.1.3): + dependencies: + '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76) + '@standard-community/standard-openapi': 0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.0.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.0.0)(openapi-types@12.1.3)(zod@3.25.76) + '@types/json-schema': 7.0.15 + openapi-types: 12.1.3 + optionalDependencies: + '@hono/standard-validator': 0.2.0(@standard-schema/spec@1.0.0)(hono@4.10.3) + hono: 4.10.3 + hono@4.10.3: {} html-encoding-sniffer@4.0.0: @@ -13818,6 +13954,8 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + openapi-types@12.1.3: {} + outdent@0.5.0: {} ox@0.9.6(typescript@5.9.3)(zod@3.25.76): @@ -15556,7 +15694,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@20.19.24)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5 From fac5510aa2ff39af30321462d75b9d1d69abc5ce Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 2 Dec 2025 09:49:11 -0500 Subject: [PATCH 02/33] chore: lint --- apps/ensapi/src/handlers/resolution-api.ts | 4 ++-- apps/ensapi/src/index.ts | 2 +- apps/ensapi/src/lib/handlers/validate.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index fe42035fc..e2b662b5f 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -1,10 +1,11 @@ +import { describeRoute, resolver } from "hono-openapi"; import { z } from "zod/v4"; import { + type Duration, makeResolvePrimaryNameResponseSchema, makeResolvePrimaryNamesResponseSchema, makeResolveRecordsResponseSchema, - type Duration, type ResolvePrimaryNameResponse, type ResolvePrimaryNamesResponse, type ResolveRecordsResponse, @@ -19,7 +20,6 @@ import { resolveReverse } from "@/lib/resolution/reverse-resolution"; import { captureTrace } from "@/lib/tracing/protocol-tracing"; import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware"; import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware"; -import { describeRoute, resolver } from "hono-openapi"; /** * The effective distance for acceleration is indexing status cache time plus diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index d07546233..016e4c786 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -3,8 +3,8 @@ import config from "@/config"; import { serve } from "@hono/node-server"; import { otel } from "@hono/otel"; -import { openAPIRouteHandler } from "hono-openapi"; import { cors } from "hono/cors"; +import { openAPIRouteHandler } from "hono-openapi"; import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; diff --git a/apps/ensapi/src/lib/handlers/validate.ts b/apps/ensapi/src/lib/handlers/validate.ts index 83246bbf2..704437424 100644 --- a/apps/ensapi/src/lib/handlers/validate.ts +++ b/apps/ensapi/src/lib/handlers/validate.ts @@ -1,5 +1,5 @@ -import { validator } from "hono-openapi"; import type { ValidationTargets } from "hono"; +import { validator } from "hono-openapi"; import type { ZodType } from "zod/v4"; import { errorResponse } from "./error-response"; From dc38b3be05cc211995a1259b65152df5765683bf Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 16 Dec 2025 14:45:28 -0500 Subject: [PATCH 03/33] chore: fix merge conflict aftermath --- .../ensnode-sdk/src/api/registrar-actions/index.ts | 1 + .../src/api/registrar-actions/zod-schemas.ts | 14 +++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/ensnode-sdk/src/api/registrar-actions/index.ts b/packages/ensnode-sdk/src/api/registrar-actions/index.ts index 8f0b7c3f5..41b6680b7 100644 --- a/packages/ensnode-sdk/src/api/registrar-actions/index.ts +++ b/packages/ensnode-sdk/src/api/registrar-actions/index.ts @@ -5,3 +5,4 @@ export * from "./request"; export * from "./response"; export * from "./serialize"; export * from "./serialized-response"; +export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts b/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts index 13ebd2351..ed99008e4 100644 --- a/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts @@ -72,7 +72,7 @@ export const makeRegistrarActionsResponseSchema = ( /** * Schema for resolver records response (addresses, texts, name) */ -const makeResolverRecordsResponseSchema = (_valueLabel: string = "Resolver Records Response") => +const makeResolverRecordsResponseSchema = () => z.object({ name: z.string().nullable().optional(), addresses: z.record(z.string(), z.string().nullable()).optional(), @@ -82,9 +82,9 @@ const makeResolverRecordsResponseSchema = (_valueLabel: string = "Resolver Recor /** * Schema for {@link ResolveRecordsResponse} */ -export const makeResolveRecordsResponseSchema = (valueLabel: string = "Resolve Records Response") => +export const makeResolveRecordsResponseSchema = () => z.object({ - records: makeResolverRecordsResponseSchema(valueLabel), + records: makeResolverRecordsResponseSchema(), accelerationRequested: z.boolean(), accelerationAttempted: z.boolean(), trace: z.any().optional(), @@ -93,9 +93,7 @@ export const makeResolveRecordsResponseSchema = (valueLabel: string = "Resolve R /** * Schema for {@link ResolvePrimaryNameResponse} */ -export const makeResolvePrimaryNameResponseSchema = ( - _valueLabel: string = "Resolve Primary Name Response", -) => +export const makeResolvePrimaryNameResponseSchema = () => z.object({ name: z.string().nullable(), accelerationRequested: z.boolean(), @@ -106,9 +104,7 @@ export const makeResolvePrimaryNameResponseSchema = ( /** * Schema for {@link ResolvePrimaryNamesResponse} */ -export const makeResolvePrimaryNamesResponseSchema = ( - _valueLabel: string = "Resolve Primary Names Response", -) => +export const makeResolvePrimaryNamesResponseSchema = () => z.object({ names: z.record(z.number(), z.string().nullable()), accelerationRequested: z.boolean(), From 132f36b32d0b4b4696301bd04e83e42f16304726 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 16 Dec 2025 14:47:10 -0500 Subject: [PATCH 04/33] chore: updated name and description for openapi spec --- apps/ensapi/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index fdf76568a..8726de2f2 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -53,9 +53,9 @@ app.get( openAPIRouteHandler(app, { documentation: { info: { - title: "ENSApi", + title: "ENSNode's APIs", version: packageJson.version, - description: "ENS resolution and analytics API", + description: "ENSNode resolution and analytics API", }, servers: [ { url: `http://localhost:${config.port}`, description: "Local Development" }, From 7f1ed5539bddc9d1cb316cd51bef32d664e6e58b Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 17 Dec 2025 23:05:05 -0500 Subject: [PATCH 05/33] chore: Updates to name-tokens-api types The Hono OpenAPI implementation with Zod produces a JSON schema where `z.undefined()` is [unrepresentable](https://zod.dev/json-schema#unrepresentable). This commit refactors the `NameTokensRequest` type and by consequence update the zod schema to pass. In order for OpenAPI JSON schema to work we cannot have any instances of unrepresentable APIs, and this so far was the only case. --- apps/ensapi/src/handlers/name-tokens-api.ts | 25 ++++++++++--------- .../src/api/name-tokens/request.ts | 21 +++++----------- packages/ensnode-sdk/src/client.ts | 2 +- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 15922625a..c13f59969 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -44,16 +44,14 @@ app.use(nameTokensApiMiddleware); * Name Tokens API can be requested by either `name` or `domainId`, and * can never be requested by both, or neither. */ -const requestQuerySchema = z.union([ - z.object({ - domainId: makeNodeSchema("request.domainId"), - name: z.undefined(), - }), - z.object({ - domainId: z.undefined(), - name: params.name, - }), -]); +const requestQuerySchema = z + .object({ + domainId: makeNodeSchema("request.domainId").optional(), + name: params.name.optional(), + }) + .refine((data) => (data.domainId !== undefined) !== (data.name !== undefined), { + message: "Exactly one of 'domainId' or 'name' must be provided", + }); app.get("/", validate("query", requestQuerySchema), async (c) => { // Invariant: context must be set by the required middleware @@ -67,7 +65,7 @@ app.get("/", validate("query", requestQuerySchema), async (c) => { } const request = c.req.valid("query") satisfies NameTokensRequest; - let domainId: Node | undefined; + let domainId: Node; if (request.name !== undefined) { const { name } = request; @@ -112,8 +110,11 @@ app.get("/", validate("query", requestQuerySchema), async (c) => { } domainId = namehash(name); - } else { + } else if (request.domainId !== undefined) { domainId = request.domainId; + } else { + // This should never happen due to Zod validation, but TypeScript needs this + throw new Error("Invariant(name-tokens-api): Either name or domainId must be provided"); } const { omnichainSnapshot } = c.var.indexingStatus.snapshot; diff --git a/packages/ensnode-sdk/src/api/name-tokens/request.ts b/packages/ensnode-sdk/src/api/name-tokens/request.ts index 7aca3cd8f..7a9eac92d 100644 --- a/packages/ensnode-sdk/src/api/name-tokens/request.ts +++ b/packages/ensnode-sdk/src/api/name-tokens/request.ts @@ -2,26 +2,17 @@ import type { Name, Node } from "../../ens"; /** * Represents request to Name Tokens API. + * + * Either `domainId` or `name` must be provided, but not both. */ -export interface NameTokensRequestByDomainId { - domainId: Node; - +export interface NameTokensRequest { /** - * Name for which name tokens were requested. + * Domain ID (namehash) for which name tokens were requested. */ - name?: undefined; -} - -/** - * Represents request to Name Tokens API. - */ -export interface NameTokensRequestByName { - domainId?: undefined; + domainId?: Node; /** * Name for which name tokens were requested. */ - name: Name; + name?: Name; } - -export type NameTokensRequest = NameTokensRequestByDomainId | NameTokensRequestByName; diff --git a/packages/ensnode-sdk/src/client.ts b/packages/ensnode-sdk/src/client.ts index a5f5aae72..cd47f501c 100644 --- a/packages/ensnode-sdk/src/client.ts +++ b/packages/ensnode-sdk/src/client.ts @@ -755,7 +755,7 @@ export class ENSNodeClient { if (request.name !== undefined) { url.searchParams.set("name", request.name); - } else { + } else if (request.domainId !== undefined) { url.searchParams.set("domainId", request.domainId); } From 6993d26838299b36beaec0fa34623a9f7cb013bb Mon Sep 17 00:00:00 2001 From: Steve Date: Thu, 18 Dec 2025 10:53:08 -0500 Subject: [PATCH 06/33] chore: relocated resolution response schemas --- .../src/api/registrar-actions/zod-schemas.ts | 45 ------------------- .../ensnode-sdk/src/api/resolution/index.ts | 1 + .../src/api/resolution/zod-schemas.ts | 44 ++++++++++++++++++ 3 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 packages/ensnode-sdk/src/api/resolution/zod-schemas.ts diff --git a/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts b/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts index ed99008e4..e99436891 100644 --- a/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/registrar-actions/zod-schemas.ts @@ -66,48 +66,3 @@ export const makeRegistrarActionsResponseSchema = ( makeRegistrarActionsResponseOkSchema(valueLabel), makeRegistrarActionsResponseErrorSchema(valueLabel), ]); - -// Resolution API - -/** - * Schema for resolver records response (addresses, texts, name) - */ -const makeResolverRecordsResponseSchema = () => - z.object({ - name: z.string().nullable().optional(), - addresses: z.record(z.string(), z.string().nullable()).optional(), - texts: z.record(z.string(), z.string().nullable()).optional(), - }); - -/** - * Schema for {@link ResolveRecordsResponse} - */ -export const makeResolveRecordsResponseSchema = () => - z.object({ - records: makeResolverRecordsResponseSchema(), - accelerationRequested: z.boolean(), - accelerationAttempted: z.boolean(), - trace: z.any().optional(), - }); - -/** - * Schema for {@link ResolvePrimaryNameResponse} - */ -export const makeResolvePrimaryNameResponseSchema = () => - z.object({ - name: z.string().nullable(), - accelerationRequested: z.boolean(), - accelerationAttempted: z.boolean(), - trace: z.any().optional(), - }); - -/** - * Schema for {@link ResolvePrimaryNamesResponse} - */ -export const makeResolvePrimaryNamesResponseSchema = () => - z.object({ - names: z.record(z.number(), z.string().nullable()), - accelerationRequested: z.boolean(), - accelerationAttempted: z.boolean(), - trace: z.any().optional(), - }); diff --git a/packages/ensnode-sdk/src/api/resolution/index.ts b/packages/ensnode-sdk/src/api/resolution/index.ts index eea524d65..f265637b1 100644 --- a/packages/ensnode-sdk/src/api/resolution/index.ts +++ b/packages/ensnode-sdk/src/api/resolution/index.ts @@ -1 +1,2 @@ export * from "./types"; +export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts b/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts new file mode 100644 index 000000000..12748559b --- /dev/null +++ b/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts @@ -0,0 +1,44 @@ +import z from "zod/v4"; + +/** + * Schema for resolver records response (addresses, texts, name) + */ +const makeResolverRecordsResponseSchema = () => + z.object({ + name: z.string().nullable().optional(), + addresses: z.record(z.string(), z.string().nullable()).optional(), + texts: z.record(z.string(), z.string().nullable()).optional(), + }); + +/** + * Schema for {@link ResolveRecordsResponse} + */ +export const makeResolveRecordsResponseSchema = () => + z.object({ + records: makeResolverRecordsResponseSchema(), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.any().optional(), + }); + +/** + * Schema for {@link ResolvePrimaryNameResponse} + */ +export const makeResolvePrimaryNameResponseSchema = () => + z.object({ + name: z.string().nullable(), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.any().optional(), + }); + +/** + * Schema for {@link ResolvePrimaryNamesResponse} + */ +export const makeResolvePrimaryNamesResponseSchema = () => + z.object({ + names: z.record(z.number(), z.string().nullable()), + accelerationRequested: z.boolean(), + accelerationAttempted: z.boolean(), + trace: z.any().optional(), + }); From aed5cd47857b18f52189520d62ec87ed71b4d987 Mon Sep 17 00:00:00 2001 From: Steve Date: Thu, 18 Dec 2025 11:34:51 -0500 Subject: [PATCH 07/33] chore: updated resolution schemas --- packages/ensnode-sdk/src/api/resolution/zod-schemas.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts b/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts index 12748559b..5beab7a42 100644 --- a/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/resolution/zod-schemas.ts @@ -18,7 +18,8 @@ export const makeResolveRecordsResponseSchema = () => records: makeResolverRecordsResponseSchema(), accelerationRequested: z.boolean(), accelerationAttempted: z.boolean(), - trace: z.any().optional(), + // TODO: Find a better way to handle recursive types, patch solution is .unknown() + trace: z.array(z.unknown()).optional(), }); /** @@ -29,7 +30,7 @@ export const makeResolvePrimaryNameResponseSchema = () => name: z.string().nullable(), accelerationRequested: z.boolean(), accelerationAttempted: z.boolean(), - trace: z.any().optional(), + trace: z.array(z.unknown()).optional(), }); /** @@ -40,5 +41,5 @@ export const makeResolvePrimaryNamesResponseSchema = () => names: z.record(z.number(), z.string().nullable()), accelerationRequested: z.boolean(), accelerationAttempted: z.boolean(), - trace: z.any().optional(), + trace: z.array(z.unknown()).optional(), }); From 4cad8dbb5cfcc1fa6e74045d3c40201b54e70d2c Mon Sep 17 00:00:00 2001 From: Steve Date: Thu, 18 Dec 2025 15:38:11 -0500 Subject: [PATCH 08/33] chore: revert registrar api --- apps/ensapi/src/handlers/ensnode-api.ts | 92 ++++++++++++++----- .../src/api/indexing-status/index.ts | 1 + .../ensnode-sdk/src/tokenscope/zod-schemas.ts | 2 + 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 5c3186cde..a10fc80ef 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -1,9 +1,13 @@ import config from "@/config"; +import { describeRoute, resolver } from "hono-openapi"; + import { IndexingStatusResponseCodes, type IndexingStatusResponseError, type IndexingStatusResponseOk, + makeENSApiPublicConfigSchema, + makeIndexingStatusResponseSchema, serializeENSApiPublicConfig, serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; @@ -18,35 +22,77 @@ import resolutionApi from "./resolution-api"; const app = factory.createApp(); // include ENSApi Public Config endpoint -app.get("/config", async (c) => { - const ensApiPublicConfig = buildEnsApiPublicConfig(config); - return c.json(serializeENSApiPublicConfig(ensApiPublicConfig)); -}); +app.get( + "/config", + describeRoute({ + summary: "Get ENSApi Configuration", + description: "Returns the public configuration of the ENSApi instance", + responses: { + 200: { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: resolver(makeENSApiPublicConfigSchema()), + }, + }, + }, + }, + }), + async (c) => { + const ensApiPublicConfig = buildEnsApiPublicConfig(config); + return c.json(serializeENSApiPublicConfig(ensApiPublicConfig)); + }, +); // include ENSIndexer Indexing Status endpoint -app.get("/indexing-status", async (c) => { - // context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(ensnode-api): indexingStatusMiddleware required`); - } +app.get( + "/indexing-status", + describeRoute({ + summary: "Get ENSIndexer Indexing Status", + description: "Returns the current indexing status of the ENSIndexer", + responses: { + 200: { + description: "Successfully retrieved indexing status", + content: { + "application/json": { + schema: resolver(makeIndexingStatusResponseSchema()), + }, + }, + }, + 500: { + description: "Error retrieving indexing status", + content: { + "application/json": { + schema: resolver(makeIndexingStatusResponseSchema()), + }, + }, + }, + }, + }), + async (c) => { + // context must be set by the required middleware + if (c.var.indexingStatus === undefined) { + throw new Error(`Invariant(ensnode-api): indexingStatusMiddleware required`); + } + + if (c.var.indexingStatus instanceof Error) { + return c.json( + serializeIndexingStatusResponse({ + responseCode: IndexingStatusResponseCodes.Error, + } satisfies IndexingStatusResponseError), + 500, + ); + } - if (c.var.indexingStatus instanceof Error) { + // return successful response using the indexing status projection from the context return c.json( serializeIndexingStatusResponse({ - responseCode: IndexingStatusResponseCodes.Error, - } satisfies IndexingStatusResponseError), - 500, + responseCode: IndexingStatusResponseCodes.Ok, + realtimeProjection: c.var.indexingStatus, + } satisfies IndexingStatusResponseOk), ); - } - - // return successful response using the indexing status projection from the context - return c.json( - serializeIndexingStatusResponse({ - responseCode: IndexingStatusResponseCodes.Ok, - realtimeProjection: c.var.indexingStatus, - } satisfies IndexingStatusResponseOk), - ); -}); + }, +); // Name Tokens API app.route("/name-tokens", nameTokensApi); diff --git a/packages/ensnode-sdk/src/api/indexing-status/index.ts b/packages/ensnode-sdk/src/api/indexing-status/index.ts index 54913353d..919f4f586 100644 --- a/packages/ensnode-sdk/src/api/indexing-status/index.ts +++ b/packages/ensnode-sdk/src/api/indexing-status/index.ts @@ -3,3 +3,4 @@ export * from "./request"; export * from "./response"; export * from "./serialize"; export * from "./serialized-response"; +export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts b/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts index 77bebabb4..24475e156 100644 --- a/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts +++ b/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts @@ -22,6 +22,8 @@ import { /** * Make schema for {@link AssetId}. + * + * TODO: Find a way to make this compatible with Zod JSON Schema: https://zod.dev/json-schema#unrepresentable */ export const makeAssetIdSchema = (valueLabel: string = "Asset ID Schema") => z.object({ From 35668e406c9cd62d80df108f390d843ff7183e1f Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 10:33:09 -0500 Subject: [PATCH 09/33] chore: moved zod schema exports to internal.ts --- apps/ensapi/src/handlers/ensnode-api.ts | 6 ++++-- apps/ensapi/src/handlers/resolution-api.ts | 12 +++++++----- .../ensnode-sdk/src/api/indexing-status/index.ts | 1 - .../ensnode-sdk/src/api/registrar-actions/index.ts | 1 - packages/ensnode-sdk/src/api/resolution/index.ts | 1 - packages/ensnode-sdk/src/internal.ts | 2 ++ 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index a10fc80ef..819e38cab 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -6,11 +6,13 @@ import { IndexingStatusResponseCodes, type IndexingStatusResponseError, type IndexingStatusResponseOk, - makeENSApiPublicConfigSchema, - makeIndexingStatusResponseSchema, serializeENSApiPublicConfig, serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; +import { + makeENSApiPublicConfigSchema, + makeIndexingStatusResponseSchema, +} from "@ensnode/ensnode-sdk/internal"; import { buildEnsApiPublicConfig } from "@/config/config.schema"; import { factory } from "@/lib/hono-factory"; diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index 9ac261518..b7ac8f674 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -1,15 +1,17 @@ import { describeRoute, resolver } from "hono-openapi"; import { z } from "zod/v4"; +import type { + Duration, + ResolvePrimaryNameResponse, + ResolvePrimaryNamesResponse, + ResolveRecordsResponse, +} from "@ensnode/ensnode-sdk"; import { - type Duration, makeResolvePrimaryNameResponseSchema, makeResolvePrimaryNamesResponseSchema, makeResolveRecordsResponseSchema, - type ResolvePrimaryNameResponse, - type ResolvePrimaryNamesResponse, - type ResolveRecordsResponse, -} from "@ensnode/ensnode-sdk"; +} from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; diff --git a/packages/ensnode-sdk/src/api/indexing-status/index.ts b/packages/ensnode-sdk/src/api/indexing-status/index.ts index 919f4f586..54913353d 100644 --- a/packages/ensnode-sdk/src/api/indexing-status/index.ts +++ b/packages/ensnode-sdk/src/api/indexing-status/index.ts @@ -3,4 +3,3 @@ export * from "./request"; export * from "./response"; export * from "./serialize"; export * from "./serialized-response"; -export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/api/registrar-actions/index.ts b/packages/ensnode-sdk/src/api/registrar-actions/index.ts index 41b6680b7..8f0b7c3f5 100644 --- a/packages/ensnode-sdk/src/api/registrar-actions/index.ts +++ b/packages/ensnode-sdk/src/api/registrar-actions/index.ts @@ -5,4 +5,3 @@ export * from "./request"; export * from "./response"; export * from "./serialize"; export * from "./serialized-response"; -export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/api/resolution/index.ts b/packages/ensnode-sdk/src/api/resolution/index.ts index f265637b1..eea524d65 100644 --- a/packages/ensnode-sdk/src/api/resolution/index.ts +++ b/packages/ensnode-sdk/src/api/resolution/index.ts @@ -1,2 +1 @@ export * from "./types"; -export * from "./zod-schemas"; diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index dde184884..793f4a22a 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -14,8 +14,10 @@ export * from "./api/indexing-status/zod-schemas"; export * from "./api/registrar-actions/zod-schemas"; +export * from "./api/resolution/zod-schemas"; export * from "./api/shared/errors/zod-schemas"; export * from "./api/shared/pagination/zod-schemas"; +export * from "./ensapi/config/zod-schemas"; export * from "./ensindexer/config/zod-schemas"; export * from "./ensindexer/indexing-status/zod-schemas"; export * from "./registrars/zod-schemas"; From ddfad21f8f621a95bc7a1a437a191079abfeb4c4 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 10:52:49 -0500 Subject: [PATCH 10/33] chore: applied suggested changes from PR #1414 --- apps/ensapi/src/handlers/name-tokens-api.ts | 187 +++++++++++------- .../src/api/name-tokens/deserialize.ts | 4 +- .../src/api/name-tokens/zod-schemas.ts | 123 ++++++------ packages/ensnode-sdk/src/internal.ts | 1 + .../ensnode-sdk/src/tokenscope/zod-schemas.ts | 39 +++- 5 files changed, 211 insertions(+), 143 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index c13f59969..ce975c9e6 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -1,5 +1,6 @@ import config from "@/config"; +import { describeRoute, resolver } from "hono-openapi"; import { namehash } from "viem"; import z from "zod/v4"; @@ -14,7 +15,7 @@ import { type PluginName, serializeNameTokensResponse, } from "@ensnode/ensnode-sdk"; -import { makeNodeSchema } from "@ensnode/ensnode-sdk/internal"; +import { makeNameTokensResponseSchema, makeNodeSchema } from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; @@ -53,47 +54,117 @@ const requestQuerySchema = z message: "Exactly one of 'domainId' or 'name' must be provided", }); -app.get("/", validate("query", requestQuerySchema), async (c) => { - // Invariant: context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(name-tokens-api): indexingStatusMiddleware required`); - } - - // Invariant: Indexing Status has been resolved successfully. - if (c.var.indexingStatus instanceof Error) { - throw new Error(`Invariant(name-tokens-api): Indexing Status has to be resolved successfully`); - } - - const request = c.req.valid("query") satisfies NameTokensRequest; - let domainId: Node; +app.get( + "/", + describeRoute({ + summary: "Get Name Tokens", + description: "Returns name tokens for requested identifier (domainId, or name)", + responses: { + 200: { + description: "Successfully retrieved name tokens", + content: { + "application/json": { + schema: resolver(makeNameTokensResponseSchema("Name Tokens Response", true), { + elo: 1, + }), + }, + }, + }, + 500: { + description: "Error retrieving name tokens", + content: { + "application/json": { + schema: resolver(makeNameTokensResponseSchema("Name Tokens Response", true), { + elo: 2, + }), + }, + }, + }, + }, + }), + validate("query", requestQuerySchema), + async (c) => { + // Invariant: context must be set by the required middleware + if (c.var.indexingStatus === undefined) { + throw new Error(`Invariant(name-tokens-api): indexingStatusMiddleware required`); + } - if (request.name !== undefined) { - const { name } = request; + // Invariant: Indexing Status has been resolved successfully. + if (c.var.indexingStatus instanceof Error) { + throw new Error( + `Invariant(name-tokens-api): Indexing Status has to be resolved successfully`, + ); + } - // return 404 when the requested name was the ENS Root - if (name === ENS_ROOT) { - return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, - error: { - message: "No indexed Name Tokens found", - details: `The 'name' param must not be ENS Root, no tokens exist for it.`, - }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), - 404, + const request = c.req.valid("query") satisfies NameTokensRequest; + let domainId: Node; + + if (request.name !== undefined) { + const { name } = request; + + // return 404 when the requested name was the ENS Root + if (name === ENS_ROOT) { + return c.json( + serializeNameTokensResponse({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, + error: { + message: "No indexed Name Tokens found", + details: `The 'name' param must not be ENS Root, no tokens exist for it.`, + }, + } satisfies NameTokensResponseErrorNameTokensNotIndexed), + 404, + ); + } + + const parentNode = namehash(getParentNameFQDN(name)); + const subregistry = indexedSubregistries.find( + (subregistry) => subregistry.node === parentNode, ); + + // Return 404 response with error code for Name Tokens Not Indexed when + // the parent name of the requested name was not registered in any of + // the actively indexed subregistries. + if (!subregistry) { + logger.error( + `This ENSNode instance has not been configured to index tokens for the requested name: '${name}'.`, + ); + + return c.json( + serializeNameTokensResponse({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, + error: { + message: "No indexed Name Tokens found", + details: `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, + }, + } satisfies NameTokensResponseErrorNameTokensNotIndexed), + 404, + ); + } + + domainId = namehash(name); + } else if (request.domainId !== undefined) { + domainId = request.domainId; + } else { + // This should never happen due to Zod validation, but TypeScript needs this + throw new Error("Invariant(name-tokens-api): Either name or domainId must be provided"); } - const parentNode = namehash(getParentNameFQDN(name)); - const subregistry = indexedSubregistries.find((subregistry) => subregistry.node === parentNode); + const { omnichainSnapshot } = c.var.indexingStatus.snapshot; + const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor; + + const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf); // Return 404 response with error code for Name Tokens Not Indexed when - // the parent name of the requested name was not registered in any of - // the actively indexed subregistries. - if (!subregistry) { + // the no name tokens were found for the domain ID associated with + // the requested name. + if (!registeredNameTokens) { + const errorMessageSubject = + request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`; + logger.error( - `This ENSNode instance has not been configured to index tokens for the requested name: '${name}'.`, + `This ENSNode instance has never indexed tokens for the requested ${errorMessageSubject}.`, ); return c.json( @@ -102,56 +173,20 @@ app.get("/", validate("query", requestQuerySchema), async (c) => { errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, error: { message: "No indexed Name Tokens found", - details: `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, + details: `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, }, } satisfies NameTokensResponseErrorNameTokensNotIndexed), 404, ); } - domainId = namehash(name); - } else if (request.domainId !== undefined) { - domainId = request.domainId; - } else { - // This should never happen due to Zod validation, but TypeScript needs this - throw new Error("Invariant(name-tokens-api): Either name or domainId must be provided"); - } - - const { omnichainSnapshot } = c.var.indexingStatus.snapshot; - const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor; - - const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf); - - // Return 404 response with error code for Name Tokens Not Indexed when - // the no name tokens were found for the domain ID associated with - // the requested name. - if (!registeredNameTokens) { - const errorMessageSubject = - request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`; - - logger.error( - `This ENSNode instance has never indexed tokens for the requested ${errorMessageSubject}.`, - ); - return c.json( serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, - error: { - message: "No indexed Name Tokens found", - details: `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, - }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), - 404, + responseCode: NameTokensResponseCodes.Ok, + registeredNameTokens, + }), ); - } - - return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Ok, - registeredNameTokens, - }), - ); -}); + }, +); export default app; diff --git a/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts b/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts index a0fb34ac0..c16c1de0f 100644 --- a/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts +++ b/packages/ensnode-sdk/src/api/name-tokens/deserialize.ts @@ -9,7 +9,9 @@ import { makeNameTokensResponseSchema } from "./zod-schemas"; export function deserializedNameTokensResponse( maybeResponse: SerializedNameTokensResponse, ): NameTokensResponse { - const parsed = makeNameTokensResponseSchema().safeParse(maybeResponse); + const parsed = makeNameTokensResponseSchema("Name Tokens Response", false).safeParse( + maybeResponse, + ); if (parsed.error) { throw new Error(`Cannot deserialize NameTokensResponse:\n${prettifyError(parsed.error)}\n`); diff --git a/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts b/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts index 6bf23db3e..344f2d108 100644 --- a/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts +++ b/packages/ensnode-sdk/src/api/name-tokens/zod-schemas.ts @@ -1,6 +1,5 @@ import { namehash } from "viem"; import z from "zod/v4"; -import type { ParsePayload } from "zod/v4/core"; import { makeNodeSchema, @@ -22,78 +21,78 @@ import { type RegisteredNameTokens, } from "./response"; -function invariant_nameIsAssociatedWithDomainId(ctx: ParsePayload) { - const { name, domainId } = ctx.value; - - if (namehash(name) !== domainId) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `'name' must be associated with 'domainId': ${domainId}`, - }); - } -} - -function invariant_nameTokensOwnershipTypeNameWrapperRequiresOwnershipTypeFullyOnchainOrUnknown( - ctx: ParsePayload, -) { - const { tokens } = ctx.value; - const containsOwnershipNameWrapper = tokens.some( - (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.NameWrapper, - ); - const containsOwnershipFullyOnchainOrUnknown = tokens.some( - (t) => - t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain || - t.ownership.ownershipType === NameTokenOwnershipTypes.Unknown, - ); - if (containsOwnershipNameWrapper && !containsOwnershipFullyOnchainOrUnknown) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `'tokens' must contain name token with ownership type 'fully-onchain' or 'unknown' when name token with ownership type 'namewrapper' in listed`, - }); - } -} - -function invariant_nameTokensContainAtMostOneWithOwnershipTypeEffective( - ctx: ParsePayload, -) { - const { tokens } = ctx.value; - const tokensCountWithOwnershipFullyOnchain = tokens.filter( - (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain, - ).length; - if (tokensCountWithOwnershipFullyOnchain > 1) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: `'tokens' must contain at most one name token with ownership type 'fully-onchain', current count: ${tokensCountWithOwnershipFullyOnchain}`, - }); - } -} - /** * Schema for {@link RegisteredNameTokens}. */ -export const makeRegisteredNameTokenSchema = (valueLabel: string = "Registered Name Token") => +export const makeRegisteredNameTokenSchema = ( + valueLabel: string = "Registered Name Token", + serializable?: SerializableType, +) => z .object({ domainId: makeNodeSchema(`${valueLabel}.domainId`), name: makeReinterpretedNameSchema(valueLabel), - tokens: z.array(makeNameTokenSchema(`${valueLabel}.tokens`)).nonempty(), + tokens: z.array(makeNameTokenSchema(`${valueLabel}.tokens`, serializable)).nonempty(), expiresAt: makeUnixTimestampSchema(`${valueLabel}.expiresAt`), accurateAsOf: makeUnixTimestampSchema(`${valueLabel}.accurateAsOf`), }) - .check(invariant_nameIsAssociatedWithDomainId) - .check(invariant_nameTokensContainAtMostOneWithOwnershipTypeEffective) - .check(invariant_nameTokensOwnershipTypeNameWrapperRequiresOwnershipTypeFullyOnchainOrUnknown); + .check(function invariant_nameIsAssociatedWithDomainId(ctx) { + const { name, domainId } = ctx.value; + + if (namehash(name) !== domainId) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'name' must be associated with 'domainId': ${domainId}`, + }); + } + }) + .check( + function invariant_nameTokensOwnershipTypeNameWrapperRequiresOwnershipTypeFullyOnchainOrUnknown( + ctx, + ) { + const { tokens } = ctx.value; + const containsOwnershipNameWrapper = tokens.some( + (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.NameWrapper, + ); + const containsOwnershipFullyOnchainOrUnknown = tokens.some( + (t) => + t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain || + t.ownership.ownershipType === NameTokenOwnershipTypes.Unknown, + ); + if (containsOwnershipNameWrapper && !containsOwnershipFullyOnchainOrUnknown) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'tokens' must contain name token with ownership type 'fully-onchain' or 'unknown' when name token with ownership type 'namewrapper' in listed`, + }); + } + }, + ) + .check(function invariant_nameTokensContainAtMostOneWithOwnershipTypeEffective(ctx) { + const { tokens } = ctx.value; + const tokensCountWithOwnershipFullyOnchain = tokens.filter( + (t) => t.ownership.ownershipType === NameTokenOwnershipTypes.FullyOnchain, + ).length; + if (tokensCountWithOwnershipFullyOnchain > 1) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'tokens' must contain at most one name token with ownership type 'fully-onchain', current count: ${tokensCountWithOwnershipFullyOnchain}`, + }); + } + }); /** * Schema for {@link NameTokensResponseOk} */ -export const makeNameTokensResponseOkSchema = (valueLabel: string = "Name Tokens Response OK") => +export const makeNameTokensResponseOkSchema = ( + valueLabel: string = "Name Tokens Response OK", + serializable?: SerializableType, +) => z.strictObject({ responseCode: z.literal(NameTokensResponseCodes.Ok), - registeredNameTokens: makeRegisteredNameTokenSchema(`${valueLabel}.nameTokens`), + registeredNameTokens: makeRegisteredNameTokenSchema(`${valueLabel}.nameTokens`, serializable), }); /** @@ -145,8 +144,12 @@ export const makeNameTokensResponseErrorSchema = ( /** * Schema for {@link NameTokensResponse} */ -export const makeNameTokensResponseSchema = (valueLabel: string = "Name Tokens Response") => - z.discriminatedUnion("responseCode", [ - makeNameTokensResponseOkSchema(valueLabel), +export const makeNameTokensResponseSchema = ( + valueLabel: string = "Name Tokens Response", + serializable?: SerializableType, +) => { + return z.discriminatedUnion("responseCode", [ + makeNameTokensResponseOkSchema(valueLabel, serializable ?? false), makeNameTokensResponseErrorSchema(valueLabel), ]); +}; diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 793f4a22a..a8370509d 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -14,6 +14,7 @@ export * from "./api/indexing-status/zod-schemas"; export * from "./api/registrar-actions/zod-schemas"; +export * from "./api/name-tokens/zod-schemas"; export * from "./api/resolution/zod-schemas"; export * from "./api/shared/errors/zod-schemas"; export * from "./api/shared/pagination/zod-schemas"; diff --git a/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts b/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts index 24475e156..8d866e6ab 100644 --- a/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts +++ b/packages/ensnode-sdk/src/tokenscope/zod-schemas.ts @@ -20,17 +20,41 @@ import { type NameTokenOwnershipUnknown, } from "./name-token"; +const tokenIdSchemaSerializable = z.string(); +const tokenIdSchemaNative = z.preprocess( + (v) => (typeof v === "string" ? BigInt(v) : v), + z.bigint().positive(), +); + +export function makeTokenIdSchema( + _valueLabel: string, + serializable: SerializableType, +): SerializableType extends true ? typeof tokenIdSchemaSerializable : typeof tokenIdSchemaNative; +export function makeTokenIdSchema( + _valueLabel: string = "Token ID Schema", + serializable: true | false = false, +): typeof tokenIdSchemaSerializable | typeof tokenIdSchemaNative { + if (serializable) { + return tokenIdSchemaSerializable; + } else { + return tokenIdSchemaNative; + } +} + /** * Make schema for {@link AssetId}. * - * TODO: Find a way to make this compatible with Zod JSON Schema: https://zod.dev/json-schema#unrepresentable */ -export const makeAssetIdSchema = (valueLabel: string = "Asset ID Schema") => - z.object({ +export const makeAssetIdSchema = ( + valueLabel: string = "Asset ID Schema", + serializable?: SerializableType, +) => { + return z.object({ assetNamespace: z.enum(AssetNamespaces), contract: makeAccountIdSchema(valueLabel), - tokenId: z.preprocess((v) => (typeof v === "string" ? BigInt(v) : v), z.bigint().positive()), + tokenId: makeTokenIdSchema(valueLabel, serializable ?? false), }); +}; /** * Make schema for {@link AssetIdString}. @@ -139,9 +163,12 @@ export const makeNameTokenOwnershipSchema = (valueLabel: string = "Name Token Ow /** * Make schema for {@link NameToken}. */ -export const makeNameTokenSchema = (valueLabel: string = "Name Token Schema") => +export const makeNameTokenSchema = ( + valueLabel: string = "Name Token Schema", + serializable?: SerializableType, +) => z.object({ - token: makeAssetIdSchema(`${valueLabel}.token`), + token: makeAssetIdSchema(`${valueLabel}.token`, serializable), ownership: makeNameTokenOwnershipSchema(`${valueLabel}.ownership`), From d51eaade270e032aabeee9abb2032d965289b2aa Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 11:00:58 -0500 Subject: [PATCH 11/33] chore: rename resolver and remove unnecessary comments --- apps/ensapi/src/handlers/ensnode-api.ts | 10 ++++------ apps/ensapi/src/handlers/name-tokens-api.ts | 6 +++--- apps/ensapi/src/handlers/resolution-api.ts | 8 ++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 819e38cab..0456fc806 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import { describeRoute, resolver } from "hono-openapi"; +import { describeRoute, resolver as validationResolver } from "hono-openapi"; import { IndexingStatusResponseCodes, @@ -23,7 +23,6 @@ import resolutionApi from "./resolution-api"; const app = factory.createApp(); -// include ENSApi Public Config endpoint app.get( "/config", describeRoute({ @@ -34,7 +33,7 @@ app.get( description: "Successfully retrieved configuration", content: { "application/json": { - schema: resolver(makeENSApiPublicConfigSchema()), + schema: validationResolver(makeENSApiPublicConfigSchema()), }, }, }, @@ -46,7 +45,6 @@ app.get( }, ); -// include ENSIndexer Indexing Status endpoint app.get( "/indexing-status", describeRoute({ @@ -57,7 +55,7 @@ app.get( description: "Successfully retrieved indexing status", content: { "application/json": { - schema: resolver(makeIndexingStatusResponseSchema()), + schema: validationResolver(makeIndexingStatusResponseSchema()), }, }, }, @@ -65,7 +63,7 @@ app.get( description: "Error retrieving indexing status", content: { "application/json": { - schema: resolver(makeIndexingStatusResponseSchema()), + schema: validationResolver(makeIndexingStatusResponseSchema()), }, }, }, diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index ce975c9e6..50444e5f0 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import { describeRoute, resolver } from "hono-openapi"; +import { describeRoute, resolver as validationResolver } from "hono-openapi"; import { namehash } from "viem"; import z from "zod/v4"; @@ -64,7 +64,7 @@ app.get( description: "Successfully retrieved name tokens", content: { "application/json": { - schema: resolver(makeNameTokensResponseSchema("Name Tokens Response", true), { + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { elo: 1, }), }, @@ -74,7 +74,7 @@ app.get( description: "Error retrieving name tokens", content: { "application/json": { - schema: resolver(makeNameTokensResponseSchema("Name Tokens Response", true), { + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { elo: 2, }), }, diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index b7ac8f674..c131d17be 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -1,4 +1,4 @@ -import { describeRoute, resolver } from "hono-openapi"; +import { describeRoute, resolver as validationResolver } from "hono-openapi"; import { z } from "zod/v4"; import type { @@ -58,7 +58,7 @@ app.get( description: "Successfully resolved records", content: { "application/json": { - schema: resolver(makeResolveRecordsResponseSchema()), + schema: validationResolver(makeResolveRecordsResponseSchema()), }, }, }, @@ -127,7 +127,7 @@ app.get( description: "Successfully resolved name", content: { "application/json": { - schema: resolver(makeResolvePrimaryNameResponseSchema()), + schema: validationResolver(makeResolvePrimaryNameResponseSchema()), }, }, }, @@ -186,7 +186,7 @@ app.get( description: "Successfully resolved records", content: { "application/json": { - schema: resolver(makeResolvePrimaryNamesResponseSchema()), + schema: validationResolver(makeResolvePrimaryNamesResponseSchema()), }, }, }, From 37382abbba44b709cab7133fd033ae3818829c53 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 11:53:56 -0500 Subject: [PATCH 12/33] chore: applied suggestions from review --- apps/ensapi/src/handlers/ensnode-api.ts | 8 ++++---- apps/ensapi/src/handlers/name-tokens-api.ts | 4 ++-- apps/ensapi/src/index.ts | 10 ++++------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 0456fc806..c19e9a14f 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -26,11 +26,11 @@ const app = factory.createApp(); app.get( "/config", describeRoute({ - summary: "Get ENSApi Configuration", - description: "Returns the public configuration of the ENSApi instance", + summary: "Get ENSApi Public Config", + description: "Gets the public config of the ENSApi instance", responses: { 200: { - description: "Successfully retrieved configuration", + description: "Successfully retrieved ENSApi public config", content: { "application/json": { schema: validationResolver(makeENSApiPublicConfigSchema()), @@ -84,7 +84,7 @@ app.get( ); } - // return successful response using the indexing status projection from the context + // return successful response using the indexing status projection from the middleware context return c.json( serializeIndexingStatusResponse({ responseCode: IndexingStatusResponseCodes.Ok, diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 50444e5f0..8582c0420 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -45,7 +45,7 @@ app.use(nameTokensApiMiddleware); * Name Tokens API can be requested by either `name` or `domainId`, and * can never be requested by both, or neither. */ -const requestQuerySchema = z +const nameTokensQuerySchema = z .object({ domainId: makeNodeSchema("request.domainId").optional(), name: params.name.optional(), @@ -82,7 +82,7 @@ app.get( }, }, }), - validate("query", requestQuerySchema), + validate("query", nameTokensQuerySchema), async (c) => { // Invariant: context must be set by the required middleware if (c.var.indexingStatus === undefined) { diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index 8726de2f2..1deb8b5f1 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -53,14 +53,12 @@ app.get( openAPIRouteHandler(app, { documentation: { info: { - title: "ENSNode's APIs", + title: "ENSApi APIs", version: packageJson.version, - description: "ENSNode resolution and analytics API", + description: + "APIs for ENS resolution, navigating the ENS nameforest, and metadata about an ENSNode", }, - servers: [ - { url: `http://localhost:${config.port}`, description: "Local Development" }, - // Add your production servers here - ], + servers: [{ url: `http://localhost:${config.port}`, description: "Local Development" }], }, }), ); From c8daee54a01ff0ae8eb60923f02eb5bb75a1b457 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 14:56:03 -0500 Subject: [PATCH 13/33] chore: small updates per review feedback --- apps/ensapi/src/handlers/ensnode-api.ts | 4 ++-- apps/ensapi/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 8874c2592..985aa15a0 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -49,7 +49,7 @@ app.get( "/indexing-status", describeRoute({ summary: "Get ENSIndexer Indexing Status", - description: "Returns the current indexing status of the ENSIndexer", + description: "Returns the most recent available indexing status snapshot from the ENSIndexer", responses: { 200: { description: "Successfully retrieved indexing status", @@ -59,7 +59,7 @@ app.get( }, }, }, - 500: { + 503: { description: "Error retrieving indexing status", content: { "application/json": { diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index d61211b59..1784a7950 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -67,7 +67,7 @@ app.get( }), ); -// will automatically 500 if config is not available due to ensIndexerPublicConfigMiddleware +// will automatically 503 if config is not available due to ensIndexerPublicConfigMiddleware app.get("/health", async (c) => { return c.json({ ok: true }); }); From 3547a153e392f6d4b2a3357aa9636c6d92dab85c Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 14:56:27 -0500 Subject: [PATCH 14/33] chore: updated logic for indexing error --- apps/ensapi/src/handlers/ensnode-api.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 985aa15a0..53182cbca 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -71,11 +71,7 @@ app.get( }), async (c) => { // context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(ensnode-api): indexingStatusMiddleware required`); - } - - if (c.var.indexingStatus instanceof Error) { + if (c.var.indexingStatus === undefined || c.var.indexingStatus instanceof Error) { return c.json( serializeIndexingStatusResponse({ responseCode: IndexingStatusResponseCodes.Error, From bb6766c69fe127cec43c1d44a674f64eb163a305 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 15:02:50 -0500 Subject: [PATCH 15/33] chore: updated response codes for name tokens api --- apps/ensapi/src/handlers/name-tokens-api.ts | 38 ++++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 8582c0420..88bdf723d 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -70,8 +70,8 @@ app.get( }, }, }, - 500: { - description: "Error retrieving name tokens", + 404: { + description: "Name tokens not indexed", content: { "application/json": { schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { @@ -80,19 +80,47 @@ app.get( }, }, }, + 503: { + description: "Service unavailable - indexing status not ready", + content: { + "application/json": { + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { + elo: 3, + }), + }, + }, + }, }, }), validate("query", nameTokensQuerySchema), async (c) => { // Invariant: context must be set by the required middleware if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(name-tokens-api): indexingStatusMiddleware required`); + return c.json( + serializeNameTokensResponse({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, + error: { + message: "Name Tokens API is not available yet", + details: "Indexing status middleware is required but not initialized.", + }, + }), + 503, + ); } // Invariant: Indexing Status has been resolved successfully. if (c.var.indexingStatus instanceof Error) { - throw new Error( - `Invariant(name-tokens-api): Indexing Status has to be resolved successfully`, + return c.json( + serializeNameTokensResponse({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, + error: { + message: "Name Tokens API is not available yet", + details: "The indexing status must be available and resolved successfully.", + }, + }), + 503, ); } From 818bbc20876397386cc87cd771b5fdc83f1fc67a Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 15:12:17 -0500 Subject: [PATCH 16/33] chore: updated query object for docs --- apps/ensapi/src/handlers/resolution-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/resolution-api.ts b/apps/ensapi/src/handlers/resolution-api.ts index c131d17be..42c05558c 100644 --- a/apps/ensapi/src/handlers/resolution-api.ts +++ b/apps/ensapi/src/handlers/resolution-api.ts @@ -69,7 +69,7 @@ app.get( "query", z .object({ - ...params.selectionParams.shape, + selection: params.selection, trace: params.trace, accelerate: params.accelerate, }) From 1ce85770d97adf3691737923e9ffc3ebaa13f1a4 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 19 Dec 2025 15:38:18 -0500 Subject: [PATCH 17/33] chore: lint --- packages/ensnode-sdk/src/internal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index a8370509d..38c3fd736 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -13,8 +13,8 @@ */ export * from "./api/indexing-status/zod-schemas"; -export * from "./api/registrar-actions/zod-schemas"; export * from "./api/name-tokens/zod-schemas"; +export * from "./api/registrar-actions/zod-schemas"; export * from "./api/resolution/zod-schemas"; export * from "./api/shared/errors/zod-schemas"; export * from "./api/shared/pagination/zod-schemas"; From 11181af0cc23a52d2c65dda64b8c9e38f7a8c75f Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 22 Dec 2025 16:05:50 -0500 Subject: [PATCH 18/33] chore: updated lndexing-status description --- apps/ensapi/src/handlers/ensnode-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 53182cbca..9139ee670 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -49,7 +49,7 @@ app.get( "/indexing-status", describeRoute({ summary: "Get ENSIndexer Indexing Status", - description: "Returns the most recent available indexing status snapshot from the ENSIndexer", + description: "Returns the indexing status snapshot most recently captured from ENSIndexer", responses: { 200: { description: "Successfully retrieved indexing status", From a6373806287d5f89cf421b1715bbd1dba712fd2b Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 22 Dec 2025 16:59:17 -0500 Subject: [PATCH 19/33] chore: applied suggestions from review --- apps/ensapi/src/handlers/ensnode-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 9139ee670..2d7c94586 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -60,7 +60,7 @@ app.get( }, }, 503: { - description: "Error retrieving indexing status", + description: "Indexing status snapshot unavailable", content: { "application/json": { schema: validationResolver(makeIndexingStatusResponseSchema()), From 25596f4e96e26c439a442d4a6ce71712981feeb1 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 10:43:54 -0500 Subject: [PATCH 20/33] chore: applied suggestions from review --- apps/ensapi/src/handlers/name-tokens-api.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 88bdf723d..aba425211 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -58,10 +58,10 @@ app.get( "/", describeRoute({ summary: "Get Name Tokens", - description: "Returns name tokens for requested identifier (domainId, or name)", + description: "Returns name tokens for the requested identifier (domainId or name)", responses: { 200: { - description: "Successfully retrieved name tokens", + description: "Name tokens known", content: { "application/json": { schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { @@ -154,10 +154,6 @@ app.get( // the parent name of the requested name was not registered in any of // the actively indexed subregistries. if (!subregistry) { - logger.error( - `This ENSNode instance has not been configured to index tokens for the requested name: '${name}'.`, - ); - return c.json( serializeNameTokensResponse({ responseCode: NameTokensResponseCodes.Error, @@ -191,10 +187,6 @@ app.get( const errorMessageSubject = request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`; - logger.error( - `This ENSNode instance has never indexed tokens for the requested ${errorMessageSubject}.`, - ); - return c.json( serializeNameTokensResponse({ responseCode: NameTokensResponseCodes.Error, From c6ad74374dc2eee03a5844eccfe8487fa4471715 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 14:39:12 -0500 Subject: [PATCH 21/33] chore: update validation errror response --- .../ensapi/src/lib/handlers/error-response.ts | 40 +++++++++++++++++-- apps/ensapi/src/lib/handlers/validate.ts | 6 ++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/apps/ensapi/src/lib/handlers/error-response.ts b/apps/ensapi/src/lib/handlers/error-response.ts index 1b30604f1..ba4327f8b 100644 --- a/apps/ensapi/src/lib/handlers/error-response.ts +++ b/apps/ensapi/src/lib/handlers/error-response.ts @@ -4,20 +4,43 @@ import { treeifyError, ZodError } from "zod/v4"; import type { ErrorResponse } from "@ensnode/ensnode-sdk"; +/** + * Represents a validation issue from the Standard Schema specification. + * This is a subset of the StandardSchemaV1.Issue type used for validation errors. + */ +interface ValidationIssue { + readonly message: string; + readonly path?: readonly PropertyKey[]; +} + +/** + * Type guard to check if input is a Standard Schema issues array + */ +const isStandardSchemaIssues = (input: unknown): input is readonly ValidationIssue[] => { + return ( + Array.isArray(input) && + input.length > 0 && + typeof input[0] === "object" && + input[0] !== null && + "message" in input[0] + ); +}; + /** * Creates a standardized error response for the ENSApi. * * Handles different types of errors and converts them to appropriate HTTP responses - * with consistent error formatting. ZodErrors return 400 status codes with validation - * details, while other errors return 500 status codes. + * with consistent error formatting. ZodErrors and Standard Schema validation issues + * return 400 status codes with validation details, while other errors return 500 + * status codes. * * @param c - Hono context object - * @param input - The error input (ZodError, Error, string, or unknown) + * @param input - The error input (ZodError, ValidationIssue[], Error, string, or unknown) * @returns JSON error response with appropriate HTTP status code */ export const errorResponse = ( c: Context, - input: ZodError | Error | string | unknown, + input: ZodError | readonly ValidationIssue[] | Error | string | unknown, statusCode: ClientErrorStatusCode | ServerErrorStatusCode = 500, ) => { if (input instanceof ZodError) { @@ -27,6 +50,15 @@ export const errorResponse = ( ); } + if (isStandardSchemaIssues(input)) { + // Convert Standard Schema issues to ZodError for consistent formatting + const zodError = new ZodError(input as ZodError["issues"]); + return c.json( + { message: "Invalid Input", details: treeifyError(zodError) } satisfies ErrorResponse, + 400, + ); + } + if (input instanceof Error) { return c.json({ message: input.message } satisfies ErrorResponse, statusCode); } diff --git a/apps/ensapi/src/lib/handlers/validate.ts b/apps/ensapi/src/lib/handlers/validate.ts index 704437424..2eac132c1 100644 --- a/apps/ensapi/src/lib/handlers/validate.ts +++ b/apps/ensapi/src/lib/handlers/validate.ts @@ -20,5 +20,9 @@ export const validate = validator(target, schema, (result, c) => { // if validation failed, return our custom-formatted ErrorResponse instead of default - if (!result.success) return errorResponse(c, result.error); + if (!result.success) { + // Pass the Standard Schema issues array to errorResponse + // It will handle converting them to the proper format + return errorResponse(c, result.error); + } }); From d8925ade9b6731392d1f637a69c884dda25778fe Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 15:29:12 -0500 Subject: [PATCH 22/33] chore: updated validate and error-response --- apps/ensapi/package.json | 1 + .../ensapi/src/lib/handlers/error-response.ts | 33 ++++--------------- apps/ensapi/src/lib/handlers/validate.ts | 9 ++--- pnpm-lock.yaml | 8 +++++ 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/apps/ensapi/package.json b/apps/ensapi/package.json index af341698f..d421d0f66 100644 --- a/apps/ensapi/package.json +++ b/apps/ensapi/package.json @@ -41,6 +41,7 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@ponder/utils": "catalog:", + "@standard-schema/utils": "^0.3.0", "date-fns": "catalog:", "drizzle-orm": "catalog:", "hono": "catalog:", diff --git a/apps/ensapi/src/lib/handlers/error-response.ts b/apps/ensapi/src/lib/handlers/error-response.ts index ba4327f8b..4b3f7e5a4 100644 --- a/apps/ensapi/src/lib/handlers/error-response.ts +++ b/apps/ensapi/src/lib/handlers/error-response.ts @@ -1,46 +1,25 @@ import type { Context } from "hono"; import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status"; +import { SchemaError } from "@standard-schema/utils"; import { treeifyError, ZodError } from "zod/v4"; import type { ErrorResponse } from "@ensnode/ensnode-sdk"; -/** - * Represents a validation issue from the Standard Schema specification. - * This is a subset of the StandardSchemaV1.Issue type used for validation errors. - */ -interface ValidationIssue { - readonly message: string; - readonly path?: readonly PropertyKey[]; -} - -/** - * Type guard to check if input is a Standard Schema issues array - */ -const isStandardSchemaIssues = (input: unknown): input is readonly ValidationIssue[] => { - return ( - Array.isArray(input) && - input.length > 0 && - typeof input[0] === "object" && - input[0] !== null && - "message" in input[0] - ); -}; - /** * Creates a standardized error response for the ENSApi. * * Handles different types of errors and converts them to appropriate HTTP responses - * with consistent error formatting. ZodErrors and Standard Schema validation issues + * with consistent error formatting. ZodErrors and Standard Schema validation errors * return 400 status codes with validation details, while other errors return 500 * status codes. * * @param c - Hono context object - * @param input - The error input (ZodError, ValidationIssue[], Error, string, or unknown) + * @param input - The error input (ZodError, SchemaError, Error, string, or unknown) * @returns JSON error response with appropriate HTTP status code */ export const errorResponse = ( c: Context, - input: ZodError | readonly ValidationIssue[] | Error | string | unknown, + input: ZodError | SchemaError | Error | string | unknown, statusCode: ClientErrorStatusCode | ServerErrorStatusCode = 500, ) => { if (input instanceof ZodError) { @@ -50,9 +29,9 @@ export const errorResponse = ( ); } - if (isStandardSchemaIssues(input)) { + if (input instanceof SchemaError) { // Convert Standard Schema issues to ZodError for consistent formatting - const zodError = new ZodError(input as ZodError["issues"]); + const zodError = new ZodError(input.issues as ZodError["issues"]); return c.json( { message: "Invalid Input", details: treeifyError(zodError) } satisfies ErrorResponse, 400, diff --git a/apps/ensapi/src/lib/handlers/validate.ts b/apps/ensapi/src/lib/handlers/validate.ts index 2eac132c1..c301fe1e2 100644 --- a/apps/ensapi/src/lib/handlers/validate.ts +++ b/apps/ensapi/src/lib/handlers/validate.ts @@ -1,5 +1,6 @@ import type { ValidationTargets } from "hono"; import { validator } from "hono-openapi"; +import { SchemaError } from "@standard-schema/utils"; import type { ZodType } from "zod/v4"; import { errorResponse } from "./error-response"; @@ -7,7 +8,7 @@ import { errorResponse } from "./error-response"; /** * Creates a Hono validation middleware with custom error formatting. * - * Wraps the Hono zValidator with custom error handling that uses the + * Wraps the Hono validator with custom error handling that uses the * errorResponse function for consistent error formatting across the API. * * @param target - The validation target (param, query, json, etc.) @@ -21,8 +22,8 @@ export const validate = { // if validation failed, return our custom-formatted ErrorResponse instead of default if (!result.success) { - // Pass the Standard Schema issues array to errorResponse - // It will handle converting them to the proper format - return errorResponse(c, result.error); + // Wrap the Standard Schema issues in a SchemaError instance + // for consistent error handling in errorResponse + return errorResponse(c, new SchemaError(result.error)); } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b48337690..0f9d54782 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -337,6 +337,9 @@ importers: '@ponder/utils': specifier: 'catalog:' version: 0.2.14(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76)) + '@standard-schema/utils': + specifier: ^0.3.0 + version: 0.3.0 date-fns: specifier: 'catalog:' version: 4.1.0 @@ -3173,6 +3176,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -10551,6 +10557,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 From c00f656762cc209d9959d029ded571d9a21cbef5 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 15:42:17 -0500 Subject: [PATCH 23/33] chore: refactored 404 responses from name tokens api --- apps/ensapi/src/handlers/name-tokens-api.ts | 56 +++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index aba425211..b424026ad 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -20,15 +20,12 @@ import { makeNameTokensResponseSchema, makeNodeSchema } from "@ensnode/ensnode-s import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; import { factory } from "@/lib/hono-factory"; -import { makeLogger } from "@/lib/logger"; import { findRegisteredNameTokensForDomain } from "@/lib/name-tokens/find-name-tokens-for-domain"; import { getIndexedSubregistries } from "@/lib/name-tokens/get-indexed-subregistries"; import { nameTokensApiMiddleware } from "@/middleware/name-tokens.middleware"; const app = factory.createApp(); -const logger = makeLogger("name-tokens-api"); - const indexedSubregistries = getIndexedSubregistries( config.namespace, config.ensIndexerPublicConfig.plugins as PluginName[], @@ -54,6 +51,20 @@ const nameTokensQuerySchema = z message: "Exactly one of 'domainId' or 'name' must be provided", }); +/** + * Factory function for creating a 404 Name Tokens Not Indexed error response + */ +const makeNameTokensNotIndexedResponse = ( + details: string, +): NameTokensResponseErrorNameTokensNotIndexed => ({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, + error: { + message: "No indexed Name Tokens found", + details, + }, +}); + app.get( "/", describeRoute({ @@ -133,14 +144,11 @@ app.get( // return 404 when the requested name was the ENS Root if (name === ENS_ROOT) { return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, - error: { - message: "No indexed Name Tokens found", - details: `The 'name' param must not be ENS Root, no tokens exist for it.`, - }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `The 'name' param must not be ENS Root, no tokens exist for it.`, + ), + ), 404, ); } @@ -155,14 +163,11 @@ app.get( // the actively indexed subregistries. if (!subregistry) { return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, - error: { - message: "No indexed Name Tokens found", - details: `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, - }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, + ), + ), 404, ); } @@ -188,14 +193,11 @@ app.get( request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`; return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.NameTokensNotIndexed, - error: { - message: "No indexed Name Tokens found", - details: `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, - }, - } satisfies NameTokensResponseErrorNameTokensNotIndexed), + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, + ), + ), 404, ); } From 58b9e73b486a8d33ac2d67d0cfb76d8e6dff50f7 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 16:01:58 -0500 Subject: [PATCH 24/33] chore: added 400 reponse to name tokens api schema --- apps/ensapi/src/handlers/name-tokens-api.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index b424026ad..0e550a7c3 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -15,7 +15,11 @@ import { type PluginName, serializeNameTokensResponse, } from "@ensnode/ensnode-sdk"; -import { makeNameTokensResponseSchema, makeNodeSchema } from "@ensnode/ensnode-sdk/internal"; +import { + makeNameTokensResponseSchema, + makeNodeSchema, + ErrorResponseSchema, +} from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; import { validate } from "@/lib/handlers/validate"; @@ -81,12 +85,22 @@ app.get( }, }, }, + 400: { + description: "Invalid input", + content: { + "application/json": { + schema: validationResolver(ErrorResponseSchema, { + elo: 2, + }), + }, + }, + }, 404: { description: "Name tokens not indexed", content: { "application/json": { schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { - elo: 2, + elo: 3, }), }, }, @@ -96,7 +110,7 @@ app.get( content: { "application/json": { schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { - elo: 3, + elo: 4, }), }, }, From c436fd0fed12a818f19e4c5adcc140ae940b465f Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 16:44:30 -0500 Subject: [PATCH 25/33] chore: updated error message for 503 --- apps/ensapi/src/handlers/name-tokens-api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 0e550a7c3..365413193 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -142,7 +142,8 @@ app.get( errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, error: { message: "Name Tokens API is not available yet", - details: "The indexing status must be available and resolved successfully.", + details: + "Indexing status has not yet reached the required state to enable the Name Tokens API.", }, }), 503, From f91a9f84258df23f7676129bdb57dc541e066bf6 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Dec 2025 16:50:23 -0500 Subject: [PATCH 26/33] chore: updated comment --- apps/ensapi/src/handlers/name-tokens-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 365413193..1d71fa51e 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -134,7 +134,7 @@ app.get( ); } - // Invariant: Indexing Status has been resolved successfully. + // Indexing Status has been resolved successfully. if (c.var.indexingStatus instanceof Error) { return c.json( serializeNameTokensResponse({ From 7603c97de48de3a165046db4c52cf8604f81140c Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 29 Dec 2025 13:20:52 -0500 Subject: [PATCH 27/33] chore: updated description for name tokens api 503 response --- apps/ensapi/src/handlers/name-tokens-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 1d71fa51e..629c08019 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -106,7 +106,7 @@ app.get( }, }, 503: { - description: "Service unavailable - indexing status not ready", + description: "Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)", content: { "application/json": { schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { From 6a8ac0bbe6bd0cec5e7424e9b30823b275613f6d Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 29 Dec 2025 13:47:50 -0500 Subject: [PATCH 28/33] chore: refactored response schema and added 500 case --- apps/ensapi/src/handlers/name-tokens-api.ts | 27 +++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 629c08019..b9576f1c6 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -79,9 +79,7 @@ app.get( description: "Name tokens known", content: { "application/json": { - schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { - elo: 1, - }), + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), }, }, }, @@ -89,9 +87,7 @@ app.get( description: "Invalid input", content: { "application/json": { - schema: validationResolver(ErrorResponseSchema, { - elo: 2, - }), + schema: validationResolver(ErrorResponseSchema), }, }, }, @@ -99,19 +95,24 @@ app.get( description: "Name tokens not indexed", content: { "application/json": { - schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { - elo: 3, - }), + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: validationResolver(ErrorResponseSchema), }, }, }, 503: { - description: "Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)", + description: + "Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)", content: { "application/json": { - schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true), { - elo: 4, - }), + schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), }, }, }, From 508260af12c14953103cc2f9cbdfff76e765a3d0 Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 29 Dec 2025 14:06:14 -0500 Subject: [PATCH 29/33] chore: update comment --- apps/ensapi/src/handlers/name-tokens-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index b9576f1c6..7cd758107 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -175,8 +175,8 @@ app.get( ); // Return 404 response with error code for Name Tokens Not Indexed when - // the parent name of the requested name was not registered in any of - // the actively indexed subregistries. + // the parent name of the requested name does not match any of the + // actively indexed subregistries. if (!subregistry) { return c.json( serializeNameTokensResponse( From 487a39a2177973051613ac7d3a55a771bedc3d9a Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 29 Dec 2025 14:17:07 -0500 Subject: [PATCH 30/33] chore: lint --- apps/ensapi/src/handlers/name-tokens-api.ts | 2 +- apps/ensapi/src/index.ts | 2 +- apps/ensapi/src/lib/handlers/error-response.ts | 2 +- apps/ensapi/src/lib/handlers/validate.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index 7cd758107..c6dad3ca0 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -16,9 +16,9 @@ import { serializeNameTokensResponse, } from "@ensnode/ensnode-sdk"; import { + ErrorResponseSchema, makeNameTokensResponseSchema, makeNodeSchema, - ErrorResponseSchema, } from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index 29825526f..9c7ca9c53 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -4,8 +4,8 @@ import config from "@/config"; import { serve } from "@hono/node-server"; import { otel } from "@hono/otel"; import { cors } from "hono/cors"; -import { openAPIRouteHandler } from "hono-openapi"; import { html } from "hono/html"; +import { openAPIRouteHandler } from "hono-openapi"; import { prettyPrintJson } from "@ensnode/ensnode-sdk/internal"; diff --git a/apps/ensapi/src/lib/handlers/error-response.ts b/apps/ensapi/src/lib/handlers/error-response.ts index 610f8d71b..d1f489b04 100644 --- a/apps/ensapi/src/lib/handlers/error-response.ts +++ b/apps/ensapi/src/lib/handlers/error-response.ts @@ -1,6 +1,6 @@ +import { SchemaError } from "@standard-schema/utils"; import type { Context } from "hono"; import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status"; -import { SchemaError } from "@standard-schema/utils"; import { treeifyError, ZodError } from "zod/v4"; import type { ErrorResponse } from "@ensnode/ensnode-sdk"; diff --git a/apps/ensapi/src/lib/handlers/validate.ts b/apps/ensapi/src/lib/handlers/validate.ts index c301fe1e2..4316e07bb 100644 --- a/apps/ensapi/src/lib/handlers/validate.ts +++ b/apps/ensapi/src/lib/handlers/validate.ts @@ -1,6 +1,6 @@ +import { SchemaError } from "@standard-schema/utils"; import type { ValidationTargets } from "hono"; import { validator } from "hono-openapi"; -import { SchemaError } from "@standard-schema/utils"; import type { ZodType } from "zod/v4"; import { errorResponse } from "./error-response"; From ce1208747960b5c85c596f540787a2a8955abfb2 Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 29 Dec 2025 17:29:42 -0500 Subject: [PATCH 31/33] chore: updated comments --- apps/ensapi/src/handlers/name-tokens-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index c6dad3ca0..e8a81c913 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -135,7 +135,7 @@ app.get( ); } - // Indexing Status has been resolved successfully. + // Check if Indexing Status resolution failed. if (c.var.indexingStatus instanceof Error) { return c.json( serializeNameTokensResponse({ @@ -202,7 +202,7 @@ app.get( const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf); // Return 404 response with error code for Name Tokens Not Indexed when - // the no name tokens were found for the domain ID associated with + // no name tokens were found for the domain ID associated with // the requested name. if (!registeredNameTokens) { const errorMessageSubject = From aaeb6b2f2f1f5253948fbe0005cfafd55df99229 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 30 Dec 2025 09:39:25 -0500 Subject: [PATCH 32/33] chore: updated indexing status checks --- apps/ensapi/src/handlers/ensnode-api.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/ensapi/src/handlers/ensnode-api.ts b/apps/ensapi/src/handlers/ensnode-api.ts index 2d7c94586..e3e19d779 100644 --- a/apps/ensapi/src/handlers/ensnode-api.ts +++ b/apps/ensapi/src/handlers/ensnode-api.ts @@ -71,7 +71,11 @@ app.get( }), async (c) => { // context must be set by the required middleware - if (c.var.indexingStatus === undefined || c.var.indexingStatus instanceof Error) { + if (c.var.indexingStatus === undefined) { + throw new Error(`Invariant(indexing-status): indexingStatusMiddleware required`); + } + + if (c.var.indexingStatus instanceof Error) { return c.json( serializeIndexingStatusResponse({ responseCode: IndexingStatusResponseCodes.Error, From 95b79ec6e3495b07c8667c91f290183a485696bf Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 30 Dec 2025 09:47:39 -0500 Subject: [PATCH 33/33] docs(changeset): Adds OpenAPI schema endpoint and route descriptions to ENSApi --- .changeset/vast-parents-roll.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/vast-parents-roll.md diff --git a/.changeset/vast-parents-roll.md b/.changeset/vast-parents-roll.md new file mode 100644 index 000000000..cd32acf8e --- /dev/null +++ b/.changeset/vast-parents-roll.md @@ -0,0 +1,6 @@ +--- +"@ensnode/ensnode-sdk": patch +"ensapi": patch +--- + +Adds OpenAPI schema endpoint and route descriptions to ENSApi