diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..bb80ce4cd --- /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/.github/workflows/test.yml b/.github/workflows/test.yml index fa643dbd5..04fadafad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,11 @@ jobs: - name: Build ensjs run: pnpm -F @ensdomains/ensjs build + - name: Publish on pkg.pr.new + run: pnpm dlx pkg-pr-new publish './packages/ensjs' - name: Run tests - run: pnpm -F @ensdomains/ensjs tenv start --extra-time 11368000 + run: pnpm -F @ensdomains/ensjs tenv start --extra-time 11368000 --verbosity 1 + lint: name: Lint runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..702fd1bde --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,75 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Development +```bash +# Install dependencies (from root) +pnpm install + +# Build the main package +cd packages/ensjs && pnpm build + +# Run tests +cd packages/ensjs && pnpm test + +# Run tests in watch mode +cd packages/ensjs && pnpm test:watch + +# Run a single test file +cd packages/ensjs && pnpm test path/to/file.test.ts + +# Lint/format code (from root or package) +pnpm lint + +# Start test environment +cd packages/ensjs && pnpm tenv +``` + +### Publishing +```bash +# Version packages with changesets +pnpm chgset:version + +# Publish packages +pnpm release +``` + +## Architecture + +ENSjs v3 is a monorepo containing the core ENS JavaScript library built on top of viem. + +### Client Types +- **EnsPublicClient**: Read operations (getOwner, getResolver, getName, etc.) +- **EnsWalletClient**: Write operations (registerName, transferName, setRecords, etc.) +- **EnsSubgraphClient**: Subgraph queries for historical/aggregate data + +### Function Organization +``` +src/functions/ +├── public/ # Read functions (e.g., getAddressRecord, getTextRecord) +├── wallet/ # Write functions (e.g., setResolver, setRecords) +├── subgraph/ # Subgraph queries (e.g., getNamesForAddress) +└── dns/ # DNS-related functions +``` + +### Key Patterns +- All functions are designed to be batchable using viem's multicall +- Extensive use of TypeScript generics for type safety +- Custom error types in `/errors/` for detailed error handling +- Utility functions in `/utils/` for common operations like name normalization + +### Testing +- Uses Vitest with happy-dom environment +- Test files co-located with source (*.test.ts) +- ENS test environment available via `pnpm tenv` for integration testing +- Tests should not run in parallel due to shared blockchain state + +### Code Style +- Biome for linting and formatting (2 spaces, single quotes) +- TypeScript strict mode enabled +- No explicit `any` types (warning) +- No non-null assertions allowed +- Imports are automatically organized \ No newline at end of file diff --git a/package.json b/package.json index 842279ad2..da28262eb 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/.claude/settings.local.json b/packages/ensjs/.claude/settings.local.json new file mode 100644 index 000000000..aaa9b54b3 --- /dev/null +++ b/packages/ensjs/.claude/settings.local.json @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "Bash(node:*)", + "Bash(pnpm tenv start:*)", + "Bash(pnpm tenv:*)", + "Bash(pnpm -F @ensdomains/ensjs tenv start:*)", + "Bash(grep:*)", + "Bash(pnpm list:*)", + "Bash(pnpm add:*)", + "Bash(pnpm test:*)", + "Bash(pnpm prepack:*)", + "Bash(find:*)", + "Bash(ls:*)", + "WebFetch(domain:docs.anthropic.com)", + "Bash(claude mcp list:*)", + "mcp__github__list_workflow_runs", + "mcp__github__list_workflows", + "mcp__github__get_job_logs", + "Bash(pnpm lint:*)", + "Bash(pnpm build:*)", + "Bash(pnpm tsc:*)", + "Bash(git add:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/packages/ensjs/deploy/00_register_concurrently.ts b/packages/ensjs/deploy/00_register_concurrently.ts index 0af4bf9d1..42d981023 100644 --- a/packages/ensjs/deploy/00_register_concurrently.ts +++ b/packages/ensjs/deploy/00_register_concurrently.ts @@ -1,4 +1,4 @@ -import type { DeployFunction } from 'hardhat-deploy/dist/types.js' +import type { DeployFunction } from 'hardhat-deploy/dist/types.ts' import { MAX_DATE_INT } from '../dist/utils/consts.js' import { encodeFuses } from '../dist/utils/fuses.js' diff --git a/packages/ensjs/package.json b/packages/ensjs/package.json index ab2f0e987..02321ad82 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", "types": "./dist/index.d.ts", @@ -78,7 +78,7 @@ "clean": "rm -rf ./dist ./README.md ./LICENSE", "lint": "eslint ./src/* --no-error-on-unmatched-pattern", "build": "tsc --project tsconfig.build.json", - "tsn": "TS_NODE_PROJECT=tsconfig.node.json node --loader ts-node/esm", + "tsn": "node --loader ts-node/esm", "prepublish": "pnpm build && cp ../../README.md ../../LICENSE ./", "prepack": "pnpm tsn ./scripts/prepack.ts", "ver": "pnpm tsn ./scripts/updateVersion.ts", @@ -88,7 +88,7 @@ }, "dependencies": { "@adraffy/ens-normalize": "1.10.1", - "@ensdomains/address-encoder": "1.1.1", + "@ensdomains/address-encoder": "1.1.3", "@ensdomains/content-hash": "3.1.0-rc.1", "@ensdomains/dnsprovejs": "^0.5.1", "abitype": "^1.0.0", diff --git a/packages/ensjs/scripts/prepack.ts b/packages/ensjs/scripts/prepack.ts index 42249cbc1..33e304560 100644 --- a/packages/ensjs/scripts/prepack.ts +++ b/packages/ensjs/scripts/prepack.ts @@ -2,16 +2,20 @@ import fs from 'node:fs' import path from 'node:path' +import { fileURLToPath } from 'node:url' /* eslint-disable no-continue */ import jsonFs from 'jsonfile' +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + type Exports = { [key: string]: string | { types?: string; import: string; default: string } } // Generates a package.json to be published to NPM with only the necessary fields. function generatePackageJson() { - const packageJsonPath = path.join(import.meta.dirname, '../package.json') + const packageJsonPath = path.join(__dirname, '../package.json') const tmpPackageJson = jsonFs.readFileSync(packageJsonPath) jsonFs.writeFileSync(`${packageJsonPath}.tmp`, tmpPackageJson, { spaces: 2 }) @@ -42,10 +46,10 @@ function generatePackageJson() { // Generate proxy packages for each export. const files_ = [...files] for (const [key, value] of Object.entries(exports_ as Exports)) { + console.log(key, value) if (typeof value === 'string') continue if (key === '.') continue - if (!value.default || !value.import) - throw new Error('`default` and `import` are required.') + if (!value.default) throw new Error('`default` is required.') if (!fs.existsSync(key)) fs.mkdirSync(key) if (!fs.existsSync(`${key}/package.json`)) fs.writeFileSync( @@ -57,7 +61,7 @@ function generatePackageJson() { if (k === 'import') return 'module' if (k === 'default') return 'main' if (k === 'types') return 'types' - throw new Error('Invalid key') + return k // Allow other keys to pass through })() return `"${key_}": "${v.replace('./', '../')}"` }) diff --git a/packages/ensjs/src/contracts/consts.ts b/packages/ensjs/src/contracts/consts.ts index e6ef5f6f6..8999f583b 100644 --- a/packages/ensjs/src/contracts/consts.ts +++ b/packages/ensjs/src/contracts/consts.ts @@ -19,6 +19,9 @@ export const supportedContracts = [ 'ensRegistry', 'ensReverseRegistrar', 'ensUniversalResolver', + 'legacyEthRegistrarController', + 'legacyPublicResolver', + 'ensDefaultReverseRegistrar', ] as const export type SupportedChain = (typeof supportedChains)[number] @@ -54,7 +57,16 @@ export const addresses = { address: '0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb', }, ensUniversalResolver: { - address: '0x5a9236e72a66d3e08b83dcf489b4d850792b6009', + address: '0xaBd80E8a13596fEeA40Fd26fD6a24c3fe76F05fB', + }, + ensDefaultReverseRegistrar: { + address: '0x283F227c4Bd38ecE252C4Ae7ECE650B0e913f1f9', + }, + legacyEthRegistrarController: { + address: '0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5', + }, + legacyPublicResolver: { + address: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41', }, }, [holesky.id]: { @@ -88,6 +100,15 @@ export const addresses = { ensUniversalResolver: { address: '0xf606bc986635dab91b189aee8f565f45a0336f89', }, + legacyEthRegistrarController: { + address: '0xf13fC748601fDc5afA255e9D9166EB43f603a903', + }, + legacyPublicResolver: { + address: '0xc5e43b622b5e6C379a984E9BdB34E9A545564fA5', + }, + ensDefaultReverseRegistrar: { + address: '0x0000000000000000000000000000000000000000', + }, }, [sepolia.id]: { ensBaseRegistrarImplementation: { @@ -115,10 +136,19 @@ export const addresses = { address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', }, ensReverseRegistrar: { - address: '0xCF75B92126B02C9811d8c632144288a3eb84afC8', + address: '0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6', }, ensUniversalResolver: { - address: '0x49c9331501b37191d54f5e332b307df82d15e9cc', + address: '0xb7B7DAdF4D42a08B3eC1d3A1079959Dfbc8CFfCC', + }, + ensDefaultReverseRegistrar: { + address: '0x4F382928805ba0e23B30cFB75fC9E848e82DFD47', + }, + legacyEthRegistrarController: { + address: '0x7e02892cfc2Bfd53a75275451d73cF620e793fc0', + }, + legacyPublicResolver: { + address: '0x0CeEC524b2807841739D3B5E161F5bf1430FFA48', }, }, } as const satisfies Record< @@ -159,6 +189,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 41349ac0f..389a40713 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 861f760aa..db4c70fce 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 000000000..c92ed99ab --- /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 000000000..244969830 --- /dev/null +++ b/packages/ensjs/src/errors/register.ts @@ -0,0 +1,22 @@ +import type { Address } from 'viem' +import { EMPTY_ADDRESS } from '../utils/consts.js' +import { BaseError } from './base.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/utils.ts b/packages/ensjs/src/errors/utils.ts index 6e7258cf2..fbdd5995c 100644 --- a/packages/ensjs/src/errors/utils.ts +++ b/packages/ensjs/src/errors/utils.ts @@ -79,7 +79,7 @@ export class FusesFuseNotAllowedError extends BaseError { export class FusesInvalidUnnamedFuseError extends BaseError { override name = 'FusesInvalidUnnamedFuseError' - constructor({ fuse }: { fuse: any }) { + constructor({ fuse }: { fuse: unknown }) { super(`${fuse} is not a valid unnamed fuse`, { metaMessages: [ '- If you are trying to set a named fuse, use the named property', diff --git a/packages/ensjs/src/errors/version.ts b/packages/ensjs/src/errors/version.ts index bc2da28ee..d32216124 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 000000000..475a0c9b0 --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyCommitName.test.ts @@ -0,0 +1,59 @@ +import type { Address, Hex } from 'viem' +import { afterEach, beforeAll, beforeEach, expect, it } from 'vitest' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { legacyEthRegistrarControllerCommitmentsSnippet } from '../../contracts/legacyEthRegistrarController.js' +import { + publicClient, + testClient, + waitForTransaction, + walletClient, +} from '../../test/addTestContracts.js' +import { + type LegacyRegistrationParameters, + makeLegacyCommitment, +} from '../../utils/legacyRegisterHelpers.js' +import legacyCommitName from './legacyCommitName.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 000000000..dc3b27a7b --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyCommitName.ts @@ -0,0 +1,127 @@ +import { + type Account, + type Hash, + type SendTransactionParameters, + type Transport, + encodeFunctionData, +} from 'viem' +import { sendTransaction } from 'viem/actions' +import type { ChainWithEns, ClientWithAccount } from '../../contracts/consts.js' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { legacyEthRegistrarControllerCommitSnippet } from '../../contracts/legacyEthRegistrarController.js' +import { UnsupportedNameTypeError } from '../../errors/general.js' +import type { + Prettify, + SimpleTransactionRequest, + WriteTransactionParameters, +} from '../../types.js' +import { EMPTY_ADDRESS } from '../../utils/consts.js' +import { getNameType } from '../../utils/getNameType.js' +import { + type LegacyRegistrationParameters, + makeLegacyCommitment, +} from '../../utils/legacyRegisterHelpers.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 000000000..73a7a2b51 --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyRegisterName.test.ts @@ -0,0 +1,113 @@ +import type { Address, 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 type { LegacyRegistrationParameters } from '../../utils/legacyRegisterHelpers.js' +import type { RegistrationParameters } from '../../utils/registerHelpers.js' +import getOwner from '../public/getOwner.js' +import getPrice from '../public/getPrice.js' +import legacyCommitName from './legacyCommitName.js' +import legacyRegisterName from './legacyRegisterName.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 000000000..ac918c4ac --- /dev/null +++ b/packages/ensjs/src/functions/wallet/legacyRegisterName.ts @@ -0,0 +1,159 @@ +import { + type Account, + type Hash, + type SendTransactionParameters, + type Transport, + encodeFunctionData, +} from 'viem' +import { sendTransaction } from 'viem/actions' +import type { ChainWithEns, ClientWithAccount } from '../../contracts/consts.js' +import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' +import { + legacyEthRegistrarControllerRegisterSnippet, + legacyEthRegistrarControllerRegisterWithConfigSnippet, +} from '../../contracts/legacyEthRegistrarController.js' +import { UnsupportedNameTypeError } from '../../errors/general.js' +import type { + Prettify, + SimpleTransactionRequest, + WriteTransactionParameters, +} from '../../types.js' +import { getNameType } from '../../utils/getNameType.js' +import { + type LegacyRegistrationParameters, + isLegacyRegistrationWithConfigParameters, + makeLegacyRegistrationTuple, + makeLegacyRegistrationWithConfigTuple, +} from '../../utils/legacyRegisterHelpers.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 52e41d7e5..17e67b4d0 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/test/setup.ts b/packages/ensjs/src/test/setup.ts index 3342ef1c4..3ac4cb1de 100644 --- a/packages/ensjs/src/test/setup.ts +++ b/packages/ensjs/src/test/setup.ts @@ -1,5 +1,25 @@ import { beforeAll, vi } from 'vitest' +// Mock localStorage for Node environment +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString() + }, + removeItem: (key: string) => { + delete store[key] + }, + clear: () => { + store = {} + }, + } +})() + +// @ts-ignore +global.localStorage = localStorageMock + beforeAll(() => { vi.mock('../errors/error-utils.ts', () => ({ getVersion: vi.fn().mockReturnValue('@ensdomains/ensjs@1.0.0-mock.0'), diff --git a/packages/ensjs/src/utils/index.ts b/packages/ensjs/src/utils/index.ts index 1132520bd..9c54f5729 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 000000000..aa63253ad --- /dev/null +++ b/packages/ensjs/src/utils/legacyRegisterHelpers.test.ts @@ -0,0 +1,239 @@ +import type { Address, Hex } from 'viem' +import { beforeAll, describe, expect, it } from 'vitest' +import { getChainContractAddress } from '../contracts/getChainContractAddress.js' +import { + legacyEthRegistrarControllerMakeCommitmentSnippet, + legacyEthRegistrarControllerMakeCommitmentWithConfigSnippet, +} from '../contracts/legacyEthRegistrarController.js' +import { publicClient, walletClient } from '../test/addTestContracts.js' +import { EMPTY_ADDRESS } from './consts.js' +import { + type LegacyRegistrationParameters, + isLegacyRegistrationWithConfigParameters, + makeLegacyCommitment, + makeLegacyCommitmentTuple, + makeLegacyCommitmentWithConfigTuple, + makeLegacyRegistrationTuple, + makeLegacyRegistrationWithConfigTuple, +} from './legacyRegisterHelpers.js' +import { randomSecret } from './registerHelpers.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 000000000..94499be61 --- /dev/null +++ b/packages/ensjs/src/utils/legacyRegisterHelpers.ts @@ -0,0 +1,147 @@ +import { + type Address, + type Hex, + encodePacked, + keccak256, + labelhash, +} from 'viem' +import { LegacyRegistrationInvalidConfigError } from '../errors/register.js' +import { EMPTY_ADDRESS } from './consts.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 b4ae03820..d0f463cec 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, diff --git a/packages/ensjs/utils/legacyNameGenerator.ts b/packages/ensjs/utils/legacyNameGenerator.ts index 8f1041b85..db8a83b8b 100644 --- a/packages/ensjs/utils/legacyNameGenerator.ts +++ b/packages/ensjs/utils/legacyNameGenerator.ts @@ -32,8 +32,10 @@ const makeNameGenerator = async ( addr, ]) + const nonce = nonceManager.getNonce(namedOwner) + console.log('Nonce for', label, 'by', namedOwner, 'is', nonce) return controller.write.commit([commitment], { - nonce: nonceManager.getNonce(namedOwner), + nonce, account: registrant, }) }, diff --git a/packages/ensjs/utils/nonceManager.ts b/packages/ensjs/utils/nonceManager.ts index cc2ec5025..bc464781f 100644 --- a/packages/ensjs/utils/nonceManager.ts +++ b/packages/ensjs/utils/nonceManager.ts @@ -20,8 +20,10 @@ const makeNonceManager = async (href: HardhatRuntimeEnvironment) => { return { getNonce: (name: string) => { + console.log('Getting nonce for', name, nonceMap[name]) const nonce = nonceMap[name] nonceMap[name]++ + console.log('Incremented nonce for', name, nonceMap[name]) return nonce }, } diff --git a/packages/ensjs/utils/wrappedNameGenerator.ts b/packages/ensjs/utils/wrappedNameGenerator.ts index eeb301a95..fd89410c3 100644 --- a/packages/ensjs/utils/wrappedNameGenerator.ts +++ b/packages/ensjs/utils/wrappedNameGenerator.ts @@ -43,8 +43,10 @@ const makeNameGenerator = async ( fuses, ]) + const nonce = nonceManager.getNonce(namedOwner) + console.log('Nonce for', label, 'by', namedOwner, 'is', nonce) return controller.write.commit([commitment], { - nonce: nonceManager.getNonce(namedOwner), + nonce, account: owner, }) }, @@ -70,11 +72,13 @@ const makeNameGenerator = async ( const price = await controller.read.rentPrice([label, duration]) const priceWithBuffer = (price.base * 105n) / 100n + const nonce = nonceManager.getNonce(namedOwner) + console.log('Nonce for', label, 'by', namedOwner, 'is', nonce) return controller.write.register( [label, owner, duration, secret, resolver, data, reverseRecord, fuses], { value: priceWithBuffer, - nonce: nonceManager.getNonce(namedOwner), + nonce, account: owner, }, ) diff --git a/packages/ensjs/vitest.config.ts b/packages/ensjs/vitest.config.ts index 9ca54c28f..57422543f 100644 --- a/packages/ensjs/vitest.config.ts +++ b/packages/ensjs/vitest.config.ts @@ -11,5 +11,6 @@ export default defineConfig({ setupFiles: ['./src/test/setup.ts'], include: ['src/**/*.test.ts'], exclude: ['data/**/*'], + testTimeout: 10000, }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2129486ae..e1476374a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: 1.10.1 version: 1.10.1 '@ensdomains/address-encoder': - specifier: 1.1.1 - version: 1.1.1 + specifier: 1.1.3 + version: 1.1.3 '@ensdomains/content-hash': specifier: 3.1.0-rc.1 version: 3.1.0-rc.1 @@ -369,8 +369,8 @@ packages: '@ensdomains/address-encoder@1.0.0-rc.3': resolution: {integrity: sha512-8o6zH69rObIqDY4PusEWuN9jvVOct+9jj9AOPO7ifc3ev8nmsly0e8TE1sHkhk0iKFbd3DlSsUnJ+yuRWmdLCQ==} - '@ensdomains/address-encoder@1.1.1': - resolution: {integrity: sha512-yg7s+suCuKRhaGsgLu57W/jxIs/Lnqs/SU7jT7UwS4ATSnW94jbUCbmyyZ82CQwKsmwaUE8uYvvVb4N6lfz29A==} + '@ensdomains/address-encoder@1.1.3': + resolution: {integrity: sha512-QS4ax0YkA8tsbQcWgBNmLLtb3aG0jsOQtED9SRyX9Ixflt1jDMuC/0i3ONMnNJNG5HTIko0Te4Y1JIGXjNcnUg==} '@ensdomains/buffer@0.1.1': resolution: {integrity: sha512-92SfSiNS8XorgU7OUBHo/i1ZU7JV7iz/6bKuLPNVsMxV79/eI7fJR6jfJJc40zAHjs3ha+Xo965Idomlq3rqnw==} @@ -5323,7 +5323,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 - '@ensdomains/address-encoder@1.1.1': + '@ensdomains/address-encoder@1.1.3': dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0