From b227233f891c4ed128ad56ec8865885fb61c408a Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 8 Jan 2025 22:29:04 +0800 Subject: [PATCH 1/9] add option for legacy registration --- package.json | 4 +- pnpm-lock.yaml | 85 ++++++-- .../[name]/registration/steps/Complete.tsx | 65 +----- .../chain/useEstimateGasWithStateOverride.ts | 4 +- .../gasEstimation/calculateTransactions.ts | 80 +++++++ .../gasEstimation/useEstimateRegistration.ts | 47 ++-- ...useRegistrationValueFromRegisterReceipt.ts | 116 ++++++++++ .../transaction/commitName.ts | 6 +- .../transaction/registerName.ts | 10 +- src/utils/chains/makeLocalhostChainWithEns.ts | 6 + src/utils/coin.ts | 5 +- .../registration/isLegacyRegistration.test.ts | 201 ++++++++++++++++++ .../registration/isLegacyRegistration.ts | 29 +++ .../makeLegacyRegistrationParams.test.ts | 82 +++++++ .../makeLegacyRegistrationParams.ts | 34 +++ 15 files changed, 654 insertions(+), 120 deletions(-) create mode 100644 src/hooks/gasEstimation/calculateTransactions.ts create mode 100644 src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts create mode 100644 src/utils/registration/isLegacyRegistration.test.ts create mode 100644 src/utils/registration/isLegacyRegistration.ts create mode 100644 src/utils/registration/makeLegacyRegistrationParams.test.ts create mode 100644 src/utils/registration/makeLegacyRegistrationParams.ts diff --git a/package.json b/package.json index 924500452..bd5b9c9b3 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@ensdomains/address-encoder": "1.1.1", "@ensdomains/content-hash": "^3.0.0-beta.5", "@ensdomains/ens-contracts": "1.2.0-beta.0", - "@ensdomains/ensjs": "4.0.2", + "@ensdomains/ensjs": "4.0.3-alpha.0", "@ensdomains/thorin": "0.6.50", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^14.0.2", @@ -287,4 +287,4 @@ } }, "packageManager": "pnpm@9.3.0" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5bc20d3f..2ddaa727d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,8 +125,8 @@ importers: specifier: 1.2.0-beta.0 version: 1.2.0-beta.0 '@ensdomains/ensjs': - specifier: 4.0.2 - version: 4.0.2(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + specifier: 4.0.3-alpha.0 + version: 4.0.3-alpha.0(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) '@ensdomains/thorin': specifier: 0.6.50 version: 0.6.50(react-dom@18.3.1(react@18.3.1))(react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)) @@ -492,6 +492,48 @@ importers: specifier: ^1.0.0-pre.53 version: 1.0.0-pre.53 + .yalc/@ensdomains/ens-test-env: + dependencies: + '@ethersproject/wallet': + specifier: ^5.6.0 + version: 5.7.0 + ansi-colors: + specifier: ^4.1.1 + version: 4.1.3 + cli-progress: + specifier: ^3.10.0 + version: 3.12.0 + commander: + specifier: ^9.3.0 + version: 9.5.0 + concurrently: + specifier: ^7.1.0 + version: 7.6.0 + docker-compose: + specifier: ^0.24.7 + version: 0.24.8 + dotenv: + specifier: ^16.0.1 + version: 16.4.5 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + lz4: + specifier: ^0.6.5 + version: 0.6.5 + progress-stream: + specifier: ^2.0.0 + version: 2.0.0 + tar-fs: + specifier: ^2.1.1 + version: 2.1.1 + viem: + specifier: ^2.21.37 + version: 2.21.40(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) + wait-on: + specifier: ^6.0.1 + version: 6.0.1 + packages: '@adobe/css-tools@4.3.3': @@ -1630,8 +1672,8 @@ packages: '@ensdomains/ensjs@2.1.0': resolution: {integrity: sha512-GRbGPT8Z/OJMDuxs75U/jUNEC0tbL0aj7/L/QQznGYKm/tiasp+ndLOaoULy9kKJFC0TBByqfFliEHDgoLhyog==} - '@ensdomains/ensjs@4.0.2': - resolution: {integrity: sha512-4vDIZEFAa1doNA3H9MppUHxflSDYYPVNyaDbdHLksTa4taq3y4dGpletX67Xea8nxI+cMfjEi4nOzLJmPzRE/g==} + '@ensdomains/ensjs@4.0.3-alpha.0': + resolution: {integrity: sha512-BSGzy86XnNxNhP3zEdo5Nbb1dufsarTD4WO4G/I87Ux9v5CAwiIJZ8n0cNHpJwh3SZDDLZotZFVf1LIMQgcNuA==} peerDependencies: viem: ^2.9.2 @@ -8868,6 +8910,7 @@ packages: sudo-prompt@9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. summary@2.1.0: resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==} @@ -11491,9 +11534,9 @@ snapshots: '@ensdomains/address-encoder@1.1.1': dependencies: - '@noble/curves': 1.4.0 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 '@ensdomains/buffer@0.1.1': {} @@ -11560,7 +11603,7 @@ snapshots: - bufferutil - utf-8-validate - '@ensdomains/ensjs@4.0.2(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': + '@ensdomains/ensjs@4.0.3-alpha.0(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': dependencies: '@adraffy/ens-normalize': 1.10.1 '@ensdomains/address-encoder': 1.1.1 @@ -12402,8 +12445,8 @@ snapshots: '@metamask/utils@8.4.0': dependencies: '@ethereumjs/tx': 4.2.0 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 '@types/debug': 4.1.12 debug: 4.3.4(supports-color@8.1.1) pony-cause: 2.1.11 @@ -13171,19 +13214,19 @@ snapshots: dependencies: '@noble/hashes': 1.2.0 '@noble/secp256k1': 1.7.1 - '@scure/base': 1.1.6 + '@scure/base': 1.1.9 '@scure/bip32@1.3.3': dependencies: '@noble/curves': 1.3.0 '@noble/hashes': 1.3.3 - '@scure/base': 1.1.6 + '@scure/base': 1.1.9 '@scure/bip32@1.4.0': dependencies: '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@scure/base': 1.1.9 '@scure/bip32@1.5.0': dependencies: @@ -13194,17 +13237,17 @@ snapshots: '@scure/bip39@1.1.1': dependencies: '@noble/hashes': 1.2.0 - '@scure/base': 1.1.6 + '@scure/base': 1.1.9 '@scure/bip39@1.2.2': dependencies: '@noble/hashes': 1.3.3 - '@scure/base': 1.1.6 + '@scure/base': 1.1.9 '@scure/bip39@1.3.0': dependencies: '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@scure/base': 1.1.9 '@scure/bip39@1.4.0': dependencies: @@ -15274,7 +15317,7 @@ snapshots: capnp-ts@0.7.0: dependencies: debug: 4.3.6(supports-color@5.5.0) - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - supports-color @@ -16522,7 +16565,7 @@ snapshots: ethereum-bloom-filters@1.1.0: dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 ethereum-cryptography@0.1.3: dependencies: @@ -18252,7 +18295,7 @@ snapshots: media-query-parser@2.0.2: dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.25.0 media-typer@0.3.0: {} @@ -21594,8 +21637,8 @@ snapshots: webauthn-p256@0.0.5: dependencies: - '@noble/curves': 1.4.0 - '@noble/hashes': 1.4.0 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 webextension-polyfill@0.10.0: {} diff --git a/src/components/pages/profile/[name]/registration/steps/Complete.tsx b/src/components/pages/profile/[name]/registration/steps/Complete.tsx index bf647e8d7..0978a22c7 100644 --- a/src/components/pages/profile/[name]/registration/steps/Complete.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Complete.tsx @@ -3,7 +3,6 @@ import { Fragment, useEffect, useMemo, useState } from 'react' import type ConfettiT from 'react-confetti' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' -import { decodeEventLog } from 'viem' import { useAccount } from 'wagmi' import { tokenise } from '@ensdomains/ensjs/utils' @@ -12,6 +11,7 @@ import { Button, mq, Typography } from '@ensdomains/thorin' import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' import NFTTemplate from '@app/components/@molecules/NFTTemplate/NFTTemplate' import { Card } from '@app/components/Card' +import { useRegistrationValueFromRegisterReceipt } from '@app/hooks/pages/register/useRegistrationValueFromRegisterReceipt' import useWindowSize from '@app/hooks/useWindowSize' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' import { dateFromDateDiff } from '@app/utils/date' @@ -125,47 +125,6 @@ const SubtitleWithGradient = styled(Typography)( `, ) -const nameRegisteredSnippet = [ - { - 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 - const Confetti = dynamic(() => import('react-confetti').then((mod) => mod.default as typeof ConfettiT), ) @@ -190,26 +149,10 @@ const useEthInvoice = ( const commitReceipt = commitTxFlow?.minedData const registerReceipt = registerTxFlow?.minedData - const registrationValue = useMemo(() => { - if (!registerReceipt) return null - for (const log of registerReceipt.logs) { - try { - const { - args: { baseCost, premium }, - } = decodeEventLog({ - abi: nameRegisteredSnippet, - topics: log.topics, - data: log.data, - eventName: 'NameRegistered', - }) - return baseCost + premium - // eslint-disable-next-line no-empty - } catch {} - } - return null - }, [registerReceipt]) + const { data: registrationValue, isLoading: isRegistrationValueLoading } = + useRegistrationValueFromRegisterReceipt({ registerReceipt }) - const isLoading = !commitReceipt || !registerReceipt + const isLoading = !commitReceipt || !registerReceipt || isRegistrationValueLoading useEffect(() => { const storage = localStorage.getItem(`avatar-src-${name}`) diff --git a/src/hooks/chain/useEstimateGasWithStateOverride.ts b/src/hooks/chain/useEstimateGasWithStateOverride.ts index 34c23b8e4..1614da6a5 100644 --- a/src/hooks/chain/useEstimateGasWithStateOverride.ts +++ b/src/hooks/chain/useEstimateGasWithStateOverride.ts @@ -76,14 +76,14 @@ type StateOverride = { } } -type TransactionItem = { +export type TransactionItem = { [TName in TransactionName]: Omit, 'client' | 'connectorClient'> & { name: TName stateOverride?: UserStateOverrides } }[TransactionName] -type UseEstimateGasWithStateOverrideParameters< +export type UseEstimateGasWithStateOverrideParameters< TransactionItems extends TransactionItem[] | readonly TransactionItem[], > = { transactions: TransactionItems diff --git a/src/hooks/gasEstimation/calculateTransactions.ts b/src/hooks/gasEstimation/calculateTransactions.ts new file mode 100644 index 000000000..c7a259fb9 --- /dev/null +++ b/src/hooks/gasEstimation/calculateTransactions.ts @@ -0,0 +1,80 @@ +import { match } from 'ts-pattern' +import { Address, parseEther } from 'viem' + +import { + makeCommitment, + makeLegacyCommitment, + RegistrationParameters, +} from '@ensdomains/ensjs/utils' + +import { isLegacyRegistration } from '@app/utils/registration/isLegacyRegistration' +import { makeLegacyRegistrationParams } from '@app/utils/registration/makeLegacyRegistrationParams' + +import { useEstimateGasWithStateOverride } from '../chain/useEstimateGasWithStateOverride' + +type ReturnType = null | Parameters[0]['transactions'] + +export const calculateTransactions = ({ + registrationParams, + ethRegistrarControllerAddress, + legacyEthRegistrarControllerAddress, + fiveMinutesAgoInSeconds, + price, +}: { + registrationParams?: RegistrationParameters + ethRegistrarControllerAddress: unknown + legacyEthRegistrarControllerAddress: unknown + fiveMinutesAgoInSeconds: number + price?: { base: bigint; premium: bigint } +}): ReturnType => { + if ( + !registrationParams || + !ethRegistrarControllerAddress || + !legacyEthRegistrarControllerAddress || + !price + ) + return null + + const isLegacy = isLegacyRegistration(registrationParams) + + const registrationStateOverride = match(isLegacy) + .with(true, () => ({ + address: legacyEthRegistrarControllerAddress as Address, + stateDiff: [ + { + slot: 5, + keys: [makeLegacyCommitment(makeLegacyRegistrationParams(registrationParams))], + value: BigInt(fiveMinutesAgoInSeconds), + }, + ], + })) + .with(false, () => ({ + address: ethRegistrarControllerAddress as Address, + stateDiff: [ + { + slot: 1, + keys: [makeCommitment(registrationParams)], + value: BigInt(fiveMinutesAgoInSeconds), + }, + ], + })) + .exhaustive() + + return [ + { + name: 'commitName', + data: registrationParams, + }, + { + name: 'registerName', + data: registrationParams, + stateOverride: [ + registrationStateOverride, + { + address: registrationParams.owner, + balance: price ? (price.base + price.premium) * 2n + parseEther('10000') : undefined, + }, + ], + }, + ] as const +} diff --git a/src/hooks/gasEstimation/useEstimateRegistration.ts b/src/hooks/gasEstimation/useEstimateRegistration.ts index 3d969e678..7b8dff5b2 100644 --- a/src/hooks/gasEstimation/useEstimateRegistration.ts +++ b/src/hooks/gasEstimation/useEstimateRegistration.ts @@ -1,7 +1,4 @@ import { useMemo } from 'react' -import { parseEther } from 'viem' - -import { makeCommitment } from '@ensdomains/ensjs/utils' import { RegistrationReducerDataItem } from '@app/components/pages/profile/[name]/registration/types' import { deriveYearlyFee } from '@app/utils/utils' @@ -13,6 +10,7 @@ import { useEstimateGasWithStateOverride } from '../chain/useEstimateGasWithStat import { useGasPrice } from '../chain/useGasPrice' import { usePrice } from '../ensjs/public/usePrice' import useRegistrationParams from '../useRegistrationParams' +import { calculateTransactions } from './calculateTransactions' type UseEstimateFullRegistrationParameters = { registrationData: RegistrationReducerDataItem @@ -38,7 +36,12 @@ export const useEstimateFullRegistration = ({ contract: 'ensEthRegistrarController', }) - const commitment = useMemo(() => makeCommitment(registrationParams), [registrationParams]) + const legacyEthRegistrarControllerAddress = useContractAddress({ + contract: 'legacyEthRegistrarController', + }) + + console.log('ethRegistrarControllerAddress', ethRegistrarControllerAddress) + console.log('legacyEthRegistrarControllerAddress', legacyEthRegistrarControllerAddress) const { data: blockTimestamp } = useBlockTimestamp() // default to use block timestamp as reference @@ -54,34 +57,16 @@ export const useEstimateFullRegistration = ({ [timestampReference], ) + const transactions = calculateTransactions({ + registrationParams, + ethRegistrarControllerAddress, + legacyEthRegistrarControllerAddress, + fiveMinutesAgoInSeconds, + price, + }) const { data, isLoading } = useEstimateGasWithStateOverride({ - transactions: [ - { - name: 'commitName', - data: registrationParams, - }, - { - name: 'registerName', - data: registrationParams, - stateOverride: [ - { - address: ethRegistrarControllerAddress, - stateDiff: [ - { - slot: 1, - keys: [commitment], - value: BigInt(fiveMinutesAgoInSeconds), - }, - ], - }, - { - address: registrationParams.owner, - balance: price ? (price.base + price.premium) * 2n + parseEther('10000') : undefined, - }, - ], - }, - ], - enabled: !!ethRegistrarControllerAddress && !!price, + transactions: transactions!, + enabled: !!transactions, }) const premiumFee = price?.premium diff --git a/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts b/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts new file mode 100644 index 000000000..34de823b4 --- /dev/null +++ b/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts @@ -0,0 +1,116 @@ +import { decodeEventLog } from 'viem' + +import { MinedData } from '@app/types' +import { useQuery } from '@app/utils/query/useQuery' + +const legacyNameRegisteredEventSnippet = [ + { + 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 + +const decodeLegacyNameRegisteredEventLog = (log: MinedData['logs'][number]): Promise => + new Promise((resolve, reject) => { + try { + const t = decodeEventLog({ + abi: legacyNameRegisteredEventSnippet, + topics: log.topics, + data: log.data, + eventName: 'NameRegistered', + }) + if (!t.args.cost) reject() + resolve(t.args.cost) + } catch { + reject() + } + }) + +const nameRegisteredEventSnippet = [ + { + 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 + +const decodeWrappedNameRegisteredEventLog = (log: MinedData['logs'][number]): Promise => + new Promise((resolve, reject) => { + try { + const t = decodeEventLog({ + abi: nameRegisteredEventSnippet, + topics: log.topics, + data: log.data, + eventName: 'NameRegistered', + }) + resolve(t.args.baseCost + t.args.premium) + } catch { + reject() + } + }) + +export const useRegistrationValueFromRegisterReceipt = ({ + registerReceipt, +}: { + registerReceipt?: MinedData +}) => { + return useQuery({ + queryKey: ['registration-value', registerReceipt], + queryFn: async () => { + try { + const promises = registerReceipt!.logs + .map((log) => [ + decodeLegacyNameRegisteredEventLog(log), + decodeWrappedNameRegisteredEventLog(log), + ]) + .flat() + return await Promise.any(promises) + } catch { + return null + } + }, + enabled: !!registerReceipt, + retry: false, + }) +} diff --git a/src/transaction-flow/transaction/commitName.ts b/src/transaction-flow/transaction/commitName.ts index b3cb4167a..0b568b4e4 100644 --- a/src/transaction-flow/transaction/commitName.ts +++ b/src/transaction-flow/transaction/commitName.ts @@ -1,9 +1,11 @@ import type { TFunction } from 'react-i18next' import { RegistrationParameters } from '@ensdomains/ensjs/utils' -import { commitName } from '@ensdomains/ensjs/wallet' +import { commitName, legacyCommitName } from '@ensdomains/ensjs/wallet' import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' +import { isLegacyRegistration } from '@app/utils/registration/isLegacyRegistration' +import { makeLegacyRegistrationParams } from '@app/utils/registration/makeLegacyRegistrationParams' type Data = RegistrationParameters & { name: string } @@ -27,6 +29,8 @@ const displayItems = ( ] const transaction = async ({ connectorClient, data }: TransactionFunctionParameters) => { + if (isLegacyRegistration(data)) + return legacyCommitName.makeFunctionData(connectorClient, makeLegacyRegistrationParams(data)) return commitName.makeFunctionData(connectorClient, data) } diff --git a/src/transaction-flow/transaction/registerName.ts b/src/transaction-flow/transaction/registerName.ts index 3a1d95dfb..a6bcfafc9 100644 --- a/src/transaction-flow/transaction/registerName.ts +++ b/src/transaction-flow/transaction/registerName.ts @@ -2,11 +2,14 @@ import type { TFunction } from 'react-i18next' import { getPrice } from '@ensdomains/ensjs/public' import { RegistrationParameters } from '@ensdomains/ensjs/utils' -import { registerName } from '@ensdomains/ensjs/wallet' +import { legacyRegisterName, registerName } from '@ensdomains/ensjs/wallet' import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types' +import { isLegacyRegistration } from '@app/utils/registration/isLegacyRegistration' import { calculateValueWithBuffer, formatDurationOfDates } from '@app/utils/utils' +import { makeLegacyRegistrationParams } from '../../utils/registration/makeLegacyRegistrationParams' + type Data = RegistrationParameters const now = Math.floor(Date.now()) const displayItems = ( @@ -41,6 +44,11 @@ const transaction = async ({ const value = price.base + price.premium const valueWithBuffer = calculateValueWithBuffer(value) + if (isLegacyRegistration(data)) + return legacyRegisterName.makeFunctionData(connectorClient, { + ...makeLegacyRegistrationParams(data), + value: valueWithBuffer, + }) return registerName.makeFunctionData(connectorClient, { ...data, value: valueWithBuffer, diff --git a/src/utils/chains/makeLocalhostChainWithEns.ts b/src/utils/chains/makeLocalhostChainWithEns.ts index 295e9c1e8..d1836de60 100644 --- a/src/utils/chains/makeLocalhostChainWithEns.ts +++ b/src/utils/chains/makeLocalhostChainWithEns.ts @@ -43,6 +43,12 @@ export const makeLocalhostChainWithEns = ( ensDnssecImpl: { address: deploymentAddresses_.DNSSECImpl, }, + legacyEthRegistrarController: { + address: deploymentAddresses_.LegacyETHRegistrarController, + }, + legacyPublicResolver: { + address: deploymentAddresses_.LegacyPublicResolver, + }, }, subgraphs: { ens: { diff --git a/src/utils/coin.ts b/src/utils/coin.ts index 74bf3d1ca..a77145b50 100644 --- a/src/utils/coin.ts +++ b/src/utils/coin.ts @@ -1,5 +1,8 @@ import { getAddress } from 'viem' +export const isEthCoin = (coin: string | number): boolean => + (typeof coin === 'string' && coin.toLowerCase() === 'eth') || coin === 60 + export const normalizeCoinAddress = ({ coin, address, @@ -8,7 +11,7 @@ export const normalizeCoinAddress = ({ address?: string | null }): string => { if (!address) return '' - if (coin === 'eth' || coin === 'ETH' || coin === 60) { + if (isEthCoin(coin)) { try { return getAddress(address) } catch { diff --git a/src/utils/registration/isLegacyRegistration.test.ts b/src/utils/registration/isLegacyRegistration.test.ts new file mode 100644 index 000000000..d6e1b11d1 --- /dev/null +++ b/src/utils/registration/isLegacyRegistration.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it } from 'vitest' + +import { RegistrationParameters } from '@ensdomains/ensjs/utils' + +import { isLegacyRegistration } from './isLegacyRegistration' + +describe('isLegacyRegistration', () => { + it('should return true when there are no records, fuses, or reverse record', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(true) + }) + + it('should return true when there are no records, fuses, or reverse record', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 'eth', value: '0x123' }], + texts: [], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(true) + }) + + it('should return false when there are records', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 'eth', value: '0x123' }], + texts: [], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(true) + }) + + it('should return false when there are records', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 60, value: '0x123' }], + texts: [], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(true) + }) + + it('should return false when there are records', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 'ETH', value: '0x123' }], + texts: [], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(true) + }) + + it('should return false when there are records', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [ + { coin: 'eth', value: '0x123' }, + { coin: 'btc', value: '0x123' }, + ], + texts: [], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(false) + }) + + it('should return false when there are records', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 'ETH', value: '0x123' }], + texts: [], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(true) + }) + + it('should return false when there are records', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 'ETH', value: '0x123' }], + texts: [{ key: 'test', value: 'test' }], + contentHash: '', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(false) + }) + + it('should return false when there are fuses', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: ['CANNOT_APPROVE'], + unnamed: [], + }, + records: { + coins: [{ coin: 'eth', value: '0x123' }], + texts: [], + contentHash: '0xcontenthash', + }, + reverseRecord: false, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(false) + }) + + it('should return false when there is a reverse record', () => { + const params = { + name: 'test', + owner: '0x123', + secret: '0x123', + duration: 100, + fuses: { + named: [], + unnamed: [], + }, + records: { + coins: [{ coin: 'eth', value: '0x123' }], + texts: [], + contentHash: '', + }, + reverseRecord: true, + } as RegistrationParameters + expect(isLegacyRegistration(params)).toBe(false) + }) +}) diff --git a/src/utils/registration/isLegacyRegistration.ts b/src/utils/registration/isLegacyRegistration.ts new file mode 100644 index 000000000..5d574d70b --- /dev/null +++ b/src/utils/registration/isLegacyRegistration.ts @@ -0,0 +1,29 @@ +import { RegistrationParameters } from '@ensdomains/ensjs/utils' + +const hasFuses = ({ fuses }: RegistrationParameters) => { + const hasNamedFuses = fuses?.named && fuses.named.length > 0 + const hasUnnameFuses = fuses?.unnamed && fuses.unnamed.length > 0 + return hasNamedFuses || hasUnnameFuses +} + +const hasRecords = ({ records }: RegistrationParameters) => { + const hasAddressRecords = + records?.coins && + records.coins?.filter(({ coin }) => { + const cond1 = typeof coin === 'string' && coin.toLowerCase() !== 'eth' + const cond2 = typeof coin === 'number' && coin !== 60 + return cond1 || cond2 + }).length > 0 + const hasTextRecord = !!records?.texts && records.texts.length > 0 + const hasContentHash = !!records?.contentHash + return hasAddressRecords || hasTextRecord || hasContentHash +} + +export const isLegacyRegistration = (params: RegistrationParameters) => { + console.log('isLegacyRegistration', params) + console.log('hasRecord', hasRecords(params)) + console.log('hasFuses', hasFuses(params)) + console.log('reverseRecord', params.reverseRecord) + const test = !hasRecords(params) && !hasFuses(params) && !params.reverseRecord + return test +} diff --git a/src/utils/registration/makeLegacyRegistrationParams.test.ts b/src/utils/registration/makeLegacyRegistrationParams.test.ts new file mode 100644 index 000000000..8dac02e6a --- /dev/null +++ b/src/utils/registration/makeLegacyRegistrationParams.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, expectTypeOf, it } from 'vitest' + +import { makeLegacyRegistrationParams } from './makeLegacyRegistrationParams' +import { RegistrationParameters } from '@ensdomains/ensjs/utils' + +describe('makeLegacyRegistrationParams', () => { + it('should return base legacy registration parameters', () => { + const params: RegistrationParameters = { + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + } + + expect(makeLegacyRegistrationParams(params)).toEqual({ + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + }) + }) + + it('should return legacy registration parameters with resolverAddress and address if resolverAddress exists', () => { + const params: RegistrationParameters = { + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + resolverAddress: '0xresolverAddress', + } + + expect(makeLegacyRegistrationParams(params)).toEqual({ + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + resolverAddress: '0xresolverAddress', + address: '0xowner', + }) + }) + + it('should return address from eth record if it exists', () => { + const params: RegistrationParameters = { + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + resolverAddress: '0xresolverAddress', + records: { + coins: [{ coin: 'eth', value: '0xother' }], + } + } + + expect(makeLegacyRegistrationParams(params)).toEqual({ + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + resolverAddress: '0xresolverAddress', + address: '0xother', + }) + }) + + it('should not return address if resolverAddress is undefined', () => { + const params: RegistrationParameters = { + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + records: { + coins: [{ coin: 'eth', value: '0xother' }], + } + } + + expect(makeLegacyRegistrationParams(params)).toEqual({ + name: 'test', + owner: '0xowner', + duration: 100, + secret: '0xsecret', + }) + }) +}) diff --git a/src/utils/registration/makeLegacyRegistrationParams.ts b/src/utils/registration/makeLegacyRegistrationParams.ts new file mode 100644 index 000000000..3fd01e9af --- /dev/null +++ b/src/utils/registration/makeLegacyRegistrationParams.ts @@ -0,0 +1,34 @@ +import { Address } from 'viem' + +import { LegacyRegistrationParameters, RegistrationParameters } from '@ensdomains/ensjs/utils' + +import { isEthCoin } from '../coin' +import { emptyAddress } from '../constants' + +export const makeLegacyRegistrationParams = ({ + name, + owner, + records, + duration, + secret, + resolverAddress = emptyAddress, +}: RegistrationParameters): LegacyRegistrationParameters => { + if (resolverAddress === emptyAddress) + return { + name, + owner, + duration, + secret, + } + + const address = (records?.coins?.find(({ coin }) => isEthCoin(coin))?.value as Address) || owner + + return { + name, + owner, + duration, + secret, + resolverAddress, + address, + } +} From 442ccde3d295ebe9d9368ac896f6eb665968ccd9 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Thu, 9 Jan 2025 14:14:52 +0800 Subject: [PATCH 2/9] update e2e tests --- e2e/specs/stateless/registerName.spec.ts | 58 +++++++++++++++++-- .../chain/useEstimateGasWithStateOverride.ts | 3 +- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 38d86e3d5..be144a316 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -60,6 +60,8 @@ test.describe.serial('normal registration', () => { await homePage.goto() await login.connect() + await page.pause() + await test.step('should redirect to registration page', async () => { await homePage.searchInput.fill(name) await page.locator(`[data-testid="search-result-name"]`, { hasText: name }).waitFor() @@ -233,8 +235,11 @@ test.describe.serial('normal registration', () => { await expect(page.getByText(`You are now the owner of ${name}`)).toBeVisible() // calculate date one year from now - const date = new Date() - date.setFullYear(date.getFullYear() + 1) + const date = await page.evaluate(() => { + const _date = new Date() + _date.setFullYear(_date.getFullYear() + 1) + return _date + }) const formattedDate = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', @@ -255,6 +260,10 @@ test.describe.serial('normal registration', () => { accounts.getAddress('user', 5), ) }) + + await test.step('confirm name is wrapped', async () => { + await expect(page.getByTestId('permissions-tab')).toBeVisible() + }) }) test('should not direct to the registration page on search, and show all records from registration', async ({ @@ -326,6 +335,11 @@ test.describe.serial('normal registration', () => { await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) }) @@ -370,6 +384,10 @@ test('should allow registering a premium name', async ({ await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) + + await test.step('confirm name is unwrapped', async () => { + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) test('should allow registering a name and resuming from the commit toast', async ({ @@ -458,9 +476,9 @@ test('should allow registering with a specific date', async ({ page, login, make await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() await expect(registrationPage.primaryNameToggle).not.toBeChecked() - await test.step('should show correct price data (for 2.5 years)', async () => { - await expect(registrationPage.yearMarker(0)).toHaveText(/11% gas/) - await expect(registrationPage.yearMarker(1)).toHaveText(/6% gas/) + await test.step('should show correct price marker data for unwrapped registration (for 2.5 years)', async () => { + await expect(registrationPage.yearMarker(0)).toHaveText(/9% gas/) + await expect(registrationPage.yearMarker(1)).toHaveText(/5% gas/) await expect(registrationPage.yearMarker(2)).toHaveText(/2% gas/) }) }) @@ -533,6 +551,11 @@ test('should allow registering a premium name with a specific date', async ({ await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) test('should allow registering a premium name for two months', async ({ @@ -601,6 +624,11 @@ test('should allow registering a premium name for two months', async ({ await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) test('should not allow registering a premium name for less than 28 days', async ({ @@ -680,6 +708,11 @@ test('should not allow registering a premium name for less than 28 days', async await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) test('should allow normal registration for a month', async ({ @@ -800,6 +833,11 @@ test('should allow normal registration for a month', async ({ await expect(page.getByTestId('address-profile-button-eth')).toHaveText( accounts.getAddress('user', 5), ) + + await test.step('confirm name is wrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).toBeVisible() + }) }) test('should not allow normal registration less than 28 days', async ({ @@ -931,6 +969,11 @@ test('should not allow normal registration less than 28 days', async ({ await expect(page.getByTestId('address-profile-button-eth')).toHaveText( accounts.getAddress('user', 5), ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) test('should be able to detect an existing commit created on a private mempool', async ({ @@ -1031,6 +1074,11 @@ test('should be able to detect an existing commit created on a private mempool', await expect(page.getByTestId('address-profile-button-eth')).toHaveText( accounts.getAddress('user', 5), ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) }) }) diff --git a/src/hooks/chain/useEstimateGasWithStateOverride.ts b/src/hooks/chain/useEstimateGasWithStateOverride.ts index 1614da6a5..4d4da63f7 100644 --- a/src/hooks/chain/useEstimateGasWithStateOverride.ts +++ b/src/hooks/chain/useEstimateGasWithStateOverride.ts @@ -295,9 +295,10 @@ export const useEstimateGasWithStateOverride = < const data = useMemo(() => { if (!gasPrice || !query.data) { + const transactions = params.transactions ?? [] return { gasEstimate: 0n, - gasEstimateArray: params.transactions.map(() => 0n) as GasEstimateArray, + gasEstimateArray: transactions.map(() => 0n) as GasEstimateArray, gasCost: 0n, gasCostEth: '0', } From e07aac036e5675674ab42db71dc147d6cf429c18 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Fri, 10 Jan 2025 23:36:05 +0800 Subject: [PATCH 3/9] update useSimulateRegistration for legacy eth controller --- e2e/specs/stateless/registerName.spec.ts | 2 + package.json | 2 +- pnpm-lock.yaml | 10 +-- .../registration/useSimulateRegistration.ts | 64 +++++++++++++++++-- .../makeLegacyRegistrationParams.ts | 17 ++--- 5 files changed, 72 insertions(+), 23 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index be144a316..69b401c3b 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -689,6 +689,7 @@ test('should not allow registering a premium name for less than 28 days', async expect(page.getByText('28 days registration', { exact: true })).toBeVisible() }) + await page.pause() await page.getByTestId('payment-choice-ethereum').click() await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() await page.getByTestId('next-button').click() @@ -1172,6 +1173,7 @@ test.describe('Error handling', () => { }) await test.step('transaction: commit', async () => { + await page.pause() await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() await expect(page.getByText(`Your "Start timer" transaction was successful`)).toBeVisible() diff --git a/package.json b/package.json index bd5b9c9b3..7fc82f698 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@ensdomains/address-encoder": "1.1.1", "@ensdomains/content-hash": "^3.0.0-beta.5", "@ensdomains/ens-contracts": "1.2.0-beta.0", - "@ensdomains/ensjs": "4.0.3-alpha.0", + "@ensdomains/ensjs": "4.0.3-alpha.11", "@ensdomains/thorin": "0.6.50", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^14.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ddaa727d..8d2de77ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,8 +125,8 @@ importers: specifier: 1.2.0-beta.0 version: 1.2.0-beta.0 '@ensdomains/ensjs': - specifier: 4.0.3-alpha.0 - version: 4.0.3-alpha.0(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + specifier: 4.0.3-alpha.11 + version: 4.0.3-alpha.11(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) '@ensdomains/thorin': specifier: 0.6.50 version: 0.6.50(react-dom@18.3.1(react@18.3.1))(react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)) @@ -1672,8 +1672,8 @@ packages: '@ensdomains/ensjs@2.1.0': resolution: {integrity: sha512-GRbGPT8Z/OJMDuxs75U/jUNEC0tbL0aj7/L/QQznGYKm/tiasp+ndLOaoULy9kKJFC0TBByqfFliEHDgoLhyog==} - '@ensdomains/ensjs@4.0.3-alpha.0': - resolution: {integrity: sha512-BSGzy86XnNxNhP3zEdo5Nbb1dufsarTD4WO4G/I87Ux9v5CAwiIJZ8n0cNHpJwh3SZDDLZotZFVf1LIMQgcNuA==} + '@ensdomains/ensjs@4.0.3-alpha.11': + resolution: {integrity: sha512-+cp9TLY04n1jvO6ZpmH3/IJjdunmbMiUg4H6r2pYRC/NU9WzYsAhvOCl93KPMXJL/73btnuwG1I/aoZjtcagqA==} peerDependencies: viem: ^2.9.2 @@ -11603,7 +11603,7 @@ snapshots: - bufferutil - utf-8-validate - '@ensdomains/ensjs@4.0.3-alpha.0(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': + '@ensdomains/ensjs@4.0.3-alpha.11(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': dependencies: '@adraffy/ens-normalize': 1.10.1 '@ensdomains/address-encoder': 1.1.1 diff --git a/src/hooks/registration/useSimulateRegistration.ts b/src/hooks/registration/useSimulateRegistration.ts index 962e311ea..e133b095c 100644 --- a/src/hooks/registration/useSimulateRegistration.ts +++ b/src/hooks/registration/useSimulateRegistration.ts @@ -1,8 +1,18 @@ +import { Address } from 'viem' import { usePublicClient, useSimulateContract, UseSimulateContractParameters } from 'wagmi' -import { ethRegistrarControllerRegisterSnippet } from '@ensdomains/ensjs/contracts' -import { makeRegistrationTuple, RegistrationParameters } from '@ensdomains/ensjs/utils' +import { + ethRegistrarControllerRegisterSnippet, + legacyEthRegistrarControllerRegisterWithConfigSnippet, +} from '@ensdomains/ensjs/contracts' +import { + makeLegacyRegistrationWithConfigTuple, + makeRegistrationTuple, + RegistrationParameters, +} from '@ensdomains/ensjs/utils' +import { isLegacyRegistration } from '@app/utils/registration/isLegacyRegistration' +import { makeLegacyRegistrationParams } from '@app/utils/registration/makeLegacyRegistrationParams' import { calculateValueWithBuffer } from '@app/utils/utils' import { usePrice } from '../ensjs/public/usePrice' @@ -11,6 +21,46 @@ type UseSimulateRegistrationParameters = Pick + +type UseSimulateLegacyEthRegistrarControllerRegisterReturnType = UseSimulateContractParameters< + typeof legacyEthRegistrarControllerRegisterWithConfigSnippet, + 'registerWithConfig' +> + +type MakeSimulateRegistrationParamsReturnType = + | UseSimulateEthRegistrarControllerRegisterReturnType + | UseSimulateLegacyEthRegistrarControllerRegisterReturnType + +export const makeSimulateRegistrationParams = ({ + registrationParams, + ensEthRegistrarControllerAddress, + legacyEthRegistrarControllerAddress, +}: { + registrationParams: RegistrationParameters + ensEthRegistrarControllerAddress: Address + legacyEthRegistrarControllerAddress: Address +}): MakeSimulateRegistrationParamsReturnType => { + if (isLegacyRegistration(registrationParams)) { + return { + abi: legacyEthRegistrarControllerRegisterWithConfigSnippet, + address: legacyEthRegistrarControllerAddress, + functionName: 'registerWithConfig', + args: makeLegacyRegistrationWithConfigTuple(makeLegacyRegistrationParams(registrationParams)), + } + } + + return { + abi: ethRegistrarControllerRegisterSnippet, + address: ensEthRegistrarControllerAddress, + functionName: 'register', + args: makeRegistrationTuple(registrationParams), + } +} + export const useSimulateRegistration = ({ registrationParams, query, @@ -27,10 +77,12 @@ export const useSimulateRegistration = ({ const value = base + premium return useSimulateContract({ - abi: ethRegistrarControllerRegisterSnippet, - address: client.chain.contracts.ensEthRegistrarController.address, - functionName: 'register', - args: makeRegistrationTuple(registrationParams), + ...makeSimulateRegistrationParams({ + registrationParams, + ensEthRegistrarControllerAddress: client.chain.contracts.ensEthRegistrarController.address, + legacyEthRegistrarControllerAddress: + client.chain.contracts.legacyEthRegistrarController.address, + }), value: calculateValueWithBuffer(value), query, }) diff --git a/src/utils/registration/makeLegacyRegistrationParams.ts b/src/utils/registration/makeLegacyRegistrationParams.ts index 3fd01e9af..6b1231ee7 100644 --- a/src/utils/registration/makeLegacyRegistrationParams.ts +++ b/src/utils/registration/makeLegacyRegistrationParams.ts @@ -1,6 +1,9 @@ import { Address } from 'viem' -import { LegacyRegistrationParameters, RegistrationParameters } from '@ensdomains/ensjs/utils' +import { + LegacyRegistrationWithConfigParameters, + RegistrationParameters, +} from '@ensdomains/ensjs/utils' import { isEthCoin } from '../coin' import { emptyAddress } from '../constants' @@ -12,15 +15,7 @@ export const makeLegacyRegistrationParams = ({ duration, secret, resolverAddress = emptyAddress, -}: RegistrationParameters): LegacyRegistrationParameters => { - if (resolverAddress === emptyAddress) - return { - name, - owner, - duration, - secret, - } - +}: RegistrationParameters): LegacyRegistrationWithConfigParameters => { const address = (records?.coins?.find(({ coin }) => isEthCoin(coin))?.value as Address) || owner return { @@ -30,5 +25,5 @@ export const makeLegacyRegistrationParams = ({ secret, resolverAddress, address, - } + } as LegacyRegistrationWithConfigParameters } From 65e15bcae3c5d44656907040459b2e84024607cc Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 13 Jan 2025 16:05:06 +0800 Subject: [PATCH 4/9] fix for finding existing commit --- e2e/specs/stateless/registerName.spec.ts | 144 +++++++++++++-- .../registration/steps/Transactions.tsx | 11 +- .../registration/useExistingCommitment.ts | 169 +++++++++++++++++- .../registration/isLegacyRegistration.ts | 8 +- 4 files changed, 306 insertions(+), 26 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 69b401c3b..edd84704b 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -1,7 +1,10 @@ import { expect } from '@playwright/test' -import { Hash, isHash } from 'viem' +import { Hash } from 'viem' -import { ethRegistrarControllerCommitSnippet } from '@ensdomains/ensjs/contracts' +import { + ethRegistrarControllerCommitSnippet, + legacyEthRegistrarControllerCommitSnippet, +} from '@ensdomains/ensjs/contracts' import { setPrimaryName } from '@ensdomains/ensjs/wallet' import { Web3RequestKind } from '@ensdomains/headless-web3-provider' @@ -971,18 +974,19 @@ test('should not allow normal registration less than 28 days', async ({ accounts.getAddress('user', 5), ) - await test.step('confirm name is unwrapped', async () => { + await test.step('confirm name is wrapped (set primary name)', async () => { await page.pause() - await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + await expect(page.getByTestId('permissions-tab')).toBeVisible() }) }) -test('should be able to detect an existing commit created on a private mempool', async ({ +test('should be able to detect an existing commit created on a private mempool for a wrapped registration', async ({ page, login, accounts, time, wallet, + consoleListener, makePageObject, }) => { test.slow() @@ -991,6 +995,9 @@ test('should be able to detect an existing commit created on a private mempool', const homePage = makePageObject('HomePage') const registrationPage = makePageObject('RegistrationPage') const transactionModal = makePageObject('TransactionModal') + await consoleListener.initialize({ + regex: /commit is:/, + }) await time.sync(500) @@ -1002,28 +1009,23 @@ test('should be able to detect an existing commit created on a private mempool', await homePage.searchInput.press('Enter') await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() - await registrationPage.primaryNameToggle.uncheck() + await registrationPage.primaryNameToggle.check() // should go to profile editor step await page.getByTestId('next-button').click() + await page.getByTestId('profile-submit-button').click() + await test.step('should be able to find an existing commit', async () => { await page.getByTestId('next-button').click() await transactionModal.closeButton.click() - let commitHash: Hash | undefined - let attempts = 0 - while (!commitHash && attempts < 4) { - // eslint-disable-next-line no-await-in-loop - const message = await page.waitForEvent('console') - // eslint-disable-next-line no-await-in-loop - const txt = await message.text() - const hash = txt.split(':')[1]?.trim() as Hash - if (isHash(hash)) commitHash = hash - attempts += 1 - } - expect(commitHash!).toBeDefined() + await page.pause() + + await expect(consoleListener.getMessages().length).toBeGreaterThan(0) + const commitHash = consoleListener.getMessages()[0].split(':')[1]?.trim() as Hash + console.log('>>>>>', commitHash) const approveTx = await walletClient.writeContract({ abi: ethRegistrarControllerCommitSnippet, @@ -1078,11 +1080,115 @@ test('should be able to detect an existing commit created on a private mempool', await test.step('confirm name is unwrapped', async () => { await page.pause() - await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + await expect(page.getByTestId('permissions-tab')).toBeVisible() }) }) }) +test('should be able to detect an existing commit created on a private mempool for a legacy registration', async ({ + page, + login, + accounts, + time, + wallet, + consoleListener, + makePageObject, +}) => { + test.slow() + + const name = `registration-normal-${Date.now()}.eth` + const homePage = makePageObject('HomePage') + const registrationPage = makePageObject('RegistrationPage') + const transactionModal = makePageObject('TransactionModal') + await consoleListener.initialize({ + regex: /commit is:/, + }) + + await time.sync(500) + + await homePage.goto() + await login.connect() + + // should redirect to registration page + await homePage.searchInput.fill(name) + await homePage.searchInput.press('Enter') + await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() + + await registrationPage.primaryNameToggle.uncheck() + + // should go to profile editor step + await page.getByTestId('next-button').click() + + // await page.getByTestId('profile-submit-button').click() + + await test.step('should be able to find an existing commit', async () => { + await page.getByTestId('next-button').click() + + await transactionModal.closeButton.click() + + await expect(consoleListener.getMessages().length).toBeGreaterThan(0) + const commitHash = consoleListener.getMessages()[0].split(':')[1]?.trim() as Hash + console.log('>>>>>', commitHash) + + const approveTx = await walletClient.writeContract({ + abi: legacyEthRegistrarControllerCommitSnippet, + address: testClient.chain.contracts.legacyEthRegistrarController.address, + args: [commitHash!], + functionName: 'commit', + account: createAccounts().getAddress('user') as `0x${string}`, + }) + await waitForTransaction(approveTx) + + await page.route('https://api.findblock.xyz/**/*', async (route) => { + await route.fulfill({ + json: { + ok: true, + data: { + hash: approveTx, + }, + }, + }) + }) + + // should show countdown + await expect(page.getByTestId('countdown-circle')).toBeVisible() + await expect(page.getByTestId('countdown-circle')).toContainText(/^[0-6]?[0-9]$/) + await testClient.increaseTime({ seconds: 60 }) + + await page.pause() + await expect(page.getByTestId('countdown-complete-check')).toBeVisible({ timeout: 10000 }) + }) + + await test.step('should be able to complete registration when modal is closed', async () => { + await expect(page.getByTestId('finish-button')).toBeEnabled() + + // should save the registration state and the transaction status + await page.reload() + await expect(page.getByTestId('finish-button')).toBeEnabled() + + // should allow finalising registration and automatically go to the complete step + await page.getByTestId('finish-button').click() + await expect(page.getByText('Open Wallet')).toBeVisible() + await transactionModal.confirmButton.click() + + await transactionModal.closeButton.click() + + await expect(transactionModal.transactionModal).toHaveCount(0) + + await wallet.authorize(Web3RequestKind.SendTransaction) + // await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + accounts.getAddress('user', 5), + ) + + await test.step('confirm name is unwrapped', async () => { + await page.pause() + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) + }) +}) test.describe('Error handling', () => { test('should be able to detect an existing commit created on a private mempool', async ({ page, diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index a02e00894..48c19406e 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -4,7 +4,7 @@ import styled, { css } from 'styled-components' import { match, P } from 'ts-pattern' import { useAccount } from 'wagmi' -import { makeCommitment } from '@ensdomains/ensjs/utils' +import { makeCommitment, makeLegacyCommitment } from '@ensdomains/ensjs/utils' import { Button, CountdownCircle, Dialog, Heading, mq, Spinner } from '@ensdomains/thorin' import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' @@ -19,8 +19,10 @@ import useRegistrationParams from '@app/hooks/useRegistrationParams' import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' import { createTransactionItem } from '@app/transaction-flow/transaction' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { makeLegacyRegistrationParams } from '@app/utils/registration/makeLegacyRegistrationParams' import { ONE_DAY } from '@app/utils/time' +import { isLegacyRegistration } from '../../../../../../utils/registration/isLegacyRegistration' import { RegistrationReducerDataItem } from '../types' const PATTERNS = { @@ -241,8 +243,13 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { const commitCouldBeFound = !commitTx?.stage || commitTx.stage === 'confirm' || commitTx.stage === 'failed' + const isLegacyCommit = isLegacyRegistration(registrationParams) useExistingCommitment({ - commitment: makeCommitment(registrationParams), + registrationParams, + commitment: isLegacyCommit + ? makeLegacyCommitment(makeLegacyRegistrationParams(registrationParams)) + : makeCommitment(registrationParams), + isLegacyCommit, enabled: commitCouldBeFound, commitKey, }) diff --git a/src/hooks/registration/useExistingCommitment.ts b/src/hooks/registration/useExistingCommitment.ts index 5af4e5f8a..1619fc677 100644 --- a/src/hooks/registration/useExistingCommitment.ts +++ b/src/hooks/registration/useExistingCommitment.ts @@ -13,6 +13,8 @@ import { ethRegistrarControllerCommitmentsSnippet, ethRegistrarControllerCommitSnippet, getChainContractAddress, + legacyEthRegistrarControllerCommitmentsSnippet, + legacyEthRegistrarControllerCommitSnippet, } from '@ensdomains/ensjs/contracts' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' @@ -29,6 +31,7 @@ import { getBlockMetadataByTimestamp } from './utils/getBlockMetadataByTimestamp type UseExistingCommitmentParameters = { commitment?: Hex commitKey?: string + isLegacyCommit?: boolean } type UseExistingCommitmentInternalParameters = { @@ -121,7 +124,7 @@ const execTransactionSnippet = [ }, ] as const -const getExistingCommitmentQueryFn = +const getExistingWrappedCommitmentQueryFn = (config: ConfigWithEns) => ({ addRecentTransaction, @@ -173,6 +176,8 @@ const getExistingCommitmentQueryFn = if (commitmentAge > maxCommitmentAge) return { status: 'commitmentExpired', timestamp: commitmentTimestampNumber } as const + console.log('date', new Date(commitmentTimestampNumber)) + const blockMetadata = await getBlockMetadataByTimestamp(client, { timestamp: commitmentTimestamp, }) @@ -244,6 +249,168 @@ const getExistingCommitmentQueryFn = } as const } +const getExistingLegacyCommitmentQueryFn = + (config: ConfigWithEns) => + ({ + addRecentTransaction, + setTransactionHashFromUpdate, + isSafeTx, + }: UseExistingCommitmentInternalParameters) => + async ({ + queryKey: [{ commitment, commitKey }, chainId, address], + }: QueryFunctionContext>): Promise => { + console.log('getExistingLegacyCommitmentQueryFn', commitment, commitKey, chainId, address) + if (!commitment) throw new Error('commitment is required') + if (!commitKey) throw new Error('commitKey is required') + if (!address) throw new Error('address is required') + + const client = config.getClient({ chainId }) + const legacyEthRegistrarControllerAddress = getChainContractAddress({ + client, + contract: 'legacyEthRegistrarController', + }) + const multicall3Address = getChainContractAddress({ + client, + contract: 'multicall3', + }) + + const [commitmentTimestamp, maxCommitmentAge, blockTimestamp] = await Promise.all([ + readContract(client, { + abi: legacyEthRegistrarControllerCommitmentsSnippet, + address: legacyEthRegistrarControllerAddress, + functionName: 'commitments', + args: [commitment], + }), + readContract(client, { + abi: maxCommitmentAgeSnippet, + address: legacyEthRegistrarControllerAddress, + functionName: 'maxCommitmentAge', + }), + readContract(client, { + abi: getCurrentBlockTimestampSnippet, + address: multicall3Address, + functionName: 'getCurrentBlockTimestamp', + }), + ]) + console.log('>>>>>>>>>>>>>>>', commitmentTimestamp, maxCommitmentAge, blockTimestamp) + if (!commitmentTimestamp || commitmentTimestamp === 0n) return null + + const commitmentAge = blockTimestamp - commitmentTimestamp + const commitmentTimestampNumber = Number(commitmentTimestamp) + + console.log('date', new Date(commitmentTimestampNumber * 1000)) + + const existsFailure = () => + ({ status: 'commitmentExists', timestamp: commitmentTimestampNumber }) as const + + if (commitmentAge > maxCommitmentAge) + return { status: 'commitmentExpired', timestamp: commitmentTimestampNumber } as const + + const blockMetadata = await getBlockMetadataByTimestamp(client, { + timestamp: commitmentTimestamp, + }) + if (!blockMetadata.ok) return existsFailure() + + const blockData = await getBlock(client, { + blockHash: blockMetadata.data.hash, + includeTransactions: true, + }).catch(() => null) + console.log('>>>>>>>>>>>>>>> blockData <<<<<<<<<<<<<', blockData) + if (!blockData) return existsFailure() + + const inputData = encodeFunctionData({ + abi: legacyEthRegistrarControllerCommitSnippet, + args: [commitment], + functionName: 'commit', + }) + + const transaction = (() => { + const checksummedAddress = getAddress(address) + const checksummedEthRegistrarControllerAddress = getAddress( + legacyEthRegistrarControllerAddress, + ) + if (isSafeTx) { + const execTransactionFunctionSelector = toFunctionSelector(execTransactionSnippet[0]) + const foundTransaction = blockData.transactions.find((t) => { + // safe transaction gets sent to the safe contract itself + if (!t.to || getAddress(t.to) !== checksummedAddress) return false + if (!t.input.startsWith(execTransactionFunctionSelector)) return false + const { args: safeTxData } = decodeFunctionData({ + abi: execTransactionSnippet, + data: t.input, + }) + if (getAddress(safeTxData[0]) !== checksummedEthRegistrarControllerAddress) return false + if (getAddress(safeTxData[2]) !== inputData) return false + return true + }) + return foundTransaction + } + const foundTransaction = blockData.transactions.find((t) => { + if (getAddress(t.from) !== checksummedAddress) return false + if (!t.to || getAddress(t.to) !== checksummedEthRegistrarControllerAddress) return false + if (t.input !== inputData) return false + return true + }) + return foundTransaction + })() + + console.log('>>>>>>>>>>>>>>> transaction <<<<<<<<<<<<<', transaction) + if (!transaction) return existsFailure() + + const transactionReceipt = await getTransactionReceipt(client, { + hash: transaction.hash, + }) + + if (transactionReceipt.status !== 'success') return existsFailure() + + setTransactionHashFromUpdate(commitKey, transaction.hash) + addRecentTransaction({ + ...transaction, + hash: transaction.hash, + action: 'commitName', + key: commitKey, + input: inputData, + timestamp: commitmentTimestampNumber, + isSafeTx, + searchRetries: 0, + }) + + return { + status: 'transactionExists', + timestamp: commitmentTimestampNumber, + } as const + } + +const getExistingCommitmentQueryFn = + (config: ConfigWithEns) => + ({ + addRecentTransaction, + setTransactionHashFromUpdate, + isSafeTx, + }: UseExistingCommitmentInternalParameters) => + async ( + context: QueryFunctionContext>, + ): Promise => { + const { + queryKey: [{ commitment, commitKey, isLegacyCommit }, , address], + } = context + if (!commitment) throw new Error('commitment is required') + if (!commitKey) throw new Error('commitKey is required') + if (!address) throw new Error('address is required') + + if (isLegacyCommit) + return getExistingLegacyCommitmentQueryFn(config)({ + addRecentTransaction, + setTransactionHashFromUpdate, + isSafeTx, + })(context) + return getExistingWrappedCommitmentQueryFn(config)({ + addRecentTransaction, + setTransactionHashFromUpdate, + isSafeTx, + })(context) + } + export const useExistingCommitment = ({ // config enabled = true, diff --git a/src/utils/registration/isLegacyRegistration.ts b/src/utils/registration/isLegacyRegistration.ts index 5d574d70b..267bf6259 100644 --- a/src/utils/registration/isLegacyRegistration.ts +++ b/src/utils/registration/isLegacyRegistration.ts @@ -20,10 +20,10 @@ const hasRecords = ({ records }: RegistrationParameters) => { } export const isLegacyRegistration = (params: RegistrationParameters) => { - console.log('isLegacyRegistration', params) - console.log('hasRecord', hasRecords(params)) - console.log('hasFuses', hasFuses(params)) - console.log('reverseRecord', params.reverseRecord) + // console.log('isLegacyRegistration', params) + // console.log('hasRecord', hasRecords(params)) + // console.log('hasFuses', hasFuses(params)) + // console.log('reverseRecord', params.reverseRecord) const test = !hasRecords(params) && !hasFuses(params) && !params.reverseRecord return test } From cd8b7c67a490b355c2cfe490b7c0589fe041f6eb Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Tue, 14 Jan 2025 00:45:14 +0800 Subject: [PATCH 5/9] fix unit tests --- e2e/specs/stateless/registerName.spec.ts | 7 ++-- pnpm-lock.yaml | 2 +- .../transaction/registerName.test.ts | 4 +- .../registration/isLegacyRegistration.ts | 9 +++-- .../makeLegacyRegistrationParams.test.ts | 37 +------------------ 5 files changed, 14 insertions(+), 45 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index edd84704b..1da9c1283 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -536,6 +536,8 @@ test('should allow registering a premium name with a specific date', async ({ }) await page.getByTestId('payment-choice-ethereum').click() + await page.getByTestId('primary-name-toggle').check() + await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() await page.getByTestId('next-button').click() if (await page.getByTestId('profile-submit-button').isVisible()) { @@ -555,9 +557,8 @@ test('should allow registering a premium name with a specific date', async ({ new RegExp(accounts.getAddress('user', 5)), ) - await test.step('confirm name is unwrapped', async () => { - await page.pause() - await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + await test.step('confirm name is wrapped', async () => { + await expect(page.getByTestId('permissions-tab')).toBeVisible() }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d2de77ae..8a02e6a56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12460,7 +12460,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.1.0 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 '@scure/base': 1.1.6 '@types/debug': 4.1.12 debug: 4.3.6(supports-color@5.5.0) diff --git a/src/transaction-flow/transaction/registerName.test.ts b/src/transaction-flow/transaction/registerName.test.ts index d0e8ed825..e6ad2968f 100644 --- a/src/transaction-flow/transaction/registerName.test.ts +++ b/src/transaction-flow/transaction/registerName.test.ts @@ -3,7 +3,7 @@ import { mockFunction } from '@app/test-utils' import { expect, it, vi } from 'vitest' import { getPrice } from '@ensdomains/ensjs/public' -import { registerName } from '@ensdomains/ensjs/wallet' +import { registerName, legacyRegisterName } from '@ensdomains/ensjs/wallet' import registerNameFlowTransaction from './registerName' @@ -12,9 +12,11 @@ vi.mock('@ensdomains/ensjs/wallet') const mockGetPrice = mockFunction(getPrice) const mockRegisterName = mockFunction(registerName.makeFunctionData) +const mockLegacyRegisterName = mockFunction(legacyRegisterName.makeFunctionData) mockGetPrice.mockImplementation(async () => ({ base: 100n, premium: 0n })) mockRegisterName.mockImplementation((...args: any[]) => args as any) +mockLegacyRegisterName.mockImplementation((...args: any[]) => args as any) it('adds a 2% value buffer to the transaction from the real price', async () => { const result = (await registerNameFlowTransaction.transaction({ diff --git a/src/utils/registration/isLegacyRegistration.ts b/src/utils/registration/isLegacyRegistration.ts index 267bf6259..fb3287af9 100644 --- a/src/utils/registration/isLegacyRegistration.ts +++ b/src/utils/registration/isLegacyRegistration.ts @@ -20,10 +20,11 @@ const hasRecords = ({ records }: RegistrationParameters) => { } export const isLegacyRegistration = (params: RegistrationParameters) => { - // console.log('isLegacyRegistration', params) - // console.log('hasRecord', hasRecords(params)) - // console.log('hasFuses', hasFuses(params)) - // console.log('reverseRecord', params.reverseRecord) + console.log('isLegacyRegistration', params) + console.log('hasRecord', hasRecords(params)) + console.log('hasFuses', hasFuses(params)) + console.log('reverseRecord', params.reverseRecord) const test = !hasRecords(params) && !hasFuses(params) && !params.reverseRecord + console.log('test', test) return test } diff --git a/src/utils/registration/makeLegacyRegistrationParams.test.ts b/src/utils/registration/makeLegacyRegistrationParams.test.ts index 8dac02e6a..9f65dffd1 100644 --- a/src/utils/registration/makeLegacyRegistrationParams.test.ts +++ b/src/utils/registration/makeLegacyRegistrationParams.test.ts @@ -4,23 +4,7 @@ import { makeLegacyRegistrationParams } from './makeLegacyRegistrationParams' import { RegistrationParameters } from '@ensdomains/ensjs/utils' describe('makeLegacyRegistrationParams', () => { - it('should return base legacy registration parameters', () => { - const params: RegistrationParameters = { - name: 'test', - owner: '0xowner', - duration: 100, - secret: '0xsecret', - } - - expect(makeLegacyRegistrationParams(params)).toEqual({ - name: 'test', - owner: '0xowner', - duration: 100, - secret: '0xsecret', - }) - }) - - it('should return legacy registration parameters with resolverAddress and address if resolverAddress exists', () => { + it('should return owner as address if no eth record exists', () => { const params: RegistrationParameters = { name: 'test', owner: '0xowner', @@ -60,23 +44,4 @@ describe('makeLegacyRegistrationParams', () => { address: '0xother', }) }) - - it('should not return address if resolverAddress is undefined', () => { - const params: RegistrationParameters = { - name: 'test', - owner: '0xowner', - duration: 100, - secret: '0xsecret', - records: { - coins: [{ coin: 'eth', value: '0xother' }], - } - } - - expect(makeLegacyRegistrationParams(params)).toEqual({ - name: 'test', - owner: '0xowner', - duration: 100, - secret: '0xsecret', - }) - }) }) From e8cd4a24047474536e5baf1180a9d25e4497f8b9 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Tue, 14 Jan 2025 02:29:35 +0800 Subject: [PATCH 6/9] clean up and update ensjs next --- package.json | 2 +- pnpm-lock.yaml | 10 +-- .../gasEstimation/useEstimateRegistration.ts | 3 - ...useRegistrationValueFromRegisterReceipt.ts | 65 ++----------------- .../registration/useExistingCommitment.ts | 8 --- .../registration/isLegacyRegistration.ts | 8 +-- 6 files changed, 14 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index 7fc82f698..3a2ace533 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@ensdomains/address-encoder": "1.1.1", "@ensdomains/content-hash": "^3.0.0-beta.5", "@ensdomains/ens-contracts": "1.2.0-beta.0", - "@ensdomains/ensjs": "4.0.3-alpha.11", + "@ensdomains/ensjs": "4.0.3-alpha.12", "@ensdomains/thorin": "0.6.50", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^14.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a02e6a56..406847b67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,8 +125,8 @@ importers: specifier: 1.2.0-beta.0 version: 1.2.0-beta.0 '@ensdomains/ensjs': - specifier: 4.0.3-alpha.11 - version: 4.0.3-alpha.11(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + specifier: 4.0.3-alpha.12 + version: 4.0.3-alpha.12(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) '@ensdomains/thorin': specifier: 0.6.50 version: 0.6.50(react-dom@18.3.1(react@18.3.1))(react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)) @@ -1672,8 +1672,8 @@ packages: '@ensdomains/ensjs@2.1.0': resolution: {integrity: sha512-GRbGPT8Z/OJMDuxs75U/jUNEC0tbL0aj7/L/QQznGYKm/tiasp+ndLOaoULy9kKJFC0TBByqfFliEHDgoLhyog==} - '@ensdomains/ensjs@4.0.3-alpha.11': - resolution: {integrity: sha512-+cp9TLY04n1jvO6ZpmH3/IJjdunmbMiUg4H6r2pYRC/NU9WzYsAhvOCl93KPMXJL/73btnuwG1I/aoZjtcagqA==} + '@ensdomains/ensjs@4.0.3-alpha.12': + resolution: {integrity: sha512-tJX+URJtSOtLgydE4V6uLOuG7hrMDah/ZOA2u/0hZuzhPwpVhS1jQRXU1aVnyT3klaLH5QqRL2J3SebzdxqGLQ==} peerDependencies: viem: ^2.9.2 @@ -11603,7 +11603,7 @@ snapshots: - bufferutil - utf-8-validate - '@ensdomains/ensjs@4.0.3-alpha.11(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': + '@ensdomains/ensjs@4.0.3-alpha.12(encoding@0.1.13)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': dependencies: '@adraffy/ens-normalize': 1.10.1 '@ensdomains/address-encoder': 1.1.1 diff --git a/src/hooks/gasEstimation/useEstimateRegistration.ts b/src/hooks/gasEstimation/useEstimateRegistration.ts index 7b8dff5b2..d03eea2f7 100644 --- a/src/hooks/gasEstimation/useEstimateRegistration.ts +++ b/src/hooks/gasEstimation/useEstimateRegistration.ts @@ -40,9 +40,6 @@ export const useEstimateFullRegistration = ({ contract: 'legacyEthRegistrarController', }) - console.log('ethRegistrarControllerAddress', ethRegistrarControllerAddress) - console.log('legacyEthRegistrarControllerAddress', legacyEthRegistrarControllerAddress) - const { data: blockTimestamp } = useBlockTimestamp() // default to use block timestamp as reference // if no block timestamp, use local time as fallback diff --git a/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts b/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts index 34de823b4..f0001137f 100644 --- a/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts +++ b/src/hooks/pages/register/useRegistrationValueFromRegisterReceipt.ts @@ -1,28 +1,18 @@ import { decodeEventLog } from 'viem' +import { + ethRegistrarControllerNameRegisteredEventSnippet, + legacyEthRegistrarControllerNameRegisteredEventSnippet, +} from '@ensdomains/ensjs/contracts' + import { MinedData } from '@app/types' import { useQuery } from '@app/utils/query/useQuery' -const legacyNameRegisteredEventSnippet = [ - { - 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 - const decodeLegacyNameRegisteredEventLog = (log: MinedData['logs'][number]): Promise => new Promise((resolve, reject) => { try { const t = decodeEventLog({ - abi: legacyNameRegisteredEventSnippet, + abi: legacyEthRegistrarControllerNameRegisteredEventSnippet, topics: log.topics, data: log.data, eventName: 'NameRegistered', @@ -34,52 +24,11 @@ const decodeLegacyNameRegisteredEventLog = (log: MinedData['logs'][number]): Pro } }) -const nameRegisteredEventSnippet = [ - { - 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 - const decodeWrappedNameRegisteredEventLog = (log: MinedData['logs'][number]): Promise => new Promise((resolve, reject) => { try { const t = decodeEventLog({ - abi: nameRegisteredEventSnippet, + abi: ethRegistrarControllerNameRegisteredEventSnippet, topics: log.topics, data: log.data, eventName: 'NameRegistered', diff --git a/src/hooks/registration/useExistingCommitment.ts b/src/hooks/registration/useExistingCommitment.ts index 1619fc677..78a33ea9f 100644 --- a/src/hooks/registration/useExistingCommitment.ts +++ b/src/hooks/registration/useExistingCommitment.ts @@ -176,8 +176,6 @@ const getExistingWrappedCommitmentQueryFn = if (commitmentAge > maxCommitmentAge) return { status: 'commitmentExpired', timestamp: commitmentTimestampNumber } as const - console.log('date', new Date(commitmentTimestampNumber)) - const blockMetadata = await getBlockMetadataByTimestamp(client, { timestamp: commitmentTimestamp, }) @@ -259,7 +257,6 @@ const getExistingLegacyCommitmentQueryFn = async ({ queryKey: [{ commitment, commitKey }, chainId, address], }: QueryFunctionContext>): Promise => { - console.log('getExistingLegacyCommitmentQueryFn', commitment, commitKey, chainId, address) if (!commitment) throw new Error('commitment is required') if (!commitKey) throw new Error('commitKey is required') if (!address) throw new Error('address is required') @@ -292,14 +289,11 @@ const getExistingLegacyCommitmentQueryFn = functionName: 'getCurrentBlockTimestamp', }), ]) - console.log('>>>>>>>>>>>>>>>', commitmentTimestamp, maxCommitmentAge, blockTimestamp) if (!commitmentTimestamp || commitmentTimestamp === 0n) return null const commitmentAge = blockTimestamp - commitmentTimestamp const commitmentTimestampNumber = Number(commitmentTimestamp) - console.log('date', new Date(commitmentTimestampNumber * 1000)) - const existsFailure = () => ({ status: 'commitmentExists', timestamp: commitmentTimestampNumber }) as const @@ -315,7 +309,6 @@ const getExistingLegacyCommitmentQueryFn = blockHash: blockMetadata.data.hash, includeTransactions: true, }).catch(() => null) - console.log('>>>>>>>>>>>>>>> blockData <<<<<<<<<<<<<', blockData) if (!blockData) return existsFailure() const inputData = encodeFunctionData({ @@ -354,7 +347,6 @@ const getExistingLegacyCommitmentQueryFn = return foundTransaction })() - console.log('>>>>>>>>>>>>>>> transaction <<<<<<<<<<<<<', transaction) if (!transaction) return existsFailure() const transactionReceipt = await getTransactionReceipt(client, { diff --git a/src/utils/registration/isLegacyRegistration.ts b/src/utils/registration/isLegacyRegistration.ts index fb3287af9..8e02a0d7d 100644 --- a/src/utils/registration/isLegacyRegistration.ts +++ b/src/utils/registration/isLegacyRegistration.ts @@ -20,11 +20,5 @@ const hasRecords = ({ records }: RegistrationParameters) => { } export const isLegacyRegistration = (params: RegistrationParameters) => { - console.log('isLegacyRegistration', params) - console.log('hasRecord', hasRecords(params)) - console.log('hasFuses', hasFuses(params)) - console.log('reverseRecord', params.reverseRecord) - const test = !hasRecords(params) && !hasFuses(params) && !params.reverseRecord - console.log('test', test) - return test + return !hasRecords(params) && !hasFuses(params) && !params.reverseRecord } From 0c067102f3af09bd755993561179bb9a550bc428 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Sun, 19 Jan 2025 20:01:25 +0100 Subject: [PATCH 7/9] add e2e and unit tests --- e2e/specs/stateless/registerName.spec.ts | 328 ++++++++++++++- .../utils/useExistingCommitment.test.ts | 394 ++++++++++++++++++ 2 files changed, 719 insertions(+), 3 deletions(-) create mode 100644 src/hooks/registration/utils/useExistingCommitment.test.ts diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 1da9c1283..bf7ad74fc 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -26,7 +26,7 @@ import { test.describe.serial('normal registration', () => { const name = `registration-normal-${Date.now()}.eth` - test('should allow normal registration', async ({ + test('should allow normal registration, if primary name is set, name is wrapped', async ({ page, login, accounts, @@ -297,7 +297,7 @@ test.describe.serial('normal registration', () => { ) }) - test('should allow registering a non-primary name', async ({ + test('registering a non-primary name should not be wrapped', async ({ page, accounts, time, @@ -346,7 +346,7 @@ test.describe.serial('normal registration', () => { }) }) -test('should allow registering a premium name', async ({ +test('registering a premium name with no records should not be wrapped', async ({ page, login, accounts, @@ -391,6 +391,328 @@ test('should allow registering a premium name', async ({ await test.step('confirm name is unwrapped', async () => { await expect(page.getByTestId('permissions-tab')).not.toBeVisible() }) + + const morePage = makePageObject('MorePage') + await morePage.goto(premiumName) + + await expect(morePage.wrapButton).toBeVisible() +}) + +test('registering a premium name with primary name not set should not be wrapped', async ({ + page, + login, + accounts, + makeName, + makePageObject, +}) => { + const premiumName = await makeName( + { + label: 'premium', + owner: 'user2', + type: 'legacy', + duration: -7890000 - 4 * 345600, // 3 months 4 days + }, + { timeOffset: 500 }, + ) + + await setPrimaryName(walletClient, { + name: 'premium', + account: createAccounts().getAddress('user2') as `0x${string}`, + }) + + const transactionModal = makePageObject('TransactionModal') + + await page.goto(`/${premiumName}/register`) + await login.connect() + + await page.getByTestId('payment-choice-ethereum').click() + await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() + await page.getByTestId('next-button').click() + if (await page.getByTestId('profile-submit-button').isVisible()) { + await page.getByTestId('profile-submit-button').click() + } + + await page.getByTestId('next-button').click() + await transactionModal.confirm() + + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await testClient.increaseTime({ seconds: 120 }) + await page.getByTestId('finish-button').click() + await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + new RegExp(accounts.getAddress('user', 5)), + ) + + await test.step('confirm name is wrapped', async () => { + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) + + const morePage = makePageObject('MorePage') + await morePage.goto(premiumName) + await expect(morePage.wrapButton).toBeVisible() + + const recordsPage = makePageObject('RecordsPage') + await recordsPage.goto(premiumName) + + await expect(recordsPage.getRecordValue('address', 'ETH')).toHaveText( + createAccounts().getAddress('user') as `0x${string}`, + ) +}) + +test('registering a premium name with primary name set should be wrapped', async ({ + page, + login, + accounts, + makeName, + makePageObject, +}) => { + const premiumName = await makeName( + { + label: 'premium', + owner: 'user2', + type: 'legacy', + duration: -7890000 - 4 * 345600, // 3 months 4 days + }, + { timeOffset: 500 }, + ) + + await setPrimaryName(walletClient, { + name: 'premium', + account: createAccounts().getAddress('user') as `0x${string}`, + }) + + const transactionModal = makePageObject('TransactionModal') + + await page.goto(`/${premiumName}/register`) + await login.connect() + + await page.getByTestId('payment-choice-ethereum').click() + await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() + await page.getByTestId('next-button').click() + if (await page.getByTestId('profile-submit-button').isVisible()) { + await page.getByTestId('profile-submit-button').click() + } + + await page.getByTestId('next-button').click() + await transactionModal.confirm() + + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await testClient.increaseTime({ seconds: 120 }) + await page.getByTestId('finish-button').click() + await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + new RegExp(accounts.getAddress('user', 5)), + ) + + await test.step('confirm name is wrapped', async () => { + await expect(page.getByTestId('permissions-tab')).toBeVisible() + }) + + const morePage = makePageObject('MorePage') + await morePage.goto(premiumName) + await expect(morePage.wrapButton).toHaveCount(0) + + const recordsPage = makePageObject('RecordsPage') + await recordsPage.goto(premiumName) + + await expect(recordsPage.getRecordValue('address', 'ETH')).toHaveText( + createAccounts().getAddress('user') as `0x${string}`, + ) +}) + +test('registering a premium name with existing records should not be wrapped', async ({ + page, + login, + accounts, + makeName, + makePageObject, +}) => { + const premiumName = await makeName( + { + label: 'premium', + owner: 'user2', + type: 'legacy', + records: { + coins: [ + { + coin: 'etcLegacy', + value: '0x42D63ae25990889E35F215bC95884039Ba354115', + }, + { + coin: 'ETH', + value: createAccounts().getAddress('user') as `0x${string}`, + }, + ], + }, + duration: -7890000 - 4 * 345600, // 3 months 4 days + }, + { timeOffset: 500 }, + ) + + const transactionModal = makePageObject('TransactionModal') + + await page.goto(`/${premiumName}/register`) + await login.connect() + + await page.getByTestId('payment-choice-ethereum').click() + await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() + await page.getByTestId('next-button').click() + if (await page.getByTestId('profile-submit-button').isVisible()) { + await page.getByTestId('profile-submit-button').click() + } + + await page.getByTestId('next-button').click() + await transactionModal.confirm() + + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await testClient.increaseTime({ seconds: 120 }) + await page.getByTestId('finish-button').click() + await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + new RegExp(accounts.getAddress('user', 5)), + ) + + const recordsPage = makePageObject('RecordsPage') + await recordsPage.goto(premiumName) + + await expect(recordsPage.getRecordValue('address', 'ETH')).toHaveText( + createAccounts().getAddress('user') as `0x${string}`, + ) +}) + +test('registering a wrapped premium name with no records should not be wrapped', async ({ + page, + login, + accounts, + makeName, + makePageObject, +}) => { + const premiumName = await makeName( + { + label: 'premium', + owner: 'user2', + type: 'wrapped', + duration: -7890000 - 4 * 345600, // 3 months 4 days + }, + { timeOffset: 500 }, + ) + + const transactionModal = makePageObject('TransactionModal') + + await page.goto(`/${premiumName}/register`) + await login.connect() + + await page.getByTestId('payment-choice-ethereum').click() + await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() + await page.getByTestId('next-button').click() + if (await page.getByTestId('profile-submit-button').isVisible()) { + await page.getByTestId('profile-submit-button').click() + } + + await page.getByTestId('next-button').click() + await transactionModal.confirm() + + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await testClient.increaseTime({ seconds: 120 }) + await page.getByTestId('finish-button').click() + await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + new RegExp(accounts.getAddress('user', 5)), + ) + + await test.step('confirm name is unwrapped', async () => { + await expect(page.getByTestId('permissions-tab')).not.toBeVisible() + }) + + const morePage = makePageObject('MorePage') + await morePage.goto(premiumName) + + await expect(morePage.wrapButton).toBeVisible() + + const recordsPage = makePageObject('RecordsPage') + await recordsPage.goto(premiumName) + + await expect(recordsPage.getRecordValue('address', 'ETH')).toHaveText( + createAccounts().getAddress('user') as `0x${string}`, + ) +}) + +test('registering a wrapped premium name with records set should be wrapped', async ({ + page, + login, + accounts, + makeName, + makePageObject, +}) => { + const premiumName = await makeName( + { + label: 'premium', + owner: 'user', + type: 'wrapped', + duration: -7890000 - 4 * 345600, // 3 months 4 days + records: { + coins: [ + { + coin: 'ETH', + value: createAccounts().getAddress('user') as `0x${string}`, + }, + { + coin: 'etcLegacy', + value: '0x42D63ae25990889E35F215bC95884039Ba354115', + }, + ], + }, + }, + { timeOffset: 500 }, + ) + + const transactionModal = makePageObject('TransactionModal') + + await page.goto(`/${premiumName}/register`) + await login.connect() + + await page.getByTestId('payment-choice-ethereum').click() + await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() + await page.getByTestId('next-button').click() + if (await page.getByTestId('profile-submit-button').isVisible()) { + await page.getByTestId('profile-submit-button').click() + } + + await page.getByTestId('next-button').click() + await transactionModal.confirm() + + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await testClient.increaseTime({ seconds: 120 }) + await page.getByTestId('finish-button').click() + await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + new RegExp(accounts.getAddress('user', 5)), + ) + + await test.step('confirm name is wrapped', async () => { + await expect(page.getByTestId('permissions-tab')).toBeVisible() + }) + + const morePage = makePageObject('MorePage') + await morePage.goto(premiumName) + await expect(morePage.wrapButton).toHaveCount(0) + + const recordsPage = makePageObject('RecordsPage') + await recordsPage.goto(premiumName) + + await expect(recordsPage.getRecordValue('address', 'ETH')).toHaveText( + createAccounts().getAddress('user') as `0x${string}`, + ) }) test('should allow registering a name and resuming from the commit toast', async ({ diff --git a/src/hooks/registration/utils/useExistingCommitment.test.ts b/src/hooks/registration/utils/useExistingCommitment.test.ts new file mode 100644 index 000000000..22db8ffad --- /dev/null +++ b/src/hooks/registration/utils/useExistingCommitment.test.ts @@ -0,0 +1,394 @@ +/* eslint-disable no-promise-executor-return */ +import { mockFunction, renderHook, waitFor } from '@app/test-utils' + +import { getBlock, getTransactionReceipt, readContract } from 'viem/actions' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useAccount, useBlockNumber, useChainId, useConfig, usePublicClient } from 'wagmi' + +import { useChainName } from '@app/hooks/chain/useChainName' +import { useInvalidateOnBlock } from '@app/hooks/chain/useInvalidateOnBlock' +import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTransaction' +import { useIsSafeApp } from '@app/hooks/useIsSafeApp' +import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' + +import { useExistingCommitment } from '../useExistingCommitment' +import { getBlockMetadataByTimestamp } from './getBlockMetadataByTimestamp' + +vi.mock('@app/hooks/chain/useChainName') +vi.mock('@app/hooks/transactions/useAddRecentTransaction') +vi.mock('@app/transaction-flow/TransactionFlowProvider') +vi.mock('@app/hooks/chain/useInvalidateOnBlock') +vi.mock('@app/hooks/useIsSafeApp') +vi.mock('wagmi') +vi.mock('viem/actions') +vi.mock('../utils/getBlockMetadataByTimestamp') +vi.mock('@ensdomains/ensjs/contracts', () => ({ + ethRegistrarControllerCommitSnippet: [ + { + inputs: [{ name: 'commitment', type: 'bytes32' }], + name: 'commit', + outputs: [], + type: 'function', + }, + ], + ethRegistrarControllerCommitmentsSnippet: [ + { + inputs: [{ name: 'commitment', type: 'bytes32' }], + name: 'commitments', + outputs: [{ name: '', type: 'uint256' }], + type: 'function', + }, + ], + legacyEthRegistrarControllerCommitSnippet: [ + { + inputs: [{ name: 'commitment', type: 'bytes32' }], + name: 'commit', + outputs: [], + type: 'function', + }, + ], + legacyEthRegistrarControllerCommitmentsSnippet: [ + { + inputs: [{ name: 'commitment', type: 'bytes32' }], + name: 'commitments', + outputs: [{ name: '', type: 'uint256' }], + type: 'function', + }, + ], + ethRegistrarControllerErrors: [], + ethRegistrarControllerABI: [], + ethRegistrarControllerInterface: {}, + legacyEthRegistrarControllerABI: [], + legacyEthRegistrarControllerInterface: {}, + nameWrapperErrors: [], + nameWrapperABI: [], + nameWrapperInterface: {}, + nameWrapperCommitSnippet: [ + { + inputs: [{ name: 'commitment', type: 'bytes32' }], + name: 'commit', + outputs: [], + type: 'function', + }, + ], + nameWrapperCommitmentsSnippet: [ + { + inputs: [{ name: 'commitment', type: 'bytes32' }], + name: 'commitments', + outputs: [{ name: '', type: 'uint256' }], + type: 'function', + }, + ], + getChainContractAddress: ({ client, contract }: { client: any; contract: string }) => + client.chain.contracts[contract].address, + __esModule: true, + default: {}, +})) + +const mockUseChainName = mockFunction(useChainName) +const mockUseAddRecentTransaction = mockFunction(useAddRecentTransaction) +const mockUseTransactionFlow = mockFunction(useTransactionFlow) +const mockUsePublicClient = mockFunction(usePublicClient) +const mockUseAccount = mockFunction(useAccount) +const mockUseConfig = mockFunction(useConfig) +const mockUseChainId = mockFunction(useChainId) +const mockUseBlockNumber = mockFunction(useBlockNumber) +const mockUseInvalidateOnBlock = mockFunction(useInvalidateOnBlock) +const mockUseIsSafeApp = mockFunction(useIsSafeApp) +const mockReadContract = mockFunction(readContract) +const mockGetBlock = mockFunction(getBlock) +const mockGetTransactionReceipt = mockFunction(getTransactionReceipt) +const mockGetBlockMetadataByTimestamp = mockFunction(getBlockMetadataByTimestamp) + +describe('useExistingCommitment', () => { + const mockCommitment = '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + const mockCommitKey = 'commit-test-0xaddress' + const mockAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + const mockTime = 1000n + const mockMaxAge = 300n // 5 minutes in seconds + + const mockClient = { + request: vi.fn(), + chain: { + contracts: { + ensEthRegistrarController: { address: '0xcontroller' }, + legacyEthRegistrarController: { address: '0xlegacycontroller' }, + multicall3: { address: '0xmulticall' }, + }, + }, + } + + beforeEach(() => { + vi.clearAllMocks() + mockUseChainName.mockReturnValue('mainnet') + mockUseAddRecentTransaction.mockReturnValue(vi.fn()) + mockUseTransactionFlow.mockReturnValue({ + setTransactionHashFromUpdate: vi.fn(), + }) + mockUsePublicClient.mockReturnValue(mockClient) + mockUseAccount.mockReturnValue({ address: mockAddress }) + mockUseChainId.mockReturnValue(1) + mockUseConfig.mockReturnValue({ + blockExplorers: { + default: { + name: 'Etherscan', + url: 'https://etherscan.io', + apiUrl: 'https://api.etherscan.io/api', + }, + }, + getClient: () => mockClient, + _isEns: true, + }) + mockUseBlockNumber.mockReturnValue({ data: 1234n }) + mockUseInvalidateOnBlock.mockReturnValue({ data: undefined }) + mockUseIsSafeApp.mockReturnValue({ data: false, isLoading: false }) + }) + + describe('No Existing Commitment', () => { + it('should return null when no commitment exists', async () => { + mockReadContract.mockResolvedValueOnce(0n) // commitment timestamp + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: false, + scopeKey: mockAddress, + }), + ) + + await waitFor(() => { + expect(result.current.data).toBeNull() + }) + }) + }) + + describe('Valid Commitment', () => { + it('should return commitmentExists for valid recent commitment', async () => { + mockReadContract + .mockResolvedValueOnce(mockTime) // commitment timestamp + .mockResolvedValueOnce(mockMaxAge) // max age + .mockResolvedValueOnce(mockTime + 30n) // current block timestamp (30s after commitment) + + mockGetBlockMetadataByTimestamp.mockResolvedValueOnce({ + ok: false, + }) + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: false, + scopeKey: mockAddress, + }), + ) + + await waitFor(() => { + expect(result.current.data).toEqual({ + status: 'commitmentExists', + timestamp: Number(mockTime), + }) + }) + }) + + it('should verify commitment', async () => { + mockReadContract + .mockResolvedValueOnce(mockTime) // commitment timestamp + .mockResolvedValueOnce(mockMaxAge) // max age + .mockResolvedValueOnce(mockTime + 30n) // current block timestamp (30s after commitment) + + mockGetBlockMetadataByTimestamp.mockResolvedValueOnce({ + ok: false, + }) + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: false, + scopeKey: mockAddress, + }), + ) + + await waitFor(() => { + expect(result.current.data).toEqual({ + status: 'commitmentExists', + timestamp: Number(mockTime), + }) + }) + + // Verify it used the correct contract address + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + address: '0xcontroller', + }), + ) + + // Verify it called the correct contract functions + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + functionName: 'commitments', + args: [mockCommitment], + }), + ) + + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + functionName: 'maxCommitmentAge', + }), + ) + + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + functionName: 'getCurrentBlockTimestamp', + }), + ) + }) + }) + + describe('Expired Commitment', () => { + it('should return commitmentExpired for old commitment', async () => { + mockReadContract + .mockResolvedValueOnce(mockTime) // commitment timestamp + .mockResolvedValueOnce(mockMaxAge) // max age + .mockResolvedValueOnce(mockTime + mockMaxAge + 1n) // current block timestamp (past max age) + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: false, + scopeKey: mockAddress, + }), + ) + + await waitFor(() => { + expect(result.current.data).toEqual({ + status: 'commitmentExpired', + timestamp: Number(mockTime), + }) + }) + }) + }) + + describe('Legacy Commitment', () => { + it('should handle legacy commitment check', async () => { + mockReadContract + .mockResolvedValueOnce(mockTime) // commitment timestamp + .mockResolvedValueOnce(mockMaxAge) // max age + .mockResolvedValueOnce(mockTime + 30n) // current block timestamp + + mockGetBlockMetadataByTimestamp.mockResolvedValueOnce({ + ok: false, + }) + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: true, + scopeKey: mockAddress, + }), + ) + + await waitFor(() => { + expect(result.current.data).toEqual({ + status: 'commitmentExists', + timestamp: Number(mockTime), + }) + }) + + // Verify it used legacy controller address + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + address: '0xlegacycontroller', + }), + ) + }) + }) + + describe('Wrapped Commitment', () => { + it('should verify wrapped commitment functionality', async () => { + mockReadContract + .mockResolvedValueOnce(mockTime) // commitment timestamp + .mockResolvedValueOnce(mockMaxAge) // max age + .mockResolvedValueOnce(mockTime + 30n) // current block timestamp (30s after commitment) + + mockGetBlockMetadataByTimestamp.mockResolvedValueOnce({ + ok: false, + }) + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: false, + scopeKey: mockAddress, + isWrappedCommitment: true, + }), + ) + + await waitFor(() => { + expect(result.current.data).toEqual({ + status: 'commitmentExists', + timestamp: Number(mockTime), + }) + }) + + // Verify it used the correct contract address + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + address: '0xcontroller', + }), + ) + + // Verify it called the correct contract functions + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + functionName: 'commitments', + args: [mockCommitment], + }), + ) + + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + functionName: 'maxCommitmentAge', + }), + ) + + expect(mockReadContract).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + functionName: 'getCurrentBlockTimestamp', + }), + ) + }) + }) + + describe('Error Handling', () => { + it('should handle contract read errors', async () => { + mockReadContract.mockRejectedValueOnce(new Error('Contract error')) + + const { result } = renderHook(() => + useExistingCommitment({ + commitment: mockCommitment, + commitKey: mockCommitKey, + isLegacyCommit: false, + scopeKey: mockAddress, + }), + ) + + await waitFor(() => { + expect(result.current.error).toBeTruthy() + }) + }) + }) +}) From 3a159d3fd6b48c9515fde8578b88b86de5734849 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 20 Jan 2025 13:03:10 +0800 Subject: [PATCH 8/9] code clean up --- e2e/specs/stateless/registerName.spec.ts | 22 ++----------------- .../registration/steps/Transactions.tsx | 2 +- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index bf7ad74fc..12f2b8135 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -63,8 +63,6 @@ test.describe.serial('normal registration', () => { await homePage.goto() await login.connect() - await page.pause() - await test.step('should redirect to registration page', async () => { await homePage.searchInput.fill(name) await page.locator(`[data-testid="search-result-name"]`, { hasText: name }).waitFor() @@ -340,7 +338,6 @@ test.describe.serial('normal registration', () => { ) await test.step('confirm name is unwrapped', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).not.toBeVisible() }) }) @@ -368,6 +365,7 @@ test('registering a premium name with no records should not be wrapped', async ( await page.goto(`/${premiumName}/register`) await login.connect() + await page.getByTestId('primary-name-toggle').uncheck() await page.getByTestId('payment-choice-ethereum').click() await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() await page.getByTestId('next-button').click() @@ -608,6 +606,7 @@ test('registering a wrapped premium name with no records should not be wrapped', await page.goto(`/${premiumName}/register`) await login.connect() + await page.getByTestId('primary-name-toggle').uncheck() await page.getByTestId('payment-choice-ethereum').click() await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() await page.getByTestId('next-button').click() @@ -952,7 +951,6 @@ test('should allow registering a premium name for two months', async ({ ) await test.step('confirm name is unwrapped', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).not.toBeVisible() }) }) @@ -1015,7 +1013,6 @@ test('should not allow registering a premium name for less than 28 days', async expect(page.getByText('28 days registration', { exact: true })).toBeVisible() }) - await page.pause() await page.getByTestId('payment-choice-ethereum').click() await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() await page.getByTestId('next-button').click() @@ -1037,7 +1034,6 @@ test('should not allow registering a premium name for less than 28 days', async ) await test.step('confirm name is unwrapped', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).not.toBeVisible() }) }) @@ -1162,7 +1158,6 @@ test('should allow normal registration for a month', async ({ ) await test.step('confirm name is wrapped', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).toBeVisible() }) }) @@ -1298,7 +1293,6 @@ test('should not allow normal registration less than 28 days', async ({ ) await test.step('confirm name is wrapped (set primary name)', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).toBeVisible() }) }) @@ -1344,11 +1338,8 @@ test('should be able to detect an existing commit created on a private mempool f await transactionModal.closeButton.click() - await page.pause() - await expect(consoleListener.getMessages().length).toBeGreaterThan(0) const commitHash = consoleListener.getMessages()[0].split(':')[1]?.trim() as Hash - console.log('>>>>>', commitHash) const approveTx = await walletClient.writeContract({ abi: ethRegistrarControllerCommitSnippet, @@ -1394,7 +1385,6 @@ test('should be able to detect an existing commit created on a private mempool f await expect(transactionModal.transactionModal).toHaveCount(0) await wallet.authorize(Web3RequestKind.SendTransaction) - // await transactionModal.confirm() await page.getByTestId('view-name').click() await expect(page.getByTestId('address-profile-button-eth')).toHaveText( @@ -1402,7 +1392,6 @@ test('should be able to detect an existing commit created on a private mempool f ) await test.step('confirm name is unwrapped', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).toBeVisible() }) }) @@ -1442,8 +1431,6 @@ test('should be able to detect an existing commit created on a private mempool f // should go to profile editor step await page.getByTestId('next-button').click() - // await page.getByTestId('profile-submit-button').click() - await test.step('should be able to find an existing commit', async () => { await page.getByTestId('next-button').click() @@ -1451,7 +1438,6 @@ test('should be able to detect an existing commit created on a private mempool f await expect(consoleListener.getMessages().length).toBeGreaterThan(0) const commitHash = consoleListener.getMessages()[0].split(':')[1]?.trim() as Hash - console.log('>>>>>', commitHash) const approveTx = await walletClient.writeContract({ abi: legacyEthRegistrarControllerCommitSnippet, @@ -1478,7 +1464,6 @@ test('should be able to detect an existing commit created on a private mempool f await expect(page.getByTestId('countdown-circle')).toContainText(/^[0-6]?[0-9]$/) await testClient.increaseTime({ seconds: 60 }) - await page.pause() await expect(page.getByTestId('countdown-complete-check')).toBeVisible({ timeout: 10000 }) }) @@ -1499,7 +1484,6 @@ test('should be able to detect an existing commit created on a private mempool f await expect(transactionModal.transactionModal).toHaveCount(0) await wallet.authorize(Web3RequestKind.SendTransaction) - // await transactionModal.confirm() await page.getByTestId('view-name').click() await expect(page.getByTestId('address-profile-button-eth')).toHaveText( @@ -1507,7 +1491,6 @@ test('should be able to detect an existing commit created on a private mempool f ) await test.step('confirm name is unwrapped', async () => { - await page.pause() await expect(page.getByTestId('permissions-tab')).not.toBeVisible() }) }) @@ -1602,7 +1585,6 @@ test.describe('Error handling', () => { }) await test.step('transaction: commit', async () => { - await page.pause() await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() await expect(page.getByText(`Your "Start timer" transaction was successful`)).toBeVisible() diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index 48c19406e..40fed49e8 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -19,10 +19,10 @@ import useRegistrationParams from '@app/hooks/useRegistrationParams' import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' import { createTransactionItem } from '@app/transaction-flow/transaction' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { isLegacyRegistration } from '@app/utils/registration/isLegacyRegistration' import { makeLegacyRegistrationParams } from '@app/utils/registration/makeLegacyRegistrationParams' import { ONE_DAY } from '@app/utils/time' -import { isLegacyRegistration } from '../../../../../../utils/registration/isLegacyRegistration' import { RegistrationReducerDataItem } from '../types' const PATTERNS = { From a0bd54a164d5f4bf3caa2b8e25ed39d3d8d083ce Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 20 Jan 2025 14:17:14 +0800 Subject: [PATCH 9/9] fix a registerName test --- e2e/specs/stateless/registerName.spec.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 12f2b8135..812e07612 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -678,12 +678,21 @@ test('registering a wrapped premium name with records set should be wrapped', as await page.goto(`/${premiumName}/register`) await login.connect() + await page.getByTestId('primary-name-toggle').uncheck() await page.getByTestId('payment-choice-ethereum').click() await expect(page.getByTestId('invoice-item-2-amount')).toBeVisible() await page.getByTestId('next-button').click() - if (await page.getByTestId('profile-submit-button').isVisible()) { + + await page.getByTestId('setup-profile-button').click() + + await test.step('add a text record', async () => { + await page.getByTestId('show-add-profile-records-modal-button').click() + await page.getByTestId('confirmation-dialog-confirm-button').click() + await page.getByTestId('profile-record-option-name').click() + await page.getByTestId('add-profile-records-button').click() + await page.getByTestId('profile-record-input-input-name').fill('Test Name') await page.getByTestId('profile-submit-button').click() - } + }) await page.getByTestId('next-button').click() await transactionModal.confirm() @@ -694,6 +703,7 @@ test('registering a wrapped premium name with records set should be wrapped', as await transactionModal.confirm() await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) @@ -709,6 +719,7 @@ test('registering a wrapped premium name with records set should be wrapped', as const recordsPage = makePageObject('RecordsPage') await recordsPage.goto(premiumName) + await expect(recordsPage.getRecordValue('text', 'name')).toHaveText('Test Name') await expect(recordsPage.getRecordValue('address', 'ETH')).toHaveText( createAccounts().getAddress('user') as `0x${string}`, )