diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts
index b36103402..a4f906b5b 100644
--- a/e2e/specs/stateless/extendNames.spec.ts
+++ b/e2e/specs/stateless/extendNames.spec.ts
@@ -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()
@@ -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()
@@ -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()
@@ -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()
diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx
index a33e334ee..0144df1c1 100644
--- a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx
+++ b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx
@@ -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`
@@ -231,29 +231,22 @@ 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 (
- {value}
+ {value.duration}
- {t('transaction.extendNames.newExpiry', { date: formatExpiry(date) })}
+ {t('transaction.extendNames.newExpiry', { date: value.newExpiry })}
)
@@ -261,6 +254,10 @@ const DurationValue = ({ value }: { value: string | undefined }) => {
const DisplayItemValue = (props: Omit) => {
const { value, type } = props as TransactionDisplayItem
+ if (type === 'duration') {
+ return
+ }
+
if (type === 'address') {
return
}
@@ -276,9 +273,6 @@ const DisplayItemValue = (props: Omit) => {
if (type === 'records') {
return
}
- if (type === 'duration') {
- return
- }
return {value}
}
diff --git a/src/transaction-flow/transaction/extendNames.test.ts b/src/transaction-flow/transaction/extendNames.test.ts
new file mode 100644
index 000000000..7081c2352
--- /dev/null
+++ b/src/transaction-flow/transaction/extendNames.test.ts
@@ -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')
+ })
+ })
+})
diff --git a/src/transaction-flow/transaction/extendNames.ts b/src/transaction-flow/transaction/extendNames.ts
index 786cbd062..550554472 100644
--- a/src/transaction-flow/transaction/extendNames.ts
+++ b/src/transaction-flow/transaction/extendNames.ts
@@ -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[]
@@ -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',
diff --git a/src/types/index.ts b/src/types/index.ts
index ae8dd814d..63dee4de2 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -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'