Skip to content

Commit

Permalink
feat: add RecoveryEvent.SUCCESS notifications (#2944)
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook authored Dec 1, 2023
1 parent e0e6d15 commit 6db5d17
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('useRecoveryPendingTxs', () => {
const delayModifierAddress = faker.finance.ethereumAddress()
const txHash = faker.string.hexadecimal()
const recoveryTxHash = faker.string.hexadecimal()
const txType = faker.helpers.enumValue(RecoveryTxType)
const { result } = renderHook(() => useRecoveryPendingTxs())

expect(result.current).toStrictEqual({})
Expand All @@ -18,12 +19,12 @@ describe('useRecoveryPendingTxs', () => {
moduleAddress: delayModifierAddress,
txHash,
recoveryTxHash,
txType: faker.helpers.enumValue(RecoveryTxType),
txType,
})
})

expect(result.current).toStrictEqual({
[recoveryTxHash]: RecoveryEvent.PROCESSING,
[recoveryTxHash]: { status: RecoveryEvent.PROCESSING, txType },
})
})

Expand Down Expand Up @@ -80,7 +81,7 @@ describe('useRecoveryPendingTxs', () => {
})

expect(result.current).toStrictEqual({
[recoveryTxHash]: RecoveryEvent.PROCESSED,
[recoveryTxHash]: { status: RecoveryEvent.PROCESSED, txType },
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { faker } from '@faker-js/faker'

import { renderHook } from '@/tests/test-utils'
import { useRecoverySuccessEvents } from '../useRecoverySuccessEvents'
import { recoveryDispatch, RecoveryEvent, RecoveryTxType } from '@/services/recovery/recoveryEvents'

jest.mock('@/services/recovery/recoveryEvents', () => ({
...jest.requireActual('@/services/recovery/recoveryEvents'),
recoveryDispatch: jest.fn(),
}))

const mockRecoveryDispatch = recoveryDispatch as jest.MockedFunction<typeof recoveryDispatch>

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

it('should not dispatch SUCCESS event if recoveryState is not defined', () => {
const pending = {
[faker.string.hexadecimal()]: {},
}

const { result } = renderHook(() => useRecoverySuccessEvents(pending as any))

expect(result.current).toBeUndefined()

expect(mockRecoveryDispatch).not.toHaveBeenCalled()
})

it('should not dispatch SUCCESS event if recoveryState is empty', () => {
const pending = {
[faker.string.hexadecimal()]: {},
}
const recoveryState = [] as any[]

const { result } = renderHook(() => useRecoverySuccessEvents(pending as any, recoveryState))

expect(result.current).toBeUndefined()

expect(mockRecoveryDispatch).not.toHaveBeenCalled()
})

it('should not dispatch SUCCESS event if pending is empty', () => {
const pending = {}
const recoveryState = [{ queue: [] }]

const { result } = renderHook(() => useRecoverySuccessEvents(pending, recoveryState as any))

expect(result.current).toBeUndefined()

expect(mockRecoveryDispatch).not.toHaveBeenCalled()
})

it('should not dispatch SUCCESS event if pending is not PROCESSED', () => {
const pending = {
[faker.string.hexadecimal()]: {
status: faker.helpers.arrayElement([RecoveryEvent.PROCESSING, RecoveryEvent.FAILED]),
},
}
const recoveryState = [{ queue: [] }]

renderHook(() => useRecoverySuccessEvents(pending as any, recoveryState as any))

expect(mockRecoveryDispatch).not.toHaveBeenCalled()
})

it('should dispatch SUCCESS event if pending is PROCESSED and txType is PROPOSAL', () => {
const pending = {
[faker.string.hexadecimal()]: {
status: RecoveryEvent.PROCESSED,
txType: RecoveryTxType.PROPOSAL,
},
}
const recoveryState = [{ queue: [] }]

renderHook(() => useRecoverySuccessEvents(pending as any, recoveryState as any))

expect(mockRecoveryDispatch).toHaveBeenCalledWith(RecoveryEvent.SUCCESS, {
recoveryTxHash: expect.any(String),
txType: RecoveryTxType.PROPOSAL,
})
})

it('should not dispatch SUCCESS event if pending is PROCESSED and txType is not PROPOSAL and there is a queue', () => {
const recoveryTxHash = faker.string.hexadecimal()
const pending = {
[recoveryTxHash]: {
status: RecoveryEvent.PROCESSED,
txType: faker.helpers.arrayElement([RecoveryTxType.EXECUTION, RecoveryTxType.SKIP_EXPIRED]),
},
}
const recoveryState = [
{
queue: [
{
args: {
txHash: recoveryTxHash,
},
},
],
},
]

renderHook(() => useRecoverySuccessEvents(pending as any, recoveryState as any))

expect(mockRecoveryDispatch).not.toHaveBeenCalled()
})

it('should dispatch SUCCESS event if pending is PROCESSED and pending transaction is not queued', () => {
const pending = {
[faker.string.hexadecimal()]: {
status: RecoveryEvent.PROCESSED,
txType: RecoveryTxType.PROPOSAL,
},
}
const recoveryState = [{ queue: [] }]

renderHook(() => useRecoverySuccessEvents(pending as any, recoveryState as any))

expect(mockRecoveryDispatch).toHaveBeenCalledWith(RecoveryEvent.SUCCESS, {
recoveryTxHash: expect.any(String),
txType: RecoveryTxType.PROPOSAL,
})
})
})
3 changes: 3 additions & 0 deletions src/components/recovery/RecoveryContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ReactElement, ReactNode } from 'react'
import { useRecoveryState } from './useRecoveryState'
import { useRecoveryDelayModifiers } from './useRecoveryDelayModifiers'
import { useRecoveryPendingTxs } from './useRecoveryPendingTxs'
import { useRecoverySuccessEvents } from './useRecoverySuccessEvents'
import type { AsyncResult } from '@/hooks/useAsync'
import type { RecoveryState } from '@/services/recovery/recovery-state'

