Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Interacting with a Resolver

Set addresses, text records, and more

Record updates go to the name's current resolver. Changing which resolver a name uses is a separate transaction on the registry that holds the name.

Use a Connected Wallet

These examples assume your web app has already connected to the user's browser wallet, such as MetaMask, and the wallet is on Ethereum Mainnet. window.ethereum is the connection provided by that wallet. ENSjs uses a standard viem Wallet Client to request transactions for the user to approve.

The shared client.ts setup below reads the already-connected account and attaches it to the wallet client, as ENSjs requires. Load it after your app's wallet connection has completed, and replace <MAINNET_RPC_URL> with your Ethereum Mainnet RPC endpoint. If your app already has viem clients and a connected account, reuse those in the examples below.

client.ts
import 'viem/window'
 
import { addEnsContracts } from '@ensdomains/ensjs'
import { createPublicClient, createWalletClient, custom, http } from 'viem'
import { mainnet } from 'viem/chains'
 
if (!window.ethereum) throw new Error('No browser wallet found')
 
export const [account] = await window.ethereum.request({ method: 'eth_accounts' })
if (!account) throw new Error('Connect your wallet before running this example')
 
const chain = addEnsContracts(mainnet)
export const client = createPublicClient({
  chain,
  transport: http('<MAINNET_RPC_URL>'),
})
export const wallet = createWalletClient({
  account,
  chain,
  transport: custom(window.ethereum),
})

Checking Interface Support

Call supportsInterface(bytes4) on the discovered resolver before selecting a setter. A positive result describes the API, not the caller's permission to use it. See Resolver Interface Standards for the ENSv2 interfaces.

The Permissioned Resolver exposes records through resolve(bytes,bytes). Its setters take a DNS-encoded name. In contrast, the v1 Public Resolver's setters take a namehash. For text writes, the v2 setter interface is 0xc7279f88, while the legacy text-reading interface is 0x59d1d43c.

Updating a User's Record

This example finds the writable resolver with ENSjs, checks its interface, and selects the matching setRecords action. It supports the v1 Public Resolver and the v2 Permissioned Resolver. Custom resolvers may require a different write API.

ENSjs
import { getResolver } from '@ensdomains/ensjs/public'
import { setRecords as setRecordsV1 } from '@ensdomains/ensjs/wallet/v1'
import { setRecords as setRecordsV2 } from '@ensdomains/ensjs/wallet/v2'
import { parseAbi } from 'viem'
import { normalize } from 'viem/ens'
 
import { account, client, wallet } from './client'
 
export async function setProfileText(
  input: string,
  key: string,
  value: string
) {
  const name = normalize(input)
  const resolverAddress = await getResolver(client, { name })
  if (!resolverAddress) throw new Error('No resolver set')
 
  const supports = (interfaceId: `0x${string}`) =>
    client.readContract({
      address: resolverAddress,
      abi: parseAbi(['function supportsInterface(bytes4) view returns (bool)']),
      functionName: 'supportsInterface',
      args: [interfaceId],
    })
 
  const isV2 = await supports('0xc7279f88')
  if (!isV2 && !(await supports('0x59d1d43c'))) {
    throw new Error('This resolver needs a custom record editor')
  }
  const setRecords = isV2 ? setRecordsV2 : setRecordsV1
  const hash = await setRecords(wallet, {
    account,
    name,
    resolverAddress,
    texts: [{ key, value }],
  })
  const receipt = await client.waitForTransactionReceipt({ hash })
  if (receipt.status !== 'success') throw new Error('Transaction reverted')
  return receipt
}

For ENSv1, the name's manager or an approved operator can update records on the Public Resolver. For a Permissioned Resolver, the caller needs the relevant resolver role, such as ROLE_SET_TEXT, for the key being changed or on the root resource. Owning the name's registry token does not automatically grant resolver roles.

Update a User's Resolver

Changing a resolver changes the source of a name's records. It does not copy records from the old resolver. Obtain the user's consent and prepare the replacement resolver before switching.

Check the Registry State First

For a second-level .eth name such as alice.eth, read getStatus(uint256(labelhash('alice'))) on the ENSv2 ETHRegistry. The status values are:

  • 2 (REGISTERED): use the ENSv2 path for the active name, whether newly registered or migrated.
  • 1 (RESERVED): a reservation is not an owned ENSv2 token. Confirm the name's active ENSv1 ownership before using the v1 path. An expired v1 name may still have a v2 reservation.
  • 0 (AVAILABLE): stop and check expiry or registration. Do not infer that the name is writable in ENSv1.

This check is specific to second-level .eth names. For subnames and other TLDs, inspect the registry hierarchy and its migration or reservation rules. A missing v2 owner alone does not prove that the v1 path is valid. See Migration.

ENSv2 Names

Call setResolver on the registry that holds the name. ENSjs's getNameRegistries discovers the hierarchy: entry zero is the name's own subregistry, and entry one is the registry holding its label. The wallet action takes that holding registry and the label.

Use this path for registered ENSv2 names. An unmigrated .eth name's v2 reservation does not make it an owned v2 name. Use the v1 path below until it migrates. Resolver-only subnames have no individual registry entry to update.

ENSjs v2
import { getNameRegistries, getOwner } from '@ensdomains/ensjs/public/v2'
import { setResolver } from '@ensdomains/ensjs/wallet/v2'
import { type Address, zeroAddress } from 'viem'
import { normalize } from 'viem/ens'
 
import { account, client, wallet } from './client'
 
export async function changeV2Resolver(
  input: string,
  resolverAddress: Address
) {
  const name = normalize(input)
  const owner = await getOwner(client, { name })
  if (owner === zeroAddress) {
    throw new Error('Name has no active ENSv2 owner. Check its registry state.')
  }
  const registries = await getNameRegistries(client, { name })
  const registryAddress = registries[1]
  if (!registryAddress || registryAddress === zeroAddress) {
    throw new Error('No registry holds this name')
  }
  const hash = await setResolver(wallet, {
    account,
    label: name.split('.')[0],
    registryAddress,
    resolverAddress,
  })
  const receipt = await client.waitForTransactionReceipt({ hash })
  if (receipt.status !== 'success') throw new Error('Transaction reverted')
  return receipt
}

The owner check rejects reserved, expired, and absent entries before sending a transaction. It does not establish the connected account’s permissions. The caller needs ROLE_SET_RESOLVER on that name or an applicable root role in its registry.

Unmigrated ENSv1 Names

For ENSv1 names, ENSjs discovers whether ownership is held through the Name Wrapper and selects the corresponding v1 action target. Use this path only after confirming the name has not migrated.

ENSjs v1
import { getOwner } from '@ensdomains/ensjs/public/v1'
import { setResolver } from '@ensdomains/ensjs/wallet/v1'
import { type Address } from 'viem'
import { normalize } from 'viem/ens'
 
import { account, client, wallet } from './client'
 
export async function changeV1Resolver(
  input: string,
  resolverAddress: Address
) {
  const name = normalize(input)
  const ownership = await getOwner(client, { name })
  if (!ownership) throw new Error('Name has no owner')
 
  const hash = await setResolver(wallet, {
    account,
    name,
    contract:
      ownership.ownershipLevel === 'nameWrapper' ? 'nameWrapper' : 'registry',
    resolverAddress,
  })
  const receipt = await client.waitForTransactionReceipt({ hash })
  if (receipt.status !== 'success') throw new Error('Transaction reverted')
  return receipt
}

The connected account must be authorized by the ENSv1 Registry or Name Wrapper. Wrapper fuses can also prevent changing a resolver.

Layer 2 and Offchain Resolvers

A resolver that reads records through CCIP Read may have a separate write API on another chain or service. Follow that resolver's integration instructions. A successful read through the Universal Resolver does not establish how to write its records.