diff --git a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/ExpirySection.tsx b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/ExpirySection.tsx index 90ac85fc9..77ae1571f 100644 --- a/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/ExpirySection.tsx +++ b/src/components/pages/profile/[name]/tabs/OwnershipTab/sections/ExpirySection/ExpirySection.tsx @@ -115,7 +115,6 @@ type Props = { } export const ExpirySection = ({ name, details }: Props) => { - console.log(details) const { t } = useTranslation('profile') const expiry = useExpiryDetails({ name, details }) const actions = useExpiryActions({ name, expiryDetails: expiry.data }) diff --git a/src/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts b/src/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts index d344e479f..561d70d46 100644 --- a/src/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts +++ b/src/transaction-flow/input/EditRoles/hooks/useSimpleSearch.ts @@ -1,72 +1,90 @@ import { isAddress } from '@ethersproject/address' -import { useCallback } from 'react' -import { useMutation } from 'wagmi' +import { useEffect } from 'react' +import { useMutation, useQueryClient } from 'wagmi' import { normalise } from '@ensdomains/ensjs/utils/normalise' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' import { useEns } from '@app/utils/EnsProvider' +import { useQueryKeys } from '@app/utils/cacheKeyFactory' type Result = { name?: string; address: string } +type Options = { cache?: boolean } + +export const useSimpleSearch = (options: Options = {}) => { + const cache = options.cache ?? true -export const useSimpleSearch = () => { const { ready, getAddr, getName } = useEns() + const queryClient = useQueryClient() + const queryKey = useQueryKeys() + + useEffect(() => { + return () => { + queryClient.removeQueries(queryKey.simpleSearchBase(), { exact: false }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - const queryByName = useCallback( - async (name: string): Promise => { - try { - const normalisedName = normalise(name) - const record = await getAddr(normalisedName, '60') - const address = typeof record === 'string' ? record : record?.addr - if (!address) throw new Error('No address found') - return { - name: normalisedName, - address, - } - } catch { - return null + const queryByName = async (name: string): Promise => { + try { + const normalisedName = normalise(name) + const record = await getAddr(normalisedName, '60') + const address = typeof record === 'string' ? record : record?.addr + if (!address) throw new Error('No address found') + return { + name: normalisedName, + address, } - }, - [getAddr], - ) + } catch { + return null + } + } - const queryByAddress = useCallback( - async (address: string): Promise => { - try { - const name = await getName(address) - return { - name: name?.name, - address, - } - } catch { - return null + const queryByAddress = async (address: string): Promise => { + try { + const name = await getName(address) + return { + name: name?.name, + address, } + } catch { + return null + } + } + + const { mutate, isLoading, ...rest } = useMutation( + async (query: string) => { + if (!ready) throw new Error('ENSJS not ready') + if (query.length < 3) throw new Error('Query too short') + if (cache) { + const cachedData = queryClient.getQueryData(queryKey.simpleSearch(query)) + if (cachedData) return cachedData + } + const results = await Promise.allSettled([ + queryByName(query), + queryByName(`${query}.eth`), + ...(isAddress(query) ? [queryByAddress(query)] : []), + ]) + const filteredData = results + .filter>( + (item): item is PromiseFulfilledResult => + item.status === 'fulfilled' && !!item.value, + ) + .map((item) => item.value) + .reduce((acc, cur) => { + return { + ...acc, + [cur.address]: cur, + } + }, {}) + return Object.values(filteredData) as Result[] + }, + { + onSuccess: (data, variables) => { + queryClient.setQueryData(queryKey.simpleSearch(variables), data) + }, }, - [getName], ) - - const { mutate, isLoading, ...rest } = useMutation(async (query: string) => { - if (!ready) throw new Error('ENSJS not ready') - if (query.length < 3) throw new Error('Query too short') - const results = await Promise.allSettled([ - queryByName(query), - queryByName(`${query}.eth`), - ...(isAddress(query) ? [queryByAddress(query)] : []), - ]) - const filteredData = results - .filter>( - (item): item is PromiseFulfilledResult => - item.status === 'fulfilled' && !!item.value, - ) - .map((item) => item.value) - .reduce((acc, cur) => { - return { - ...acc, - [cur.address]: cur, - } - }, {}) - return Object.values(filteredData) as Result[] - }) const debouncedMutate = useDebouncedCallback(mutate, 500) return { diff --git a/src/transaction-flow/input/SendName/SendName-flow.tsx b/src/transaction-flow/input/SendName/SendName-flow.tsx index f9fb03b94..e3415c5aa 100644 --- a/src/transaction-flow/input/SendName/SendName-flow.tsx +++ b/src/transaction-flow/input/SendName/SendName-flow.tsx @@ -10,10 +10,10 @@ import { useAccountSafely } from '@app/hooks/useAccountSafely' import { useBasicName } from '@app/hooks/useBasicName' import { useNameType } from '@app/hooks/useNameType' import { useResolver } from '@app/hooks/useResolver' -import { makeTransactionItem } from '@app/transaction-flow/transaction' import { TransactionDialogPassthrough } from '@app/transaction-flow/types' import { checkCanSend, senderRole } from './utils/checkCanSend' +import { getSendNameTransactions } from './utils/getSendNameTransactions' import { CannotSendView } from './views/CannotSendView' import { ConfirmationView } from './views/ConfirmationView' import { SearchView } from './views/SearchView/SearchView' @@ -99,42 +99,14 @@ const SendName = ({ data: { name }, dispatch, onDismiss }: Props) => { const isOwnerOrManager = account.address === basic.ownerData?.owner || basic.ownerData?.registrant === account.address - const setEthRecordOnly = transactions.setEthRecord && !transactions.resetProfile - // Anytime you reset the profile you will need to set the eth record as well - const setEthRecordAndResetProfile = transactions.resetProfile - - const _transactions = [ - setEthRecordOnly - ? makeTransactionItem('updateEthAddress', { name, address: recipient }) - : null, - setEthRecordAndResetProfile - ? makeTransactionItem('resetProfileWithRecords', { - name, - records: { - coinTypes: [{ key: 'ETH', value: recipient }], - }, - resolver: resolver.data, - }) - : null, - - transactions.sendManager && !!abilities.data?.sendNameFunctionCallDetails?.sendManager - ? makeTransactionItem(isOwnerOrManager ? 'transferName' : 'transferSubname', { - name, - newOwner: recipient, - sendType: 'sendManager', - contract: abilities.data?.sendNameFunctionCallDetails?.sendManager?.contract, - reclaim: abilities.data?.sendNameFunctionCallDetails?.sendManager?.method === 'reclaim', - }) - : null, - transactions.sendOwner && !!abilities.data?.sendNameFunctionCallDetails?.sendOwner - ? makeTransactionItem(isOwnerOrManager ? 'transferName' : 'transferSubname', { - name, - newOwner: recipient, - sendType: 'sendOwner', - contract: abilities.data?.sendNameFunctionCallDetails?.sendOwner?.contract, - }) - : null, - ].filter((transaction) => !!transaction) + const _transactions = getSendNameTransactions({ + name, + recipient, + transactions, + isOwnerOrManager, + abilities: abilities.data, + resolverAddress: resolver.data, + }) if (_transactions.length === 0) return diff --git a/src/transaction-flow/input/SendName/utils/getSendNameTransactions.test.ts b/src/transaction-flow/input/SendName/utils/getSendNameTransactions.test.ts new file mode 100644 index 000000000..984f00976 --- /dev/null +++ b/src/transaction-flow/input/SendName/utils/getSendNameTransactions.test.ts @@ -0,0 +1,200 @@ +import { getSendNameTransactions } from './getSendNameTransactions'; +import { makeTransactionItem } from '@app/transaction-flow/transaction'; + +describe('getSendNameTransactions', () => { + it('should return 3 transactions (resetProfileWithRecords, transferName, transferName) if setEthRecord, resetProfile, sendManager and sendOwner is true', () => { + expect(getSendNameTransactions({ + name: 'test.eth', + recipient:'0xrecipient', + transactions: { + setEthRecord: true, + resetProfile: true, + sendManager: true, + sendOwner: true + }, + abilities: { + sendNameFunctionCallDetails: { + sendOwner: { + contract: 'registry', + method: 'safeTransferFrom' + }, + sendManager: { + contract: 'registry', + method: 'reclaim' + } + } + } as any, + isOwnerOrManager: true, + resolverAddress: '0xresolver' + })).toEqual([ + makeTransactionItem('resetProfileWithRecords', { name: 'test.eth', records: {coinTypes: [{key: 'ETH', value: '0xrecipient'}]}, resolver: '0xresolver' }), + makeTransactionItem('transferName', {name: 'test.eth', newOwner: '0xrecipient', sendType: 'sendManager', contract: 'registry', reclaim: true}), + makeTransactionItem('transferName', { + name: 'test.eth', + newOwner: '0xrecipient', + sendType: 'sendOwner', + contract: 'registry', + }) + ]) + }) + + it('should return 3 transactions (resetProfileWithRecords, transferName, transferName) if setEthRecord, resetProfile, sendManager and sendOwner is true', () => { + expect(getSendNameTransactions({ + name: 'test.eth', + recipient:'0xrecipient', + transactions: { + setEthRecord: false, + resetProfile: true, + sendManager: true, + sendOwner: true + }, + abilities: { + sendNameFunctionCallDetails: { + sendOwner: { + contract: 'registry', + method: 'safeTransferFrom' + }, + sendManager: { + contract: 'registry', + method: 'reclaim' + } + } + } as any, + isOwnerOrManager: true, + resolverAddress: '0xresolver' + })).toEqual([ + makeTransactionItem('resetProfileWithRecords', { name: 'test.eth', records: {coinTypes: [{key: 'ETH', value: '0xrecipient'}]}, resolver: '0xresolver' }), + makeTransactionItem('transferName', {name: 'test.eth', newOwner: '0xrecipient', sendType: 'sendManager', contract: 'registry', reclaim: true}), + makeTransactionItem('transferName', { + name: 'test.eth', + newOwner: '0xrecipient', + sendType: 'sendOwner', + contract: 'registry', + }) + ]) + }) + + it('should return 3 transactions (updateEthAddress, transferName, transferName) if resetProfile, sendManager and sendOwner is true', () => { + expect(getSendNameTransactions({ + name: 'test.eth', + recipient:'0xrecipient', + transactions: { + setEthRecord: true, + resetProfile: false, + sendManager: true, + sendOwner: true + }, + abilities: { + sendNameFunctionCallDetails: { + sendOwner: { + contract: 'registry', + method: 'safeTransferFrom' + }, + sendManager: { + contract: 'registry', + method: 'reclaim' + } + } + } as any, + isOwnerOrManager: true, + resolverAddress: '0xresolver' + })).toEqual([ + makeTransactionItem('updateEthAddress', { name: 'test.eth', address: '0xrecipient' }), + makeTransactionItem('transferName', {name: 'test.eth', newOwner: '0xrecipient', sendType: 'sendManager', contract: 'registry', reclaim: true}), + makeTransactionItem('transferName', { + name: 'test.eth', + newOwner: '0xrecipient', + sendType: 'sendOwner', + contract: 'registry', + }) + ]) + }) + + it('should return 2 transactions (transferName, transferName) if sendManager and sendOwner is true', () => { + expect(getSendNameTransactions({ + name: 'test.eth', + recipient:'0xrecipient', + transactions: { + setEthRecord: false, + resetProfile: false, + sendManager: true, + sendOwner: true + }, + abilities: { + sendNameFunctionCallDetails: { + sendOwner: { + contract: 'registry', + method: 'safeTransferFrom' + }, + sendManager: { + contract: 'registry', + method: 'reclaim' + } + } + } as any, + isOwnerOrManager: true, + resolverAddress: '0xresolver' + })).toEqual([ + makeTransactionItem('transferName', {name: 'test.eth', newOwner: '0xrecipient', sendType: 'sendManager', contract: 'registry', reclaim: true}), + makeTransactionItem('transferName', { + name: 'test.eth', + newOwner: '0xrecipient', + sendType: 'sendOwner', + contract: 'registry', + }) + ]) + }) + + it('should return 2 transactions (transferSubname, transferSubname) if sendManager and sendOwner is true and isOwnerOrManager is false', () => { + expect(getSendNameTransactions({ + name: 'test.eth', + recipient:'0xrecipient', + transactions: { + setEthRecord: false, + resetProfile: false, + sendManager: true, + sendOwner: true + }, + abilities: { + sendNameFunctionCallDetails: { + sendOwner: { + contract: 'registry', + method: 'safeTransferFrom' + }, + sendManager: { + contract: 'registry', + method: 'reclaim' + } + } + } as any, + isOwnerOrManager: true, + resolverAddress: '0xresolver' + })).toEqual([ + makeTransactionItem('transferName', {name: 'test.eth', newOwner: '0xrecipient', sendType: 'sendManager', contract: 'registry', reclaim: true}), + makeTransactionItem('transferName', { + name: 'test.eth', + newOwner: '0xrecipient', + sendType: 'sendOwner', + contract: 'registry', + }) + ]) + }) + + it('should return 0 transactions if sendManager and sendOwner is true but abilities.sendNameFunctionCallDetails is undefined', () => { + expect(getSendNameTransactions({ + name: 'test.eth', + recipient:'0xrecipient', + transactions: { + setEthRecord: false, + resetProfile: false, + sendManager: true, + sendOwner: true + }, + abilities: { + sendNameFunctionCallDetails: undefined + } as any, + isOwnerOrManager: true, + resolverAddress: '0xresolver' + })).toEqual([]) + }) +}) \ No newline at end of file diff --git a/src/transaction-flow/input/SendName/utils/getSendNameTransactions.ts b/src/transaction-flow/input/SendName/utils/getSendNameTransactions.ts new file mode 100644 index 000000000..9d2c51809 --- /dev/null +++ b/src/transaction-flow/input/SendName/utils/getSendNameTransactions.ts @@ -0,0 +1,57 @@ +import type { useAbilities } from '@app/hooks/abilities/useAbilities' +import { makeTransactionItem } from '@app/transaction-flow/transaction' + +import type { SendNameForm } from '../SendName-flow' + +export const getSendNameTransactions = ({ + name, + recipient, + transactions, + abilities, + isOwnerOrManager, + resolverAddress, +}: { + name: string + recipient: SendNameForm['recipient'] + transactions: SendNameForm['transactions'] + abilities: ReturnType['data'] + isOwnerOrManager: boolean + resolverAddress: string +}) => { + const setEthRecordOnly = transactions.setEthRecord && !transactions.resetProfile + // Anytime you reset the profile you will need to set the eth record as well + const setEthRecordAndResetProfile = transactions.resetProfile + + const _transactions = [ + setEthRecordOnly ? makeTransactionItem('updateEthAddress', { name, address: recipient }) : null, + setEthRecordAndResetProfile + ? makeTransactionItem('resetProfileWithRecords', { + name, + records: { + coinTypes: [{ key: 'ETH', value: recipient }], + }, + resolver: resolverAddress, + }) + : null, + + transactions.sendManager && !!abilities?.sendNameFunctionCallDetails?.sendManager + ? makeTransactionItem(isOwnerOrManager ? 'transferName' : 'transferSubname', { + name, + newOwner: recipient, + sendType: 'sendManager', + contract: abilities?.sendNameFunctionCallDetails?.sendManager?.contract, + reclaim: abilities?.sendNameFunctionCallDetails?.sendManager?.method === 'reclaim', + }) + : null, + transactions.sendOwner && !!abilities?.sendNameFunctionCallDetails?.sendOwner + ? makeTransactionItem(isOwnerOrManager ? 'transferName' : 'transferSubname', { + name, + newOwner: recipient, + sendType: 'sendOwner', + contract: abilities?.sendNameFunctionCallDetails?.sendOwner?.contract, + }) + : null, + ].filter((transaction) => !!transaction) + + return _transactions +} diff --git a/src/utils/cacheKeyFactory.ts b/src/utils/cacheKeyFactory.ts index 61b2a455b..1b2c34e55 100644 --- a/src/utils/cacheKeyFactory.ts +++ b/src/utils/cacheKeyFactory.ts @@ -171,6 +171,8 @@ export const useQueryKeys = () => { 'wrapperApprovedForAll', ], isSafeApp: (connectorId: string | undefined) => [...globalKeys, connectorId, 'isSafeApp'], + simpleSearch: (query: string) => [...globalKeys, 'simpleSearch', query], + simpleSearchBase: () => [...globalKeys, 'simpleSearch'], globalIndependent: { isSupportedTLD: (tld: string) => [tld, 'isSupportedTLD'], zorb: (input: string, type: string, bg: string, fg: string, accent: string) => [