Expand All @@ -21,6 +22,8 @@ export function RecoveryProvider({ children }: { children: ReactNode }): ReactEl
const [recoveryState, recoveryStateError, recoveryStateLoading] = useRecoveryState(delayModifiers)
const pending = useRecoveryPendingTxs()

useRecoverySuccessEvents(pending, recoveryState)

const data = recoveryState
const error = delayModifiersError || recoveryStateError
const loading = delayModifiersLoading || recoveryStateLoading
Expand Down
19 changes: 16 additions & 3 deletions src/components/recovery/RecoveryContext/useRecoveryPendingTxs.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { useEffect, useState } from 'react'

import { RecoveryEvent, recoverySubscribe } from '@/services/recovery/recoveryEvents'
import type { RecoveryTxType } from '@/services/recovery/recoveryEvents'

export type PendingRecoveryTransactions = { [recoveryTxHash: string]: RecoveryEvent }
type PendingRecoveryTransactions = {
[recoveryTxHash: string]: {
status: RecoveryEvent
txType: RecoveryTxType
}
}

const pendingStatuses: { [key in RecoveryEvent]: RecoveryEvent | null } = {
[RecoveryEvent.PROCESSING_BY_SMART_CONTRACT_WALLET]: null,
[RecoveryEvent.PROCESSING]: RecoveryEvent.PROCESSING,
[RecoveryEvent.PROCESSED]: RecoveryEvent.PROCESSED,
[RecoveryEvent.SUCCESS]: null,
[RecoveryEvent.REVERTED]: null,
[RecoveryEvent.FAILED]: null,
}

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

useEffect(() => {
Expand All @@ -30,7 +37,13 @@ export function useRecoveryPendingTxs(): PendingRecoveryTransactions {
return rest
}

return { ...prev, [recoveryTxHash]: status }
return {
...prev,
[recoveryTxHash]: {
txType: detail.txType,
status,
},
}
})
}),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useEffect } from 'react'

import { RecoveryEvent, RecoveryTxType, recoveryDispatch } from '@/services/recovery/recoveryEvents'
import type { RecoveryState } from '@/services/recovery/recovery-state'
import type { useRecoveryPendingTxs } from './useRecoveryPendingTxs'

