Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
e6f7b84
apply easy comments
sevenzing May 30, 2026
d5d3633
final fixes (hope so)
sevenzing May 30, 2026
74ef718
Merge branch 'main' into ll/omnigraph-resolution-api
sevenzing May 30, 2026
514e12b
fix tests
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
9 changes: 9 additions & 0 deletions .changeset/omnigraph-resolution-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"ensapi": patch
Comment thread
sevenzing marked this conversation as resolved.
---

Changes related to **Omnigraph**:
Comment thread
sevenzing marked this conversation as resolved.

- add `Domain.resolve { records, trace, acceleration, profile? }` for forward resolution driven by the GraphQL selection set
- add `Account.resolve { primaryName(by: ...), primaryNames(where: ...) }` for reverse (ENSIP-19 primary name) resolution with `@oneOf` inputs (`coinType`/`chain`, `coinTypes`/`chains`)
- add `PrimaryNameRecord.resolve { records, ... }` for forward resolution of the resolved primary name
1 change: 1 addition & 0 deletions apps/ensapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"@ensdomains/ensjs": "^4.0.2",
"@ensdomains/address-encoder": "^1.1.2",
"@ensnode/datasources": "workspace:*",
"@ensnode/ensdb-sdk": "workspace:*",
"@ensnode/ensnode-sdk": "workspace:*",
Expand Down
29 changes: 25 additions & 4 deletions apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
import type { Duration } from "enssdk";

import {
hasOmnigraphApiConfigSupport,
hasOmnigraphApiIndexingStatusSupport,
} from "@ensnode/ensnode-sdk";

import di from "@/di";
import { errorResponse } from "@/lib/handlers/error-response";
import { createApp } from "@/lib/hono-factory";
import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware";
import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware";
import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware";

/**
* The maximum distance (in seconds) from the current time to the latest indexed block
* for a chain to be considered "realtime" and thus eligible for protocol acceleration.
*/
const MAX_REALTIME_DISTANCE_TO_ACCELERATE: Duration = 600; // 10 minutes

const app = createApp({ middlewares: [indexingStatusMiddleware] });
const app = createApp({
middlewares: [
indexingStatusMiddleware,
makeIsRealtimeMiddleware("omnigraph-api", MAX_REALTIME_DISTANCE_TO_ACCELERATE),
canAccelerateMiddleware,
],
});

