Skip to content

Commit 390fff8

Browse files
TateBjxom
andauthored
feat(ens): UniversalResolver v3 (#3848)
* feat: universalresolver v3 support * lint * update with latest UR deployment + remove unused UR compatibility * fix error test + remove unused abis * unofficial UR deployments * evmChainIdToCoinType fallback * evmChainIdToCoinType 1>60 * chore: tweaks * chore: format --------- Co-authored-by: jxom <7336481+jxom@users.noreply.github.com>
1 parent 3182189 commit 390fff8

26 files changed

Lines changed: 511 additions & 452 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"viem": minor
3+
---
4+
5+
Added support for chain-specific ENS resolution and ENS UniversalResolver v3.

pnpm-lock.yaml

Lines changed: 14 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/actions/ens/getEnsAddress.test.ts

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeAll, describe, expect, test } from 'vitest'
1+
import { beforeAll, expect, test } from 'vitest'
22

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

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

5353
test('gets address for name with coinType', async () => {
5454
await expect(
55-
getEnsAddress(client, { name: 'awkweb.eth', coinType: 60 }),
55+
getEnsAddress(client, { name: 'awkweb.eth', coinType: 60n }),
56+
).resolves.toMatchInlineSnapshot(
57+
'"0xa0cf798816d4b9b9866b5330eea46a18382f251e"',
58+
)
59+
})
60+
61+
test('gets address for name with chainId 1', async () => {
62+
await expect(
63+
getEnsAddress(client, { name: 'awkweb.eth', chainId: 1 }),
5664
).resolves.toMatchInlineSnapshot(
5765
'"0xa0cf798816d4b9b9866b5330eea46a18382f251e"',
5866
)
5967
})
6068

6169
test('name without address with coinType', async () => {
6270
await expect(
63-
getEnsAddress(client, { name: 'awkweb.eth', coinType: 61 }),
71+
getEnsAddress(client, { name: 'awkweb.eth', coinType: 61n }),
72+
).resolves.toBeNull()
73+
})
74+
75+
test('name with address with chainId', async () => {
76+
await expect(
77+
getEnsAddress(client, { name: 'taytems.eth', chainId: 10 }),
78+
).resolves.toMatchInlineSnapshot(
79+
'"0x8e8db5ccef88cca9d624701db544989c996e3216"',
80+
)
81+
})
82+
83+
test('name without address with chainId', async () => {
84+
await expect(
85+
getEnsAddress(client, { name: 'awkweb.eth', chainId: 10 }),
6486
).resolves.toBeNull()
6587
})
6688

