Skip to content

Commit

Permalink
Merge pull request #917 from ensdomains/extend-names-duration-fix
Browse files Browse the repository at this point in the history
Extend names duration fix
  • Loading branch information
sugh01 authored Dec 18, 2024
2 parents d385c3a + 15643a5 commit 328692a
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 28 deletions.
16 changes: 16 additions & 0 deletions e2e/specs/stateless/extendNames.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ test('should be able to extend a name by a month', async ({
await test.step('should extend', async () => {
await extendNamesModal.getExtendButton.click()
const transactionModal = makePageObject('TransactionModal')

// Verify duration and new expiry display in transaction modal
await expect(page.getByText('1 month')).toBeVisible()

await transactionModal.autoComplete()

const newTimestamp = await profilePage.getExpiryTimestamp()
Expand Down Expand Up @@ -442,6 +446,10 @@ test('should be able to extend a name by a day', async ({
await test.step('should extend', async () => {
await extendNamesModal.getExtendButton.click()
const transactionModal = makePageObject('TransactionModal')

// Verify duration and new expiry display in transaction modal
await expect(page.getByText('1 day')).toBeVisible()

await transactionModal.autoComplete()

const newTimestamp = await profilePage.getExpiryTimestamp()
Expand Down Expand Up @@ -516,6 +524,10 @@ test('should be able to extend a name in grace period by a month', async ({
await test.step('should extend', async () => {
await extendNamesModal.getExtendButton.click()
const transactionModal = makePageObject('TransactionModal')

// Verify duration and new expiry display in transaction modal
await expect(page.getByText('1 month')).toBeVisible()

await transactionModal.autoComplete()

const newTimestamp = await profilePage.getExpiryTimestamp()
Expand Down Expand Up @@ -592,6 +604,10 @@ test('should be able to extend a name in grace period by 1 day', async ({
await test.step('should extend', async () => {
await extendNamesModal.getExtendButton.click()
const transactionModal = makePageObject('TransactionModal')

// Verify duration and new expiry display in transaction modal
await expect(page.getByText('1 day')).toBeVisible()

await transactionModal.autoComplete()

const newTimestamp = await profilePage.getExpiryTimestamp()
Expand Down
30 changes: 12 additions & 18 deletions src/components/@molecules/TransactionDialogManager/DisplayItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AvatarWithZorb, NameAvatar } from '@app/components/AvatarWithZorb'
import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName'
import { useBeautifiedName } from '@app/hooks/useBeautifiedName'
import { TransactionDisplayItem } from '@app/types'
import { formatExpiry, shortenAddress } from '@app/utils/utils'
import { shortenAddress } from '@app/utils/utils'

const Container = styled.div(
({ theme }) => css`
Expand Down Expand Up @@ -231,36 +231,33 @@ const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => {
)
}

const DurationValue = ({ value }: { value: string | undefined }) => {
const DurationValue = ({
value,
}: {
value: { duration: string; newExpiry?: string | undefined }
}) => {
const { t } = useTranslation('transactionFlow')

if (!value) return null

const regex = /(\d+)\s*years?\s*(?:,?\s*(\d+)?\s*months?)?/
const matches = value.match(regex) ?? []

const years = parseInt(matches.at(1) ?? '0')
const months = parseInt(matches.at(2) ?? '0')

const date = new Date()

if (years > 0) date.setFullYear(date.getFullYear() + years)
if (months > 0) date.setMonth(date.getMonth() + months)

return (
<DurationContainer>
<Typography ellipsis>
<strong>{value}</strong>
<strong>{value.duration}</strong>
</Typography>
<Typography color="textTertiary" fontVariant="small">
{t('transaction.extendNames.newExpiry', { date: formatExpiry(date) })}
{t('transaction.extendNames.newExpiry', { date: value.newExpiry })}
</Typography>
</DurationContainer>
)
}

const DisplayItemValue = (props: Omit<TransactionDisplayItem, 'label'>) => {
const { value, type } = props as TransactionDisplayItem
if (type === 'duration') {
return <DurationValue value={value} />
}

if (type === 'address') {
return <AddressValue value={value} />
}
Expand All @@ -276,9 +273,6 @@ const DisplayItemValue = (props: Omit<TransactionDisplayItem, 'label'>) => {
if (type === 'records') {
return <RecordsValue value={value} />
}
if (type === 'duration') {
return <DurationValue value={value} />
}
return <ValueTypography fontVariant="bodyBold">{value}</ValueTypography>
}

Expand Down
224 changes: 224 additions & 0 deletions src/transaction-flow/transaction/extendNames.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { mockFunction } from '@app/test-utils'

import { beforeEach, describe, expect, it, vi } from 'vitest'

import { getPrice } from '@ensdomains/ensjs/public'
import { renewNames } from '@ensdomains/ensjs/wallet'

import { ClientWithEns, ConnectorClientWithEns } from '@app/types'

import extendNamesTransaction from './extendNames'

vi.mock('@ensdomains/ensjs/public')
vi.mock('@ensdomains/ensjs/wallet')

const mockGetPrice = mockFunction(getPrice)
const mockRenewNames = mockFunction(renewNames.makeFunctionData)

describe('extendNamesTransaction', () => {
const mockClient = {} as ClientWithEns
const mockConnectorClient = {} as ConnectorClientWithEns

const mockT = vi.fn((key: string, options?: any) => {
if (key === 'transaction.extendNames.actionValue') return 'Extend'
if (key === 'transaction.extendNames.costValue') return `Cost: ${options.value}`
if (key === 'unit.years') return `${options.count} year`
if (key === 'unit.months') return `${options.count} months`
if (key === 'unit.days') return `${options.count} days`
return key
})

describe('displayItems', () => {
it('should format display items correctly for a single name', () => {
const JAN_1_2022_TIMESTAMP = 1640995200000 // 2022-01-01T00:00:00.000Z
const ONE_YEAR_SECONDS = 31536000

const data = {
names: ['test.eth'],
duration: ONE_YEAR_SECONDS,
startDateTimestamp: JAN_1_2022_TIMESTAMP,
displayPrice: '0.1 ETH',
}

const result = extendNamesTransaction.displayItems(data, mockT)

expect(result).toEqual([
{
label: 'name',
value: 'test.eth',
type: 'name',
},
{
label: 'action',
value: 'Extend',
},
{
label: 'duration',
type: 'duration',
value: {
duration: '1 year',
newExpiry: 'January 1, 2023',
},
},
{
label: 'cost',
value: 'Cost: 0.1 ETH',
},
])
})

it('should format display items correctly for multiple names', () => {
const JAN_1_2022_TIMESTAMP = 1640995200000 // 2022-01-01T00:00:00.000Z
const ONE_YEAR_SECONDS = 31536000

const data = {
names: ['test1.eth', 'test2.eth'],
duration: ONE_YEAR_SECONDS,
startDateTimestamp: JAN_1_2022_TIMESTAMP,
displayPrice: '0.2 ETH',
}

const result = extendNamesTransaction.displayItems(data, mockT)

expect(result).toEqual([
{
label: 'name',
value: '2 names',
type: undefined,
},
{
label: 'action',
value: 'Extend',
},
{
label: 'duration',
type: 'duration',
value: {
duration: '1 year',
newExpiry: 'January 1, 2023',
},
},
{
label: 'cost',
value: 'Cost: 0.2 ETH',
},
])
})

it('should handle different duration values', () => {
const JAN_1_2022_TIMESTAMP = 1640995200000 // 2022-01-01T00:00:00.000Z

const testCases = [
{
duration: 2592000,
expectedDuration: '30 days',
expectedExpiry: 'January 31, 2022',
},
{
duration: 15768000,
expectedDuration: '6 months, 1 days',
expectedExpiry: 'July 2, 2022',
},
{
duration: 31536000,
expectedDuration: '1 year',
expectedExpiry: 'January 1, 2023',
},
{
duration: 157680000,
expectedDuration: '4 year, 11 months',
expectedExpiry: 'December 31, 2026',
},
]

testCases.forEach(({ duration, expectedDuration, expectedExpiry }) => {
const data = {
names: ['test.eth'],
duration,
startDateTimestamp: JAN_1_2022_TIMESTAMP,
displayPrice: '0.1 ETH',
}

const result = extendNamesTransaction.displayItems(data, mockT)
expect(result[2]).toEqual({
label: 'duration',
type: 'duration',
value: {
duration: expectedDuration,
newExpiry: expectedExpiry,
},
})
})
})

it('should return display items without newExpiry when startDateTimestamp is not provided', () => {
const data = {
names: ['test.eth'],
duration: 31536000,
displayPrice: '1.02',
}

const result = extendNamesTransaction.displayItems(data, mockT)
expect(result[2].value.newExpiry).toBeUndefined()
})
})

describe('transaction', () => {
beforeEach(() => {
mockGetPrice.mockReset()
mockRenewNames.mockReset()
})

it('should calculate price and create transaction data', async () => {
const data = {
names: ['test.eth'],
duration: 31536000, // 1 year
}

mockGetPrice.mockResolvedValue({ base: BigInt('1000000000000000000'), premium: 0n }) // 1 ETH
mockRenewNames.mockResolvedValue({
to: '0x123' as Address,
data: '0x456' as Hex,
value: BigInt('1020000000000000000'), // 1.02 ETH (with 2% buffer)
})

const result = await extendNamesTransaction.transaction({
client: mockClient,
connectorClient: mockConnectorClient,
data,
})

expect(mockGetPrice).toHaveBeenCalledWith(mockClient, {
nameOrNames: ['test.eth'],
duration: 31536000,
})
expect(mockRenewNames).toHaveBeenCalledWith(mockConnectorClient, {
nameOrNames: ['test.eth'],
duration: 31536000,
value: BigInt('1020000000000000000'),
})
expect(result).toEqual({
to: '0x123',
data: '0x456',
value: BigInt('1020000000000000000'),
})
})

it('should throw error when price is not found', async () => {
const data = {
names: ['test.eth'],
duration: 31536000,
}

mockGetPrice.mockResolvedValue(null)

await expect(
extendNamesTransaction.transaction({
client: mockClient,
connectorClient: mockConnectorClient,
data,
}),
).rejects.toThrow('No price found')
})
})
})
17 changes: 11 additions & 6 deletions src/transaction-flow/transaction/extendNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { renewNames } from '@ensdomains/ensjs/wallet'

import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'

import { calculateValueWithBuffer, formatDurationOfDates } from '../../utils/utils'
import { calculateValueWithBuffer, formatDurationOfDates, formatExpiry } from '../../utils/utils'

type Data = {
names: string[]
Expand All @@ -31,11 +31,16 @@ const displayItems = (
{
type: 'duration',
label: 'duration',
value: formatDurationOfDates({
startDate: startDateTimestamp ? new Date(startDateTimestamp) : undefined,
endDate: startDateTimestamp ? new Date(startDateTimestamp + duration * 1000) : undefined,
t,
}),
value: {
duration: formatDurationOfDates({
startDate: startDateTimestamp ? new Date(startDateTimestamp) : undefined,
endDate: startDateTimestamp ? new Date(startDateTimestamp + duration * 1000) : undefined,
t,
}),
newExpiry: startDateTimestamp
? formatExpiry(new Date(startDateTimestamp + duration * 1000))
: undefined,
},
},
{
label: 'cost',
Expand Down
16 changes: 12 additions & 4 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ interface TransactionDisplayItemBase {
useRawLabel?: boolean
}

export interface TransactionDisplayItemSingle extends TransactionDisplayItemBase {
type?: 'name' | 'subname' | 'address' | 'duration' | undefined
value: string
}
export type TransactionDisplayItemSingle =
| (TransactionDisplayItemBase & {
type?: 'name' | 'subname' | 'address' | undefined
value: string
})
| (TransactionDisplayItemBase & {
type: 'duration'
value: {
duration: string
newExpiry?: string
}
})

export interface TransactionDisplayItemList extends TransactionDisplayItemBase {
type: 'list'
Expand Down

0 comments on commit 328692a

Please sign in to comment.