Skip to content

Commit bdc9d7c

Browse files
committed
refactor: use zod-openapi for name-tokens-api
1 parent c6e7d5c commit bdc9d7c

3 files changed

Lines changed: 145 additions & 164 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createRoute } from "@hono/zod-openapi";
2+
import { z } from "zod/v4";
3+
4+
import { makeNodeSchema } from "@ensnode/ensnode-sdk/internal";
5+
6+
import { params } from "@/lib/handlers/params.schema";
7+
8+
export const basePath = "/api/name-tokens";
9+
10+
/**
11+
* Request Query Schema
12+
*
13+
* Name Tokens API can be requested by either `name` or `domainId`, and
14+
* can never be requested by both, or neither.
15+
*/
16+
export const nameTokensQuerySchema = z
17+
.object({
18+
domainId: makeNodeSchema("request.domainId").optional().describe("Domain node hash identifier"),
19+
name: params.name.optional().describe("ENS name to look up tokens for"),
20+
})
21+
.refine((data) => (data.domainId !== undefined) !== (data.name !== undefined), {
22+
message: "Exactly one of 'domainId' or 'name' must be provided",
23+
});
24+
25+
export type NameTokensQuery = z.output<typeof nameTokensQuerySchema>;
26+
27+
export const getNameTokensRoute = createRoute({
28+
method: "get",
29+
path: "/",
30+
tags: ["Explore"],
31+
summary: "Get Name Tokens",
32+
description: "Returns name tokens for the requested identifier (domainId or name)",
33+
request: {
34+
query: nameTokensQuerySchema,
35+
},
36+
responses: {
37+
200: {
38+
description: "Name tokens known",
39+
},
40+
400: {
41+
description: "Invalid input",
42+
},
43+
404: {
44+
description: "Name tokens not indexed",
45+
},
46+
500: {
47+
description: "Internal server error",
48+
},
49+
503: {
50+
description:
51+
"Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)",
52+
},
53+
},
54+
});
55+
56+
export const routes = [getNameTokensRoute];
Lines changed: 87 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import config from "@/config";
22

3-
import { describeRoute, resolver as validationResolver } from "hono-openapi";
43
import { namehash } from "viem";
5-
import { z } from "zod/v4";
64

