Skip to main content
Resolving ENS names is the process of looking up the address or other records associated with a human-readable name like vitalik.eth. This guide covers all aspects of ENS resolution.

Overview

ENS resolution is a two-step process:
  1. Query the ENS Registry to find the resolver for a name
  2. Query the Resolver to get the address or other records
This separation allows name owners to use different resolver implementations while maintaining ownership in the registry.

Basic Resolution

1

Import Required Libraries

import { createPublicClient, http, namehash } from 'viem'
import { mainnet } from 'viem/chains'

const ENS_REGISTRY_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
2

Convert Name to Node

Use namehash to convert the ENS name to a node (bytes32).
const node = namehash('vitalik.eth')
// Result: 0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835
Namehash is a recursive algorithm that converts a name into a unique 32-byte identifier. It ensures consistent hashing across the ENS system.
3

Get Resolver from Registry

Query the ENS registry to find which resolver contract handles this name.
const ensRegistry = await viem.getContractAt(
  'ENSRegistry',
  ENS_REGISTRY_ADDRESS
)

const resolverAddress = await ensRegistry.read.resolver([node])

if (resolverAddress === '0x0000000000000000000000000000000000000000') {
  throw new Error('No resolver set for this name')
}
4

Query Resolver for Address

Get the Ethereum address from the resolver.
const resolver = await viem.getContractAt(
  'PublicResolver',
  resolverAddress
)

const address = await resolver.read.addr([node])
console.log('Resolved address:', address)
Source: ~/workspace/source/contracts/resolvers/profiles/AddrResolver.sol:36-40

Resolving Multi-Chain Addresses

ENS supports addresses for multiple blockchains using coin type identifiers (SLIP-44).

Common Coin Types

BlockchainCoin Type
Bitcoin0
Ethereum60
Binance Smart Chain714
Polygon966
Arbitrum9001

Resolution Example

import { namehash } from 'viem'

const node = namehash('example.eth')
const resolver = await viem.getContractAt('PublicResolver', resolverAddress)

// Get Ethereum address (coin type 60)
const ethAddr = await resolver.read.addr([node, 60])
console.log('Ethereum:', ethAddr)

// Get Bitcoin address (coin type 0)  
const btcAddr = await resolver.read.addr([node, 0])
console.log('Bitcoin:', btcAddr)

// Get Polygon address (coin type 966)
const maticAddr = await resolver.read.addr([node, 966])
console.log('Polygon:', maticAddr)
Source: ~/workspace/source/contracts/resolvers/profiles/AddrResolver.sol:73-86
If a specific coin type address is not set but the chain is EVM-compatible, the resolver may fall back to the default EVM address (coin type 2147492101).

Resolving Text Records

Text records store arbitrary key-value data associated with ENS names.

Common Text Record Keys

  • email - Email address
  • url - Website URL
  • avatar - Avatar image URL or NFT identifier
  • description - Profile description
  • notice - Notice to users
  • keywords - Comma-separated keywords
  • com.github - GitHub username
  • com.twitter - Twitter handle
  • com.discord - Discord username
  • org.telegram - Telegram username

Reading Text Records

const node = namehash('vitalik.eth')
const resolver = await viem.getContractAt('PublicResolver', resolverAddress)

// Get individual text records
const email = await resolver.read.text([node, 'email'])
const url = await resolver.read.text([node, 'url'])
const avatar = await resolver.read.text([node, 'avatar'])
const twitter = await resolver.read.text([node, 'com.twitter'])
const github = await resolver.read.text([node, 'com.github'])

console.log('Email:', email)
console.log('Website:', url)
console.log('Avatar:', avatar)
console.log('Twitter:', twitter)
console.log('GitHub:', github)
Source: ~/workspace/source/contracts/resolvers/profiles/TextResolver.sol:28-33

Content Hash Resolution

Content hashes link ENS names to decentralized content (IPFS, Swarm, etc.).
import { namehash } from 'viem'

const node = namehash('example.eth')
const resolver = await viem.getContractAt('PublicResolver', resolverAddress)

const contentHash = await resolver.read.contenthash([node])

// Content hash is encoded according to EIP-1577
// Example: 0xe301... for IPFS
// Example: 0xe401... for Swarm

if (contentHash && contentHash !== '0x') {
  console.log('Content hash:', contentHash)
  // Decode based on the codec (first bytes)
}
Use libraries like @ensdomains/content-hash to decode content hashes into IPFS CIDs or Swarm hashes.

Complete Resolver Example

Here’s a complete function that resolves all common records:
import { namehash, zeroAddress } from 'viem'

async function resolveENSName(name) {
  const node = namehash(name)
  
  // Get resolver
  const ensRegistry = await viem.getContractAt(
    'ENSRegistry',
    ENS_REGISTRY_ADDRESS
  )
  const resolverAddress = await ensRegistry.read.resolver([node])
  
  if (resolverAddress === zeroAddress) {
    return { error: 'No resolver set' }
  }
  
  const resolver = await viem.getContractAt('PublicResolver', resolverAddress)
  
  // Resolve all records
  const [address, ethAddress, btcAddress, email, url, avatar, contentHash] = 
    await Promise.all([
      resolver.read.addr([node]),
      resolver.read.addr([node, 60]),
      resolver.read.addr([node, 0]),
      resolver.read.text([node, 'email']),
      resolver.read.text([node, 'url']),
      resolver.read.text([node, 'avatar']),
      resolver.read.contenthash([node])
    ])
  
  return {
    name,
    node,
    resolver: resolverAddress,
    addresses: {
      eth: address,
      ethereum: ethAddress,
      bitcoin: btcAddress
    },
    records: {
      email,
      url,
      avatar,
      contentHash
    }
  }
}

