Skip to main content
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.

ENS Resolution

Resolve Ethereum Name Service (ENS) names to addresses and retrieve ENS records using Voltaire primitives and Contract modules.

Overview

ENS resolution requires two components:
  1. Ens module - Name validation, normalization, and namehash computation
  2. Contract module - Interacting with ENS registry and resolver contracts

Contract Addresses (Mainnet)

ContractAddress
ENS Registry0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
Public Resolver0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
Reverse Registrar0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb

Resolving ENS Name to Address

import { Contract } from '@voltaire/contract';
import * as Ens from '@tevm/voltaire/Ens';
import * as Hex from '@tevm/voltaire/Hex';

// ENS Registry ABI (minimal for resolution)
const ensRegistryAbi = [
  {
    type: 'function',
    name: 'resolver',
    stateMutability: 'view',
    inputs: [{ type: 'bytes32', name: 'node' }],
    outputs: [{ type: 'address', name: '' }]
  }
] as const;

// Public Resolver ABI
const resolverAbi = [
  {
    type: 'function',
    name: 'addr',
    stateMutability: 'view',
    inputs: [{ type: 'bytes32', name: 'node' }],
    outputs: [{ type: 'address', name: '' }]
  },
  {
    type: 'function',
    name: 'text',
    stateMutability: 'view',
    inputs: [
      { type: 'bytes32', name: 'node' },
      { type: 'string', name: 'key' }
    ],
    outputs: [{ type: 'string', name: '' }]
  },
  {
    type: 'function',
    name: 'name',
    stateMutability: 'view',
    inputs: [{ type: 'bytes32', name: 'node' }],
    outputs: [{ type: 'string', name: '' }]
  }
] as const;

async function resolveEnsName(name: string, provider: any) {
  // Step 1: Validate and normalize the name
  Ens.validate(name); // Throws if invalid
  const normalized = Ens.normalize(name);

  // Step 2: Compute namehash
  const node = Ens.namehash(normalized);
  const nodeHex = Hex.fromBytes(node);

  // Step 3: Get resolver address from registry
  const registry = Contract({
    address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
    abi: ensRegistryAbi,
    provider
  });

  const resolverAddress = await registry.read.resolver(nodeHex);

  if (resolverAddress === '0x0000000000000000000000000000000000000000') {
    throw new Error(`No resolver found for ${name}`);
  }

  // Step 4: Get address from resolver
  const resolver = Contract({
    address: resolverAddress,
    abi: resolverAbi,
    provider
  });

  const address = await resolver.read.addr(nodeHex);

  return address;
}

// Usage
const address = await resolveEnsName('vitalik.eth', provider);
console.log(address); // 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Reverse Resolution (Address to ENS)

Reverse resolution looks up the primary ENS name for an address.
import { Contract } from '@voltaire/contract';
import * as Ens from '@tevm/voltaire/Ens';
import * as Hex from '@tevm/voltaire/Hex';
import * as Address from '@tevm/voltaire/Address';

async function reverseResolve(address: string, provider: any) {
  // Step 1: Compute reverse node
  // Format: <address-lowercase-without-0x>.addr.reverse
  const addrLower = address.toLowerCase().slice(2);
  const reverseName = `${addrLower}.addr.reverse`;

  const node = Ens.namehash(reverseName);
  const nodeHex = Hex.fromBytes(node);

  // Step 2: Get resolver from registry
  const registry = Contract({
    address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
    abi: ensRegistryAbi,
    provider
  });

  const resolverAddress = await registry.read.resolver(nodeHex);

  if (resolverAddress === '0x0000000000000000000000000000000000000000') {
    return null; // No reverse record set
  }

  // Step 3: Get name from resolver
  const resolver = Contract({
    address: resolverAddress,
    abi: resolverAbi,
    provider
  });

  const name = await resolver.read.name(nodeHex);

  if (!name || name === '') {
    return null;
  }

  // Step 4: Verify forward resolution matches (security check)
  const forwardAddress = await resolveEnsName(name, provider);

  if (forwardAddress.toLowerCase() !== address.toLowerCase()) {
    return null; // Forward resolution doesn't match - invalid reverse record
  }

  return name;
}

