Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a73e92b
chckpoint: further progress on enssdk and enskit re:omnigraph
shrugs Apr 3, 2026
7e8b6a7
Merge branch 'main' into feat/enskit-omnigraph
shrugs Apr 3, 2026
621f46a
enskit: minified introspection, graphcache local resolvers, CORS fix
shrugs Apr 3, 2026
b43f3bc
refactor: move all id types and construction libs to enssdk
shrugs Apr 3, 2026
279b37a
refactor: move tokenid interpretation to enssdk and fix assetid token…
shrugs Apr 3, 2026
46f60ea
fix: local resolvers for partial results of top-level queries
shrugs Apr 3, 2026
5630889
refactor: move Query.account and Query.permission to existing pattern
shrugs Apr 3, 2026
5cc24ae
fix: registry local cache demo
shrugs Apr 3, 2026
be86f9d
tidy up registration example
shrugs Apr 3, 2026
872e341
fix: refactor to lib/cache-exchange and add connection pagination
shrugs Apr 3, 2026
f89746f
feat: pagination example
shrugs Apr 3, 2026
3b4fa04
fix: address PR review feedback
shrugs Apr 3, 2026
70419ea
fix: tests
shrugs Apr 3, 2026
2bbad78
fix: lint
shrugs Apr 3, 2026
20bdae6
fix: include response body in omnigraph error, pass fetch to urql
shrugs Apr 3, 2026
5cc7e1e
Merge branch 'main' into feat/enskit-omnigraph
shrugs Apr 6, 2026
e864730
refactor: move uint encoding to _lib, fix package versions for react etc
shrugs Apr 6, 2026
e9535e6
fix: licenses across monorepo
shrugs Apr 6, 2026
f1069aa
refactor: based on pr feedback
shrugs Apr 6, 2026
b885797
refactor: migrate scalar name to InterpretedName
shrugs Apr 6, 2026
161020b
fix: generate gql schema
shrugs Apr 6, 2026
500ed34
Merge branch 'main' into feat/enskit-omnigraph
shrugs Apr 6, 2026
5b3f6fe
fix: example queries use new scalar
shrugs Apr 6, 2026
b10d1ce
fix: more scalars
shrugs Apr 6, 2026
baa69d9
fix: scalar update oops
shrugs Apr 6, 2026
21670a3
fix: improve terminology of zeroLower32Bits
shrugs Apr 6, 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
5 changes: 5 additions & 0 deletions .changeset/warm-ghosts-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": minor
---

Change `Query.permissions` to accept `by: { id, contract }` and `Query.account` to accept `by: { id, address }`, matching the `by` input pattern of `Query.registry` and `Query.resolver`.
8 changes: 4 additions & 4 deletions apps/ensadmin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
"lucide-react": "catalog:",
"next": "^16.1.7",
"next-themes": "^0.4.6",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "catalog:",
"react-dom": "catalog:",
"rooks": "^8.4.0",
"serve": "^14.2.5",
"sonner": "^2.0.3",
Expand All @@ -68,8 +68,8 @@
"devDependencies": {
"@testing-library/react": "catalog:",
"@types/node": "catalog:",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@tailwindcss/postcss": "^4.1.18",
"postcss": "^8.5.6",
"tailwindcss": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion apps/ensapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"start": "tsx src/index.ts",
"dev": "tsx watch --env-file ./.env.local src/index.ts",
"test": "vitest",
"test:integration": "vitest run --config vitest.integration.config.ts",
"lint": "biome check --write .",
"lint:ci": "biome ci",
"typecheck": "tsgo --noEmit",
Expand Down Expand Up @@ -68,6 +67,7 @@
"@ensnode/shared-configs": "workspace:*",
"@types/node": "catalog:",
"@types/prismjs": "^1.26.6",
"@urql/introspection": "^1.2.1",
"chalk": "^5.6.2",
"graphql-request": "^7.4.0",
"pino-pretty": "^13.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
type ReferrerMetrics,
} from "@namehash/ens-referrals/v1";
import { and, asc, count, desc, eq, gte, isNotNull, lte, ne, sql, sum } from "drizzle-orm";
import { stringifyAccountId } from "enssdk";
import { type Address, zeroAddress } from "viem";

