Skip to content

Commit

Permalink
Feat/BLS-Execution (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickimoore committed Apr 18, 2023
1 parent 26bc797 commit 1a72c81
Show file tree
Hide file tree
Showing 26 changed files with 890 additions and 85 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"quotes": ["error", "single"],
"no-duplicate-imports": "error",
"react/prop-types": 0,
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
},
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ build-storybook.log

# local-testnet
/local-testnet/testnet-data
/local-testnet/bls_to_execution_changes
/out
.env
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@uiw/react-textarea-code-editor": "^2.1.1",
"autoprefixer": "^9",
"axios": "^0.27.2",
"bootstrap-icons": "^1.9.1",
Expand Down
6 changes: 6 additions & 0 deletions src/api/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ export const fetchValidatorMetrics = async (
await axios.post(`${protocol}://${address}:${port}/lighthouse/ui/validator_metrics`, {
indices,
})

export const broadcastBlsChange = async ({ protocol, address, port }: Endpoint, data: any) =>
await axios.post(
`${protocol}://${address}:${port}/eth/v1/beacon/pool/bls_to_execution_changes`,
data,
)
116 changes: 116 additions & 0 deletions src/components/BlsExecutionModal/BlsExecutionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import RodalModal from '../RodalModal/RodalModal'
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { beaconNodeEndpoint, isBlsExecutionModal, isProcessBls } from '../../recoil/atoms'
import useMediaQuery from '../../hooks/useMediaQuery'
import Typography from '../Typography/Typography'
import CodeInput from '../CodeInput/CodeInput'
import ValidatorDisclosure from '../Disclosures/ValidatorDisclosure'
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 { broadcastBlsChange } from '../../api/beacon'
import { toast } from 'react-toastify'
import axios, { AxiosError } from 'axios'
import useLocalStorage from '../../hooks/useLocalStorage'
import { Storage } from '../../constants/enums'

const BlsExecutionModal = () => {
const { t } = useTranslation()
const beaconEndpoint = useRecoilValue(beaconNodeEndpoint)
const [isModal, toggleModal] = useRecoilState(isBlsExecutionModal)
const isTablet = useMediaQuery('(max-width: 1024px)')
const [blsJson, setJson] = useState(MOCK_BLS_JSON)
const setIsProcess = useSetRecoilState(isProcessBls)
const [, storeIsBlsProcessing] = useLocalStorage<boolean>(Storage.BLS_PROCESSING, false)

const closeModal = () => toggleModal(false)
const setJsonValue = (value: string) => setJson(value)

const handleError = (code?: number) => {
let message = t('error.unknownError', { type: 'BEACON' })

if (code === 400) {
message = t('error.executionFailure')
}

toast.error(message, {
position: 'top-right',
autoClose: 5000,
theme: 'colored',
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
})
}

const submitChange = async () => {
try {
const { status } = await broadcastBlsChange(beaconEndpoint, blsJson)

if (status != 200) {
handleError(status)
return
}

setIsProcess(true)
storeIsBlsProcessing(true)
} catch (e) {
if (axios.isAxiosError(e)) {
const axiosError = e as AxiosError

handleError(axiosError.response?.status)
}
}
}

return (
<RodalModal
isVisible={isModal}
styles={{
width: 'fit-content',
maxWidth: isTablet ? '448px' : '900px',
height: isTablet ? '540px' : 'max-content',
}}
onClose={closeModal}
>
<div>
<GradientHeader title={t('blsExecution.modal.title')} />
<div className='p-6 space-y-4'>
<Typography type='text-caption1'>
<Trans i18nKey='blsExecution.modal.subTitle'>
<br />
</Trans>
{' ---'}
</Typography>
<Typography className='w-3/4' type='text-caption1'>
{t('blsExecution.modal.description')}
</Typography>
<Typography className='w-3/4' type='text-caption1'>
<Trans i18nKey='blsExecution.modal.followLink'>
<a
className='text-blue-500 underline'
target='_blank'
rel='noreferrer'
href={WithdrawalInfoLink}
/>
</Trans>
</Typography>
<CodeInput value={blsJson} onChange={setJsonValue} />
</div>
{isModal && (
<div className='p-3 border-t-style100'>
<ValidatorDisclosure
onAccept={submitChange}
ctaType={ButtonFace.SECONDARY}
ctaText={t('blsExecution.modal.cta')}
/>
</div>
)}
</div>
</RodalModal>
)
}

export default BlsExecutionModal
31 changes: 31 additions & 0 deletions src/components/CodeInput/CodeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import CodeEditor from '@uiw/react-textarea-code-editor'
import { FC } from 'react'

export interface CodeInputProps {
onChange: (value: string) => void
value: string
placeholder?: string
}

const CodeInput: FC<CodeInputProps> = ({ onChange, value, placeholder }) => {
return (
<div className='w-full max-h-48 overflow-scroll'>
<CodeEditor
rows={5}
className='dark:bg-dark750'
value={value}
language='json'
placeholder={placeholder}
onChange={(evn) => onChange(evn.target.value)}
padding={15}
style={{
fontSize: 12,
fontFamily:
'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace',
}}
/>
</div>
)
}

export default CodeInput
10 changes: 6 additions & 4 deletions src/components/Disclosures/ValidatorDisclosure.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { useState } from 'react'
import ViewDisclosures from '../ViewDisclosures/ViewDisclosures'
import React, { FC, useState } from 'react'
import ViewDisclosures, { ViewDisclosuresProps } from '../ViewDisclosures/ViewDisclosures'
import DisclosureModal from '../DisclosureModal/DisclosureModal'
import validatorDisclosure from '../../assets/images/validatorDisclosure.png'
import Typography from '../Typography/Typography'
import { useTranslation } from 'react-i18next'
import useUiMode from '../../hooks/useUiMode'

const ValidatorDisclosure = () => {
type ValidatorDisclosureProps = Omit<ViewDisclosuresProps, 'onClick'>

const ValidatorDisclosure: FC<ValidatorDisclosureProps> = (props) => {
const { t } = useTranslation()
const { mode } = useUiMode()
const [isOpen, toggleModal] = useState(false)
Expand All @@ -16,7 +18,7 @@ const ValidatorDisclosure = () => {

return (
<>
<ViewDisclosures onClick={openModal} />
<ViewDisclosures {...props} onClick={openModal} />
<DisclosureModal
mode={mode}
backgroundImage={validatorDisclosure}
Expand Down
38 changes: 38 additions & 0 deletions src/components/GradientHeader/GradientHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Waves from '../../assets/images/waves.png'
import { FC } from 'react'
import addClassString from '../../utilities/addClassString'
import Typography from '../Typography/Typography'

export interface GradientHeaderProps {
className?: string
title?: string
}

const GradientHeader: FC<GradientHeaderProps> = ({ className, title }) => {
const classes = addClassString('w-full h-36 relative', [className])

return (
<div className={classes}>
<div
className='w-full h-full bg-no-repeat bg-right opacity-10'
style={{ backgroundImage: `url(${Waves})` }}
/>
<div className='absolute top-0 left-0 w-3/4 h-full bg-gradient-to-r from-white dark:from-dark750 via-white dark:via-dark750 to-transparent' />
{title && (
<div className='absolute top-0 left-0 w-full h-full flex items-center p-5 z-20'>
<Typography
type='text-subtitle1'
color='text-transparent'
darkMode='text-transparent'
className='primary-gradient-text capitalize'
fontWeight='font-light'
>
{title}
</Typography>
</div>
)}
</div>
)
}

export default GradientHeader
9 changes: 8 additions & 1 deletion src/components/SessionAuthModal/SessionAuthModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const SessionAuthModal: FC<SessionAuthModalProps> = ({

const handleError = () => {
playErrorAnim()
setPassword('')
setCount((prevState) => prevState + 1)
}

Expand All @@ -86,6 +87,7 @@ const SessionAuthModal: FC<SessionAuthModalProps> = ({
return
}
setCount(0)
setPassword('')
onSuccess(token)
} catch (e) {
handleError()
Expand Down Expand Up @@ -129,9 +131,14 @@ const SessionAuthModal: FC<SessionAuthModalProps> = ({
</>
)

const closeAuthModal = () => {
onClose?.()
setPassword('')
}

return (
<>
<RodalModal onClose={onClose} isVisible={isOpen}>
<RodalModal onClose={closeAuthModal} isVisible={isOpen}>
<div className='p-4'>
<div className='border-b-style500 pb-4 mb-4'>
<Typography
Expand Down
2 changes: 1 addition & 1 deletion src/components/ToolTip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Tooltip: FC<TooltipProps> = ({
<div id={id} className={classes} data-tooltip-content={text}>
{children}
<TooltipComponent
className='shadow-xl'
className='shadow-xl z-50'
place={place}
style={{
maxWidth,
Expand Down
Loading

0 comments on commit 1a72c81

Please sign in to comment.