diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bb80ce4c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +name: Prerelease + +on: + release: + types: [published] +jobs: + release: + name: Release + permissions: + id-token: write + contents: write + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + + - uses: pnpm/action-setup@v4 + with: + version: 9.4.0 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - run: pnpm install --frozen-lockfile + + - name: Set up git + run: | + git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'github-actions[bot]' + + - name: Bump version to ${{ github.event.release.tag_name }} + run: | + pnpm -F @ensdomains/ensjs ver ${{ github.event.release.tag_name }} + git add . + git commit -m "${{ github.event.release.tag_name }}" + + - name: Publish + env: + NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true + run: | + pnpm config set //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} + pnpm -F @ensdomains/ensjs publish --tag next --no-git-checks + + - name: Push changes + run: git push + env: + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/package.json b/package.json index 23c342c3..52198bd5 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "publish:local:ens-test-env": "yalc publish packages/ens-test-env --push --up", "publish:local:ensjs": "yalc publish packages/ensjs --push --up", "chgset:version": "changeset version && pnpm install", + "chgset:version:prerelease": "changeset pre enter next && pnpm chgset:version", "chgset:run": "changeset", "release": "pnpm publish -r --access public && changeset tag", "chgset": "pnpm chgset:run && pnpm chgset:version" diff --git a/packages/ensjs/package.json b/packages/ensjs/package.json index a119e637..eb91c48d 100644 --- a/packages/ensjs/package.json +++ b/packages/ensjs/package.json @@ -1,6 +1,6 @@ { "name": "@ensdomains/ensjs", - "version": "4.0.2", + "version": "4.0.3-alpha.12", "description": "ENS javascript library for contract interaction", "type": "module", "main": "./dist/cjs/index.js", diff --git a/packages/ensjs/src/contracts/consts.ts b/packages/ensjs/src/contracts/consts.ts index af67f61f..c9837de2 100644 --- a/packages/ensjs/src/contracts/consts.ts +++ b/packages/ensjs/src/contracts/consts.ts @@ -18,6 +18,8 @@ export const supportedContracts = [ 'ensRegistry', 'ensReverseRegistrar', 'ensUniversalResolver', + 'legacyEthRegistrarController', + 'legacyPublicResolver', ] as const export type SupportedChain = (typeof supportedChains)[number] @@ -55,6 +57,12 @@ export const addresses = { ensUniversalResolver: { address: '0xce01f8eee7E479C928F8919abD53E553a36CeF67', }, + legacyEthRegistrarController: { + address: '0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5', + }, + legacyPublicResolver: { + address: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41', + }, }, 5: { ensBaseRegistrarImplementation: { @@ -87,6 +95,12 @@ export const addresses = { ensUniversalResolver: { address: '0x898A1182F3C2BBBF0b16b4DfEf63E9c3e9eB4821', }, + legacyEthRegistrarController: { + address: '0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5', + }, + legacyPublicResolver: { + address: '0xDaaF96c344f63131acadD0Ea35170E7892d3dfBA', + }, }, 17000: { ensBaseRegistrarImplementation: { @@ -119,6 +133,12 @@ export const addresses = { ensUniversalResolver: { address: '0xa6ac935d4971e3cd133b950ae053becd16fe7f3b', }, + legacyEthRegistrarController: { + address: '0xf13fC748601fDc5afA255e9D9166EB43f603a903', + }, + legacyPublicResolver: { + address: '0xc5e43b622b5e6C379a984E9BdB34E9A545564fA5', + }, }, 11155111: { ensBaseRegistrarImplementation: { @@ -151,6 +171,12 @@ export const addresses = { ensUniversalResolver: { address: '0xc8af999e38273d658be1b921b88a9ddf005769cc', }, + legacyEthRegistrarController: { + address: '0x7e02892cfc2Bfd53a75275451d73cF620e793fc0', + }, + legacyPublicResolver: { + address: '0x0CeEC524b2807841739D3B5E161F5bf1430FFA48', + }, }, } as const satisfies Record< SupportedChain, @@ -195,6 +221,8 @@ type EnsChainContracts = { ensReverseRegistrar: ChainContract ensBulkRenewal: ChainContract ensDnssecImpl: ChainContract + legacyEthRegistrarController: ChainContract + legacyPublicResolver: ChainContract } type BaseChainContracts = { diff --git a/packages/ensjs/src/contracts/ethRegistrarController.ts b/packages/ensjs/src/contracts/ethRegistrarController.ts index 41349ac0..389a4071 100644 --- a/packages/ensjs/src/contracts/ethRegistrarController.ts +++ b/packages/ensjs/src/contracts/ethRegistrarController.ts @@ -206,3 +206,44 @@ export const ethRegistrarControllerRenewSnippet = [ type: 'function', }, ] as const + +export const ethRegistrarControllerNameRegisteredEventSnippet = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + name: 'name', + type: 'string', + }, + { + indexed: true, + name: 'label', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: false, + name: 'baseCost', + type: 'uint256', + }, + { + indexed: false, + name: 'premium', + type: 'uint256', + }, + { + indexed: false, + name: 'expires', + type: 'uint256', + }, + ], + name: 'NameRegistered', + type: 'event', + }, +] as const diff --git a/packages/ensjs/src/contracts/index.ts b/packages/ensjs/src/contracts/index.ts index b998202a..0b64bb81 100644 --- a/packages/ensjs/src/contracts/index.ts +++ b/packages/ensjs/src/contracts/index.ts @@ -40,8 +40,23 @@ export { ethRegistrarControllerRegisterSnippet, ethRegistrarControllerRenewSnippet, ethRegistrarControllerRentPriceSnippet, + ethRegistrarControllerNameRegisteredEventSnippet, } from './ethRegistrarController.js' export { getChainContractAddress } from './getChainContractAddress.js' +export { + legacyEthRegistrarControllerAvailableSnippet, + legacyEthRegistrarControllerCommitSnippet, + legacyEthRegistrarControllerCommitmentsSnippet, + legacyEthRegistrarControllerMakeCommitmentSnippet, + legacyEthRegistrarControllerMakeCommitmentWithConfigSnippet, + legacyEthRegistrarControllerRegisterSnippet, + legacyEthRegistrarControllerRegisterWithConfigSnippet, + legacyEthRegistrarControllerRenewSnippet, + legacyEthRegistrarControllerRentPriceSnippet, + legacyEthRegistrarControllerSupportsInterfaceSnippet, + legacyEthRegistrarControllerTransferOwnershipSnippet, + legacyEthRegistrarControllerNameRegisteredEventSnippet, +} from './legacyEthRegistrarController.js' export { multicallGetCurrentBlockTimestampSnippet, multicallTryAggregateSnippet, diff --git a/packages/ensjs/src/contracts/legacyEthRegistrarController.ts b/packages/ensjs/src/contracts/legacyEthRegistrarController.ts new file mode 100644 index 00000000..c92ed99a --- /dev/null +++ b/packages/ensjs/src/contracts/legacyEthRegistrarController.ts @@ -0,0 +1,194 @@ +export const legacyEthRegistrarControllerAvailableSnippet = [ + { + constant: true, + inputs: [{ internalType: 'string', name: 'name', type: 'string' }], + name: 'available', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerCommitSnippet = [ + { + constant: false, + inputs: [{ internalType: 'bytes32', name: 'commitment', type: 'bytes32' }], + name: 'commit', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerCommitmentsSnippet = [ + { + constant: true, + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'commitments', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerMakeCommitmentSnippet = [ + { + constant: true, + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'bytes32', name: 'secret', type: 'bytes32' }, + ], + name: 'makeCommitment', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + payable: false, + stateMutability: 'pure', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerMakeCommitmentWithConfigSnippet = [ + { + constant: true, + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'bytes32', name: 'secret', type: 'bytes32' }, + { internalType: 'address', name: 'resolver', type: 'address' }, + { internalType: 'address', name: 'addr', type: 'address' }, + ], + name: 'makeCommitmentWithConfig', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + payable: false, + stateMutability: 'pure', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerRegisterSnippet = [ + { + constant: false, + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + { internalType: 'bytes32', name: 'secret', type: 'bytes32' }, + ], + name: 'register', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerRegisterWithConfigSnippet = [ + { + constant: false, + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + { internalType: 'bytes32', name: 'secret', type: 'bytes32' }, + { internalType: 'address', name: 'resolver', type: 'address' }, + { internalType: 'address', name: 'addr', type: 'address' }, + ], + name: 'registerWithConfig', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerRenewSnippet = [ + { + constant: false, + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + ], + name: 'renew', + outputs: [], + payable: true, + stateMutability: 'payable', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerRentPriceSnippet = [ + { + constant: true, + inputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'uint256', name: 'duration', type: 'uint256' }, + ], + name: 'rentPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerSupportsInterfaceSnippet = [ + { + constant: true, + inputs: [{ internalType: 'bytes4', name: 'interfaceID', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + payable: false, + stateMutability: 'pure', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerTransferOwnershipSnippet = [ + { + constant: false, + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +] as const + +export const legacyEthRegistrarControllerNameRegisteredEventSnippet = [ + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'string', name: 'name', type: 'string' }, + { + indexed: true, + internalType: 'bytes32', + name: 'label', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'cost', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'expires', + type: 'uint256', + }, + ], + name: 'NameRegistered', + type: 'event', + }, +] as const diff --git a/packages/ensjs/src/errors/register.ts b/packages/ensjs/src/errors/register.ts new file mode 100644 index 00000000..69d84ec0 --- /dev/null +++ b/packages/ensjs/src/errors/register.ts @@ -0,0 +1,22 @@ +import type { Address } from 'viem' +import { BaseError } from './base.js' +import { EMPTY_ADDRESS } from '../utils/consts.js' + +export class LegacyRegistrationInvalidConfigError extends BaseError { + override name = 'LegacyRegistrationInvalidConfigError' + + constructor({ + resolverAddress, + address, + }: { + resolverAddress?: Address + address?: Address + }) { + super(`Resolver address is required when setting an address`, { + metaMessages: [ + `- resolverAddress: ${resolverAddress || EMPTY_ADDRESS}`, + `- addr: ${address || EMPTY_ADDRESS}`, + ], + }) + } +} diff --git a/packages/ensjs/src/errors/version.ts b/packages/ensjs/src/errors/version.ts index bc2da28e..d3221612 100644 --- a/packages/ensjs/src/errors/version.ts +++ b/packages/ensjs/src/errors/version.ts @@ -1 +1 @@ -export const version = 'v4.0.2-alpha.5' +export const version = 'v4.0.3-alpha.12' diff --git a/packages/ensjs/src/functions/wallet/legacyCommitName.test.ts b/packages/ensjs/src/functions/wallet/legacyCommitName.test.ts new file mode 100644 index 00000000..7796346d --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyCommitName.test.ts @@ -0,0 +1,59 @@ +import { type Address, type Hex } from 'viem' +import { afterEach, beforeAll, beforeEach, expect, it } from 'vitest' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { + publicClient, + testClient, + waitForTransaction, + walletClient, +} from '../../test/addTestContracts.js' +import { + makeLegacyCommitment, + type LegacyRegistrationParameters, +} from '../../utils/legacyRegisterHelpers.js' +import legacyCommitName from './legacyCommitName.js' +import { legacyEthRegistrarControllerCommitmentsSnippet } from '../../contracts/legacyEthRegistrarController.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 secret = `0x${'a'.repeat(64)}` as Hex + +it('should return a commit transaction and succeed', async () => { + const params: LegacyRegistrationParameters = { + name: 'wrapped-with-subnames.eth', + duration: 31536000, + owner: accounts[1], + secret, + } + const tx = await legacyCommitName(walletClient, { + ...params, + account: accounts[1], + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const commitment = await publicClient.readContract({ + abi: legacyEthRegistrarControllerCommitmentsSnippet, + functionName: 'commitments', + address: getChainContractAddress({ + client: publicClient, + contract: 'legacyEthRegistrarController', + }), + args: [makeLegacyCommitment(params)], + }) + expect(commitment).toBeTruthy() +}) diff --git a/packages/ensjs/src/functions/wallet/legacyCommitName.ts b/packages/ensjs/src/functions/wallet/legacyCommitName.ts new file mode 100644 index 00000000..85fb2a09 --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyCommitName.ts @@ -0,0 +1,127 @@ +import { + encodeFunctionData, + type Account, + type Hash, + type SendTransactionParameters, + type Transport, +} from 'viem' +import { sendTransaction } from 'viem/actions' +import type { ChainWithEns, ClientWithAccount } from '../../contracts/consts.js' +import { legacyEthRegistrarControllerCommitSnippet } from '../../contracts/legacyEthRegistrarController.js' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { UnsupportedNameTypeError } from '../../errors/general.js' +import type { + Prettify, + SimpleTransactionRequest, + WriteTransactionParameters, +} from '../../types.js' +import { getNameType } from '../../utils/getNameType.js' +import { + makeLegacyCommitment, + type LegacyRegistrationParameters, +} from '../../utils/legacyRegisterHelpers.js' +import { EMPTY_ADDRESS } from '../../utils/consts.js' + +export type LegacyCommitNameDataParameters = LegacyRegistrationParameters + +export type LegacyCommitNameDataReturnType = SimpleTransactionRequest + +export type LegacyCommitNameParameters< + TChain extends ChainWithEns, + TAccount extends Account | undefined, + TChainOverride extends ChainWithEns | undefined, +> = Prettify< + LegacyCommitNameDataParameters & + WriteTransactionParameters +> + +export type LegacyCommitNameReturnType = Hash + +export const makeFunctionData = < + TChain extends ChainWithEns, + TAccount extends Account | undefined, +>( + wallet: ClientWithAccount, + args: LegacyCommitNameDataParameters, +): LegacyCommitNameDataReturnType => { + const nameType = getNameType(args.name) + if (nameType !== 'eth-2ld') + throw new UnsupportedNameTypeError({ + nameType, + supportedNameTypes: ['eth-2ld'], + details: 'Only 2ld-eth name registration is supported', + }) + + return { + to: getChainContractAddress({ + client: wallet, + contract: 'legacyEthRegistrarController', + }), + data: encodeFunctionData({ + abi: legacyEthRegistrarControllerCommitSnippet, + functionName: 'commit', + args: [makeLegacyCommitment(args)], + }), + } +} + +/** + * Commits a name to be registered + * @param wallet - {@link ClientWithAccount} + * @param parameters - {@link LegacyCommitNameParameters} + * @returns Transaction hash. {@link LegacyCommitNameReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { addEnsContracts } from '@ensdomains/ensjs' + * import { commitName } from '@ensdomains/ensjs/wallet' + * import { randomSecret } from '@ensdomains/ensjs/utils' + * + * const wallet = createWalletClient({ + * chain: addEnsContracts(mainnet), + * transport: custom(window.ethereum), + * }) + * const secret = randomSecret() + * const hash = await commitName(wallet, { + * name: 'example.eth', + * owner: '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7', + * duration: 31536000, // 1 year + * secret, + * }) + * // 0x... + */ +async function legacyCommitName< + TChain extends ChainWithEns, + TAccount extends Account | undefined, + TChainOverride extends ChainWithEns | undefined = ChainWithEns, +>( + wallet: ClientWithAccount, + { + name, + owner, + duration, + secret, + resolverAddress = EMPTY_ADDRESS, + address = EMPTY_ADDRESS, + ...txArgs + }: LegacyCommitNameParameters, +): Promise { + const data = makeFunctionData(wallet, { + name, + owner, + duration, + secret, + resolverAddress, + address, + }) + const writeArgs = { + ...data, + ...txArgs, + } as SendTransactionParameters + return sendTransaction(wallet, writeArgs) +} + +legacyCommitName.makeFunctionData = makeFunctionData + +export default legacyCommitName diff --git a/packages/ensjs/src/functions/wallet/legacyRegisterName.test.ts b/packages/ensjs/src/functions/wallet/legacyRegisterName.test.ts new file mode 100644 index 00000000..0bade3db --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyRegisterName.test.ts @@ -0,0 +1,113 @@ +import { type Address, type Hex } from 'viem' +import { afterEach, beforeAll, beforeEach, expect, it } from 'vitest' +import { + publicClient, + testClient, + waitForTransaction, + walletClient, +} from '../../test/addTestContracts.js' +import type { RegistrationParameters } from '../../utils/registerHelpers.js' +import getPrice from '../public/getPrice.js' +import legacyCommitName from './legacyCommitName.js' +import legacyRegisterName from './legacyRegisterName.js' +import getOwner from '../public/getOwner.js' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import type { LegacyRegistrationParameters } from '../../utils/legacyRegisterHelpers.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 secret = `0x${'a'.repeat(64)}` as Hex + +it('should return a registration without resolverAddress or address transaction and succeed', async () => { + const params: RegistrationParameters = { + name: 'cool-swag.eth', + duration: 31536000, + owner: accounts[1], + secret, + } + const commitTx = await legacyCommitName(walletClient, { + ...params, + account: accounts[1], + }) + expect(commitTx).toBeTruthy() + const commitReceipt = await waitForTransaction(commitTx) + + expect(commitReceipt.status).toBe('success') + + await testClient.increaseTime({ seconds: 61 }) + await testClient.mine({ blocks: 1 }) + + const price = await getPrice(publicClient, { + nameOrNames: params.name, + duration: params.duration, + }) + const total = price!.base + price!.premium + + const tx = await legacyRegisterName(walletClient, { + ...params, + account: accounts[1], + value: total, + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const owner = await getOwner(publicClient, { name: params.name }) + expect(owner?.registrant).toBe(accounts[1]) +}) + +it('should return a registration transaction and succeed', async () => { + const params: LegacyRegistrationParameters = { + name: 'cool-swaggy.eth', + duration: 31536000, + owner: accounts[1], + secret, + resolverAddress: getChainContractAddress({ + client: walletClient, + contract: 'legacyPublicResolver', + }), + address: accounts[2], + } + const commitTx = await legacyCommitName(walletClient, { + ...params, + account: accounts[1], + }) + expect(commitTx).toBeTruthy() + const commitReceipt = await waitForTransaction(commitTx) + + expect(commitReceipt.status).toBe('success') + + await testClient.increaseTime({ seconds: 61 }) + await testClient.mine({ blocks: 1 }) + + const price = await getPrice(publicClient, { + nameOrNames: params.name, + duration: params.duration, + }) + const total = price!.base + price!.premium + + const tx = await legacyRegisterName(walletClient, { + ...params, + account: accounts[1], + value: total * 2n, + }) + expect(tx).toBeTruthy() + const receipt = await waitForTransaction(tx) + expect(receipt.status).toBe('success') + + const owner = await getOwner(publicClient, { name: params.name }) + expect(owner?.registrant).toBe(accounts[1]) +}) diff --git a/packages/ensjs/src/functions/wallet/legacyRegisterName.ts b/packages/ensjs/src/functions/wallet/legacyRegisterName.ts new file mode 100644 index 00000000..a3dbbf42 --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyRegisterName.ts @@ -0,0 +1,159 @@ +import { + encodeFunctionData, + type Account, + type Hash, + type SendTransactionParameters, + type Transport, +} from 'viem' +import { sendTransaction } from 'viem/actions' +import type { ChainWithEns, ClientWithAccount } from '../../contracts/consts.js' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { UnsupportedNameTypeError } from '../../errors/general.js' +import type { + Prettify, + SimpleTransactionRequest, + WriteTransactionParameters, +} from '../../types.js' +import { getNameType } from '../../utils/getNameType.js' +import { + makeLegacyRegistrationTuple, + type LegacyRegistrationParameters, + isLegacyRegistrationWithConfigParameters, + makeLegacyRegistrationWithConfigTuple, +} from '../../utils/legacyRegisterHelpers.js' +import { + legacyEthRegistrarControllerRegisterSnippet, + legacyEthRegistrarControllerRegisterWithConfigSnippet, +} from '../../contracts/legacyEthRegistrarController.js' + +export type LegacyRegisterNameDataParameters = LegacyRegistrationParameters & { + /** Value of registration */ + value: bigint +} + +export type LegacyRegisterNameDataReturnType = SimpleTransactionRequest & { + value: bigint +} + +export type LegacyRegisterNameParameters< + TChain extends ChainWithEns, + TAccount extends Account | undefined, + TChainOverride extends ChainWithEns | undefined, +> = Prettify< + LegacyRegisterNameDataParameters & + WriteTransactionParameters +> + +export type LegacyRegisterNameReturnType = Hash + +export const makeFunctionData = < + TChain extends ChainWithEns, + TAccount extends Account | undefined, +>( + wallet: ClientWithAccount, + { value, ...args }: LegacyRegisterNameDataParameters, +): LegacyRegisterNameDataReturnType => { + const nameType = getNameType(args.name) + if (nameType !== 'eth-2ld') + throw new UnsupportedNameTypeError({ + nameType, + supportedNameTypes: ['eth-2ld'], + details: 'Only 2ld-eth name registration is supported', + }) + + return { + to: getChainContractAddress({ + client: wallet, + contract: 'legacyEthRegistrarController', + }), + data: isLegacyRegistrationWithConfigParameters(args) + ? encodeFunctionData({ + abi: legacyEthRegistrarControllerRegisterWithConfigSnippet, + functionName: 'registerWithConfig', + args: makeLegacyRegistrationWithConfigTuple(args), + }) + : encodeFunctionData({ + abi: legacyEthRegistrarControllerRegisterSnippet, + functionName: 'register', + args: makeLegacyRegistrationTuple(args), + }), + value, + } +} + +/** + * Registers a name on ENS + * @param wallet - {@link ClientWithAccount} + * @param parameters - {@link RegisterNameParameters} + * @returns Transaction hash. {@link LegacyRegisterNameReturnType} + * + * @example + * import { createPublicClient, createWalletClient, http, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { addEnsContracts } from '@ensdomains/ensjs' + * import { getPrice } from '@ensdomains/ensjs/public' + * import { randomSecret } from '@ensdomains/ensjs/utils' + * import { commitName, registerName } from '@ensdomains/ensjs/wallet' + * + * const mainnetWithEns = addEnsContracts(mainnet) + * const client = createPublicClient({ + * chain: mainnetWithEns, + * transport: http(), + * }) + * const wallet = createWalletClient({ + * chain: mainnetWithEns, + * transport: custom(window.ethereum), + * }) + * const secret = randomSecret() + * const params = { + * name: 'example.eth', + * owner: '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7', + * duration: 31536000, // 1 year + * secret, + * } + * + * const commitmentHash = await commitName(wallet, params) + * await client.waitForTransactionReceipt({ hash: commitmentHash }) // wait for commitment to finalise + * await new Promise((resolve) => setTimeout(resolve, 60 * 1_000)) // wait for commitment to be valid + * + * const { base, premium } = await getPrice(client, { nameOrNames: params.name, duration: params.duration }) + * const value = (base + premium) * 110n / 100n // add 10% to the price for buffer + * const hash = await registerName(wallet, { ...params, value }) + * // 0x... + */ +async function legacyRegisterName< + TChain extends ChainWithEns, + TAccount extends Account | undefined, + TChainOverride extends ChainWithEns | undefined = ChainWithEns, +>( + wallet: ClientWithAccount, + { + name, + owner, + duration, + secret, + resolverAddress, + address, + value, + ...txArgs + }: LegacyRegisterNameParameters, +): Promise { + const data = makeFunctionData(wallet, { + name, + owner, + duration, + secret, + resolverAddress, + address, + value, + }) + const writeArgs = { + ...data, + ...txArgs, + } as SendTransactionParameters + return sendTransaction(wallet, writeArgs) +} + +legacyRegisterName.makeFunctionData = makeFunctionData + +export default legacyRegisterName diff --git a/packages/ensjs/src/test/addTestContracts.ts b/packages/ensjs/src/test/addTestContracts.ts index 58945e49..32aebaf3 100644 --- a/packages/ensjs/src/test/addTestContracts.ts +++ b/packages/ensjs/src/test/addTestContracts.ts @@ -34,6 +34,8 @@ type ContractName = | 'StaticBulkRenewal' | 'DNSSECImpl' | 'Root' + | 'LegacyETHRegistrarController' + | 'LegacyPublicResolver' export const deploymentAddresses = JSON.parse( process.env.DEPLOYMENT_ADDRESSES!, @@ -82,6 +84,12 @@ export const localhost = { ensDnssecImpl: { address: deploymentAddresses.DNSSECImpl, }, + legacyEthRegistrarController: { + address: deploymentAddresses.LegacyETHRegistrarController, + }, + legacyPublicResolver: { + address: deploymentAddresses.LegacyPublicResolver, + }, }, subgraphs: { ens: { diff --git a/packages/ensjs/src/utils/index.ts b/packages/ensjs/src/utils/index.ts index 1132520b..9c54f572 100644 --- a/packages/ensjs/src/utils/index.ts +++ b/packages/ensjs/src/utils/index.ts @@ -104,6 +104,21 @@ export { saveLabel, saveName, } from './labels.js' +export { + makeLegacyCommitment, + makeLegacyCommitmentFromTuple, + makeLegacyCommitmentTuple, + makeLegacyCommitmentWithConfigTuple, + makeLegacyRegistrationTuple, + makeLegacyRegistrationWithConfigTuple, + isLegacyRegistrationWithConfigParameters, + type LegacyCommitmentTuple, + type LegacyCommitmentWithConfigTuple, + type LegacyRegistrationParameters, + type LegacyRegistrationWithConfigParameters, + type LegacyRegistrationTuple, + type LegacyRegistrationWithConfigTuple, +} from './legacyRegisterHelpers.js' export { makeSafeSecondsDate } from './makeSafeSecondsDate.js' export { beautify, diff --git a/packages/ensjs/src/utils/legacyRegisterHelpers.test.ts b/packages/ensjs/src/utils/legacyRegisterHelpers.test.ts new file mode 100644 index 00000000..28ea2d2e --- /dev/null +++ b/packages/ensjs/src/utils/legacyRegisterHelpers.test.ts @@ -0,0 +1,239 @@ +import { type Address, type Hex } from 'viem' +import { beforeAll, describe, expect, it } from 'vitest' +import { + isLegacyRegistrationWithConfigParameters, + makeLegacyCommitment, + makeLegacyCommitmentTuple, + makeLegacyRegistrationTuple, + type LegacyRegistrationParameters, + makeLegacyCommitmentWithConfigTuple, + makeLegacyRegistrationWithConfigTuple, +} from './legacyRegisterHelpers.js' +import { EMPTY_ADDRESS } from './consts.js' +import { randomSecret } from './registerHelpers.js' +import { publicClient, walletClient } from '../test/addTestContracts.js' +import { + legacyEthRegistrarControllerMakeCommitmentSnippet, + legacyEthRegistrarControllerMakeCommitmentWithConfigSnippet, +} from '../contracts/legacyEthRegistrarController.js' +import { getChainContractAddress } from '../contracts/getChainContractAddress.js' + +let accounts: Address[] +let resolverAddress: Address +let secret: Hex +let owner: Address +let address: Address +const duration = 31536000 +const name = 'test.eth' +const makeSnapshot = (addr: Address) => ` + [LegacyRegistrationInvalidConfigError: Resolver address is required when setting an address + + - resolverAddress: 0x0000000000000000000000000000000000000000 + - addr: ${addr} + + Version: @ensdomains/ensjs@1.0.0-mock.0] + ` + +beforeAll(async () => { + accounts = await walletClient.getAddresses() + resolverAddress = getChainContractAddress({ + client: publicClient, + contract: 'legacyPublicResolver', + }) + secret = randomSecret() + ;[owner, address] = accounts +}) + +describe('isLegacyRegistrationWithConfigParameters', () => { + it('return false when resolverAddress and address are undefined', () => { + expect( + isLegacyRegistrationWithConfigParameters({ + name, + owner, + duration, + secret, + }), + ).toBe(false) + }) + + it('return false when resolverAddress and address are empty addresses', () => { + expect( + isLegacyRegistrationWithConfigParameters({ + name, + owner, + duration, + secret, + resolverAddress: EMPTY_ADDRESS, + address: EMPTY_ADDRESS, + }), + ).toBe(false) + }) + + it('return true when resolverAddress and address are defined', () => { + expect( + isLegacyRegistrationWithConfigParameters({ + name, + owner, + duration, + secret, + resolverAddress, + address, + }), + ).toBe(true) + }) + + it('return true when resolverAddress is defined and address is NOT defined', () => { + expect( + isLegacyRegistrationWithConfigParameters({ + name, + owner, + duration, + secret, + resolverAddress, + }), + ).toBe(true) + }) + + it('should throw an error when address is defined and resolverAddress is NOT defined', () => { + expect(() => + isLegacyRegistrationWithConfigParameters({ + name, + owner, + duration, + secret, + address, + }), + ).toThrowErrorMatchingInlineSnapshot(makeSnapshot(address)) + }) + + it('should throw an error when address is defined and resolverAddress is empty address', () => { + expect(() => + isLegacyRegistrationWithConfigParameters({ + name, + owner, + duration, + secret, + resolverAddress: EMPTY_ADDRESS, + address, + }), + ).toThrowErrorMatchingInlineSnapshot(makeSnapshot(address)) + }) +}) + +describe('makeLegacyCommitmentTuple', () => { + it('should return args for makeCommit if resolverAddress and address are undefined', () => { + const tuple = makeLegacyCommitmentTuple({ + name: 'test.eth', + owner, + duration, + secret, + }) + expect(tuple).toEqual(['test', owner, secret]) + }) +}) + +describe('makeLegacyCommitmentWithConfigTuple', () => { + it('should return args for makeCommitWithConfig if resolverAddress is defined', () => { + const tuple = makeLegacyCommitmentWithConfigTuple({ + name: 'test.eth', + owner, + duration, + secret, + resolverAddress, + }) + expect(tuple).toEqual([ + 'test', + owner, + secret, + resolverAddress, + EMPTY_ADDRESS, + ]) + }) +}) + +describe('makeLegacyRegistrationTuple', () => { + it('should return args for register if resolverAddress and address or undefined', () => { + const params: LegacyRegistrationParameters = { + name: 'test.eth', + owner: accounts[0], + duration: 31536000, + secret, + } + expect(makeLegacyRegistrationTuple(params)).toEqual([ + 'test', + accounts[0], + 31536000n, + secret, + ]) + }) +}) + +describe('makeLegacyRegistrationWithConfigTuple', () => { + it('should return args for register if resolverAddress and address are defined', () => { + const tuple = makeLegacyRegistrationWithConfigTuple({ + name: 'test.eth', + owner, + duration, + secret, + resolverAddress, + address, + }) + expect(tuple).toEqual([ + 'test', + owner, + 31536000n, + secret, + resolverAddress, + address, + ]) + }) +}) + +describe('makeLegacyCommitment', () => { + it('should match a commitment generated from makeCommitment', async () => { + const params = { + name, + owner, + duration: 31536000, + secret, + } as const + const commitment = makeLegacyCommitment(params) + + const commitment2 = await publicClient.readContract({ + abi: legacyEthRegistrarControllerMakeCommitmentSnippet, + functionName: 'makeCommitment', + address: getChainContractAddress({ + client: publicClient, + contract: 'legacyEthRegistrarController', + }), + args: makeLegacyCommitmentTuple(params), + }) + + expect(commitment).toBe(commitment2) + }) + + it('should match a commitment generated from makeCommitmentWithConfig', async () => { + const params = { + name, + owner, + duration, + secret, + resolverAddress, + address, + } as const + + const commitment = makeLegacyCommitment(params) + + const commitment2 = await publicClient.readContract({ + abi: legacyEthRegistrarControllerMakeCommitmentWithConfigSnippet, + functionName: 'makeCommitmentWithConfig', + address: getChainContractAddress({ + client: publicClient, + contract: 'legacyEthRegistrarController', + }), + args: makeLegacyCommitmentWithConfigTuple(params), + }) + + expect(commitment).toBe(commitment2) + }) +}) diff --git a/packages/ensjs/src/utils/legacyRegisterHelpers.ts b/packages/ensjs/src/utils/legacyRegisterHelpers.ts new file mode 100644 index 00000000..7f305bf4 --- /dev/null +++ b/packages/ensjs/src/utils/legacyRegisterHelpers.ts @@ -0,0 +1,147 @@ +import { + keccak256, + labelhash, + type Address, + type Hex, + encodePacked, +} from 'viem' +import { EMPTY_ADDRESS } from './consts.js' +import { LegacyRegistrationInvalidConfigError } from '../errors/register.js' + +export type LegacyRegistrationParameters = { + /** Name to register */ + name: string + /** Address to set owner to */ + owner: Address + /** Duration of registration */ + duration: number + /** Random 32 bytes to use for registration */ + secret: Hex + /** Custom resolver address, defaults to empty address */ + resolverAddress?: Address + /** Address to set upon registration, defaults to empty address */ + address?: Address +} + +export type LegacyRegistrationWithConfigParameters = + LegacyRegistrationParameters & { + resolverAddress: Address + address?: Address + } + +export const isLegacyRegistrationWithConfigParameters = ( + params: LegacyRegistrationParameters, +): params is LegacyRegistrationWithConfigParameters => { + const { resolverAddress = EMPTY_ADDRESS, address = EMPTY_ADDRESS } = + params as LegacyRegistrationWithConfigParameters + + if (resolverAddress === EMPTY_ADDRESS && address !== EMPTY_ADDRESS) + throw new LegacyRegistrationInvalidConfigError({ + resolverAddress, + address, + }) + return resolverAddress !== EMPTY_ADDRESS || address !== EMPTY_ADDRESS +} + +export type LegacyCommitmentTuple = [label: string, owner: Address, secret: Hex] + +export type LegacyCommitmentWithConfigTuple = [ + label: string, + owner: Address, + resolverAddress: Address, + address: Address, + secret: Hex, +] + +export type LegacyRegistrationTuple = [ + label: string, + owner: Address, + duration: bigint, + secret: Hex, +] + +export type LegacyRegistrationWithConfigTuple = [ + label: string, + owner: Address, + duration: bigint, + secret: Hex, + resolverAddress: Address, + address: Address, +] + +export const makeLegacyCommitmentTuple = ( + params: LegacyRegistrationParameters, +): LegacyCommitmentTuple => { + const { name, owner, secret } = params + const label = name.split('.')[0] + return [label, owner, secret] +} + +export const makeLegacyCommitmentWithConfigTuple = ( + params: LegacyRegistrationWithConfigParameters, +): LegacyCommitmentWithConfigTuple => { + const { + name, + owner, + secret, + resolverAddress = EMPTY_ADDRESS, + address = EMPTY_ADDRESS, + } = params as LegacyRegistrationWithConfigParameters + const label = name.split('.')[0] + return [label, owner, secret, resolverAddress, address] +} + +export const makeLegacyRegistrationTuple = ({ + name, + owner, + secret, + duration, +}: LegacyRegistrationParameters): LegacyRegistrationTuple => { + const label = name.split('.')[0] + return [label, owner, BigInt(duration), secret] +} + +export const makeLegacyRegistrationWithConfigTuple = ({ + name, + owner, + secret, + duration, + resolverAddress, + address = EMPTY_ADDRESS, +}: LegacyRegistrationWithConfigParameters): LegacyRegistrationWithConfigTuple => { + const label = name.split('.')[0] + return [label, owner, BigInt(duration), secret, resolverAddress, address] +} + +export const makeLegacyCommitmentFromTuple = ([label, ...others]: + | LegacyCommitmentTuple + | LegacyCommitmentWithConfigTuple): Hex => { + const labelHash = labelhash(label) + const params = [labelHash, ...others] as const + + if (params.length === 3) + return keccak256(encodePacked(['bytes32', 'address', 'bytes32'], params)) + + const [ + owner, + secret, + resolverAddress = EMPTY_ADDRESS, + address = EMPTY_ADDRESS, + ] = others + + return keccak256( + encodePacked( + ['bytes32', 'address', 'address', 'address', 'bytes32'], + [labelHash, owner, resolverAddress, address, secret], + ), + ) +} + +export const makeLegacyCommitment = ( + params: LegacyRegistrationParameters | LegacyRegistrationWithConfigParameters, +): Hex => { + const touple = isLegacyRegistrationWithConfigParameters(params) + ? makeLegacyCommitmentWithConfigTuple(params) + : makeLegacyCommitmentTuple(params) + return makeLegacyCommitmentFromTuple(touple) +} diff --git a/packages/ensjs/src/wallet.ts b/packages/ensjs/src/wallet.ts index b4ae0382..d0f463ce 100644 --- a/packages/ensjs/src/wallet.ts +++ b/packages/ensjs/src/wallet.ts @@ -26,6 +26,20 @@ export { type DeleteSubnameParameters, type DeleteSubnameReturnType, } from './functions/wallet/deleteSubname.js' +export { + default as legacyCommitName, + type LegacyCommitNameDataParameters, + type LegacyCommitNameDataReturnType, + type LegacyCommitNameParameters, + type LegacyCommitNameReturnType, +} from './functions/wallet/legacyCommitName.js' +export { + default as legacyRegisterName, + type LegacyRegisterNameDataParameters, + type LegacyRegisterNameDataReturnType, + type LegacyRegisterNameParameters, + type LegacyRegisterNameReturnType, +} from './functions/wallet/legacyRegisterName.js' export { default as registerName, type RegisterNameDataParameters,