Skip to content

Commit

Permalink
Feat: update graffiti (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickimoore authored Jul 25, 2023
1 parent e808e6b commit 78dd21a
Show file tree
Hide file tree
Showing 17 changed files with 159 additions and 43 deletions.
10 changes: 10 additions & 0 deletions src/api/lighthouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export const fetchValidatorGraffiti = async (baseValidatorUrl: string, token: st
headers: { Authorization: `Bearer ${token}` },
})

export const updateValidator = async (
baseUrl: string,
pubKey: string,
token: string,
data: Record<string, any>,
) =>
await axios.patch(`${baseUrl}/validators/${pubKey}`, data, {
headers: { Authorization: `Bearer ${token}` },
})

export const signVoluntaryExit = async (baseUrl: string, token: string, pubkey: string) =>
await axios.post(`${baseUrl}/eth/v1/validator/${pubkey}/voluntary_exit`, null, {
headers: { Authorization: `Bearer ${token}` },
Expand Down
9 changes: 5 additions & 4 deletions src/components/BlsExecutionModal/BlsExecutionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import RodalModal from '../RodalModal/RodalModal'
import { useRecoilState, useRecoilValue } from 'recoil'
import { isBlsExecutionModal, processingBlsValidators, activeDevice } from '../../recoil/atoms'
import { activeDevice, isBlsExecutionModal, processingBlsValidators } from '../../recoil/atoms'
import useMediaQuery from '../../hooks/useMediaQuery'
import Typography from '../Typography/Typography'
import CodeInput from '../CodeInput/CodeInput'
Expand All @@ -9,14 +9,15 @@ import { useState } from 'react'
import { MOCK_BLS_JSON, WithdrawalInfoLink } from '../../constants/constants'
import GradientHeader from '../GradientHeader/GradientHeader'
import { ButtonFace } from '../Button/Button'
import { useTranslation, Trans } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { broadcastBlsChange } from '../../api/beacon'
import axios, { AxiosError } from 'axios'
import useLocalStorage from '../../hooks/useLocalStorage'
import { Storage } from '../../constants/enums'
import isValidJSONArray from '../../utilities/isValidJson'
import getValuesFromObjArray from '../../utilities/getValuesFromObjArray'
import displayToast from '../../utilities/displayToast'
import { ToastType } from '../../types'

const BlsExecutionModal = () => {
const { t } = useTranslation()
Expand All @@ -42,7 +43,7 @@ const BlsExecutionModal = () => {
message = t('error.invalidJson')
}

displayToast(message, 'error')
displayToast(message, ToastType.ERROR)
}

const submitChange = async () => {
Expand Down Expand Up @@ -73,7 +74,7 @@ const BlsExecutionModal = () => {
storeIsBlsProcessing(JSON.stringify(targetIndices))
closeModal()
setJson(MOCK_BLS_JSON)
displayToast(t('success.blsExecution'), 'success')
displayToast(t('success.blsExecution'), ToastType.SUCCESS)
} catch (e) {
setLoading(false)
if (axios.isAxiosError(e)) {
Expand Down
4 changes: 3 additions & 1 deletion src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
uiMode?: UiMode
isDisableToggle?: boolean
isDisablePaste?: boolean
inputStyle?: 'primary' | 'secondary'
inputStyle?: 'primary' | 'secondary' | 'noBorder'
icon?: string
isAutoFocus?: boolean
}
Expand Down Expand Up @@ -63,6 +63,8 @@ const Input: FC<InputProps> = ({

const generateInputStyle = () => {
switch (inputStyle) {
case 'noBorder':
return 'text-dark500 p-2 bg-transparent'
case 'secondary':
return 'text-dark500 border-style p-2 bg-transparent'
default:
Expand Down
12 changes: 10 additions & 2 deletions src/components/LoadingDots/LoadingDots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import { FC } from 'react'

export interface LoadingDotsProps {
className?: string
size?: number
color?: string
darkColor?: string
}
const LoadingDots: FC<LoadingDotsProps> = ({ className }) => {
const circleCommonClasses = 'h-2 w-2 bg-primary200 dark:bg-white rounded-full'
const LoadingDots: FC<LoadingDotsProps> = ({
className,
size = 2,
color = 'bg-primary200',
darkColor = 'dark:bg-white',
}) => {
const circleCommonClasses = `h-${size} w-${size} ${color} ${darkColor} rounded-full`

return (
<div data-testid='container' className={`${className ? `flex ${className}` : 'flex'}`}>
Expand Down
27 changes: 22 additions & 5 deletions src/components/ValidatorActionIcon/ValidatorActionIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { FC } from 'react'
import addClassString from '../../utilities/addClassString'
import LoadingDots from '../LoadingDots/LoadingDots'

export interface ValidatorActionIconProps {
icon: string
Expand All @@ -7,6 +9,8 @@ export interface ValidatorActionIconProps {
color?: string
size?: string
onClick?: () => void
isLoading?: boolean
isDisabled?: boolean
}

const ValidatorActionIcon: FC<ValidatorActionIconProps> = ({
Expand All @@ -15,14 +19,27 @@ const ValidatorActionIcon: FC<ValidatorActionIconProps> = ({
backgroundColor = 'bg-dark25 dark:bg-dark750',
border = 'border border-primary100 dark:border-primary',
color = 'text-primary',
isDisabled,
isLoading,
onClick,
}) => {
const classes = addClassString(
'w-8 h-8 cursor-pointer rounded-full flex items-center justify-center',
[
border,
backgroundColor,
isDisabled && 'opacity-20 pointer-events-none',
isLoading && 'pointer-events-none',
],
)

return (
<div
onClick={onClick}
className={`${border} ${backgroundColor} w-8 h-8 cursor-pointer rounded-full flex items-center justify-center`}
>
<i className={`${icon} ${color} ${size}`} />
<div onClick={onClick} className={classes}>
{isLoading ? (
<LoadingDots size={1} darkColor='dark:bg-primary200' />
) : (
<i className={`${icon} ${color} ${size}`} />
)}
</div>
)
}
Expand Down
39 changes: 30 additions & 9 deletions src/components/ValidatorGraffitiInput/ValidatorGraffitiInput.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import Typography from '../Typography/Typography'
import ValidatorActionIcon from '../ValidatorActionIcon/ValidatorActionIcon'
import { useTranslation } from 'react-i18next'
import { FC } from 'react'
import DisabledTooltip from '../DisabledTooltip/DisabledTooltip'
import { ChangeEvent, FC, useState } from 'react'
import Input from '../Input/Input'

export interface ValidatorGraffitiInputProps {
value?: string
isLoading?: boolean
onSubmit: (value: string) => void
}

const ValidatorGraffitiInput: FC<ValidatorGraffitiInputProps> = ({ value }) => {
const ValidatorGraffitiInput: FC<ValidatorGraffitiInputProps> = ({
value,
onSubmit,
isLoading,
}) => {
const { t } = useTranslation()

const [input, setInput] = useState('')

const onChange = (e: ChangeEvent<HTMLInputElement>) => setInput(e.target.value)
const isDirty = input && value !== input
const submitGraffiti = () => onSubmit(input as string)

return (
<div className='w-full border-t flex'>
<div className='border-r py-2 px-6 w-42'>
Expand All @@ -18,12 +31,20 @@ const ValidatorGraffitiInput: FC<ValidatorGraffitiInputProps> = ({ value }) => {
</Typography>
</div>
<div className='flex-1 flex justify-between items-center px-8 py-3'>
<Typography type='text-caption1' color='text-primary'>
{value || t('graffitiGoesHere')}
</Typography>
<DisabledTooltip>
<ValidatorActionIcon icon='bi-pencil-square' color='text-primary' />
</DisabledTooltip>
<Input
value={input}
onChange={onChange}
inputStyle='noBorder'
className='text-caption1 placeholder:text-primary text-primary'
placeholder={value || t('graffitiGoesHere')}
/>
<ValidatorActionIcon
onClick={submitGraffiti}
isLoading={isLoading}
isDisabled={!isDirty}
icon='bi-pencil-square'
color='text-primary'
/>
</div>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/ValidatorModal/views/ValidatorDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const ValidatorDetails = () => {

const combinedStatus = getAvgEffectivenessStatus(combinedEffectiveness)

const { graffiti } = useValidatorGraffiti(validator)
const { isLoading, graffiti, updateGraffiti } = useValidatorGraffiti(validator)

const usdBalance = (balance || 0) * (rates['USD'] || 0)

Expand Down Expand Up @@ -132,7 +132,7 @@ const ValidatorDetails = () => {
</div>
</div>
</div>
<ValidatorGraffitiInput value={graffiti} />
<ValidatorGraffitiInput isLoading={isLoading} onSubmit={updateGraffiti} value={graffiti} />
<ValidatorDetailTable validator={validator} />
<ValidatorActions
isExitAction={!isExited}
Expand Down
9 changes: 5 additions & 4 deletions src/components/ValidatorModal/views/ValidatorExit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { ValidatorModalView } from '../../../constants/enums'
import BasicValidatorMetrics from '../../BasicValidatorMetrics/BasicValidatorMetrics'
import InfoBox, { InfoBoxType } from '../../InfoBox/InfoBox'
import Button, { ButtonFace } from '../../Button/Button'
import { useTranslation, Trans } from 'react-i18next'
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'

export interface ValidatorExitProps {
validator: ValidatorInfo
Expand All @@ -39,7 +40,7 @@ const ValidatorExit: FC<ValidatorExitProps> = ({ validator }) => {
}
} catch (e) {
setLoading(false)
displayToast(t('error.unableToSignExit'), 'error')
displayToast(t('error.unableToSignExit'), ToastType.ERROR)
}
}
const submitSignedMessage = async (data: SignedExitData) => {
Expand All @@ -48,12 +49,12 @@ const ValidatorExit: FC<ValidatorExitProps> = ({ validator }) => {

if (status === 200) {
setLoading(false)
displayToast(t('success.validatorExit'), 'success')
displayToast(t('success.validatorExit'), ToastType.SUCCESS)
closeModal()
}
} catch (e) {
setLoading(false)
displayToast(t('error.invalidExit'), 'error')
displayToast(t('error.invalidExit'), ToastType.ERROR)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/forms/ConfigConnectionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AxiosError } from 'axios'
import { fetchVersion } from '../api/lighthouse'
import { DeviceKeyStorage, DeviceListStorage } from '../types/storage'
import { fetchBeaconVersion } from '../api/beacon'
import { DeviceList, DeviceSettings, Endpoint } from '../types'
import { DeviceList, DeviceSettings, Endpoint, ToastType } from '../types'
import { useTranslation } from 'react-i18next'
import isRequiredVersion from '../utilities/isRequiredVersion'
import { REQUIRED_VALIDATOR_VERSION } from '../constants/constants'
Expand Down Expand Up @@ -162,7 +162,7 @@ const ConfigConnectionForm: FC<ConfigConnectionFormProps> = ({ children }) => {
if (e instanceof AxiosError && e.response?.status === 403) {
message = t('error.invalidApiToken')
}
displayToast(message, 'error')
displayToast(message, ToastType.ERROR)
}

const handleValidationErrors = (values: ConnectionForm) => {
Expand Down
4 changes: 2 additions & 2 deletions src/forms/EditValidatorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSetRecoilState } from 'recoil'
import { validatorAliases } from '../recoil/atoms'
import displayToast from '../utilities/displayToast'
import useLocalStorage from '../hooks/useLocalStorage'
import { ValAliases } from '../types'
import { ToastType, ValAliases } from '../types'
import { useTranslation } from 'react-i18next'

export interface EditValidatorFormProps {
Expand Down Expand Up @@ -50,7 +50,7 @@ const EditValidatorForm: FC<EditValidatorFormProps> = ({ children, validator })
setAlias((prev) => ({ ...prev, [index]: nameString }))
storeValAliases({ ...aliases, [index]: nameString })

displayToast(t('validatorEdit.successUpdate'), 'success')
displayToast(t('validatorEdit.successUpdate'), ToastType.SUCCESS)

reset()
}
Expand Down
5 changes: 3 additions & 2 deletions src/forms/SessionAuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { yupResolver } from '@hookform/resolvers/yup'
import { sessionAuthValidation } from '../validation/sessionAuthValidation'
import { OnboardView } from '../constants/enums'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { onBoardView, activeDevice, deviceSettings } from '../recoil/atoms'
import { activeDevice, deviceSettings, onBoardView } from '../recoil/atoms'
import useLocalStorage from '../hooks/useLocalStorage'
import CryptoJS from 'crypto-js'
import { useTranslation } from 'react-i18next'
import displayToast from '../utilities/displayToast'
import { DeviceListStorage } from '../types/storage'
import { ToastType } from '../types'

export interface SessionAuthForm {
password: string
Expand Down Expand Up @@ -74,7 +75,7 @@ const SessionAuthForm: FC<SessionAuthFormProps> = ({ children }) => {
viewSetup()
return
}
if (!isValid) displayToast(t('error.sessionAuth.invalidPassword'), 'error')
if (!isValid) displayToast(t('error.sessionAuth.invalidPassword'), ToastType.ERROR)
return
}

Expand Down
18 changes: 15 additions & 3 deletions src/hooks/__tests__/useValidatorGraffiti.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,31 @@ describe('useValidatorGraffiti hook', () => {
mockedRecoilValue.mockReturnValueOnce('mock-validator-url')
const { result } = renderHook(() => useValidatorGraffiti())

expect(result.current).toEqual({ graffiti: undefined })
expect(JSON.stringify(result.current)).toStrictEqual(
JSON.stringify({
graffiti: undefined,
isLoading: false,
updateGraffiti: () => {},
}),
)
})

it('should return graffiti', () => {
mockedRecoilValue.mockReturnValueOnce('mock-api-token')
mockedRecoilValue.mockReturnValueOnce('mock-validator-url')
jest.spyOn(React, 'useState').mockReturnValue([{ 'mock-pub-key': 'mock-graffiti' }, jest.fn()])
jest.spyOn(React, 'useState').mockReturnValueOnce([false, jest.fn()])
jest
.spyOn(React, 'useState')
.mockReturnValueOnce([{ 'mock-pub-key': 'mock-graffiti' }, jest.fn()])
const { result } = renderHook(() => useValidatorGraffiti(mockValidatorInfo))

expect(result.current).toEqual({ graffiti: 'mock-graffiti' })
expect(JSON.stringify(result.current)).toEqual(
JSON.stringify({ isLoading: false, graffiti: 'mock-graffiti', updateGraffiti: () => {} }),
)
})

it('should not call fetchValidatorGraffiti', () => {
mockedRecoilValue.mockReturnValueOnce(false)
mockedRecoilValue.mockReturnValueOnce(undefined)
mockedRecoilValue.mockReturnValueOnce(undefined)
renderHook(() => useValidatorGraffiti(mockValidatorInfo))
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useApiValidation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Endpoint } from '../types'
import { Endpoint, ToastType } from '../types'
import { useCallback, useEffect, useState } from 'react'
import { debounce } from '../utilities/debounce'
import axios from 'axios'
Expand Down Expand Up @@ -28,9 +28,9 @@ const useApiValidation = (path: string, type: ApiType, isToastAlert: boolean, da
if (!isToastAlert) return

if (code === 'ERR_NETWORK') {
displayToast(t('error.networkError', { type }), 'error')
displayToast(t('error.networkError', { type }), ToastType.ERROR)
} else {
displayToast(t('error.unknownError', { type }), 'error')
displayToast(t('error.unknownError', { type }), ToastType.ERROR)
}
}
}),
Expand Down
Loading

0 comments on commit 78dd21a

Please sign in to comment.