import { evmChainIdToCoinType } from '@ensdomains/address-encoder/utils' import { type Address, type BaseError, type Hex, decodeErrorResult, decodeFunctionResult, encodeFunctionData, zeroAddress, } from 'viem' import { normalize } from 'viem/ens' import type { ClientWithEns } from '../../contracts/consts.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import { universalResolverReverseSnippet, universalResolverReverseWithGatewaysSnippet, } from '../../contracts/universalResolver.js' import type { GenericPassthrough, TransactionRequestWithPassthrough, } from '../../types.js' import { checkSafeUniversalResolverData } from '../../utils/checkSafeUniversalResolverData.js' import { type GeneratedFunction, generateFunction, } from '../../utils/generateFunction.js' import { getRevertErrorData } from '../../utils/getRevertErrorData.js' /** * Checks whether a name is already normalised, without coercing it. * * Mirrors viem's `getEnsName` normalization enforcement (wevm/viem#4756): * a name is only returned if it is already in its normalised form. Returns * `false` if the name cannot be normalised (i.e. `normalize` throws). */ const isNormalised = (name: string): boolean => { try { return name === normalize(name) } catch { return false } } type GetNameCoinTypeParameters = { coinType: number chainId?: never } type GetNameChainIdParameters = { chainId: number coinType?: never } export type GetNameParameters = { /** Address to get name for */ address: Address /** Coin type to use for reverse resolution */ coinType?: GetNameCoinTypeParameters['coinType'] /** Chain ID to use for reverse resolution */ chainId?: GetNameChainIdParameters['chainId'] /** Whether or not to allow mismatched forward resolution */ allowMismatch?: boolean /** Whether or not to throw decoding errors */ strict?: boolean /** Batch gateway URLs to use for resolving CCIP-read requests. */ gatewayUrls?: string[] // biome-ignore lint/complexity/noBannedTypes: empty object represents no additional parameters in discriminated union } & (GetNameCoinTypeParameters | GetNameChainIdParameters | {}) export type GetNameReturnType = { /** Primary name for address */ name: string /** Indicates if forward resolution for name matches address */ match: boolean /** Resolver address for reverse node */ reverseResolverAddress: Address /** Resolver address for resolved name */ resolverAddress: Address } const encode = ( client: ClientWithEns, { address, coinType, chainId, gatewayUrls, }: Omit, ): TransactionRequestWithPassthrough => { const to = getChainContractAddress({ client, contract: 'ensUniversalResolver', }) const args = [ address, chainId ? evmChainIdToCoinType(chainId) : coinType || 60n, ] as const return { to, ...(gatewayUrls?.length ? { data: encodeFunctionData({ abi: universalResolverReverseWithGatewaysSnippet, functionName: 'reverseWithGateways', args: [...args, gatewayUrls] as const, }), passthrough: { args: [...args, gatewayUrls], address: to, }, } : { data: encodeFunctionData({ abi: universalResolverReverseSnippet, functionName: 'reverse', args, }), passthrough: { args, address: to, }, }), } } const decode = async ( _client: ClientWithEns, data: Hex | BaseError, passthrough: GenericPassthrough, { allowMismatch, strict, gatewayUrls }: GetNameParameters, ): Promise => { const isSafe = checkSafeUniversalResolverData(data, { strict, abi: gatewayUrls ? universalResolverReverseWithGatewaysSnippet : universalResolverReverseSnippet, args: passthrough.args, functionName: 'reverse', address: passthrough.address, }) if (!isSafe) { if (!allowMismatch) return null const errorData = getRevertErrorData(data) if (!errorData) return null try { const decodedError = decodeErrorResult({ abi: universalResolverReverseSnippet, data: errorData, }) if (decodedError.errorName !== 'ReverseAddressMismatch') return null const [name] = decodedError.args if (!isNormalised(name)) return null return { name, match: false, reverseResolverAddress: zeroAddress, resolverAddress: zeroAddress, } } catch { return null } } try { const [unnormalisedName, resolverAddress, reverseResolverAddress] = decodeFunctionResult({ abi: universalResolverReverseSnippet, functionName: 'reverse', data, }) if (!unnormalisedName) return null if (!isNormalised(unnormalisedName)) return null return { name: unnormalisedName, match: true, reverseResolverAddress, resolverAddress, } } catch (error) { if (strict) throw error return null } } type BatchableFunctionObject = GeneratedFunction /** * Gets the primary name for an address * @param client - {@link ClientWithEns} * @param parameters - {@link GetNameParameters} * @returns Name data object, or `null` if no primary name is set. {@link GetNameReturnType} * * @example * import { createPublicClient, http } from 'viem' * import { mainnet } from 'viem/chains' * import { addEnsContracts } from '@etn-sc/ensjs' * import { getName } from '@etn-sc/ensjs/public' * * const client = createPublicClient({ * chain: addEnsContracts(mainnet), * transport: http(), * }) * const result = await getName(client, { address: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5' }) * // { name: 'nick.eth', match: true, reverseResolverAddress: '0xa2c122be93b0074270ebee7f6b7292c7deb45047', resolverAddress: '0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41' } */ const getName = generateFunction({ encode, decode }) as (( client: ClientWithEns, { address, allowMismatch, strict }: GetNameParameters, ) => Promise) & BatchableFunctionObject export default getName