diff --git a/.changeset/recently-registered-subdomains-index.md b/.changeset/recently-registered-subdomains-index.md
new file mode 100644
index 0000000000..7a8644f18b
--- /dev/null
+++ b/.changeset/recently-registered-subdomains-index.md
@@ -0,0 +1,6 @@
+---
+"@ensnode/ensdb-sdk": patch
+"ensindexer": patch
+---
+
+Index-accelerate `REGISTRATION_TIMESTAMP` / `REGISTRATION_EXPIRY`-ordered domain queries (e.g. `Domain.subdomains(order: { by: REGISTRATION_TIMESTAMP, dir: DESC })`). Previously these joined `domains → latest_registration_indexes → registrations` and sorted the full registry partition — ~55s for `.eth`'s subdomains. The latest registration's start/expiry is now mirrored onto the Domain row (`__latestRegistrationStart` / `__latestRegistrationExpiry`) with composite indexes `(registry_id,
, id)`, turning the query into an index-ordered scan. The sort columns are NOT NULL — an absent value (no registration, or a never-expiring registration) is materialized as a `+∞` sentinel — so a single plain composite per column serves both directions with a plain keyset tuple, and the sentinel sorts last for ASC and first for DESC.
diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts
index 38620c9cbf..758bdfff8d 100644
--- a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts
+++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts
@@ -20,12 +20,24 @@ function getOrderColumn(orderBy: typeof DomainsOrderBy.$inferType): SQL {
case "DEPTH":
return sql`${ensIndexerSchema.domain.canonicalDepth}`;
case "REGISTRATION_TIMESTAMP":
- return sql`${ensIndexerSchema.registration.start}`;
+ return sql`${ensIndexerSchema.domain.__latestRegistrationStart}`;
case "REGISTRATION_EXPIRY":
- return sql`${ensIndexerSchema.registration.expiry}`;
+ return sql`${ensIndexerSchema.domain.__latestRegistrationExpiry}`;
}
}
+/**
+ * Whether the ORDER BY for this column needs an explicit NULLS LAST clause.
+ *
+ * The registration sort columns (`Domain.__latestRegistration*`) materialize an infinity sentinel
+ * (see `REGISTRATION_SORT_SENTINEL`) in place of an absent value, so they're NOT NULL — there are no
+ * NULLs to sort last, and a plain `(registry_id, , id)` composite serves both directions.
+ * NAME / DEPTH columns are nullable and keep NULLS LAST.
+ */
+export function shouldUseNullsLast(orderBy: typeof DomainsOrderBy.$inferType): boolean {
+ return orderBy === "NAME" || orderBy === "DEPTH";
+}
+
/**
* Build a cursor filter for keyset pagination on findDomains results.
*
@@ -67,7 +79,9 @@ export function cursorFilter(
const idCmp = sql`${ensIndexerSchema.domain.id} ${op} ${cursor.id}`;
// NULL cursor values need explicit handling because Postgres tuple comparison with NULL yields
- // NULL/unknown. With NULLS LAST ordering, non-NULL values come before NULL values.
+ // NULL/unknown. Reached only for NAME/DEPTH (whose columns are nullable, NULLS LAST); registration
+ // sort columns are sentinel-backed NOT NULL, so their cursor value is never null. With NULLS LAST,
+ // non-NULL values come before NULL values.
if (cursor.value === null) {
return direction === "after"
? sql`(${orderColumn} IS NULL AND ${idCmp})`
@@ -84,6 +98,7 @@ export function cursorFilter(
return sql`${cursor.value}::int`;
case "REGISTRATION_TIMESTAMP":
case "REGISTRATION_EXPIRY":
+ // ponder bigints are numeric(78,0)
return sql`${cursor.value}::numeric(78,0)`;
}
})();
@@ -110,11 +125,13 @@ export function orderFindDomains(
const effectiveDesc = isEffectiveDesc(orderDir, inverted);
const orderColumn = getOrderColumn(orderBy);
- // Always use NULLS LAST so unregistered domains (NULL registration fields)
- // appear at the end regardless of sort direction
- const primaryOrder = effectiveDesc
- ? sql`${orderColumn} DESC NULLS LAST`
- : sql`${orderColumn} ASC NULLS LAST`;
+ const primaryOrder = shouldUseNullsLast(orderBy)
+ ? effectiveDesc
+ ? sql`${orderColumn} DESC NULLS LAST`
+ : sql`${orderColumn} ASC NULLS LAST`
+ : effectiveDesc
+ ? sql`${orderColumn} DESC`
+ : sql`${orderColumn} ASC`;
const { ensIndexerSchema } = di.context;
// Always include id as tiebreaker for stable ordering
diff --git a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts
index 31bf50abf6..aebed94ffe 100644
--- a/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts
+++ b/apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts
@@ -5,8 +5,6 @@ import type { NormalizedAddress, RegistryId } from "enssdk";
import di from "@/di";
import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span";
-import { makeLogger } from "@/lib/logger";
-import type { Context } from "@/omnigraph-api/context";
import { DomainCursors } from "@/omnigraph-api/lib/find-domains/domain-cursor";
import {
cursorFilter,
@@ -14,12 +12,11 @@ import {
} from "@/omnigraph-api/lib/find-domains/find-domains-resolver-helpers";
import type { DomainOrderValue } from "@/omnigraph-api/lib/find-domains/types";
import { lazyConnection } from "@/omnigraph-api/lib/lazy-connection";
-import { rejectAnyErrors } from "@/omnigraph-api/lib/reject-any-errors";
import {
PAGINATION_DEFAULT_MAX_SIZE,
PAGINATION_DEFAULT_PAGE_SIZE,
} from "@/omnigraph-api/schema/constants";
-import { type Domain, DomainInterfaceRef } from "@/omnigraph-api/schema/domain";
+import type { Domain } from "@/omnigraph-api/schema/domain";
import type {
DomainsNameFilter,
DomainsOrderInput,
@@ -30,7 +27,6 @@ import type { ENSProtocolVersion } from "@/omnigraph-api/schema/ens-protocol-ver
type DomainWithOrderValue = Domain & { __orderValue: DomainOrderValue };
const tracer = trace.getTracer("find-domains");
-const logger = makeLogger("find-domains");
const DOMAINS_DEFAULT_ORDER = { by: "NAME", dir: "ASC" } satisfies DomainsOrderValue;
@@ -92,36 +88,27 @@ function getDefaultOrder(where: DomainsWhere | undefined | null): DomainsOrderVa
}
/**
- * GraphQL API resolver for domain connection queries. Builds a single flat SELECT over
- * `domains` with conditional joins (parent registry / registration) driven by the supplied
- * `where` filters and ordering. Handles cursor-based pagination, ordering, and dataloader
- * loading. Used by `Query.domains`, `Account.domains`, `Registry.domains`, and `Domain.subdomains`.
+ * GraphQL API resolver for domain connection queries. Handles cursor-based pagination and ordering.
+ * Used by `Query.domains`, `Account.domains`, `Registry.domains`, and `Domain.subdomains`.
*
- * @param context - The GraphQL Context, required for Dataloader access
* @param args - Compound `where` filter, optional ordering, and relay connection args
*/
-export function resolveFindDomains(
- context: Context,
- {
- where,
- order,
- ...connectionArgs
- }: {
- where?: DomainsWhere | null;
- order?: typeof DomainsOrderInput.$inferInput | null;
- first?: number | null;
- last?: number | null;
- before?: string | null;
- after?: string | null;
- },
-) {
+export function resolveFindDomains({
+ where,
+ order,
+ ...connectionArgs
+}: {
+ where?: DomainsWhere | null;
+ order?: typeof DomainsOrderInput.$inferInput | null;
+ first?: number | null;
+ last?: number | null;
+ before?: string | null;
+ after?: string | null;
+}) {
const defaultOrder = getDefaultOrder(where);
const orderBy = order?.by ?? defaultOrder.by;
const orderDir = order?.dir ?? defaultOrder.dir;
- const needsRegistrationJoin =
- orderBy === "REGISTRATION_TIMESTAMP" || orderBy === "REGISTRATION_EXPIRY";
-
const { ensIndexerSchema } = di.context;
const filterConditions = and(
@@ -172,85 +159,26 @@ export function resolveFindDomains(
const beforeCursor = before ? DomainCursors.decode(before) : undefined;
const afterCursor = after ? DomainCursors.decode(after) : undefined;
- // SELECT only `id` plus the active order column when it requires a JOIN. NAME/DEPTH
- // order values are read back from the dataloader-hydrated Domain — for those orderings
- // the keyset query stays narrow enough for an index-only scan against the composite
- // indexes on `domains`.
- const registrationValueColumn = (() => {
- switch (orderBy) {
- case "REGISTRATION_TIMESTAMP":
- return ensIndexerSchema.registration.start;
- case "REGISTRATION_EXPIRY":
- return ensIndexerSchema.registration.expiry;
- default:
- return sql`NULL`.as("registration_value");
- }
- })();
-
const { ensDb } = di.context;
- let query = ensDb
- .select({
- id: ensIndexerSchema.domain.id,
- registrationValue: registrationValueColumn,
- })
- .from(ensIndexerSchema.domain)
- .$dynamic();
-
- if (needsRegistrationJoin) {
- query = query
- .leftJoin(
- ensIndexerSchema.latestRegistrationIndex,
- eq(ensIndexerSchema.latestRegistrationIndex.domainId, ensIndexerSchema.domain.id),
- )
- .leftJoin(
- ensIndexerSchema.registration,
- and(
- eq(ensIndexerSchema.registration.domainId, ensIndexerSchema.domain.id),
- eq(
- ensIndexerSchema.registration.registrationIndex,
- ensIndexerSchema.latestRegistrationIndex.registrationIndex,
- ),
- ),
- );
- }
-
- const finalQuery = query
- .where(
- and(
- filterConditions,
- beforeCursor ? cursorFilter(beforeCursor, orderBy, orderDir, "before") : undefined,
- afterCursor ? cursorFilter(afterCursor, orderBy, orderDir, "after") : undefined,
- ),
- )
- .orderBy(...orderClauses)
- .limit(limit);
-
- logger.debug({ sql: finalQuery.toSQL() });
+ const query = ensDb.query.domain.findMany({
+ where: and(
+ filterConditions,
+ beforeCursor ? cursorFilter(beforeCursor, orderBy, orderDir, "before") : undefined,
+ afterCursor ? cursorFilter(afterCursor, orderBy, orderDir, "after") : undefined,
+ ),
+ orderBy: orderClauses,
+ limit,
+ with: { label: true },
+ });
- const results = await withActiveSpanAsync(
+ const domains = await withActiveSpanAsync(
tracer,
"find-domains.connection",
{ orderBy, orderDir, limit },
- () => finalQuery.execute(),
+ () => query.execute(),
);
- const loadedDomains = await withActiveSpanAsync(
- tracer,
- "find-domains.dataloader",
- { count: results.length },
- () =>
- rejectAnyErrors(
- DomainInterfaceRef.getDataloader(context).loadMany(
- results.map((result) => result.id),
- ),
- ),
- );
-
- const registrationValueById = needsRegistrationJoin
- ? new Map(results.map((r) => [r.id, r.registrationValue ?? null]))
- : null;
-
- return loadedDomains.map((domain): DomainWithOrderValue => {
+ return domains.map((domain): DomainWithOrderValue => {
const __orderValue: DomainOrderValue = (() => {
switch (orderBy) {
case "NAME":
@@ -258,16 +186,9 @@ export function resolveFindDomains(
case "DEPTH":
return domain.canonicalDepth;
case "REGISTRATION_TIMESTAMP":
+ return domain.__latestRegistrationStart;
case "REGISTRATION_EXPIRY":
- // `registrationValueById` is populated iff `needsRegistrationJoin` is true,
- // which is exactly the REGISTRATION_* arms here. `loadedDomains` is keyed by
- // the same ids as `results`, so the lookup is guaranteed to hit.
- if (registrationValueById === null) {
- throw new Error(
- `Invariant: registrationValueById should be populated when orderBy=${orderBy}`,
- );
- }
- return registrationValueById.get(domain.id) ?? null;
+ return domain.__latestRegistrationExpiry;
}
})();
return { ...domain, __orderValue };
diff --git a/apps/ensapi/src/omnigraph-api/schema/account.ts b/apps/ensapi/src/omnigraph-api/schema/account.ts
index 783d832b3f..e6020042bd 100644
--- a/apps/ensapi/src/omnigraph-api/schema/account.ts
+++ b/apps/ensapi/src/omnigraph-api/schema/account.ts
@@ -17,7 +17,11 @@ import {
RESOLVE_ACCELERATE_ARG,
} from "@/omnigraph-api/schema/constants";
import { DomainInterfaceRef } from "@/omnigraph-api/schema/domain";
-import { AccountDomainsWhereInput, DomainsOrderInput } from "@/omnigraph-api/schema/domain-inputs";
+import {
+ AccountDomainsWhereInput,
+ DOMAINS_ORDERING_DESCRIPTION,
+ DomainsOrderInput,
+} from "@/omnigraph-api/schema/domain-inputs";
import { EventRef } from "@/omnigraph-api/schema/event";
import { AccountEventsWhereInput } from "@/omnigraph-api/schema/event-inputs";
import { PermissionsUserRef } from "@/omnigraph-api/schema/permissions";
@@ -115,14 +119,14 @@ AccountRef.implement({
// Account.domains
////////////////////
domains: t.connection({
- description: "The Domains that are owned by the Account.",
+ description: `The Domains that are owned by the Account. ${DOMAINS_ORDERING_DESCRIPTION}`,
type: DomainInterfaceRef,
args: {
where: t.arg({ type: AccountDomainsWhereInput }),
order: t.arg({ type: DomainsOrderInput }),
},
- resolve: (parent, { where, order, ...connectionArgs }, context) =>
- resolveFindDomains(context, {
+ resolve: (parent, { where, order, ...connectionArgs }) =>
+ resolveFindDomains({
where: { ...where, ownerId: parent.id },
order,
...connectionArgs,
diff --git a/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts b/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts
index 885b6b7cf7..0d2ae274cb 100644
--- a/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts
+++ b/apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts
@@ -146,12 +146,36 @@ export const SubdomainsWhereInput = builder.inputType("SubdomainsWhereInput", {
//////////////////////
export const DomainsOrderBy = builder.enumType("DomainsOrderBy", {
- description: "Fields by which domains can be ordered",
- values: ["NAME", "DEPTH", "REGISTRATION_TIMESTAMP", "REGISTRATION_EXPIRY"] as const,
+ description: "Fields by which domains can be ordered.",
+ values: {
+ NAME: { description: "Order by the Domain's Canonical Name, alphabetically." },
+ DEPTH: {
+ description: "Order by Canonical Name depth (number of labels); e.g. `eth` < `vitalik.eth`.",
+ },
+ REGISTRATION_TIMESTAMP: {
+ description:
+ "Order by the start time of the Domain's latest Registration. A Domain with no Registration has no timestamp and sorts last when `dir: ASC` (“oldest registered first”) and first when `dir: DESC` (“most recently registered first”).",
+ },
+ REGISTRATION_EXPIRY: {
+ description:
+ "Order by the expiry of the Domain's latest Registration. A Domain that never expires (or has no Registration) is treated as +∞: it sorts last when `dir: ASC` (“expiring soonest first”) and first when `dir: DESC` (“expiring latest first”).",
+ },
+ },
});
export type DomainsOrderByValue = typeof DomainsOrderBy.$inferType;
+/**
+ * Shared description fragment documenting ordering / NULL-placement behavior, appended to every
+ * find-domains-based connection field (Query.domains, Account.domains, Registry.domains,
+ * Domain.subdomains). Kept in one place so the semantics are documented exactly once.
+ */
+export const DOMAINS_ORDERING_DESCRIPTION =
+ "Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or " +
+ "REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no " +
+ "Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when " +
+ "`dir: ASC` and first when `dir: DESC`.";
+
export const DomainsOrderInput = builder.inputType("DomainsOrderInput", {
description: "Ordering options for domains query. If no order is provided, the default is ASC.",
fields: (t) => ({
diff --git a/apps/ensapi/src/omnigraph-api/schema/domain.ts b/apps/ensapi/src/omnigraph-api/schema/domain.ts
index 8c56dec249..beec7d262b 100644
--- a/apps/ensapi/src/omnigraph-api/schema/domain.ts
+++ b/apps/ensapi/src/omnigraph-api/schema/domain.ts
@@ -37,6 +37,7 @@ import {
} from "@/omnigraph-api/schema/constants";
import { DomainCanonicalRef } from "@/omnigraph-api/schema/domain-canonical";
import {
+ DOMAINS_ORDERING_DESCRIPTION,
DomainPermissionsWhereInput,
DomainsOrderInput,
SubdomainsWhereInput,
@@ -279,16 +280,16 @@ DomainInterfaceRef.implement({
// Domain.subdomains
/////////////////////
subdomains: t.connection({
- description: "All Domains that are direct descendants of this Domain in the namegraph.",
+ description: `All Domains that are direct descendants of this Domain in the namegraph. ${DOMAINS_ORDERING_DESCRIPTION}`,
type: DomainInterfaceRef,
args: {
where: t.arg({ type: SubdomainsWhereInput }),
order: t.arg({ type: DomainsOrderInput }),
},
- resolve: (parent, { where, order, ...connectionArgs }, context) => {
+ resolve: (parent, { where, order, ...connectionArgs }) => {
if (!parent.subregistryId) return EMPTY_CONNECTION;
- return resolveFindDomains(context, {
+ return resolveFindDomains({
where: { ...where, registryId: parent.subregistryId },
order,
...connectionArgs,
diff --git a/apps/ensapi/src/omnigraph-api/schema/query.ts b/apps/ensapi/src/omnigraph-api/schema/query.ts
index 324d1d9dd2..445bbed274 100644
--- a/apps/ensapi/src/omnigraph-api/schema/query.ts
+++ b/apps/ensapi/src/omnigraph-api/schema/query.ts
@@ -14,6 +14,7 @@ import { AccountByInput, AccountRef } from "@/omnigraph-api/schema/account";
import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants";
import { DomainInterfaceRef } from "@/omnigraph-api/schema/domain";
import {
+ DOMAINS_ORDERING_DESCRIPTION,
DomainIdInput,
DomainsOrderInput,
DomainsWhereInput,
@@ -106,14 +107,14 @@ builder.queryType({
// Find Domains
////////////////
domains: t.connection({
- description: "Find Canonical Domains by Name.",
+ description: `Find Canonical Domains by Name. ${DOMAINS_ORDERING_DESCRIPTION}`,
type: DomainInterfaceRef,
args: {
where: t.arg({ type: DomainsWhereInput, required: true }),
order: t.arg({ type: DomainsOrderInput }),
},
- resolve: (_, { where, order, ...connectionArgs }, context) =>
- resolveFindDomains(context, { where, order, ...connectionArgs }),
+ resolve: (_, { where, order, ...connectionArgs }) =>
+ resolveFindDomains({ where, order, ...connectionArgs }),
}),
//////////////////////////////////
diff --git a/apps/ensapi/src/omnigraph-api/schema/registry.ts b/apps/ensapi/src/omnigraph-api/schema/registry.ts
index d38d641e46..2c1155ae80 100644
--- a/apps/ensapi/src/omnigraph-api/schema/registry.ts
+++ b/apps/ensapi/src/omnigraph-api/schema/registry.ts
@@ -13,7 +13,11 @@ import { lazyConnection } from "@/omnigraph-api/lib/lazy-connection";
import { AccountIdInput, AccountIdRef } from "@/omnigraph-api/schema/account-id";
import { ID_PAGINATED_CONNECTION_ARGS } from "@/omnigraph-api/schema/constants";
import { DomainInterfaceRef } from "@/omnigraph-api/schema/domain";
-import { DomainsOrderInput, RegistryDomainsWhereInput } from "@/omnigraph-api/schema/domain-inputs";
+import {
+ DOMAINS_ORDERING_DESCRIPTION,
+ DomainsOrderInput,
+ RegistryDomainsWhereInput,
+} from "@/omnigraph-api/schema/domain-inputs";
import { PermissionsRef } from "@/omnigraph-api/schema/permissions";
///////////////////////////////////
@@ -121,14 +125,14 @@ RegistryInterfaceRef.implement({
// Registry.domains
//////////////////////
domains: t.connection({
- description: "The Domains managed by this Registry.",
+ description: `The Domains managed by this Registry. ${DOMAINS_ORDERING_DESCRIPTION}`,
type: DomainInterfaceRef,
args: {
where: t.arg({ type: RegistryDomainsWhereInput }),
order: t.arg({ type: DomainsOrderInput }),
},
- resolve: (parent, { where, order, ...connectionArgs }, context) =>
- resolveFindDomains(context, {
+ resolve: (parent, { where, order, ...connectionArgs }) =>
+ resolveFindDomains({
where: { ...where, registryId: parent.id },
order,
...connectionArgs,
diff --git a/apps/ensapi/src/test/integration/find-domains/test-domain-pagination.ts b/apps/ensapi/src/test/integration/find-domains/test-domain-pagination.ts
index 96c4fe4d3a..6d82de80bd 100644
--- a/apps/ensapi/src/test/integration/find-domains/test-domain-pagination.ts
+++ b/apps/ensapi/src/test/integration/find-domains/test-domain-pagination.ts
@@ -1,5 +1,6 @@
import { beforeAll, describe, expect, it } from "vitest";
+import { shouldUseNullsLast } from "@/omnigraph-api/lib/find-domains/find-domains-resolver-helpers";
import type { DomainsOrderByValue, DomainsOrderInput } from "@/omnigraph-api/schema/domain-inputs";
import type { OrderDirectionValue } from "@/omnigraph-api/schema/order-direction";
import type { PaginatedDomainResult } from "@/test/integration/find-domains/domain-pagination-queries";
@@ -58,22 +59,36 @@ function assertOrdering(
) {
const values = domains.map((n) => getSortValue(n, by));
+ // Registration orderings use SQL-default NULL placement (ASC → last, DESC → first); NAME/DEPTH
+ // keep NULLS LAST in both directions. See find-domains-resolver-helpers.ts.
+ const nullsFirst = !shouldUseNullsLast(by) && dir === "DESC";
+
for (let i = 0; i < values.length - 1; i++) {
const a = values[i];
const b = values[i + 1];
- // nulls sort last regardless of direction
- if (a === null) {
- // a is null => b must also be null (everything after should be null)
- expect(
- b,
- `expected null at index ${i + 1} because index ${i} was null (nulls last)`,
- ).toBeNull();
- continue;
- }
- if (b === null) {
+ if (nullsFirst) {
+ // nulls first: a non-null must not be followed by a null
+ if (b === null) {
+ expect(
+ a,
+ `expected null at index ${i} because index ${i + 1} was null (nulls first)`,
+ ).toBeNull();
+ continue;
+ }
+ // a is null, b is non-null => fine (null sorts first)
+ if (a === null) continue;
+ } else {
+ // nulls last: a null must be followed only by nulls
+ if (a === null) {
+ expect(
+ b,
+ `expected null at index ${i + 1} because index ${i} was null (nulls last)`,
+ ).toBeNull();
+ continue;
+ }
// a is non-null, b is null => fine (null sorts last)
- continue;
+ if (b === null) continue;
}
if (by === "NAME") {
diff --git a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts
index 779c7f0602..1e83b03f67 100644
--- a/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts
+++ b/apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts
@@ -1,5 +1,7 @@
import { type DomainId, makeRegistrationId, makeRenewalId } from "enssdk";
+import { REGISTRATION_SORT_SENTINEL } from "@ensnode/ensdb-sdk/ensindexer-abstract";
+
import { ensIndexerSchema, type IndexingEngineContext } from "@/lib/indexing-engines/ponder";
/**
@@ -18,6 +20,28 @@ import { ensIndexerSchema, type IndexingEngineContext } from "@/lib/indexing-eng
* each type of entity.
*/
+/**
+ * Materializes the latest Registration's sort keys onto the Domain row, if it exists.
+ *
+ * Preminted names (BaseRegistrar `registerOnly` on Basenames/Lineanames) have a Registration before
+ * their Domain row exists, and Ponder's `update` throws on an absent row. Such a Domain materializes
+ * these columns from its latest Registration when it's created (see ENSv1Registry NewOwner).
+ */
+async function materializeDomainLatestRegistration(
+ context: IndexingEngineContext,
+ domainId: DomainId,
+ values: Partial<
+ Pick<
+ typeof ensIndexerSchema.domain.$inferInsert,
+ "__latestRegistrationStart" | "__latestRegistrationExpiry"
+ >
+ >,
+) {
+ const domain = await context.ensDb.find(ensIndexerSchema.domain, { id: domainId });
+ if (!domain) return;
+ await context.ensDb.update(ensIndexerSchema.domain, { id: domainId }).set(values);
+}
+
/**
* Gets the latest Registration for the provided `domainId`.
*/
@@ -55,6 +79,39 @@ export async function insertLatestRegistration(
.insert(ensIndexerSchema.latestRegistrationIndex)
.values({ domainId, registrationIndex })
.onConflictDoUpdate({ registrationIndex });
+
+ // conditionally materialize Domain.__latestRegistration*
+ await materializeDomainLatestRegistration(context, domainId, {
+ __latestRegistrationStart: values.start,
+ __latestRegistrationExpiry: values.expiry ?? REGISTRATION_SORT_SENTINEL,
+ });
+}
+
+/**
+ * Updates the expiry of a Domain's latest Registration.
+ *
+ * @dev materializes Domain.__latestRegistrationExpiry
+ * @dev callers MUST pass the Domain's _latest_ Registration; we don't validate that the provided
+ * `registrationId` is actually the latest
+ */
+export async function updateLatestRegistrationExpiry(
+ context: IndexingEngineContext,
+ {
+ domainId,
+ registrationId,
+ expiry,
+ }: {
+ domainId: DomainId;
+ registrationId: ReturnType;
+ expiry: bigint | null;
+ },
+) {
+ await context.ensDb.update(ensIndexerSchema.registration, { id: registrationId }).set({ expiry });
+
+ // conditionally materialize Domain.__latestRegistrationExpiry
+ await materializeDomainLatestRegistration(context, domainId, {
+ __latestRegistrationExpiry: expiry ?? REGISTRATION_SORT_SENTINEL,
+ });
}
/**
diff --git a/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/BaseRegistrar.ts b/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/BaseRegistrar.ts
index f6f359eeb6..a4dd89d171 100644
--- a/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/BaseRegistrar.ts
+++ b/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/BaseRegistrar.ts
@@ -23,6 +23,7 @@ import {
getLatestRegistration,
insertLatestRegistration,
insertLatestRenewal,
+ updateLatestRegistrationExpiry,
} from "@/lib/ensv2/registration-db-helpers";
import { getThisAccountId } from "@/lib/get-this-account-id";
import {
@@ -237,9 +238,11 @@ export default function () {
const duration = expiry - registration.expiry;
// update the registration
- await context.ensDb
- .update(ensIndexerSchema.registration, { id: registration.id })
- .set({ expiry });
+ await updateLatestRegistrationExpiry(context, {
+ domainId,
+ registrationId: registration.id,
+ expiry,
+ });
// insert Renewal
const eventId = await ensureEvent(context, event);
diff --git a/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/ENSv1Registry.ts b/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/ENSv1Registry.ts
index 773533e9e4..2c2a6d1047 100644
--- a/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/ENSv1Registry.ts
+++ b/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/ENSv1Registry.ts
@@ -14,6 +14,7 @@ import {
} from "enssdk";
import { isAddressEqual, zeroAddress } from "viem";
+import { REGISTRATION_SORT_SENTINEL } from "@ensnode/ensdb-sdk/ensindexer-abstract";
import {
ENSNamespaceIds,
getENSRootChainId,
@@ -31,6 +32,7 @@ import {
} from "@/lib/ensv2/canonicality-db-helpers";
import { ensureDomainEvent, ensureEvent } from "@/lib/ensv2/event-db-helpers";
import { ensureLabel, ensureUnknownLabel, labelExists } from "@/lib/ensv2/label-db-helpers";
+import { getLatestRegistration } from "@/lib/ensv2/registration-db-helpers";
import { ensureRegistry } from "@/lib/ensv2/registry-db-helpers";
import { getThisAccountId } from "@/lib/get-this-account-id";
import { healAddrReverseSubnameLabel } from "@/lib/heal-addr-reverse-subname-label";
@@ -178,6 +180,10 @@ export default function () {
// (BaseRegistrar, NameWrapper) because (a) the root Registry is the source of truth even when
// no Registrar is in use, and (b) Registrar events fire _after_ Registry events, so they
// re-materialize over the value we set here.
+ // a preminted name (BaseRegistrar `registerOnly`) has a Registration before its Domain exists, so
+ // materialize its sort keys here on creation; a normal name has none yet (its Registrar
+ // NameRegistered materializes them afterwards). Set on insert only so owner-changes don't clobber.
+ const registration = await getLatestRegistration(context, domainId);
await context.ensDb
.insert(ensIndexerSchema.domain)
.values({
@@ -188,6 +194,12 @@ export default function () {
labelHash,
ownerId,
rootRegistryOwnerId: ownerId,
+ ...(registration
+ ? {
+ __latestRegistrationStart: registration.start,
+ __latestRegistrationExpiry: registration.expiry ?? REGISTRATION_SORT_SENTINEL,
+ }
+ : {}),
})
.onConflictDoUpdate({ ownerId, rootRegistryOwnerId: ownerId });
diff --git a/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/NameWrapper.ts b/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/NameWrapper.ts
index beedc194b7..2c3068a046 100644
--- a/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/NameWrapper.ts
+++ b/apps/ensindexer/src/plugins/unigraph/handlers/ensv1/NameWrapper.ts
@@ -32,6 +32,7 @@ import {
getLatestRegistration,
insertLatestRegistration,
insertLatestRenewal,
+ updateLatestRegistrationExpiry,
} from "@/lib/ensv2/registration-db-helpers";
import { getThisAccountId } from "@/lib/get-this-account-id";
import {
@@ -299,7 +300,9 @@ export default function () {
});
} else {
// otherwise, deactivate the latest registration by setting its expiry to this block
- await context.ensDb.update(ensIndexerSchema.registration, { id: registration.id }).set({
+ await updateLatestRegistrationExpiry(context, {
+ domainId,
+ registrationId: registration.id,
expiry: event.block.timestamp,
});
}
@@ -375,9 +378,12 @@ export default function () {
);
}
- await context.ensDb
- .update(ensIndexerSchema.registration, { id: registration.id })
- .set({ expiry });
+ // update Registration expiry
+ await updateLatestRegistrationExpiry(context, {
+ domainId,
+ registrationId: registration.id,
+ expiry,
+ });
// push event to domain history
const eventId = await ensureEvent(context, event);
diff --git a/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts
index e9f9059ab7..2aefcd4f87 100644
--- a/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts
+++ b/apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts
@@ -31,6 +31,7 @@ import { ensureLabel } from "@/lib/ensv2/label-db-helpers";
import {
getLatestRegistration,
insertLatestRegistration,
+ updateLatestRegistrationExpiry,
} from "@/lib/ensv2/registration-db-helpers";
import { ensureRegistry } from "@/lib/ensv2/registry-db-helpers";
import { getThisAccountId } from "@/lib/get-this-account-id";
@@ -196,10 +197,15 @@ export default function () {
// unregistering a label just immediately sets its expiration to event.block.timestamp, which
// effectively removes it from resolution (which interprets expired names as non-existent)
const unregistrantId = await ensureAccount(context, unregistrant);
- await context.ensDb.update(ensIndexerSchema.registration, { id: registration.id }).set({
+ // update registration expiry to now
+ await updateLatestRegistrationExpiry(context, {
+ domainId,
+ registrationId: registration.id,
expiry: event.block.timestamp,
- unregistrantId,
});
+ await context.ensDb
+ .update(ensIndexerSchema.registration, { id: registration.id })
+ .set({ unregistrantId });
// NOTE(shrugs): PermissionedRegistry also increments eacVersionId and tokenVersionId if there was a
// previous owner, but i'm not sure if we need to handle that detail here
@@ -243,10 +249,12 @@ export default function () {
);
}
- // update Registration
- await context.ensDb
- .update(ensIndexerSchema.registration, { id: registration.id })
- .set({ expiry });
+ // update Registration (and its materialized mirror on the Domain)
+ await updateLatestRegistrationExpiry(context, {
+ domainId,
+ registrationId: registration.id,
+ expiry,
+ });
// push event to domain history
const senderId = await ensureAccount(context, sender);
diff --git a/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx b/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx
index bd2f2e852f..e83c38ff4e 100644
--- a/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx
+++ b/docs/ensnode.io/src/content/docs/docs/integrate/unigraph/schema-reference.mdx
@@ -218,24 +218,26 @@ The `domains.owner_id` for ENSv1 Domains is the materialized effective owner. EN
Domain-Resolver relations are tracked via the Protocol Acceleration plugin, not stored on the domain row. Parent-domain traversal of the canonical nametree is supported directly via the materialized `canonical_path` / `canonical_label_hash_path` arrays; non-canonical traversal walks the `registries.canonical_domain_id` ↔ `domains.subregistry_id` pointers at query-time.
:::
-| Column | Type | Nullable | Description |
-| --------------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `id` | `text` | no | ENSv1DomainId: `{ENSv1RegistryId}/{node}`. ENSv2DomainId: CAIP-19 asset identifier. Primary key. |
-| `type` | `DomainType` | no | `ENSv1Domain` or `ENSv2Domain`. |
-| `registry_id` | `text` | no | The registry this domain belongs to. |
-| `subregistry_id` | `text` | yes | The registry that manages subdomains of this domain, if any. |
-| `token_id` | `numeric(78)` | yes | ENSv2 only: the TokenId within the ENSv2Registry. `null` for ENSv1 domains. |
-| `node` | `text` | yes | ENSv1 only: the domain's namehash. `null` for ENSv2 domains. |
-| `label_hash` | `text` | no | Represents a labelHash. References `labels.label_hash`. |
-| `owner_id` | `text` | yes | If `ENSv1Domain`, the materialized effective owner address. If `ENSv2Domain`, the on-chain owner address (the HCA account address if used). |
-| `root_registry_owner_id` | `text` | yes | ENSv1 only: the owner recorded in the root ENSv1 registry. `null` for ENSv2 domains. |
-| `canonical` | `boolean` | no | Whether this Domain is part of the canonical nametree. This encodes bi-directional agreement between `domains.subregistry_id` and `registries.canonical_domain_id`, so traversal of the canonical nametree filtered to domains/registries where `canonical=true` is safe and doesn't require edge-authenticating oneself (i.e. don't need to compare `domains.subregistry_id` and `registries.canonical_domain_id` in the query, can just `WHERE canonical = true`). Mirrors the parent Registry's flag. Default `false`. |
-| `canonical_name` | `text` | yes | Materialized Canonical Name, `NULL` iff `canonical = false`. Maintained by `canonicality-db-helpers.ts`. Use for exact matches (`canonical_name = 'vitalik.eth'`) and display. Example: `"vitalik.eth"`. |
-| `__canonical_name_prefix` | `text` | yes | Materialized prefix of `canonical_name` (first 64 code points), `NULL` iff `canonical = false`. Maintained by `canonicality-db-helpers.ts`. Use for left-anchored / substring search (`__canonical_name_prefix ILIKE 'vit%'`, case-insensitive to match the Omnigraph `starts_with` filter) and NAME ordering without `canonical_name`'s full-length btree size hazard. The `__` prefix marks it an internal implementation detail — query `canonical_name` for exact matches and display. |
-| `canonical_label_hash_path` | `text[]` | yes | Materialized Canonical LabelHashPath, `NULL` iff `canonical = false`. Head-first (root → leaf), i.e. `[labelhash("eth"), labelhash("vitalik")]` for `"vitalik.eth"`. Maintained by `canonicality-db-helpers.ts`. |
-| `canonical_path` | `text[]` | yes | Materialized Canonical Domain Path, `NULL` iff `canonical = false`. Head-first (root → leaf), i.e. `[DomainId("eth"), DomainId("vitalik")]` for `"vitalik.eth"`. Maintained by `canonicality-db-helpers.ts`. |
-| `canonical_depth` | `integer` | yes | Materialized Canonical Depth, `NULL` iff `canonical = false`. The depth of this Domain in the Canonical Nametree, i.e. the number of Labels in its Canonical Name (e.g. `"eth"` depth 1, `"vitalik.eth"` depth 2). Maintained by `canonicality-db-helpers.ts`. |
-| `canonical_node` | `text` | yes | Materialized Canonical Node, `NULL` iff `canonical = false`. The computed Node (via `namehash`) of this Domain's Canonical Name. Maintained by `canonicality-db-helpers.ts`. |
+| Column | Type | Nullable | Description |
+| ------------------------------ | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | `text` | no | ENSv1DomainId: `{ENSv1RegistryId}/{node}`. ENSv2DomainId: CAIP-19 asset identifier. Primary key. |
+| `type` | `DomainType` | no | `ENSv1Domain` or `ENSv2Domain`. |
+| `registry_id` | `text` | no | The registry this domain belongs to. |
+| `subregistry_id` | `text` | yes | The registry that manages subdomains of this domain, if any. |
+| `token_id` | `numeric(78)` | yes | ENSv2 only: the TokenId within the ENSv2Registry. `null` for ENSv1 domains. |
+| `node` | `text` | yes | ENSv1 only: the domain's namehash. `null` for ENSv2 domains. |
+| `label_hash` | `text` | no | Represents a labelHash. References `labels.label_hash`. |
+| `owner_id` | `text` | yes | If `ENSv1Domain`, the materialized effective owner address. If `ENSv2Domain`, the on-chain owner address (the HCA account address if used). |
+| `root_registry_owner_id` | `text` | yes | ENSv1 only: the owner recorded in the root ENSv1 registry. `null` for ENSv2 domains. |
+| `canonical` | `boolean` | no | Whether this Domain is part of the canonical nametree. This encodes bi-directional agreement between `domains.subregistry_id` and `registries.canonical_domain_id`, so traversal of the canonical nametree filtered to domains/registries where `canonical=true` is safe and doesn't require edge-authenticating oneself (i.e. don't need to compare `domains.subregistry_id` and `registries.canonical_domain_id` in the query, can just `WHERE canonical = true`). Mirrors the parent Registry's flag. Default `false`. |
+| `canonical_name` | `text` | yes | Materialized Canonical Name, `NULL` iff `canonical = false`. Maintained by `canonicality-db-helpers.ts`. Use for exact matches (`canonical_name = 'vitalik.eth'`) and display. Example: `"vitalik.eth"`. |
+| `__canonical_name_prefix` | `text` | yes | Materialized prefix of `canonical_name` (first 64 code points), `NULL` iff `canonical = false`. Maintained by `canonicality-db-helpers.ts`. Use for left-anchored / substring search (`__canonical_name_prefix ILIKE 'vit%'`, case-insensitive to match the Omnigraph `starts_with` filter) and NAME ordering without `canonical_name`'s full-length btree size hazard. The `__` prefix marks it an internal implementation detail — query `canonical_name` for exact matches and display. |
+| `canonical_label_hash_path` | `text[]` | yes | Materialized Canonical LabelHashPath, `NULL` iff `canonical = false`. Head-first (root → leaf), i.e. `[labelhash("eth"), labelhash("vitalik")]` for `"vitalik.eth"`. Maintained by `canonicality-db-helpers.ts`. |
+| `canonical_path` | `text[]` | yes | Materialized Canonical Domain Path, `NULL` iff `canonical = false`. Head-first (root → leaf), i.e. `[DomainId("eth"), DomainId("vitalik")]` for `"vitalik.eth"`. Maintained by `canonicality-db-helpers.ts`. |
+| `canonical_depth` | `integer` | yes | Materialized Canonical Depth, `NULL` iff `canonical = false`. The depth of this Domain in the Canonical Nametree, i.e. the number of Labels in its Canonical Name (e.g. `"eth"` depth 1, `"vitalik.eth"` depth 2). Maintained by `canonicality-db-helpers.ts`. |
+| `canonical_node` | `text` | yes | Materialized Canonical Node, `NULL` iff `canonical = false`. The computed Node (via `namehash`) of this Domain's Canonical Name. Maintained by `canonicality-db-helpers.ts`. |
+| `__latest_registration_start` | `numeric(78)` | no | Materialized `start` of this Domain's latest Registration, or the `REGISTRATION_SORT_SENTINEL` (`2^256 − 1`) when the Domain has no Registration. Mirror of the latest `registration.start`, maintained by `registration-db-helpers.ts`. Backs `REGISTRATION_TIMESTAMP`-ordered find-domains queries via the `(registry_id, __latest_registration_start, id)` composite index, avoiding a join through `latest_registration_indexes` → `registrations` and a sort. Held `NOT NULL` (absent → sentinel) so the keyset stays a plain tuple compare. The `__` prefix marks it an internal materialized mirror — the canonical (possibly null) value lives on the Registration entity. |
+| `__latest_registration_expiry` | `numeric(78)` | no | Materialized `expiry` of this Domain's latest Registration, or the `REGISTRATION_SORT_SENTINEL` (`2^256 − 1`) when the Domain has no Registration or its latest Registration never expires (effectively +∞). Mirror of the latest `registration.expiry`, maintained by `registration-db-helpers.ts`. Backs `REGISTRATION_EXPIRY`-ordered queries; see `__latest_registration_start`. |
**Indexes:**
@@ -250,6 +252,8 @@ Domain-Resolver relations are tracked via the Protocol Acceleration plugin, not
- `canonical_label_hash_path` (GIN containment for `cascadeLabelHeal`'s `canonical_label_hash_path @> ARRAY[lh]` lookup)
- `canonical_node` (hash, for resolver-record → canonical-domain joins)
- `canonical_depth` (btree, for `ORDER BY canonical_depth` — typeahead and depth-ordered browse)
+- `(registry_id, __latest_registration_start, id)` (composite for registry-scoped `WHERE registry_id = X ORDER BY __latest_registration_start LIMIT N` — `REGISTRATION_TIMESTAMP` ordering; the `NOT NULL` columns let one plain composite serve both ASC and DESC keyset scans)
+- `(registry_id, __latest_registration_expiry, id)` (composite, as above for `REGISTRATION_EXPIRY` ordering)
**Relations:** belongs to one `registries` record, belongs to one `registries` record (as subregistry), has one `accounts` record (owner), has one `accounts` record (rootRegistryOwner), has one `labels` record, has many `registrations` records.
diff --git a/docs/ensnode.io/src/data/omnigraph-examples/examples.json b/docs/ensnode.io/src/data/omnigraph-examples/examples.json
index 99ac34e43f..822c5542a4 100644
--- a/docs/ensnode.io/src/data/omnigraph-examples/examples.json
+++ b/docs/ensnode.io/src/data/omnigraph-examples/examples.json
@@ -31,6 +31,13 @@
"name": "roppp.eth"
}
},
+ {
+ "id": "domain-records",
+ "query": "query DomainRecords(\n $name: InterpretedName!\n) {\n domain(by: { name: $name }) {\n canonical { name { interpreted } }\n resolve {\n records {\n addresses(coinTypes: [60]) { coinType address }\n texts(keys: [\"description\"]) { key value }\n }\n }\n }\n}",
+ "variables": {
+ "name": "roppp.eth"
+ }
+ },
{
"id": "domain-subdomains",
"query": "query DomainSubdomains($name: InterpretedName!) {\n domain(by: {name: $name}) {\n canonical { name { interpreted beautified } }\n subdomains(first: 10) {\n edges {\n node {\n canonical { name { interpreted beautified } }\n }\n }\n }\n }\n}",
@@ -38,6 +45,13 @@
"name": "eth"
}
},
+ {
+ "id": "domain-subdomains-recently-registered",
+ "query": "query RecentlyRegisteredSubdomains($name: InterpretedName!) {\n domain(by: {name: $name}) {\n canonical { name { interpreted beautified } }\n subdomains(first: 10, order: {by: REGISTRATION_TIMESTAMP, dir: DESC}) {\n edges {\n node {\n canonical { name { interpreted beautified } }\n }\n }\n }\n }\n}",
+ "variables": {
+ "name": "eth"
+ }
+ },
{
"id": "subdomains-pagination",
"query": "query SubdomainsPagination($first: Int!, $after: String) {\n domain(by: { name: \"eth\" }) {\n canonical { name { interpreted } }\n\n # paginate child names: pass pageInfo.endCursor back as $after for the next page\n subdomains(first: $first, after: $after) {\n totalCount\n pageInfo { hasNextPage endCursor }\n edges {\n cursor\n node {\n canonical { name { interpreted } }\n }\n }\n }\n }\n}",
@@ -60,6 +74,13 @@
"address": "0x801d2e48d378f161dba7ad7ad002ad557714c191"
}
},
+ {
+ "id": "account-primary-names",
+ "query": "query AccountPrimaryNames($address: Address!) {\n account(by: { address: $address }) {\n address\n resolve {\n primaryNames(where: { chainNames: [ETHEREUM, BASE] }) {\n coinType\n chainName\n name { interpreted beautified }\n resolve {\n records {\n addresses(coinTypes: [60]) {\n coinType\n address\n }\n }\n }\n }\n }\n }\n}",
+ "variables": {
+ "address": "0x801d2e48d378f161dba7ad7ad002ad557714c191"
+ }
+ },
{
"id": "account-events",
"query": "query AccountEvents(\n $address: Address!\n) {\n account(by: { address: $address }) {\n events { totalCount edges { node { topics data timestamp } } }\n }\n}",
@@ -134,5 +155,12 @@
"id": "eth-by-version",
"query": "query GetEthDomains {\n domains(where: { name: { eq: \"eth\" } }) {\n edges {\n node {\n __typename\n id\n }\n }\n }\n}",
"variables": {}
+ },
+ {
+ "id": "domain-profile",
+ "query": "query DomainProfile($name: InterpretedName!) {\n domain(by: { name: $name }) {\n resolve {\n profile {\n description\n avatar { httpUrl }\n addresses { ethereum }\n socials { github { handle httpUrl } }\n website { httpUrl }\n email\n }\n }\n }\n}",
+ "variables": {
+ "name": "vitalik.eth"
+ }
}
]
diff --git a/docs/ensnode.io/src/data/omnigraph-examples/meta.ts b/docs/ensnode.io/src/data/omnigraph-examples/meta.ts
index 92ca459228..86ecfa4652 100644
--- a/docs/ensnode.io/src/data/omnigraph-examples/meta.ts
+++ b/docs/ensnode.io/src/data/omnigraph-examples/meta.ts
@@ -23,6 +23,11 @@ export const OMNIGRAPH_EXAMPLES_META: Record<
description: "Paginate direct child names under a parent domain.",
category: "Resolution",
},
+ "domain-subdomains-recently-registered": {
+ name: "Recently Registered Subdomains",
+ description: "List a parent domain's subdomains ordered by most recent registration first.",
+ category: "Resolution",
+ },
"domain-events": {
name: "Domain Events",
description: "Raw contract events associated with a domain’s registry records.",
diff --git a/docs/ensnode.io/src/data/omnigraph-examples/responses.json b/docs/ensnode.io/src/data/omnigraph-examples/responses.json
index 76741bb403..bbe71c56f2 100644
--- a/docs/ensnode.io/src/data/omnigraph-examples/responses.json
+++ b/docs/ensnode.io/src/data/omnigraph-examples/responses.json
@@ -2536,5 +2536,81 @@
}
}
}
+ },
+ "domain-subdomains-recently-registered": {
+ "data": {
+ "domain": {
+ "canonical": {
+ "name": {
+ "interpreted": "eth",
+ "beautified": "eth"
+ }
+ },
+ "subdomains": {
+ "edges": [
+ {
+ "node": {
+ "canonical": {
+ "name": {
+ "interpreted": "wrapnation.eth",
+ "beautified": "wrapnation.eth"
+ }
+ }
+ }
+ },
+ {
+ "node": {
+ "canonical": {
+ "name": {
+ "interpreted": "katrenpadu.eth",
+ "beautified": "katrenpadu.eth"
+ }
+ }
+ }
+ },
+ {
+ "node": {
+ "canonical": {
+ "name": {
+ "interpreted": "roppp.eth",
+ "beautified": "roppp.eth"
+ }
+ }
+ }
+ },
+ {
+ "node": {
+ "canonical": {
+ "name": {
+ "interpreted": "sfmpfvtoicv2ok.eth",
+ "beautified": "sfmpfvtoicv2ok.eth"
+ }
+ }
+ }
+ },
+ {
+ "node": {
+ "canonical": {
+ "name": {
+ "interpreted": "sfmpfv44d0mig.eth",
+ "beautified": "sfmpfv44d0mig.eth"
+ }
+ }
+ }
+ },
+ {
+ "node": {
+ "canonical": {
+ "name": {
+ "interpreted": "sfmpfv44d0res.eth",
+ "beautified": "sfmpfv44d0res.eth"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
}
}
diff --git a/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql b/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql
index 0c9e37a06b..5007fd6372 100644
--- a/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql
+++ b/docs/ensnode.io/src/data/omnigraph-examples/schema.graphql
@@ -1,3 +1,18 @@
+"""
+Execution status metadata for a resolver strategy.
+"""
+type AccelerationStatus {
+ """
+ Whether protocol acceleration was attempted at runtime.
+ """
+ attempted: Boolean!
+
+ """
+ Whether protocol acceleration was requested by the caller.
+ """
+ requested: Boolean!
+}
+
"""
Represents an individual Account, keyed by its Address.
"""
@@ -8,7 +23,7 @@ type Account {
address: Address!
"""
- The Domains that are owned by the Account.
+ The Domains that are owned by the Account. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
domains(
after: String
@@ -56,6 +71,17 @@ type Account {
last: Int
): AccountRegistryPermissionsConnection
+ """
+ Resolve primary names for this Account.
+ """
+ resolve(
+ """
+ When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.
+ @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration
+ """
+ accelerate: Boolean = true
+ ): ReverseResolve!
+
"""
The Permissions on Resolvers granted to this Account.
"""
@@ -270,6 +296,11 @@ type BaseRegistrarRegistration implements Registration {
"""
start: BigInt!
+ """
+ The TokenId for this Domain in the BaseRegistrar. This is the bigint encoding of the Domain's LabelHash.
+ """
+ tokenId: BigInt!
+
"""
The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).
"""
@@ -296,6 +327,21 @@ BigInt represents non-fractional signed whole numeric values.
"""
scalar BigInt
+"""
+BinanceAddress represents a Bech32-encoded Binance Chain (BNB) address (coin type 714).
+"""
+scalar BinanceAddress
+
+"""
+BitcoinAddress represents a Base58Check-encoded Bitcoin address (coin type 0).
+"""
+scalar BitcoinAddress
+
+"""
+BitcoinCashAddress represents a CashAddr-encoded Bitcoin Cash address (coin type 145).
+"""
+scalar BitcoinCashAddress
+
"""
A Canonical Name, exposed in each representation we support.
"""
@@ -316,11 +362,28 @@ ChainId represents an enssdk#ChainId.
"""
scalar ChainId
+"""
+The names of chains that the Omnigraph API supports identifying by name as a syntactic convenience. The Omnigraph API supports identification of additional chains beyond this list, but those chains must be identified through other mechanisms such as `coinType` or `chainId`.
+"""
+enum ChainName {
+ ARBITRUM_ONE
+ BASE
+ ETHEREUM
+ LINEA
+ OPTIMISM
+ SCROLL
+}
+
"""
CoinType represents an enssdk#CoinType.
"""
scalar CoinType
+"""
+DogecoinAddress represents a Base58Check-encoded Dogecoin address (coin type 3).
+"""
+scalar DogecoinAddress
+
"""
Represents a Domain, i.e. an individual Label within the ENS namegraph. It may or may not be Canonical. It may be an ENSv1Domain or an ENSv2Domain.
"""
@@ -352,7 +415,7 @@ interface Domain {
label: Label!
"""
- If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).
+ If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).
"""
owner: Account
@@ -376,13 +439,24 @@ interface Domain {
"""
registry: Registry!
+ """
+ Resolve protocol-level data for this Domain.
+ """
+ resolve(
+ """
+ When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.
+ @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration
+ """
+ accelerate: Boolean = true
+ ): ForwardResolve!
+
"""
Resolver relationship metadata for this Domain.
"""
resolver: DomainResolver!
"""
- All Domains that are direct descendents of this Domain in the namegraph.
+ All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
subdomains(
after: String
@@ -473,6 +547,46 @@ input DomainPermissionsWhereInput {
user: DomainPermissionsUserFilter
}
+"""
+The interpreted profile of an ENS name.
+"""
+type DomainProfile {
+ """
+ The interpreted addresses on the profile of an ENS name.
+ """
+ addresses: ProfileAddresses
+
+ """
+ Interpreted avatar metadata. Returns null when the raw avatar record is unset or empty.
+ """
+ avatar: ProfileAvatar
+
+ """
+ The interpreted description on the profile of an ENS name, or null when unset.
+ """
+ description: String
+
+ """
+ The interpreted email address on the profile of an ENS name, or null when unset or invalid.
+ """
+ email: Email
+
+ """
+ Interpreted header metadata. Returns null when the raw header record is unset or empty.
+ """
+ header: ProfileHeader
+
+ """
+ The interpreted social accounts on the profile of an ENS name.
+ """
+ socials: ProfileSocials
+
+ """
+ The interpreted website on the profile of an ENS name.
+ """
+ website: ProfileWebsite
+}
+
type DomainRegistrationsConnection {
edges: [DomainRegistrationsConnectionEdge!]!
pageInfo: PageInfo!
@@ -526,12 +640,27 @@ input DomainsNameFilter @oneOf {
}
"""
-Fields by which domains can be ordered
+Fields by which domains can be ordered.
"""
enum DomainsOrderBy {
+ """
+ Order by Canonical Name depth (number of labels); e.g. `eth` < `vitalik.eth`.
+ """
DEPTH
+
+ """
+ Order by the Domain's Canonical Name, alphabetically.
+ """
NAME
+
+ """
+ Order by the expiry of the Domain's latest Registration. A Domain that never expires (or has no Registration) is treated as +∞: it sorts last when `dir: ASC` (“expiring soonest first”) and first when `dir: DESC` (“expiring latest first”).
+ """
REGISTRATION_EXPIRY
+
+ """
+ Order by the start time of the Domain's latest Registration. A Domain with no Registration has no timestamp and sorts last when `dir: ASC` (“oldest registered first”) and first when `dir: DESC` (“most recently registered first”).
+ """
REGISTRATION_TIMESTAMP
}
@@ -602,7 +731,7 @@ type ENSv1Domain implements Domain {
node: Node!
"""
- If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).
+ If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).
"""
owner: Account
@@ -626,6 +755,17 @@ type ENSv1Domain implements Domain {
"""
registry: Registry!
+ """
+ Resolve protocol-level data for this Domain.
+ """
+ resolve(
+ """
+ When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.
+ @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration
+ """
+ accelerate: Boolean = true
+ ): ForwardResolve!
+
"""
Resolver relationship metadata for this Domain.
"""
@@ -637,7 +777,7 @@ type ENSv1Domain implements Domain {
rootRegistryOwner: Account
"""
- All Domains that are direct descendents of this Domain in the namegraph.
+ All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
subdomains(
after: String
@@ -669,7 +809,7 @@ type ENSv1Registry implements Registry {
contract: AccountId!
"""
- The Domains managed by this Registry.
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
domains(
after: String
@@ -711,7 +851,7 @@ type ENSv1VirtualRegistry implements Registry {
contract: AccountId!
"""
- The Domains managed by this Registry.
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
domains(
after: String
@@ -774,7 +914,7 @@ type ENSv2Domain implements Domain {
label: Label!
"""
- If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).
+ If this is an ENSv1Domain, this is the effective owner of the Domain (derived from the Registry, the Registrar, or the NameWrapper, in that order). If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).
"""
owner: Account
@@ -809,13 +949,24 @@ type ENSv2Domain implements Domain {
"""
registry: Registry!
+ """
+ Resolve protocol-level data for this Domain.
+ """
+ resolve(
+ """
+ When true (default), Protocol Acceleration will be conditionally used by the server to perform resolution when it is relevant. If false, Protocol Acceleration will be disabled.
+ @see https://ensnode.io/docs/integrate/omnigraph/protocol-acceleration
+ """
+ accelerate: Boolean = true
+ ): ForwardResolve!
+
"""
Resolver relationship metadata for this Domain.
"""
resolver: DomainResolver!
"""
- All Domains that are direct descendents of this Domain in the namegraph.
+ All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
subdomains(
after: String
@@ -863,7 +1014,7 @@ type ENSv2Registry implements Registry {
contract: AccountId!
"""
- The Domains managed by this Registry.
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
domains(
after: String
@@ -1010,6 +1161,11 @@ type ENSv2RegistryReservation implements Registration {
unregistrant: Account
}
+"""
+Email represents a validated contact email address.
+"""
+scalar Email
+
"""
An Event represents a discrete Log Event that was emitted on an EVM chain, including associated metadata.
"""
@@ -1180,11 +1336,41 @@ input EventsWhereInput {
timestamp: EventsTimestampFilter
}
+"""
+Nested domain resolution container exposing resolved data for the domain.
+"""
+type ForwardResolve {
+ """
+ Whether protocol acceleration was requested and attempted for this resolution.
+ """
+ acceleration: AccelerationStatus!
+
+ """
+ The interpreted profile of an ENS name. Returns null when the name is not resolvable (non-canonical, unnormalized, or no profile records were selected).
+ """
+ profile: DomainProfile
+
+ """
+ Resolved ENS records via the ENS protocol. Null when the name is not resolvable (non-canonical, unnormalized, or no records field was selected).
+ """
+ records: ResolvedRecords
+
+ """
+ Protocol trace tree emitted by resolution, represented as untyped JSON for schema stability. This data model should be expected to experience breaking changes.
+ """
+ trace: JSON
+}
+
"""
Hex represents viem#Hex.
"""
scalar Hex
+"""
+InterfaceId represents an ERC-165 interface id (4-byte hex selector).
+"""
+scalar InterfaceId
+
"""
InterpretedLabel represents an enssdk#InterpretedLabel.
"""
@@ -1195,6 +1381,11 @@ InterpretedName represents an enssdk#InterpretedName.
"""
scalar InterpretedName
+"""
+JSON represents arbitrary JSON-serializable data.
+"""
+scalar JSON
+
"""
Represents a Label within ENS, providing its hash and interpreted representation.
"""
@@ -1218,6 +1409,16 @@ type Label {
interpreted: InterpretedLabel!
}
+"""
+LitecoinAddress represents a Base58Check-encoded Litecoin address (coin type 2).
+"""
+scalar LitecoinAddress
+
+"""
+MonacoinAddress represents a Base58Check-encoded Monacoin address (coin type 22).
+"""
+scalar MonacoinAddress
+
"""
Constructs a reference to a specific Node via one of `name` or `node`.
"""
@@ -1485,6 +1686,196 @@ PermissionsUserId represents an enssdk#PermissionsUserId.
"""
scalar PermissionsUserId
+"""
+Select a primary name lookup target. Exactly one of `coinType` or `chainName` must be provided.
+"""
+input PrimaryNameByInput @oneOf {
+ """
+ A `ChainName` to resolve the primary name for.
+ """
+ chainName: ChainName
+
+ """
+ The ENSIP-9 coin type to resolve the primary name for.
+ """
+ coinType: CoinType
+}
+
+"""
+An ENSIP-19 primary name for an Account on a specific coin type.
+"""
+type PrimaryNameRecord {
+ """
+ The chain corresponding to `coinType`, or null when `coinType` is not represented in `ChainName`.
+ """
+ chainName: ChainName
+
+ """
+ The canonical ENSIP-9 coin type for this primary name lookup.
+ """
+ coinType: CoinType!
+
+ """
+ The validated primary name for this Account on this coin type, or null if none is set.
+ """
+ name: CanonicalName
+
+ """
+ Forward resolve data for this primary name.
+ """
+ resolve: ForwardResolve!
+}
+
+"""
+Filter primary name lookups. Exactly one of `coinTypes` or `chainNames` must be provided.
+"""
+input PrimaryNamesWhereInput @oneOf {
+ """
+ `ChainName` values to resolve primary names for.
+ """
+ chainNames: [ChainName!]
+
+ """
+ Coin types to resolve primary names for.
+ """
+ coinTypes: [CoinType!]
+}
+
+"""
+The interpreted addresses on the profile of an ENS name.
+"""
+type ProfileAddresses {
+ """
+ The interpreted Base address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ base: Address
+
+ """
+ The interpreted Binance Chain (BNB) address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ binance: BinanceAddress
+
+ """
+ The interpreted Bitcoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ bitcoin: BitcoinAddress
+
+ """
+ The interpreted Bitcoin Cash address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ bitcoincash: BitcoinCashAddress
+
+ """
+ The interpreted Dogecoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ dogecoin: DogecoinAddress
+
+ """
+ The interpreted Ethereum address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ ethereum: Address
+
+ """
+ The interpreted Litecoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ litecoin: LitecoinAddress
+
+ """
+ The interpreted Monacoin address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ monacoin: MonacoinAddress
+
+ """
+ The interpreted Ripple (XRP) address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ ripple: RippleAddress
+
+ """
+ The interpreted Rootstock (RBTC) address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ rootstock: RootstockAddress
+
+ """
+ The interpreted Solana address. Returns null when the raw address record is unset, empty (`0x`), all-zero, not valid hex, or cannot be decoded and encoded for this coin type per ENSIP-9.
+ """
+ solana: SolanaAddress
+}
+
+"""
+The interpreted avatar image on the profile of an ENS name.
+"""
+type ProfileAvatar {
+ """
+ HTTP-compatible URL for fetching the avatar image in web browsers. Abstraction over the raw avatar record (IPFS, CAIP NFT references, etc.). Returns null when the raw value is not a direct http(s) URL and no fallback URL can be derived (including when the ENS Metadata Service is unavailable for this namespace). See https://docs.ens.domains/ensip/12.
+ """
+ httpUrl: String
+}
+
+"""
+The interpreted header image on the profile of an ENS name.
+"""
+type ProfileHeader {
+ """
+ HTTP-compatible URL for fetching the header image in web browsers. Abstraction over the raw header record (IPFS, CAIP NFT references, etc.). Returns null when the raw value is not a direct http(s) URL and no fallback URL can be derived (including when the ENS Metadata Service is unavailable for this namespace). See https://docs.ens.domains/ensip/12.
+ """
+ httpUrl: String
+}
+
+"""
+An interpreted social account on the profile of an ENS name.
+"""
+type ProfileSocialAccount {
+ """
+ The handle of the social account.
+ """
+ handle: String!
+
+ """
+ The HTTP-compatible url to the social account.
+ """
+ httpUrl: String!
+}
+
+"""
+The interpreted social accounts on the profile of an ENS name.
+"""
+type ProfileSocials {
+ """
+ The interpreted GitHub account. Returns null when the raw record is unset, empty, or cannot be parsed as a GitHub handle or profile URL.
+ """
+ github: ProfileSocialAccount
+
+ """
+ The interpreted Keybase account. Returns null when the raw record is unset, empty, or cannot be parsed as a Keybase handle or profile URL.
+ """
+ keybase: ProfileSocialAccount
+
+ """
+ The interpreted LinkedIn account. Returns null when the raw record is unset, empty, or cannot be parsed as a LinkedIn handle or profile URL.
+ """
+ linkedin: ProfileSocialAccount
+
+ """
+ The interpreted Telegram account. Returns null when the raw record is unset, empty, or cannot be parsed as a Telegram handle or profile URL.
+ """
+ telegram: ProfileSocialAccount
+
+ """
+ The interpreted X (Twitter) account. Returns null when the raw record is unset, empty, or cannot be parsed as a X (Twitter) handle or profile URL.
+ """
+ twitter: ProfileSocialAccount
+}
+
+"""
+The interpreted website on the profile of an ENS name.
+"""
+type ProfileWebsite {
+ """
+ The HTTP-compatible website URL. Returns null when the raw url record is unset, empty, not an http(s) URL, or cannot be parsed as a valid URL.
+ """
+ httpUrl: String
+}
+
type Query {
"""
Identify an Account by ID or Address.
@@ -1497,7 +1888,7 @@ type Query {
domain(by: DomainIdInput!): Domain
"""
- Find Canonical Domains by Name.
+ Find Canonical Domains by Name. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
domains(
after: String
@@ -1631,7 +2022,7 @@ interface Registry {
contract: AccountId!
"""
- The Domains managed by this Registry.
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value (no Registration, or a never-expiring Registration treated as +∞) sort last when `dir: ASC` and first when `dir: DESC`.
"""
domains(
after: String
@@ -1770,6 +2161,130 @@ RenewalId represents an enssdk#RenewalId.
"""
scalar RenewalId
+"""
+A resolved ABI record for an ENS name.
+"""
+type ResolvedAbiRecord {
+ contentType: BigInt!
+ data: Hex!
+}
+
+"""
+A resolved address record for an ENS name.
+"""
+type ResolvedAddressRecord {
+ """
+ The "raw" resolved address record as hex, or null if not set, empty ("0x"), or zeroAddress. Decode with ENSIP-9 (https://docs.ens.domains/ensip/9) and address-encoder (https://github.com/ensdomains/address-encoder) for the associated coin type. Guaranteed to be at least one byte of hex data. There is no guarantee that an EVM CoinType returns an address value of any particular byte length.
+ """
+ address: Hex
+
+ """
+ The coin type for this address record.
+ """
+ coinType: CoinType!
+}
+
+"""
+A resolved ERC-165 interface implementer record for an ENS name.
+"""
+type ResolvedInterfaceRecord {
+ implementer: Address
+ interfaceId: InterfaceId!
+}
+
+"""
+A resolved PubkeyResolver (x, y) pair for an ENS name.
+"""
+type ResolvedPubkeyRecord {
+ x: Hex!
+ y: Hex!
+}
+
+"""
+A resolved 'raw' text record for an ENS name. Value is any possible string and may require additional validation or preprocessing before use.
+"""
+type ResolvedRawTextRecord {
+ """
+ The text record key.
+ """
+ key: String!
+
+ """
+ The 'raw' text record value, or null if not set. Value is any possible string and may require additional validation or preprocessing before use.
+ """
+ value: String
+}
+
+"""
+Records resolved for a specific ENS name via the ENS protocol.
+"""
+type ResolvedRecords {
+ """
+ The first stored ABI matching the requested content-type bitmask, or null if not set.
+ """
+ abi(
+ """
+ Content-type bitmask; the resolver returns the first stored ABI whose bit is set (lowest bit first).
+ """
+ contentTypeMask: BigInt!
+ ): ResolvedAbiRecord
+
+ """
+ Resolved address records for the requested coin types.
+ """
+ addresses(
+ """
+ Coin types to resolve (e.g. `60` for ETH).
+ """
+ coinTypes: [CoinType!]!
+ ): [ResolvedAddressRecord!]!
+
+ """
+ The ENSIP-7 contenthash record raw bytes, or null if not set.
+ """
+ contenthash: Hex
+
+ """
+ The IDNSZoneResolver zonehash raw bytes, or null if not set.
+ """
+ dnszonehash: Hex
+
+ """
+ Resolved ERC-165 interface implementer records for the requested ids.
+ """
+ interfaces(
+ """
+ ERC-165 interface ids to resolve (4-byte hex selectors).
+ """
+ ids: [InterfaceId!]!
+ ): [ResolvedInterfaceRecord!]!
+
+ """
+ The PubkeyResolver (x, y) pair, or null if not set.
+ """
+ pubkey: ResolvedPubkeyRecord
+
+ """
+ The `name` record value used in Reverse Resolution (ENSIP-19), or null if not set. To reduce a common point of developer confusion the Omnigraph API represents this as the `reverseName` rather than the `name` record which is what this field actually resolves to onchain.
+ """
+ reverseName: String
+
+ """
+ Resolved text records for the requested keys.
+ """
+ texts(
+ """
+ Text record keys to resolve (e.g. `avatar`, `description`).
+ """
+ keys: [String!]!
+ ): [ResolvedRawTextRecord!]!
+
+ """
+ The IVersionableResolver version, or null if not set or unavailable.
+ """
+ version: BigInt
+}
+
"""
A Resolver represents a Resolver contract on-chain.
"""
@@ -1913,6 +2428,56 @@ ResolverRecordsId represents an enssdk#ResolverRecordsId.
"""
scalar ResolverRecordsId
+"""
+Nested account resolution container exposing primary name resolution.
+"""
+type ReverseResolve {
+ """
+ Whether protocol acceleration was requested and attempted for this reverse resolution.
+ """
+ acceleration: AccelerationStatus!
+
+ """
+ The primary name for this Account on a specific coin type or chain name.
+ """
+ primaryName(
+ """
+ Select a coin type or chain name to resolve a primary name for.
+ """
+ by: PrimaryNameByInput!
+ ): PrimaryNameRecord!
+
+ """
+ Primary names for this Account on the requested coin types or chain names.
+ """
+ primaryNames(
+ """
+ Select coin types or chain names to resolve primary names for.
+ """
+ where: PrimaryNamesWhereInput!
+ ): [PrimaryNameRecord!]!
+
+ """
+ Protocol trace tree emitted by reverse resolution, represented as JSON for schema stability. This data model should be expected to experience breaking changes.
+ """
+ trace: JSON!
+}
+
+"""
+RippleAddress represents a Base58Check-encoded Ripple (XRP) address (coin type 144).
+"""
+scalar RippleAddress
+
+"""
+RootstockAddress represents an EIP-55 checksummed Rootstock (RBTC) address (coin type 137).
+"""
+scalar RootstockAddress
+
+"""
+SolanaAddress represents a Base58-encoded Solana address (coin type 501).
+"""
+scalar SolanaAddress
+
"""
Filter for Domain.subdomains query.
"""
@@ -1993,7 +2558,7 @@ type WrappedBaseRegistrarRegistration {
fuses: Int!
"""
- The TokenID for this Domain in the NameWrapper.
+ The TokenId for this Domain in the NameWrapper. This is the bigint encoding of the Domain's Node.
"""
tokenId: BigInt!
}
diff --git a/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json b/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json
index d30f06ad8a..5cf00d34d4 100644
--- a/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json
+++ b/docs/ensnode.io/src/data/omnigraph-examples/snapshot.json
@@ -1,8 +1,8 @@
{
"version": "v1.15.1",
- "commit": "c9ceadd18",
+ "commit": "100dbced1",
"sdkVersion": "1.15.1",
"schemaTag": "v1.15.1",
"endpoint": "https://api.v2-sepolia.ensnode.io",
- "snapshottedAt": "2026-05-25"
+ "snapshottedAt": "2026-06-04"
}
diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.ts
index af50232041..f0b2e60f83 100644
--- a/packages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.ts
+++ b/packages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.ts
@@ -259,6 +259,18 @@ export const relations_registry = relations(registry, ({ one, many }) => ({
// Domains
///////////
+/**
+ * Sentinel "+∞" sort value materialized into `Domain.__latestRegistrationStart` /
+ * `__latestRegistrationExpiry` when the value is absent — a Domain with no Registration, or a
+ * Registration that never expires. Holding those sort columns NOT NULL (absent → this sentinel)
+ * lets REGISTRATION_*-ordered find-domains queries use a plain `(registry_id, col, id)` composite
+ * index in both directions with a plain keyset tuple, with no NULL-placement special casing.
+ *
+ * @dev uint256 max — larger than every real timestamp/expiry, including the uint64-max ENSv2
+ * "never expires" expiry. It sorts last for ASC ("oldest"/"expiring soonest") and first for DESC.
+ */
+export const REGISTRATION_SORT_SENTINEL = 2n ** 256n - 1n;
+
export const domainType = onchainEnum("DomainType", ["ENSv1Domain", "ENSv2Domain"]);
/**
@@ -375,6 +387,36 @@ export const domain = onchainTable(
*/
canonicalNode: t.hex().$type(),
+ /**
+ * Materialized `start` of this Domain's latest Registration, or {@link REGISTRATION_SORT_SENTINEL}
+ * when the Domain has no Registration. Mirror of the latest `registration.start` (see
+ * `latestRegistrationIndex`), maintained inline by `registration-db-helpers.ts`.
+ *
+ * @dev Exists purely so REGISTRATION_TIMESTAMP-ordered find-domains queries can use the
+ * `(registry_id, __latest_registration_start, id)` composite index instead of joining through
+ * `latest_registration_indexes` → `registrations` and sorting. Held NOT NULL (absent → sentinel)
+ * so the keyset stays a plain tuple compare with no NULL-placement special casing; see
+ * find-domains-resolver-helpers.ts. Double-underscore prefix marks it as an internal
+ * materialized mirror, not part of the on-chain protocol surface; the canonical (possibly null)
+ * value lives on the Registration entity.
+ */
+ __latestRegistrationStart: t
+ .bigint("__latest_registration_start")
+ .notNull()
+ .default(REGISTRATION_SORT_SENTINEL),
+
+ /**
+ * Materialized `expiry` of this Domain's latest Registration, or {@link REGISTRATION_SORT_SENTINEL}
+ * when the Domain has no Registration or its latest Registration never expires (effectively +∞).
+ * Mirror of the latest `registration.expiry`, maintained inline by `registration-db-helpers.ts`.
+ *
+ * @dev See `__latestRegistrationStart`. Backs REGISTRATION_EXPIRY-ordered queries.
+ */
+ __latestRegistrationExpiry: t
+ .bigint("__latest_registration_expiry")
+ .notNull()
+ .default(REGISTRATION_SORT_SENTINEL),
+
// NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin
}),
(t) => ({
@@ -405,6 +447,24 @@ export const domain = onchainTable(
byCanonicalNode: index().using("hash", t.canonicalNode),
// btree for ORDER BY canonical_depth (typeahead and DEPTH-ordered browse)
byCanonicalDepth: index().on(t.canonicalDepth),
+
+ // Composites for `WHERE registry_id = X ORDER BY LIMIT N`
+ // (REGISTRATION_TIMESTAMP / REGISTRATION_EXPIRY ordering in find-domains queries). The latest
+ // registration's start/expiry is mirrored onto the Domain row (see `__latestRegistration*`) so
+ // the order is an index-ordered scan, not a join through `latest_registration_indexes` →
+ // `registrations` followed by a sort. The columns are NOT NULL (absent → `REGISTRATION_SORT_SENTINEL`),
+ // so a single plain composite per column serves both ASC and DESC (forward / backward scan) with
+ // a plain keyset tuple — see find-domains-resolver-helpers.ts.
+ byRegistryAndLatestRegistrationStart: index().on(
+ t.registryId,
+ t.__latestRegistrationStart,
+ t.id,
+ ),
+ byRegistryAndLatestRegistrationExpiry: index().on(
+ t.registryId,
+ t.__latestRegistrationExpiry,
+ t.id,
+ ),
}),
);
diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts
index 7e4dbfe747..5518f38f32 100644
--- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts
+++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts
@@ -249,6 +249,27 @@ query DomainSubdomains($name: InterpretedName!) {
variables: { default: { name: "eth" } },
},
+ ////////////////////////////////////
+ // Most Recently Registered Subdomains
+ ////////////////////////////////////
+ {
+ id: "domain-subdomains-recently-registered",
+ query: `
+query RecentlyRegisteredSubdomains($name: InterpretedName!) {
+ domain(by: {name: $name}) {
+ canonical { name { interpreted beautified } }
+ subdomains(first: 10, order: {by: REGISTRATION_TIMESTAMP, dir: DESC}) {
+ edges {
+ node {
+ canonical { name { interpreted beautified } }
+ }
+ }
+ }
+ }
+}`,
+ variables: { default: { name: "eth" } },
+ },
+
////////////////////////
// Subdomains Pagination
////////////////////////
diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql
index 7d907ed9ae..58f5bb9fc1 100644
--- a/packages/enssdk/src/omnigraph/generated/schema.graphql
+++ b/packages/enssdk/src/omnigraph/generated/schema.graphql
@@ -12,7 +12,9 @@ type Account {
"""An EVM Address that uniquely identifies this Account on-chain."""
address: Address!
- """The Domains that are owned by the Account."""
+ """
+ The Domains that are owned by the Account. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
+ """
domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: AccountDomainsWhereInput): AccountDomainsConnection
"""
@@ -343,7 +345,7 @@ interface Domain {
resolver: DomainResolver!
"""
- All Domains that are direct descendants of this Domain in the namegraph.
+ All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
"""
subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection
@@ -495,11 +497,24 @@ input DomainsNameFilter @oneOf {
starts_with: String
}
-"""Fields by which domains can be ordered"""
+"""Fields by which domains can be ordered."""
enum DomainsOrderBy {
+ """
+ Order by Canonical Name depth (number of labels); e.g. `eth` < `vitalik.eth`.
+ """
DEPTH
+
+ """Order by the Domain's Canonical Name, alphabetically."""
NAME
+
+ """
+ Order by the expiry of the Domain's latest Registration. A Domain that never expires (or has no Registration) is treated as +∞: it sorts last when `dir: ASC` (“expiring soonest first”) and first when `dir: DESC` (“expiring latest first”).
+ """
REGISTRATION_EXPIRY
+
+ """
+ Order by the start time of the Domain's latest Registration. A Domain with no Registration has no timestamp and sorts last when `dir: ASC` (“oldest registered first”) and first when `dir: DESC` (“most recently registered first”).
+ """
REGISTRATION_TIMESTAMP
}
@@ -584,7 +599,7 @@ type ENSv1Domain implements Domain {
rootRegistryOwner: Account
"""
- All Domains that are direct descendants of this Domain in the namegraph.
+ All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
"""
subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection
@@ -604,7 +619,9 @@ type ENSv1Registry implements Registry {
"""
contract: AccountId!
- """The Domains managed by this Registry."""
+ """
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
+ """
domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection
"""A unique reference to this Registry."""
@@ -629,7 +646,9 @@ type ENSv1VirtualRegistry implements Registry {
"""
contract: AccountId!
- """The Domains managed by this Registry."""
+ """
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
+ """
domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection
"""A unique reference to this Registry."""
@@ -700,7 +719,7 @@ type ENSv2Domain implements Domain {
resolver: DomainResolver!
"""
- All Domains that are direct descendants of this Domain in the namegraph.
+ All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
"""
subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection
@@ -732,7 +751,9 @@ type ENSv2Registry implements Registry {
"""
contract: AccountId!
- """The Domains managed by this Registry."""
+ """
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
+ """
domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection
"""A unique reference to this Registry."""
@@ -1399,7 +1420,9 @@ type Query {
"""Identify a Domain by Name or DomainId"""
domain(by: DomainIdInput!): Domain
- """Find Canonical Domains by Name."""
+ """
+ Find Canonical Domains by Name. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
+ """
domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: DomainsWhereInput!): QueryDomainsConnection
"""Identify Permissions by ID or AccountId."""
@@ -1502,7 +1525,9 @@ interface Registry {
"""
contract: AccountId!
- """The Domains managed by this Registry."""
+ """
+ The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
+ """
domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection
"""A unique reference to this Registry."""
diff --git a/packages/ensskills/skills/omnigraph/SKILL.md b/packages/ensskills/skills/omnigraph/SKILL.md
index b7dc798f6d..3fe5b0ad72 100644
--- a/packages/ensskills/skills/omnigraph/SKILL.md
+++ b/packages/ensskills/skills/omnigraph/SKILL.md
@@ -74,7 +74,7 @@ If a question genuinely isn't expressible in the Omnigraph schema, the underlyin
- account(by: AccountByInput!): Account — Identify an Account by ID or Address.
- domain(by: DomainIdInput!): Domain — Identify a Domain by Name or DomainId
-- domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: DomainsWhereInput!): QueryDomainsConnection — Find Canonical Domains by Name.
+- domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: DomainsWhereInput!): QueryDomainsConnection — Find Canonical Domains by Name. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
- permissions(by: PermissionsIdInput!): Permissions — Identify Permissions by ID or AccountId.
- registry(by: RegistryIdInput!): Registry — Identify a Registry by ID or AccountId. If querying by `contract`, only concrete Registries will be returned.
- resolver(by: ResolverIdInput!): Resolver — Identify a Resolver by ID or AccountId.
@@ -97,7 +97,7 @@ _Represents a Domain, i.e. an individual Label within the ENS namegraph. It may
- registry: Registry! — The Registry under which this Domain exists.
- resolve(accelerate: Boolean): ForwardResolve! — Resolve protocol-level data for this Domain.
- resolver: DomainResolver! — Resolver relationship metadata for this Domain.
-- subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection — All Domains that are direct descendants of this Domain in the namegraph.
+- subdomains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: SubdomainsWhereInput): DomainSubdomainsConnection — All Domains that are direct descendants of this Domain in the namegraph. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
- subregistry: Registry — The Registry this Domain declares as its Subregistry, if exists.
#### DomainCanonical
@@ -114,7 +114,7 @@ _Canonicality metadata for a Domain, including its name, depth, path, and node (
_Represents an individual Account, keyed by its Address._
- address: Address! — An EVM Address that uniquely identifies this Account on-chain.
-- domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: AccountDomainsWhereInput): AccountDomainsConnection — The Domains that are owned by the Account.
+- domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: AccountDomainsWhereInput): AccountDomainsConnection — The Domains that are owned by the Account. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
- events(after: String, before: String, first: Int, last: Int, where: AccountEventsWhereInput): AccountEventsConnection — All Events for which this Account is the HCA-aware `sender` (i.e. `Event.sender`).
- id: Address! — A unique reference to this Account.
- permissions(after: String, before: String, first: Int, last: Int, where: AccountPermissionsWhereInput): AccountPermissionsConnection — The Permissions granted to this Account, optionally filtered to Permissions in a specific contract.
@@ -146,7 +146,7 @@ _A Registry represents a Registry contract in the ENS namegraph. It may be an EN
- canonical: Boolean! — Whether the Registry is Canonical.
- contract: AccountId! — Contract metadata for this Registry. If this is an ENSv1VirtualRegistry, this will reference the concrete Registry contract under which the parent Domain exists.
-- domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection — The Domains managed by this Registry.
+- domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: RegistryDomainsWhereInput): RegistryDomainsConnection — The Domains managed by this Registry. Ordered by the `order` argument (default: NAME, ASC). When ordering by REGISTRATION_TIMESTAMP or REGISTRATION_EXPIRY, Domains lacking that value — no Registration for REGISTRATION_TIMESTAMP; no Registration or a never-expiring one (treated as +∞) for REGISTRATION_EXPIRY — sort last when `dir: ASC` and first when `dir: DESC`.
- id: RegistryId! — A unique reference to this Registry.
- parents(after: String, before: String, first: Int, last: Int): RegistryParentsConnection — The Domains for which this Registry is a Subregistry.
- permissions: Permissions — The Permissions managed by this Registry.
@@ -444,6 +444,41 @@ Variables:
}
```
+### domain-subdomains-recently-registered
+
+```graphql
+query RecentlyRegisteredSubdomains($name: InterpretedName!) {
+ domain(by: { name: $name }) {
+ canonical {
+ name {
+ interpreted
+ beautified
+ }
+ }
+ subdomains(first: 10, order: { by: REGISTRATION_TIMESTAMP, dir: DESC }) {
+ edges {
+ node {
+ canonical {
+ name {
+ interpreted
+ beautified
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "name": "eth"
+}
+```
+
### subdomains-pagination
```graphql