diff --git a/packages/ensjs/archive.tar.lz4 b/packages/ensjs/archive.tar.lz4 index 81d8a65b..bc61da09 100644 Binary files a/packages/ensjs/archive.tar.lz4 and b/packages/ensjs/archive.tar.lz4 differ diff --git a/packages/ensjs/package.json b/packages/ensjs/package.json index 3f021407..724db8b1 100644 --- a/packages/ensjs/package.json +++ b/packages/ensjs/package.json @@ -111,7 +111,7 @@ }, "devDependencies": { "@ensdomains/buffer": "^0.0.13", - "@ensdomains/ens-contracts": "0.0.17", + "@ensdomains/ens-contracts": "0.0.22", "@ensdomains/ens-test-env": "workspace:*", "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", "@openzeppelin/contracts": "^4.5.0", diff --git a/packages/ensjs/src/functions/public/_getAbi.ts b/packages/ensjs/src/functions/public/_getAbi.ts index 787fdbf2..7c3e655d 100644 --- a/packages/ensjs/src/functions/public/_getAbi.ts +++ b/packages/ensjs/src/functions/public/_getAbi.ts @@ -48,6 +48,8 @@ const decode = async ( _client: ClientWithEns, data: Hex, ): Promise => { + if (data === '0x') return null + const [bigintContentType, encodedAbiData] = decodeFunctionResult({ abi: publicResolverAbiSnippet, functionName: 'ABI', diff --git a/packages/ensjs/src/functions/public/_getAddr.ts b/packages/ensjs/src/functions/public/_getAddr.ts index af746d74..40225d6e 100644 --- a/packages/ensjs/src/functions/public/_getAddr.ts +++ b/packages/ensjs/src/functions/public/_getAddr.ts @@ -74,6 +74,7 @@ const decode = async ( data: Hex, { coin }: InternalGetAddrParameters, ): Promise => { + if (data === '0x') return null if (!coin) { coin = 60 } diff --git a/packages/ensjs/src/functions/public/_getContentHash.ts b/packages/ensjs/src/functions/public/_getContentHash.ts index e7a0c7e9..678b218a 100644 --- a/packages/ensjs/src/functions/public/_getContentHash.ts +++ b/packages/ensjs/src/functions/public/_getContentHash.ts @@ -36,6 +36,8 @@ const decode = async ( _client: ClientWithEns, data: Hex, ): Promise => { + if (data === '0x') return null + const response = decodeFunctionResult({ abi: publicResolverContenthashSnippet, functionName: 'contenthash', diff --git a/packages/ensjs/src/functions/public/_getText.ts b/packages/ensjs/src/functions/public/_getText.ts index bea4ba2e..3fce958e 100644 --- a/packages/ensjs/src/functions/public/_getText.ts +++ b/packages/ensjs/src/functions/public/_getText.ts @@ -33,6 +33,8 @@ const decode = async ( _client: ClientWithEns, data: Hex, ): Promise => { + if (data === '0x') return null + const response = decodeFunctionResult({ abi: publicResolverTextSnippet, functionName: 'text', diff --git a/packages/ensjs/src/functions/public/batch.test.ts b/packages/ensjs/src/functions/public/batch.test.ts index 9430f291..369bcf3c 100644 --- a/packages/ensjs/src/functions/public/batch.test.ts +++ b/packages/ensjs/src/functions/public/batch.test.ts @@ -1,7 +1,10 @@ import { createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' import { addEnsContracts } from '../../contracts/addEnsContracts.js' -import { publicClient } from '../../tests/addTestContracts.js' +import { + deploymentAddresses, + publicClient, +} from '../../tests/addTestContracts.js' import batch from './batch.js' import getAddressRecord from './getAddressRecord.js' import getName from './getName.js' @@ -31,8 +34,8 @@ describe('batch', () => { { "match": true, "name": "with-profile.eth", - "resolverAddress": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", - "reverseResolverAddress": "0x70e0bA845a1A0F2DA3359C97E0285013525FFC49", + "resolverAddress": "${deploymentAddresses.LegacyPublicResolver}", + "reverseResolverAddress": "${deploymentAddresses.PublicResolver}", }, ] `) diff --git a/packages/ensjs/src/functions/public/getAbiRecord.ts b/packages/ensjs/src/functions/public/getAbiRecord.ts index a132c47f..569637d4 100644 --- a/packages/ensjs/src/functions/public/getAbiRecord.ts +++ b/packages/ensjs/src/functions/public/getAbiRecord.ts @@ -1,6 +1,10 @@ import type { Hex } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' -import type { Prettify, SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + Prettify, + SimpleTransactionRequest, +} from '../../types.js' import { generateFunction, type GeneratedFunction, @@ -26,9 +30,9 @@ const encode = ( const decode = async ( client: ClientWithEns, data: Hex, + passthrough: GenericPassthrough, ): Promise => { - const urData = await universalWrapper.decode(client, data) - if (!urData) return null + const urData = await universalWrapper.decode(client, data, passthrough) return _getAbi.decode(client, urData.data) } diff --git a/packages/ensjs/src/functions/public/getAddressRecord.ts b/packages/ensjs/src/functions/public/getAddressRecord.ts index 18c98544..2bc41551 100644 --- a/packages/ensjs/src/functions/public/getAddressRecord.ts +++ b/packages/ensjs/src/functions/public/getAddressRecord.ts @@ -1,6 +1,10 @@ import type { Hex } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' -import type { Prettify, SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + Prettify, + SimpleTransactionRequest, +} from '../../types.js' import { generateFunction, type GeneratedFunction, @@ -26,10 +30,10 @@ const encode = ( const decode = async ( client: ClientWithEns, data: Hex, + passthrough: GenericPassthrough, args: GetAddressRecordParameters, ): Promise => { - const urData = await universalWrapper.decode(client, data) - if (!urData) return null + const urData = await universalWrapper.decode(client, data, passthrough) return _getAddr.decode(client, urData.data, args) } diff --git a/packages/ensjs/src/functions/public/getAvailable.ts b/packages/ensjs/src/functions/public/getAvailable.ts index 5f558ab5..0e8020f1 100644 --- a/packages/ensjs/src/functions/public/getAvailable.ts +++ b/packages/ensjs/src/functions/public/getAvailable.ts @@ -1,4 +1,5 @@ import { + BaseError, decodeFunctionResult, encodeFunctionData, labelhash, @@ -50,8 +51,9 @@ const encode = ( const decode = async ( _client: ClientWithEns, - data: Hex, + data: Hex | BaseError, ): Promise => { + if (typeof data === 'object') throw data const result = decodeFunctionResult({ abi: baseRegistrarAvailableSnippet, functionName: 'available', diff --git a/packages/ensjs/src/functions/public/getContentHashRecord.ts b/packages/ensjs/src/functions/public/getContentHashRecord.ts index 82f8a405..7f507aeb 100644 --- a/packages/ensjs/src/functions/public/getContentHashRecord.ts +++ b/packages/ensjs/src/functions/public/getContentHashRecord.ts @@ -1,6 +1,10 @@ -import type { Hex } from 'viem' +import type { BaseError, Hex } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' -import type { Prettify, SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + Prettify, + TransactionRequestWithPassthrough, +} from '../../types.js' import { generateFunction, type GeneratedFunction, @@ -20,17 +24,17 @@ export type GetContentHashRecordReturnType = const encode = ( client: ClientWithEns, { name }: GetContentHashRecordParameters, -): SimpleTransactionRequest => { +): TransactionRequestWithPassthrough => { const prData = _getContentHash.encode(client, { name }) return universalWrapper.encode(client, { name, data: prData.data }) } const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, ): Promise => { - const urData = await universalWrapper.decode(client, data) - if (!urData) return null + const urData = await universalWrapper.decode(client, data, passthrough) return _getContentHash.decode(client, urData.data) } diff --git a/packages/ensjs/src/functions/public/getExpiry.ts b/packages/ensjs/src/functions/public/getExpiry.ts index 3e23b04d..fe7230cb 100644 --- a/packages/ensjs/src/functions/public/getExpiry.ts +++ b/packages/ensjs/src/functions/public/getExpiry.ts @@ -1,4 +1,5 @@ import { + BaseError, decodeFunctionResult, encodeFunctionData, labelhash, @@ -110,9 +111,10 @@ const encode = ( const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, { name, contract }: GetExpiryParameters, ): Promise => { + if (typeof data === 'object') throw data const labels = name.split('.') const result = await multicallWrapper.decode(client, data, []) diff --git a/packages/ensjs/src/functions/public/getName.test.ts b/packages/ensjs/src/functions/public/getName.test.ts index 2aa9cb53..91979dd9 100644 --- a/packages/ensjs/src/functions/public/getName.test.ts +++ b/packages/ensjs/src/functions/public/getName.test.ts @@ -1,6 +1,29 @@ -import { publicClient } from '../../tests/addTestContracts.js' +import type { Address, Hex } from 'viem' +import { + deploymentAddresses, + publicClient, + testClient, + waitForTransaction, + walletClient, +} from '../../tests/addTestContracts.js' +import setPrimaryName from '../wallet/setPrimaryName.js' import getName from './getName.js' +let snapshot: Hex +let accounts: Address[] + +beforeAll(async () => { + accounts = await walletClient.getAddresses() +}) + +beforeEach(async () => { + snapshot = await testClient.snapshot() +}) + +afterEach(async () => { + await testClient.revert({ id: snapshot }) +}) + describe('getName', () => { it('should get a primary name from an address', async () => { const result = await getName(publicClient, { @@ -10,37 +33,34 @@ describe('getName', () => { { "match": true, "name": "with-profile.eth", - "resolverAddress": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", - "reverseResolverAddress": "0x70e0bA845a1A0F2DA3359C97E0285013525FFC49", + "resolverAddress": "${deploymentAddresses.LegacyPublicResolver}", + "reverseResolverAddress": "${deploymentAddresses.PublicResolver}", } `) - // expect(result).toBeTruthy() - // if (result) { - // expect(result.name).toBe('with-profile.eth') - // expect(result.match).toBeTruthy() - // } }) - it.todo( - 'should return null for an address with no primary name', - // async () => { - // const result = await getName(testClient, { - // address: '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0', - // }) - // expect(result).toMatchInlineSnapshot(``) - // }, - ) - it.todo( - 'should return with a false match for a name with no forward resolution', - // async () => { - // const tx = await ensInstance.setName('with-profile.eth') - // await tx?.wait() + it('should return null for an address with no primary name', async () => { + const result = await getName(publicClient, { + address: '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0', + }) + expect(result).toBeNull() + }) + it('should return with a false match for a name with no forward resolution', async () => { + const tx = await setPrimaryName(walletClient, { + name: 'with-profile.eth', + account: accounts[0], + }) + await waitForTransaction(tx) - // const result = await ensInstance.getName(accounts[0]) - // expect(result).toBeTruthy() - // if (result) { - // expect(result.name).toBe('with-profile.eth') - // expect(result.match).toBeFalsy() - // } - // }, - ) + const result = await getName(publicClient, { + address: accounts[0], + }) + expect(result).toMatchInlineSnapshot(` + { + "match": false, + "name": "with-profile.eth", + "resolverAddress": "${deploymentAddresses.LegacyPublicResolver}", + "reverseResolverAddress": "${deploymentAddresses.PublicResolver}", + } + `) + }) }) diff --git a/packages/ensjs/src/functions/public/getName.ts b/packages/ensjs/src/functions/public/getName.ts index 6a9a8b5a..6d14960b 100644 --- a/packages/ensjs/src/functions/public/getName.ts +++ b/packages/ensjs/src/functions/public/getName.ts @@ -1,6 +1,9 @@ import { + BaseError, + decodeErrorResult, decodeFunctionResult, encodeFunctionData, + getContractError, toHex, type Address, type Hex, @@ -8,11 +11,15 @@ import { import type { ClientWithEns } from '../../contracts/consts.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import { universalResolverReverseSnippet } from '../../contracts/universalResolver.js' -import type { SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + TransactionRequestWithPassthrough, +} from '../../types.js' import { generateFunction, type GeneratedFunction, } from '../../utils/generateFunction.js' +import { getRevertErrorData } from '../../utils/getRevertErrorData.js' import { packetToBytes } from '../../utils/hexEncodedName.js' export type GetNameParameters = { @@ -34,23 +41,50 @@ export type GetNameReturnType = { const encode = ( client: ClientWithEns, { address }: GetNameParameters, -): SimpleTransactionRequest => { +): TransactionRequestWithPassthrough => { const reverseNode = `${address.toLowerCase().substring(2)}.addr.reverse` + const to = getChainContractAddress({ + client, + contract: 'ensUniversalResolver', + }) + const args = [toHex(packetToBytes(reverseNode))] as const return { - to: getChainContractAddress({ client, contract: 'ensUniversalResolver' }), + to, data: encodeFunctionData({ abi: universalResolverReverseSnippet, functionName: 'reverse', - args: [toHex(packetToBytes(reverseNode))], + args, }), + passthrough: { address, args }, } } const decode = async ( _client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, { address }: GetNameParameters, ): Promise => { + if (typeof data === 'object') { + const errorData = getRevertErrorData(data) + if (errorData) { + const decodedError = decodeErrorResult({ + abi: universalResolverReverseSnippet, + data: errorData, + }) + if ( + decodedError.errorName === 'ResolverNotFound' || + decodedError.errorName === 'ResolverWildcardNotSupported' + ) + return null + } + throw getContractError(data, { + abi: universalResolverReverseSnippet, + functionName: 'reverse', + args: passthrough.args, + address: passthrough.address, + }) + } const result = decodeFunctionResult({ abi: universalResolverReverseSnippet, functionName: 'reverse', diff --git a/packages/ensjs/src/functions/public/getOwner.test.ts b/packages/ensjs/src/functions/public/getOwner.test.ts index bf2e0f4e..5ab68677 100644 --- a/packages/ensjs/src/functions/public/getOwner.test.ts +++ b/packages/ensjs/src/functions/public/getOwner.test.ts @@ -1,4 +1,7 @@ -import { publicClient } from '../../tests/addTestContracts.js' +import { + deploymentAddresses, + publicClient, +} from '../../tests/addTestContracts.js' import getOwner from './getOwner.js' describe('getOwner', () => { @@ -15,7 +18,7 @@ describe('getOwner', () => { const result = await getOwner(publicClient, { name: 'expired-wrapped.eth' }) expect(result).toMatchInlineSnapshot(` { - "owner": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", + "owner": "${deploymentAddresses.NameWrapper}", "ownershipLevel": "registrar", "registrant": null, } diff --git a/packages/ensjs/src/functions/public/getOwner.ts b/packages/ensjs/src/functions/public/getOwner.ts index 3419823b..8c5afd40 100644 --- a/packages/ensjs/src/functions/public/getOwner.ts +++ b/packages/ensjs/src/functions/public/getOwner.ts @@ -1,4 +1,4 @@ -import { decodeAbiParameters, type Address, type Hex } from 'viem' +import { BaseError, decodeAbiParameters, type Address, type Hex } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import type { SimpleTransactionRequest } from '../../types.js' @@ -106,9 +106,10 @@ const addressDecode = (data: Hex) => const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, { name, contract }: GetOwnerParameters, ): Promise => { + if (typeof data === 'object') throw data const labels = name.split('.') if (contract || labels.length === 1) { const singleOwner = addressDecode(data) diff --git a/packages/ensjs/src/functions/public/getPrice.ts b/packages/ensjs/src/functions/public/getPrice.ts index 93122512..1e262200 100644 --- a/packages/ensjs/src/functions/public/getPrice.ts +++ b/packages/ensjs/src/functions/public/getPrice.ts @@ -1,4 +1,9 @@ -import { decodeFunctionResult, encodeFunctionData, type Hex } from 'viem' +import { + BaseError, + decodeFunctionResult, + encodeFunctionData, + type Hex, +} from 'viem' import { bulkRenewalRentPriceSnippet } from '../../contracts/bulkRenewal.js' import type { ClientWithEns } from '../../contracts/consts.js' import { ethRegistrarControllerRentPriceSnippet } from '../../contracts/ethRegistrarController.js' @@ -85,9 +90,10 @@ const encode = ( const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, { nameOrNames }: GetPriceParameters, ): Promise => { + if (typeof data === 'object') throw data const isBulkRenewal = Array.isArray(nameOrNames) && nameOrNames.length > 1 if (isBulkRenewal) { const result = await multicallWrapper.decode(client, data, []) diff --git a/packages/ensjs/src/functions/public/getRecords.test.ts b/packages/ensjs/src/functions/public/getRecords.test.ts index e6d31f33..5a23ef83 100644 --- a/packages/ensjs/src/functions/public/getRecords.test.ts +++ b/packages/ensjs/src/functions/public/getRecords.test.ts @@ -1,4 +1,7 @@ -import { publicClient } from '../../tests/addTestContracts.js' +import { + deploymentAddresses, + publicClient, +} from '../../tests/addTestContracts.js' import getRecords from './getRecords.js' describe('getRecords()', () => { @@ -29,7 +32,7 @@ describe('getRecords()', () => { "value": "bc1qjqg9slurvjukfl92wp58y94480fvh4uc2pwa6n", }, ], - "resolverAddress": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "resolverAddress": "${deploymentAddresses.LegacyPublicResolver}", "texts": [ { "key": "description", diff --git a/packages/ensjs/src/functions/public/getRecords.ts b/packages/ensjs/src/functions/public/getRecords.ts index 10324c74..6c37067e 100644 --- a/packages/ensjs/src/functions/public/getRecords.ts +++ b/packages/ensjs/src/functions/public/getRecords.ts @@ -1,9 +1,10 @@ import { + BaseError, decodeAbiParameters, + decodeErrorResult, decodeFunctionResult, encodeFunctionData, - labelhash, - stringToBytes, + getContractError, toHex, type Address, type Hex, @@ -17,9 +18,10 @@ import type { SimpleTransactionRequest, TransactionRequestWithPassthrough, } from '../../types.js' +import { EMPTY_ADDRESS } from '../../utils/consts.js' import { generateFunction } from '../../utils/generateFunction.js' +import { getRevertErrorData } from '../../utils/getRevertErrorData.js' import { packetToBytes } from '../../utils/hexEncodedName.js' -import { encodeLabelhash } from '../../utils/labels.js' import _getAbi, { type InternalGetAbiReturnType } from './_getAbi.js' import _getAddr from './_getAddr.js' import _getContentHash, { @@ -152,21 +154,11 @@ const encode = ( } } - // allow names larger than 255 bytes to be resolved - const formattedName = name - .split('.') - .map((label) => - stringToBytes(label).byteLength > 255 - ? encodeLabelhash(labelhash(label)) - : label, - ) - .join('.') - const encoded = encodeFunctionData({ abi: universalResolverResolveArraySnippet, functionName: 'resolve', args: [ - toHex(packetToBytes(formattedName)), + toHex(packetToBytes(name)), calls.filter((c) => c).map((c) => c!.call.data), ], }) @@ -180,7 +172,7 @@ const encode = ( const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, passthrough: (CallObj | null)[], { name, resolver }: TParams, ): Promise> => { @@ -197,6 +189,50 @@ const decode = async ( resolverAddress = resolver.address recordData = result.map((r) => r.returnData) } else { + if (typeof data === 'object') { + const errorData = getRevertErrorData(data) + if (errorData) { + const decodedError = decodeErrorResult({ + abi: universalResolverResolveArraySnippet, + data: errorData, + }) + if ( + decodedError.errorName === 'ResolverNotFound' || + decodedError.errorName === 'ResolverWildcardNotSupported' + ) + return passthrough.reduce( + (prev, curr) => { + if (!curr) return prev + if (curr.type === 'coin' && !('coin' in prev)) { + return { ...prev, coins: [] } + } + if (curr.type === 'text' && !('texts' in prev)) { + return { ...prev, texts: [] } + } + if (curr.type === 'contentHash' && !('contentHash' in prev)) { + return { ...prev, contentHash: null } + } + // abi + return { ...prev, abi: null } + }, + { + resolverAddress: EMPTY_ADDRESS, + } as unknown as GetRecordsReturnType, + ) + } + throw getContractError(data, { + abi: universalResolverResolveArraySnippet, + functionName: 'resolve', + args: [ + toHex(packetToBytes(name)), + calls.filter((c) => c).map((c) => c!.call.data), + ], + address: getChainContractAddress({ + client, + contract: 'ensUniversalResolver', + }), + }) + } const result = decodeFunctionResult({ abi: universalResolverResolveArraySnippet, functionName: 'resolve', diff --git a/packages/ensjs/src/functions/public/getResolver.ts b/packages/ensjs/src/functions/public/getResolver.ts index b462f251..11ac119c 100644 --- a/packages/ensjs/src/functions/public/getResolver.ts +++ b/packages/ensjs/src/functions/public/getResolver.ts @@ -1,6 +1,8 @@ import { + BaseError, decodeFunctionResult, encodeFunctionData, + getContractError, toHex, type Address, type Hex, @@ -8,7 +10,10 @@ import { import type { ClientWithEns } from '../../contracts/consts.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import { universalResolverFindResolverSnippet } from '../../contracts/universalResolver.js' -import type { SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + TransactionRequestWithPassthrough, +} from '../../types.js' import { EMPTY_ADDRESS } from '../../utils/consts.js' import { generateFunction, @@ -26,21 +31,35 @@ export type GetResolverReturnType = Address | null const encode = ( client: ClientWithEns, { name }: GetResolverParameters, -): SimpleTransactionRequest => { +): TransactionRequestWithPassthrough => { + const address = getChainContractAddress({ + client, + contract: 'ensUniversalResolver', + }) + const args = [toHex(packetToBytes(name))] as const return { - to: getChainContractAddress({ client, contract: 'ensUniversalResolver' }), + to: address, data: encodeFunctionData({ abi: universalResolverFindResolverSnippet, functionName: 'findResolver', - args: [toHex(packetToBytes(name))], + args, }), + passthrough: { address, args }, } } const decode = async ( _client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, ): Promise => { + if (typeof data === 'object') + throw getContractError(data, { + abi: universalResolverFindResolverSnippet, + functionName: 'findResolver', + args: passthrough.args, + address: passthrough.address, + }) const response = decodeFunctionResult({ abi: universalResolverFindResolverSnippet, functionName: 'findResolver', diff --git a/packages/ensjs/src/functions/public/getSupportedInterfaces.ts b/packages/ensjs/src/functions/public/getSupportedInterfaces.ts index c69b7c94..073601f0 100644 --- a/packages/ensjs/src/functions/public/getSupportedInterfaces.ts +++ b/packages/ensjs/src/functions/public/getSupportedInterfaces.ts @@ -1,4 +1,4 @@ -import { encodeFunctionData, type Address, type Hex } from 'viem' +import { BaseError, encodeFunctionData, type Address, type Hex } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' import { erc165SupportsInterfaceSnippet } from '../../contracts/erc165.js' import type { @@ -41,9 +41,10 @@ const encode = ( const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, passthrough: SimpleTransactionRequest[], ): Promise => { + if (typeof data === 'object') throw data const result = await multicallWrapper.decode(client, data, passthrough) return result.map((r) => r.success && r.returnData === '0x01') } diff --git a/packages/ensjs/src/functions/public/getTextRecord.ts b/packages/ensjs/src/functions/public/getTextRecord.ts index 10f0b2b1..ee8dd32f 100644 --- a/packages/ensjs/src/functions/public/getTextRecord.ts +++ b/packages/ensjs/src/functions/public/getTextRecord.ts @@ -1,6 +1,10 @@ -import type { Hex } from 'viem' +import type { BaseError, Hex } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' -import type { Prettify, SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + Prettify, + SimpleTransactionRequest, +} from '../../types.js' import { generateFunction, type GeneratedFunction, @@ -25,10 +29,10 @@ const encode = ( const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, ): Promise => { - const urData = await universalWrapper.decode(client, data) - if (!urData) return null + const urData = await universalWrapper.decode(client, data, passthrough) return _getText.decode(client, urData.data) } diff --git a/packages/ensjs/src/functions/public/getWrapperData.ts b/packages/ensjs/src/functions/public/getWrapperData.ts index 95507f92..4cde3422 100644 --- a/packages/ensjs/src/functions/public/getWrapperData.ts +++ b/packages/ensjs/src/functions/public/getWrapperData.ts @@ -1,6 +1,8 @@ import { + BaseError, decodeFunctionResult, encodeFunctionData, + getContractError, type Address, type Hex, } from 'viem' @@ -9,8 +11,9 @@ import { getChainContractAddress } from '../../contracts/getChainContractAddress import { nameWrapperGetDataSnippet } from '../../contracts/nameWrapper.js' import type { DateWithValue, + GenericPassthrough, Prettify, - SimpleTransactionRequest, + TransactionRequestWithPassthrough, } from '../../types.js' import { EMPTY_ADDRESS } from '../../utils/consts.js' import { decodeFuses } from '../../utils/fuses.js' @@ -40,21 +43,35 @@ export type GetWrapperDataReturnType = Prettify<{ const encode = ( client: ClientWithEns, { name }: GetWrapperDataParameters, -): SimpleTransactionRequest => { +): TransactionRequestWithPassthrough => { + const address = getChainContractAddress({ + client, + contract: 'ensNameWrapper', + }) + const args = [BigInt(namehash(name))] as const return { - to: getChainContractAddress({ client, contract: 'ensNameWrapper' }), + to: address, data: encodeFunctionData({ abi: nameWrapperGetDataSnippet, functionName: 'getData', - args: [BigInt(namehash(name))], + args, }), + passthrough: { address, args }, } } const decode = async ( _client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, ): Promise => { + if (typeof data === 'object') + throw getContractError(data, { + abi: nameWrapperGetDataSnippet, + functionName: 'getData', + args: passthrough.args, + address: passthrough.address, + }) const [owner, fuses, expiry] = decodeFunctionResult({ abi: nameWrapperGetDataSnippet, functionName: 'getData', diff --git a/packages/ensjs/src/functions/public/getWrapperName.ts b/packages/ensjs/src/functions/public/getWrapperName.ts index 4682d289..5d17fb7e 100644 --- a/packages/ensjs/src/functions/public/getWrapperName.ts +++ b/packages/ensjs/src/functions/public/getWrapperName.ts @@ -1,13 +1,18 @@ import { + BaseError, decodeFunctionResult, encodeFunctionData, + getContractError, hexToBytes, type Hex, } from 'viem' import type { ClientWithEns } from '../../contracts/consts.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import { nameWrapperNamesSnippet } from '../../contracts/nameWrapper.js' -import type { SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + TransactionRequestWithPassthrough, +} from '../../types.js' import { generateFunction, type GeneratedFunction, @@ -25,21 +30,35 @@ export type GetWrapperNameReturnType = string | null const encode = ( client: ClientWithEns, { name }: GetWrapperNameParameters, -): SimpleTransactionRequest => { +): TransactionRequestWithPassthrough => { + const address = getChainContractAddress({ + client, + contract: 'ensNameWrapper', + }) + const args = [namehash(name)] as const return { - to: getChainContractAddress({ client, contract: 'ensNameWrapper' }), + to: address, data: encodeFunctionData({ abi: nameWrapperNamesSnippet, functionName: 'names', - args: [namehash(name)], + args, }), + passthrough: { address, args }, } } const decode = async ( _client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, ): Promise => { + if (typeof data === 'object') + throw getContractError(data, { + abi: nameWrapperNamesSnippet, + functionName: 'names', + args: passthrough.args, + address: passthrough.address, + }) const result = decodeFunctionResult({ abi: nameWrapperNamesSnippet, functionName: 'names', diff --git a/packages/ensjs/src/functions/public/multicallWrapper.ts b/packages/ensjs/src/functions/public/multicallWrapper.ts index c7fd2b33..73a2e441 100644 --- a/packages/ensjs/src/functions/public/multicallWrapper.ts +++ b/packages/ensjs/src/functions/public/multicallWrapper.ts @@ -1,6 +1,8 @@ import { + BaseError, decodeFunctionResult, encodeFunctionData, + getContractError, offchainLookup, type Hex, } from 'viem' @@ -42,9 +44,16 @@ const encode = ( const decode = async ( client: ClientWithEns, - data: Hex, + data: Hex | BaseError, transactions: TransactionRequestWithPassthrough[], ): Promise => { + if (typeof data === 'object') { + throw getContractError(data, { + abi: multicallTryAggregateSnippet, + functionName: 'tryAggregate', + args: [], + }) + } const result = decodeFunctionResult({ abi: multicallTryAggregateSnippet, functionName: 'tryAggregate', diff --git a/packages/ensjs/src/functions/public/universalWrapper.ts b/packages/ensjs/src/functions/public/universalWrapper.ts index 26fb9f22..bcafbed4 100644 --- a/packages/ensjs/src/functions/public/universalWrapper.ts +++ b/packages/ensjs/src/functions/public/universalWrapper.ts @@ -1,6 +1,9 @@ import { + BaseError, + decodeErrorResult, decodeFunctionResult, encodeFunctionData, + getContractError, labelhash, toBytes, toHex, @@ -10,8 +13,13 @@ import { import type { ClientWithEns } from '../../contracts/consts.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import { universalResolverResolveSnippet } from '../../contracts/universalResolver.js' -import type { SimpleTransactionRequest } from '../../types.js' +import type { + GenericPassthrough, + TransactionRequestWithPassthrough, +} from '../../types.js' +import { EMPTY_ADDRESS } from '../../utils/consts.js' import { generateFunction } from '../../utils/generateFunction.js' +import { getRevertErrorData } from '../../utils/getRevertErrorData.js' import { packetToBytes } from '../../utils/hexEncodedName.js' import { encodeLabelhash } from '../../utils/labels.js' @@ -28,7 +36,7 @@ export type UniversalWrapperReturnType = { const encode = ( client: ClientWithEns, { name, data }: UniversalWrapperParameters, -): SimpleTransactionRequest => { +): TransactionRequestWithPassthrough => { const nameWithSizedLabels = name .split('.') .map((label) => { @@ -39,20 +47,54 @@ const encode = ( return label }) .join('.') + const to = getChainContractAddress({ + client, + contract: 'ensUniversalResolver', + }) + const args = [toHex(packetToBytes(nameWithSizedLabels)), data] as const return { to: getChainContractAddress({ client, contract: 'ensUniversalResolver' }), data: encodeFunctionData({ abi: universalResolverResolveSnippet, functionName: 'resolve', - args: [toHex(packetToBytes(nameWithSizedLabels)), data], + args, }), + passthrough: { + args, + address: to, + }, } } const decode = async ( _client: ClientWithEns, - data: Hex, + data: Hex | BaseError, + passthrough: GenericPassthrough, ): Promise => { + if (typeof data === 'object') { + const errorData = getRevertErrorData(data) + if (errorData) { + const decodedError = decodeErrorResult({ + abi: universalResolverResolveSnippet, + data: errorData, + }) + if ( + decodedError.errorName === 'ResolverNotFound' || + decodedError.errorName === 'ResolverWildcardNotSupported' + ) + return { + data: '0x', + resolver: EMPTY_ADDRESS, + } + } + throw getContractError(data, { + abi: universalResolverResolveSnippet, + functionName: 'resolve', + args: passthrough.args, + address: passthrough.address, + }) + } + const result = decodeFunctionResult({ abi: universalResolverResolveSnippet, functionName: 'resolve', diff --git a/packages/ensjs/src/tests/addTestContracts.ts b/packages/ensjs/src/tests/addTestContracts.ts index 05005345..edcffa30 100644 --- a/packages/ensjs/src/tests/addTestContracts.ts +++ b/packages/ensjs/src/tests/addTestContracts.ts @@ -31,7 +31,7 @@ type ContractName = | 'ENSRegistry' | 'ReverseRegistrar' | 'UniversalResolver' - | 'BulkRenewal' + | 'StaticBulkRenewal' | 'DNSSECImpl' | 'LegacyDNSRegistrar' | 'LegacyDNSSECImpl' @@ -74,7 +74,7 @@ export const localhost = { address: deploymentAddresses.ReverseRegistrar, }, ensBulkRenewal: { - address: deploymentAddresses.BulkRenewal, + address: deploymentAddresses.StaticBulkRenewal, }, ensDnssecImpl: { address: deploymentAddresses.LegacyDNSSECImpl, diff --git a/packages/ensjs/src/types.ts b/packages/ensjs/src/types.ts index e2a78167..4ea05112 100644 --- a/packages/ensjs/src/types.ts +++ b/packages/ensjs/src/types.ts @@ -1,5 +1,6 @@ import type { Account, + Address, Client, SendTransactionParameters, TransactionRequest, @@ -18,6 +19,8 @@ export type TransactionRequestWithPassthrough = SimpleTransactionRequest & { passthrough?: any } +export type GenericPassthrough = { args: any; address: Address } + export type Extended = { [K in keyof Client]?: undefined } & { [key: string]: unknown } diff --git a/packages/ensjs/src/utils/generateFunction.ts b/packages/ensjs/src/utils/generateFunction.ts index 40fe4313..b097802a 100644 --- a/packages/ensjs/src/utils/generateFunction.ts +++ b/packages/ensjs/src/utils/generateFunction.ts @@ -1,3 +1,4 @@ +import { BaseError } from 'viem' import { call } from 'viem/actions' import type { ClientWithEns } from '../contracts/consts.js' import type { TransactionRequestWithPassthrough } from '../types.js' @@ -62,8 +63,12 @@ export const generateFunction = < }) => { const single = async function (client, ...args) { const { passthrough, ...encodedData } = encode(client, ...args) - const { data: result } = await call(client, encodedData) - if (!result) return null + const result = await call(client, encodedData) + .then((ret) => ret.data) + .catch((e) => { + if (!(e instanceof BaseError)) throw e + return e + }) if (passthrough) return decode(client, result, passthrough, ...args) return decode(client, result, ...args) } as TFunction diff --git a/packages/ensjs/src/utils/getRevertErrorData.ts b/packages/ensjs/src/utils/getRevertErrorData.ts new file mode 100644 index 00000000..4682fa78 --- /dev/null +++ b/packages/ensjs/src/utils/getRevertErrorData.ts @@ -0,0 +1,7 @@ +import { BaseError, RawContractError } from 'viem' + +export const getRevertErrorData = (err: unknown) => { + if (!(err instanceof BaseError)) return undefined + const error = err.walk() as RawContractError + return typeof error.data === 'object' ? error.data.data : error.data +} diff --git a/packages/ensjs/src/utils/hexEncodedName.ts b/packages/ensjs/src/utils/hexEncodedName.ts index 8df957fe..2f3e5f4b 100644 --- a/packages/ensjs/src/utils/hexEncodedName.ts +++ b/packages/ensjs/src/utils/hexEncodedName.ts @@ -1,29 +1,32 @@ // Adapted from https://github.com/mafintosh/dns-packet -import { bytesToString, stringToBytes, type ByteArray } from 'viem' +import { bytesToString, labelhash, stringToBytes, type ByteArray } from 'viem' +import { encodeLabelhash } from './labels.js' /* * @description Encodes a DNS packet into a ByteArray containing a UDP payload. */ export function packetToBytes(packet: string): ByteArray { - function length(value: string) { - if (value === '.' || value === '..') return 1 - return stringToBytes(value.replace(/^\.|\.$/gm, '')).length + 2 - } - - const bytes = new Uint8Array(length(packet)) // strip leading and trailing `.` const value = packet.replace(/^\.|\.$/gm, '') - if (!value.length) return bytes + if (value.length === 0) return new Uint8Array(1) + + const bytes = new Uint8Array(stringToBytes(value).byteLength + 2) let offset = 0 const list = value.split('.') for (let i = 0; i < list.length; i += 1) { - const encoded = stringToBytes(list[i]) + let encoded = stringToBytes(list[i]) + // if the length is > 255, make the encoded label value a labelhash + // this is compatible with the universal resolver + if (encoded.byteLength > 255) + encoded = stringToBytes(encodeLabelhash(labelhash(list[i]))) bytes[offset] = encoded.length bytes.set(encoded, offset + 1) offset += encoded.length + 1 } + if (bytes.byteLength !== offset + 1) return bytes.slice(0, offset + 1) + return bytes } diff --git a/packages/ensjs/src/utils/ownerFromContract.test.ts b/packages/ensjs/src/utils/ownerFromContract.test.ts index 1c465bab..7b7d9134 100644 --- a/packages/ensjs/src/utils/ownerFromContract.test.ts +++ b/packages/ensjs/src/utils/ownerFromContract.test.ts @@ -1,5 +1,5 @@ import { getVersion } from '../errors/error-utils.js' -import { publicClient } from '../tests/addTestContracts.js' +import { deploymentAddresses, publicClient } from '../tests/addTestContracts.js' import { namehash } from './normalise.js' import { ownerFromContract } from './ownerFromContract.js' @@ -13,7 +13,7 @@ it('uses nameWrapper contract when contract is nameWrapper', () => { .toMatchInlineSnapshot(` { "data": "0x6352211eeb4f647bea6caa36333c816d7b46fdcb05f9466ecacc140ea8c66faf15b3d9f1", - "to": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", + "to": "${deploymentAddresses.NameWrapper}", } `) }) @@ -22,7 +22,7 @@ it('uses registry contract when contract is registry', () => { .toMatchInlineSnapshot(` { "data": "0x02571be3eb4f647bea6caa36333c816d7b46fdcb05f9466ecacc140ea8c66faf15b3d9f1", - "to": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "to": "${deploymentAddresses.ENSRegistry}", } `) }) @@ -36,7 +36,7 @@ it('uses registrar contract when contract is registrar', () => { ).toMatchInlineSnapshot(` { "data": "0x6352211e9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658", - "to": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "to": "${deploymentAddresses.BaseRegistrarImplementation}", } `) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea87fb78..af45a13b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,7 +102,7 @@ importers: '@ensdomains/buffer': ^0.0.13 '@ensdomains/content-hash': ^2.5.7 '@ensdomains/dnsprovejs': ^0.4.1 - '@ensdomains/ens-contracts': 0.0.17 + '@ensdomains/ens-contracts': 0.0.22 '@ensdomains/ens-test-env': workspace:* '@nomiclabs/hardhat-ethers': npm:hardhat-deploy-ethers@0.3.0-beta.13 '@openzeppelin/contracts': ^4.5.0 @@ -156,7 +156,7 @@ importers: traverse: 0.6.6 devDependencies: '@ensdomains/buffer': 0.0.13_hardhat@2.16.1 - '@ensdomains/ens-contracts': 0.0.17_hardhat@2.16.1 + '@ensdomains/ens-contracts': 0.0.22 '@ensdomains/ens-test-env': link:../ens-test-env '@nomiclabs/hardhat-ethers': /hardhat-deploy-ethers/0.3.0-beta.13_drbn5rr2wg6skrabufgggpzpcu '@openzeppelin/contracts': 4.7.3 @@ -629,6 +629,10 @@ packages: - web3-utils dev: true + /@ensdomains/buffer/0.1.1: + resolution: {integrity: sha512-92SfSiNS8XorgU7OUBHo/i1ZU7JV7iz/6bKuLPNVsMxV79/eI7fJR6jfJJc40zAHjs3ha+Xo965Idomlq3rqnw==} + dev: true + /@ensdomains/content-hash/2.5.7: resolution: {integrity: sha512-WNdGKCeubMIAfyPYTMlKeX6cgXKIEo42OcWPOLBiclzJwMibkVqpaGgWKVH9dniJq7bLXLa2tQ0k/F1pt6gUxA==} dependencies: @@ -646,25 +650,13 @@ packages: typescript-logging: 1.0.1 dev: false - /@ensdomains/ens-contracts/0.0.17_hardhat@2.16.1: - resolution: {integrity: sha512-Dv4y8V5UhswWk/GMwJVu4gV17uoAvSVmzjG4DVSbP7DF3ITtVGwDYclejMQ9JIg8jn84pHSKawzExiC+xJrT3g==} + /@ensdomains/ens-contracts/0.0.22: + resolution: {integrity: sha512-kNu7pp68/5KfJ5wkswnUS4NfI9Ek4zGi0nnNSmGf1WXs6BHU9NYRVR6NnoVzb1B+cZ658e1v2srTtvmBYYIYzg==} dependencies: - '@ensdomains/buffer': 0.0.13_hardhat@2.16.1 + '@ensdomains/buffer': 0.1.1 '@ensdomains/solsha1': 0.0.3 '@openzeppelin/contracts': 4.7.3 dns-packet: 5.4.0 - transitivePeerDependencies: - - '@nomiclabs/hardhat-web3' - - bufferutil - - encoding - - hardhat - - supports-color - - utf-8-validate - - web3 - - web3-core-helpers - - web3-core-promievent - - web3-eth-abi - - web3-utils dev: true /@ensdomains/ens/0.4.5: