Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions .changeset/large-cameras-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
Comment thread
shrugs marked this conversation as resolved.
"ensindexer": minor
"ensapi": minor
Comment on lines +2 to +3
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changeset is marked as a minor bump, but the PR removes exported public API from @ensnode/datasources (e.g. ensTestEnvL1Chain/ensTestEnvL2Chain) and removes a datasource name (DatasourceNames.ENSv2ETHRegistry). Under semver this is a breaking change and should be released as a major bump for the fixed version group.

Suggested change
"ensindexer": minor
"ensapi": minor
"ensindexer": major
"ensapi": major

Copilot uses AI. Check for mistakes.
---

The `ens-test-env` namespace now functions against devnet commit `762de44`, which includes the major refactor of ENSv2 onto the ENS Root Chain, away from Namechain.
Comment thread
shrugs marked this conversation as resolved.
118 changes: 11 additions & 107 deletions apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,23 @@
import config from "@/config";

import { getUnixTime } from "date-fns";
import { Param, sql } from "drizzle-orm";
import { namehash } from "viem";

import { DatasourceNames } from "@ensnode/datasources";
import * as schema from "@ensnode/ensnode-schema";
import {
type DomainId,
type ENSv2DomainId,
ETH_NODE,
getENSv2RootRegistryId,
type InterpretedName,
interpretedLabelsToInterpretedName,
interpretedLabelsToLabelHashPath,
interpretedNameToInterpretedLabels,
isRegistrationFullyExpired,
type LabelHash,
type LiteralLabel,
labelhashLiteralLabel,
makeENSv1DomainId,
makeRegistryId,
makeSubdomainNode,
maybeGetDatasourceContract,
type RegistryId,
} from "@ensnode/ensnode-sdk";

import { getLatestRegistration } from "@/graphql-api/lib/get-latest-registration";
import { db } from "@/lib/db";
import { makeLogger } from "@/lib/logger";

const logger = makeLogger("get-domain-by-fqdn");

// TODO(ensv2): can make this getDatasourceContract once ENSv2 Datasources are available in all namespaces
const V2_ROOT_ETH_REGISTRY = maybeGetDatasourceContract(
Comment thread
shrugs marked this conversation as resolved.
config.namespace,
DatasourceNames.ENSv2Root,
"ETHRegistry",
);

// TODO(ensv2): can make this getDatasourceContract once ENSv2 Datasources are available in all namespaces
const V2_NAMECHAIN_ETH_REGISTRY = maybeGetDatasourceContract(
config.namespace,
DatasourceNames.ENSv2ETHRegistry,
"ETHRegistry",
);

const ETH_LABELHASH = labelhashLiteralLabel("eth" as LiteralLabel);
const ROOT_REGISTRY_ID = getENSv2RootRegistryId(config.namespace);

