diff --git a/src/components/ValidatorModal/views/ValidatorExit.tsx b/src/components/ValidatorModal/views/ValidatorExit.tsx index 75893299..e0bb10e5 100644 --- a/src/components/ValidatorModal/views/ValidatorExit.tsx +++ b/src/components/ValidatorModal/views/ValidatorExit.tsx @@ -1,5 +1,5 @@ import ValidatorInfoHeader from '../../ValidatorInfoHeader/ValidatorInfoHeader' -import { SignedExitData, ValidatorInfo } from '../../../types/validator' +import { ValidatorInfo } from '../../../types/validator' import { FC, useContext, useState } from 'react' import Typography from '../../Typography/Typography' import { ValidatorModalContext } from '../ValidatorModal' @@ -9,13 +9,10 @@ import InfoBox, { InfoBoxType } from '../../InfoBox/InfoBox' import Button, { ButtonFace } from '../../Button/Button' import { Trans, useTranslation } from 'react-i18next' import addClassString from '../../../utilities/addClassString' -import { signVoluntaryExit } from '../../../api/lighthouse' import { useRecoilValue } from 'recoil' -import { submitSignedExit } from '../../../api/beacon' import ExitDisclosure from '../../Disclosures/ExitDisclosure' -import displayToast from '../../../utilities/displayToast' import { activeDevice } from '../../../recoil/atoms' -import { ToastType } from '../../../types' +import useExitValidator from '../../../hooks/useExitValidator' export interface ValidatorExitProps { validator: ValidatorInfo @@ -24,48 +21,27 @@ export interface ValidatorExitProps { const ValidatorExit: FC = ({ validator }) => { const { t } = useTranslation() const { pubKey } = validator - const [isLoading, setLoading] = useState(false) const { rawValidatorUrl, apiToken, beaconUrl } = useRecoilValue(activeDevice) const [isAccept, setIsAccept] = useState(false) const { moveToView, closeModal } = useContext(ValidatorModalContext) const viewDetails = () => moveToView(ValidatorModalView.DETAILS) + const { isLoading, setLoading, getSignedExit, submitSignedMessage } = useExitValidator( + apiToken, + pubKey, + beaconUrl, + ) + const acceptBtnClasses = addClassString('', [isAccept && 'border-success !text-success']) const checkMarkClasses = addClassString('bi bi-check-circle ml-4', [isAccept && 'text-success']) - const getSignedExit = async (url: string): Promise => { - try { - const { data } = await signVoluntaryExit(url, apiToken, pubKey) - - if (data) { - return data - } - } catch (e) { - setLoading(false) - displayToast(t('error.unableToSignExit'), ToastType.ERROR) - } - } - const submitSignedMessage = async (data: SignedExitData) => { - try { - const { status } = await submitSignedExit(beaconUrl, data) - - if (status === 200) { - setLoading(false) - displayToast(t('success.validatorExit'), ToastType.SUCCESS) - closeModal() - } - } catch (e) { - setLoading(false) - displayToast(t('error.invalidExit'), ToastType.ERROR) - } - } - const confirmExit = async () => { setLoading(true) const message = await getSignedExit(rawValidatorUrl) if (message) { - void (await submitSignedMessage(message)) + await submitSignedMessage(message) + closeModal() } } const toggleAccept = () => setIsAccept((prev) => !prev) diff --git a/src/hooks/__tests__/useExitValidator.spec.ts b/src/hooks/__tests__/useExitValidator.spec.ts new file mode 100644 index 00000000..57367e55 --- /dev/null +++ b/src/hooks/__tests__/useExitValidator.spec.ts @@ -0,0 +1,82 @@ +import { renderHook, act } from '@testing-library/react-hooks' +import useExitValidator from '../useExitValidator' +import { signVoluntaryExit } from '../../api/lighthouse' +import { submitSignedExit } from '../../api/beacon' +import displayToast from '../../utilities/displayToast' + +jest.mock('../../api/lighthouse') +jest.mock('../../api/beacon') +jest.mock('../../utilities/displayToast') +jest.mock('react-i18next') + +const mockedSignVoluntaryExit = signVoluntaryExit as jest.MockedFn +const mockedDisplayToast = displayToast as jest.MockedFn +const mockedSubmitExit = submitSignedExit as jest.MockedFn + +const mockExitData = { + message: { + epoch: 'mock-epoch', + validator_index: '0', + }, + signature: 'mock-signature', +} + +const mockResponse = { + data: undefined, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, +} + +const setup = async (signingResponse: any, submissionResponse: any) => { + mockedSignVoluntaryExit.mockResolvedValue(signingResponse) + mockedSubmitExit.mockResolvedValue(submissionResponse) + const { result } = renderHook(() => useExitValidator('testToken', 'testPubKey', 'testUrl')) + + let signedExitData + await act(async () => { + signedExitData = await result.current.getSignedExit('testUrl') + if (signedExitData) { + await result.current.submitSignedMessage(signedExitData) + } + }) + + return { result, signedExitData } +} + +describe('useExitValidator', () => { + beforeEach(() => { + mockedSignVoluntaryExit.mockClear() + mockedSubmitExit.mockClear() + mockedDisplayToast.mockClear() + }) + + it('should handle successful signing and submission when returned data.data', async () => { + const { signedExitData } = await setup( + { ...mockResponse, data: { data: mockExitData } }, + mockResponse, + ) + expect(signedExitData).toEqual(mockExitData) + expect(displayToast).toHaveBeenCalledWith('success.validatorExit', 'success') + }) + + it('should handle successful signing and submission when returned data', async () => { + const { signedExitData } = await setup({ ...mockResponse, data: mockExitData }, mockResponse) + expect(signedExitData).toEqual(mockExitData) + expect(displayToast).toHaveBeenCalledWith('success.validatorExit', 'success') + }) + + it('should handle error during signing', async () => { + await setup(Promise.reject(new Error('Error during signing')), mockResponse) + expect(displayToast).toHaveBeenCalledWith('error.unableToSignExit', 'error') + }) + + it('should handle error during submission', async () => { + await setup( + { ...mockResponse, data: mockExitData }, + Promise.reject(new Error('Error during submission')), + ) + expect(displayToast).toHaveBeenCalledWith('error.invalidExit', 'error') + }) +}) diff --git a/src/hooks/useExitValidator.ts b/src/hooks/useExitValidator.ts new file mode 100644 index 00000000..86aa3006 --- /dev/null +++ b/src/hooks/useExitValidator.ts @@ -0,0 +1,47 @@ +import { SignedExitData } from '../types/validator' +import { signVoluntaryExit } from '../api/lighthouse' +import displayToast from '../utilities/displayToast' +import { ToastType } from '../types' +import { submitSignedExit } from '../api/beacon' +import { useTranslation } from 'react-i18next' +import { useState } from 'react' + +const useExitValidator = (apiToken: string, pubKey: string, beaconUrl: string) => { + const { t } = useTranslation() + const [isLoading, setLoading] = useState(false) + + const getSignedExit = async (url: string): Promise => { + try { + const { data } = await signVoluntaryExit(url, apiToken, pubKey) + + if (data) { + return data?.data || data + } + } catch (e) { + setLoading(false) + displayToast(t('error.unableToSignExit'), ToastType.ERROR) + } + } + const submitSignedMessage = async (data: SignedExitData) => { + try { + const { status } = await submitSignedExit(beaconUrl, data) + + if (status === 200) { + setLoading(false) + displayToast(t('success.validatorExit'), ToastType.SUCCESS) + } + } catch (e) { + setLoading(false) + displayToast(t('error.invalidExit'), ToastType.ERROR) + } + } + + return { + isLoading, + setLoading, + getSignedExit, + submitSignedMessage, + } +} + +export default useExitValidator