Skip to content

feat: universalresolver v3 support #3512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
47 changes: 21 additions & 26 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: 22_138_945n,
jsonRpcUrl: anvilMainnet.forkUrl,
})
await setVitalikResolver()
Expand Down Expand Up @@ -52,15 +52,29 @@ 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('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 @@ -82,7 +96,7 @@ test('name with resolver that does not support addr - strict', async () => {
).rejects.toMatchInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "resolve" reverted.

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

Contract Call:
Expand Down Expand Up @@ -132,7 +146,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 @@ -172,25 +186,6 @@ test('custom universal resolver address', async () => {
)
})

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 +225,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 22138945 (current block 14353601).

Version: [email protected]]
`)
Expand Down
41 changes: 32 additions & 9 deletions src/actions/ens/getEnsAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import {
import { type TrimErrorType, trim } from '../../utils/data/trim.js'
import { type ToHexErrorType, toHex } from '../../utils/encoding/toHex.js'
import { isNullUniversalResolverError } from '../../utils/ens/errors.js'
import {
type EvmChainIdToCoinTypeError,
evmChainIdToCoinType,
} from '../../utils/ens/evmChainIdToCoinType.js'
import { type NamehashErrorType, namehash } from '../../utils/ens/namehash.js'
import {
type PacketToBytesErrorType,
Expand All @@ -37,8 +41,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 @@ -47,12 +49,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 =
| EvmChainIdToCoinTypeError
| GetChainContractAddressErrorType
| EncodeFunctionDataErrorType
| NamehashErrorType
Expand Down Expand Up @@ -96,6 +110,7 @@ export async function getEnsAddress<chain extends Chain | undefined>(
blockNumber,
blockTag,
coinType,
chainId,
name,
gatewayUrls,
strict,
Expand All @@ -116,19 +131,23 @@ export async function getEnsAddress<chain extends Chain | undefined>(
})
}

const args = (() => {
if (coinType != null) return [namehash(name), BigInt(coinType)] as const
if (chainId != null)
return [namehash(name), evmChainIdToCoinType(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',
args: [toHex(packetToBytes(name)), functionData],
blockNumber,
blockTag,
Expand All @@ -139,15 +158,19 @@ export async function getEnsAddress<chain extends Chain | undefined>(
const res = gatewayUrls
? await readContractAction({
...readContractParameters,
functionName: 'resolveWithGateways',
args: [...readContractParameters.args, gatewayUrls],
})
: await readContractAction(readContractParameters)
: await readContractAction({
...readContractParameters,
functionName: 'resolve',
})

if (res[0] === '0x') return null

const address = decodeFunctionResult({
abi: addressResolverAbi,
args: coinType != null ? [namehash(name), BigInt(coinType)] : undefined,
args,
functionName: 'addr',
data: res[0],
})
Expand Down
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: 22_138_945n,
jsonRpcUrl: anvilMainnet.forkUrl,
})
})
Expand Down
69 changes: 20 additions & 49 deletions src/actions/ens/getEnsName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const client = anvilMainnet.getClient()

beforeAll(async () => {
await reset(client, {
blockNumber: 19_258_213n,
blockNumber: 22_138_945n,
jsonRpcUrl: anvilMainnet.forkUrl,
})
await setVitalikResolver()
Expand Down Expand Up @@ -79,12 +79,13 @@ test('address with primary name that has no resolver - strict', async () => {
).rejects.toMatchInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "reverse" reverted.

Error: ResolverWildcardNotSupported()
Error: ResolverNotFound(bytes name)
(0x0b726574e286a9efb88f726e0365746800)

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: reverse(bytes reverseName)
args: (0x28303030303030303030303030363161643865653139303731303530386138313861653533323563330461646472077265766572736500)
function: reverse(bytes reverseName, uint256 coinType)
args: (0x00000000000061aD8EE190710508A818aE5325C3, 60)

Docs: https://viem.sh/docs/contract/readContract
Version: [email protected]]
Expand All @@ -111,13 +112,13 @@ describe('primary name with resolver that does not support text()', () => {
).rejects.toMatchInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "reverse" reverted.

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

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: reverse(bytes reverseName)
args: (0x28643864613662663236393634616639643765656439653033653533343135643337616139363034350461646472077265766572736500)
function: reverse(bytes reverseName, uint256 coinType)
args: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 60)

Docs: https://viem.sh/docs/contract/readContract
Version: [email protected]]
Expand Down Expand Up @@ -145,12 +146,13 @@ describe('primary name with non-contract resolver', () => {
).rejects.toMatchInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "reverse" reverted.

Error: ResolverNotContract()
Error: ResolverNotContract(bytes name, address resolver)
(0x08766275746572696e0365746800, 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045)

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: reverse(bytes reverseName)
args: (0x28643864613662663236393634616639643765656439653033653533343135643337616139363034350461646472077265766572736500)
function: reverse(bytes reverseName, uint256 coinType)
args: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 60)

Docs: https://viem.sh/docs/contract/readContract
Version: [email protected]]
Expand All @@ -165,7 +167,7 @@ describe('http error', () => {
server = await createHttpServer((_, res) => {
const parsed = parseAbi([
'function query((address,string[],bytes)[]) returns (bool[],bytes[])',
'error HttpError((uint16,string)[])',
'error HttpError(uint16,string)',
])

const encoded = encodeFunctionResult({
Expand All @@ -177,7 +179,7 @@ describe('http error', () => {
encodeErrorResult({
abi: parsed,
errorName: 'HttpError',
args: [[[404, 'Not Found']]],
args: [404, 'Not Found'],
}),
],
],
Expand Down Expand Up @@ -205,52 +207,21 @@ describe('http error', () => {
gatewayUrls: [server!.url],
strict: true,
}),
).rejects.toThrowError(`The contract function "reverse" reverted.
).rejects.toThrowError(`The contract function "reverseWithGateways" reverted.

Error: HttpError((uint16 status, string message)[])`)
Error: HttpError(uint16 status, string message)`)
})
})

test('custom universal resolver address', async () => {
await expect(
getEnsName(client, {
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376',
universalResolverAddress: '0x64969fb44091A7E5fA1213D30D7A7e8488edf693',
}),
).resolves.toMatchInlineSnapshot('"awkweb.eth"')
})

describe('universal resolver with generic errors', () => {
test('address with primary name that has no resolver', async () => {
await expect(
getEnsName(client, {
address: '0x00000000000061aD8EE190710508A818aE5325C3',
universalResolverAddress: '0xc0497E381f536Be9ce14B0dD3817cBcAe57d2F62',
}),
).resolves.toMatchInlineSnapshot('null')
})
test('address with primary name that has no resolver - strict', async () => {
await expect(
getEnsName(client, {
address: '0x00000000000061aD8EE190710508A818aE5325C3',
universalResolverAddress: '0xc0497E381f536Be9ce14B0dD3817cBcAe57d2F62',
strict: true,
}),
).rejects.toMatchInlineSnapshot(`
[ContractFunctionExecutionError: The contract function "reverse" reverted with the following reason:
UniversalResolver: Wildcard on non-extended resolvers is not supported

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: reverse(bytes reverseName)
args: (0x28303030303030303030303030363161643865653139303731303530386138313861653533323563330461646472077265766572736500)

Docs: https://viem.sh/docs/contract/readContract
Version: [email protected]]
`)
})
})

test('chain not provided', async () => {
await expect(
getEnsName(
Expand Down Expand Up @@ -295,7 +266,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 22138945 (current block 14353601).

Version: [email protected]]
`)
Expand All @@ -312,8 +283,8 @@ test('invalid universal resolver address', async () => {

Contract Call:
address: 0x0000000000000000000000000000000000000000
function: reverse(bytes reverseName)
args: (0x28613063663739383831366434623962393836366235333330656561343661313833383266323531650461646472077265766572736500)
function: reverse(bytes reverseName, uint256 coinType)
args: (0xA0Cf798816D4b9b9866b5330EEa46a18382f251e, 60)

Docs: https://viem.sh/docs/contract/readContract
Version: [email protected]]
Expand Down
Loading
Loading