# Ens

> ENS (Ethereum Name Service) development — name resolution, registration via commit-reveal, text/address records, reverse resolution, avatar retrieval, subdomains, and Name Wrapper integration. Covers viem's built-in ENS actions, resolver patterns, CCIP-Read for offchain data, and contract interactions with the ENS registry on Ethereum mainnet.

- Skill: `justaname-id/ens` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add justaname-id/ens`
- Raw SKILL.md: https://api.skillmd.com/api/skills/justaname-id/ens/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: Apache-2.0
- Author: justaname-id (https://skillmd.com/u/justaname-id)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/justaname-id/ens

---


# ENS (Ethereum Name Service)

ENS maps human-readable names (alice.eth) to Ethereum addresses, content hashes, and arbitrary metadata. It is the identity layer for Ethereum — used for wallets, dApps, and onchain profiles. The architecture separates the registry (who owns a name) from resolvers (what data a name points to).

## What You Probably Got Wrong

- **ENS uses `namehash`, not plain strings** -- The registry and resolvers never see "alice.eth" as a string. Names are normalized (UTS-46), then hashed with the recursive `namehash` algorithm (EIP-137). If you pass a raw string to a contract call, it will not work. viem handles this automatically in its ENS actions but you must use `namehash()` and `labelhash()` for direct contract calls.
- **Registry vs Resolver vs Registrar -- three different contracts** -- The Registry tracks name ownership and which resolver to use. The Resolver stores records (address, text, contenthash). The Registrar handles `.eth` name registration and renewal. Confusing these is the most common ENS integration bug.
- **`.eth` registrar uses commit-reveal, not a single transaction** -- Registration requires two transactions separated by at least 60 seconds: first `commit(secret)`, wait, then `register(name, owner, duration, secret, ...)`. This prevents frontrunning. Skipping the wait or reusing a secret will revert.
- **Reverse resolution is opt-in** -- An address only has a "primary name" if the owner explicitly set it via the Reverse Registrar. Do not assume every address has a reverse record. Always handle `null` returns from `getEnsName()`.
- **Name Wrapper changes ownership semantics** -- Since 2023, ENS names can be "wrapped" as ERC-1155 tokens via the Name Wrapper contract. Wrapped names have fuses that permanently restrict operations (cannot unwrap, cannot set resolver, etc.). Check `isWrapped` before assuming standard ownership patterns.
- **CCIP-Read (ERC-3668) enables offchain resolution** -- Resolvers can return an `OffchainLookup` error that instructs the client to fetch data from an offchain gateway and verify it onchain. This powers offchain subdomains, L2 resolution, and gasless record updates. viem handles CCIP-Read automatically.
- **Wildcard resolution (ENSIP-10) is real** -- Resolvers can implement `resolve(bytes name, bytes data)` to handle any subdomain dynamically, even ones not explicitly registered. This is how services like cb.id and lens.xyz work.
- **ENS names expire** -- `.eth` names require annual renewal. Expired names enter a 90-day grace period, then a 21-day premium auction, then become available. Do not cache resolution results indefinitely.
- **`normalize()` before any ENS operation** -- Names must be UTS-46 normalized before hashing. "Alice.ETH" and "alice.eth" produce different hashes. viem normalizes automatically, but if you build raw calldata you must normalize first using `@adraffy/ens-normalize`.

## Quick Start

### Installation

```bash
npm install viem
```

viem has built-in ENS support -- no additional packages needed for resolution.

### Forward Resolution (Name to Address)

```typescript
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const address = await client.getEnsAddress({
  name: "vitalik.eth",
});
// "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```

### Reverse Resolution (Address to Name)

```typescript
const name = await client.getEnsName({
  address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
});
// "vitalik.eth" (or null if no primary name set)
```

### Get Avatar

```typescript
const avatar = await client.getEnsAvatar({
  name: "vitalik.eth",
});
// HTTPS URL to avatar image, or null
```