// Usage
const result = await resolveENSName('vitalik.eth')
console.log(result)

Checking if Records Exist

Before querying records, you can check if they’re set:
const node = namehash('example.eth')
const resolver = await viem.getContractAt('PublicResolver', resolverAddress)

// Check if Ethereum address exists
const hasEthAddr = await resolver.read.hasAddr([node, 60])

if (hasEthAddr) {
  const address = await resolver.read.addr([node, 60])
  console.log('Address:', address)
}
Source: ~/workspace/source/contracts/resolvers/profiles/AddrResolver.sol:89-96

Subdomain Resolution

Subdomains work exactly the same way as second-level domains:
// Resolve a subdomain
const subdomainNode = namehash('app.example.eth')
const resolverAddress = await ensRegistry.read.resolver([subdomainNode])

if (resolverAddress !== zeroAddress) {
  const resolver = await viem.getContractAt('PublicResolver', resolverAddress)
  const address = await resolver.read.addr([subdomainNode])
  console.log('Subdomain address:', address)
}
Each subdomain can have its own resolver, independent of the parent domain.

Error Handling

async function safeResolveENS(name) {
  try {
    const node = namehash(name)
    const resolverAddress = await ensRegistry.read.resolver([node])
    
    if (resolverAddress === zeroAddress) {
      return { error: 'NO_RESOLVER', message: 'No resolver set for this name' }
    }
    
    const resolver = await viem.getContractAt('PublicResolver', resolverAddress)
    const address = await resolver.read.addr([node])
    
    if (address === zeroAddress) {
      return { error: 'NO_ADDRESS', message: 'No address set for this name' }
    }
    
    return { success: true, address }
    
  } catch (error) {
    console.error('Resolution failed:', error)
    return { 
      error: 'RESOLUTION_FAILED',
      message: error.message 
    }
  }
}

Resolver Interface Support

Check if a resolver supports specific interfaces:
const resolver = await viem.getContractAt('PublicResolver', resolverAddress)

// Check interface support
const supportsAddr = await resolver.read.supportsInterface(['0x3b3b57de']) // IAddrResolver
const supportsText = await resolver.read.supportsInterface(['0x59d1d43c']) // ITextResolver
const supportsContentHash = await resolver.read.supportsInterface(['0xbc1c58d1']) // IContentHashResolver

console.log('Supports addr():', supportsAddr)
console.log('Supports text():', supportsText)
console.log('Supports contenthash():', supportsContentHash)
Source: ~/workspace/source/contracts/resolvers/PublicResolver.sol:129-148

PublicResolver Supported Interfaces

The PublicResolver implements multiple resolver profiles:
  • IAddrResolver - Basic address resolution (EIP-137)
  • IAddressResolver - Multi-chain addresses (EIP-2304)
  • ITextResolver - Text records (EIP-634)
  • IContentHashResolver - Content hashes (EIP-1577)
  • INameResolver - Reverse resolution (EIP-181)
  • IABIResolver - Contract ABI (EIP-205)
  • IPubkeyResolver - Public keys (EIP-619)
  • IInterfaceResolver - Interface detection (EIP-165)
  • IDNSRecordResolver - DNS records
  • IDNSZoneResolver - DNS zones
Source: ~/workspace/source/contracts/resolvers/PublicResolver.sol:19-29

Best Practices

Never hardcode resolver addresses. Always look them up from the ENS registry, as name owners can change resolvers at any time.
Implement caching for resolver lookups, but with appropriate TTL values. ENS registry records include a TTL field you can use.
When resolving names in production, consider using a service like the ENS Universal Resolver for better reliability and wildcard support.

Performance Optimization

// Batch multiple resolutions
async function resolveBatch(names) {
  const resolverLookups = names.map(async name => {
    const node = namehash(name)
    return {
      name,
      node,
      resolver: await ensRegistry.read.resolver([node])
    }
  })
  
  const results = await Promise.all(resolverLookups)
  
  // Group by resolver to batch calls
  const byResolver = {}
  for (const { name, node, resolver } of results) {
    if (!byResolver[resolver]) byResolver[resolver] = []
    byResolver[resolver].push({ name, node })
  }
  
  // Resolve addresses in batches per resolver
  const addresses = {}
  for (const [resolverAddr, items] of Object.entries(byResolver)) {
    const resolver = await viem.getContractAt('PublicResolver', resolverAddr)
    const addrs = await Promise.all(
      items.map(({ node }) => resolver.read.addr([node]))
    )
    items.forEach(({ name }, i) => {
      addresses[name] = addrs[i]
    })
  }
  
  return addresses
}

Network Deployments

Mainnet

  • ENS Registry: 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
  • Public Resolver: Check latest deployment

Sepolia Testnet

  • ENS Registry: 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
  • Public Resolver: Check latest deployment

Next Steps

Build docs developers (and LLMs) love