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/ready-mugs-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ensnode/ensnode-sdk": minor
"ensapi": minor
---

Extended the `registrar-actions` endpoint to support filtering by `decodedReferrer` and pagination.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function useStatefulRegistrarActions({
// We use `isRegistrarActionsApiSupported` to enable query in those cases.
const registrarActionsQuery = useRegistrarActions({
order: RegistrarActionsOrders.LatestRegistrarActions,
itemsPerPage,
recordsPerPage: itemsPerPage,
query: {
enabled: isRegistrarActionsApiSupported,
},
Expand Down
51 changes: 38 additions & 13 deletions apps/ensapi/src/handlers/registrar-actions-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import z from "zod/v4";

import {
buildPageContext,
RECORDS_PER_PAGE_DEFAULT,
RECORDS_PER_PAGE_MAX,
type RegistrarActionsFilter,
RegistrarActionsOrders,
RegistrarActionsResponseCodes,
Expand All @@ -9,7 +12,11 @@ import {
registrarActionsFilter,
serializeRegistrarActionsResponse,
} from "@ensnode/ensnode-sdk";
import { makeNodeSchema, makePositiveIntegerSchema } from "@ensnode/ensnode-sdk/internal";
import {
makeLowercaseAddressSchema,
makeNodeSchema,
makePositiveIntegerSchema,
} from "@ensnode/ensnode-sdk/internal";

import { params } from "@/lib/handlers/params.schema";
import { validate } from "@/lib/handlers/validate";
Expand All @@ -26,9 +33,6 @@ const logger = makeLogger("registrar-actions-api");
// It makes the routes available if all prerequisites are met.
app.use(registrarActionsApiMiddleware);

const RESPONSE_ITEMS_PER_PAGE_DEFAULT = 25;
const RESPONSE_ITEMS_PER_PAGE_MAX = 100;

/**
* Get Registrar Actions
*
Expand All @@ -49,8 +53,9 @@ const RESPONSE_ITEMS_PER_PAGE_MAX = 100;
*
* Responds with:
* - 400 error response for bad input, such as:
* - (if provided) `limit` search param is not
* a positive integer <= {@link RESPONSE_ITEMS_PER_PAGE_MAX}.
* - (if provided) `page` search param is not a positive integer.
* - (if provided) `recordsPerPage` search param is not
* a positive integer <= {@link RECORDS_PER_PAGE_MAX}.
* - (if provided) `orderBy` search param is not part of {@link RegistrarActionsOrders}.
* - 500 error response for cases such as:
* - Connected ENSNode has not all required plugins set to active.
Expand All @@ -74,19 +79,27 @@ app.get(
.enum(RegistrarActionsOrders)
.default(RegistrarActionsOrders.LatestRegistrarActions),

itemsPerPage: params.queryParam
page: params.queryParam
.optional()
.default(1)
.pipe(z.coerce.number())
.pipe(makePositiveIntegerSchema("page")),

recordsPerPage: params.queryParam
.optional()
.default(RESPONSE_ITEMS_PER_PAGE_DEFAULT)
.default(RECORDS_PER_PAGE_DEFAULT)
.pipe(z.coerce.number())
.pipe(makePositiveIntegerSchema().max(RESPONSE_ITEMS_PER_PAGE_MAX)),
.pipe(makePositiveIntegerSchema("recordsPerPage").max(RECORDS_PER_PAGE_MAX)),

withReferral: params.boolstring.optional().default(false),

decodedReferrer: makeLowercaseAddressSchema("decodedReferrer").optional(),
}),
),
async (c) => {
try {
const { parentNode } = c.req.valid("param");
const { orderBy, itemsPerPage, withReferral } = c.req.valid("query");
const { orderBy, page, recordsPerPage, withReferral, decodedReferrer } = c.req.valid("query");

const filters: RegistrarActionsFilter[] = [];

Expand All @@ -98,18 +111,30 @@ app.get(
filters.push(registrarActionsFilter.withReferral(true));
}

// Find the latest "logical registrar actions".
const registrarActions = await findRegistrarActions({
if (decodedReferrer) {
filters.push(registrarActionsFilter.byDecodedReferrer(decodedReferrer));
}

// Calculate offset from page and recordsPerPage
const offset = (page - 1) * recordsPerPage;

// Find the latest "logical registrar actions" with pagination
const { registrarActions, totalRecords } = await findRegistrarActions({
filters,
orderBy,
limit: itemsPerPage,
limit: recordsPerPage,
offset,
});

// Build page context
const pageContext = buildPageContext(page, recordsPerPage, totalRecords);

// respond with success response
return c.json(
serializeRegistrarActionsResponse({
responseCode: RegistrarActionsResponseCodes.Ok,
registrarActions,
pageContext,
} satisfies RegistrarActionsResponseOk),
);
} catch (error) {
Expand Down
62 changes: 58 additions & 4 deletions apps/ensapi/src/lib/registrar-actions/find-registrar-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ function buildWhereClause(filters: RegistrarActionsFilter[] | undefined): SQL[]
return filterSql;
}

case RegistrarActionsFilterTypes.ByDecodedReferrer:
// apply decoded referrer equality filter
return eq(schema.registrarActions.decodedReferrer, filter.value);

default:
// Invariant: Unknown filter type — should never occur
throw new Error(`Unknown filter type`);
Expand All @@ -76,6 +80,40 @@ interface FindRegistrarActionsOptions {
orderBy: RegistrarActionsOrder;

limit: number;

offset: number;
}

/**
* Internal function which executes a query to count total records matching the filters.
*/
export async function _countRegistrarActions(
filters: RegistrarActionsFilter[] | undefined,
): Promise<number> {
const countQuery = db
.select({
count: schema.registrarActions.id,
})
.from(schema.registrarActions)
// join Registration Lifecycles associated with Registrar Actions
.innerJoin(
schema.registrationLifecycles,
eq(schema.registrarActions.node, schema.registrationLifecycles.node),
)
// join Domains associated with Registration Lifecycles
.innerJoin(
schema.subgraph_domain,
eq(schema.registrationLifecycles.node, schema.subgraph_domain.id),
)
// join Subregistries associated with Registration Lifecycles
.innerJoin(
schema.subregistries,
eq(schema.registrationLifecycles.subregistryId, schema.subregistries.subregistryId),
)
.where(and(...buildWhereClause(filters)));

const result = await countQuery;
return result.length;
}

/**
Expand Down Expand Up @@ -111,7 +149,8 @@ export async function _findRegistrarActions(options: FindRegistrarActionsOptions
)
.where(and(...buildWhereClause(options.filters)))
.orderBy(buildOrderByClause(options.orderBy))
.limit(options.limit);
.limit(options.limit)
.offset(options.offset);

const records = await query;

Expand Down Expand Up @@ -199,16 +238,31 @@ function _mapToNamedRegistrarAction(record: MapToNamedRegistrarActionArgs): Name
};
}

/**
* Result from finding registrar actions with pagination.
*/
export interface FindRegistrarActionsResult {
registrarActions: NamedRegistrarAction[];
totalRecords: number;
}

/**
* Find Registrar Actions, including Domain info
*
* @param {SQL} options.orderBy configures which column and order apply to results.
* @param {number} options.limit configures how many items to include in results.
* @param {number} options.offset configures how many items to skip.
*/
export async function findRegistrarActions(
options: FindRegistrarActionsOptions,
): Promise<NamedRegistrarAction[]> {
const records = await _findRegistrarActions(options);
): Promise<FindRegistrarActionsResult> {
const [records, totalRecords] = await Promise.all([
_findRegistrarActions(options),
_countRegistrarActions(options.filters),
]);

return records.map((record) => _mapToNamedRegistrarAction(record));
return {
registrarActions: records.map((record) => _mapToNamedRegistrarAction(record)),
totalRecords,
};
}
22 changes: 22 additions & 0 deletions packages/ensnode-sdk/src/api/registrar-actions/filters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Address } from "viem";

import type { Node } from "../../ens";
import {
type RegistrarActionsFilter,
type RegistrarActionsFilterByDecodedReferrer,
RegistrarActionsFilterTypes,
type RegistrarActionsFilterWithEncodedReferral,
} from "./request";
Expand Down Expand Up @@ -36,7 +39,26 @@ function withReferral(withReferral: boolean | undefined): RegistrarActionsFilter
} satisfies RegistrarActionsFilterWithEncodedReferral;
}

/**
* Build a "decoded referrer" filter object for Registrar Actions query.
*/
function byDecodedReferrer(decodedReferrer: Address): RegistrarActionsFilter;
function byDecodedReferrer(decodedReferrer: undefined): undefined;
function byDecodedReferrer(
decodedReferrer: Address | undefined,
): RegistrarActionsFilter | undefined {
if (typeof decodedReferrer === "undefined") {
return undefined;
}

return {
filterType: RegistrarActionsFilterTypes.ByDecodedReferrer,
value: decodedReferrer,
} satisfies RegistrarActionsFilterByDecodedReferrer;
}

export const registrarActionsFilter = {
byParentNode,
withReferral,
byDecodedReferrer,
};
23 changes: 13 additions & 10 deletions packages/ensnode-sdk/src/api/registrar-actions/request.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { Address } from "viem";

import type { Node } from "../../ens";
import type { RequestPageParams } from "../shared/pagination";

/**
* Records Filters: Filter Types
*/
export const RegistrarActionsFilterTypes = {
BySubregistryNode: "bySubregistryNode",
WithEncodedReferral: "withEncodedReferral",
ByDecodedReferrer: "byDecodedReferrer",
} as const;

export type RegistrarActionsFilterType =
Expand All @@ -20,9 +24,15 @@ export type RegistrarActionsFilterWithEncodedReferral = {
filterType: typeof RegistrarActionsFilterTypes.WithEncodedReferral;
};

export type RegistrarActionsFilterByDecodedReferrer = {
filterType: typeof RegistrarActionsFilterTypes.ByDecodedReferrer;
value: Address;
};

export type RegistrarActionsFilter =
| RegistrarActionsFilterBySubregistryNode
| RegistrarActionsFilterWithEncodedReferral;
| RegistrarActionsFilterWithEncodedReferral
| RegistrarActionsFilterByDecodedReferrer;

/**
* Records Orders
Expand All @@ -37,7 +47,7 @@ export type RegistrarActionsOrder =
/**
* Represents a request to Registrar Actions API.
*/
export type RegistrarActionsRequest = {
export interface RegistrarActionsRequest extends RequestPageParams {
/**
* Filters to be applied while generating results.
*/
Expand All @@ -47,11 +57,4 @@ export type RegistrarActionsRequest = {
* Order applied while generating results.
*/
order?: RegistrarActionsOrder;

/**
* Limit the count of items per page to selected count of records.
*
* Guaranteed to be a positive integer (if defined).
*/
itemsPerPage?: number;
};
}
2 changes: 2 additions & 0 deletions packages/ensnode-sdk/src/api/registrar-actions/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { InterpretedName } from "../../ens";
import type { RegistrarAction } from "../../registrars";
import type { IndexingStatusResponseCodes } from "../indexing-status";
import type { ErrorResponse } from "../shared/errors";
import type { ResponsePageContext } from "../shared/pagination";

/**
* A status code for Registrar Actions API responses.
Expand Down Expand Up @@ -47,6 +48,7 @@ export interface NamedRegistrarAction {
export type RegistrarActionsResponseOk = {
responseCode: typeof RegistrarActionsResponseCodes.Ok;
registrarActions: NamedRegistrarAction[];
pageContext: ResponsePageContext;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function serializeRegistrarActionsResponse(
return {
responseCode: response.responseCode,
registrarActions: response.registrarActions.map(serializeNamedRegistrarAction),
pageContext: response.pageContext,
} satisfies SerializedRegistrarActionsResponseOk;

case RegistrarActionsResponseCodes.Error:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ describe("ENSNode API Schema", () => {
validNamedRegistrarActionEncodedLabelHash,
validNamedRegistrarActionNormalizedWithReferral,
],
pageContext: {
page: 1,
recordsPerPage: 10,
totalRecords: 2,
totalPages: 1,
hasNext: false,
hasPrev: false,
startIndex: 0,
endIndex: 1,
},
} satisfies SerializedRegistrarActionsResponseOk;

const validResponseError = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ParsePayload } from "zod/v4/core";

import { makeRegistrarActionSchema, makeReinterpretedNameSchema } from "../../internal";
import { ErrorResponseSchema } from "../shared/errors/zod-schemas";
import { makeResponsePageContextSchema } from "../shared/pagination/zod-schemas";
import { type NamedRegistrarAction, RegistrarActionsResponseCodes } from "./response";

function invariant_registrationLifecycleNodeMatchesName(ctx: ParsePayload<NamedRegistrarAction>) {
Expand Down Expand Up @@ -40,6 +41,7 @@ export const makeRegistrarActionsResponseOkSchema = (
z.strictObject({
responseCode: z.literal(RegistrarActionsResponseCodes.Ok),
registrarActions: z.array(makeNamedRegistrarActionSchema(valueLabel)),
pageContext: makeResponsePageContextSchema(`${valueLabel}.pageContext`),
});

/**
Expand Down
Loading