Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e80b9f4
Implement operation result pattern in ENSApi error handlers
tk-o Jan 18, 2026
56aa797
Implement operation result pattern for ENSApi /amirealtime endpoint
tk-o Jan 18, 2026
902d556
Implement operation result pattern for Registrar Actions API
tk-o Jan 18, 2026
412e722
Apply PR feedback from AI agents
tk-o Jan 19, 2026
02fbeb3
Add `buildRouteResponsesDescription` function for ensuring comprehens…
tk-o Jan 19, 2026
ac5bb11
Handle "unhandled result code" while generating HTTP response from a …
tk-o Jan 19, 2026
1cd4b10
Improve code style in Registrar Actions API
tk-o Jan 19, 2026
a649edd
Fix typos
tk-o Jan 19, 2026
d6575b5
Fix type casting
tk-o Jan 19, 2026
1246ce7
Include InvalidRequest result code in docs
tk-o Jan 19, 2026
2d86cc2
Apply PR feedback: use concrete Result OK types
tk-o Jan 20, 2026
6565097
feat(ensnode-sdk): introduce `InsufficientIndexingProgress` result code
tk-o Jan 21, 2026
159d210
feat(ensapi): update OpenAPI route descriptions
tk-o Jan 21, 2026
fe3ffcc
feat(ensadmin): update `StatefulFetchRegistrarActionsNotReady` type
tk-o Jan 21, 2026
8ea9155
fix(ensnode-sdk): workaround for zod schema imports
tk-o Jan 21, 2026
229d8d8
fix(ensnode-sdk): introduce `getSufficientIndexingProgressChainCursor…
tk-o Jan 22, 2026
35fa0b7
fix(ensnode-sdk): dependency import issue
tk-o Jan 22, 2026
50b5e18
Apply PR feedback from AI agents
tk-o Jan 22, 2026
f5d75f7
Apply PR feedback from AI agents
tk-o Jan 22, 2026
d4261da
Apply PR feedback from self-review
tk-o Jan 22, 2026
59f3700
Apply PR feedback from AI agents
tk-o Jan 22, 2026
9b46b51
refactor: split Zod schemas for OpenAPI spec
tk-o Jan 23, 2026
3c4b632
docs: update Zod schemas to include field descriptions for OpenAPI sp…
tk-o Jan 23, 2026
13d5336
Update Result type
tk-o Jan 27, 2026
ad74cb7
Update `api` module in ENSNode SDK
tk-o Jan 27, 2026
8526bfd
Introduce `IndexingStatusForSupportedApiResult` to ENSNode SDK
tk-o Jan 27, 2026
a01108d
Define OpenAPI responses in separate files
tk-o Jan 27, 2026
b64544c
Change how indexing status is fetched by ENSApi route handlers
tk-o Jan 27, 2026
aef406a
Update API docs
tk-o Jan 27, 2026
4f1302e
Align ENSAdmin with updated ENSNode SDK types
tk-o Jan 27, 2026
6395275
Fix unit tests
tk-o Jan 27, 2026
5768d6b
Update data model for `ResultInsufficientIndexingProgressData`
tk-o Jan 27, 2026
a4cb033
Introduce API Hanlder Prerequisites Validation
tk-o Jan 28, 2026
860d697
Move test objects into "mocks" file
tk-o Jan 28, 2026
42c6d24
Support multi-version Registrar Actions API
tk-o Jan 28, 2026
0cb64fd
Refactor Registrar Actions API
tk-o Jan 28, 2026
bf40907
Add relevat test coverage for function
tk-o Jan 28, 2026
3cb3cf6
Update V1 handler path prefix
tk-o Jan 28, 2026
3e58e07
Fix Zod schemas for parsing result error objects
tk-o Jan 28, 2026
1b33cad
Minor cleanup
tk-o Jan 28, 2026
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
69 changes: 41 additions & 28 deletions apps/ensapi/src/handlers/amirealtime-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
buildResultOk,
type CrossChainIndexingStatusSnapshot,
createRealtimeIndexingStatusProjection,
type UnixTimestamp,
Expand Down Expand Up @@ -69,9 +70,11 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(200);
expect(responseJson).toMatchObject({
maxWorstCaseDistance: 300,
});
expect(responseJson).toMatchObject(
buildResultOk({
maxWorstCaseDistance: 300,
}),
);
});

it("should accept valid maxWorstCaseDistance query param (set to `0`)", async () => {
Expand All @@ -84,9 +87,11 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(200);
expect(responseJson).toMatchObject({
maxWorstCaseDistance: 0,
});
expect(responseJson).toMatchObject(
buildResultOk({
maxWorstCaseDistance: 0,
}),
);
});

it("should use default maxWorstCaseDistance when unset", async () => {
Expand All @@ -99,9 +104,11 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(200);
expect(responseJson).toMatchObject({
maxWorstCaseDistance: AMIREALTIME_DEFAULT_MAX_WORST_CASE_DISTANCE,
});
expect(responseJson).toMatchObject(
buildResultOk({
maxWorstCaseDistance: AMIREALTIME_DEFAULT_MAX_WORST_CASE_DISTANCE,
}),
);
});

it("should use default maxWorstCaseDistance when not provided", async () => {
Expand All @@ -114,9 +121,11 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(200);
expect(responseJson).toMatchObject({
maxWorstCaseDistance: AMIREALTIME_DEFAULT_MAX_WORST_CASE_DISTANCE,
});
expect(responseJson).toMatchObject(
buildResultOk({
maxWorstCaseDistance: AMIREALTIME_DEFAULT_MAX_WORST_CASE_DISTANCE,
}),
);
});

it("should reject invalid maxWorstCaseDistance (negative number)", async () => {
Expand Down Expand Up @@ -161,11 +170,13 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(200);
expect(responseJson).toMatchObject({
maxWorstCaseDistance: 10,
slowestChainIndexingCursor: 1766123720,
worstCaseDistance: 9,
});
expect(responseJson).toStrictEqual(
buildResultOk({
maxWorstCaseDistance: 10,
slowestChainIndexingCursor: 1766123720,
worstCaseDistance: 9,
}),
);
});

it("should return 200 when worstCaseDistance equals maxWorstCaseDistance", async () => {
Expand All @@ -178,11 +189,13 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(200);
expect(responseJson).toMatchObject({
maxWorstCaseDistance: 10,
slowestChainIndexingCursor: 1766123719,
worstCaseDistance: 10,
});
expect(responseJson).toStrictEqual(
buildResultOk({
maxWorstCaseDistance: 10,
slowestChainIndexingCursor: 1766123719,
worstCaseDistance: 10,
}),
);
});

it("should return 503 when worstCaseDistance exceeds maxWorstCaseDistance", async () => {
Expand All @@ -195,13 +208,13 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(503);
expect(responseJson).toHaveProperty("message");
expect(responseJson.message).toMatch(
expect(responseJson).toHaveProperty("errorMessage");
expect(responseJson.errorMessage).toMatch(
/Indexing Status 'worstCaseDistance' must be below or equal to the requested 'maxWorstCaseDistance'; worstCaseDistance = 11; maxWorstCaseDistance = 10/,
);
});

it("should return 500 when indexing status has not been resolved", async () => {
it("should return 503 when indexing status has not been resolved", async () => {
// Arrange: set `indexingStatus` context var
indexingStatusMiddlewareMock.mockImplementation(async (c, next) => {
c.set("indexingStatus", new Error("Network error"));
Expand All @@ -215,9 +228,9 @@ describe("amirealtime-api", () => {

// Assert
expect(response.status).toBe(503);
expect(responseJson).toHaveProperty("message");
expect(responseJson.message).toMatch(
/Indexing Status has to be resolved successfully before 'maxWorstCaseDistance' can be applied./,
expect(responseJson).toHaveProperty("errorMessage");
expect(responseJson.errorMessage).toMatch(
/Indexing Status must be resolved successfully before 'maxWorstCaseDistance' can be applied./,
);
});
});
Expand Down
69 changes: 44 additions & 25 deletions apps/ensapi/src/handlers/amirealtime-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ import { minutesToSeconds } from "date-fns";
import { describeRoute } from "hono-openapi";
import z from "zod/v4";

import type { Duration } from "@ensnode/ensnode-sdk";
import {
type AmIRealtimeResult,
buildResultInternalServerError,
buildResultOk,
buildResultServiceUnavailable,
type Duration,
ResultCodes,
} from "@ensnode/ensnode-sdk";
import { makeDurationSchema } from "@ensnode/ensnode-sdk/internal";

import { errorResponse } from "@/lib/handlers/error-response";
import { params } from "@/lib/handlers/params.schema";
import { buildRouteResponsesDescription } from "@/lib/handlers/route-responses-description";
import { validate } from "@/lib/handlers/validate";
import { factory } from "@/lib/hono-factory";
import { resultIntoHttpResponse } from "@/lib/result/result-into-http-response";

const app = factory.createApp();

Expand All @@ -24,16 +32,22 @@ app.get(
summary: "Check indexing progress",
description:
"Checks if the indexing progress is guaranteed to be within a requested worst-case distance of realtime",
responses: {
200: {
responses: buildRouteResponsesDescription<AmIRealtimeResult>({
[ResultCodes.Ok]: {
description:
"Indexing progress is guaranteed to be within the requested distance of realtime",
},
503: {
[ResultCodes.InvalidRequest]: {
description: "Invalid request parameters",
},
[ResultCodes.InternalServerError]: {
description: "Indexing progress cannot be determined due to an internal server error",
},
[ResultCodes.ServiceUnavailable]: {
description:
"Indexing progress is not guaranteed to be within the requested distance of realtime or indexing status unavailable",
},
},
}),
}),
validate(
"query",
Expand All @@ -46,41 +60,46 @@ app.get(
}),
),
async (c) => {
// context must be set by the required middleware
// Invariant: Indexing Status must be available in application context
if (c.var.indexingStatus === undefined) {
throw new Error(`Invariant(amirealtime-api): indexingStatusMiddleware required.`);
const result = buildResultInternalServerError(
`Invariant(amirealtime-api): Indexing Status must be available in application context.`,
);

return resultIntoHttpResponse(c, result);
}

// return 503 response error with details on prerequisite being unavailable
// Invariant: Indexing Status must be resolved successfully before 'maxWorstCaseDistance' can be applied
if (c.var.indexingStatus instanceof Error) {
return errorResponse(
c,
`Invariant(amirealtime-api): Indexing Status has to be resolved successfully before 'maxWorstCaseDistance' can be applied.`,
503,
const result = buildResultServiceUnavailable(
`Invariant(amirealtime-api): Indexing Status must be resolved successfully before 'maxWorstCaseDistance' can be applied.`,
);

return resultIntoHttpResponse(c, result);
}

const { maxWorstCaseDistance } = c.req.valid("query");
const { worstCaseDistance, snapshot } = c.var.indexingStatus;
const { slowestChainIndexingCursor } = snapshot;

// return 503 response error with details on
// requested `maxWorstCaseDistance` vs. actual `worstCaseDistance`
// Case: worst-case distance exceeds requested maximum
if (worstCaseDistance > maxWorstCaseDistance) {
return errorResponse(
c,
const result = buildResultServiceUnavailable(
Copy link
Member

Choose a reason for hiding this comment

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

I note how we are now using buildResultServiceUnavailable for multiple scenarios here which isn't ideal. See for example up above in this file how there's another case where this is also used.

Suggest we introduce a distinct result code for this case.

Additionally, I think for this case, it would be nice to still return each of the following fields in the response data model so the data model is the same as the success case, it just has a distinct result code.

  • maxWorstCaseDistance
  • slowestChainIndexingCursor
  • worstCaseDistance

Copy link
Contributor Author

@tk-o tk-o Jan 22, 2026

Choose a reason for hiding this comment

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

As discussed on Slack, I created a dedicated ResultInsufficientIndexingProgressData type for capturing useful data when indexing progress is insufficient. This type is shared between "Am I Realtime?" API and Registrar Actions API.

`Indexing Status 'worstCaseDistance' must be below or equal to the requested 'maxWorstCaseDistance'; worstCaseDistance = ${worstCaseDistance}; maxWorstCaseDistance = ${maxWorstCaseDistance}`,
503,
);

return resultIntoHttpResponse(c, result);
}

// return 200 response OK with current details on `maxWorstCaseDistance`,
// `slowestChainIndexingCursor`, and `worstCaseDistance`
return c.json({
maxWorstCaseDistance,
slowestChainIndexingCursor,
worstCaseDistance,
});
// Case: worst-case distance is within requested maximum
return resultIntoHttpResponse(
c,
buildResultOk({
maxWorstCaseDistance,
slowestChainIndexingCursor,
worstCaseDistance,
}),
);
},
);

Expand Down
Loading
Loading