Skip to content

Commit

Permalink
refactor: recovery loading + trigger (#2850)
Browse files Browse the repository at this point in the history
* refactor: recovery loading + trigger

* refactor: remove slice + move state to context

* fix: rename context + create helper hook
  • Loading branch information
iamacook authored Nov 23, 2023
1 parent c46fcbb commit 2a3ad26
Show file tree
Hide file tree
Showing 51 changed files with 1,135 additions and 1,103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { RecoveryInfo } from '@/components/recovery/RecoveryInfo'
import { RecoveryStatus } from '@/components/recovery/RecoveryStatus'
import { RecoveryType } from '@/components/recovery/RecoveryType'
import { AppRoutes } from '@/config/routes'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

import css from './styles.module.css'

Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/PendingTxs/PendingTxList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { MultisigExecutionInfo, Transaction } from '@safe-global/safe-gatew

import { safeInfoBuilder } from '@/tests/builders/safe'
import { _getTransactionsToDisplay } from './PendingTxsList'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

describe('_getTransactionsToDisplay', () => {
it('should return the recovery queue if it has more than or equal to MAX_TXS items', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/PendingTxs/PendingTxsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import useSafeInfo from '@/hooks/useSafeInfo'
import { useRecoveryQueue } from '@/hooks/useRecoveryQueue'
import { PendingRecoveryListItem } from './PendingRecoveryListItem'
import type { SafeInfo, Transaction } from '@safe-global/safe-gateway-typescript-sdk'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

const MAX_TXS = 4

Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/RecoveryHeader/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers'

import { _RecoveryHeader } from '.'
import { render } from '@/tests/test-utils'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

describe('RecoveryHeader', () => {
it('should not render a widget if the chain does not support recovery', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/RecoveryHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { RecoveryProposalCard } from '@/components/recovery/RecoveryCards/Recove
import { RecoveryInProgressCard } from '@/components/recovery/RecoveryCards/RecoveryInProgressCard'
import { WidgetContainer, WidgetBody } from '../styled'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

export function _RecoveryHeader({
isGuardian,
Expand Down
2 changes: 1 addition & 1 deletion src/components/recovery/CancelRecoveryButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import IconButton from '@mui/material/IconButton'
import CheckWallet from '@/components/common/CheckWallet'
import { TxModalContext } from '@/components/tx-flow'
import { CancelRecoveryFlow } from '@/components/tx-flow/flows/CancelRecovery'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

export function CancelRecoveryButton({
recovery,
Expand Down
6 changes: 5 additions & 1 deletion src/components/recovery/ExecuteRecoveryButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Button, SvgIcon, Tooltip } from '@mui/material'
import { useContext } from 'react'
import type { SyntheticEvent, ReactElement } from 'react'

import RocketIcon from '@/public/images/transactions/rocket.svg'
Expand All @@ -9,7 +10,8 @@ import useOnboard from '@/hooks/wallets/useOnboard'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useRecoveryTxState } from '@/hooks/useRecoveryTxState'
import { Errors, logError } from '@/services/exceptions'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import { RecoveryContext } from '../RecoveryContext'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

export function ExecuteRecoveryButton({
recovery,
Expand All @@ -21,6 +23,7 @@ export function ExecuteRecoveryButton({
const { isExecutable } = useRecoveryTxState(recovery)
const onboard = useOnboard()
const { safe } = useSafeInfo()
const { refetch } = useContext(RecoveryContext)

const onClick = async (e: SyntheticEvent) => {
e.stopPropagation()
Expand All @@ -36,6 +39,7 @@ export function ExecuteRecoveryButton({
chainId: safe.chainId,
args: recovery.args,
delayModifierAddress: recovery.address,
refetchRecoveryData: refetch,
})
} catch (e) {
logError(Errors._812, e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Countdown } from '@/components/common/Countdown'
import RecoveryPending from '@/public/images/common/recovery-pending.svg'
import ExternalLink from '@/components/common/ExternalLink'
import { AppRoutes } from '@/config/routes'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

import css from './styles.module.css'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fireEvent, waitFor } from '@testing-library/react'
import { render } from '@/tests/test-utils'
import { RecoveryInProgressCard } from '../RecoveryInProgressCard'
import { useRecoveryTxState } from '@/hooks/useRecoveryTxState'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

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

Expand Down
127 changes: 127 additions & 0 deletions src/components/recovery/RecoveryContext/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { faker } from '@faker-js/faker'
import { useContext } from 'react'

import { useCurrentChain, useHasFeature } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useWeb3ReadOnly } from '@/hooks/wallets/web3'
import { getDelayModifiers } from '@/services/recovery/delay-modifier'
import { getRecoveryState } from '@/services/recovery/recovery-state'
import { txDispatch, TxEvent } from '@/services/tx/txEvents'
import { chainBuilder } from '@/tests/builders/chains'
import { addressExBuilder, safeInfoBuilder } from '@/tests/builders/safe'
import { act, fireEvent, render, waitFor } from '@/tests/test-utils'
import { RecoveryContext, RecoveryProvider } from '..'
import { getTxDetails } from '@/services/tx/txDetails'

jest.mock('@/services/recovery/delay-modifier')
jest.mock('@/services/recovery/recovery-state')

const mockGetDelayModifiers = getDelayModifiers as jest.MockedFunction<typeof getDelayModifiers>
const mockGetRecoveryState = getRecoveryState as jest.MockedFunction<typeof getRecoveryState>

jest.mock('@/hooks/useSafeInfo')
jest.mock('@/hooks/wallets/web3')
jest.mock('@/hooks/useChains')
jest.mock('@/services/tx/txDetails')

const mockUseSafeInfo = useSafeInfo as jest.MockedFunction<typeof useSafeInfo>
const mockUseWeb3ReadOnly = useWeb3ReadOnly as jest.MockedFunction<typeof useWeb3ReadOnly>
const mockUseCurrentChain = useCurrentChain as jest.MockedFunction<typeof useCurrentChain>
const mockUseHasFeature = useHasFeature as jest.MockedFunction<typeof useHasFeature>
const mockGetTxDetails = getTxDetails as jest.MockedFunction<typeof getTxDetails>

describe('RecoveryContext', () => {
beforeEach(() => {
jest.clearAllMocks()

// Clear memoization cache
getTxDetails.cache.clear?.()
})

it('should refetch manually calling it', async () => {
mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
const chainId = '5'
const safe = safeInfoBuilder()
.with({ chainId, modules: [addressExBuilder().build()] })
.build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)
const chain = chainBuilder().build()
mockUseCurrentChain.mockReturnValue(chain)
const delayModifiers = [{}]
mockGetDelayModifiers.mockResolvedValue(delayModifiers as any)

function Test() {
const { refetch } = useContext(RecoveryContext)

return <button onClick={refetch}>Refetch</button>
}

const { queryByText } = render(
<RecoveryProvider>
<Test />
</RecoveryProvider>,
)

await waitFor(() => {
expect(mockGetDelayModifiers).toHaveBeenCalledTimes(1)
expect(mockGetRecoveryState).toHaveBeenCalledTimes(1)
})

act(() => {
fireEvent.click(queryByText('Refetch')!)
})

await waitFor(() => {
expect(mockGetRecoveryState).toHaveBeenCalledTimes(2)
})

expect(mockGetDelayModifiers).toHaveBeenCalledTimes(1)
})

it('should refetch when interacting with a Delay Modifier', async () => {
mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
const chainId = '5'
const safe = safeInfoBuilder()
.with({ chainId, modules: [addressExBuilder().build()] })
.build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)
const chain = chainBuilder().build()
mockUseCurrentChain.mockReturnValue(chain)
const delayModifierAddress = faker.finance.ethereumAddress()
mockGetDelayModifiers.mockResolvedValue([{ address: delayModifierAddress } as any])
mockGetTxDetails.mockResolvedValue({ txData: { to: { value: delayModifierAddress } } } as any)

render(
<RecoveryProvider>
<></>
</RecoveryProvider>,
)

await waitFor(() => {
expect(mockGetDelayModifiers).toHaveBeenCalledTimes(1)
expect(mockGetRecoveryState).toHaveBeenCalledTimes(1)
})

const txId = faker.string.alphanumeric()

act(() => {
txDispatch(TxEvent.PROCESSED, {
txId,
safeAddress: faker.finance.ethereumAddress(),
})
})

await waitFor(() => {
expect(mockGetTxDetails).toHaveBeenCalledTimes(1)
expect(mockGetTxDetails).toHaveBeenNthCalledWith(1, txId, safe.chainId)

expect(mockGetRecoveryState).toHaveBeenCalledTimes(2)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useHasFeature } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useWeb3ReadOnly } from '@/hooks/wallets/web3'
import { getSpendingLimitModuleAddress } from '@/services/contracts/spendingLimitContracts'
import { getDelayModifiers } from '@/services/recovery/delay-modifier'
import { addressExBuilder, safeInfoBuilder } from '@/tests/builders/safe'
import { act, renderHook } from '@/tests/test-utils'
import { useDelayModifiers } from '../useDelayModifiers'

jest.mock('@/services/recovery/delay-modifier')

const mockGetDelayModifiers = getDelayModifiers as jest.MockedFunction<typeof getDelayModifiers>

jest.mock('@/hooks/useSafeInfo')
jest.mock('@/hooks/wallets/web3')
jest.mock('@/hooks/useChains')

const mockUseSafeInfo = useSafeInfo as jest.MockedFunction<typeof useSafeInfo>
const mockUseWeb3ReadOnly = useWeb3ReadOnly as jest.MockedFunction<typeof useWeb3ReadOnly>
const mockUseHasFeature = useHasFeature as jest.MockedFunction<typeof useHasFeature>

describe('useDelayModifiers', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should not fetch if the current chain does not support Delay Modifiers', async () => {
jest.useFakeTimers()

mockUseHasFeature.mockReturnValue(false)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
const safe = safeInfoBuilder().build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)

const { result } = renderHook(() => useDelayModifiers())

// Give enough time for loading to occur, if it will
await act(async () => {
jest.advanceTimersByTime(10)
})

expect(result.current).toEqual([undefined, undefined, false])
expect(mockGetDelayModifiers).not.toHaveBeenCalledTimes(1)

jest.useRealTimers()
})

it('should not fetch is there is no provider', async () => {
jest.useFakeTimers()

mockUseHasFeature.mockReturnValue(true)
mockUseWeb3ReadOnly.mockReturnValue(undefined)
const safe = safeInfoBuilder().build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)

const { result } = renderHook(() => useDelayModifiers())

// Give enough time for loading to occur, if it will
await act(async () => {
jest.advanceTimersByTime(10)
})

expect(result.current).toEqual([undefined, undefined, false])
expect(mockGetDelayModifiers).not.toHaveBeenCalledTimes(1)

jest.useRealTimers()
})

it('should not fetch if there is no Safe modules enabled', async () => {
jest.useFakeTimers()

mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
const safe = safeInfoBuilder().with({ modules: [] }).build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)

const { result } = renderHook(() => useDelayModifiers())

// Give enough time for loading to occur, if it will
await act(async () => {
jest.advanceTimersByTime(10)
})

expect(result.current).toEqual([undefined, undefined, false])
expect(mockGetDelayModifiers).not.toHaveBeenCalledTimes(1)

jest.useRealTimers()
})

it('should not fetch if only the spending limit is enabled', async () => {
jest.useFakeTimers()

mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
const chainId = '5'
const safe = safeInfoBuilder()
.with({ chainId, modules: [{ value: getSpendingLimitModuleAddress(chainId)! }] })
.build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)

const { result } = renderHook(() => useDelayModifiers())

// Give enough time for loading to occur, if it will
await act(async () => {
jest.advanceTimersByTime(10)
})

expect(result.current).toEqual([undefined, undefined, false])
expect(mockGetDelayModifiers).not.toHaveBeenCalledTimes(1)

jest.useRealTimers()
})

it('should otherwise fetch', async () => {
mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
const chainId = '5'
const safe = safeInfoBuilder()
.with({ chainId, modules: [addressExBuilder().build()] })
.build()
const safeInfo = { safe, safeAddress: safe.address.value }
mockUseSafeInfo.mockReturnValue(safeInfo as any)

renderHook(() => useDelayModifiers())

expect(mockGetDelayModifiers).toHaveBeenCalledTimes(1)
})
})
Loading

0 comments on commit 2a3ad26

Please sign in to comment.