Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/short-cobras-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/datasources": patch
---

Introduces a temporary `sepolia-v2` ENS Namespace, intended for testing of ephemeral ENSv2 deployments to the Sepolia chain. This feature is intended for developers of the ENS protocol, and is highly experimental and should be considered unstable.
4 changes: 4 additions & 0 deletions apps/ensadmin/src/lib/default-records-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const DefaultRecordsSelection = {
addresses: getCommonCoinTypes(ENSNamespaceIds.Sepolia),
texts: TEXTS,
},
[ENSNamespaceIds.SepoliaV2]: {
addresses: getCommonCoinTypes(ENSNamespaceIds.SepoliaV2),
texts: TEXTS,
},
[ENSNamespaceIds.EnsTestEnv]: {
addresses: getCommonCoinTypes(ENSNamespaceIds.EnsTestEnv),
texts: TEXTS,
Expand Down
138 changes: 75 additions & 63 deletions apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import config from "@/config";

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

import { DatasourceNames, getDatasource, maybeGetDatasource } from "@ensnode/datasources";
import { DatasourceNames } from "@ensnode/datasources";
import * as schema from "@ensnode/ensnode-schema";
import {
type DomainId,
Expand All @@ -22,24 +22,17 @@ import {
makeENSv1DomainId,
makeRegistryId,
makeSubdomainNode,
maybeGetDatasourceContract,
type RegistryId,
} from "@ensnode/ensnode-sdk";

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

const ensroot = getDatasource(config.namespace, DatasourceNames.ENSRoot);
const namechain = maybeGetDatasource(config.namespace, DatasourceNames.Namechain);
import logger from "@/lib/logger";

const ETH_LABELHASH = labelhashLiteralLabel("eth" as LiteralLabel);

const ROOT_REGISTRY_ID = getENSv2RootRegistryId(config.namespace);

const ENS_ROOT_V2_ETH_REGISTRY_ID = makeRegistryId({
chainId: ensroot.chain.id,
address: ensroot.contracts.ETHRegistry.address,
});

/**
* Gets the DomainId of the Domain addressed by `name`.
*/
Expand Down Expand Up @@ -128,72 +121,91 @@ async function v2_getDomainIdByFqdn(
// biome-ignore lint/style/noNonNullAssertion: length check above
const leaf = rows[rows.length - 1]!;

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

console.log(name);
console.log(JSON.stringify(rows, null, 2));

// we did not find an exact match for the Domain within ENSv2 on the ENS Root Chain
///////////////////////////
// 1. 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 (leaf.registry_id === ENS_ROOT_V2_ETH_REGISTRY_ID) {
// TODO(ensv2): remove when all namspaces have Namechain datasource defined
// if namechain doesn't exist, we can't bridge the request to that Registry, so terminate
if (!namechain) return null;

// Invariant: must be >= 2LD
if (labelHashPath.length < 2) {
throw new Error(`Invariant: Not >= 2LD??`);
}

// Invariant: must be a .eth subname
if (labelHashPath[0] !== ETH_LABELHASH) {
throw new Error(`Invariant: Not .eth subname????`);
}

// Invariant: must be a .eth subname
if (leaf.label_hash !== labelhash("eth")) {
throw new Error(`Invariant: Not .eth subname??`);
}

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

// 1. 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)) {
console.log(
`ETHTLDResolver deferring to actively registered name ${dotEth2LDNode} in ENSv1...`,
);
return await v1_getDomainIdByFqdn(name);
}

// 2. otherwise, direct to Namechain ENSv2 .eth Registry
const nameWithoutTld = interpretedLabelsToInterpretedName(
interpretedNameToInterpretedLabels(name).slice(0, -1),
///////////////////////////

// TODO: can hoist these datasource lookups and registry id construction once ENSv2 is fully
// deployed in all namespaces, is for backwards-compatible behavior
// if there's no ENSv2 ETH Registry on the ENS Root Chain, the domain was not found
const ENS_ROOT_V2_ETH_REGISTRY = maybeGetDatasourceContract(
config.namespace,
DatasourceNames.ENSRoot,
"ETHRegistry",
);
if (!ENS_ROOT_V2_ETH_REGISTRY) return null;

const ENS_ROOT_V2_ETH_REGISTRY_ID = makeRegistryId(ENS_ROOT_V2_ETH_REGISTRY);

// if the path did not terminate at the .eth Registry, then there's nothing to be done and the domain was not found
if (leaf.registry_id !== ENS_ROOT_V2_ETH_REGISTRY_ID) 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}).`,
);
console.log(
`ETHTLDResolver deferring '${nameWithoutTld}' to ENSv2 .eth Registry on Namechain...`,
}

// 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}).`,
);
}

