Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions .changeset/fine-keys-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": minor
---

ENSv2GraphQL API: Introduce `Domain.subdomainCount`.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import {
} from "@ensnode/ensnode-sdk";

import { db } from "@/lib/db";
import { makeLogger } from "@/lib/logger";

const ROOT_REGISTRY_ID = getENSv2RootRegistryId(config.namespace);

const logger = makeLogger("get-domain-by-interpreted-name");
const v1Logger = makeLogger("get-domain-by-interpreted-name:v1");
const v2Logger = makeLogger("get-domain-by-interpreted-name:v2");

/**
* Gets the DomainId of the Domain addressed by `name`.
*/
Expand All @@ -28,30 +33,36 @@ export async function getDomainIdByInterpretedName(
): Promise<DomainId | null> {
// Domains addressable in v2 are preferred, but v1 lookups are cheap, so just do them both ahead of time
const [v1DomainId, v2DomainId] = await Promise.all([
v1_getDomainIdByFqdn(name),
v2_getDomainIdByFqdn(ROOT_REGISTRY_ID, name),
v1_getDomainIdByInterpretedName(name),
v2_getDomainIdByInterpretedName(ROOT_REGISTRY_ID, name),
]);

logger.debug({ v1DomainId, v2DomainId });

// prefer v2Domain over v1Domain
return v2DomainId || v1DomainId || null;
}