## Name Resolution

### Forward Resolution with Coin Types

ENS can store addresses for any blockchain, not just Ethereum. Each chain has a SLIP-44 coin type.

```typescript
const ethAddress = await client.getEnsAddress({
  name: "vitalik.eth",
});

// BTC address (coin type 0)
const btcAddress = await client.getEnsAddress({
  name: "vitalik.eth",
  coinType: 0,
});

// Solana address (coin type 501)
const solAddress = await client.getEnsAddress({
  name: "vitalik.eth",
  coinType: 501,
});
```

### Text Records

ENS text records store arbitrary key-value metadata. Standard keys are defined in ENSIP-5.

```typescript
const twitter = await client.getEnsText({
  name: "vitalik.eth",
  key: "com.twitter",
});

const github = await client.getEnsText({
  name: "vitalik.eth",
  key: "com.github",
});

const email = await client.getEnsText({
  name: "vitalik.eth",
  key: "email",
});

const url = await client.getEnsText({
  name: "vitalik.eth",
  key: "url",
});

const description = await client.getEnsText({
  name: "vitalik.eth",
  key: "description",
});

// Avatar is also a text record (ENSIP-12 supports NFT references)
const avatarRecord = await client.getEnsText({
  name: "vitalik.eth",
  key: "avatar",
});
// Can be HTTPS URL, IPFS URI, or NFT reference like
// "eip155:1/erc721:0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1234"
```

#### Standard Text Record Keys

| Key | Description |
|-----|-------------|
| `email` | Email address |
| `url` | Website URL |
| `avatar` | Avatar image (HTTPS, IPFS, or NFT reference) |
| `description` | Short bio |
| `display` | Display name (may differ from ENS name) |
| `com.twitter` | Twitter/X handle |
| `com.github` | GitHub username |
| `com.discord` | Discord username |
| `org.telegram` | Telegram handle |
| `notice` | Contract notice text |
| `keywords` | Comma-separated keywords |
| `header` | Profile header/banner image |

### Content Hash

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const RESOLVER_ABI = parseAbi([
  "function contenthash(bytes32 node) view returns (bytes)",
]);

const node = namehash("vitalik.eth");

// First get the resolver address
const resolverAddress = await client.getEnsResolver({
  name: "vitalik.eth",
});

const contenthash = await client.readContract({
  address: resolverAddress,
  abi: RESOLVER_ABI,
  functionName: "contenthash",
  args: [node],
});
// Encoded content hash (IPFS, Swarm, Arweave, etc.)
```

### Batch Resolution with Multicall

```typescript
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const RESOLVER_ABI = parseAbi([
  "function addr(bytes32 node) view returns (address)",
  "function text(bytes32 node, string key) view returns (string)",
]);

const node = namehash("vitalik.eth");

const resolverAddress = await client.getEnsResolver({
  name: "vitalik.eth",
});

const results = await client.multicall({
  contracts: [
    {
      address: resolverAddress,
      abi: RESOLVER_ABI,
      functionName: "addr",
      args: [node],
    },
    {
      address: resolverAddress,
      abi: RESOLVER_ABI,
      functionName: "text",
      args: [node, "com.twitter"],
    },
    {
      address: resolverAddress,
      abi: RESOLVER_ABI,
      functionName: "text",
      args: [node, "com.github"],
    },
    {
      address: resolverAddress,
      abi: RESOLVER_ABI,
      functionName: "text",
      args: [node, "url"],
    },
  ],
});

