diff --git a/apps/ensapi/src/handlers/name-tokens-api.routes.ts b/apps/ensapi/src/handlers/name-tokens-api.routes.ts new file mode 100644 index 000000000..f3a308c01 --- /dev/null +++ b/apps/ensapi/src/handlers/name-tokens-api.routes.ts @@ -0,0 +1,85 @@ +import { createRoute } from "@hono/zod-openapi"; +import { z } from "zod/v4"; + +import { + ErrorResponseSchema, + makeNameTokensResponseSchema, + makeNodeSchema, +} from "@ensnode/ensnode-sdk/internal"; + +import { params } from "@/lib/handlers/params.schema"; + +export const basePath = "/api/name-tokens"; + +/** + * Request Query Schema + * + * Name Tokens API can be requested by either `name` or `domainId`, and + * can never be requested by both, or neither. + */ +export const nameTokensQuerySchema = z + .object({ + domainId: makeNodeSchema("request.domainId").optional().describe("Domain node hash identifier"), + name: params.name.optional().describe("ENS name to look up tokens for"), + }) + .refine((data) => (data.domainId !== undefined) !== (data.name !== undefined), { + message: "Exactly one of 'domainId' or 'name' must be provided", + }); + +export type NameTokensQuery = z.output; + +export const getNameTokensRoute = createRoute({ + method: "get", + path: "/", + tags: ["Explore"], + summary: "Get Name Tokens", + description: "Returns name tokens for the requested identifier (domainId or name)", + request: { + query: nameTokensQuerySchema, + }, + responses: { + 200: { + description: "Name tokens known", + content: { + "application/json": { + schema: makeNameTokensResponseSchema("Name Tokens Response", true), + }, + }, + }, + 400: { + description: "Invalid input", + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + }, + 404: { + description: "Name tokens not indexed", + content: { + "application/json": { + schema: makeNameTokensResponseSchema("Name Tokens Response", true), + }, + }, + }, + 500: { + description: "Internal server error", + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + }, + 503: { + description: + "Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)", + content: { + "application/json": { + schema: makeNameTokensResponseSchema("Name Tokens Response", true), + }, + }, + }, + }, +}); + +export const routes = [getNameTokensRoute]; diff --git a/apps/ensapi/src/handlers/name-tokens-api.ts b/apps/ensapi/src/handlers/name-tokens-api.ts index fead1a0da..888d9b645 100644 --- a/apps/ensapi/src/handlers/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/name-tokens-api.ts @@ -1,8 +1,6 @@ import config from "@/config"; -import { describeRoute, resolver as validationResolver } from "hono-openapi"; import { namehash } from "viem"; -import { z } from "zod/v4"; import { ENS_ROOT, @@ -15,20 +13,15 @@ import { type PluginName, serializeNameTokensResponse, } from "@ensnode/ensnode-sdk"; -import { - ErrorResponseSchema, - makeNameTokensResponseSchema, - makeNodeSchema, -} from "@ensnode/ensnode-sdk/internal"; - -import { params } from "@/lib/handlers/params.schema"; -import { validate } from "@/lib/handlers/validate"; -import { factory } from "@/lib/hono-factory"; + +import { createApp } from "@/lib/hono-factory"; 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(); +import { getNameTokensRoute } from "./name-tokens-api.routes"; + +const app = createApp(); const indexedSubregistries = getIndexedSubregistries( config.namespace, @@ -40,21 +33,6 @@ const indexedSubregistries = getIndexedSubregistries( // and if not returns the appropriate HTTP 503 (Service Unavailable) error. app.use(nameTokensApiMiddleware); -/** - * Request Query Schema - * - * Name Tokens API can be requested by either `name` or `domainId`, and - * can never be requested by both, or neither. - */ -const nameTokensQuerySchema = z - .object({ - domainId: makeNodeSchema("request.domainId").optional().describe("Domain node hash identifier"), - name: params.name.optional().describe("ENS name to look up tokens for"), - }) - .refine((data) => (data.domainId !== undefined) !== (data.name !== undefined), { - message: "Exactly one of 'domainId' or 'name' must be provided", - }); - /** * Factory function for creating a 404 Name Tokens Not Indexed error response */ @@ -69,163 +47,110 @@ const makeNameTokensNotIndexedResponse = ( }, }); -app.get( - "/", - describeRoute({ - tags: ["Explore"], - summary: "Get Name Tokens", - description: "Returns name tokens for the requested identifier (domainId or name)", - responses: { - 200: { - description: "Name tokens known", - content: { - "application/json": { - schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), - }, - }, - }, - 400: { - description: "Invalid input", - content: { - "application/json": { - schema: validationResolver(ErrorResponseSchema), - }, - }, - }, - 404: { - description: "Name tokens not indexed", - content: { - "application/json": { - 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)", - content: { - "application/json": { - schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)), - }, +app.openapi(getNameTokensRoute, async (c) => { + // Invariant: context must be set by the required middleware + if (c.var.indexingStatus === undefined) { + 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.", }, - }, - }, - }), - validate("query", nameTokensQuerySchema), - async (c) => { - // Invariant: context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - 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, - ); - } + }), + 503, + ); + } - // Check if Indexing Status resolution failed. - if (c.var.indexingStatus instanceof Error) { - return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, - error: { - message: "Name Tokens API is not available yet", - details: - "Indexing status has not yet reached the required state to enable the Name Tokens API.", - }, - }), - 503, - ); - } + // Check if Indexing Status resolution failed. + if (c.var.indexingStatus instanceof Error) { + return c.json( + serializeNameTokensResponse({ + responseCode: NameTokensResponseCodes.Error, + errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, + error: { + message: "Name Tokens API is not available yet", + details: + "Indexing status has not yet reached the required state to enable the Name Tokens API.", + }, + }), + 503, + ); + } - const request = c.req.valid("query") satisfies NameTokensRequest; - let domainId: Node; + const request = c.req.valid("query") satisfies NameTokensRequest; + let domainId: Node; - if (request.name !== undefined) { - const { name } = request; + 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( - makeNameTokensNotIndexedResponse( - `The 'name' param must not be ENS Root, no tokens exist for it.`, - ), + // return 404 when the requested name was the ENS Root + if (name === ENS_ROOT) { + return c.json( + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `The 'name' param must not be ENS Root, no tokens exist for it.`, ), - 404, - ); - } - - const parentNode = namehash(getParentNameFQDN(name)); - const subregistry = indexedSubregistries.find( - (subregistry) => subregistry.node === parentNode, + ), + 404, ); - - // Return 404 response with error code for Name Tokens Not Indexed when - // the parent name of the requested name does not match any of the - // actively indexed subregistries. - if (!subregistry) { - return c.json( - serializeNameTokensResponse( - makeNameTokensNotIndexedResponse( - `This ENSNode instance has not been configured to index tokens for the requested name: '${name}`, - ), - ), - 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); + 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 - // 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}'`; - + // the parent name of the requested name does not match any of the + // actively indexed subregistries. + if (!subregistry) { return c.json( serializeNameTokensResponse( makeNameTokensNotIndexedResponse( - `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, + `This ENSNode instance has not been configured to index tokens for the requested name: '${name}'`, ), ), 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 + // 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}'`; + return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Ok, - registeredNameTokens, - }), + serializeNameTokensResponse( + makeNameTokensNotIndexedResponse( + `No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`, + ), + ), + 404, ); - }, -); + } + + return c.json( + serializeNameTokensResponse({ + responseCode: NameTokensResponseCodes.Ok, + registeredNameTokens, + }), + 200, + ); +}); export default app; diff --git a/apps/ensapi/src/stub-routes.ts b/apps/ensapi/src/stub-routes.ts index 73a0372a9..4dda90d2d 100644 --- a/apps/ensapi/src/stub-routes.ts +++ b/apps/ensapi/src/stub-routes.ts @@ -2,6 +2,7 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import * as amIRealtimeRoutes from "./handlers/amirealtime-api.routes"; import * as ensnodeRoutes from "./handlers/ensnode-api.routes"; +import * as nameTokensRoutes from "./handlers/name-tokens-api.routes"; import * as resolutionRoutes from "./handlers/resolution-api.routes"; /** @@ -12,7 +13,7 @@ import * as resolutionRoutes from "./handlers/resolution-api.routes"; export function createStubRoutesForSpec() { const app = new OpenAPIHono(); - const routeGroups = [amIRealtimeRoutes, ensnodeRoutes, resolutionRoutes]; + const routeGroups = [amIRealtimeRoutes, ensnodeRoutes, nameTokensRoutes, resolutionRoutes]; for (const group of routeGroups) { for (const route of group.routes) {