/**
Expand All @@ -61,7 +32,7 @@ export async function getDomainIdByInterpretedName(
v2_getDomainIdByFqdn(ROOT_REGISTRY_ID, name),
]);

// prefer v2DomainId
// prefer v2DomainId if exists
Comment thread
shrugs marked this conversation as resolved.
Outdated
Comment thread
shrugs marked this conversation as resolved.
Outdated
return v2DomainId || v1DomainId || null;
Comment thread
lightwalker-eth marked this conversation as resolved.
}

Expand All @@ -77,15 +48,12 @@ async function v1_getDomainIdByFqdn(name: InterpretedName): Promise<DomainId | n
}

/**
* Forward-traverses the ENSv2 namegraph in order to identify the Domain addressed by `name`.
*
* If the exact Domain was not found, and the path terminates at a bridging resolver, bridge to the
* indicated Registry and continue traversing.
* Forward-traverses the ENSv2 namegraph from the specified root in order to identify the Domain
* addressed by `name`.
*/
async function v2_getDomainIdByFqdn(
registryId: RegistryId,
rootRegistryId: RegistryId,
name: InterpretedName,
{ now } = { now: BigInt(getUnixTime(new Date())) },
): Promise<DomainId | null> {
const labelHashPath = interpretedLabelsToLabelHashPath(interpretedNameToInterpretedLabels(name));

Expand All @@ -102,7 +70,7 @@ async function v2_getDomainIdByFqdn(
NULL::text AS label_hash,
0 AS depth
FROM ${schema.registry} r
WHERE r.id = ${registryId}
WHERE r.id = ${rootRegistryId}

UNION ALL

Expand Down Expand Up @@ -131,80 +99,16 @@ async function v2_getDomainIdByFqdn(
depth: number;
}[];

// this was a query for a TLD and it does not exist in ENS Root Chain ENSv2
// this was a query for a TLD and it does not exist within the ENSv2 namegraph
if (rows.length === 0) return null;

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

/////////////////////////////////////////////////////////////////////////////////
// 1. An exact match was found for the Domain within ENSv2 on the ENS Root Chain.
/////////////////////////////////////////////////////////////////////////////////
// the v2Domain was found iff there is an exact match within the ENSv2 namegraph
const exact = rows.length === labelHashPath.length;
if (exact) {
logger.debug(`Found '${name}' in ENSv2 from Registry ${registryId}`);
return leaf.domain_id;
}

/////////////////////////////////////////////////////////////////////////////////
// 2. ETHTLDResolver
// if the path terminates at the .eth Registry, we must implement the logic in ETHTLDResolver
// TODO: we could add an additional invariant that the .eth v2 Registry does indeed have the ETHTLDResolver
// set as its resolver, but that is unnecessary at the moment and incurs additional db requests or a join against
// domain_resolver_relationships
// TODO: generalize this into other future bridging resolvers depending on how basenames etc do it
/////////////////////////////////////////////////////////////////////////////////

if (!V2_ROOT_ETH_REGISTRY) return null;

// 2.1: if the path did not terminate at the .eth Registry, then the domain was not found
if (leaf.registry_id !== makeRegistryId(V2_ROOT_ETH_REGISTRY)) return null;

logger.debug({ name, rows });

// Invariant: must be >= 2LD
if (labelHashPath.length < 2) {
throw new Error(`Invariant: '${name}' is not >= 2LD (has depth ${labelHashPath.length})!`);
}

// Invariant: LabelHashPath must originate at 'eth'
if (labelHashPath[0] !== ETH_LABELHASH) {
throw new Error(
`Invariant: '${name}' terminated at .eth Registry but the queried labelHashPath (${JSON.stringify(labelHashPath)}) does not originate with 'eth' (${ETH_LABELHASH}).`,
);
}

// Invariant: The path must terminate at 'eth' as well.
if (leaf.label_hash !== ETH_LABELHASH) {
throw new Error(
`Invariant: the leaf identified (${leaf.label_hash}) does not match 'eth' (${ETH_LABELHASH}).`,
);
}

// construct the node of the 2ld
const dotEth2LDNode = makeSubdomainNode(labelHashPath[1], ETH_NODE);

// 2.2: if there's an active registration in ENSv1 for the .eth 2LD, then resolve from ENSv1
const ensv1DomainId = makeENSv1DomainId(dotEth2LDNode);
const registration = await getLatestRegistration(ensv1DomainId);

if (registration && !isRegistrationFullyExpired(registration, now)) {
logger.debug(
`ETHTLDResolver deferring to actively registered name ${dotEth2LDNode} in ENSv1...`,
);
return await v1_getDomainIdByFqdn(name);
}

// 2.3: otherwise, direct to Namechain ENSv2 .eth Registry
// if there's no ETHRegistry on Namechain, the domain was not found
if (!V2_NAMECHAIN_ETH_REGISTRY) return null;

const nameWithoutTld = interpretedLabelsToInterpretedName(
interpretedNameToInterpretedLabels(name).slice(0, -1),
);
logger.debug(
`ETHTLDResolver deferring '${nameWithoutTld}' to ENSv2 .eth Registry on Namechain...`,
);

return v2_getDomainIdByFqdn(makeRegistryId(V2_NAMECHAIN_ETH_REGISTRY), nameWithoutTld, { now });
if (exact) return leaf.domain_id;

// otherwise, the v2 domain was not found
return null;
Comment thread
shrugs marked this conversation as resolved.
Comment thread
shrugs marked this conversation as resolved.
}
4 changes: 2 additions & 2 deletions apps/ensapi/src/lib/public-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import config from "@/config";

import { ccipRequest, createPublicClient, fallback, http, type PublicClient } from "viem";

import { ensTestEnvL1Chain } from "@ensnode/datasources";
import { ensTestEnvChain } from "@ensnode/datasources";
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
import type { ChainId } from "@ensnode/ensnode-sdk";

const _cache = new Map<ChainId, PublicClient>();
Expand Down Expand Up @@ -31,7 +31,7 @@ export function getPublicClient(chainId: ChainId): PublicClient {
// from within the Docker container. So here, if we're handling a CCIP-Read request on
// the ens-test-env L1 Chain, we add the ens-test-env's docker-compose-specific url as
// a fallback if the default (http://localhost:8547) fails.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (chainId === ensTestEnvL1Chain.id) {
if (chainId === ensTestEnvChain.id) {
Comment thread
shrugs marked this conversation as resolved.
Outdated
return ccipRequest({ data, sender, urls: [...urls, "http://devnet:8547"] });
}
Comment thread
shrugs marked this conversation as resolved.
Outdated

Expand Down
5 changes: 2 additions & 3 deletions apps/ensindexer/src/config/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { ensTestEnvL1Chain, ensTestEnvL2Chain } from "@ensnode/datasources";
import { ensTestEnvChain } from "@ensnode/datasources";
import { ENSNamespaceIds, PluginName } from "@ensnode/ensnode-sdk";
import type { RpcConfig } from "@ensnode/ensnode-sdk/internal";

Expand Down Expand Up @@ -661,8 +661,7 @@ describe("config (minimal base env)", () => {
stubEnv({ NAMESPACE: "ens-test-env", PLUGINS: "subgraph" });

const config = await getConfig();
expect(config.rpcConfigs.has(ensTestEnvL1Chain.id)).toBe(true);
expect(config.rpcConfigs.has(ensTestEnvL2Chain.id)).toBe(true);
expect(config.rpcConfigs.has(ensTestEnvChain.id)).toBe(true);
});
});

