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

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

export const basePath = "/v1/ensanalytics";

/**
* Query parameters schema for referrer leaderboard page requests.
* Validates edition slug, page number, and records per page.
*/
const referrerLeaderboardPageQuerySchema = z.object({
edition: makeReferralProgramEditionSlugSchema("edition"),
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"),
});

// Editions query parameter schema
const editionsQuerySchema = z.object({
editions: z
.string()
.describe("Comma-separated list of edition slugs")
.transform((value) => value.split(",").map((s) => s.trim()))
.pipe(makeReferrerMetricsEditionsArraySchema("editions")),
});

export const getReferralLeaderboardRoute = createRoute({
method: "get",
path: "/referral-leaderboard",
tags: ["ENSAwards"],
summary: "Get Referrer Leaderboard (v1)",
description: "Returns a paginated page from the referrer leaderboard for a specific edition",
request: {
query: referrerLeaderboardPageQuerySchema,
},
responses: {
200: {
description: "Successfully retrieved referrer leaderboard page",
},
404: {
description: "Unknown edition slug",
},
500: {
description: "Internal server error",
},
503: {
description: "Service unavailable",
},
},
});

export const getReferrerDetailRoute = createRoute({
method: "get",
path: "/referrer/{referrer}",
tags: ["ENSAwards"],
summary: "Get Referrer Detail for Editions (v1)",
description: `Returns detailed information for a specific referrer for the requested editions. Requires 1-${MAX_EDITIONS_PER_REQUEST} distinct edition slugs. All requested editions must be recognized and have cached data, or the request fails.`,
request: {
params: referrerAddressSchema,
query: editionsQuerySchema,
},
responses: {
200: {
description: "Successfully retrieved referrer detail for requested editions",
},
400: {
description: "Invalid request",
},
404: {
description: "Unknown edition slug",
},
500: {
description: "Internal server error",
},
503: {
description: "Service unavailable",
},
},
});

export const getEditionsRoute = createRoute({
method: "get",
path: "/editions",
tags: ["ENSAwards"],
summary: "Get Edition Config Set (v1)",
description:
"Returns the currently configured referral program edition config set. Editions are sorted in descending order by start timestamp (most recent first).",
responses: {
200: {
description: "Successfully retrieved edition config set",
},
500: {
description: "Internal server error",
},
503: {
description: "Service unavailable",
},
},
});

export const routes = [getReferralLeaderboardRoute, getReferrerDetailRoute, getEditionsRoute];
52 changes: 25 additions & 27 deletions apps/ensapi/src/handlers/ensanalytics-api-v1.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 @@ -91,28 +90,32 @@ describe("/v1/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;
const edition = "2025-12";

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

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

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

// Assert: 1st page results
const expectedResponsePage1 = {
Expand Down Expand Up @@ -211,16 +214,14 @@ describe("/v1/ensanalytics", () => {
return await next();
});

// Arrange: create the test client from the app instance
const client = testClient(app);
const recordsPerPage = 10;
const edition = "2025-12";

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

// Assert: empty page results
const expectedResponse = {
Expand Down Expand Up @@ -282,15 +283,12 @@ describe("/v1/ensanalytics", () => {
return await next();
});

// Arrange: create the test client from the app instance
const client = testClient(app);
const recordsPerPage = 10;
const invalidEdition = "invalid-edition";

// Act: send test request with invalid edition slug
const httpResponse = await client["referral-leaderboard"].$get(
{ query: { edition: invalidEdition, recordsPerPage: `${recordsPerPage}`, page: "1" } },
{},
const httpResponse = await app.request(
`/referral-leaderboard?edition=${invalidEdition}&recordsPerPage=${recordsPerPage}&page=1`,
);
const responseData = await httpResponse.json();
const response = deserializeReferrerLeaderboardPageResponse(responseData);
Expand Down
Loading
Loading