Skip to content
Merged
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
21 changes: 14 additions & 7 deletions docs/ens-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,25 @@ fails closed (`ENSv2 is not deployed on mainnet`).

### Discovery anchor

The only configured address is the Universal Resolver proxy
`0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe` (stable across ENS redeploys).
Everything else is read at runtime: `ROOT_REGISTRY()` → `getSubregistry("eth")`
→ ETHRegistry → … Implementation addresses (VerifiableFactory, UserRegistry,
PermissionedResolver) come from `ensV2ConfigFor` defaults and are **overridable**
because ENS redeploys Sepolia periodically.
The only configured discovery address is a `UniversalResolverV2`; everything
else is read at runtime: `ROOT_REGISTRY()` → `getSubregistry("eth")` → ETHRegistry
→ … The default is the `UniversalResolverV2` of one specific Sepolia deployment
(`0x4a1817d13e9cf196f471725176355c1234b63c70`, root `0x8115…4354`), **not** the
public `UpgradableUniversalResolverProxy` `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe`:
ENS repoints that proxy between Sepolia redeploys (observed twice on 2026-09-14/15),
which moves `ROOT_REGISTRY` and orphans every name registered on the previous
stack — including `enspack.eth`. Pinning keeps discovery, the implementation
addresses and the ETHRegistrar (used by fork tests) mutually consistent. Set
`ENSPACK_ENSV2_UNIVERSAL_RESOLVER` to the proxy to follow ENS, and update all
five overrides together when moving to a new deployment. Names on a pinned
deployment resolve through enspack tooling and services; the ENS app only shows
whatever deployment the proxy currently points at.

### Deployment-specific overrides