const NAMECHAIN_V2_ETH_REGISTRY_ID = makeRegistryId({
chainId: namechain.chain.id,
address: namechain.contracts.ETHRegistry.address,
});
// construct the node of the 2ld
const dotEth2LDNode = makeSubdomainNode(labelHashPath[1], ETH_NODE);

return v2_getDomainIdByFqdn(NAMECHAIN_V2_ETH_REGISTRY_ID, nameWithoutTld);
// 1. 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);
}

// finally, not found
return null;
// 2. otherwise, direct to Namechain ENSv2 .eth Registry
// if there's no ETHRegistry on Namechain, the domain was not found
const NAMECHAIN_V2_ETH_REGISTRY = maybeGetDatasourceContract(
config.namespace,
DatasourceNames.Namechain,
"ETHRegistry",
);
if (!NAMECHAIN_V2_ETH_REGISTRY) return null;

const NAMECHAIN_V2_ETH_REGISTRY_ID = makeRegistryId(NAMECHAIN_V2_ETH_REGISTRY);

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

return v2_getDomainIdByFqdn(NAMECHAIN_V2_ETH_REGISTRY_ID, nameWithoutTld, { now });
}
13 changes: 8 additions & 5 deletions apps/ensindexer/src/lib/ponder-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,19 @@ 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;