Expand Down
11 changes: 3 additions & 8 deletions apps/ensindexer/src/lib/ponder-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import { z } from "zod/v4";
import {
type ContractConfig,
type DatasourceName,
ensTestEnvL1Chain,
ensTestEnvL2Chain,
ensTestEnvChain,
maybeGetDatasource,
} from "@ensnode/datasources";
import type { Blockrange, ChainId, ENSNamespaceId } from "@ensnode/ensnode-sdk";
Expand Down Expand Up @@ -305,12 +304,8 @@ export function chainsConnectionConfig(
);
}

// NOTE: disable cache on local chains (e.g. ens-test-env, devnet)
const disableCache =
chainId === 31337 ||
chainId === 1337 ||
chainId === ensTestEnvL1Chain.id ||
chainId === ensTestEnvL2Chain.id;
// NOTE: disable cache on local chains (e.g. ganache, anvil, ens-test-env)
const disableCache = chainId === 31337 || chainId === 1337 || chainId === ensTestEnvChain.id;

return {
[chainId.toString()]: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import config from "@/config";

import { type Context, ponder } from "ponder:registry";
import schema from "ponder:schema";
import { type Address, hexToBigInt, labelhash } from "viem";

import { DatasourceNames } from "@ensnode/datasources";
import {
type AccountId,
accountIdEqual,
getCanonicalId,
getDatasourceContract,
getENSv2RootRegistry,
interpretAddress,
isRegistrationFullyExpired,
type LiteralLabel,
labelhashLiteralLabel,
makeENSv2DomainId,
makeRegistryId,
PluginName,
Expand Down Expand Up @@ -84,26 +77,7 @@ export default function () {
})
.onConflictDoNothing();

// TODO(ensv2): hoist this access once all namespaces declare ENSv2 contracts
const ENSV2_ROOT_REGISTRY = getENSv2RootRegistry(config.namespace);
const ENSV2_L2_ETH_REGISTRY = getDatasourceContract(
config.namespace,
DatasourceNames.ENSv2ETHRegistry,
"ETHRegistry",
);

// if this Registry is Bridged, we know its Canonical Domain and can set it here
// TODO(bridged-registries): generalize this to future ENSv2 Bridged Resolvers
if (accountIdEqual(registry, ENSV2_L2_ETH_REGISTRY)) {
const domainId = makeENSv2DomainId(
ENSV2_ROOT_REGISTRY,
getCanonicalId(labelhashLiteralLabel("eth" as LiteralLabel)),
);
await context.db
.insert(schema.registryCanonicalDomain)
.values({ registryId: registryId, domainId })
.onConflictDoUpdate({ domainId });
}
// TODO(bridged-registries): upon registry creation, write the registry's canonical domain here

// ensure discovered Label
await ensureLabel(context, label);
Expand Down
11 changes: 5 additions & 6 deletions apps/ensindexer/src/plugins/ensv2/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ const ALL_DATASOURCE_NAMES = [
DatasourceNames.Basenames,
DatasourceNames.Lineanames,
DatasourceNames.ENSv2Root,
DatasourceNames.ENSv2ETHRegistry,
];

export default createPlugin({
Expand All @@ -81,8 +80,8 @@ export default createPlugin({
} = getRequiredDatasources(config.namespace, REQUIRED_DATASOURCE_NAMES);

const {
ENSv2ETHRegistry, //
basenames,
ENSv2Root, //
basenames, //
lineanames,
} = maybeGetDatasources(config.namespace, ALL_DATASOURCE_NAMES);

Expand Down Expand Up @@ -138,11 +137,11 @@ export default createPlugin({
[namespaceContract(pluginName, "ETHRegistrar")]: {
abi: ETHRegistrarABI,
chain: {
...(ENSv2ETHRegistry &&
...(ENSv2Root &&
chainConfigForContract(
config.globalBlockrange,
ENSv2ETHRegistry.chain.id,
ENSv2ETHRegistry.contracts.ETHRegistrar,
ENSv2Root.chain.id,
ENSv2Root.contracts.ETHRegistrar,
)),
},
},
Expand Down
Loading