Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
eae25f6
add records resolution
sevenzing Apr 21, 2026
bbf391c
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 19, 2026
e8653a4
small docker fixes
sevenzing May 20, 2026
6ff2930
add graphql styled records selection
sevenzing May 20, 2026
713f1ee
add primary names field in omnigraph
sevenzing May 20, 2026
319a99d
forgot introspection
sevenzing May 20, 2026
32f53c8
remove default chain id and add disableAcceleration
sevenzing May 20, 2026
09b1435
fix docs
sevenzing May 20, 2026
7668d50
fix tests a little bit
sevenzing May 20, 2026
e1e5680
refactor
sevenzing May 20, 2026
2e00efe
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 20, 2026
c88dde2
fix tests
sevenzing May 20, 2026
380dadf
forgot changeset
sevenzing May 20, 2026
4fef571
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 20, 2026
cf7cf01
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 20, 2026
b1232ab
fix PR suggestions
sevenzing May 21, 2026
bf275b9
fix the cast problem
sevenzing May 21, 2026
cbdca11
remove seed-cli
sevenzing May 21, 2026
8acc0b2
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 21, 2026
60d2793
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 22, 2026
0550de5
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 26, 2026
7c80a61
update resolution api
sevenzing May 27, 2026
caa8f76
fix PR comments
sevenzing May 27, 2026
ab2b849
lint + generate
sevenzing May 27, 2026
5b0b1d2
default chain id
sevenzing May 27, 2026
4e94c06
add resolve { } object and trace to resolve { }
sevenzing May 28, 2026
f5bacdd
self review
sevenzing May 28, 2026
c5330f0
fix for greptile review
sevenzing May 28, 2026
6f50a9d
add EMBEDDED_DATA for AccelerationStatus
sevenzing May 28, 2026
a0008ea
self review again
sevenzing May 28, 2026
c5d7818
fix no selection bug
sevenzing May 28, 2026
e0d7f4d
rename constants
sevenzing May 29, 2026
fdd1b72
fix for comments on PR review from @shrugs
sevenzing May 29, 2026
c0bbdb8
apply more PR comment fixes
sevenzing May 30, 2026
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
1 change: 1 addition & 0 deletions apps/ensapi/src/omnigraph-api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import "./schema/permissions";
import "./schema/query";
import "./schema/registry";
import "./schema/renewal";
import "./schema/resolution";
import "./schema/resolver-records";
import "./schema/scalars";

Expand Down
48 changes: 48 additions & 0 deletions apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { InterpretedLabel, InterpretedName } from "enssdk";
import { beforeAll, describe, expect, it } from "vitest";

import { DEVNET_OWNER } from "@ensnode/ensnode-sdk/internal";

import { DEVNET_ETH_LABELS } from "@/test/integration/devnet-names";
import {
DomainSubdomainsPaginated,
Expand Down Expand Up @@ -252,3 +254,49 @@ describe("Domain.events filtering (EventsWhereInput)", () => {
expect(events.length).toBe(0);
});
});

