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
74 changes: 74 additions & 0 deletions apps/ensapi/src/handlers/ensanalytics-api.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createRoute } from "@hono/zod-openapi";
import { REFERRERS_PER_LEADERBOARD_PAGE_MAX } from "@namehash/ens-referrals";
import { z } from "zod/v4";

import { makeLowercaseAddressSchema } from "@ensnode/ensnode-sdk/internal";

export const basePath = "/ensanalytics";

// Pagination query parameters schema (mirrors ReferrerLeaderboardPageRequest)
const paginationQuerySchema = z.object({
page: z
.optional(z.coerce.number().int().min(1, "Page must be a positive integer"))
.describe("Page number for pagination"),
recordsPerPage: z
.optional(
z.coerce
.number()
.int()
.min(1, "Records per page must be at least 1")
.max(
REFERRERS_PER_LEADERBOARD_PAGE_MAX,
`Records per page must not exceed ${REFERRERS_PER_LEADERBOARD_PAGE_MAX}`,
),
)
.describe("Number of referrers per page"),
});

// Referrer address parameter schema
const referrerAddressSchema = z.object({
referrer: makeLowercaseAddressSchema("Referrer address").describe("Referrer Ethereum address"),
});

export const getReferrerLeaderboardRoute = createRoute({
method: "get",
path: "/referrers",
tags: ["ENSAwards"],
summary: "Get Referrer Leaderboard",
description: "Returns a paginated page from the referrer leaderboard",
request: {
query: paginationQuerySchema,
},
responses: {
200: {
description: "Successfully retrieved referrer leaderboard page",
},
500: {
description: "Internal server error",
},
},
Comment on lines +42 to +49
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Document 400 validation responses in both route descriptors.

Line 40 and Line 59 define validated inputs, but the responses blocks don’t include 400. This makes generated OpenAPI incomplete for client/error handling.

🛠️ Proposed update
 export const getReferrerLeaderboardRoute = createRoute({
@@
   responses: {
       description: "Successfully retrieved referrer leaderboard page",
     },
+    400: {
+      description: "Invalid query parameters",
+    },
       description: "Internal server error",
     },
   },
 });
