Skip to content

Commit f492ef8

Browse files
authored
refactor: use zod-openapi for name-tokens-api (#1684)
1 parent c6e7d5c commit f492ef8

3 files changed

Lines changed: 175 additions & 164 deletions

File tree

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

231156
export default app;

0 commit comments

Comments
 (0)