return {
[chainId.toString()]: {
id: chainId,
rpc: rpcConfig.httpRPCs.map((httpRPC) => httpRPC.toString()),
ws: rpcConfig.websocketRPC?.toString(),
// NOTE: disable cache on local chains (e.g. Anvil, Ganache)
...((chainId === 31337 ||
chainId === 1337 ||
chainId === ensTestEnvL1Chain.id ||
chainId === ensTestEnvL2Chain.id) && { disableCache: true }),
disableCache,
} satisfies ChainConfig,
};
}
Expand Down
29 changes: 13 additions & 16 deletions apps/ensindexer/src/plugins/ensv2/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
* - RequiredAndNotNull opposite type: RequiredToBeNull<T, keys> for constraining polymorphic entities in graphql schema
* - re-asses NameWrapper expiry logic — compare to subgraph implementation & see if we can simplify
* - indexes based on graphql queries, ask claude to compile recommendations
* - modify Registration schema to more closely match ENSv2, map v1 into it
* - Renewals (v1, v2)
* - include similar /latest / superceding logic, need to be able to reference latest renewal to upsert referrers
* - ThreeDNS
* - Migration
* - need to understand migration pattern better
Expand All @@ -21,11 +18,8 @@
* - Query.permissions(by: { contract: { } })
* - custom wrapper for resolveCursorConnection with typesafety that applies defaults and auto-decodes cursors to the indicated type
* - Pothos envelop plugins (aliases, depth, tokens, whatever)
* - BEFORE MERGE: revert sepolia.ts namespace back to original, including ensv2 stubs
*
* PENDING ENS TEAM
* - DedicatedResolver moving to EAC
* - depends on: namechain --testNames script not crashing in commit >= 803a940
* - Domain.canonical/Domain.canonicalPath/Domain.fqdn depends on:
* - depends on: Registry.canonicalName implementation + indexing
* - Signal Pattern for Registry contracts
Expand Down Expand Up @@ -105,17 +99,18 @@ export default createPlugin({
abi: RegistryABI,
chain: [ensroot, namechain]
.filter((ds) => !!ds)
.reduce(
(memo, datasource) => ({
.reduce((memo, datasource) => {
// TODO(ensv2-coverage): remove once ENSv2 exists in all namespaces
if (!("Registry" in datasource.contracts)) return memo;
return {
...memo,
...chainConfigForContract(
config.globalBlockrange,
datasource.chain.id,
datasource.contracts.Registry,
),
}),
{},
),
};
}, {}),
},

///////////////////////////////////
Expand All @@ -125,17 +120,19 @@ export default createPlugin({
abi: EnhancedAccessControlABI,
chain: [ensroot, namechain]
.filter((ds) => !!ds)
.reduce(
(memo, datasource) => ({
.reduce((memo, datasource) => {
// TODO(ensv2-coverage): remove once ENSv2 exists in all namespaces
if (!("EnhancedAccessControl" in datasource.contracts)) return memo;

return {
...memo,
...chainConfigForContract(
config.globalBlockrange,
datasource.chain.id,
datasource.contracts.EnhancedAccessControl,
),
}),
{},
),
};
}, {}),
},

//////////////////////////
Expand Down
12 changes: 7 additions & 5 deletions apps/ensindexer/src/plugins/protocol-acceleration/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,13 @@ export default createPlugin({
[namespaceContract(pluginName, "ENSv2Registry")]: {
abi: RegistryABI,
chain: {
...chainConfigForContract(
config.globalBlockrange,
ensroot.chain.id,
ensroot.contracts.Registry,
),
// TODO(ensv2-coverage): remove this conditional once ENSv2 exists in all namespaces
...("Registry" in ensroot.contracts &&
chainConfigForContract(
config.globalBlockrange,
ensroot.chain.id,
ensroot.contracts.Registry,
)),
...(namechain &&
chainConfigForContract(
config.globalBlockrange,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
"undici@<6.23.0": ">=6.23.0",
"devalue@>=5.3.0 <=5.6.1": ">=5.6.2",
"h3@<=1.15.4": ">=1.15.5",
"tar@<=7.5.3": ">=7.5.4",
"lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23",
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23"
"tar@<=7.5.3": "^7.5.4",
"lodash@<=4.17.22": "^4.17.23",
"lodash-es@<=4.17.22": "^4.17.23"
},
"ignoredBuiltDependencies": [
"bun"
Expand Down
20 changes: 1 addition & 19 deletions packages/datasources/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { Abi, Address, Chain } from "viem";
export const ENSNamespaceIds = {
Mainnet: "mainnet",
Sepolia: "sepolia",
SepoliaV2: "sepolia-v2",
EnsTestEnv: "ens-test-env",
} as const;

Expand Down Expand Up @@ -108,25 +109,6 @@ export type ENSNamespace = {
[DatasourceNames.ENSRoot]: Datasource;
} & Partial<Record<Exclude<DatasourceName, "ensroot">, Datasource>>;

/**
* Helper type to merge multiple types into one.
*/
type MergedTypes<T> = (T extends any ? (x: T) => void : never) extends (x: infer R) => void
? R
: never;

/**
* Preserves the chain union while merging contracts from multiple objects
*/
export type MergeNamespaces<T extends ENSNamespace> = T extends ENSNamespace
? {
chain: T extends { chain: infer C } ? C : never;
contracts: T extends { [DatasourceNames.ENSRoot]: { contracts: infer C } }
? MergedTypes<C>
: never;
}
: never;

/**
* Helper type to extract the datasource type for a specific datasource name across all namespaces.
* Returns the union of all possible datasource types for that datasource name, or never if not found.
Expand Down
Loading