Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions apps/ensapi/src/handlers/name-tokens-api.routes.ts
Original file line number Diff line number Diff line change
@@ -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<typeof nameTokensQuerySchema>;

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];
251 changes: 88 additions & 163 deletions apps/ensapi/src/handlers/name-tokens-api.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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
*/
Expand All @@ -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;
Loading
Loading