const [addr, twitter, github, url] = results.map((r) => r.result);
```

## Registration

### Commit-Reveal Process

ENS `.eth` registration uses a two-step commit-reveal to prevent frontrunning. You must wait at least 60 seconds between commit and register.

```typescript
import {
  createPublicClient,
  createWalletClient,
  http,
  parseAbi,
  encodePacked,
  keccak256,
  parseEther,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const ETH_REGISTRAR_CONTROLLER =
  "0x253553366Da8546fC250F225fe3d25d0C782303b" as const;

const CONTROLLER_ABI = parseAbi([
  "function rentPrice(string name, uint256 duration) view returns (tuple(uint256 base, uint256 premium))",
  "function available(string name) view returns (bool)",
  "function makeCommitment(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) pure returns (bytes32)",
  "function commit(bytes32 commitment) external",
  "function register(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) payable",
]);

const PUBLIC_RESOLVER = "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63" as const;

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

async function registerName(label: string, durationSeconds: bigint) {
  // 1. Check availability
  const isAvailable = await client.readContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI,
    functionName: "available",
    args: [label],
  });
  if (!isAvailable) throw new Error(`${label}.eth is not available`);

  // 2. Get price
  const rentPrice = await client.readContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI,
    functionName: "rentPrice",
    args: [label, durationSeconds],
  });
  // Add 10% buffer for price fluctuation during commit-reveal wait
  const totalPrice =
    ((rentPrice.base + rentPrice.premium) * 110n) / 100n;

  // 3. Generate secret (random 32 bytes)
  const secret = keccak256(
    encodePacked(["address", "uint256"], [account.address, BigInt(Date.now())])
  );

  // 4. Create commitment
  const commitment = await client.readContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI,
    functionName: "makeCommitment",
    args: [
      label,
      account.address,
      durationSeconds,
      secret,
      PUBLIC_RESOLVER,
      [],              // data (encoded resolver calls to set records at registration)
      true,            // reverseRecord (set as primary name)
      0,               // ownerControlledFuses (0 = no fuses)
    ],
  });

  // 5. Submit commitment
  const commitHash = await walletClient.writeContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI,
    functionName: "commit",
    args: [commitment],
  });
  await client.waitForTransactionReceipt({ hash: commitHash });
  console.log("Commitment submitted. Waiting 60 seconds...");

  // 6. Wait at least 60 seconds (minCommitmentAge)
  await new Promise((resolve) => setTimeout(resolve, 65_000));

  // 7. Register
  const registerHash = await walletClient.writeContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI,
    functionName: "register",
    args: [
      label,
      account.address,
      durationSeconds,
      secret,
      PUBLIC_RESOLVER,
      [],
      true,
      0,
    ],
    value: totalPrice,
  });
  const receipt = await client.waitForTransactionReceipt({
    hash: registerHash,
  });

  if (receipt.status !== "success") {
    throw new Error("Registration transaction reverted");
  }

  console.log(`Registered ${label}.eth for ${durationSeconds / 31536000n} year(s)`);
  return receipt;
}

// Register for 1 year (365 days in seconds)
await registerName("myname", 31536000n);
```

### Renewal

```typescript
const CONTROLLER_ABI_RENEW = parseAbi([
  "function rentPrice(string name, uint256 duration) view returns (tuple(uint256 base, uint256 premium))",
  "function renew(string name, uint256 duration) payable",
]);