// Usage
const name = await reverseResolve(
  '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  provider
);
console.log(name); // vitalik.eth
Always verify forward resolution when doing reverse lookups. A user can set any name as their reverse record, but only the verified forward resolution is authoritative.

Getting ENS Records

ENS supports various text records like avatar, email, URL, and custom records.
async function getEnsRecords(name: string, provider: any) {
  // Validate and normalize
  Ens.validate(name);
  const normalized = Ens.normalize(name);
  const node = Ens.namehash(normalized);
  const nodeHex = Hex.fromBytes(node);

  // Get resolver
  const registry = Contract({
    address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
    abi: ensRegistryAbi,
    provider
  });

  const resolverAddress = await registry.read.resolver(nodeHex);

  if (resolverAddress === '0x0000000000000000000000000000000000000000') {
    throw new Error(`No resolver found for ${name}`);
  }

  const resolver = Contract({
    address: resolverAddress,
    abi: resolverAbi,
    provider
  });

  // Fetch multiple records
  const [avatar, url, description, twitter, github] = await Promise.all([
    resolver.read.text(nodeHex, 'avatar').catch(() => ''),
    resolver.read.text(nodeHex, 'url').catch(() => ''),
    resolver.read.text(nodeHex, 'description').catch(() => ''),
    resolver.read.text(nodeHex, 'com.twitter').catch(() => ''),
    resolver.read.text(nodeHex, 'com.github').catch(() => '')
  ]);

  return { avatar, url, description, twitter, github };
}

// Usage
const records = await getEnsRecords('vitalik.eth', provider);
console.log(records);
// {
//   avatar: 'eip155:1/erc1155:0xb32979486938aa9694bfc898f35dbed459f44424/10063',
//   url: 'https://vitalik.eth.limo',
//   description: '',
//   twitter: 'VitalikButerin',
//   github: 'vbuterin'
// }

Common Text Record Keys

KeyDescription
avatarNFT or URL for profile image
urlWebsite URL
descriptionProfile description
emailEmail address
com.twitterTwitter/X handle
com.githubGitHub username
com.discordDiscord username
noticeLegal notice

ENS Name Validation

Validate ENS names before resolution to prevent homograph attacks.
import * as Ens from '@tevm/voltaire/Ens';

// Check if valid
const isValid = Ens.isValid('vitalik.eth'); // true
const isInvalid = Ens.isValid('invalid\x00.eth'); // false

// Validate (throws on invalid)
try {
  Ens.validate('vitalik.eth'); // OK
  Ens.validate('VITALIK.ETH'); // OK (will be normalized)
  Ens.validate(''); // Throws InvalidEnsNameError
} catch (error) {
  console.error('Invalid ENS name:', error.message);
}

// Normalize names (lowercase, ENSIP-15 compliant)
const normalized = Ens.normalize('VitaliK.ETH'); // 'vitalik.eth'

// Beautify (preserve display formatting)
const beautified = Ens.beautify('TEST.eth'); // 'test.eth'
Always normalize ENS names before computing namehash. Names like “Vitalik.eth” and “vitalik.eth” have different namehashes if not normalized first.

Computing Namehash

The namehash is a recursive hash used to identify ENS names on-chain.
import * as Ens from '@tevm/voltaire/Ens';
import * as Hex from '@tevm/voltaire/Hex';

// Compute namehash for a full name
const node = Ens.namehash('vitalik.eth');
console.log(Hex.fromBytes(node));
// 0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835

// Compute labelhash for a single label
const label = Ens.labelhash('vitalik');
console.log(Hex.fromBytes(label));
// 0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc

// Empty name has zero hash
const emptyNode = Ens.namehash('');
console.log(Hex.fromBytes(emptyNode));
// 0x0000000000000000000000000000000000000000000000000000000000000000