export function useRecoverySuccessEvents(
pending: ReturnType<typeof useRecoveryPendingTxs>,
recoveryState?: RecoveryState,
): void {
useEffect(() => {
const pendingEntries = Object.entries(pending)

if (!recoveryState || recoveryState.length === 0 || pendingEntries.length === 0) {
return
}

pendingEntries.forEach(([recoveryTxHash, { txType, status }]) => {
// Transaction successfully executed, waiting for recovery state to be loaded again
if (status !== RecoveryEvent.PROCESSED) {
return
}

const isQueued = recoveryState.some(({ queue }) => queue.some(({ args }) => args.txHash === recoveryTxHash))

if (isQueued) {
// Only proposals should appear in the queue
if (txType === RecoveryTxType.PROPOSAL) {
recoveryDispatch(RecoveryEvent.SUCCESS, {
recoveryTxHash,
txType,
})
}
} else {
// Executions/cancellations are removed from the queue
recoveryDispatch(RecoveryEvent.SUCCESS, {
recoveryTxHash,
txType,
})
}
})
}, [pending, recoveryState])
}
2 changes: 1 addition & 1 deletion src/components/recovery/RecoveryStatus/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const RecoveryStatus = ({ recovery }: { recovery: RecoveryQueueItem }): R
const { isExecutable, isExpired } = useRecoveryTxState(recovery)
const { pending } = useContext(RecoveryContext)

const pendingTxStatus = pending?.[recovery.args.txHash]
const pendingTxStatus = pending?.[recovery.args.txHash]?.status

const status = pendingTxStatus ? (
<>
Expand Down
18 changes: 13 additions & 5 deletions src/hooks/useRecoveryTxNotification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import { RecoveryEvent, RecoveryTxType, recoverySubscribe } from '@/services/rec
import { getExplorerLink } from '@/utils/gateway'
import { useCurrentChain } from './useChains'

const SUCCESS_EVENTS = [
RecoveryEvent.PROCESSING_BY_SMART_CONTRACT_WALLET,
RecoveryEvent.PROCESSED,
RecoveryEvent.SUCCESS,
]

const RecoveryTxNotifications = {
[RecoveryEvent.PROCESSING_BY_SMART_CONTRACT_WALLET]: 'Confirm the execution in your wallet.',
[RecoveryEvent.PROCESSING]: 'Validating...',
[RecoveryEvent.PROCESSED]: 'Successfully validated. Loading...',
[RecoveryEvent.REVERTED]: 'Reverted. Please check your gas settings.',
[RecoveryEvent.FAILED]: 'Failed.',
// TODO: Add success event
// [RecoveryEvent.SUCCESS]: 'Successfully executed.',
[RecoveryEvent.SUCCESS]: 'Successfully executed.',
}

const RecoveryTxNotificationTitles = {
Expand All @@ -37,9 +43,11 @@ export function useRecoveryTxNotifications(): void {
return
}

const unsubFns = Object.entries(RecoveryTxNotifications).map(([event, notification]) =>
recoverySubscribe(event as RecoveryEvent, async (detail) => {
const isSuccess = event === RecoveryEvent.PROCESSED
const entries = Object.entries(RecoveryTxNotifications) as Array<[keyof typeof RecoveryTxNotifications, string]>

const unsubFns = entries.map(([event, notification]) =>
recoverySubscribe(event, async (detail) => {
const isSuccess = SUCCESS_EVENTS.includes(event)
const isError = 'error' in detail

const txHash = 'txHash' in detail ? detail.txHash : undefined
Expand Down
11 changes: 8 additions & 3 deletions src/services/recovery/recoveryEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import EventBus from '../EventBus'

export enum RecoveryEvent {
PROCESSING_BY_SMART_CONTRACT_WALLET = 'PROCESSING_BY_SMART_CONTRACT_WALLET',
PROCESSING = 'PROCESSING',
REVERTED = 'REVERTED',
PROCESSED = 'PROCESSED',
PROCESSING = 'PROCESSING', // Submitted to the blockchain
PROCESSED = 'PROCESSED', // Executed on the blockchain
SUCCESS = 'SUCCESS', // Loaded from the blockchain
FAILED = 'FAILED',
REVERTED = 'REVERTED',
}

export enum RecoveryTxType {
Expand Down Expand Up @@ -47,6 +48,10 @@ export interface RecoveryEvents {
error: Error
txType: RecoveryTxType
}
[RecoveryEvent.SUCCESS]: {
recoveryTxHash: string
txType: RecoveryTxType
}
}

const recoveryEventBus = new EventBus<RecoveryEvents>()
Expand Down

0 comments on commit 6db5d17

Please sign in to comment.