@@
 export const getReferrerDetailRoute = createRoute({
@@
   responses: {
       description: "Successfully retrieved referrer detail",
     },
+    400: {
+      description: "Invalid referrer address parameter",
+    },
       description: "Internal server error",
     },
       description: "Service unavailable - referrer leaderboard data not yet cached",

As per coding guidelines: "Map errors to response codes: validation errors (ZodError/Standard Schema) → 400 with { message, details }."

Also applies to: 61-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/handlers/ensanalytics-api.routes.ts` around lines 42 - 49,
The OpenAPI responses for the two route descriptors with validated inputs are
missing a 400 validation response; update the responses objects in
ensanalytics-api.routes.ts (the route descriptors that define validated inputs
around the referrer leaderboard and the other validated route) to include a 400
entry that maps validation errors to a body shape like { message, details } and
mentions ZodError/standard schema validation, so generated docs include
validation error responses for both routes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai this was already the behaviour in the current API. We should follow up with a feature request for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@notrab, that makes sense — since this PR maintains existing behavior, we can defer the 400 response documentation improvement. Would you like me to open a GitHub issue to track adding 400 validation error responses to the OpenAPI route descriptors for ensanalytics-api (and potentially other migrated routes)?


🧠 Learnings used
Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-22T20:37:05.869Z
Learning: Applies to apps/{ensapi,ensadmin,ensrainbow,fallback-ensapi}/**/*.{ts,tsx} : Use existing Hono validation middleware (Zod schemas + `validate()` from `apps/ensapi/src/lib/handlers/validate.ts`) for API request validation instead of manually calling `zod.parse`/`safeParse` in route handlers

Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-22T20:37:05.869Z
Learning: Applies to apps/{ensapi,ensadmin,ensrainbow,fallback-ensapi}/**/*.{ts,tsx} : Map errors to response codes: validation errors (ZodError/Standard Schema) → 400 with `{ message, details }`; known client errors → 4xx with `{ message }`; server errors → 500 with `{ message }`

Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-22T20:37:05.869Z
Learning: Applies to apps/{ensapi,ensadmin,ensrainbow,fallback-ensapi}/**/*.{ts,tsx} : Use response shape `{ message: string, details?: unknown }` for API errors; do not add a `code` field inconsistently (may be adopted project-wide later)

Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-22T20:37:05.869Z
Learning: Applies to apps/{ensapi,ensadmin,ensrainbow,fallback-ensapi}/**/*.{ts,tsx} : Use the shared `errorResponse` helper from `apps/ensapi/src/lib/handlers/error-response.ts` for all error responses in ENSApi and apply equivalent patterns in other Hono apps

Learnt from: notrab
Repo: namehash/ensnode PR: 1673
File: apps/ensapi/src/handlers/registrar-actions-api.ts:102-115
Timestamp: 2026-02-25T07:04:48.803Z
Learning: The registrar-actions-api endpoint (apps/ensapi/src/handlers/registrar-actions-api.ts) uses `serializeRegistrarActionsResponse` with `RegistrarActionsResponseError` and includes a `responseCode` field. This differs from the standard ENSApi `errorResponse` helper pattern to maintain backward compatibility and avoid breaking the established API contract.

Learnt from: notrab
Repo: namehash/ensnode PR: 1661
File: apps/ensapi/src/handlers/amirealtime-api.ts:41-45
Timestamp: 2026-02-22T09:10:50.106Z
Learning: In the ensnode/ensapi project, prefer consistency with existing codebase patterns for c.json() calls; explicit status codes (e.g., c.json(payload, 200)) are not required when the default behavior (200) is already consistent across the codebase.

Learnt from: notrab
Repo: namehash/ensnode PR: 1631
File: apps/ensapi/src/handlers/ensnode-api.ts:23-27
Timestamp: 2026-02-18T16:11:00.691Z
Learning: Dynamic imports inside request handlers can be acceptable if used judiciously, but they introduce asynchronous loading and potential variability in startup latency. This pattern should be reviewed on a per-file basis: prefer top-level imports for commonly used modules and reserve dynamic import for conditional loading or rare code paths. If you continue to use dynamic imports in ensnode-api.ts, ensure you profile startup and per-request latency, verify error handling for promise rejections, and document the reasoning for caching behavior.

Learnt from: Goader
Repo: namehash/ensnode PR: 1663
File: packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts:74-96
Timestamp: 2026-02-24T15:53:06.633Z
Learning: In TypeScript code reviews, prefer placing invariants on type aliases only when the invariant is context-independent or reused across multiple fields. If an invariant depends on surrounding rules or object semantics (e.g., field-specific metrics), keep the invariant as a field JSDoc instead. This guideline applies to TS files broadly (e.g., the repo's v1/award-models and similar modules).

});

export const getReferrerDetailRoute = createRoute({
method: "get",
path: "/referrers/{referrer}",
tags: ["ENSAwards"],
summary: "Get Referrer Detail",
description: "Returns detailed information for a specific referrer by address",
request: {
params: referrerAddressSchema,
},
responses: {
200: {
description: "Successfully retrieved referrer detail",
},
500: {
description: "Internal server error",
},
503: {
description: "Service unavailable - referrer leaderboard data not yet cached",
},
},
});

export const routes = [getReferrerLeaderboardRoute, getReferrerDetailRoute];
43 changes: 21 additions & 22 deletions apps/ensapi/src/handlers/ensanalytics-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { testClient } from "hono/testing";
import { describe, expect, it, vi } from "vitest"; // Or your preferred test runner
import { describe, expect, it, vi } from "vitest";

import { ENSNamespaceIds } from "@ensnode/datasources";

Expand Down Expand Up @@ -53,27 +52,31 @@ describe("/ensanalytics", () => {
const allPossibleReferrers = referrerLeaderboardPageResponseOk.data.referrers;
const allPossibleReferrersIterator = allPossibleReferrers[Symbol.iterator]();

// Arrange: create the test client from the app instance
const client = testClient(app);
const recordsPerPage = 10;

// Act: send test request to fetch 1st page
const responsePage1 = await client.referrers
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "1" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);
const httpResponsePage1 = await app.request(
`/referrers?recordsPerPage=${recordsPerPage}&page=1`,
);
const responsePage1 = deserializeReferrerLeaderboardPageResponse(
await httpResponsePage1.json(),
);

// Act: send test request to fetch 2nd page
const responsePage2 = await client.referrers
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "2" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);
const httpResponsePage2 = await app.request(
`/referrers?recordsPerPage=${recordsPerPage}&page=2`,
);
const responsePage2 = deserializeReferrerLeaderboardPageResponse(
await httpResponsePage2.json(),
);

// Act: send test request to fetch 3rd page
const responsePage3 = await client.referrers
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "3" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);
const httpResponsePage3 = await app.request(
`/referrers?recordsPerPage=${recordsPerPage}&page=3`,
);
const responsePage3 = deserializeReferrerLeaderboardPageResponse(
await httpResponsePage3.json(),
);

// Assert: 1st page results
const expectedResponsePage1 = {
Expand Down Expand Up @@ -144,15 +147,11 @@ describe("/ensanalytics", () => {
return await next();
});

// Arrange: create the test client from the app instance
const client = testClient(app);
const recordsPerPage = 10;

// Act: send test request to fetch 1st page
const response = await client.referrers
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "1" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);
const httpResponse = await app.request(`/referrers?recordsPerPage=${recordsPerPage}&page=1`);
const response = deserializeReferrerLeaderboardPageResponse(await httpResponse.json());

// Assert: empty page results
const expectedResponse = {
Expand Down
Loading
Loading