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

refactor: recovery loading + trigger #2850

Merged
merged 5 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
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/RecoveryLoaderContext'

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/RecoveryLoaderContext'

export function _RecoveryHeader({
isGuardian,
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 { RecoveryLoaderContext } from '../RecoveryLoaderContext'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryLoaderContext'

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(RecoveryLoaderContext)

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/RecoveryLoaderContext'

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/RecoveryLoaderContext'

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

Expand Down
2 changes: 1 addition & 1 deletion src/components/recovery/RecoveryDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import useSafeInfo from '@/hooks/useSafeInfo'
import ErrorMessage from '@/components/tx/ErrorMessage'
import { RecoverySigners } from '../RecoverySigners'
import { Errors, logError } from '@/services/exceptions'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryLoaderContext'

import txDetailsCss from '@/components/transactions/TxDetails/styles.module.css'
import summaryCss from '@/components/transactions/TxDetails/Summary/styles.module.css'
Expand Down
5 changes: 2 additions & 3 deletions src/components/recovery/RecoveryList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import type { ReactElement } from 'react'

import { TxListGrid } from '@/components/transactions/TxList'
import { RecoveryListItem } from '@/components/recovery/RecoveryListItem'
import { selectRecoveryQueues } from '@/store/recoverySlice'
import { useAppSelector } from '@/store'
import { useRecoveryQueue } from '@/hooks/useRecoveryQueue'

import labelCss from '@/components/transactions/GroupLabel/styles.module.css'

export function RecoveryList(): ReactElement | null {
const queue = useAppSelector(selectRecoveryQueues)
const queue = useRecoveryQueue()

if (queue.length === 0) {
return null
Expand Down
2 changes: 1 addition & 1 deletion src/components/recovery/RecoveryListItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ReactElement } from 'react'
import txListItemCss from '@/components/transactions/TxListItem/styles.module.css'
import { RecoverySummary } from '../RecoverySummary'
import { RecoveryDetails } from '../RecoveryDetails'
import type { RecoveryQueueItem } from '@/store/recoverySlice'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryLoaderContext'

export function RecoveryListItem({ item }: { item: RecoveryQueueItem }): ReactElement {
return (
Expand Down
127 changes: 127 additions & 0 deletions src/components/recovery/RecoveryLoaderContext/__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 { RecoveryLoaderContext, RecoveryLoaderProvider } 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('RecoveryLoaderContext', () => {
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(RecoveryLoaderContext)

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

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

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(
<RecoveryLoaderProvider>
<></>
</RecoveryLoaderProvider>,
)

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
Loading