app.use(async (c, next) => {
const configPrerequisite = hasOmnigraphApiConfigSupport(di.context.stackInfo.ensIndexer);
// 503 if Omnigraph API is not available due to config prerequisites not met
if (!configPrerequisite.supported) {
return c.text(`Service Unavailable: ${configPrerequisite.reason}`, 503);
return errorResponse(c, `Service Unavailable: ${configPrerequisite.reason}`, 503);
Comment thread
sevenzing marked this conversation as resolved.
}

// 503 if indexing status snapshot is not available yet
if (c.var.indexingStatus instanceof Error) {
return c.text(`Service Unavailable: Indexing Status Snapshot is not available yet`, 503);
return errorResponse(
c,
"Service Unavailable: Indexing Status Snapshot is not available yet",
503,
);
}

// 503 if omnigraph API not available due to indexing status prerequisites not met
Expand All @@ -27,7 +48,7 @@ app.use(async (c, next) => {
);

if (!indexingStatusPrerequisite.supported) {
return c.text(`Service Unavailable: ${indexingStatusPrerequisite.reason}`, 503);
return errorResponse(c, `Service Unavailable: ${indexingStatusPrerequisite.reason}`, 503);
}

await next();
Expand Down
8 changes: 4 additions & 4 deletions apps/ensapi/src/handlers/api/resolution/resolution-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {

import { createApp } from "@/lib/hono-factory";
import { resolveForward } from "@/lib/resolution/forward-resolution";
import { resolvePrimaryNames } from "@/lib/resolution/multichain-primary-name-resolution";
import { resolveReverse } from "@/lib/resolution/reverse-resolution";
import { resolvePrimaryNamesByChainIds } from "@/lib/resolution/multichain-primary-name-resolution";
import { resolveReverseByChainId } from "@/lib/resolution/reverse-resolution";
import { runWithTrace } from "@/lib/tracing/tracing-api";
import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware";
import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware";
Expand Down Expand Up @@ -90,7 +90,7 @@ app.openapi(resolvePrimaryNameRoute, async (c) => {
const canAccelerate = c.var.canAccelerate;

const { result, trace } = await runWithTrace(() =>
resolveReverse(address, chainId, { accelerate, canAccelerate }),
resolveReverseByChainId(address, chainId, { accelerate, canAccelerate }),
);

const response = {
Expand Down Expand Up @@ -118,7 +118,7 @@ app.openapi(resolvePrimaryNamesRoute, async (c) => {
const { chainIds, trace: showTrace, accelerate } = c.req.valid("query");
const canAccelerate = c.var.canAccelerate;
const { result, trace } = await runWithTrace(() =>
resolvePrimaryNames(address, chainIds, { accelerate, canAccelerate }),
resolvePrimaryNamesByChainIds(address, chainIds, { accelerate, canAccelerate }),
);

const response = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { trace } from "@opentelemetry/api";
import type { Address, CoinType } from "enssdk";
import { mainnet } from "viem/chains";

import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources";
import {
type MultichainPrimaryNameResolutionArgs,
type MultichainPrimaryNameResolutionResult,
type ReverseResolutionResult,
uniq,
} from "@ensnode/ensnode-sdk";

import di from "@/di";
import { withActiveSpanAsync } from "@/lib/instrumentation/auto-span";
import { resolveReverse } from "@/lib/resolution/reverse-resolution";
import { resolveReverse, resolveReverseByChainId } from "@/lib/resolution/reverse-resolution";

const tracer = trace.getTracer("multichain-primary-name-resolution");

const getENSIP19SupportedChainIds = () => {
export const getENSIP19SupportedChainIds = () => {
return uniq([
// always include Mainnet, because its chainId corresponds to the ENS Root Chain's coinType,
// regardless of the current namespace
Expand All @@ -34,6 +36,36 @@ const getENSIP19SupportedChainIds = () => {
]);
};

export type MultichainPrimaryNameByCoinTypeResolutionResult = Partial<
Record<CoinType, ReverseResolutionResult>
>;

type PrimaryNameResolutionOptions = Parameters<typeof resolveReverse>[2];

/**
* Batch-resolves an address' primary name for each requested coin type.
*
* @see https://docs.ens.domains/ensip/19
*/
export async function resolvePrimaryNamesByCoinTypes(
address: Address,
coinTypes: CoinType[],
options: PrimaryNameResolutionOptions,
): Promise<MultichainPrimaryNameByCoinTypeResolutionResult> {
const names = await withActiveSpanAsync(
tracer,
"resolvePrimaryNamesByCoinTypes",
{ address },
() => Promise.all(coinTypes.map((coinType) => resolveReverse(address, coinType, options))),
);

return coinTypes.reduce((memo, coinType, i) => {
// biome-ignore lint/style/noNonNullAssertion: names[i] guaranteed to be defined
memo[coinType] = names[i]!;
return memo;
}, {} as MultichainPrimaryNameByCoinTypeResolutionResult);
}

/**
* Implements batch resolution of an address' Primary Name across the provided `chainIds`.
*
Expand All @@ -46,17 +78,19 @@ const getENSIP19SupportedChainIds = () => {
* @param options.accelerate Whether acceleration is requested (default: true)
* @param options.canAccelerate Whether acceleration is currently possible (default: false)
*/
export async function resolvePrimaryNames(
export async function resolvePrimaryNamesByChainIds(
address: MultichainPrimaryNameResolutionArgs["address"],
chainIds: MultichainPrimaryNameResolutionArgs["chainIds"] = getENSIP19SupportedChainIds(),
options: Parameters<typeof resolveReverse>[2],
options: Parameters<typeof resolveReverseByChainId>[2],
): Promise<MultichainPrimaryNameResolutionResult> {
// parallel reverseResolve
const names = await withActiveSpanAsync(tracer, "resolvePrimaryNames", { address }, () =>
Promise.all(chainIds.map((chainId) => resolveReverse(address, chainId, options))),
const names = await withActiveSpanAsync(
tracer,
"resolvePrimaryNamesByChainIds",
{ address },
() =>
Promise.all(chainIds.map((chainId) => resolveReverseByChainId(address, chainId, options))),
);

// key results by chainId
return chainIds.reduce((memo, chainId, i) => {
// biome-ignore lint/style/noNonNullAssertion: names[i] guaranteed to be defined
memo[chainId] = names[i]!;
Expand Down
48 changes: 34 additions & 14 deletions apps/ensapi/src/lib/resolution/reverse-resolution.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { coinTypeReverseLabel, evmChainIdToCoinType, reverseName } from "enssdk";
import {
type Address,
type CoinType,
coinTypeReverseLabel,
type DefaultableChainId,
evmChainIdToCoinType,
reverseName,
} from "enssdk";
import { isAddress, isAddressEqual } from "viem";

import {
type ResolverRecordsSelection,
type ReverseResolutionArgs,
ReverseResolutionProtocolStep,
type ReverseResolutionResult,
TraceableENSProtocol,
Expand All @@ -24,45 +30,44 @@ export const REVERSE_RESOLUTION_SELECTION = {

const tracer = trace.getTracer("reverse-resolution");

type ReverseResolutionOptions = Parameters<typeof resolveForward>[2];

/**
* Implements ENS Reverse Resolution, including support for ENSIP-19 L2 Primary Names.
* Implements ENS Reverse Resolution for a specific coin type, including ENSIP-19 L2 Primary Names.
*
* @see https://docs.ens.domains/ensip/19/#algorithm
*
* The DEFAULT_EVM_CHAIN_ID (0) is a valid chainId in this context.
*
* @param address the adddress whose Primary Name to resolve
* @param chainId the chainId within which to resolve the address' Primary Name
* @param coinType the coinType within which to resolve the address' Primary Name
* @param options Optional settings
* @param options.accelerate Whether to accelerate resolution (default: true)
* @param options.accelerate Whether to attempt accelerated resolution (default: true)
* @param options.canAccelerate Whether acceleration is currently possible (default: false)
*/
export async function resolveReverse(
address: ReverseResolutionArgs["address"],
chainId: ReverseResolutionArgs["chainId"],
options: Parameters<typeof resolveForward>[2],
address: Address,
coinType: CoinType,
options: ReverseResolutionOptions,
): Promise<ReverseResolutionResult> {
const { accelerate = true } = options;

// trace for external consumers
return withProtocolStep(
TraceableENSProtocol.ReverseResolution,
ReverseResolutionProtocolStep.Operation,
{ address, chainId, accelerate },
{ address, coinType, accelerate },
(protocolTracingSpan) =>
// trace for internal metrics
withActiveSpanAsync(
tracer,
`resolveReverse(${address}, chainId: ${chainId})`,
{ address, chainId, accelerate },
`resolveReverseByCoinType(${address}, coinType: ${coinType})`,
{ address, coinType, accelerate },
async (span) => {
/////////////////////////////////////////////////////////
// Reverse Resolution
// https://docs.ens.domains/ensip/19/#algorithm
/////////////////////////////////////////////////////////

// Steps 1-3 — Resolve coinType-specific name record
const coinType = evmChainIdToCoinType(chainId);
const _reverseName = reverseName(address, coinType);
const { name } = await withProtocolStep(
TraceableENSProtocol.ReverseResolution,
Expand Down Expand Up @@ -173,3 +178,18 @@ export async function resolveReverse(
),
);
}

/**
* Thin `chainId` wrapper around {@link resolveReverse} for callers at the REST API boundary.
*
* @param address The address whose primary name to resolve.
* @param chainId The EVM chain id for the reverse lookup. `0` is valid as the ENSIP-19 default chain id.
* @param options Optional resolution settings.
*/
export async function resolveReverseByChainId(
address: Address,
chainId: DefaultableChainId,
options: ReverseResolutionOptions,
): Promise<ReverseResolutionResult> {
return resolveReverse(address, evmChainIdToCoinType(chainId), options);
}
8 changes: 6 additions & 2 deletions apps/ensapi/src/omnigraph-api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import type {
CoinType,
DomainId,
Hex,
InterfaceId,
InterpretedLabel,
InterpretedName,
JsonValue,
Node,
NormalizedAddress,
PermissionsId,
Expand All @@ -28,7 +30,7 @@ import type {
import { getNamedType } from "graphql";
import superjson from "superjson";

import type { context } from "@/omnigraph-api/context";
import type { Context } from "@/omnigraph-api/context";

const tracer = trace.getTracer("graphql");
const createSpan = createOpenTelemetryWrapper(tracer, {
Expand Down Expand Up @@ -61,10 +63,12 @@ const createSpan = createOpenTelemetryWrapper(tracer, {
export type BuilderScalars = {
ID: { Input: string; Output: string };
BigInt: { Input: bigint; Output: bigint };
JSON: { Input: JsonValue; Output: JsonValue };
Address: { Input: NormalizedAddress; Output: NormalizedAddress };
Hex: { Input: Hex; Output: Hex };
ChainId: { Input: ChainId; Output: ChainId };
CoinType: { Input: CoinType; Output: CoinType };
InterfaceId: { Input: InterfaceId; Output: InterfaceId };
Node: { Input: Node; Output: Node };
InterpretedName: { Input: InterpretedName; Output: InterpretedName };
InterpretedLabel: { Input: InterpretedLabel; Output: InterpretedLabel };
Expand All @@ -82,7 +86,7 @@ export type BuilderScalars = {
};

export const builder = new SchemaBuilder<{
Context: ReturnType<typeof context>;
Context: Context;
Scalars: BuilderScalars;

// the following ensures via typechecker that every t.connection returns a totalCount field
Expand Down
9 changes: 8 additions & 1 deletion apps/ensapi/src/omnigraph-api/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { inArray } from "drizzle-orm";
import type { DomainId, RegistryId } from "enssdk";

import di from "@/di";
import type { CanAccelerateMiddlewareVariables } from "@/middleware/can-accelerate.middleware";

/** Server context passed from Hono into GraphQL Yoga via `yoga.fetch(request, serverContext)`. */
export type OmnigraphYogaServerContext = CanAccelerateMiddlewareVariables;

const createRegistryParentDomainLoader = () =>
new DataLoader<RegistryId, DomainId | null>(async (registryIds) => {
Expand All @@ -24,9 +28,12 @@ const createRegistryParentDomainLoader = () =>
*
* @dev make sure that anything that is per-request (like dataloaders) are newly created in this fn
*/
export const context = () => ({
export const createOmnigraphContext = (serverContext: OmnigraphYogaServerContext) => ({
now: BigInt(getUnixTime(new Date())),
loaders: {
registryParentDomain: createRegistryParentDomainLoader(),
},
canAccelerate: serverContext.canAccelerate,
});

export type Context = ReturnType<typeof createOmnigraphContext>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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 as createContext } from "@/omnigraph-api/context";
import type { Context } from "@/omnigraph-api/context";
import { DomainCursors } from "@/omnigraph-api/lib/find-domains/domain-cursor";
import {
cursorFilter,
Expand Down Expand Up @@ -102,7 +102,7 @@ function getDefaultOrder(where: DomainsWhere | undefined | null): DomainsOrderVa
* @param args - Compound `where` filter, optional ordering, and relay connection args
*/
export function resolveFindDomains(
context: ReturnType<typeof createContext>,
context: Context,
{
where,
order,
Expand Down
Loading
Loading