Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: recovery queue #2832

Merged
merged 2 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions public/images/common/clock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions public/images/common/recovery-plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions src/components/common/Countdown/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { _getCountdown } from '.'

describe('getCountdown', () => {
it('should convert 0 seconds to 0 days, 0 hours, and 0 minutes', () => {
const result = _getCountdown(0)
expect(result).toEqual({ days: 0, hours: 0, minutes: 0 })
})

it('should convert 3600 seconds to 0 days, 1 hour, and 0 minutes', () => {
const result = _getCountdown(3600)
expect(result).toEqual({ days: 0, hours: 1, minutes: 0 })
})

it('should convert 86400 seconds to 1 day, 0 hours, and 0 minutes', () => {
const result = _getCountdown(86400)
expect(result).toEqual({ days: 1, hours: 0, minutes: 0 })
})

it('should convert 123456 seconds to 1 day, 10 hours, and 17 minutes', () => {
const result = _getCountdown(123456)
expect(result).toEqual({ days: 1, hours: 10, minutes: 17 })
})
})
49 changes: 49 additions & 0 deletions src/components/common/Countdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Typography, Box } from '@mui/material'
import type { ReactElement } from 'react'

export function _getCountdown(seconds: number): { days: number; hours: number; minutes: number } {
const MINUTE_IN_SECONDS = 60
const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS
const DAY_IN_SECONDS = 24 * HOUR_IN_SECONDS

const days = Math.floor(seconds / DAY_IN_SECONDS)

const remainingSeconds = seconds % DAY_IN_SECONDS
const hours = Math.floor(remainingSeconds / HOUR_IN_SECONDS)
const minutes = Math.floor((remainingSeconds % HOUR_IN_SECONDS) / MINUTE_IN_SECONDS)

return { days, hours, minutes }
}

export function Countdown({ seconds }: { seconds: number }): ReactElement | null {
if (seconds <= 0) {
return null
}

const { days, hours, minutes } = _getCountdown(seconds)

return (
<Box display="flex" gap={1}>
<TimeLeft value={days} unit="day" />
<TimeLeft value={hours} unit="hr" />
<TimeLeft value={minutes} unit="min" />
</Box>
)
}

function TimeLeft({ value, unit }: { value: number; unit: string }): ReactElement | null {
if (value === 0) {
return null
}

return (
<div>
<Typography fontWeight={700} component="span">
{value}
</Typography>{' '}
<Typography color="primary.light" component="span">
{value === 1 ? unit : `${unit}s`}
</Typography>
</div>
)
}
191 changes: 110 additions & 81 deletions src/components/dashboard/RecoveryInProgress/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,86 @@ import { render } from '@testing-library/react'
import { BigNumber } from 'ethers'

import { _RecoveryInProgress } from '.'
import type { RecoveryQueueItem, RecoveryState } from '@/store/recoverySlice'
import { useRecoveryTxState } from '@/hooks/useRecoveryTxState'
import type { RecoveryQueueItem } from '@/store/recoverySlice'

jest.mock('@/hooks/useRecoveryTxState')

const mockUseRecoveryTxState = useRecoveryTxState as jest.MockedFunction<typeof useRecoveryTxState>