Full Resolution Helper

A complete helper function combining all functionality:
import { Contract } from '@voltaire/contract';
import * as Ens from '@tevm/voltaire/Ens';
import * as Hex from '@tevm/voltaire/Hex';

const ENS_REGISTRY = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e';

const ensRegistryAbi = [
  {
    type: 'function',
    name: 'resolver',
    stateMutability: 'view',
    inputs: [{ type: 'bytes32', name: 'node' }],
    outputs: [{ type: 'address', name: '' }]
  }
] as const;

const resolverAbi = [
  {
    type: 'function',
    name: 'addr',
    stateMutability: 'view',
    inputs: [{ type: 'bytes32', name: 'node' }],
    outputs: [{ type: 'address', name: '' }]
  },
  {
    type: 'function',
    name: 'text',
    stateMutability: 'view',
    inputs: [
      { type: 'bytes32', name: 'node' },
      { type: 'string', name: 'key' }
    ],
    outputs: [{ type: 'string', name: '' }]
  },
  {
    type: 'function',
    name: 'name',
    stateMutability: 'view',
    inputs: [{ type: 'bytes32', name: 'node' }],
    outputs: [{ type: 'string', name: '' }]
  }
] as const;

export async function resolveEns(input: string, provider: any) {
  // Determine if input is address or name
  const isAddress = input.startsWith('0x') && input.length === 42;

  if (isAddress) {
    // Reverse resolution
    const addrLower = input.toLowerCase().slice(2);
    const reverseName = `${addrLower}.addr.reverse`;
    const node = Ens.namehash(reverseName);
    const nodeHex = Hex.fromBytes(node);

    const registry = Contract({
      address: ENS_REGISTRY,
      abi: ensRegistryAbi,
      provider
    });

    const resolverAddress = await registry.read.resolver(nodeHex);
    if (resolverAddress === '0x0000000000000000000000000000000000000000') {
      return { address: input, name: null, records: {} };
    }

    const resolver = Contract({
      address: resolverAddress,
      abi: resolverAbi,
      provider
    });

    const name = await resolver.read.name(nodeHex);
    if (!name) {
      return { address: input, name: null, records: {} };
    }

    // Verify forward resolution
    const result = await resolveEns(name, provider);
    if (result.address?.toLowerCase() !== input.toLowerCase()) {
      return { address: input, name: null, records: {} };
    }

    return { ...result, name };
  }

  // Forward resolution
  Ens.validate(input);
  const normalized = Ens.normalize(input);
  const node = Ens.namehash(normalized);
  const nodeHex = Hex.fromBytes(node);

  const registry = Contract({
    address: ENS_REGISTRY,
    abi: ensRegistryAbi,
    provider
  });

  const resolverAddress = await registry.read.resolver(nodeHex);
  if (resolverAddress === '0x0000000000000000000000000000000000000000') {
    return { address: null, name: normalized, records: {} };
  }

  const resolver = Contract({
    address: resolverAddress,
    abi: resolverAbi,
    provider
  });

  const [address, avatar, url] = await Promise.all([
    resolver.read.addr(nodeHex).catch(() => null),
    resolver.read.text(nodeHex, 'avatar').catch(() => ''),
    resolver.read.text(nodeHex, 'url').catch(() => '')
  ]);

  return {
    address,
    name: normalized,
    records: { avatar, url }
  };
}

// Usage
const result = await resolveEns('vitalik.eth', provider);
console.log(result);
// {
//   address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
//   name: 'vitalik.eth',
//   records: { avatar: '...', url: 'https://vitalik.eth.limo' }
// }

L2 ENS Resolution

For L2 networks, use the same contract addresses but connect to an L1 provider for resolution, or use CCIP-read compatible resolvers.
// For L2s that support CCIP-read (EIP-3668)
// The resolver contract handles cross-chain reads automatically
const l2Result = await resolveEns('name.eth', l1Provider);

See Also