/**
* Retrieves the ENSv1DomainId for the provided `name`, if exists.
*/
async function v1_getDomainIdByFqdn(name: InterpretedName): Promise<DomainId | null> {
async function v1_getDomainIdByInterpretedName(name: InterpretedName): Promise<DomainId | null> {
const node = namehash(name);
const domainId = makeENSv1DomainId(node);

const domain = await db.query.v1Domain.findFirst({ where: (t, { eq }) => eq(t.id, domainId) });
return domain?.id ?? null;
const exists = domain !== undefined;

v1Logger.debug({ node, exists });

return exists ? domainId : null;
}

/**
* Forward-traverses the ENSv2 namegraph from the specified root in order to identify the Domain
* addressed by `name`.
*/
async function v2_getDomainIdByFqdn(
async function v2_getDomainIdByInterpretedName(
rootRegistryId: RegistryId,
name: InterpretedName,
): Promise<DomainId | null> {
Expand Down Expand Up @@ -100,13 +111,19 @@ async function v2_getDomainIdByFqdn(
}[];

// this was a query for a TLD and it does not exist within the ENSv2 namegraph
if (rows.length === 0) return null;
if (rows.length === 0) {
v2Logger.debug({ labelHashPath, rows });
return null;
}
Comment thread
shrugs marked this conversation as resolved.

// biome-ignore lint/style/noNonNullAssertion: length check above
const leaf = rows[rows.length - 1]!;

// the v2Domain was found iff there is an exact match within the ENSv2 namegraph
const exact = rows.length === labelHashPath.length;

v2Logger.debug({ labelHashPath, rows, exact });

Comment thread
shrugs marked this conversation as resolved.
if (exact) return leaf.domain_id;

// otherwise, the v2 domain was not found
Expand Down
55 changes: 40 additions & 15 deletions apps/ensapi/src/graphql-api/schema/domain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type ResolveCursorConnectionArgs, resolveCursorConnection } from "@pothos/plugin-relay";
import { eq } from "drizzle-orm";

import * as schema from "@ensnode/ensnode-schema";
import {
type DomainId,
type ENSv1DomainId,
Expand All @@ -17,6 +19,7 @@ import { rejectAnyErrors } from "@/graphql-api/lib/reject-any-errors";
import { AccountRef } from "@/graphql-api/schema/account";
import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants";
import { cursors } from "@/graphql-api/schema/cursors";
import { LabelRef } from "@/graphql-api/schema/label";
import { OrderDirection } from "@/graphql-api/schema/order-direction";
import { RegistrationInterfaceRef } from "@/graphql-api/schema/registration";
import { RegistryRef } from "@/graphql-api/schema/registry";
Expand Down Expand Up @@ -79,33 +82,35 @@ export type Domain = Exclude<typeof DomainInterfaceRef.$inferType, DomainId>;
// DomainInterface Implementation
//////////////////////////////////
DomainInterfaceRef.implement({
description: "a Domain",
description:
"A Domain represents an individual Label within the ENS namegraph. It may or may not be Canonical. It may be an ENSv1Domain or an ENSv2Domain.",
fields: (t) => ({
//////////////////////
/////////////
// Domain.id
//////////////////////
/////////////
id: t.field({
description: "TODO",
type: "DomainId",
nullable: false,
resolve: (parent) => parent.id,
}),

//////////////////////
////////////////
// Domain.label
//////////////////////
////////////////
label: t.field({
type: "String",
description: "TODO",
type: LabelRef,
description: "The Label this Domain represents in the ENS Namegraph",
nullable: false,
resolve: ({ label }) => label.interpreted,
resolve: (parent) => parent.label,
}),
Comment thread
shrugs marked this conversation as resolved.

///////////////
// Domain.name
///////////////
name: t.field({
description: "TODO",
description:
"The Canonical Name for this Domain. If the Domain is not Canonical, then `name` will be null.",
type: "Name",
nullable: true,
resolve: async (domain, args, context) => {
Expand Down Expand Up @@ -154,19 +159,19 @@ DomainInterfaceRef.implement({
},
}),

//////////////////////
////////////////
// Domain.owner
//////////////////////
////////////////
owner: t.field({
type: AccountRef,
description: "TODO",
nullable: true,
resolve: (parent) => parent.ownerId,
}),

//////////////////////
///////////////////
// Domain.resolver
//////////////////////
///////////////////
resolver: t.field({
description: "TODO",
type: ResolverRef,
Expand All @@ -178,7 +183,8 @@ DomainInterfaceRef.implement({
// Domain.registration
///////////////////////
registration: t.field({
description: "TODO",
description:
"The latest Registration for this Domain. If the Domain doesn't have an associated Registration, then `registration` will be null.",
type: RegistrationInterfaceRef,
nullable: true,
resolve: (parent) => getLatestRegistration(parent.id),
Expand All @@ -188,7 +194,7 @@ DomainInterfaceRef.implement({
// Domain.registrations
////////////////////////
registrations: t.connection({
description: "TODO",
description: "All Registrations for a Domain, including the latest Registration.",
type: RegistrationInterfaceRef,
resolve: (parent, args, context) =>
resolveCursorConnection(
Expand All @@ -207,6 +213,25 @@ DomainInterfaceRef.implement({
),
}),

/////////////////////////
// Domain.subdomainCount
/////////////////////////
subdomainCount: t.field({
description: "TODO",
type: "Int",
nullable: false,
resolve: async (parent) => {
Comment thread
shrugs marked this conversation as resolved.
if (isENSv1Domain(parent)) {
return db.$count(schema.v1Domain, eq(schema.v1Domain.parentId, parent.id));
} else {
const { subregistryId } = parent;
if (subregistryId === null) return 0;

return db.$count(schema.v2Domain, eq(schema.v2Domain.registryId, subregistryId));
}
Comment thread
shrugs marked this conversation as resolved.
},
}),

/////////////////////
// Domain.subdomains
/////////////////////
Expand Down
38 changes: 24 additions & 14 deletions apps/ensapi/src/graphql-api/schema/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ EventRef.implement({
}),

///////////////////
// Event.address
// Event.blockHash
///////////////////
address: t.field({
blockHash: t.field({
description: "TODO",
type: "Address",
type: "Hex",
nullable: false,
resolve: (parent) => parent.address,
resolve: (parent) => parent.blockHash,
}),

///////////////////
Expand All @@ -60,16 +60,6 @@ EventRef.implement({
resolve: (parent) => parent.timestamp,
}),

///////////////////
// Event.blockHash
///////////////////
blockHash: t.field({
description: "TODO",
type: "Hex",
nullable: false,
resolve: (parent) => parent.blockHash,
}),

/////////////////////////
// Event.transactionHash
/////////////////////////
Expand All @@ -80,6 +70,26 @@ EventRef.implement({
resolve: (parent) => parent.transactionHash,
}),

//////////////
// Event.from
//////////////
from: t.field({
description: "TODO",
type: "Address",
nullable: false,
resolve: (parent) => parent.from,
}),

///////////////////
// Event.address
///////////////////
address: t.field({
description: "TODO",
type: "Address",
nullable: false,
resolve: (parent) => parent.address,
}),

//////////////////
// Event.logIndex
//////////////////
Expand Down
31 changes: 31 additions & 0 deletions apps/ensapi/src/graphql-api/schema/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type * as schema from "@ensnode/ensnode-schema";

import { builder } from "@/graphql-api/builder";

export const LabelRef = builder.objectRef<typeof schema.label.$inferSelect>("LabelRef");
Comment thread
shrugs marked this conversation as resolved.
Outdated
LabelRef.implement({
description: "Represents a Label within ENS, providing its hash and interpreted representation.",
fields: (t) => ({
//////////////
// Label.hash
//////////////
hash: t.field({
description:
"The Label's LabelHash\n(@see https://ensnode.io/docs/reference/terminology/#labels-labelhashes-labelhash-function)",
type: "Hex",
nullable: false,
resolve: (parent) => parent.labelHash,
}),

/////////////////////
// Label.interpreted
/////////////////////
interpreted: t.field({
description:
"The Label represented as an Interpreted Label. This is either a normalized Literal Label or an Encoded LabelHash. \n(@see https://ensnode.io/docs/reference/terminology/#interpreted-label)",
type: "String",
nullable: false,
resolve: (parent) => parent.interpreted,
}),
}),
});
2 changes: 1 addition & 1 deletion apps/ensapi/src/graphql-api/schema/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

import { builder } from "@/graphql-api/builder";
import { resolveFindDomains } from "@/graphql-api/lib/find-domains/find-domains-resolver";
import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-fqdn";
import { getDomainIdByInterpretedName } from "@/graphql-api/lib/get-domain-by-interpreted-name";
import { AccountRef } from "@/graphql-api/schema/account";
import { AccountIdInput } from "@/graphql-api/schema/account-id";
import { DEFAULT_CONNECTION_ARGS } from "@/graphql-api/schema/constants";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ import type { LogEvent } from "@/lib/ponder-helpers";
export async function ensureEvent(context: Context, event: LogEvent) {
await context.db.insert(schema.event).values({
id: event.id,
// chain
chainId: context.chain.id,
address: event.log.address,

// block
blockHash: event.block.hash,
timestamp: event.block.timestamp,

// transaction
transactionHash: event.transaction.hash,
from: event.transaction.from,

// log
address: event.log.address,
logIndex: event.log.logIndex,
});

return event.id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {

import { ensureAccount } from "@/lib/ensv2/account-db-helpers";
import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-log-db-helpers";
import {
getLatestRegistration,
insertLatestRegistration,
Expand All @@ -34,7 +34,7 @@ const pluginName = PluginName.ENSv2;
* ENSv1 Registry). The .eth Registrar doesn't do this, but Basenames and Lineanames do.
*
* Because they all technically have this ability, this logic avoids the invariant that an associated
* v1Domain must exist and the v1Domain.owner is conditionally materialized.
* v1Domain must exist and instead the v1Domain.owner is _conditionally_ materialized.
*
* Technically each BaseRegistrar Registration also has an associated owner that we could keep track
* of, but because we're materializing the v1Domain's effective owner, we need not explicitly track
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {

import { ensureAccount } from "@/lib/ensv2/account-db-helpers";
import { materializeENSv1DomainEffectiveOwner } from "@/lib/ensv2/domain-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-log-db-helpers";
import { ensureLabel } from "@/lib/ensv2/label-db-helpers";
import {
getLatestRegistration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "@ensnode/ensnode-sdk";

import { ensureAccount } from "@/lib/ensv2/account-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-log-db-helpers";
import { ensureLabel } from "@/lib/ensv2/label-db-helpers";
import {
getLatestRegistration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "@ensnode/ensnode-sdk";

import { ensureAccount } from "@/lib/ensv2/account-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-db-helpers";
import { ensureEvent } from "@/lib/ensv2/event-log-db-helpers";
import { getLatestRegistration, insertLatestRenewal } from "@/lib/ensv2/registration-db-helpers";
import { getThisAccountId } from "@/lib/get-this-account-id";
import { toJson } from "@/lib/json-stringify-with-bigints";
Expand Down
Loading