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

Feat: vol exit update #220

Merged
merged 2 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 10 additions & 34 deletions src/components/ValidatorModal/views/ValidatorExit.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -24,48 +21,27 @@ export interface ValidatorExitProps {
const ValidatorExit: FC<ValidatorExitProps> = ({ 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<SignedExitData | undefined> => {
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)
Expand Down
82 changes: 82 additions & 0 deletions src/hooks/__tests__/useExitValidator.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof signVoluntaryExit>
const mockedDisplayToast = displayToast as jest.MockedFn<typeof displayToast>
const mockedSubmitExit = submitSignedExit as jest.MockedFn<typeof submitSignedExit>

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')
})
})
47 changes: 47 additions & 0 deletions src/hooks/useExitValidator.ts
Original file line number Diff line number Diff line change
@@ -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<SignedExitData | undefined> => {
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
Loading