| Env | Default (docs.ens.domains Sepolia table) |
|-----|-------------------------------------------|
| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe` |
| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | `0x4a1817d13e9cf196f471725176355c1234b63c70` (deployment's UniversalResolverV2; proxy `0xeEeE…EeEe` follows ENS) |
| `ENSPACK_ENSV2_VERIFIABLE_FACTORY` | `0x10dc6333cdfe1fcef624c6e0a8221b91804cd7ef` |
| `ENSPACK_ENSV2_USER_REGISTRY_IMPL` | `0x624a25d67b59d587752ebec8dded8827dae52050` |
| `ENSPACK_ENSV2_PERMISSIONED_RESOLVER_IMPL` | `0x9eae5c2730a7dd16bdd1dee6421a1b91e3b0365e` |
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ webseed; see `packages/cli/test/get.e2e.test.ts`.
| `ETH_RPC_URL` | Mainnet (and Anvil fork) JSON-RPC |
| `SEPOLIA_RPC_URL` | Sepolia JSON-RPC |
| `ENSPACK_ENS_VERSION` | `v1` or `v2`. Overrides `--chain` default (Sepolia → v2, mainnet → v1) |
| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | ENSv2 Universal Resolver (default `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe`) |
| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | ENSv2 UniversalResolverV2 (default `0x4a1817d13e9cf196f471725176355c1234b63c70`, a pinned Sepolia deployment; see `docs/ens-v2.md`) |
| `ENSPACK_ENSV2_VERIFIABLE_FACTORY` | VerifiableFactory (Sepolia redeploys; override if ENS docs change) |
| `ENSPACK_ENSV2_USER_REGISTRY_IMPL` | UserRegistry implementation |
| `ENSPACK_ENSV2_PERMISSIONED_RESOLVER_IMPL` | PermissionedResolver implementation |
Expand Down
17 changes: 11 additions & 6 deletions packages/cli/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ function parseGateways(env: NodeJS.ProcessEnv): string[] {
return [...extras, ...DEFAULT_GATEWAYS];
}

function isPrivateKey(value: string): value is `0x${string}` {
return /^0x[0-9a-fA-F]{64}$/.test(value);
/** Secret stores often strip the `0x`; accept both forms, never log the value. */
function normalizePrivateKey(value: string): `0x${string}` | null {
const trimmed = value.trim();
const hex = trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.slice(2) : trimmed;
return /^[0-9a-fA-F]{64}$/.test(hex) ? `0x${hex}` : null;
}

/**
Expand All @@ -61,10 +64,11 @@ function publisherFromEnv(env: NodeJS.ProcessEnv): CliDeps["publisherFactory"] {
},
};
}
if (!isPrivateKey(key)) {
const normalized = normalizePrivateKey(key);
if (normalized === null) {
throw new EnspackError("PUBLISH", "ENSPACK_PUBLISHER_KEY must be a 32-byte hex private key");
}
const account = privateKeyToAccount(key);
const account = privateKeyToAccount(normalized);
const viemChain = chain === "sepolia" ? sepolia : mainnet;
const client = publicClientFor(chain, rpcUrl);
const wallet = createWalletClient({
Expand Down Expand Up @@ -102,13 +106,14 @@ function operatorKeyFromEnv(env: NodeJS.ProcessEnv): `0x${string}` {
"ENSPACK_OPERATOR_KEY is not set (falls back to ENSPACK_PUBLISHER_KEY)",
);
}
if (!isPrivateKey(key)) {
const normalized = normalizePrivateKey(key);
if (normalized === null) {
throw new EnspackError(
"PUBLISH",
"ENSPACK_OPERATOR_KEY (or ENSPACK_PUBLISHER_KEY) must be a 32-byte hex private key",
);
}
return key;
return normalized;
}

function ensSetupFromEnv(env: NodeJS.ProcessEnv): NonNullable<CliDeps["ensSetupFactory"]> {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("ensOptsFor (WP-17)", () => {
it("selects v2 on sepolia by default", () => {
const opts = ensOptsFor("sepolia", {});
expect(opts.ensVersion).toBe("v2");
expect(opts.ensV2?.universalResolver).toBe("0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe");
expect(opts.ensV2?.universalResolver).toBe("0x4A1817d13E9cF196f471725176355C1234b63C70");
});

it("selects v1 on mainnet by default", () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/ens/v2/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ export interface EnsV2Config {
ethRegistrar: `0x${string}`;
}

/**
* One coherent Sepolia deployment (docs.ens.domains table, 2026-05-25 stack). The public
* `UpgradableUniversalResolverProxy` (0xeEeE…EeEe) is repointed by ENS between redeploys, which
* silently moves ROOT_REGISTRY and orphans everything registered on the previous stack; pinning
* this deployment's own UniversalResolverV2 keeps discovery, impls and ETHRegistrar consistent.
* Set ENSPACK_ENSV2_UNIVERSAL_RESOLVER=0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe to follow ENS.
*/
const SEPOLIA_DEFAULTS = {
universalResolver: "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe",
universalResolver: "0x4a1817d13e9cf196f471725176355c1234b63c70",
verifiableFactory: "0x10dc6333cdfe1fcef624c6e0a8221b91804cd7ef",
userRegistryImpl: "0x624a25d67b59d587752ebec8dded8827dae52050",
permissionedResolverImpl: "0x9eae5c2730a7dd16bdd1dee6421a1b91e3b0365e",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/ens-v2/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { labelhash } from "viem/ens";
import { describe, expect, it } from "vitest";
import { EnspackError, ensV2ConfigFor, ensVersionFor, labelId } from "../../src/index.js";

const UR = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe";
const UR = "0x4a1817d13e9cf196f471725176355c1234b63c70";
const FACTORY = "0x10dc6333cdfe1fcef624c6e0a8221b91804cd7ef";
const ALT = "0x0000000000000000000000000000000000000001";

Expand Down
9 changes: 5 additions & 4 deletions services/registrar/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ function requireEnv(name: string): string {
}

function operatorAccount() {
const key = env("ENSPACK_OPERATOR_KEY");
if (key === undefined || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
throw new Error("ENSPACK_OPERATOR_KEY must be a 0x-prefixed 32-byte hex private key");
const raw = env("ENSPACK_OPERATOR_KEY")?.trim();
const hex = raw === undefined ? undefined : raw.replace(/^0[xX]/, "");
if (hex === undefined || !/^[0-9a-fA-F]{64}$/.test(hex)) {
throw new Error("ENSPACK_OPERATOR_KEY must be a 32-byte hex private key");
}
return privateKeyToAccount(key as Hex);
return privateKeyToAccount(`0x${hex}` as Hex);
}

function chainName(): "mainnet" | "sepolia" {
Expand Down
2 changes: 1 addition & 1 deletion services/seed/test/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("seedEnsOpts", () => {
it("defaults sepolia to v2 and honors ENSPACK_ENS_VERSION", () => {
expect(seedEnsOpts("sepolia", {}).ensVersion).toBe("v2");
expect(seedEnsOpts("sepolia", {}).ensV2?.universalResolver).toBe(
"0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe",
"0x4A1817d13E9cF196f471725176355C1234b63C70",
);
expect(seedEnsOpts("sepolia", { ENSPACK_ENS_VERSION: "v1" }).ensVersion).toBe("v1");
expect(seedEnsOpts("mainnet", {}).ensVersion).toBe("v1");
Expand Down
Loading