describe("Domain.records", () => {
Comment thread
sevenzing marked this conversation as resolved.
type DomainRecordsResult = {
domain: {
records: {
addresses: Array<{ coinType: number; address: string | null }>;
Comment thread
sevenzing marked this conversation as resolved.
Outdated
texts: Array<{ key: string; value: string | null }>;
} | null;
};
};

const DomainRecords = gql`
query DomainRecords($name: InterpretedName!, $addresses: [CoinType!], $texts: [String!]) {
domain(by: { name: $name }) {
records(selection: { addresses: $addresses, texts: $texts }) {
Comment thread
sevenzing marked this conversation as resolved.
Outdated
addresses { coinType address }
texts { key value }
}
}
}
`;

it("resolves ETH address for test.eth", async () => {
const result = await request<DomainRecordsResult>(DomainRecords, {
name: "test.eth",
addresses: [60],
texts: [],
});

expect(result.domain.records?.addresses).toEqual([{ coinType: 60, address: DEVNET_OWNER }]);
Comment thread
sevenzing marked this conversation as resolved.
Outdated
expect(result.domain.records?.texts).toEqual([]);
});

it("resolves address and text records for example.eth", async () => {
const result = await request<DomainRecordsResult>(DomainRecords, {
name: "example.eth",
addresses: [60],
texts: ["description"],
});

expect(result.domain.records?.addresses).toEqual([{ coinType: 60, address: DEVNET_OWNER }]);
expect(result.domain.records?.texts).toEqual([{ key: "description", value: "example.eth" }]);
});


});
97 changes: 72 additions & 25 deletions apps/ensapi/src/omnigraph-api/schema/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";
import { withSpanAsync } from "@/lib/instrumentation/auto-span";
import { builder } from "@/omnigraph-api/builder";
import type { context as graphqlContext } from "@/omnigraph-api/context";
import {
orderPaginationBy,
paginateBy,
Expand Down Expand Up @@ -40,8 +41,12 @@ import { EventRef, EventsWhereInput } from "@/omnigraph-api/schema/event";
import { LabelRef } from "@/omnigraph-api/schema/label";
import { OrderDirection } from "@/omnigraph-api/schema/order-direction";
import { PermissionsUserRef } from "@/omnigraph-api/schema/permissions";
import type { ResolverRecordsResponseBase } from "@ensnode/ensnode-sdk";
import { resolveForward } from "@/lib/resolution/forward-resolution";
import { runWithTrace } from "@/lib/tracing/tracing-api";
import { RegistrationInterfaceRef } from "@/omnigraph-api/schema/registration";
import { RegistryRef } from "@/omnigraph-api/schema/registry";
import { ResolvedRecordsRef, ResolveSelectionInput } from "@/omnigraph-api/schema/resolution";
import { ResolverRef } from "@/omnigraph-api/schema/resolver";

const tracer = trace.getTracer("schema/Domain");
Expand Down Expand Up @@ -101,6 +106,37 @@ export type ENSv1Domain = Exclude<typeof ENSv1DomainRef.$inferType, ENSv1DomainI
export type ENSv2Domain = Exclude<typeof ENSv2DomainRef.$inferType, ENSv2DomainId>;
export type Domain = Exclude<typeof DomainInterfaceRef.$inferType, DomainId>;

/**
* Returns the canonical interpreted name for a domain, or null if the domain is not canonical.
* Reuses the canonical path DataLoaders so repeated calls within a request are batched/cached.
*/
async function getDomainInterpretedName(
domain: Domain,
context: ReturnType<typeof graphqlContext>,
): Promise<ReturnType<typeof interpretedLabelsToInterpretedName> | null> {
const canonicalPath = isENSv1Domain(domain)
? await context.loaders.v1CanonicalPath.load(domain.id)
: await context.loaders.v2CanonicalPath.load(domain.id);

if (!canonicalPath) return null;

const domains = await rejectAnyErrors(
DomainInterfaceRef.getDataloader(context).loadMany(canonicalPath),
);

const labels = canonicalPath.map((domainId: DomainId) => {
const found = domains.find((d) => d.id === domainId);
if (!found) {
throw new Error(
`Invariant(getDomainInterpretedName): Domain in CanonicalPath not found:\nPath: ${JSON.stringify(canonicalPath)}\nDomainId: ${domainId}`,
);
}
return found.label.interpreted;
});

return interpretedLabelsToInterpretedName(labels);
}

//////////////////////////////////
// DomainInterface Implementation
//////////////////////////////////
Expand Down Expand Up @@ -137,31 +173,7 @@ DomainInterfaceRef.implement({
tracing: true,
type: "InterpretedName",
nullable: true,
resolve: async (domain, args, context) => {
const canonicalPath = isENSv1Domain(domain)
? await context.loaders.v1CanonicalPath.load(domain.id)
: await context.loaders.v2CanonicalPath.load(domain.id);
if (!canonicalPath) return null;

// TODO: this could be more efficient if the get*CanonicalPath helpers included the label
// join for us.
const domains = await rejectAnyErrors(
DomainInterfaceRef.getDataloader(context).loadMany(canonicalPath),
);

const labels = canonicalPath.map((domainId) => {
const found = domains.find((d) => d.id === domainId);
if (!found) {
throw new Error(
`Invariant(Domain.name): Domain in CanonicalPath not found:\nPath: ${JSON.stringify(canonicalPath)}\nDomainId: ${domainId}`,
);
}

return found.label.interpreted;
});

return interpretedLabelsToInterpretedName(labels);
},
resolve: (domain, args, context) => getDomainInterpretedName(domain, context),
}),

///////////////
Expand Down Expand Up @@ -206,6 +218,41 @@ DomainInterfaceRef.implement({
resolve: (parent) => getDomainResolver(parent.id),
}),

///////////////////
// Domain.records
///////////////////
records: t.field({
description:
"Resolve ENS records for this Domain via the ENS protocol. Only canonical domains can be resolved. Returns null if the domain is not canonical.",
Comment thread
sevenzing marked this conversation as resolved.
Outdated
type: ResolvedRecordsRef,
nullable: true,
Comment thread
sevenzing marked this conversation as resolved.
Outdated
args: {
selection: t.arg({
type: ResolveSelectionInput,
required: true,
description: "Which records to resolve.",
}),
},
resolve: async (domain, { selection }, context) => {
const name = await getDomainInterpretedName(domain, context);
if (!name) return null;

const { result } = await runWithTrace(() =>
resolveForward(
name,
{
name: selection.reverseName ?? undefined,
texts: selection.texts ?? undefined,
addresses: selection.addresses ?? undefined,
},
{ accelerate: false, canAccelerate: false },
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
);

return result as ResolverRecordsResponseBase;
},
Comment thread
sevenzing marked this conversation as resolved.
Outdated
}),

///////////////////////
// Domain.registration
///////////////////////
Expand Down
107 changes: 107 additions & 0 deletions apps/ensapi/src/omnigraph-api/schema/resolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { CoinType } from "enssdk";

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

///////////////////////
// ResolveSelectionInput
///////////////////////
export const ResolveSelectionInput = builder.inputType("ResolveSelectionInput", {
description:
"Specifies which ENS records to resolve. At least one field must be set to receive any records.",
fields: (t) => ({
reverseName: t.boolean({
description: "Whether to resolve the `name` record (used in Reverse Resolution, ENSIP-19).",
required: false,
}),
texts: t.stringList({
description: "Text record keys to resolve (e.g. `avatar`, `description`, `com.).",
Comment thread
sevenzing marked this conversation as resolved.
Outdated
required: false,
Comment thread
sevenzing marked this conversation as resolved.
Outdated
}),
addresses: t.field({
description: "Coin types to resolve address records for (e.g. `60` for ETH).",
type: ["CoinType"],
required: false,
}),
}),
});

///////////////////////
// ResolvedTextRecord
///////////////////////
export const ResolvedTextRecordRef = builder
.objectRef<{ key: string; value: string | null }>("ResolvedTextRecord")
.implement({
description: "A resolved text record for an ENS name.",
fields: (t) => ({
key: t.exposeString("key", {
description: "The text record key.",
nullable: false,
}),
value: t.exposeString("value", {
description: "The text record value, or null if not set.",
nullable: true,
}),
}),
});

///////////////////////////
// ResolvedAddressRecord
///////////////////////////
export const ResolvedAddressRecordRef = builder
.objectRef<{ coinType: CoinType; address: string | null }>("ResolvedAddressRecord")
.implement({
description: "A resolved address record for an ENS name.",
fields: (t) => ({
coinType: t.field({
description: "The coin type for this address record.",
type: "CoinType",
nullable: false,
resolve: (r) => r.coinType,
}),
address: t.exposeString("address", {
description: "The address value, or null if not set.",
nullable: true,
}),
}),
});

////////////////////
// ResolvedRecords
////////////////////
export const ResolvedRecordsRef = builder
.objectRef<{
Comment thread
sevenzing marked this conversation as resolved.
Outdated
name: string | null | undefined;
texts: Record<string, string | null> | undefined;
addresses: Record<CoinType, string | null> | undefined;
}>("ResolvedRecords")
.implement({
description:
"Records resolved for a specific ENS name via the ENS protocol. Only selected records are populated.",
fields: (t) => ({
reverseName: t.string({
description:
"The `name` record value used in Reverse Resolution (ENSIP-19), or null if not set or not selected.",
nullable: true,
resolve: (r) => r.name ?? null,
}),
texts: t.field({
description: "Resolved text records for selected keys.",
type: [ResolvedTextRecordRef],
nullable: false,
resolve: (r) =>
r.texts ? Object.entries(r.texts).map(([key, value]) => ({ key, value })) : [],
}),
addresses: t.field({
description: "Resolved address records for selected coin types.",
type: [ResolvedAddressRecordRef],
nullable: false,
resolve: (r) =>
r.addresses
? Object.entries(r.addresses).map(([coinType, address]) => ({
coinType: Number(coinType) as CoinType,
address,
}))
: [],
}),
}),
});
Loading
Loading