75
import {
86
ENS_ROOT,
@@ -15,20 +13,15 @@ import {
1513
type PluginName,
1614
serializeNameTokensResponse,
1715
} from "@ensnode/ensnode-sdk";
18-
import {
19-
ErrorResponseSchema,
20-
makeNameTokensResponseSchema,
21-
makeNodeSchema,
22-
} from "@ensnode/ensnode-sdk/internal";
23-
24-
import { params } from "@/lib/handlers/params.schema";
25-
import { validate } from "@/lib/handlers/validate";
26-
import { factory } from "@/lib/hono-factory";
16+
17+
import { createApp } from "@/lib/hono-factory";
2718
import { findRegisteredNameTokensForDomain } from "@/lib/name-tokens/find-name-tokens-for-domain";
2819
import { getIndexedSubregistries } from "@/lib/name-tokens/get-indexed-subregistries";
2920
import { nameTokensApiMiddleware } from "@/middleware/name-tokens.middleware";
3021

31-
const app = factory.createApp();
22+
import { getNameTokensRoute } from "./name-tokens-api.routes";
23+
24+
const app = createApp();
3225

3326
const indexedSubregistries = getIndexedSubregistries(
3427
config.namespace,
@@ -40,21 +33,6 @@ const indexedSubregistries = getIndexedSubregistries(
4033
// and if not returns the appropriate HTTP 503 (Service Unavailable) error.
4134
app.use(nameTokensApiMiddleware);
4235

43-
/**
44-
* Request Query Schema
45-
*
46-
* Name Tokens API can be requested by either `name` or `domainId`, and
47-
* can never be requested by both, or neither.
48-
*/
49-
const nameTokensQuerySchema = z
50-
.object({
51-
domainId: makeNodeSchema("request.domainId").optional().describe("Domain node hash identifier"),
52-
name: params.name.optional().describe("ENS name to look up tokens for"),
53-
})
54-
.refine((data) => (data.domainId !== undefined) !== (data.name !== undefined), {
55-
message: "Exactly one of 'domainId' or 'name' must be provided",
56-
});
57-
5836
/**
5937
* Factory function for creating a 404 Name Tokens Not Indexed error response
6038
*/
@@ -69,163 +47,109 @@ const makeNameTokensNotIndexedResponse = (
6947
},
7048
});
7149

72-
app.get(
73-
"/",
74-
describeRoute({
75-
tags: ["Explore"],
76-
summary: "Get Name Tokens",
77-
description: "Returns name tokens for the requested identifier (domainId or name)",
78-
responses: {
79-
200: {
80-
description: "Name tokens known",
81-
content: {
82-
"application/json": {
83-
schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)),
84-
},
85-
},
86-
},
87-
400: {
88-
description: "Invalid input",
89-
content: {
90-
"application/json": {
91-
schema: validationResolver(ErrorResponseSchema),
92-
},
93-
},
94-
},
95-
404: {
96-
description: "Name tokens not indexed",
97-
content: {
98-
"application/json": {
99-
schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)),
100-
},
101-
},
102-
},
103-
500: {
104-
description: "Internal server error",
105-
content: {
106-
"application/json": {
107-
schema: validationResolver(ErrorResponseSchema),
108-
},
109-
},
110-
},
111-
503: {
112-
description:
113-
"Service unavailable - Name Tokens API prerequisites not met (indexing status not ready or required plugins not activated)",
114-
content: {
115-
"application/json": {
116-
schema: validationResolver(makeNameTokensResponseSchema("Name Tokens Response", true)),
117-
},
50+
app.openapi(getNameTokensRoute, async (c) => {
51+
// Invariant: context must be set by the required middleware
52+
if (c.var.indexingStatus === undefined) {
53+
return c.json(
54+
serializeNameTokensResponse({
55+
responseCode: NameTokensResponseCodes.Error,
56+
errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported,
57+
error: {
58+
message: "Name Tokens API is not available yet",
59+
details: "Indexing status middleware is required but not initialized.",
11860
},
119-
},
120-
},
121-
}),
122-
validate("query", nameTokensQuerySchema),
123-
async (c) => {
124-
// Invariant: context must be set by the required middleware
125-
if (c.var.indexingStatus === undefined) {
126-
return c.json(
127-
serializeNameTokensResponse({
128-
responseCode: NameTokensResponseCodes.Error,
129-
errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported,
130-
error: {
131-
message: "Name Tokens API is not available yet",
132-
details: "Indexing status middleware is required but not initialized.",
133-
},
134-
}),
135-
503,
136-
);
137-
}
61+
}),
62+
503,
63+
);
64+
}
13865

139-
// Check if Indexing Status resolution failed.
140-
if (c.var.indexingStatus instanceof Error) {
141-
return c.json(
142-
serializeNameTokensResponse({
143-
responseCode: NameTokensResponseCodes.Error,
144-
errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported,
145-
error: {
146-
message: "Name Tokens API is not available yet",
147-
details:
148-
"Indexing status has not yet reached the required state to enable the Name Tokens API.",
149-
},
150-
}),
151-
503,
152-
);
153-
}
66+
// Check if Indexing Status resolution failed.
67+
if (c.var.indexingStatus instanceof Error) {
68+
return c.json(
69+
serializeNameTokensResponse({
70+
responseCode: NameTokensResponseCodes.Error,
71+
errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported,
72+
error: {
73+
message: "Name Tokens API is not available yet",
74+
details:
75+
"Indexing status has not yet reached the required state to enable the Name Tokens API.",
76+
},
77+
}),
78+
503,
79+
);
80+
}
15481

155-
const request = c.req.valid("query") satisfies NameTokensRequest;
156-
let domainId: Node;
82+
const request = c.req.valid("query") satisfies NameTokensRequest;
83+
let domainId: Node;
15784

158-
if (request.name !== undefined) {
159-
const { name } = request;
85+
if (request.name !== undefined) {
86+
const { name } = request;
16087

161-
// return 404 when the requested name was the ENS Root
162-
if (name === ENS_ROOT) {
163-
return c.json(
164-
serializeNameTokensResponse(
165-
makeNameTokensNotIndexedResponse(
166-
`The 'name' param must not be ENS Root, no tokens exist for it.`,
167-
),
88+
// return 404 when the requested name was the ENS Root
89+
if (name === ENS_ROOT) {
90+
return c.json(
91+
serializeNameTokensResponse(
92+
makeNameTokensNotIndexedResponse(
93+
`The 'name' param must not be ENS Root, no tokens exist for it.`,
16894
),
169-
404,
170-
);
171-
}
172-
173-
const parentNode = namehash(getParentNameFQDN(name));
174-
const subregistry = indexedSubregistries.find(
175-
(subregistry) => subregistry.node === parentNode,
95+
),
96+
404,
17697
);
177-
178-
// Return 404 response with error code for Name Tokens Not Indexed when
179-
// the parent name of the requested name does not match any of the
180-
// actively indexed subregistries.
181-
if (!subregistry) {
182-
return c.json(
183-
serializeNameTokensResponse(
184-
makeNameTokensNotIndexedResponse(
185-
`This ENSNode instance has not been configured to index tokens for the requested name: '${name}`,
186-
),
187-
),
188-
404,
189-
);
190-
}
191-
192-
domainId = namehash(name);
193-
} else if (request.domainId !== undefined) {
194-
domainId = request.domainId;
195-
} else {
196-
// This should never happen due to Zod validation, but TypeScript needs this
197-
throw new Error("Invariant(name-tokens-api): Either name or domainId must be provided");
19898
}
19999

200-
const { omnichainSnapshot } = c.var.indexingStatus.snapshot;
201-
const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor;
202-
203-
const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf);
100+
const parentNode = namehash(getParentNameFQDN(name));
101+
const subregistry = indexedSubregistries.find((subregistry) => subregistry.node === parentNode);
204102

205103
// Return 404 response with error code for Name Tokens Not Indexed when
206-
// no name tokens were found for the domain ID associated with
207-
// the requested name.
208-
if (!registeredNameTokens) {
209-
const errorMessageSubject =
210-
request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`;
211-
104+
// the parent name of the requested name does not match any of the
105+
// actively indexed subregistries.
106+
if (!subregistry) {
212107
return c.json(
213108
serializeNameTokensResponse(
214109
makeNameTokensNotIndexedResponse(
215-
`No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`,
110+
`This ENSNode instance has not been configured to index tokens for the requested name: '${name}`,
216111
),
217112
),
218113
404,
219114
);
220115
}
221116

117+
domainId = namehash(name);
118+
} else if (request.domainId !== undefined) {
119+
domainId = request.domainId;
120+
} else {
121+
// This should never happen due to Zod validation, but TypeScript needs this
122+
throw new Error("Invariant(name-tokens-api): Either name or domainId must be provided");
123+
}
124+
125+
const { omnichainSnapshot } = c.var.indexingStatus.snapshot;
126+
const accurateAsOf = omnichainSnapshot.omnichainIndexingCursor;
127+
128+
const registeredNameTokens = await findRegisteredNameTokensForDomain(domainId, accurateAsOf);
129+
130+
// Return 404 response with error code for Name Tokens Not Indexed when
131+
// no name tokens were found for the domain ID associated with
132+
// the requested name.
133+
if (!registeredNameTokens) {
134+
const errorMessageSubject =
135+
request.name !== undefined ? `name: '${request.name}'` : `domain ID: '${request.domainId}'`;
136+
222137
return c.json(
223-
serializeNameTokensResponse({
224-
responseCode: NameTokensResponseCodes.Ok,
225-
registeredNameTokens,
226-
}),
138+
serializeNameTokensResponse(
139+
makeNameTokensNotIndexedResponse(
140+
`No Name Tokens were indexed by this ENSNode instance for the requested ${errorMessageSubject}.`,
141+
),
142+
),
143+
404,
227144
);
228-
},
229-
);
145+
}
146+
147+
return c.json(
148+
serializeNameTokensResponse({
149+
responseCode: NameTokensResponseCodes.Ok,
150+
registeredNameTokens,
151+
}),
152+
);
153+
});
230154

231155
export default app;

apps/ensapi/src/stub-routes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { OpenAPIHono } from "@hono/zod-openapi";
22

33
import * as amIRealtimeRoutes from "./handlers/amirealtime-api.routes";
44
import * as ensnodeRoutes from "./handlers/ensnode-api.routes";
5+
import * as nameTokensRoutes from "./handlers/name-tokens-api.routes";
56
import * as resolutionRoutes from "./handlers/resolution-api.routes";
67

78
/**
@@ -12,7 +13,7 @@ import * as resolutionRoutes from "./handlers/resolution-api.routes";
1213
export function createStubRoutesForSpec() {
1314
const app = new OpenAPIHono();
1415

15-
const routeGroups = [amIRealtimeRoutes, ensnodeRoutes, resolutionRoutes];
16+
const routeGroups = [amIRealtimeRoutes, ensnodeRoutes, nameTokensRoutes, resolutionRoutes];
1617

1718
for (const group of routeGroups) {
1819
for (const route of group.routes) {

0 commit comments

Comments
 (0)