Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@
---
"ensindexer": minor
"ensapi": minor
---

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.
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(
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 v2Domain over v1Domain
return v2DomainId || v1DomainId || null;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Trailing || null is redundant.

v1DomainId is already typed DomainId | null, so null || null simply evaluates to null — the trailing operand adds no information.

🧹 Proposed cleanup
-  return v2DomainId || v1DomainId || null;
+  return v2DomainId || v1DomainId;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return v2DomainId || v1DomainId || null;
return v2DomainId || v1DomainId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts` at line 36, The return
expression unnecessarily appends "|| null" because v1DomainId is already
DomainId | null; update the return in get-domain-by-fqdn (the expression
returning v2DomainId || v1DomainId || null) to simply return v2DomainId ||
v1DomainId so the redundant fallback is removed while preserving the same typing
and behavior.

}

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;
}
18 changes: 1 addition & 17 deletions apps/ensapi/src/lib/public-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import config from "@/config";

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

import { ensTestEnvL1Chain } from "@ensnode/datasources";
import type { ChainId } from "@ensnode/ensnode-sdk";

const _cache = new Map<ChainId, PublicClient>();
Expand All @@ -24,21 +23,6 @@ export function getPublicClient(chainId: ChainId): PublicClient {
// Create an viem#PublicClient that uses a fallback() transport with all specified HTTP RPCs
createPublicClient({
transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))),
ccipRead: {
async request({ data, sender, urls }) {
// When running in Docker, ENSApi's viem should fetch the UniversalResolverGateway at
// http://devnet:8547 rather than the default of http://localhost:8547, which is unreachable
// 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.
if (chainId === ensTestEnvL1Chain.id) {
return ccipRequest({ data, sender, urls: [...urls, "http://devnet:8547"] });
}

// otherwise, handle as normal
return ccipRequest({ data, sender, urls });
},
},
}),
);
}
Expand Down
7 changes: 3 additions & 4 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 Expand Up @@ -744,7 +743,7 @@ describe("config (minimal base env)", () => {
NAMESPACE: "ens-test-env",
LABEL_SET_ID: "ens-test-env",
LABEL_SET_VERSION: "0",
RPC_URL_15658733: VALID_RPC_URL,
[`RPC_URL_${ensTestEnvChain.id}`]: VALID_RPC_URL,
});
await expect(getConfig()).resolves.toMatchObject({
namespace: ENSNamespaceIds.EnsTestEnv,
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 Expand Up @@ -183,11 +157,6 @@ export default function () {

// update Registration
await context.db.update(schema.registration, { id: registration.id }).set({ expiry });

// if newExpiry is 0, this is an `unregister` call, related to ejecting
// https://github.com/ensdomains/namechain/blob/9e31679f4ee6d8abb4d4e840cdf06f2d653a706b/contracts/src/L1/bridge/L1BridgeController.sol#L141
// TODO(migration): maybe do something special with this state?
// if (expiry === 0n) return;
},
);

Expand Down
29 changes: 9 additions & 20 deletions apps/ensindexer/src/plugins/ensv2/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
/**
* TODO
* - root can be inserted on setup or could be discovered naturally — see how that affects traversal/graphql api
* - probably easier to just insert it ahead of time like previously
* - move registration expiration shared logic to sdk/ens
* - update isRegistrationFullyExpired todo in ensapi somewhere
* - probably easier to just insert it ahead of time like previously?
* - 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
* - ThreeDNS
* - Migration
* - need to understand migration pattern better
* - individual names are migrated to v2 and can choose to move to an ENSv2 Registry on L1 or L2
* - locked names (wrapped and not unwrappable) are 'frozen' by having their fuses burned
* - will need to observe the correct event and then override the existing domain/registratioon info
* - for MigratedWrappedNameRegistries, need to check name expiry during resolution and avoid resolving expired names
* - autocomplete api
* - Query.permissions(by: { contract: { } })
* - Migration status/state
* - custom wrapper for resolveCursorConnection with typesafety that applies defaults and auto-decodes cursors to the indicated type
* - Pothos envelop plugins (aliases, depth, tokens, whatever)
*
* PENDING ENS TEAM
* - Domain.canonical/Domain.canonicalPath/Domain.fqdn depends on:
* - depends on: Registry.canonicalName implementation + indexing
* - Canonical Domain tracking
* - Signal Pattern for Registry contracts
* - depends on: ens team implementing in namechain contracts
* - depends on: ens team implementing in v2 contracts
*
* MAYBE DO LATER?
* - ? better typechecking for polymorphic entities in drizzle schema
Expand Down Expand Up @@ -69,7 +59,6 @@ const ALL_DATASOURCE_NAMES = [
DatasourceNames.Basenames,
DatasourceNames.Lineanames,
DatasourceNames.ENSv2Root,
DatasourceNames.ENSv2ETHRegistry,
];

export default createPlugin({
Expand All @@ -81,8 +70,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 +127,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