Skip to content

Commit

Permalink
refactor: add event bus
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 28, 2023
1 parent ba80fda commit 534395c
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 206 deletions.
6 changes: 2 additions & 4 deletions src/components/recovery/CancelRecoveryButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import CheckWallet from '@/components/common/CheckWallet'
import { TxModalContext } from '@/components/tx-flow'
import { CancelRecoveryFlow } from '@/components/tx-flow/flows/CancelRecovery'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import { dispatchRecoverySkipExpired } from '@/services/tx/tx-sender'
import { dispatchRecoverySkipExpired } from '@/services/recovery/recovery-sender'
import useSafeInfo from '@/hooks/useSafeInfo'
import useOnboard from '@/hooks/wallets/useOnboard'
import { trackError, Errors } from '@/services/exceptions'
import { asError } from '@/services/exceptions/utils'
import { RecoveryContext } from '../RecoveryContext'
import { useIsGuardian } from '@/hooks/useIsGuardian'
import { useRecoveryTxState } from '@/hooks/useRecoveryTxState'
import { RecoveryListItemContext } from '../RecoveryListItem/RecoveryListItemContext'
Expand All @@ -33,7 +32,6 @@ export function CancelRecoveryButton({
const { setTxFlow } = useContext(TxModalContext)
const onboard = useOnboard()
const { safe } = useSafeInfo()
const { refetch } = useContext(RecoveryContext)

const onClick = async (e: SyntheticEvent) => {
e.stopPropagation()
Expand All @@ -47,7 +45,7 @@ export function CancelRecoveryButton({
onboard,
chainId: safe.chainId,
delayModifierAddress: recovery.address,
refetchRecoveryData: refetch,
recoveryTxHash: recovery.args.txHash,
})
} catch (_err) {
const err = asError(_err)
Expand Down
13 changes: 1 addition & 12 deletions src/components/recovery/ExecuteRecoveryButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import type { SyntheticEvent, ReactElement } from 'react'
import RocketIcon from '@/public/images/transactions/rocket.svg'
import IconButton from '@mui/material/IconButton'
import CheckWallet from '@/components/common/CheckWallet'
import { dispatchRecoveryExecution } from '@/services/tx/tx-sender'
import { dispatchRecoveryExecution } from '@/services/recovery/recovery-sender'
import useOnboard from '@/hooks/wallets/useOnboard'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useRecoveryTxState } from '@/hooks/useRecoveryTxState'
import { Errors, trackError } from '@/services/exceptions'
import { asError } from '@/services/exceptions/utils'
import { RecoveryContext } from '../RecoveryContext'
import { RecoveryListItemContext } from '../RecoveryListItem/RecoveryListItemContext'
import type { RecoveryQueueItem } from '@/services/recovery/recovery-state'

Expand All @@ -24,10 +23,8 @@ export function ExecuteRecoveryButton({
}): ReactElement {
const { setSubmitError } = useContext(RecoveryListItemContext)
const { isExecutable, isPending } = useRecoveryTxState(recovery)
const { setPending } = useContext(RecoveryContext)
const onboard = useOnboard()
const { safe } = useSafeInfo()
const { refetch } = useContext(RecoveryContext)

const onClick = async (e: SyntheticEvent) => {
e.stopPropagation()
Expand All @@ -37,26 +34,18 @@ export function ExecuteRecoveryButton({
return
}

setPending((prev) => ({ ...prev, [recovery.transactionHash]: true }))

try {
await dispatchRecoveryExecution({
onboard,
chainId: safe.chainId,
args: recovery.args,
delayModifierAddress: recovery.address,
refetchRecoveryData: refetch,
})
} catch (_err) {
const err = asError(_err)

trackError(Errors._812, e)
setSubmitError(err)
} finally {
setPending((prev) => {
const { [recovery.transactionHash]: _, ...rest } = prev
return rest
})
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { RecoveryEvent, recoveryDispatch } from '@/services/recovery/recoveryEvents'
import { PendingStatus } from '@/store/pendingTxsSlice'
import { act, renderHook } from '@/tests/test-utils'
import { faker } from '@faker-js/faker'
import { useRecoveryPendingTxs } from '../useRecoveryPendingTxs'

describe('useRecoveryPendingTxs', () => {
it('should set pending status to SUBMITTING when EXECUTING event is emitted', () => {
const delayModifierAddress = faker.finance.ethereumAddress()
const transactionHash = faker.string.hexadecimal()
const { result } = renderHook(() => useRecoveryPendingTxs())

expect(result.current).toStrictEqual({})

act(() => {
recoveryDispatch(RecoveryEvent.EXECUTING, { moduleAddress: delayModifierAddress, transactionHash })
})

expect(result.current).toStrictEqual({
[transactionHash]: PendingStatus.SUBMITTING,
})
})

it('should set pending status to PROCESSING when PROCESSING event is emitted', () => {
const delayModifierAddress = faker.finance.ethereumAddress()
const transactionHash = faker.string.hexadecimal()
const { result } = renderHook(() => useRecoveryPendingTxs())

expect(result.current).toStrictEqual({})

act(() => {
recoveryDispatch(RecoveryEvent.PROCESSING, { moduleAddress: delayModifierAddress, transactionHash })
})

expect(result.current).toStrictEqual({
[transactionHash]: PendingStatus.PROCESSING,
})
})

it('should set remove the pending status when REVERTED event is emitted', () => {
const delayModifierAddress = faker.finance.ethereumAddress()
const transactionHash = faker.string.hexadecimal()
const { result } = renderHook(() => useRecoveryPendingTxs())

expect(result.current).toStrictEqual({})

act(() => {
recoveryDispatch(RecoveryEvent.EXECUTING, { moduleAddress: delayModifierAddress, transactionHash })
recoveryDispatch(RecoveryEvent.REVERTED, {
moduleAddress: delayModifierAddress,
transactionHash,
error: new Error(),
})
})

expect(result.current).toStrictEqual({})
})

it('should set remove the pending status when PROCESSED event is emitted', () => {
const delayModifierAddress = faker.finance.ethereumAddress()
const transactionHash = faker.string.hexadecimal()
const { result } = renderHook(() => useRecoveryPendingTxs())

expect(result.current).toStrictEqual({})

act(() => {
recoveryDispatch(RecoveryEvent.EXECUTING, { moduleAddress: delayModifierAddress, transactionHash })
recoveryDispatch(RecoveryEvent.PROCESSED, { moduleAddress: delayModifierAddress, transactionHash })
})

expect(result.current).toStrictEqual({})
})

// No need to test RecoveryEvent.FAILED as pending status is not set before it is dispatched
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { faker } from '@faker-js/faker'
import { useContext } from 'react'

import { useCurrentChain, useHasFeature } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
Expand All @@ -13,7 +12,8 @@ import useTxHistory from '@/hooks/useTxHistory'
import { getRecoveryDelayModifiers } from '@/services/recovery/delay-modifier'
import { useAppDispatch } from '@/store'
import { txHistorySlice } from '@/store/txHistorySlice'
import { RecoveryProvider, RecoveryContext } from '..'
import { RecoveryProvider } from '..'
import { recoveryDispatch, RecoveryEvent } from '@/services/recovery/recoveryEvents'

jest.mock('@/services/recovery/delay-modifier')
jest.mock('@/services/recovery/recovery-state')
Expand Down Expand Up @@ -76,7 +76,7 @@ describe('useRecoveryState', () => {
jest.advanceTimersByTime(10)
})

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

jest.useRealTimers()
Expand Down Expand Up @@ -120,7 +120,7 @@ describe('useRecoveryState', () => {
jest.advanceTimersByTime(10)
})

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

jest.useRealTimers()
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('useRecoveryState', () => {
})
})

it('should refetch when interacting with a Delay Modifier', async () => {
it('should refetch when interacting with a Delay Modifier via the Safe', async () => {
mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
Expand Down Expand Up @@ -227,7 +227,7 @@ describe('useRecoveryState', () => {
})
})

it('should refetch manually calling it', async () => {
it('should refetch when interacting with a Delay Modifier as a Guardian', async () => {
mockUseHasFeature.mockReturnValue(true)
const provider = {}
mockUseWeb3ReadOnly.mockReturnValue(provider as any)
Expand All @@ -239,18 +239,12 @@ describe('useRecoveryState', () => {
mockUseSafeInfo.mockReturnValue(safeInfo as any)
const chain = chainBuilder().build()
mockUseCurrentChain.mockReturnValue(chain)
const delayModifiers = [{}]
mockGetRecoveryDelayModifiers.mockResolvedValue(delayModifiers as any)

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

return <button onClick={refetch}>Refetch</button>
}
const delayModifierAddress = faker.finance.ethereumAddress()
mockGetRecoveryDelayModifiers.mockResolvedValue([{ address: delayModifierAddress } as any])

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

Expand All @@ -259,14 +253,13 @@ describe('useRecoveryState', () => {
expect(mockGetRecoveryState).toHaveBeenCalledTimes(1)
})

act(() => {
fireEvent.click(queryByText('Refetch')!)
recoveryDispatch(RecoveryEvent.PROCESSED, {
moduleAddress: delayModifierAddress,
recoveryTxHash: faker.string.hexadecimal(),
})

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

expect(mockGetRecoveryDelayModifiers).toHaveBeenCalledTimes(1)
})
})
24 changes: 7 additions & 17 deletions src/components/recovery/RecoveryContext/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
import { createContext, useContext, useState } from 'react'
import type { ReactElement, ReactNode, Dispatch, SetStateAction } from 'react'
import { createContext, useContext } from 'react'
import type { ReactElement, ReactNode } from 'react'

import { useRecoveryState } from './useRecoveryState'
import { useRecoveryDelayModifiers } from './useRecoveryDelayModifiers'
import type { AsyncResult } from '@/hooks/useAsync'
import type { RecoveryState } from '@/services/recovery/recovery-state'

type PendingRecoveryTransactions = { [txHash: string]: boolean }
import { useRecoveryPendingTxs } from './useRecoveryPendingTxs'

// State of current Safe, populated on load
export const RecoveryContext = createContext<{
state: AsyncResult<RecoveryState>
refetch: () => void
pending: PendingRecoveryTransactions
setPending: Dispatch<SetStateAction<PendingRecoveryTransactions>>
pending: ReturnType<typeof useRecoveryPendingTxs>
}>({
state: [undefined, undefined, false],
refetch: () => {},
pending: {},
setPending: () => {},
})

export function RecoveryProvider({ children }: { children: ReactNode }): ReactElement {
const [delayModifiers, delayModifiersError, delayModifiersLoading] = useRecoveryDelayModifiers()
const {
data: [recoveryState, recoveryStateError, recoveryStateLoading],
refetch,
} = useRecoveryState(delayModifiers)
const [pending, setPending] = useState<PendingRecoveryTransactions>({})
const [recoveryState, recoveryStateError, recoveryStateLoading] = useRecoveryState(delayModifiers)
const pending = useRecoveryPendingTxs()

const data = recoveryState
const error = delayModifiersError || recoveryStateError
const loading = delayModifiersLoading || recoveryStateLoading

return (
<RecoveryContext.Provider value={{ state: [data, error, loading], refetch, pending, setPending }}>
{children}
</RecoveryContext.Provider>
<RecoveryContext.Provider value={{ state: [data, error, loading], pending }}>{children}</RecoveryContext.Provider>
)
}

Expand Down
44 changes: 44 additions & 0 deletions src/components/recovery/RecoveryContext/useRecoveryPendingTxs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { RecoveryEvent, recoverySubscribe } from '@/services/recovery/recoveryEvents'
import { PendingStatus } from '@/store/pendingTxsSlice'
import { useEffect, useState } from 'react'

type PendingRecoveryTransactions = { [recoveryTxHash: string]: PendingStatus }

const pendingStatuses: { [key in RecoveryEvent]: PendingStatus | null } = {
[RecoveryEvent.EXECUTING]: PendingStatus.SUBMITTING,
[RecoveryEvent.PROCESSING]: PendingStatus.PROCESSING,
[RecoveryEvent.REVERTED]: null,
[RecoveryEvent.PROCESSED]: null,
[RecoveryEvent.FAILED]: null,
}

export function useRecoveryPendingTxs(): PendingRecoveryTransactions {
const [pending, setPending] = useState<PendingRecoveryTransactions>({})

useEffect(() => {
const unsubFns = Object.entries(pendingStatuses).map(([event, status]) =>
recoverySubscribe(event as RecoveryEvent, (detail) => {
const recoveryTxHash = 'recoveryTxHash' in detail && detail.recoveryTxHash

if (!recoveryTxHash) {
return
}

setPending((prev) => {
if (status === null) {
const { [recoveryTxHash]: _, ...rest } = prev
return rest
}

return { ...prev, [recoveryTxHash]: status }
})
}),
)

return () => {
unsubFns.forEach((unsub) => unsub())
}
}, [])

return pending
}
Loading

0 comments on commit 534395c

Please sign in to comment.