async function renewName(label: string, durationSeconds: bigint) {
  const rentPrice = await client.readContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI_RENEW,
    functionName: "rentPrice",
    args: [label, durationSeconds],
  });
  // 5% buffer for price changes
  const totalPrice = ((rentPrice.base + rentPrice.premium) * 105n) / 100n;

  const hash = await walletClient.writeContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI_RENEW,
    functionName: "renew",
    args: [label, durationSeconds],
    value: totalPrice,
  });
  const receipt = await client.waitForTransactionReceipt({ hash });

  if (receipt.status !== "success") {
    throw new Error("Renewal transaction reverted");
  }

  console.log(`Renewed ${label}.eth for ${durationSeconds / 31536000n} year(s)`);
  return receipt;
}
```

### Check Price Before Registering

```typescript
async function getRegistrationCost(
  label: string,
  durationSeconds: bigint
): Promise<{ base: bigint; premium: bigint; total: bigint }> {
  const rentPrice = await client.readContract({
    address: ETH_REGISTRAR_CONTROLLER,
    abi: CONTROLLER_ABI,
    functionName: "rentPrice",
    args: [label, durationSeconds],
  });
  return {
    base: rentPrice.base,
    premium: rentPrice.premium,
    total: rentPrice.base + rentPrice.premium,
  };
}
```

## Working with Resolvers

### Setting Text Records

```typescript
import {
  createPublicClient,
  createWalletClient,
  http,
  parseAbi,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";

const PUBLIC_RESOLVER = "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63" as const;

const RESOLVER_ABI = parseAbi([
  "function setText(bytes32 node, string key, string value) external",
  "function setAddr(bytes32 node, address addr) external",
  "function setAddr(bytes32 node, uint256 coinType, bytes value) external",
  "function setContenthash(bytes32 node, bytes hash) external",
  "function multicall(bytes[] data) external returns (bytes[])",
]);

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const node = namehash("myname.eth");

// Set a single text record
const hash = await walletClient.writeContract({
  address: PUBLIC_RESOLVER,
  abi: RESOLVER_ABI,
  functionName: "setText",
  args: [node, "com.twitter", "myhandle"],
});
await client.waitForTransactionReceipt({ hash });
```

### Batch Update Records with Multicall

Setting multiple records in a single transaction using the resolver's built-in multicall.

```typescript
import { encodeFunctionData } from "viem";

const node = namehash("myname.eth");

const calls = [
  encodeFunctionData({
    abi: RESOLVER_ABI,
    functionName: "setText",
    args: [node, "com.twitter", "myhandle"],
  }),
  encodeFunctionData({
    abi: RESOLVER_ABI,
    functionName: "setText",
    args: [node, "com.github", "mygithub"],
  }),
  encodeFunctionData({
    abi: RESOLVER_ABI,
    functionName: "setText",
    args: [node, "url", "https://mysite.com"],
  }),
  encodeFunctionData({
    abi: RESOLVER_ABI,
    functionName: "setText",
    args: [node, "email", "me@mysite.com"],
  }),
  encodeFunctionData({
    abi: RESOLVER_ABI,
    functionName: "setText",
    args: [node, "avatar", "https://mysite.com/avatar.png"],
  }),
];

const hash = await walletClient.writeContract({
  address: PUBLIC_RESOLVER,
  abi: RESOLVER_ABI,
  functionName: "multicall",
  args: [calls],
});
const receipt = await client.waitForTransactionReceipt({ hash });

if (receipt.status !== "success") {
  throw new Error("Multicall record update reverted");
}
```

### Setting the Primary Name (Reverse Record)

```typescript
const REVERSE_REGISTRAR = "0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb" as const;

const REVERSE_ABI = parseAbi([
  "function setName(string name) external returns (bytes32)",
]);

const hash = await walletClient.writeContract({
  address: REVERSE_REGISTRAR,
  abi: REVERSE_ABI,
  functionName: "setName",
  args: ["myname.eth"],
});
await client.waitForTransactionReceipt({ hash });
```

## Subdomains

### Creating an Onchain Subdomain

```typescript
import { parseAbi } from "viem";
import { namehash, labelhash } from "viem/ens";

const ENS_REGISTRY = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" as const;

const REGISTRY_ABI = parseAbi([
  "function setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl) external",
  "function owner(bytes32 node) view returns (address)",
  "function resolver(bytes32 node) view returns (address)",
]);

const parentNode = namehash("myname.eth");
const subLabel = labelhash("sub");

const hash = await walletClient.writeContract({
  address: ENS_REGISTRY,
  abi: REGISTRY_ABI,
  functionName: "setSubnodeRecord",
  args: [
    parentNode,
    subLabel,
    account.address,       // owner of sub.myname.eth
    PUBLIC_RESOLVER,       // resolver
    0n,                    // TTL
  ],
});
await client.waitForTransactionReceipt({ hash });
// sub.myname.eth now exists and points to PUBLIC_RESOLVER
```

### Offchain Subdomains (CCIP-Read / ERC-3668)

Offchain subdomains let you issue unlimited subdomains without gas costs. The resolver responds with an `OffchainLookup` error that directs the client to a gateway URL. The gateway returns signed data that is verified onchain.

This is how services like cb.id (Coinbase), uni.eth (Uniswap), and lens.xyz work.

For offchain resolution, viem handles CCIP-Read transparently -- no client-side changes needed:

```typescript
// Resolving an offchain subdomain works identically to onchain names
const address = await client.getEnsAddress({
  name: "myuser.cb.id",
});
// viem automatically:
// 1. Calls resolver.resolve(...)
// 2. Catches OffchainLookup revert
// 3. Fetches from the gateway URL
// 4. Calls resolver with the gateway proof
// 5. Returns the verified address

const avatar = await client.getEnsAvatar({
  name: "myuser.cb.id",
});
```

To build your own offchain resolver, implement ERC-3668:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IExtendedResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/IExtendedResolver.sol";

/// @notice Offchain resolver that delegates lookups to a gateway
/// @dev Implements ERC-3668 (CCIP-Read) and ENSIP-10 (wildcard resolution)
contract OffchainResolver is IExtendedResolver {
    string public url;
    address public signer;

    error OffchainLookup(
        address sender,
        string[] urls,
        bytes callData,
        bytes4 callbackFunction,
        bytes extraData
    );

    constructor(string memory _url, address _signer) {
        url = _url;
        signer = _signer;
    }

    /// @notice ENSIP-10 wildcard resolve entry point
    function resolve(
        bytes calldata name,
        bytes calldata data
    ) external view returns (bytes memory) {
        string[] memory urls = new string[](1);
        urls[0] = url;

        revert OffchainLookup(
            address(this),
            urls,
            data,
            this.resolveWithProof.selector,
            abi.encode(name, data)
        );
    }

    /// @notice Callback that verifies the gateway signature
    function resolveWithProof(
        bytes calldata response,
        bytes calldata extraData
    ) external view returns (bytes memory) {
        // Verify signature from gateway matches expected signer
        // Return decoded result
        // Implementation depends on your signing scheme
    }
}
```

## Contract Addresses

Ethereum Mainnet. Last verified: 2025-03-01.

| Contract | Address | Purpose |
|----------|---------|---------|
| ENS Registry | `0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e` | Core registry -- maps names to owners and resolvers |
| Public Resolver | `0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63` | Default resolver for address, text, contenthash, and ABI records |
| ETH Registrar Controller | `0x253553366Da8546fC250F225fe3d25d0C782303b` | Handles .eth name registration and renewal (commit-reveal) |
| Name Wrapper | `0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401` | Wraps names as ERC-1155 tokens with permission fuses |
| Reverse Registrar | `0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb` | Manages reverse records (address-to-name mapping) |
| Base Registrar (NFT) | `0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85` | ERC-721 NFT for .eth second-level names |
| Universal Resolver | `0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4` | Batch resolution with CCIP-Read support |

### Address Constants for TypeScript

```typescript
const ENS_ADDRESSES = {
  registry: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
  publicResolver: "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63",
  ethRegistrarController: "0x253553366Da8546fC250F225fe3d25d0C782303b",
  nameWrapper: "0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401",
  reverseRegistrar: "0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb",
  baseRegistrar: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
  universalResolver: "0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4",
} as const satisfies Record<string, `0x${string}`>;
```

### Sepolia Testnet

Last verified: 2025-03-01.

| Contract | Address |
|----------|---------|
| ENS Registry | `0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e` |
| Public Resolver | `0x8FADE66B79cC9f707aB26799354482EB93a5B7dD` |
| ETH Registrar Controller | `0xFED6a969AaA60E4961FCD3EBF1A2e8913DeBe6c7` |
| Name Wrapper | `0x0635513f179D50A207757E05759CbD106d7dFcE8` |
| Reverse Registrar | `0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6` |

## Error Handling

### Common Resolution Errors

```typescript
async function safeResolve(name: string) {
  try {
    const address = await client.getEnsAddress({ name });
    if (!address) {
      console.log(`${name} has no address record set`);
      return null;
    }
    return address;
  } catch (error) {
    if (error instanceof Error) {
      // Name does not exist or is malformed
      if (error.message.includes("Could not find resolver")) {
        console.log(`${name} is not registered or has no resolver`);
        return null;
      }
      // CCIP-Read gateway failure
      if (error.message.includes("OffchainLookup")) {
        console.log(`Offchain resolution failed for ${name}`);
        return null;
      }
    }
    throw error;
  }
}
```

### Common Registration Errors

| Error | Cause | Fix |
|-------|-------|-----|
| `CommitmentTooNew` | Called `register()` less than 60s after `commit()` | Wait at least 60 seconds between commit and register |
| `CommitmentTooOld` | Commitment expired (older than 24 hours) | Submit a new commitment |
| `NameNotAvailable` | Name is registered or in grace period | Check `available()` first |
| `DurationTooShort` | Duration under minimum (28 days) | Use at least 2419200 seconds |
| `InsufficientValue` | Sent less ETH than `rentPrice()` requires | Add a 5-10% buffer to `rentPrice()` result |
| `Unauthorised` | Caller is not the name owner | Verify ownership via registry before writing records |

### Validating ENS Names

```typescript
import { normalize } from "viem/ens";

function isValidEnsName(name: string): boolean {
  try {
    normalize(name);
    return true;
  } catch {
    return false;
  }
}

// normalize() throws on invalid names
// Valid: "alice.eth", "sub.alice.eth", "alice.xyz"
// Invalid: names with zero-width characters, confusable Unicode, etc.
```

## Key Constants

| Constant | Value | Notes |
|----------|-------|-------|
| Min commitment age | 60 seconds | Wait between commit and register |
| Max commitment age | 86400 seconds (24h) | Commitment expires after this |
| Min registration duration | 2419200 seconds (28 days) | Shortest allowed registration |
| Grace period | 90 days | After expiry, owner can still renew |
| Premium auction | 21 days | After grace period, decaying price auction |
| Namehash of `eth` | `0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae` | Used as parent node for .eth names |

## References

- [ENS Documentation](https://docs.ens.domains/) -- official docs covering architecture, resolution, registration, and CCIP-Read
- [EIP-137: ENS](https://eips.ethereum.org/EIPS/eip-137) -- core ENS specification (registry, namehash, resolvers)
- [EIP-181: Reverse Resolution](https://eips.ethereum.org/EIPS/eip-181) -- reverse registrar and addr.reverse namespace
- [EIP-2304: Multichain Address Resolution](https://eips.ethereum.org/EIPS/eip-2304) -- SLIP-44 coin type support in resolvers
- [ERC-3668: CCIP-Read](https://eips.ethereum.org/EIPS/eip-3668) -- offchain data retrieval standard
- [ENSIP-5: Text Records](https://docs.ens.domains/ensip/5) -- standardized text record keys
- [ENSIP-10: Wildcard Resolution](https://docs.ens.domains/ensip/10) -- dynamic subdomain resolution
- [ENSIP-12: Avatar Text Records](https://docs.ens.domains/ensip/12) -- NFT and IPFS avatar specification
- [ENS Deployments](https://docs.ens.domains/learn/deployments) -- official contract addresses per network
- [viem ENS Actions](https://viem.sh/docs/ens/getting-started) -- built-in ENS resolution in viem
- [@adraffy/ens-normalize](https://github.com/adraffy/ens-normalize.js) -- reference UTS-46 normalization library used by viem

