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:
- Query the ENS Registry to find the resolver for a name
- 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
Import Required Libraries
import { createPublicClient, http, namehash } from 'viem'
import { mainnet } from 'viem/chains'
const ENS_REGISTRY_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
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.
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')
}
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
| Blockchain | Coin Type |
|---|
| Bitcoin | 0 |
| Ethereum | 60 |
| Binance Smart Chain | 714 |
| Polygon | 966 |
| Arbitrum | 9001 |
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.
// 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