Skip to content

Commit

Permalink
fix: allow wrapped parent owner to delete unwrapped subname with mult…
Browse files Browse the repository at this point in the history
…i-step flow
  • Loading branch information
TateB committed Jun 19, 2023
1 parent 3245df3 commit 150bfc3
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 41 deletions.
6 changes: 5 additions & 1 deletion public/locales/en/transactionFlow.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,11 @@
"invalidResolver": {
"title": "Unauthorized resolver set",
"description": "To use this as your primary name you will need to set a valid resolver and update the ETH address first."
}
}
},
"multiStepSubnameDelete": {
"title": "Delete Subname",
"description": "Deleting this subname requires multiple transactions"
}
},
"errors": {
Expand Down
119 changes: 119 additions & 0 deletions src/hooks/useProfileActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvide

import { useHasGlobalError } from './errors/useHasGlobalError'
import { useProfileActions } from './useProfileActions'
import useWrapperApprovedForAll from './useWrapperApprovedForAll'

const NOW_TIMESTAMP = 1588994800000
jest.spyOn(Date, 'now').mockImplementation(() => NOW_TIMESTAMP)
Expand All @@ -23,10 +24,12 @@ jest.mock('@app/hooks/resolver/useResolverStatus', () => ({
jest.mock('@app/hooks/usePrimary')
jest.mock('@app/transaction-flow/TransactionFlowProvider')
jest.mock('./errors/useHasGlobalError')
jest.mock('./useWrapperApprovedForAll')

const mockUsePrimary = mockFunction(usePrimary)
const mockUseTransactionFlow = mockFunction(useTransactionFlow)
const mockUseHasGlobalError = mockFunction(useHasGlobalError)
const mockUseWrapperApprovedForAll = mockFunction(useWrapperApprovedForAll)

const mockCreateTransactionFlow = jest.fn()
const mockPrepareDataInput = jest.fn()
Expand Down Expand Up @@ -76,6 +79,7 @@ describe('useProfileActions', () => {
createTransactionFlow: (...args: any[]) => mockCreateTransactionFlow(...args),
})
mockUseHasGlobalError.mockReturnValue(false)
mockUseWrapperApprovedForAll.mockReturnValue({ approvedForAll: true, isLoading: false })
})

afterEach(() => {
Expand Down Expand Up @@ -142,6 +146,121 @@ describe('useProfileActions', () => {
)
})

describe('delete subname', () => {
it('should return a single transaction with normal subname', () => {
const { result } = renderHook(() => useProfileActions(props))
const deleteAction = result.current.profileActions?.find(
(a) => a.label === 'tabs.profile.actions.deleteSubname.label',
)
deleteAction!.onClick()
expect(mockCreateTransactionFlow).toHaveBeenCalledWith('deleteSubname-test.eth', {
transactions: [
{
name: 'deleteSubname',
data: {
name: 'test.eth',
contract: 'testcontract',
method: 'testmethod',
},
},
],
})
})
it('should return a two step transaction flow for an unwrapped subname with wrapped parent', () => {
const { result } = renderHook(() =>
useProfileActions({
...props,
subnameAbilities: {
...props.subnameAbilities,
canDeleteRequiresWrap: true,
},
}),
)
const deleteAction = result.current.profileActions?.find(
(a) => a.label === 'tabs.profile.actions.deleteSubname.label',
)
deleteAction!.onClick()
expect(mockCreateTransactionFlow).toHaveBeenCalledWith('deleteSubname-test.eth', {
transactions: [
{
name: 'wrapName',
data: {
name: 'test.eth',
},
},
{
name: 'deleteSubname',
data: {
contract: 'nameWrapper',
name: 'test.eth',
method: 'setRecord',
},
},
],
resumable: true,
intro: {
title: ['intro.multiStepSubnameDelete.title', { ns: 'transactionFlow' }],
content: {
name: 'GenericWithDescription',
data: {
description: 'intro.multiStepSubnameDelete.description',
},
},
},
})
})
it('should return a three step transaction flow for an unwrapped subname with wrapped parent with namewrapper not approved by address', () => {
mockUseWrapperApprovedForAll.mockReturnValue({ approvedForAll: false, isLoading: false })
const { result } = renderHook(() =>
useProfileActions({
...props,
subnameAbilities: {
...props.subnameAbilities,
canDeleteRequiresWrap: true,
},
}),
)
const deleteAction = result.current.profileActions?.find(
(a) => a.label === 'tabs.profile.actions.deleteSubname.label',
)
deleteAction!.onClick()
expect(mockCreateTransactionFlow).toHaveBeenCalledWith('deleteSubname-test.eth', {
transactions: [
{
name: 'approveNameWrapper',
data: {
address: '0x1234567890',
},
},
{
name: 'wrapName',
data: {
name: 'test.eth',
},
},
{
name: 'deleteSubname',
data: {
contract: 'nameWrapper',
name: 'test.eth',
method: 'setRecord',
},
},
],
resumable: true,
intro: {
title: ['intro.multiStepSubnameDelete.title', { ns: 'transactionFlow' }],
content: {
name: 'GenericWithDescription',
data: {
description: 'intro.multiStepSubnameDelete.description',
},
},
},
})
})
})

describe('set primary name', () => {
it('should return an action for a single transaction with base mock data', async () => {
const { result } = renderHook(() => useProfileActions(props))
Expand Down
119 changes: 79 additions & 40 deletions src/hooks/useProfileActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { checkIsDecrypted } from '@ensdomains/ensjs/utils/labels'

import { usePrimary } from '@app/hooks/usePrimary'
import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider'
import { makeIntroItem } from '@app/transaction-flow/intro'
import { makeTransactionItem } from '@app/transaction-flow/transaction'
import { GenericTransaction } from '@app/transaction-flow/types'
import { ReturnedENS } from '@app/types'
import { nameParts } from '@app/utils/name'

Expand All @@ -16,6 +18,7 @@ import { useResolverStatus } from './resolver/useResolverStatus'
import { useNameDetails } from './useNameDetails'
import { useSelfAbilities } from './useSelfAbilities'
import { useSubnameAbilities } from './useSubnameAbilities'
import useWrapperApprovedForAll from './useWrapperApprovedForAll'

type Action = {
onClick: () => void
Expand Down Expand Up @@ -62,6 +65,12 @@ export const useProfileActions = ({

const primary = usePrimary(address)

const wrappedApproved = useWrapperApprovedForAll(
address || '',
subnameAbilities.canDelete,
!!subnameAbilities.canDeleteRequiresWrap,
)

const isAvailablePrimaryName = checkAvailablePrimaryName(
primary.data?.name,
resolverStatus.data,
Expand Down Expand Up @@ -92,7 +101,10 @@ export const useProfileActions = ({
)

const isLoading =
primary.isLoading || resolverStatus.isLoading || getPrimaryNameTransactionFlowItem.isLoading
primary.isLoading ||
resolverStatus.isLoading ||
getPrimaryNameTransactionFlowItem.isLoading ||
wrappedApproved.isLoading

const profileActions = useMemo(() => {
const actions: Action[] = []
Expand Down Expand Up @@ -135,40 +147,65 @@ export const useProfileActions = ({
}

if (subnameAbilities.canDelete && subnameAbilities.canDeleteContract) {
const action = subnameAbilities.isPCCBurned
? {
label: t('tabs.profile.actions.deleteSubname.label'),
onClick: () => {
showDeleteEmancipatedSubnameWarningInput(
`delete-emancipated-subname-warning-${name}`,
{ name },
)
},
tooltipContent: hasGlobalError
? t('errors.networkError.blurb', { ns: 'common' })
: undefined,
red: true,
skip2LDEth: true,
}
: {
label: t('tabs.profile.actions.deleteSubname.label'),
onClick: () =>
createTransactionFlow(`deleteSubname-${name}`, {
transactions: [
makeTransactionItem('deleteSubname', {
name,
contract: subnameAbilities.canDeleteContract!,
method: subnameAbilities.canDeleteMethod,
const base = {
label: t('tabs.profile.actions.deleteSubname.label'),
tooltipContent: hasGlobalError
? t('errors.networkError.blurb', { ns: 'common' })
: undefined,
red: true,
skip2LDEth: true,
}
if (subnameAbilities.canDeleteRequiresWrap) {
const transactions: GenericTransaction[] = [
makeTransactionItem('wrapName', {
name,
}),
makeTransactionItem('deleteSubname', {
contract: 'nameWrapper',
name,
method: 'setRecord',
}),
]
if (!wrappedApproved.approvedForAll)
transactions.unshift(makeTransactionItem('approveNameWrapper', { address }))
actions.push({
...base,
onClick: () =>
createTransactionFlow(`deleteSubname-${name}`, {
transactions,
resumable: true,
intro: {
title: ['intro.multiStepSubnameDelete.title', { ns: 'transactionFlow' }],
content: makeIntroItem('GenericWithDescription', {
description: t('intro.multiStepSubnameDelete.description', {
ns: 'transactionFlow',
}),
],
}),
tooltipContent: hasGlobalError
? t('errors.networkError.blurb', { ns: 'common' })
: undefined,
red: true,
skip2LDEth: true,
}
actions.push(action)
}),
},
}),
})
} else {
actions.push({
...base,
onClick: subnameAbilities.isPCCBurned
? () => {
showDeleteEmancipatedSubnameWarningInput(
`delete-emancipated-subname-warning-${name}`,
{ name },
)
}
: () =>
createTransactionFlow(`deleteSubname-${name}`, {
transactions: [
makeTransactionItem('deleteSubname', {
name,
contract: subnameAbilities.canDeleteContract!,
method: subnameAbilities.canDeleteMethod,
}),
],
}),
})
}
} else if (subnameAbilities.canDeleteError) {
actions.push({
label: t('tabs.profile.actions.deleteSubname.label'),
Expand Down Expand Up @@ -204,23 +241,25 @@ export const useProfileActions = ({
return actions
}, [
address,
isLoading,
getPrimaryNameTransactionFlowItem,
name,
isAvailablePrimaryName,
selfAbilities.canEdit,
subnameAbilities.canDelete,
subnameAbilities.canDeleteContract,
subnameAbilities.canDeleteError,
subnameAbilities.canDeleteMethod,
subnameAbilities.isPCCBurned,
subnameAbilities.canReclaim,
getPrimaryNameTransactionFlowItem,
subnameAbilities.canDeleteRequiresWrap,
subnameAbilities.isPCCBurned,
subnameAbilities.canDeleteMethod,
t,
hasGlobalError,
showUnknownLabelsInput,
createTransactionFlow,
showProfileEditorInput,
wrappedApproved.approvedForAll,
showDeleteEmancipatedSubnameWarningInput,
isLoading,
hasGlobalError,
isAvailablePrimaryName,
])

return {
Expand Down
18 changes: 18 additions & 0 deletions src/hooks/useSubnameAbilities.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,24 @@ const groups = [
address: '0xName',
abilities: makeResults({ canDelete: false }),
},
{
description:
'should return canDelete is true and canDeleteRequiresWrap is true if parent is wrapped',
...unwrappedSubname,
hasSubnames: false,
address: '0xParent',
isParentWrapped: true,
parentOwnerData: makeOwnerData({
owner: '0xParent',
ownershipLevel: 'nameWrapper',
}),
abilities: makeResults({
canDelete: true,
canDeleteContract: 'nameWrapper',
canDeleteRequiresWrap: true,
canDeleteError: undefined,
}),
},
],
},
{
Expand Down
Loading

0 comments on commit 150bfc3

Please sign in to comment.