import { deserializeDuration, formatAccountId, priceEth } from "@ensnode/ensnode-sdk";
import { deserializeDuration, priceEth } from "@ensnode/ensnode-sdk";

import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";
import logger from "@/lib/logger";
Expand Down Expand Up @@ -62,7 +63,10 @@ export const getReferrerMetrics = async (
// Filter by decodedReferrer not zero address
ne(ensIndexerSchema.registrarActions.decodedReferrer, zeroAddress),
// Filter by subregistryId matching the provided subregistryId
eq(ensIndexerSchema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)),
eq(
ensIndexerSchema.registrarActions.subregistryId,
stringifyAccountId(rules.subregistryId),
),
),
)
.groupBy(ensIndexerSchema.registrarActions.decodedReferrer)
Expand Down Expand Up @@ -126,7 +130,10 @@ export const getReferralEvents = async (rules: ReferralProgramRules): Promise<Re
// Filter by decodedReferrer not zero address
ne(ensIndexerSchema.registrarActions.decodedReferrer, zeroAddress),
// Filter by subregistryId matching the provided subregistryId
eq(ensIndexerSchema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)),
eq(
ensIndexerSchema.registrarActions.subregistryId,
stringifyAccountId(rules.subregistryId),
),
),
)
.orderBy(asc(ensIndexerSchema.registrarActions.id));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
type ReferrerMetrics,
} from "@namehash/ens-referrals";
import { and, count, desc, eq, gte, isNotNull, lte, ne, sql, sum } from "drizzle-orm";
import { stringifyAccountId } from "enssdk";
import { type Address, zeroAddress } from "viem";

import { deserializeDuration, formatAccountId } from "@ensnode/ensnode-sdk";
import { deserializeDuration } from "@ensnode/ensnode-sdk";