@@ -80,15 +102,15 @@ test('name with resolver that does not support addr - strict', async () => {
80102
await expect(
81103
getEnsAddress(client, { name: 'vitalik.eth', strict: true }),
82104
).rejects.toMatchInlineSnapshot(`
83-
[ContractFunctionExecutionError: The contract function "resolve" reverted.
105+
[ContractFunctionExecutionError: The contract function "resolveWithGateways" reverted.
84106
85-
Error: ResolverError(bytes returnData)
107+
Error: ResolverError(bytes errorData)
86108
(0x)
87109
88110
Contract Call:
89111
address: 0x0000000000000000000000000000000000000000
90-
function: resolve(bytes name, bytes data, string[] gateways)
91-
args: (0x07766974616c696b0365746800, 0x3b3b57deee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835, ["x-batch-gateway:true"])
112+
function: resolveWithGateways(bytes name, bytes data, string[] gateways)
113+
args: (0x07766974616c696b0365746800, 0x3b3b57deee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835, ["x-batch-gateway:true"])
92114
93115
Docs: https://viem.sh/docs/contract/readContract
94116
Version: viem@x.y.z]
@@ -112,7 +134,6 @@ test('name with a label larger than 255 bytes', async () => {
112134
await expect(
113135
getEnsAddress(client, {
114136
name: `${'9'.repeat(291)}.eth`,
115-
universalResolverAddress: '0xc0497e381f536be9ce14b0dd3817cbcae57d2f62',
116137
}),
117138
).resolves.toMatchInlineSnapshot(
118139
`"0xcdf14B42e1D3c264F6955521944a50d9A4d5CF3a"`,
@@ -132,7 +153,7 @@ test('offchain: name without address', async () => {
132153
getEnsAddress(client, {
133154
name: 'loalsdsladasdhjasgdhasjdghasgdjgasjdasd.cb.id',
134155
}),
135-
).resolves.toMatchInlineSnapshot('null')
156+
).resolves.toBeNull()
136157
})
137158

138159
test('offchain: aggregated', async () => {
@@ -165,32 +186,13 @@ test('custom universal resolver address', async () => {
165186
await expect(
166187
getEnsAddress(client, {
167188
name: 'awkweb.eth',
168-
universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376',
189+
universalResolverAddress: '0xED73a03F19e8D849E44a39252d222c6ad5217E1e',
169190
}),
170191
).resolves.toMatchInlineSnapshot(
171192
'"0xA0Cf798816D4b9b9866b5330EEa46a18382f251e"',
172193
)
173194
})
174195

175-
describe('universal resolver with custom errors', () => {
176-
test('name without resolver', async () => {
177-
await expect(
178-
getEnsAddress(client, {
179-
name: 'random123.zzz',
180-
universalResolverAddress: '0x9380F1974D2B7064eA0c0EC251968D8c69f0Ae31',
181-
}),
182-
).resolves.toBeNull()
183-
})
184-
test('name with invalid wildcard resolver', async () => {
185-
await expect(
186-
getEnsAddress(client, {
187-
name: 'another-unregistered-name.eth',
188-
universalResolverAddress: '0x9380F1974D2B7064eA0c0EC251968D8c69f0Ae31',
189-
}),
190-
).resolves.toBeNull()
191-
})
192-
})
193-
194196
test('chain not provided', async () => {
195197
await expect(
196198
getEnsAddress(
@@ -230,7 +232,7 @@ test('universal resolver contract deployed on later block', async () => {
230232
[ChainDoesNotSupportContract: Chain "Ethereum (Local)" does not support contract "ensUniversalResolver".
231233
232234
This could be due to any of the following:
233-
- The contract "ensUniversalResolver" was not deployed until block 19258213 (current block 14353601).
235+
- The contract "ensUniversalResolver" was not deployed until block 23085558 (current block 14353601).
234236
235237
Version: viem@x.y.z]
236238
`)
@@ -243,12 +245,12 @@ test('invalid universal resolver address', async () => {
243245
universalResolverAddress: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
244246
}),
245247
).rejects.toThrowErrorMatchingInlineSnapshot(`
246-
[ContractFunctionExecutionError: The contract function "resolve" reverted.
248+
[ContractFunctionExecutionError: The contract function "resolveWithGateways" reverted.
247249
248250
Contract Call:
249251
address: 0x0000000000000000000000000000000000000000
250-
function: resolve(bytes name, bytes data, string[] gateways)
251-
args: (0x0661776b7765620365746800, 0x3b3b57de52d0f5fbf348925621be297a61b88ec492ebbbdfa9477d82892e2786020ad61c, ["x-batch-gateway:true"])
252+
function: resolveWithGateways(bytes name, bytes data, string[] gateways)
253+
args: (0x0661776b7765620365746800, 0x3b3b57de52d0f5fbf348925621be297a61b88ec492ebbbdfa9477d82892e2786020ad61c, ["x-batch-gateway:true"])
252254
253255
Docs: https://viem.sh/docs/contract/readContract
254256
Version: viem@x.y.z]

src/actions/ens/getEnsAddress.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import {
2323
} from '../../utils/chain/getChainContractAddress.js'
2424
import { type TrimErrorType, trim } from '../../utils/data/trim.js'
2525
import { type ToHexErrorType, toHex } from '../../utils/encoding/toHex.js'
26+
import {
27+
type ChainIdToCoinTypeError,
28+
chainIdToCoinType,
29+
} from '../../utils/ens/chainIdToCoinType.js'
2630
import { isNullUniversalResolverError } from '../../utils/ens/errors.js'
2731
import { localBatchGatewayUrl } from '../../utils/ens/localBatchGatewayRequest.js'
2832
import { type NamehashErrorType, namehash } from '../../utils/ens/namehash.js'
@@ -38,8 +42,6 @@ import {
3842

3943
export type GetEnsAddressParameters = Prettify<
4044
Pick<ReadContractParameters, 'blockNumber' | 'blockTag'> & {
41-
/** ENSIP-9 compliant coinType used to resolve addresses for other chains */
42-
coinType?: number | undefined
4345
/** Universal Resolver gateway URLs to use for resolving CCIP-read requests. */
4446
gatewayUrls?: string[] | undefined
4547
/** Name to get the address for. */
@@ -48,12 +50,24 @@ export type GetEnsAddressParameters = Prettify<
4850
strict?: boolean | undefined
4951
/** Address of ENS Universal Resolver Contract. */
5052
universalResolverAddress?: Address | undefined
51-
}
53+
} & (
54+
| {
55+
/** ENSIP-11 chainId used to resolve addresses for other chains */
56+
chainId?: number | undefined
57+
coinType?: undefined
58+
}
59+
| {
60+
chainId?: undefined
61+
/** ENSIP-9 compliant coinType used to resolve addresses for other chains */
62+
coinType?: bigint | undefined
63+
}
64+
)
5265
>
5366

5467
export type GetEnsAddressReturnType = Address | null
5568

5669
export type GetEnsAddressErrorType =
70+
| ChainIdToCoinTypeError
5771
| GetChainContractAddressErrorType
5872
| EncodeFunctionDataErrorType
5973
| NamehashErrorType
@@ -95,8 +109,15 @@ export async function getEnsAddress<chain extends Chain | undefined>(
95109
client: Client<Transport, chain>,
96110
parameters: GetEnsAddressParameters,
97111
): Promise<GetEnsAddressReturnType> {
98-
const { blockNumber, blockTag, coinType, name, gatewayUrls, strict } =
99-
parameters
112+
const {
113+
blockNumber,
114+
blockTag,
115+
chainId,
116+
coinType,
117+
name,
118+
gatewayUrls,
119+
strict,
120+
} = parameters
100121
const { chain } = client
101122

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

140+
const args = (() => {
141+
if (coinType != null) return [namehash(name), BigInt(coinType)] as const
142+
if (chainId != null)
143+
return [namehash(name), chainIdToCoinType(chainId)] as const
144+
return [namehash(name)] as const
145+
})()
146+
119147
try {
120148
const functionData = encodeFunctionData({
121149
abi: addressResolverAbi,
122150
functionName: 'addr',
123-
...(coinType != null
124-
? { args: [namehash(name), BigInt(coinType)] }
125-
: { args: [namehash(name)] }),
151+
args,
126152
})
127153

128154
const readContractParameters = {
129155
address: universalResolverAddress,
130156
abi: universalResolverResolveAbi,
131-
functionName: 'resolve',
157+
functionName: 'resolveWithGateways',
132158
args: [
133159
toHex(packetToBytes(name)),
134160
functionData,
@@ -146,7 +172,7 @@ export async function getEnsAddress<chain extends Chain | undefined>(
146172

147173
const address = decodeFunctionResult({
148174
abi: addressResolverAbi,
149-
args: coinType != null ? [namehash(name), BigInt(coinType)] : undefined,
175+
args,
150176
functionName: 'addr',
151177
data: res[0],
152178
})
@@ -156,7 +182,7 @@ export async function getEnsAddress<chain extends Chain | undefined>(
156182
return address
157183
} catch (err) {
158184
if (strict) throw err
159-
if (isNullUniversalResolverError(err, 'resolve')) return null
185+
if (isNullUniversalResolverError(err)) return null
160186
throw err
161187
}
162188
}

src/actions/ens/getEnsAvatar.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ beforeAll(async () => {
2020
address: address.vitalik,
2121
})
2222
await reset(client, {
23-
blockNumber: 19_258_213n,
23+
blockNumber: 23_085_558n,
2424
jsonRpcUrl: anvilMainnet.forkUrl,
2525
})
2626
})

0 commit comments

Comments
 (0)