Skip to content

Commit

Permalink
feat: recovery queue
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 21, 2023
1 parent ba3b706 commit 8da39d0
Show file tree
Hide file tree
Showing 38 changed files with 1,780 additions and 383 deletions.
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

0 comments on commit 8da39d0

Please sign in to comment.