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
6 changes: 6 additions & 0 deletions .changeset/shy-rabbits-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ensnode/ensnode-sdk": minor
"ensapi": minor
---

Made `accurateAsOf` a required field in the Registrar Actions API response (`RegistrarActionsResponseOk`).
20 changes: 17 additions & 3 deletions apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ async function fetchRegistrarActions(parentNode: Node | undefined, query: Regist
*/
app.openapi(getRegistrarActionsRoute, async (c) => {
try {
// Defensive: `registrarActionsApiMiddleware` already short-circuits with a
// serialized 503 when indexingStatus is an Error, so this branch is
// unreachable at runtime — kept only for TypeScript type narrowing.
if (c.var.indexingStatus instanceof Error) {
Copy link
Copy Markdown
Contributor

@vercel vercel bot Apr 9, 2026

Choose a reason for hiding this comment

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

Handler throws a generic Error when indexingStatus is unavailable, causing a 500 response inside try-catch instead of returning correct 503 response

Fix on Vercel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The registrar-actions handlers' if (c.var.indexingStatus instanceof Error) throw … branches are unreachable defensive code for TypeScript narrowing - registrarActionsApiMiddleware already short-circuits that case with a serialized 503 before the handler runs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Goader Thanks. Agreed with your conclusion here but it will be good to add a comment in the code here how this case is unreachable based on the registrarActionsApiMiddleware (which would return a HTTP 503 error) but we add this logic here just as a formality for type narrowing.

throw new Error("Indexing status has not been loaded yet");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggest we move the operation that builds accurateAsOf here, before the call to fetchRegistrarActions. We want to error on the side of accurateAsOf being older, rather than newer than the data being returned.

// Get the accurateAsOf timestamp from the omnichain indexing cursor
const accurateAsOf = c.var.indexingStatus.snapshot.omnichainSnapshot.omnichainIndexingCursor;

const query = c.req.valid("query");
const { registrarActions, pageContext } = await fetchRegistrarActions(undefined, query);

Expand All @@ -110,6 +120,7 @@ app.openapi(getRegistrarActionsRoute, async (c) => {
responseCode: RegistrarActionsResponseCodes.Ok,
registrarActions,
pageContext,
accurateAsOf,
} satisfies RegistrarActionsResponseOk),
);
} catch (error) {
Expand Down Expand Up @@ -163,17 +174,20 @@ app.openapi(getRegistrarActionsRoute, async (c) => {
*/
app.openapi(getRegistrarActionsByParentNodeRoute, async (c) => {
try {
// Defensive: `registrarActionsApiMiddleware` already short-circuits with a
// serialized 503 when indexingStatus is an Error, so this branch is
// unreachable at runtime — kept only for TypeScript type narrowing.
if (c.var.indexingStatus instanceof Error) {
throw new Error("Indexing status has not been loaded yet");
}

// Get the accurateAsOf timestamp from the omnichain indexing cursor
const accurateAsOf = c.var.indexingStatus.snapshot.omnichainSnapshot.omnichainIndexingCursor;

const { parentNode } = c.req.valid("param");
const query = c.req.valid("query");
const { registrarActions, pageContext } = await fetchRegistrarActions(parentNode, query);

// Get the accurateAsOf timestamp from the slowest chain indexing cursor
const accurateAsOf = c.var.indexingStatus.snapshot.slowestChainIndexingCursor;

// respond with success response
return c.json(
serializeRegistrarActionsResponse({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,11 @@ export type RegistrarActionsResponseOk = {
* The {@link UnixTimestamp} of when the data used to build the list of {@link NamedRegistrarAction} was accurate as of.
*
* @remarks
* **Note:** This value represents the `slowestChainIndexingCursor` from the latest omnichain indexing status
* **Note:** This value represents the `omnichainIndexingCursor` from the latest omnichain indexing status
* snapshot captured by ENSApi. The state returned in the response is guaranteed to be accurate as of this
* timestamp but may be from a timestamp higher than this value.
*
* **Temporary:** This field is currently optional to maintain backward compatibility with ENS Awards
* using older snapshot NPM packages. This will be changed to required in a future release.
* See: https://github.com/namehash/ensnode/issues/1497
*/
accurateAsOf?: UnixTimestamp;
accurateAsOf: UnixTimestamp;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ describe("ENSNode API Schema", () => {
expect(() => makeRegistrarActionsResponseSchema().parse(validResponseOk)).not.toThrowError();
});

it("rejects ResponseOk object missing required accurateAsOf", () => {
const { accurateAsOf: _accurateAsOf, ...invalidResponseOk } = validResponseOk;

expect(() => makeRegistrarActionsResponseSchema().parse(invalidResponseOk)).toThrowError();
});

it("can parse valid ResponseError object", () => {
const parsed = makeRegistrarActionsResponseSchema().parse(validResponseError);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const makeRegistrarActionsResponseOkSchema = (
responseCode: z.literal(RegistrarActionsResponseCodes.Ok),
registrarActions: z.array(makeNamedRegistrarActionSchema(valueLabel)),
pageContext: makeResponsePageContextSchema(`${valueLabel}.pageContext`),
accurateAsOf: makeUnixTimestampSchema(`${valueLabel}.accurateAsOf`).optional(),
accurateAsOf: makeUnixTimestampSchema(`${valueLabel}.accurateAsOf`),
});

/**
Expand Down
Loading