describe('RecoveryInProgress', () => {
beforeEach(() => {
jest.resetAllMocks()
})

it('should return null if the chain does not support recovery', () => {
const result = render(
<_RecoveryInProgress
supportsRecovery={false}
blockTimestamp={0}
recovery={[{ queue: [{ timestamp: 0 } as RecoveryQueueItem] }] as RecoveryState}
/>,
)

expect(result.container).toBeEmptyDOMElement()
})
mockUseRecoveryTxState.mockReturnValue({} as any)

it('should return a loader if there is no block timestamp', () => {
const result = render(
<_RecoveryInProgress
supportsRecovery={false}
blockTimestamp={undefined}
recovery={[{ queue: [{ timestamp: 0 } as RecoveryQueueItem] }] as RecoveryState}
timestamp={0}
queuedTxs={[{ timestamp: BigNumber.from(0) } as RecoveryQueueItem]}
/>,
)

expect(result.container).toBeEmptyDOMElement()
})

it('should return null if there are no delayed transactions', () => {
const result = render(
<_RecoveryInProgress
supportsRecovery={true}
blockTimestamp={69420}
recovery={[{ queue: [] as Array<RecoveryQueueItem> }] as RecoveryState}
/>,
)
mockUseRecoveryTxState.mockReturnValue({} as any)

const result = render(<_RecoveryInProgress supportsRecovery={true} timestamp={69420} queuedTxs={[]} />)

expect(result.container).toBeEmptyDOMElement()
})

it('should return null if all the delayed transactions are expired and invalid', () => {
mockUseRecoveryTxState.mockReturnValue({} as any)

const result = render(
<_RecoveryInProgress
supportsRecovery={true}
blockTimestamp={69420}
recovery={
[
{
queue: [
{
timestamp: 0,
validFrom: BigNumber.from(69),
expiresAt: BigNumber.from(420),
} as RecoveryQueueItem,
],
},
] as RecoveryState
}
timestamp={69420}
queuedTxs={[
{
timestamp: BigNumber.from(0),
validFrom: BigNumber.from(69),
expiresAt: BigNumber.from(420),
} as RecoveryQueueItem,
]}
/>,
)

expect(result.container).toBeEmptyDOMElement()
})

it('should return the countdown of the latest non-expired/invalid transactions if none are non-expired/valid', () => {
const mockBlockTimestamp = 69420
it('should return the countdown of the next non-expired/invalid transactions if none are non-expired/valid', () => {
mockUseRecoveryTxState.mockReturnValue({
remainingSeconds: 69 * 420 * 1337,
isExecutable: false,
isNext: true,
} as any)

const mockBlockTimestamp = BigNumber.from(69420)

const { queryByText } = render(
<_RecoveryInProgress
supportsRecovery={true}
blockTimestamp={mockBlockTimestamp}
recovery={
[
{
queue: [
{
timestamp: mockBlockTimestamp + 1,
validFrom: BigNumber.from(mockBlockTimestamp + 1), // Invalid
expiresAt: BigNumber.from(mockBlockTimestamp + 1), // Non-expired
} as RecoveryQueueItem,
{
// Older - should render this
timestamp: mockBlockTimestamp,
validFrom: BigNumber.from(mockBlockTimestamp * 4), // Invalid
expiresAt: null, // Non-expired
} as RecoveryQueueItem,
],
},
] as RecoveryState
}
timestamp={mockBlockTimestamp.toNumber()}
queuedTxs={[
{
timestamp: mockBlockTimestamp.add(1),
validFrom: mockBlockTimestamp.add(1), // Invalid
expiresAt: mockBlockTimestamp.add(1), // Non-expired
} as RecoveryQueueItem,
{
// Older - should render this
timestamp: mockBlockTimestamp,
validFrom: mockBlockTimestamp.mul(4), // Invalid
expiresAt: null, // Non-expired
} as RecoveryQueueItem,
]}
/>,
)

Expand All @@ -107,39 +94,35 @@ describe('RecoveryInProgress', () => {
expect(queryByText(unit, { exact: false })).toBeInTheDocument()
})
// Days
expect(queryByText('2')).toBeInTheDocument()
expect(queryByText('448')).toBeInTheDocument()
// Hours
expect(queryByText('9')).toBeInTheDocument()
expect(queryByText('10')).toBeInTheDocument()
// Mins
expect(queryByText('51')).toBeInTheDocument()
})

it('should return the info of the latest non-expired/valid transactions', () => {
const mockBlockTimestamp = 69420
it('should return the info of the next non-expired/valid transaction', () => {
mockUseRecoveryTxState.mockReturnValue({ isExecutable: true, remainingSeconds: 0 } as any)

const mockBlockTimestamp = BigNumber.from(69420)

const { queryByText } = render(
<_RecoveryInProgress
supportsRecovery={true}
blockTimestamp={mockBlockTimestamp}
recovery={
[
{
queue: [
{
timestamp: mockBlockTimestamp - 1,
validFrom: BigNumber.from(mockBlockTimestamp - 1), // Invalid
expiresAt: BigNumber.from(mockBlockTimestamp - 1), // Non-expired
} as RecoveryQueueItem,
{
// Older - should render this
timestamp: mockBlockTimestamp - 2,
validFrom: BigNumber.from(mockBlockTimestamp - 1), // Invalid
expiresAt: null, // Non-expired
} as RecoveryQueueItem,
],
},
] as RecoveryState
}
timestamp={mockBlockTimestamp.toNumber()}
queuedTxs={[
{
timestamp: mockBlockTimestamp.sub(1),
validFrom: mockBlockTimestamp.sub(1), // Invalid
expiresAt: mockBlockTimestamp.sub(1), // Non-expired
} as RecoveryQueueItem,
{
// Older - should render this
timestamp: mockBlockTimestamp.sub(2),
validFrom: mockBlockTimestamp.sub(1), // Invalid
expiresAt: null, // Non-expired
} as RecoveryQueueItem,
]}
/>,
)

Expand All @@ -150,4 +133,50 @@ describe('RecoveryInProgress', () => {
expect(queryByText(unit, { exact: false })).not.toBeInTheDocument()
})
})

it('should return the intemediary info for of the queued, non-expired/valid transactions', () => {
mockUseRecoveryTxState.mockReturnValue({
isExecutable: false,
isNext: false,
remainingSeconds: 69 * 420 * 1337,
} as any)

const mockBlockTimestamp = BigNumber.from(69420)

const { queryByText } = render(
<_RecoveryInProgress
supportsRecovery={true}
timestamp={mockBlockTimestamp.toNumber()}
queuedTxs={[
{
timestamp: mockBlockTimestamp.sub(1),
validFrom: mockBlockTimestamp.sub(1), // Invalid
expiresAt: mockBlockTimestamp.sub(1), // Non-expired
} as RecoveryQueueItem,
{
// Older - should render this
timestamp: mockBlockTimestamp.sub(2),
validFrom: mockBlockTimestamp.sub(1), // Invalid
expiresAt: null, // Non-expired
} as RecoveryQueueItem,
]}
/>,
)

expect(queryByText('Account recovery in progress')).toBeInTheDocument()
expect(
queryByText(
'The recovery process has started. This Account can be recovered after previous attempts are executed or skipped and the delay period has passed:',
),
)
;['day', 'hr', 'min'].forEach((unit) => {
// May be pluralised
expect(queryByText(unit, { exact: false })).toBeInTheDocument()
})
// Days
expect(queryByText('448')).toBeInTheDocument()
// Hours
expect(queryByText('10')).toBeInTheDocument()
// Mins
})
})
Loading
Loading