import { ensDb, ensIndexerSchema } from "@/lib/ensdb/singleton";
import logger from "@/lib/logger";
Expand Down Expand Up @@ -61,7 +62,10 @@ export const getReferrerMetrics = async (
// Filter by decodedReferrer not zero address
ne(ensIndexerSchema.registrarActions.decodedReferrer, zeroAddress),
// Filter by subregistryId matching the provided subregistryId
eq(ensIndexerSchema.registrarActions.subregistryId, formatAccountId(rules.subregistryId)),
eq(
ensIndexerSchema.registrarActions.subregistryId,
stringifyAccountId(rules.subregistryId),
),
),
)
.groupBy(ensIndexerSchema.registrarActions.decodedReferrer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ type MapToNamedRegistrarActionArgs = Awaited<ReturnType<typeof _findRegistrarAct
function _mapToNamedRegistrarAction(record: MapToNamedRegistrarActionArgs): NamedRegistrarAction {
// Invariant: The FQDN `name` of the Domain associated with the `node` must exist.
if (record.domain.name === null) {
throw new Error(`Domain 'name' must exists for '${record.registrationLifecycles.node}' node.`);
throw new Error(`Domain 'name' must exist for '${record.registrationLifecycles.node}' node.`);
}

// build Registration Lifecycle object
Expand Down
12 changes: 11 additions & 1 deletion apps/ensapi/src/omnigraph-api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ChainId,
CoinType,
DomainId,
InterpretedLabel,
InterpretedName,
Node,
PermissionsId,
Expand Down Expand Up @@ -56,13 +57,15 @@ const createSpan = createOpenTelemetryWrapper(tracer, {
export const builder = new SchemaBuilder<{
Context: ReturnType<typeof context>;
Scalars: {
// make sure to keep these scalars up to date with packages/enssdk/src/omnigraph/graphql.ts !
BigInt: { Input: bigint; Output: bigint };
Address: { Input: Address; Output: Address };
Hex: { Input: Hex; Output: Hex };
ChainId: { Input: ChainId; Output: ChainId };
CoinType: { Input: CoinType; Output: CoinType };
Node: { Input: Node; Output: Node };
Name: { Input: InterpretedName; Output: InterpretedName };
InterpretedName: { Input: InterpretedName; Output: InterpretedName };
InterpretedLabel: { Input: InterpretedLabel; Output: InterpretedLabel };
DomainId: { Input: DomainId; Output: DomainId };
RegistryId: { Input: RegistryId; Output: RegistryId };
ResolverId: { Input: ResolverId; Output: ResolverId };
Expand All @@ -78,6 +81,9 @@ export const builder = new SchemaBuilder<{
Connection: {
totalCount: MaybePromise<number>;
};

DefaultEdgesNullability: false;
DefaultNodeNullability: false;
}>({
plugins: [TracingPlugin, DataloaderPlugin, RelayPlugin],
tracing: {
Expand All @@ -104,5 +110,9 @@ export const builder = new SchemaBuilder<{
// disable the Query.node & Query.nodes methods
nodeQueryOptions: false,
nodesQueryOptions: false,

// globally configures Edge and Node types to be non-nullable
edgesFieldOptions: { nullable: false },
nodeFieldOptions: { nullable: false },
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export function withOrderingMetadata(base: BaseDomainSet) {
ensIndexerSchema.registration,
and(
eq(ensIndexerSchema.registration.domainId, base.domainId),
eq(ensIndexerSchema.registration.index, ensIndexerSchema.latestRegistrationIndex.index),
eq(
ensIndexerSchema.registration.registrationIndex,
ensIndexerSchema.latestRegistrationIndex.registrationIndex,
),
),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ import { ensDb } from "@/lib/ensdb/singleton";
export async function getLatestRegistration(domainId: DomainId) {
return await ensDb.query.registration.findFirst({
where: (t, { eq }) => eq(t.domainId, domainId),
orderBy: (t, { desc }) => desc(t.index),
orderBy: (t, { desc }) => desc(t.registrationIndex),
});
}
27 changes: 18 additions & 9 deletions apps/ensapi/src/omnigraph-api/lib/write-graphql-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@ import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { lexicographicSortSchema, printSchema } from "graphql";
import { minifyIntrospectionQuery } from "@urql/introspection";
import { introspectionFromSchema, lexicographicSortSchema, printSchema } from "graphql";

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

const logger = makeLogger("write-graphql-schema");

const MONOREPO_ROOT = resolve(import.meta.dirname, "../../../../../");
const ENSSDK_ROOT = resolve(MONOREPO_ROOT, "packages/enssdk/");
const OUTPUT_PATH = resolve(ENSSDK_ROOT, "src/omnigraph/generated/schema.graphql");
const GENERATED_DIR = resolve(ENSSDK_ROOT, "src/omnigraph/generated");

async function _writeGraphQLSchema() {
const { schema } = await import("@/omnigraph-api/schema");
const schemaAsString = printSchema(lexicographicSortSchema(schema));

await writeFile(OUTPUT_PATH, schemaAsString);
const { schema: unsortedSchema } = await import("@/omnigraph-api/schema");
const schema = lexicographicSortSchema(unsortedSchema);
const sdl = printSchema(schema);
const introspection = minifyIntrospectionQuery(introspectionFromSchema(schema));

await Promise.all([
writeFile(resolve(GENERATED_DIR, "schema.graphql"), sdl),
writeFile(
resolve(GENERATED_DIR, "introspection.ts"),
`export const introspection = ${JSON.stringify(introspection)} as const;\n`,
),
]);
}

/**
Expand All @@ -25,17 +34,17 @@ async function _writeGraphQLSchema() {
export async function writeGraphQLSchema() {
try {
await _writeGraphQLSchema();
logger.info(`Wrote SDL to ${OUTPUT_PATH}`);
logger.info(`Wrote SDL to ${GENERATED_DIR}`);
} catch (error) {
logger.warn(error, `Unable to write SDL to ${OUTPUT_PATH}`);
logger.warn(error, `Unable to write SDL to ${GENERATED_DIR}`);
}
}

// when executed directly (`pnpm generate:gqlschema`), write generated schema and produce an exit code
if (resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
await _writeGraphQLSchema();
console.log(`Wrote SDL to ${OUTPUT_PATH}`);
console.log(`Wrote SDL to ${GENERATED_DIR}`);
process.exit(0);
} catch (error) {
console.error(error);
Expand Down
4 changes: 2 additions & 2 deletions apps/ensapi/src/omnigraph-api/schema/account-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export const AccountIdRef = builder.objectRef<AccountId>("AccountId");
AccountIdRef.implement({
description: "A CAIP-10 Account ID including chainId and address.",
fields: (t) => ({
chainId: t.expose("chainId", { type: "ChainId" }),
address: t.expose("address", { type: "Address" }),
chainId: t.expose("chainId", { type: "ChainId", nullable: false }),
address: t.expose("address", { type: "Address", nullable: false }),
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("Account.domains", () => {

const AccountDomains = gql`
query AccountDomains($address: Address!) {
account(address: $address) {
account(by: { address: $address }) {
domains(order: { by: NAME, dir: ASC }) { edges { node { name } } }
}
}
Expand Down Expand Up @@ -90,7 +90,7 @@ describe("Account.events", () => {

const AccountEvents = gql`
query AccountEvents($address: Address!) {
account(address: $address) { events { edges { node { ...EventFragment } } } }
account(by: { address: $address }) { events { edges { node { ...EventFragment } } } }
}
${EventFragment}
`;
Expand Down Expand Up @@ -124,7 +124,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => {

const AccountEventsFiltered = gql`
query AccountEventsFiltered($address: Address!, $where: AccountEventsWhereInput, $first: Int) {
account(address: $address) { events(where: $where, first: $first) { edges { node { ...EventFragment } } } }
account(by: { address: $address }) { events(where: $where, first: $first) { edges { node { ...EventFragment } } } }
}
${EventFragment}
`;
Expand Down
13 changes: 13 additions & 0 deletions apps/ensapi/src/omnigraph-api/schema/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,16 @@ AccountRef.implement({
}),
}),
});

//////////
// Inputs
//////////

export const AccountByInput = builder.inputType("AccountByInput", {
description: "Address an Account by ID or Address.",
isOneOf: true,
fields: (t) => ({
id: t.field({ type: "Address" }),
address: t.field({ type: "Address" }),
}),
});
12 changes: 0 additions & 12 deletions apps/ensapi/src/omnigraph-api/schema/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,3 @@ export const ID_PAGINATED_CONNECTION_ARGS = {
defaultSize: PAGINATION_DEFAULT_PAGE_SIZE,
maxSize: PAGINATION_DEFAULT_MAX_SIZE,
} as const;

/**
* Connection field arguments for entities paginated by a numeric `index` column.
*
* @dev we can use the index itself as a cursor because there are no collisions within a given scope
* (i.e. the `index` is only used once per Domain's Registration or per Registration's Renewal).
*/
export const INDEX_PAGINATED_CONNECTION_ARGS = {
toCursor: <T extends { index: number }>(model: T) => cursors.encode(String(model.index)),
defaultSize: PAGINATION_DEFAULT_PAGE_SIZE,
maxSize: PAGINATION_DEFAULT_MAX_SIZE,
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("Domain.subdomains", () => {
};

const DomainSubdomains = gql`
query DomainSubdomains($name: Name!) {
query DomainSubdomains($name: InterpretedName!) {
domain(by: { name: $name }) {
subdomains { edges { node { name label { interpreted } } } }
}
Expand Down Expand Up @@ -68,7 +68,7 @@ describe("Domain.events", () => {
};

const DomainEvents = gql`
query DomainEvents($name: Name!) {
query DomainEvents($name: InterpretedName!) {
domain(by: { name: $name }) { events { edges { node { ...EventFragment } } } }
}
${EventFragment}
Expand Down Expand Up @@ -107,7 +107,7 @@ describe("Domain.events filtering (EventsWhereInput)", () => {
};

const DomainEventsFiltered = gql`
query DomainEventsFiltered($name: Name!, $where: EventsWhereInput, $first: Int) {
query DomainEventsFiltered($name: InterpretedName!, $where: EventsWhereInput, $first: Int) {
domain(by: { name: $name }) { events(where: $where, first: $first) { edges { node { ...EventFragment } } } }
}
${EventFragment}
Expand Down
24 changes: 18 additions & 6 deletions apps/ensapi/src/omnigraph-api/schema/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
paginateBy,
paginateByInt,
} from "@/omnigraph-api/lib/connection-helpers";
import { cursors } from "@/omnigraph-api/lib/cursors";
import { resolveFindDomains } from "@/omnigraph-api/lib/find-domains/find-domains-resolver";
import {
domainsBase,
Expand All @@ -33,7 +34,8 @@ import { rejectAnyErrors } from "@/omnigraph-api/lib/reject-any-errors";
import { AccountRef } from "@/omnigraph-api/schema/account";
import {
ID_PAGINATED_CONNECTION_ARGS,
INDEX_PAGINATED_CONNECTION_ARGS,
PAGINATION_DEFAULT_MAX_SIZE,
PAGINATION_DEFAULT_PAGE_SIZE,
} from "@/omnigraph-api/schema/constants";
import { EventRef, EventsWhereInput } from "@/omnigraph-api/schema/event";
import { LabelRef } from "@/omnigraph-api/schema/label";
Expand Down Expand Up @@ -134,7 +136,7 @@ DomainInterfaceRef.implement({
description:
"The Canonical Name for this Domain. If the Domain is not Canonical, then `name` will be null.",
tracing: true,
type: "Name",
type: "InterpretedName",
nullable: true,
resolve: async (domain, args, context) => {
const canonicalPath = isENSv1Domain(domain)
Expand Down Expand Up @@ -228,15 +230,25 @@ DomainInterfaceRef.implement({
totalCount: () => ensDb.$count(ensIndexerSchema.registration, scope),
connection: () =>
resolveCursorConnection(
{ ...INDEX_PAGINATED_CONNECTION_ARGS, args },
{
toCursor: (model) => cursors.encode(String(model.registrationIndex)),
defaultSize: PAGINATION_DEFAULT_PAGE_SIZE,
maxSize: PAGINATION_DEFAULT_MAX_SIZE,
args,
},
({ before, after, limit, inverted }: ResolveCursorConnectionArgs) =>
ensDb
.select()
.from(ensIndexerSchema.registration)
.where(
and(scope, paginateByInt(ensIndexerSchema.registration.index, before, after)),
and(
scope,
paginateByInt(ensIndexerSchema.registration.registrationIndex, before, after),
),
)
.orderBy(
orderPaginationBy(ensIndexerSchema.registration.registrationIndex, inverted),
)
.orderBy(orderPaginationBy(ensIndexerSchema.registration.index, inverted))
.limit(limit),
),
});
Expand Down Expand Up @@ -417,7 +429,7 @@ export const DomainIdInput = builder.inputType("DomainIdInput", {
description: "Reference a specific Domain.",
isOneOf: true,
fields: (t) => ({
name: t.field({ type: "Name" }),
name: t.field({ type: "InterpretedName" }),
id: t.field({ type: "DomainId" }),
}),
});
Expand Down
Loading
Loading