diff --git a/packages/ensjs/deploy/01_legacy_dnsregistrar.cjs b/packages/ensjs/deploy/01_legacy_dnsregistrar.cjs new file mode 100644 index 00000000..d594c4b0 --- /dev/null +++ b/packages/ensjs/deploy/01_legacy_dnsregistrar.cjs @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable import/no-extraneous-dependencies */ +const { readFile } = require('fs/promises') +const { resolve } = require('path') +const { labelhash } = require('viem') + +const ensContractsPath = './node_modules/@ensdomains/ens-contracts' +const mainnetArtifactsPath = resolve(ensContractsPath, './deployments/mainnet') + +const constructorArgs = [ + '0x00002b000100000e1000244a5c080249aac11d7b6f6446702e54a1607371607a1a41855200fd2ce1cdde32f24e8fb500002b000100000e1000244f660802e06d44b80b8f1d39a95c0b0d7c65d08458e880409bbc683457104237c7f8ec8d', +] + +/** + * @param {string} name + */ +const getMainnetArtifact = async (name) => { + const deployment = JSON.parse( + await readFile(resolve(mainnetArtifactsPath, `${name}.json`), 'utf8'), + ) + const artifact = { + _format: 'hh-sol-artifact-1', + contractName: `Legacy${name}`, + abi: deployment.abi, + bytecode: deployment.bytecode, + deployedBytecode: deployment.deployedBytecode, + linkReferences: {}, + deployedLinkReferences: {}, + } + return artifact +} + +/** + * @type {import('hardhat-deploy/types').DeployFunction} + */ +const func = async function (hre) { + const { getNamedAccounts, deployments } = hre + const { deploy } = deployments + const { deployer, owner } = await getNamedAccounts() + + const { address: legacyDnssecImplAddress } = await deploy( + 'LegacyDNSSECImpl', + { + from: deployer, + args: constructorArgs, + log: true, + contract: await getMainnetArtifact('DNSSECImpl'), + }, + ) + + const dnssec = await hre.ethers.getContract('LegacyDNSSECImpl') + + const algorithms = { + 5: 'RSASHA1Algorithm', + 7: 'RSASHA1Algorithm', + 8: 'RSASHA256Algorithm', + 13: 'P256SHA256Algorithm', + } + const digests = { + 1: 'SHA1Digest', + 2: 'SHA256Digest', + } + + const transactions = [] + for (const [id, alg] of Object.entries(algorithms)) { + const { address } = await deployments.get(alg) + if (address !== (await dnssec.algorithms(id))) { + transactions.push(await dnssec.setAlgorithm(id, address)) + } + } + + for (const [id, digest] of Object.entries(digests)) { + const { address } = await deployments.get(digest) + if (address !== (await dnssec.digests(id))) { + transactions.push(await dnssec.setDigest(id, address)) + } + } + + console.log( + `Waiting on ${transactions.length} transactions setting legacy DNSSEC parameters`, + ) + await Promise.all(transactions.map((tx) => tx.wait())) + + const root = await hre.ethers.getContract('Root') + const { address: registryAddress } = await hre.ethers.getContract( + 'ENSRegistry', + ) + const { address: publicSuffixListAddress } = await hre.ethers.getContract( + 'TLDPublicSuffixList', + ) + + const { address: legacyDnsRegistrarAddress } = await deploy( + 'LegacyDNSRegistrar', + { + from: deployer, + args: [legacyDnssecImplAddress, publicSuffixListAddress, registryAddress], + log: true, + contract: await getMainnetArtifact('DNSRegistrar'), + }, + ) + + const tx = await root + .connect(await hre.ethers.getSigner(owner)) + .setController(legacyDnsRegistrarAddress, true) + console.log( + `Setting LegacyDNSRegistrar as controller of Root... (${tx.hash})`, + ) + await tx.wait() + + const tx2 = await root + .connect(await hre.ethers.getSigner(owner)) + .setSubnodeOwner(labelhash('xyz'), legacyDnsRegistrarAddress) + console.log(`Setting LegacyDNSRegistrar as owner of xyz... (${tx2.hash})`) + await tx2.wait() +} + +func.dependencies = ['Root'] + +module.exports = func diff --git a/packages/ensjs/src/contracts/consts.ts b/packages/ensjs/src/contracts/consts.ts index 832cdb30..ca7dba80 100644 --- a/packages/ensjs/src/contracts/consts.ts +++ b/packages/ensjs/src/contracts/consts.ts @@ -17,11 +17,13 @@ export const supportedChains = ['homestead', 'goerli', 'sepolia'] as const export const supportedContracts = [ 'ensBaseRegistrarImplementation', 'ensDnsRegistrar', + 'ensLegacyDnsRegistrar', 'ensEthRegistrarController', 'ensNameWrapper', 'ensPublicResolver', 'ensReverseRegistrar', 'ensBulkRenewal', + 'ensLegacyDnssecImpl', 'ensDnssecImpl', 'ensUniversalResolver', 'ensRegistry', @@ -30,6 +32,11 @@ export const supportedContracts = [ export type SupportedChain = (typeof supportedChains)[number] export type SupportedContract = (typeof supportedContracts)[number] +type LegacyDnsContracts = 'ensLegacyDnsRegistrar' | 'ensLegacyDnssecImpl' +type DnsContracts = 'ensDnsRegistrar' | 'ensDnssecImpl' +type OptionalContracts = LegacyDnsContracts | DnsContracts +type RequiredContracts = Exclude + export const addresses = { homestead: { ensRegistry: { @@ -38,7 +45,7 @@ export const addresses = { ensBaseRegistrarImplementation: { address: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85', }, - ensDnsRegistrar: { + ensLegacyDnsRegistrar: { address: '0x58774Bb8acD458A640aF0B88238369A167546ef2', }, ensEthRegistrarController: { @@ -56,7 +63,7 @@ export const addresses = { ensBulkRenewal: { address: '0xa12159e5131b1eEf6B4857EEE3e1954744b5033A', }, - ensDnssecImpl: { + ensLegacyDnssecImpl: { address: '0x21745FF62108968fBf5aB1E07961CC0FCBeB2364', }, ensUniversalResolver: { @@ -129,7 +136,11 @@ export const addresses = { }, } as const satisfies Record< SupportedChain, - Record + Record & + ( + | Record + | Record + ) > type Subgraphs = { @@ -158,13 +169,15 @@ export const subgraphs = { type EnsChainContracts = { ensBaseRegistrarImplementation: ChainContract - ensDnsRegistrar: ChainContract + ensDnsRegistrar?: ChainContract + ensLegacyDnsRegistrar?: ChainContract ensEthRegistrarController: ChainContract ensNameWrapper: ChainContract ensPublicResolver: ChainContract ensReverseRegistrar: ChainContract ensBulkRenewal: ChainContract - ensDnssecImpl: ChainContract + ensDnssecImpl?: ChainContract + ensLegacyDnssecImpl?: ChainContract } type BaseChainContracts = { @@ -173,7 +186,10 @@ type BaseChainContracts = { ensRegistry: ChainContract } -export type ChainWithEns = TChain & { +export type ChainWithEns = Omit< + TChain, + 'contracts' +> & { contracts: BaseChainContracts & EnsChainContracts subgraphs: Subgraphs } diff --git a/packages/ensjs/src/contracts/getChainContractAddress.ts b/packages/ensjs/src/contracts/getChainContractAddress.ts index a11015a3..796c536d 100644 --- a/packages/ensjs/src/contracts/getChainContractAddress.ts +++ b/packages/ensjs/src/contracts/getChainContractAddress.ts @@ -1,4 +1,4 @@ -import type { Chain } from 'viem' +import type { Address, Chain } from 'viem' import { getChainContractAddress as _getChainContractAddress } from 'viem/utils' type ExtractContract = TClient extends { @@ -12,7 +12,8 @@ type ExtractContract = TClient extends { export const getChainContractAddress = < const TClient extends { chain: Chain }, TContracts extends ExtractContract = ExtractContract, - TContract extends keyof TContracts = keyof TContracts, + TContractName extends keyof TContracts = keyof TContracts, + TContract extends TContracts[TContractName] = TContracts[TContractName], >({ blockNumber, client, @@ -20,10 +21,14 @@ export const getChainContractAddress = < }: { blockNumber?: bigint client: TClient - contract: TContract + contract: TContractName }) => _getChainContractAddress({ blockNumber, chain: client.chain, contract: contract as string, - }) as TContracts[TContract]['address'] + }) as TContract extends { address: infer A } + ? A extends Address + ? A + : never + : Address diff --git a/packages/ensjs/src/contracts/legacyDnsRegistrar.ts b/packages/ensjs/src/contracts/legacyDnsRegistrar.ts new file mode 100644 index 00000000..d4f476ed --- /dev/null +++ b/packages/ensjs/src/contracts/legacyDnsRegistrar.ts @@ -0,0 +1,73 @@ +export const legacyDnsRegistrarProveAndClaimSnippet = [ + { + inputs: [ + { + name: 'name', + type: 'bytes', + }, + { + components: [ + { + name: 'rrset', + type: 'bytes', + }, + { + name: 'sig', + type: 'bytes', + }, + ], + name: 'input', + type: 'tuple[]', + }, + { + name: 'proof', + type: 'bytes', + }, + ], + name: 'proveAndClaim', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const + +export const legacyDnsRegistrarProveAndClaimWithResolverSnippet = [ + { + inputs: [ + { + name: 'name', + type: 'bytes', + }, + { + components: [ + { + name: 'rrset', + type: 'bytes', + }, + { + name: 'sig', + type: 'bytes', + }, + ], + name: 'input', + type: 'tuple[]', + }, + { + name: 'proof', + type: 'bytes', + }, + { + name: 'resolver', + type: 'address', + }, + { + name: 'addr', + type: 'address', + }, + ], + name: 'proveAndClaimWithResolver', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/packages/ensjs/src/contracts/legacyDnssecImpl.ts b/packages/ensjs/src/contracts/legacyDnssecImpl.ts new file mode 100644 index 00000000..74ac19ee --- /dev/null +++ b/packages/ensjs/src/contracts/legacyDnssecImpl.ts @@ -0,0 +1,46 @@ +export const legacyDnssecImplRrDataSnippet = [ + { + inputs: [ + { + name: 'dnstype', + type: 'uint16', + }, + { + name: 'name', + type: 'bytes', + }, + ], + name: 'rrdata', + outputs: [ + { + name: '', + type: 'uint32', + }, + { + name: '', + type: 'uint32', + }, + { + name: '', + type: 'bytes20', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +export const legacyDnssecImplAnchorsSnippet = [ + { + inputs: [], + name: 'anchors', + outputs: [ + { + name: '', + type: 'bytes', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const diff --git a/packages/ensjs/src/functions/dns/getDnsImportData.test.ts b/packages/ensjs/src/functions/dns/getDnsImportData.test.ts index 21e5e6d2..a59c26ad 100644 --- a/packages/ensjs/src/functions/dns/getDnsImportData.test.ts +++ b/packages/ensjs/src/functions/dns/getDnsImportData.test.ts @@ -1,35 +1,153 @@ import { SignedSet } from '@ensdomains/dnsprovejs' -import { toBytes } from 'viem' -import { publicClient } from '../../test/addTestContracts.js' +import { toBytes, type Address, type Hex } from 'viem' +import { + publicClient, + publicClientWithLegacyDns, + testClient, + waitForTransaction, + walletClient, + walletClientWithLegacyDns, +} from '../../test/addTestContracts.js' import getDnsImportData, { type RrSetWithSig } from './getDnsImportData.js' +import importDnsName from './importDnsName.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 }) +}) const decodeProofs = (proofs: RrSetWithSig[]) => proofs.map((proof) => SignedSet.fromWire( - toBytes(proof.rrset) as Buffer, - toBytes(proof.sig) as Buffer, + Buffer.from(toBytes(proof.rrset)), + Buffer.from(toBytes(proof.sig)), ), ) jest.setTimeout(10000) jest.retryTimes(2) -it('returns all rrsets', async () => { - const result = await getDnsImportData(publicClient, { - name: 'taytems.xyz', +describe('new', () => { + it('returns all rrsets', async () => { + const result = await getDnsImportData(publicClient, { + name: 'taytems.xyz', + }) + if (result.isLegacy) throw new Error('Expected non-legacy result') + expect(result.data.length).toBeGreaterThan(0) + const decodedProofs = decodeProofs(result.data) + const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') + const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') + const twoLDProofs = decodedProofs.filter( + (x) => x.signature.name === 'taytems.xyz', + ) + const threeLDProofs = decodedProofs.filter( + (x) => x.signature.name === '_ens.taytems.xyz', + ) + expect(rootProofs.length).toBeGreaterThan(0) + expect(tldProofs.length).toBeGreaterThan(0) + expect(twoLDProofs.length).toBeGreaterThan(0) + expect(threeLDProofs.length).toBeGreaterThan(0) + }) +}) + +const wait = async (ms: number) => + // eslint-disable-next-line no-promise-executor-return + new Promise((resolve) => setTimeout(resolve, ms)) + +describe('legacy', () => { + it('returns all rrsets when no proofs are known', async () => { + const result = await getDnsImportData(publicClientWithLegacyDns, { + name: 'taytems.xyz', + }) + if (!result.isLegacy) throw new Error('Expected legacy result') + expect(result.data.rrsets.length).toBeGreaterThan(0) + expect(result.data.proof).toBeInstanceOf(Uint8Array) + const decodedProofs = decodeProofs(result.data.rrsets) + const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') + const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') + const twoLDProofs = decodedProofs.filter( + (x) => x.signature.name === 'taytems.xyz', + ) + const threeLDProofs = decodedProofs.filter( + (x) => x.signature.name === '_ens.taytems.xyz', + ) + expect(rootProofs.length).toBeGreaterThan(0) + expect(tldProofs.length).toBeGreaterThan(0) + expect(twoLDProofs.length).toBeGreaterThan(0) + expect(threeLDProofs.length).toBeGreaterThan(0) + }) + it('returns rrsets up to the first unknown proof', async () => { + const tx = await importDnsName(walletClientWithLegacyDns, { + name: 'taytems.xyz', + account: accounts[0], + dnsImportData: await getDnsImportData(publicClientWithLegacyDns, { + name: 'taytems.xyz', + }), + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + await wait(5000) + + const result = await getDnsImportData(publicClientWithLegacyDns, { + name: 'lenster.xyz', + }) + if (!result.isLegacy) throw new Error('Expected legacy result') + const decodedProofs = decodeProofs(result.data.rrsets) + const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') + const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') + const twoLDProofs = decodedProofs.filter( + (x) => x.signature.name === 'lenster.xyz', + ) + const threeLDProofs = decodedProofs.filter( + (x) => x.signature.name === '_ens.lenster.xyz', + ) + expect(rootProofs).toHaveLength(0) + expect(tldProofs).toHaveLength(0) + expect(twoLDProofs.length).toBeGreaterThan(0) + expect(threeLDProofs.length).toBeGreaterThan(0) + }) + it('returns empty rrsets for all known proofs when the last proof is known', async () => { + const tx = await importDnsName(walletClientWithLegacyDns, { + name: 'taytems.xyz', + account: accounts[0], + dnsImportData: await getDnsImportData(publicClientWithLegacyDns, { + name: 'taytems.xyz', + }), + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + await wait(5000) + + const result = await getDnsImportData(publicClientWithLegacyDns, { + name: 'taytems.xyz', + }) + if (!result.isLegacy) throw new Error('Expected legacy result') + const decodedProofs = decodeProofs(result.data.rrsets) + const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') + const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') + const twoLDProofs = decodedProofs.filter( + (x) => x.signature.name === 'taytems.xyz', + ) + const threeLDProofs = decodedProofs.filter( + (x) => x.signature.name === '_ens.taytems.xyz', + ) + expect(rootProofs).toHaveLength(0) + expect(tldProofs).toHaveLength(0) + expect(twoLDProofs).toHaveLength(0) + expect(threeLDProofs).toHaveLength(0) }) - expect(result.length).toBeGreaterThan(0) - const decodedProofs = decodeProofs(result) - const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') - const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') - const twoLDProofs = decodedProofs.filter( - (x) => x.signature.name === 'taytems.xyz', - ) - const threeLDProofs = decodedProofs.filter( - (x) => x.signature.name === '_ens.taytems.xyz', - ) - expect(rootProofs.length).toBeGreaterThan(0) - expect(tldProofs.length).toBeGreaterThan(0) - expect(twoLDProofs.length).toBeGreaterThan(0) - expect(threeLDProofs.length).toBeGreaterThan(0) }) diff --git a/packages/ensjs/src/functions/dns/getDnsImportData.ts b/packages/ensjs/src/functions/dns/getDnsImportData.ts index 3e1bc3f2..f64e426d 100644 --- a/packages/ensjs/src/functions/dns/getDnsImportData.ts +++ b/packages/ensjs/src/functions/dns/getDnsImportData.ts @@ -1,11 +1,24 @@ import { SignedSet, type ProvableAnswer } from '@ensdomains/dnsprovejs' import type * as packet from 'dns-packet' -import { toHex, type Hex } from 'viem' +import { toType } from 'dns-packet/types.js' +import { + keccak256, + toBytes, + toHex, + type Client, + type Hex, + type Transport, +} from 'viem' import { readContract } from 'viem/actions' -import type { ClientWithEns } from '../../contracts/consts.js' +import type { ChainWithEns } from '../../contracts/consts.js' import { dnssecImplVerifyRrSetSnippet } from '../../contracts/dnssecImpl.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { + legacyDnssecImplAnchorsSnippet, + legacyDnssecImplRrDataSnippet, +} from '../../contracts/legacyDnssecImpl.js' import { DnsNewerRecordTypeAvailableError } from '../../index.js' +import { packetToBytes } from '../../utils/hexEncodedName.js' import type { Endpoint } from './types.js' export type GetDnsImportDataParameters = { @@ -20,7 +33,18 @@ export type RrSetWithSig = { sig: Hex } -export type GetDnsImportDataReturnType = RrSetWithSig[] +export type GetDnsImportDataReturnType = + | { + isLegacy: false + data: RrSetWithSig[] + } + | { + isLegacy: true + data: { + rrsets: RrSetWithSig[] + proof: Uint8Array + } + } // Compares two serial numbers using RFC1982 serial number math. const serialNumberGt = (i1: number, i2: number): boolean => @@ -55,7 +79,7 @@ const encodeProofs = ( * }) */ const getDnsImportData = async ( - client: ClientWithEns, + client: Client, { name, endpoint = 'https://cloudflare-dns.com/dns-query', @@ -73,6 +97,76 @@ const getDnsImportData = async ( result.proofs as SignedSet[] ).concat([result.answer]) + let isLegacy = false + + try { + getChainContractAddress({ + client, + contract: 'ensDnssecImpl', + }) + } catch { + isLegacy = true + } + + if (isLegacy) { + const ensLegacyDnssecImplAddress = getChainContractAddress({ + client, + contract: 'ensLegacyDnssecImpl', + }) + + for (let i = allProofs.length - 1; i >= 0; i -= 1) { + const proof = allProofs[i] + const hexEncodedName = toHex(packetToBytes(proof.signature.name)) + const type = toType(proof.signature.data.typeCovered) + // eslint-disable-next-line no-await-in-loop + const [inception, expiration, hash] = await readContract(client, { + abi: legacyDnssecImplRrDataSnippet, + address: ensLegacyDnssecImplAddress, + functionName: 'rrdata', + args: [type, hexEncodedName], + }) + if (serialNumberGt(inception, proof.signature.data.inception)) + throw new DnsNewerRecordTypeAvailableError({ + typeCovered: proof.signature.data.typeCovered, + signatureName: proof.signature.name, + onchainInception: inception, + dnsInception: proof.signature.data.inception, + }) + const expired = serialNumberGt(Date.now() / 1000, expiration) + const proofHash = keccak256(proof.toWire(false)).slice(0, 42) + const isKnownProof = hash === proofHash && !expired + if (isKnownProof) { + if (i === allProofs.length - 1) { + return { + isLegacy: true, + data: { rrsets: [], proof: proof.toWire(false) }, + } + } + return { + isLegacy: true, + data: { + rrsets: encodeProofs(allProofs.slice(i + 1, allProofs.length)), + proof: proof.toWire(false), + }, + } + } + } + + return { + isLegacy: true, + data: { + rrsets: encodeProofs(allProofs), + proof: toBytes( + await readContract(client, { + abi: legacyDnssecImplAnchorsSnippet, + address: ensLegacyDnssecImplAddress, + functionName: 'anchors', + }), + ), + }, + } + } + const rrsets = encodeProofs(allProofs) const [onchainRrData, inception] = await readContract(client, { @@ -97,7 +191,7 @@ const getDnsImportData = async ( if (toHex(lastProof.toWire(false)) !== onchainRrData) throw new Error('Mismatched proof data') - return rrsets + return { isLegacy: false, data: rrsets } } export default getDnsImportData diff --git a/packages/ensjs/src/functions/dns/importDnsName.test.ts b/packages/ensjs/src/functions/dns/importDnsName.test.ts index ab0d661e..1a5db4a6 100644 --- a/packages/ensjs/src/functions/dns/importDnsName.test.ts +++ b/packages/ensjs/src/functions/dns/importDnsName.test.ts @@ -1,15 +1,20 @@ -import { parseEther, type Address, type Hex } from 'viem' +import { labelhash, parseAbi, parseEther, type Address, type Hex } from 'viem' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' import { getVersion } from '../../errors/error-utils.js' import { + deploymentAddresses, publicClient, + publicClientWithLegacyDns, testClient, waitForTransaction, walletClient, + walletClientWithLegacyDns, } from '../../test/addTestContracts.js' import getOwner from '../public/getOwner.js' import getResolver from '../public/getResolver.js' -import getDnsImportData from './getDnsImportData.js' +import getDnsImportData, { + type GetDnsImportDataReturnType, +} from './getDnsImportData.js' import importDnsName from './importDnsName.js' const name = 'taytems.xyz' @@ -33,73 +38,195 @@ afterEach(async () => { jest.setTimeout(10000) jest.retryTimes(2) -it('should import a DNS name with no address', async () => { - const tx = await importDnsName(walletClient, { - name, - dnsImportData: await getDnsImportData(publicClient, { name }), - account: accounts[0], +describe('legacy', () => { + let dnsImportData: GetDnsImportDataReturnType + beforeAll(async () => { + dnsImportData = await getDnsImportData(publicClientWithLegacyDns, { name }) }) - expect(tx).toBeTruthy() - const receipt = await waitForTransaction(tx) - expect(receipt.status).toBe('success') + it('should import a DNS name with no address', async () => { + const tx = await importDnsName(walletClientWithLegacyDns, { + name, + dnsImportData, + account: accounts[0], + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') - const owner = await getOwner(publicClient, { name }) - expect(owner!.owner).toBe(address) -}) -it('should import a DNS name with an address, using default resolver', async () => { - await testClient.impersonateAccount({ address }) - await testClient.setBalance({ - address, - value: parseEther('1'), + const owner = await getOwner(publicClientWithLegacyDns, { name }) + expect(owner!.owner).toBe(address) }) - - const tx = await importDnsName(walletClient, { - name, - address, - dnsImportData: await getDnsImportData(publicClient, { name }), - account: address, + it('should import a DNS name with an address, using default resolver', async () => { + await testClient.impersonateAccount({ address }) + await testClient.setBalance({ + address, + value: parseEther('1'), + }) + + const tx = await importDnsName(walletClientWithLegacyDns, { + name, + address, + dnsImportData, + account: address, + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const owner = await getOwner(publicClientWithLegacyDns, { name }) + expect(owner!.owner).toBe(address) + const resolver = await getResolver(publicClientWithLegacyDns, { name }) + expect(resolver).toBe( + getChainContractAddress({ + client: publicClientWithLegacyDns, + contract: 'ensPublicResolver', + }), + ) + }) + it('should import a DNS name with an address, using a custom resolver', async () => { + await testClient.impersonateAccount({ address }) + await testClient.setBalance({ + address, + value: parseEther('1'), + }) + + const legacyResolverAddress = JSON.parse( + process.env.DEPLOYMENT_ADDRESSES!, + ).LegacyPublicResolver + + const tx = await importDnsName(walletClientWithLegacyDns, { + name, + address, + dnsImportData, + account: address, + resolverAddress: legacyResolverAddress, + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const owner = await getOwner(publicClientWithLegacyDns, { name }) + expect(owner!.owner).toBe(address) + const resolver = await getResolver(publicClientWithLegacyDns, { name }) + expect(resolver).toBe(legacyResolverAddress) }) - expect(tx).toBeTruthy() - const receipt = await waitForTransaction(tx) - expect(receipt.status).toBe('success') - - const owner = await getOwner(publicClient, { name }) - expect(owner!.owner).toBe(address) - const resolver = await getResolver(publicClient, { name }) - expect(resolver).toBe( - getChainContractAddress({ - client: publicClient, - contract: 'ensPublicResolver', - }), - ) }) -it('should import a DNS name with an address, using a custom resolver', async () => { - await testClient.impersonateAccount({ address }) - await testClient.setBalance({ - address, - value: parseEther('1'), + +describe('new', () => { + let dnsImportData: GetDnsImportDataReturnType + beforeAll(async () => { + dnsImportData = await getDnsImportData(publicClient, { name }) + }) + beforeEach(async () => { + const tx = await walletClient.writeContract({ + account: accounts[1], + address: deploymentAddresses.Root, + abi: parseAbi([ + 'function setSubnodeOwner(bytes32 label, address owner) external', + ] as const), + functionName: 'setSubnodeOwner', + args: [labelhash('xyz'), deploymentAddresses.DNSRegistrar], + }) + await waitForTransaction(tx) + + await testClient.impersonateAccount({ address }) + await testClient.setBalance({ + address, + value: parseEther('1'), + }) + const approveTx = await walletClient.writeContract({ + account: address, + address: deploymentAddresses.PublicResolver, + abi: parseAbi([ + 'function setApprovalForAll(address operator, bool approved) external', + ] as const), + functionName: 'setApprovalForAll', + args: [deploymentAddresses.DNSRegistrar, true], + }) + await waitForTransaction(approveTx) + await testClient.stopImpersonatingAccount({ address }) }) - const legacyResolverAddress = JSON.parse( - process.env.DEPLOYMENT_ADDRESSES!, - ).LegacyPublicResolver + it('should import a DNS name with no address', async () => { + const tx = await importDnsName(walletClient, { + name, + dnsImportData, + account: accounts[0], + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') - const tx = await importDnsName(walletClient, { - name, - address, - dnsImportData: await getDnsImportData(publicClient, { name }), - account: address, - resolverAddress: legacyResolverAddress, + const owner = await getOwner(publicClient, { name }) + expect(owner!.owner).toBe(address) + }) + + it('should import a DNS name with an address, using default resolver', async () => { + await testClient.impersonateAccount({ address }) + await testClient.setBalance({ + address, + value: parseEther('1'), + }) + + const tx = await importDnsName(walletClient, { + name, + address, + dnsImportData, + account: address, + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const owner = await getOwner(publicClient, { name }) + expect(owner!.owner).toBe(address) + const resolver = await getResolver(publicClient, { name }) + expect(resolver).toBe( + getChainContractAddress({ + client: publicClient, + contract: 'ensPublicResolver', + }), + ) + }) + it('should import a DNS name with an address, using a custom resolver', async () => { + await testClient.impersonateAccount({ address }) + await testClient.setBalance({ + address, + value: parseEther('1'), + }) + + const resolverAddress = deploymentAddresses.PublicResolver + + const approveTx = await walletClient.writeContract({ + account: address, + address: resolverAddress, + abi: parseAbi([ + 'function setApprovalForAll(address operator, bool approved) external', + ] as const), + functionName: 'setApprovalForAll', + args: [deploymentAddresses.DNSRegistrar, true], + }) + const approveReceipt = await waitForTransaction(approveTx) + expect(approveReceipt.status).toBe('success') + + const tx = await importDnsName(walletClient, { + name, + address, + dnsImportData, + account: address, + resolverAddress, + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const owner = await getOwner(publicClient, { name }) + expect(owner!.owner).toBe(address) + const resolver = await getResolver(publicClient, { name }) + expect(resolver).toBe(resolverAddress) }) - expect(tx).toBeTruthy() - const receipt = await waitForTransaction(tx) - expect(receipt.status).toBe('success') - - const owner = await getOwner(publicClient, { name }) - expect(owner!.owner).toBe(address) - const resolver = await getResolver(publicClient, { name }) - expect(resolver).toBe(legacyResolverAddress) }) + it('should throw error if resolver is specified when claiming without an address', async () => { await expect( importDnsName(walletClient, { diff --git a/packages/ensjs/src/functions/dns/importDnsName.ts b/packages/ensjs/src/functions/dns/importDnsName.ts index dff1ab1c..8d595984 100644 --- a/packages/ensjs/src/functions/dns/importDnsName.ts +++ b/packages/ensjs/src/functions/dns/importDnsName.ts @@ -6,6 +6,7 @@ import { type Hash, type SendTransactionParameters, type Transport, + type WalletClient, } from 'viem' import type { ChainWithEns, WalletWithEns } from '../../contracts/consts.js' import { @@ -13,6 +14,10 @@ import { dnsRegistrarProveAndClaimWithResolverSnippet, } from '../../contracts/dnsRegistrar.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { + legacyDnsRegistrarProveAndClaimSnippet, + legacyDnsRegistrarProveAndClaimWithResolverSnippet, +} from '../../contracts/legacyDnsRegistrar.js' import { AdditionalParameterSpecifiedError } from '../../errors/general.js' import type { Prettify, @@ -63,7 +68,7 @@ export const makeFunctionData = < TChain extends ChainWithEns, TAccount extends Account | undefined, >( - wallet: WalletWithEns, + wallet: WalletClient, { name, dnsImportData, @@ -72,10 +77,15 @@ export const makeFunctionData = < }: ImportDnsNameDataParameters, ): ImportDnsNameDataReturnType => { const hexEncodedName = toHex(packetToBytes(name)) - const dnsRegistrarAddress = getChainContractAddress({ - client: wallet, - contract: 'ensDnsRegistrar', - }) + const dnsRegistrarAddress = dnsImportData.isLegacy + ? getChainContractAddress({ + client: wallet, + contract: 'ensLegacyDnsRegistrar', + }) + : getChainContractAddress({ + client: wallet, + contract: 'ensDnsRegistrar', + }) if (!address) { if (resolverAddress) @@ -85,12 +95,25 @@ export const makeFunctionData = < details: 'resolverAddress cannot be specified when claiming without an address', }) + if (dnsImportData.isLegacy) + return { + to: dnsRegistrarAddress, + data: encodeFunctionData({ + abi: legacyDnsRegistrarProveAndClaimSnippet, + functionName: 'proveAndClaim', + args: [ + hexEncodedName, + dnsImportData.data.rrsets, + toHex(dnsImportData.data.proof), + ], + }), + } return { to: dnsRegistrarAddress, data: encodeFunctionData({ abi: dnsRegistrarProveAndClaimSnippet, functionName: 'proveAndClaim', - args: [hexEncodedName, dnsImportData], + args: [hexEncodedName, dnsImportData.data], }), } } @@ -99,12 +122,28 @@ export const makeFunctionData = < resolverAddress || getChainContractAddress({ client: wallet, contract: 'ensPublicResolver' }) + if (dnsImportData.isLegacy) + return { + to: dnsRegistrarAddress, + data: encodeFunctionData({ + abi: legacyDnsRegistrarProveAndClaimWithResolverSnippet, + functionName: 'proveAndClaimWithResolver', + args: [ + hexEncodedName, + dnsImportData.data.rrsets, + toHex(dnsImportData.data.proof), + resolverAddress_, + address, + ], + }), + } + return { to: dnsRegistrarAddress, data: encodeFunctionData({ abi: dnsRegistrarProveAndClaimWithResolverSnippet, functionName: 'proveAndClaimWithResolver', - args: [hexEncodedName, dnsImportData, resolverAddress_, address], + args: [hexEncodedName, dnsImportData.data, resolverAddress_, address], }), } } diff --git a/packages/ensjs/src/test/addTestContracts.ts b/packages/ensjs/src/test/addTestContracts.ts index 8993eddb..5cd1deae 100644 --- a/packages/ensjs/src/test/addTestContracts.ts +++ b/packages/ensjs/src/test/addTestContracts.ts @@ -35,6 +35,7 @@ type ContractName = | 'DNSSECImpl' | 'LegacyDNSRegistrar' | 'LegacyDNSSECImpl' + | 'Root' export const deploymentAddresses = JSON.parse( process.env.DEPLOYMENT_ADDRESSES!, @@ -91,6 +92,50 @@ export const localhost = { }, } as const +export const localhostWithLegacyDns = { + ..._localhost, + contracts: { + ensRegistry: { + address: deploymentAddresses.ENSRegistry, + }, + ensUniversalResolver: { + address: deploymentAddresses.UniversalResolver, + }, + multicall3: { + address: deploymentAddresses.Multicall, + }, + ensBaseRegistrarImplementation: { + address: deploymentAddresses.BaseRegistrarImplementation, + }, + ensLegacyDnsRegistrar: { + address: deploymentAddresses.LegacyDNSRegistrar, + }, + ensEthRegistrarController: { + address: deploymentAddresses.ETHRegistrarController, + }, + ensNameWrapper: { + address: deploymentAddresses.NameWrapper, + }, + ensPublicResolver: { + address: deploymentAddresses.PublicResolver, + }, + ensReverseRegistrar: { + address: deploymentAddresses.ReverseRegistrar, + }, + ensBulkRenewal: { + address: deploymentAddresses.StaticBulkRenewal, + }, + ensLegacyDnssecImpl: { + address: deploymentAddresses.LegacyDNSSECImpl, + }, + }, + subgraphs: { + ens: { + url: 'http://localhost:8000/subgraphs/name/graphprotocol/ens', + }, + }, +} as const + const transport = http('http://localhost:8545') export const publicClient: PublicClient = @@ -99,6 +144,14 @@ export const publicClient: PublicClient = transport, }) +export const publicClientWithLegacyDns: PublicClient< + typeof transport, + typeof localhostWithLegacyDns +> = createPublicClient({ + chain: localhostWithLegacyDns, + transport, +}) + export const testClient: TestClient< 'anvil', typeof transport, @@ -118,6 +171,15 @@ export const walletClient: WalletClient< transport, }) +export const walletClientWithLegacyDns: WalletClient< + typeof transport, + typeof localhostWithLegacyDns, + Account +> = createWalletClient({ + chain: localhostWithLegacyDns, + transport, +}) + export const waitForTransaction = async (hash: Hash) => new Promise((resolveFn, reject) => { publicClient