Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nervous-kangaroos-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": minor
---

Added support for chain-specific ENS resolution and ENS UniversalResolver v3.
35 changes: 14 additions & 21 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 36 additions & 34 deletions src/actions/ens/getEnsAddress.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeAll, describe, expect, test } from 'vitest'
import { beforeAll, expect, test } from 'vitest'

import { createHttpServer, setVitalikResolver } from '~test/src/utils.js'
import { anvilMainnet } from '../../../test/src/anvil.js'
Expand All @@ -12,7 +12,7 @@ const client = anvilMainnet.getClient()

beforeAll(async () => {
await reset(client, {
blockNumber: 19_258_213n,
blockNumber: 23_085_558n,
jsonRpcUrl: anvilMainnet.forkUrl,
})
await setVitalikResolver()
Expand Down Expand Up @@ -52,15 +52,37 @@ test('gets address that starts with 0s for name', async () => {

test('gets address for name with coinType', async () => {
await expect(
getEnsAddress(client, { name: 'awkweb.eth', coinType: 60 }),
getEnsAddress(client, { name: 'awkweb.eth', coinType: 60n }),
).resolves.toMatchInlineSnapshot(
'"0xa0cf798816d4b9b9866b5330eea46a18382f251e"',
)
})

test('gets address for name with chainId 1', async () => {
await expect(
getEnsAddress(client, { name: 'awkweb.eth', chainId: 1 }),
).resolves.toMatchInlineSnapshot(
'"0xa0cf798816d4b9b9866b5330eea46a18382f251e"',
)
})

test('name without address with coinType', async () => {
await expect(
getEnsAddress(client, { name: 'awkweb.eth', coinType: 61 }),
getEnsAddress(client, { name: 'awkweb.eth', coinType: 61n }),
).resolves.toBeNull()
})

test('name with address with chainId', async () => {
await expect(
getEnsAddress(client, { name: 'taytems.eth', chainId: 10 }),
).resolves.toMatchInlineSnapshot(
'"0x8e8db5ccef88cca9d624701db544989c996e3216"',
)
})

test('name without address with chainId', async () => {
await expect(
getEnsAddress(client, { name: 'awkweb.eth', chainId: 10 }),
).resolves.toBeNull()
})

Expand All @@ -80,15 +102,15 @@ test('name with resolver that does not support addr - strict', async () => {
await expect(
getEnsAddress(client, { name: 'vitalik.eth', strict: true }),
).rejects.toMatchInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "resolve" reverted.
[ContractFunctionExecutionError: The contract function "resolveWithGateways" reverted.

Error: ResolverError(bytes returnData)
Error: ResolverError(bytes errorData)
(0x)

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: resolve(bytes name, bytes data, string[] gateways)
args: (0x07766974616c696b0365746800, 0x3b3b57deee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835, ["x-batch-gateway:true"])
function: resolveWithGateways(bytes name, bytes data, string[] gateways)
args: (0x07766974616c696b0365746800, 0x3b3b57deee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835, ["x-batch-gateway:true"])

Docs: https://viem.sh/docs/contract/readContract
Version: viem@x.y.z]
Expand All @@ -112,7 +134,6 @@ test('name with a label larger than 255 bytes', async () => {
await expect(
getEnsAddress(client, {
name: `${'9'.repeat(291)}.eth`,
universalResolverAddress: '0xc0497e381f536be9ce14b0dd3817cbcae57d2f62',
}),
).resolves.toMatchInlineSnapshot(
`"0xcdf14B42e1D3c264F6955521944a50d9A4d5CF3a"`,
Expand All @@ -132,7 +153,7 @@ test('offchain: name without address', async () => {
getEnsAddress(client, {
name: 'loalsdsladasdhjasgdhasjdghasgdjgasjdasd.cb.id',
}),
).resolves.toMatchInlineSnapshot('null')
).resolves.toBeNull()
})

test('offchain: aggregated', async () => {
Expand Down Expand Up @@ -165,32 +186,13 @@ test('custom universal resolver address', async () => {
await expect(
getEnsAddress(client, {
name: 'awkweb.eth',
universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376',
universalResolverAddress: '0xED73a03F19e8D849E44a39252d222c6ad5217E1e',
}),
).resolves.toMatchInlineSnapshot(
'"0xA0Cf798816D4b9b9866b5330EEa46a18382f251e"',
)
})

describe('universal resolver with custom errors', () => {
test('name without resolver', async () => {
await expect(
getEnsAddress(client, {
name: 'random123.zzz',
universalResolverAddress: '0x9380F1974D2B7064eA0c0EC251968D8c69f0Ae31',
}),
).resolves.toBeNull()
})
test('name with invalid wildcard resolver', async () => {
await expect(
getEnsAddress(client, {
name: 'another-unregistered-name.eth',
universalResolverAddress: '0x9380F1974D2B7064eA0c0EC251968D8c69f0Ae31',
}),
).resolves.toBeNull()
})
})

test('chain not provided', async () => {
await expect(
getEnsAddress(
Expand Down Expand Up @@ -230,7 +232,7 @@ test('universal resolver contract deployed on later block', async () => {
[ChainDoesNotSupportContract: Chain "Ethereum (Local)" does not support contract "ensUniversalResolver".

This could be due to any of the following:
- The contract "ensUniversalResolver" was not deployed until block 19258213 (current block 14353601).
- The contract "ensUniversalResolver" was not deployed until block 23085558 (current block 14353601).

Version: viem@x.y.z]
`)
Expand All @@ -243,12 +245,12 @@ test('invalid universal resolver address', async () => {
universalResolverAddress: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "resolve" reverted.
[ContractFunctionExecutionError: The contract function "resolveWithGateways" reverted.

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: resolve(bytes name, bytes data, string[] gateways)
args: (0x0661776b7765620365746800, 0x3b3b57de52d0f5fbf348925621be297a61b88ec492ebbbdfa9477d82892e2786020ad61c, ["x-batch-gateway:true"])
function: resolveWithGateways(bytes name, bytes data, string[] gateways)
args: (0x0661776b7765620365746800, 0x3b3b57de52d0f5fbf348925621be297a61b88ec492ebbbdfa9477d82892e2786020ad61c, ["x-batch-gateway:true"])

Docs: https://viem.sh/docs/contract/readContract
Version: viem@x.y.z]
Expand Down
48 changes: 37 additions & 11 deletions src/actions/ens/getEnsAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
} from '../../utils/chain/getChainContractAddress.js'
import { type TrimErrorType, trim } from '../../utils/data/trim.js'
import { type ToHexErrorType, toHex } from '../../utils/encoding/toHex.js'
import {
type ChainIdToCoinTypeError,
chainIdToCoinType,
} from '../../utils/ens/chainIdToCoinType.js'
import { isNullUniversalResolverError } from '../../utils/ens/errors.js'
import { localBatchGatewayUrl } from '../../utils/ens/localBatchGatewayRequest.js'
import { type NamehashErrorType, namehash } from '../../utils/ens/namehash.js'
Expand All @@ -38,8 +42,6 @@ import {

export type GetEnsAddressParameters = Prettify<
Pick<ReadContractParameters, 'blockNumber' | 'blockTag'> & {
/** ENSIP-9 compliant coinType used to resolve addresses for other chains */
coinType?: number | undefined
/** Universal Resolver gateway URLs to use for resolving CCIP-read requests. */
gatewayUrls?: string[] | undefined
/** Name to get the address for. */
Expand All @@ -48,12 +50,24 @@ export type GetEnsAddressParameters = Prettify<
strict?: boolean | undefined
/** Address of ENS Universal Resolver Contract. */
universalResolverAddress?: Address | undefined
}
} & (
| {
/** ENSIP-11 chainId used to resolve addresses for other chains */
chainId?: number | undefined
coinType?: undefined
}
| {
chainId?: undefined
/** ENSIP-9 compliant coinType used to resolve addresses for other chains */
coinType?: bigint | undefined
}
)
>

export type GetEnsAddressReturnType = Address | null

export type GetEnsAddressErrorType =
| ChainIdToCoinTypeError
| GetChainContractAddressErrorType
| EncodeFunctionDataErrorType
| NamehashErrorType
Expand Down Expand Up @@ -95,8 +109,15 @@ export async function getEnsAddress<chain extends Chain | undefined>(
client: Client<Transport, chain>,
parameters: GetEnsAddressParameters,
): Promise<GetEnsAddressReturnType> {
const { blockNumber, blockTag, coinType, name, gatewayUrls, strict } =
parameters
const {
blockNumber,
blockTag,
chainId,
coinType,
name,
gatewayUrls,
strict,
} = parameters
const { chain } = client

const universalResolverAddress = (() => {
Expand All @@ -116,19 +137,24 @@ export async function getEnsAddress<chain extends Chain | undefined>(
const tlds = chain?.ensTlds
if (tlds && !tlds.some((tld) => name.endsWith(tld))) return null

const args = (() => {
if (coinType != null) return [namehash(name), BigInt(coinType)] as const
if (chainId != null)
return [namehash(name), chainIdToCoinType(chainId)] as const
return [namehash(name)] as const
})()

try {
const functionData = encodeFunctionData({
abi: addressResolverAbi,
functionName: 'addr',
...(coinType != null
? { args: [namehash(name), BigInt(coinType)] }
: { args: [namehash(name)] }),
args,
})

const readContractParameters = {
address: universalResolverAddress,
abi: universalResolverResolveAbi,
functionName: 'resolve',
functionName: 'resolveWithGateways',
args: [
toHex(packetToBytes(name)),
functionData,
Expand All @@ -146,7 +172,7 @@ export async function getEnsAddress<chain extends Chain | undefined>(

const address = decodeFunctionResult({
abi: addressResolverAbi,
args: coinType != null ? [namehash(name), BigInt(coinType)] : undefined,
args,
functionName: 'addr',
data: res[0],
})
Expand All @@ -156,7 +182,7 @@ export async function getEnsAddress<chain extends Chain | undefined>(
return address
} catch (err) {
if (strict) throw err
if (isNullUniversalResolverError(err, 'resolve')) return null
if (isNullUniversalResolverError(err)) return null
throw err
}
}
2 changes: 1 addition & 1 deletion src/actions/ens/getEnsAvatar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ beforeAll(async () => {
address: address.vitalik,
})
await reset(client, {
blockNumber: 19_258_213n,
blockNumber: 23_085_558n,
jsonRpcUrl: anvilMainnet.forkUrl,
})
})
Expand Down
Loading
Loading