diff --git a/src/_test/constants.ts b/src/_test/constants.ts index fe66b59e0ff..5b18416bac2 100644 --- a/src/_test/constants.ts +++ b/src/_test/constants.ts @@ -44,8 +44,9 @@ export const accounts = [ ] as const export const address = { - vitalik: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', + burn: '0x0000000000000000000000000000000000000000', usdcHolder: '0x5414d89a8bf7e99d732bc52f3e6a3ef461c0c078', + vitalik: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', } as const export const initialBlockNumber = BigInt( diff --git a/src/actions/ens/getEnsAddress.test.ts b/src/actions/ens/getEnsAddress.test.ts new file mode 100644 index 00000000000..24f8097e770 --- /dev/null +++ b/src/actions/ens/getEnsAddress.test.ts @@ -0,0 +1,37 @@ +import { expect, test } from 'vitest' + +import { publicClient } from '../../_test' +import { getEnsAddress } from './getEnsAddress' + +test('gets address for name', async () => { + await expect( + getEnsAddress(publicClient, { + name: 'awkweb.eth', + universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', + }), + ).resolves.toMatchInlineSnapshot( + '"0xA0Cf798816D4b9b9866b5330EEa46a18382f251e"', + ) +}) + +test('gets address for name', async () => { + await expect( + getEnsAddress(publicClient, { + name: 'awkweb.eth', + universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', + }), + ).resolves.toMatchInlineSnapshot( + '"0xA0Cf798816D4b9b9866b5330EEa46a18382f251e"', + ) +}) + +test('name without address', async () => { + await expect( + getEnsAddress(publicClient, { + name: 'unregistered-name.eth', + universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', + }), + ).resolves.toMatchInlineSnapshot( + '"0x0000000000000000000000000000000000000000"', + ) +}) diff --git a/src/actions/ens/getEnsAddress.ts b/src/actions/ens/getEnsAddress.ts index a5a9a88a6ab..e7d8656bdab 100644 --- a/src/actions/ens/getEnsAddress.ts +++ b/src/actions/ens/getEnsAddress.ts @@ -1,65 +1,82 @@ import { PublicClient } from '../../clients' -import type { Address } from '../../types' -import { readContract } from '../public' +import type { Address, Hex, Prettify } from '../../types' +import { decodeFunctionResult, encodeFunctionData } from '../../utils' +import { namehash, packetToBuffer } from '../../utils/ens' +import { readContract, ReadContractArgs } from '../public' -export type GetEnsNameArgs = { - /** Address to get ENS name for. */ - address: Address - // TODO: Add block number, etc. -} +export type GetEnsAddressArgs = Prettify< + Pick & { + /** ENS name to get address. */ + name: string + /** Address of ENS Universal Resolver Contract */ + universalResolverAddress: Address + } +> /** - * @description Gets primary name for specified address. + * @description Gets address for ENS name. + * + * - Calls `resolve(bytes, bytes)` on ENS Universal Resolver Contract. + * + * @example + * const ensAddress = await getEnsAddress(publicClient, { + * name: 'wagmi-dev.eth', + * universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', + * }) + * console.log(ensAddress) // '0xd2135CfB216b74109775236E36d4b433F1DF507B' */ -export async function getEnsName( +export async function getEnsAddress( client: PublicClient, - { address }: GetEnsNameArgs, + { blockNumber, blockTag, name, universalResolverAddress }: GetEnsAddressArgs, ) { - const abi = [ - { - name: 'reverse', - type: 'function', - stateMutability: 'view', - inputs: [{ type: 'bytes', name: 'reverseName' }], - outputs: [ - { type: 'string', name: 'resolvedName' }, - { type: 'address', name: 'resolvedAddress' }, - { type: 'address', name: 'reverseResolver' }, - { type: 'address', name: 'resolver' }, - ], - }, - ] as const - const reverseNode = `${address.toLowerCase().substring(2)}.addr.reverse` const res = await readContract(client, { - abi, - address: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', - functionName: 'reverse', - args: [`0x${encode(reverseNode).toString('hex')}`], + address: universalResolverAddress, + abi: [ + { + name: 'resolve', + type: 'function', + stateMutability: 'view', + inputs: [ + { name: 'name', type: 'bytes' }, + { name: 'data', type: 'bytes' }, + ], + outputs: [ + { name: '', type: 'bytes' }, + { name: 'address', type: 'address' }, + ], + }, + ], + functionName: 'resolve', + args: [ + `0x${packetToBuffer(name).toString('hex')}`, + encodeFunctionData({ + abi: [ + { + name: 'addr', + type: 'function', + stateMutability: 'view', + inputs: [{ name: 'name', type: 'bytes32' }], + outputs: [], + }, + ], + functionName: 'addr', + args: [namehash(name)], + }), + ], + blockNumber, + blockTag, + }) + return decodeFunctionResult({ + abi: [ + { + name: 'addr', + type: 'function', + stateMutability: 'view', + inputs: [], + outputs: [{ name: 'name', type: 'address' }], + }, + ], + functionName: 'addr', + data: res[0], }) - return res[0] -} - -// adapted from https://github.com/mafintosh/dns-packet -function encode(str: string) { - function encodingLength(n: string) { - if (n === '.' || n === '..') return 1 - return Buffer.byteLength(n.replace(/^\.|\.$/gm, '')) + 2 - } - - const buf = Buffer.alloc(encodingLength(str)) - let offset = 0 - - // strip leading and trailing . - const n = str.replace(/^\.|\.$/gm, '') - if (n.length) { - const list = n.split('.') - - for (let i = 0; i < list.length; i++) { - const len = buf.write(list[i], offset + 1) - buf[offset] = len - offset += len + 1 - } - } - - return buf } diff --git a/src/actions/ens/getEnsName.test.ts b/src/actions/ens/getEnsName.test.ts index 3a0ae51c548..eef41f995e1 100644 --- a/src/actions/ens/getEnsName.test.ts +++ b/src/actions/ens/getEnsName.test.ts @@ -1,28 +1,32 @@ import { expect, test } from 'vitest' -import { publicClient } from '../../_test' +import { address, publicClient } from '../../_test' import { getEnsName } from './getEnsName' -test('default', async () => { +test('gets primary name for address', async () => { await expect( getEnsName(publicClient, { address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', }), ).resolves.toMatchInlineSnapshot('"awkweb.eth"') +}) - // await expect( - // getEnsName(publicClient, { - // address: '0x5FE6C3F8d12D5Ad1480F6DC01D8c7864Aa58C523', - // }), - // ).rejects.toThrowErrorMatchingInlineSnapshot(` - // "execution reverted - - // Contract: 0x0000000000000000000000000000000000000000 - // Function: reverse(bytes address) - // Arguments: (0x28356665366333663864313264356164313438306636646330316438633738363461613538633532330461646472077265766572736500) +test('address with no primary name', async () => { + await expect( + getEnsName(publicClient, { + address: address.burn, + universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', + }), + ).resolves.toMatchInlineSnapshot('null') +}) - // Details: execution reverted - // Version: viem@1.0.2" - // `) +test('invalid universal resolver address', async () => { + await expect( + getEnsName(publicClient, { + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + universalResolverAddress: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', + }), + ).resolves.toMatchInlineSnapshot('null') }) diff --git a/src/actions/ens/getEnsName.ts b/src/actions/ens/getEnsName.ts index 9cc31e0b1a3..062290cf294 100644 --- a/src/actions/ens/getEnsName.ts +++ b/src/actions/ens/getEnsName.ts @@ -1,65 +1,58 @@ import { PublicClient } from '../../clients' -import type { Address } from '../../types' -import { readContract } from '../public' +import type { Address, Prettify } from '../../types' +import { packetToBuffer } from '../../utils/ens' +import { readContract, ReadContractArgs } from '../public' -export type GetEnsNameArgs = { - /** Address to get ENS name for. */ - address: Address - /** Universal Resolver address */ - universalResolverAddress: Address - // TODO: Add block number, etc. -} +export type GetEnsNameArgs = Prettify< + Pick & { + /** Address to get ENS name for. */ + address: Address + /** Address of ENS Universal Resolver Contract */ + universalResolverAddress: Address + } +> /** * @description Gets primary name for specified address. + * + * - Calls `reverse(bytes)` on ENS Universal Resolver Contract. + * + * @example + * const ensName = await getEnsName(publicClient, { + * address: '0xd2135CfB216b74109775236E36d4b433F1DF507B', + * universalResolverAddress: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', + * }) + * console.log(ensName) // 'wagmi-dev.eth' */ export async function getEnsName( client: PublicClient, - { address }: GetEnsNameArgs, + { address, blockNumber, blockTag, universalResolverAddress }: GetEnsNameArgs, ) { - const abi = [ - { - name: 'reverse', - type: 'function', - stateMutability: 'view', - inputs: [{ type: 'bytes', name: 'reverseName' }], - outputs: [ - { type: 'string', name: 'resolvedName' }, - { type: 'address', name: 'resolvedAddress' }, - { type: 'address', name: 'reverseResolver' }, - { type: 'address', name: 'resolver' }, - ], - }, - ] as const const reverseNode = `${address.toLowerCase().substring(2)}.addr.reverse` - const res = await readContract(client, { - abi, - address: '0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376', - functionName: 'reverse', - args: [`0x${encode(reverseNode).toString('hex')}`], - }) - return res[0] -} - -// Adapted from https://github.com/mafintosh/dns-packet -function encode(packet: string) { - function length(value: string) { - if (value === '.' || value === '..') return 1 - return Buffer.byteLength(value.replace(/^\.|\.$/gm, '')) + 2 - } - - const buffer = Buffer.alloc(length(packet)) - // strip leading and trailing `.` - const value = packet.replace(/^\.|\.$/gm, '') - if (!value.length) return buffer - - let offset = 0 - const list = value.split('.') - for (let i = 0; i < list.length; i++) { - const len = buffer.write(list[i], offset + 1) - buffer[offset] = len - offset += len + 1 + try { + const res = await readContract(client, { + address: universalResolverAddress, + abi: [ + { + name: 'reverse', + type: 'function', + stateMutability: 'view', + inputs: [{ type: 'bytes', name: 'reverseName' }], + outputs: [ + { type: 'string', name: 'resolvedName' }, + { type: 'address', name: 'resolvedAddress' }, + { type: 'address', name: 'reverseResolver' }, + { type: 'address', name: 'resolver' }, + ], + }, + ], + functionName: 'reverse', + args: [`0x${packetToBuffer(reverseNode).toString('hex')}`], + blockNumber, + blockTag, + }) + return res[0] + } catch (error) { + return null } - - return buffer } diff --git a/src/actions/ens/index.test.ts b/src/actions/ens/index.test.ts new file mode 100644 index 00000000000..78905ef6f17 --- /dev/null +++ b/src/actions/ens/index.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' + +import * as actions from './index' + +test('exports actions', () => { + expect(actions).toMatchInlineSnapshot(` + { + "getEnsAddress": [Function], + "getEnsName": [Function], + } + `) +}) diff --git a/src/actions/ens/index.ts b/src/actions/ens/index.ts index 431f25fe883..89da5885bbf 100644 --- a/src/actions/ens/index.ts +++ b/src/actions/ens/index.ts @@ -1 +1,3 @@ +export { getEnsAddress } from './getEnsAddress' + export { getEnsName } from './getEnsName' diff --git a/src/ens.test.ts b/src/ens.test.ts new file mode 100644 index 00000000000..3d054c6b2f1 --- /dev/null +++ b/src/ens.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' + +import * as ens from './ens' + +test('exports ens', () => { + expect(ens).toMatchInlineSnapshot(` + { + "getEnsAddress": [Function], + "getEnsName": [Function], + "labelhash": [Function], + "namehash": [Function], + "normalize": [Function], + } + `) +}) diff --git a/src/ens.ts b/src/ens.ts index 186c5539c6b..f1df86fedd4 100644 --- a/src/ens.ts +++ b/src/ens.ts @@ -1,4 +1,4 @@ -export { getEnsName } from './actions/ens' +export { getEnsAddress, getEnsName } from './actions/ens' export { labelhash, diff --git a/src/types/index.ts b/src/types/index.ts index 2a8477f2c6c..0d43a0aa52e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -78,6 +78,7 @@ export type { export type { PartialBy, + Prettify, MergeIntersectionProperties, OptionalNullable, } from './utils' diff --git a/src/types/utils.ts b/src/types/utils.ts index ac44ab1fa25..498f025cdce 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -45,6 +45,9 @@ type TrimRight = T extends `${infer R}${Chars}` ? TrimRight : T +// h/t https://twitter.com/mattpocockuk/status/1622730173446557697 +export type Prettify = { [K in keyof T]: T[K] } & {} + /** * @description Trims empty space from type T. * diff --git a/src/utils/abi/encodeFunctionData.ts b/src/utils/abi/encodeFunctionData.ts index 9f3f168b5b1..9d65204ae12 100644 --- a/src/utils/abi/encodeFunctionData.ts +++ b/src/utils/abi/encodeFunctionData.ts @@ -1,9 +1,6 @@ import { Abi, Narrow } from 'abitype' -import { - AbiEncodingLengthMismatchError, - AbiFunctionNotFoundError, -} from '../../errors' +import { AbiFunctionNotFoundError } from '../../errors' import { ExtractArgsFromAbi, ExtractFunctionNameFromAbi } from '../../types' import { concatHex } from '../data' import { getFunctionSignature } from '../hash' diff --git a/src/utils/ens/index.test.ts b/src/utils/ens/index.test.ts index 1e8ddfd65c3..952e092cda9 100644 --- a/src/utils/ens/index.test.ts +++ b/src/utils/ens/index.test.ts @@ -8,6 +8,7 @@ test('exports utils', () => { "labelhash": [Function], "namehash": [Function], "normalize": [Function], + "packetToBuffer": [Function], } `) }) diff --git a/src/utils/ens/index.ts b/src/utils/ens/index.ts index 05c78828a60..f2c29be86ca 100644 --- a/src/utils/ens/index.ts +++ b/src/utils/ens/index.ts @@ -3,3 +3,5 @@ export { labelhash } from './labelhash' export { namehash } from './namehash' export { normalize } from './normalize' + +export { packetToBuffer } from './packetToBuffer' diff --git a/src/utils/ens/packetToBuffer.ts b/src/utils/ens/packetToBuffer.ts new file mode 100644 index 00000000000..43f292b5aef --- /dev/null +++ b/src/utils/ens/packetToBuffer.ts @@ -0,0 +1,25 @@ +/* + * @description Encodes a DNS packet into a buffer containing a UDP payload. + */ +export function packetToBuffer(packet: string) { + // Adapted from https://github.com/mafintosh/dns-packet + function length(value: string) { + if (value === '.' || value === '..') return 1 + return Buffer.byteLength(value.replace(/^\.|\.$/gm, '')) + 2 + } + + const buffer = Buffer.alloc(length(packet)) + // strip leading and trailing `.` + const value = packet.replace(/^\.|\.$/gm, '') + if (!value.length) return buffer + + let offset = 0 + const list = value.split('.') + for (let i = 0; i < list.length; i++) { + const len = buffer.write(list[i], offset + 1) + buffer[offset] = len + offset += len + 1 + } + + return buffer +}