Skip to content

Commit 745bf8b

Browse files
committed
Implement operation result pattern for Registrar Actions API
1 parent 56aa797 commit 745bf8b

File tree

8 files changed

+165
-102
lines changed

8 files changed

+165
-102
lines changed

apps/ensapi/src/handlers/registrar-actions-api.ts

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import z from "zod/v4";
33

44
import {
55
buildPageContext,
6+
buildResultOkTimestamped,
7+
buildResultServiceUnavailable,
68
type Node,
79
RECORDS_PER_PAGE_DEFAULT,
810
RECORDS_PER_PAGE_MAX,
911
type RegistrarActionsFilter,
1012
RegistrarActionsOrders,
11-
RegistrarActionsResponseCodes,
12-
type RegistrarActionsResponseError,
13-
type RegistrarActionsResponseOk,
13+
ResultCodes,
1414
registrarActionsFilter,
15-
serializeRegistrarActionsResponse,
15+
serializeNamedRegistrarActions,
1616
} from "@ensnode/ensnode-sdk";
1717
import {
1818
makeLowercaseAddressSchema,
@@ -26,6 +26,10 @@ import { validate } from "@/lib/handlers/validate";
2626
import { factory } from "@/lib/hono-factory";
2727
import { makeLogger } from "@/lib/logger";
2828
import { findRegistrarActions } from "@/lib/registrar-actions/find-registrar-actions";
29+
import {
30+
resultCodeToHttpStatusCode,
31+
resultIntoHttpResponse,
32+
} from "@/lib/result/result-into-http-response";
2933
import { registrarActionsApiMiddleware } from "@/middleware/registrar-actions.middleware";
3034

3135
const app = factory.createApp();
@@ -161,45 +165,48 @@ app.get(
161165
summary: "Get Registrar Actions",
162166
description: "Returns all registrar actions with optional filtering and pagination",
163167
responses: {
164-
200: {
168+
[resultCodeToHttpStatusCode(ResultCodes.Ok)]: {
165169
description: "Successfully retrieved registrar actions",
166170
},
167-
400: {
171+
[resultCodeToHttpStatusCode(ResultCodes.InvalidRequest)]: {
168172
description: "Invalid query",
169173
},
170-
500: {
171-
description: "Internal server error",
174+
[resultCodeToHttpStatusCode(ResultCodes.ServiceUnavailable)]: {
175+
description: "Registrar Actions API is unavailable at the moment",
172176
},
173177
},
174178
}),
175179
validate("query", registrarActionsQuerySchema),
176180
async (c) => {
177181
try {
182+
// Middleware ensures indexingStatus is available and not an Error
183+
// This check is for TypeScript type safety
184+
if (!c.var.indexingStatus || c.var.indexingStatus instanceof Error) {
185+
throw new Error("Invariant violation: indexingStatus should be validated by middleware");
186+
}
187+
178188
const query = c.req.valid("query");
179189
const { registrarActions, pageContext } = await fetchRegistrarActions(undefined, query);
180190

181-
// respond with success response
182-
return c.json(
183-
serializeRegistrarActionsResponse({
184-
responseCode: RegistrarActionsResponseCodes.Ok,
185-
registrarActions,
191+
// Get the accurateAsOf timestamp from the slowest chain indexing cursor
192+
const accurateAsOf = c.var.indexingStatus.snapshot.slowestChainIndexingCursor;
193+
194+
const result = buildResultOkTimestamped(
195+
{
196+
registrarActions: serializeNamedRegistrarActions(registrarActions),
186197
pageContext,
187-
} satisfies RegistrarActionsResponseOk),
198+
},
199+
accurateAsOf,
188200
);
201+
202+
return resultIntoHttpResponse(c, result);
189203
} catch (error) {
190204
const errorMessage = error instanceof Error ? error.message : "Unknown error";
191205
logger.error(errorMessage);
192206

193-
// respond with 500 error response
194-
return c.json(
195-
serializeRegistrarActionsResponse({
196-
responseCode: RegistrarActionsResponseCodes.Error,
197-
error: {
198-
message: `Registrar Actions API Response is unavailable`,
199-
},
200-
} satisfies RegistrarActionsResponseError),
201-
500,
202-
);
207+
const result = buildResultServiceUnavailable("Registrar Actions API Response is unavailable");
208+
209+
return resultIntoHttpResponse(c, result);
203210
}
204211
},
205212
);
@@ -244,14 +251,14 @@ app.get(
244251
description:
245252
"Returns registrar actions filtered by parent node hash with optional additional filtering and pagination",
246253
responses: {
247-
200: {
254+
[resultCodeToHttpStatusCode(ResultCodes.Ok)]: {
248255
description: "Successfully retrieved registrar actions",
249256
},
250-
400: {
251-
description: "Invalid input",
257+
[resultCodeToHttpStatusCode(ResultCodes.InvalidRequest)]: {
258+
description: "Invalid query",
252259
},
253-
500: {
254-
description: "Internal server error",
260+
[resultCodeToHttpStatusCode(ResultCodes.ServiceUnavailable)]: {
261+
description: "Registrar Actions API is unavailable at the moment",
255262
},
256263
},
257264
}),
@@ -279,29 +286,22 @@ app.get(
279286
// Get the accurateAsOf timestamp from the slowest chain indexing cursor
280287
const accurateAsOf = c.var.indexingStatus.snapshot.slowestChainIndexingCursor;
281288

282-
// respond with success response
283-
return c.json(
284-
serializeRegistrarActionsResponse({
285-
responseCode: RegistrarActionsResponseCodes.Ok,
286-
registrarActions,
289+
const result = buildResultOkTimestamped(
290+
{
291+
registrarActions: serializeNamedRegistrarActions(registrarActions),
287292
pageContext,
288-
accurateAsOf,
289-
} satisfies RegistrarActionsResponseOk),
293+
},
294+
accurateAsOf,
290295
);
296+
297+
return resultIntoHttpResponse(c, result);
291298
} catch (error) {
292299
const errorMessage = error instanceof Error ? error.message : "Unknown error";
293300
logger.error(errorMessage);
294301

295-
// respond with 500 error response
296-
return c.json(
297-
serializeRegistrarActionsResponse({
298-
responseCode: RegistrarActionsResponseCodes.Error,
299-
error: {
300-
message: `Registrar Actions API Response is unavailable`,
301-
},
302-
} satisfies RegistrarActionsResponseError),
303-
500,
304-
);
302+
const result = buildResultServiceUnavailable("Registrar Actions API Response is unavailable");
303+
304+
return resultIntoHttpResponse(c, result);
305305
}
306306
},
307307
);

apps/ensapi/src/lib/result/result-into-http-response.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ import {
1010
ResultCodes,
1111
} from "@ensnode/ensnode-sdk";
1212

13-
import {
14-
type OpResultServer,
15-
resultCodeToHttpStatusCode,
16-
resultIntoHttpResponse,
17-
} from "./result-into-http-response";
13+
import { resultCodeToHttpStatusCode, resultIntoHttpResponse } from "./result-into-http-response";
1814

1915
describe("resultCodeToHttpStatusCode", () => {
2016
it("should return 200 for ResultCodes.Ok", () => {
@@ -55,10 +51,7 @@ describe("resultIntoHttpResponse", () => {
5551
json: vi.fn().mockReturnValue(mockResponse),
5652
} as unknown as Context;
5753

58-
const result: OpResultServer<string> = {
59-
resultCode: ResultCodes.Ok,
60-
data: "test data",
61-
};
54+
const result = buildResultOk("test data");
6255

6356
const response = resultIntoHttpResponse(mockContext, result);
6457

apps/ensapi/src/lib/result/result-into-http-response.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import type { Context } from "hono";
22
import type { ContentfulStatusCode } from "hono/utils/http-status";
33

4-
import { type AbstractResultOk, ResultCodes, type ResultServerError } from "@ensnode/ensnode-sdk";
5-
6-
export type OpResultServerOk<TData> = AbstractResultOk<TData>;
7-
8-
export type OpResultServer<TData = unknown> = OpResultServerOk<TData> | ResultServerError;
9-
10-
export type OpResultServerResultCode = OpResultServer["resultCode"];
4+
import {
5+
type OpResultServer,
6+
type OpResultServerResultCode,
7+
ResultCodes,
8+
} from "@ensnode/ensnode-sdk";
119

1210
/**
1311
* Get HTTP status code corresponding to the given operation result code.

apps/ensapi/src/middleware/registrar-actions.middleware.ts

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import config from "@/config";
22

33
import {
4-
RegistrarActionsResponseCodes,
4+
buildResultInternalServerError,
5+
buildResultServiceUnavailable,
56
registrarActionsPrerequisites,
6-
serializeRegistrarActionsResponse,
77
} from "@ensnode/ensnode-sdk";
88

99
import { factory } from "@/lib/hono-factory";
1010
import { makeLogger } from "@/lib/logger";
11+
import { resultIntoHttpResponse } from "@/lib/result/result-into-http-response";
1112

1213
const logger = makeLogger("registrar-actions.middleware");
1314

@@ -31,20 +32,20 @@ export const registrarActionsApiMiddleware = factory.createMiddleware(
3132
async function registrarActionsApiMiddleware(c, next) {
3233
// context must be set by the required middleware
3334
if (c.var.indexingStatus === undefined) {
34-
throw new Error(`Invariant(registrar-actions.middleware): indexingStatusMiddleware required`);
35+
return resultIntoHttpResponse(
36+
c,
37+
buildResultInternalServerError(
38+
`Invariant(registrar-actions.middleware): indexingStatusMiddleware required.`,
39+
),
40+
);
3541
}
3642

3743
if (!registrarActionsPrerequisites.hasEnsIndexerConfigSupport(config.ensIndexerPublicConfig)) {
38-
return c.json(
39-
serializeRegistrarActionsResponse({
40-
responseCode: RegistrarActionsResponseCodes.Error,
41-
error: {
42-
message: `Registrar Actions API is not available`,
43-
details: `Connected ENSIndexer must have all following plugins active: ${registrarActionsPrerequisites.requiredPlugins.join(", ")}`,
44-
},
45-
}),
46-
500,
47-
);
44+
const result = buildResultServiceUnavailable(`Registrar Actions API is not available`, {
45+
details: `Connected ENSIndexer must have all following plugins active: ${registrarActionsPrerequisites.requiredPlugins.join(", ")}`,
46+
});
47+
48+
return resultIntoHttpResponse(c, result);
4849
}
4950

5051
if (c.var.indexingStatus instanceof Error) {
@@ -54,31 +55,24 @@ export const registrarActionsApiMiddleware = factory.createMiddleware(
5455
`Registrar Actions API requested but indexing status is not available in context.`,
5556
);
5657

57-
return c.json(
58-
serializeRegistrarActionsResponse({
59-
responseCode: RegistrarActionsResponseCodes.Error,
60-
error: {
61-
message: `Registrar Actions API is not available`,
62-
details: `Indexing status is currently unavailable to this ENSApi instance.`,
63-
},
64-
}),
65-
500,
66-
);
58+
const result = buildResultServiceUnavailable(`Registrar Actions API is not available`, {
59+
details: `Indexing status is currently unavailable to this ENSApi instance.`,
60+
});
61+
62+
return resultIntoHttpResponse(c, result);
6763
}
6864

6965
const { omnichainSnapshot } = c.var.indexingStatus.snapshot;
7066

71-
if (!registrarActionsPrerequisites.hasIndexingStatusSupport(omnichainSnapshot.omnichainStatus))
72-
return c.json(
73-
serializeRegistrarActionsResponse({
74-
responseCode: RegistrarActionsResponseCodes.Error,
75-
error: {
76-
message: `Registrar Actions API is not available`,
77-
details: `The cached omnichain indexing status of the Connected ENSIndexer must be one of the following ${registrarActionsPrerequisites.supportedIndexingStatusIds.map((statusId) => `"${statusId}"`).join(", ")}.`,
78-
},
79-
}),
80-
500,
81-
);
67+
if (
68+
!registrarActionsPrerequisites.hasIndexingStatusSupport(omnichainSnapshot.omnichainStatus)
69+
) {
70+
const result = buildResultServiceUnavailable(`Registrar Actions API is not available`, {
71+
details: `The cached omnichain indexing status of the Connected ENSIndexer must be one of the following ${registrarActionsPrerequisites.supportedIndexingStatusIds.map((statusId) => `"${statusId}"`).join(", ")}.`,
72+
});
73+
74+
return resultIntoHttpResponse(c, result);
75+
}
8276

8377
await next();
8478
},

packages/ensnode-sdk/src/api/registrar-actions/response.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { InterpretedName } from "../../ens";
22
import type { RegistrarAction } from "../../registrars";
3-
import type { UnixTimestamp } from "../../shared";
3+
import type { OpResultServer, UnixTimestamp } from "../../shared";
44
import type { IndexingStatusResponseCodes } from "../indexing-status";
55
import type { ErrorResponse } from "../shared/errors";
66
import type { ResponsePageContext } from "../shared/pagination";
@@ -81,3 +81,16 @@ export interface RegistrarActionsResponseError {
8181
* at runtime.
8282
*/
8383
export type RegistrarActionsResponse = RegistrarActionsResponseOk | RegistrarActionsResponseError;
84+
85+
export interface RegistrarActionsResultOkData {
86+
registrarActions: NamedRegistrarAction[];
87+
paginationContext: ResponsePageContext;
88+
}
89+
90+
/**
91+
* Registrar Actions Result
92+
*
93+
* Use the `resultCode` field to determine the specific type interpretation
94+
* at runtime.
95+
*/
96+
export type RegistrarActionsResult = OpResultServer<RegistrarActionsResultOkData>;

packages/ensnode-sdk/src/api/registrar-actions/serialize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export function serializeNamedRegistrarAction({
2020
};
2121
}
2222

23+
export function serializeNamedRegistrarActions(
24+
actions: NamedRegistrarAction[],
25+
): SerializedNamedRegistrarAction[] {
26+
return actions.map(serializeNamedRegistrarAction);
27+
}
28+
2329
export function serializeRegistrarActionsResponse(
2430
response: RegistrarActionsResponse,
2531
): SerializedRegistrarActionsResponse {

packages/ensnode-sdk/src/shared/result/result-base.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { UnixTimestamp } from "../../shared";
12
import type {
23
ResultCode,
34
ResultCodeClientError,
@@ -32,6 +33,26 @@ export interface AbstractResultOk<TDataType> extends AbstractResult<typeof Resul
3233
data: TDataType;
3334
}
3435

36+
/**
37+
* Abstract representation of a successful result with data guaranteed to be
38+
* at least up to a certain timestamp.
39+
*/
40+
export interface AbstractResultOkTimestamped<TDataType> extends AbstractResultOk<TDataType> {
41+
/**
42+
* The minimum indexing cursor timestamp that the data is
43+
* guaranteed to be accurate as of.
44+
*
45+
* Guarantees:
46+
* - `data` is guaranteed to be at least up to `minIndexingCursor`, but
47+
* may be indexed with timestamps higher than `minIndexingCursor`.
48+
* - This guarantee may temporarily be violated during a chain reorg.
49+
* ENSNode automatically recovers from chain reorgs, but during one
50+
* the `minIndexingCursor` may theoretically be some seconds ahead of
51+
* the true state of indexed data.
52+
*/
53+
minIndexingCursor: UnixTimestamp;
54+
}
55+
3556
/**
3657
* Abstract representation of an error result.
3758
*/

0 commit comments

Comments
 (0)