Skip to content

Commit

Permalink
Feat/proposer duties (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickimoore authored Nov 2, 2023
1 parent 9b23a16 commit f16b391
Show file tree
Hide file tree
Showing 30 changed files with 492 additions and 128 deletions.
15 changes: 9 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { Suspense } from 'react'
import { useRecoilValue } from 'recoil'
import { appView } from './recoil/atoms'
import { AppView } from './constants/enums'
Expand All @@ -11,6 +11,7 @@ import 'rodal/lib/rodal.css'
import SSELogProvider from './components/SSELogProvider/SSELogProvider'
import SyncPollingWrapper from './wrappers/SyncPollingWrapper'
import ChangeScreen from './views/ChangeScreen'
import AppLoadFallback from './components/Fallback/AppLoadFallback'

function App() {
const view = useRecoilValue(appView)
Expand All @@ -19,11 +20,13 @@ function App() {
switch (view) {
case AppView.DASHBOARD:
return (
<SyncPollingWrapper>
<SSELogProvider>
<Dashboard />
</SSELogProvider>
</SyncPollingWrapper>
<Suspense fallback={<AppLoadFallback />}>
<SyncPollingWrapper>
<SSELogProvider>
<Dashboard />
</SSELogProvider>
</SyncPollingWrapper>
</Suspense>
)
case AppView.ONBOARD:
return <Onboard />
Expand Down
12 changes: 10 additions & 2 deletions src/components/DiagnosticTable/AlertInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import sortAlertMessagesBySeverity from '../../utilities/sortAlerts'
import { StatusColor } from '../../types'
import AlertFilterSettings, { FilterValue } from '../AlertFilterSettings/AlertFilterSettings'
import useMediaQuery from '../../hooks/useMediaQuery'
import { useRecoilValue } from 'recoil'
import ProposerAlerts from '../ProposerAlerts/ProposerAlerts'
import { proposerDuties } from '../../recoil/atoms'

const AlertInfo = () => {
const { t } = useTranslation()
const { alerts, dismissAlert, resetDismissed } = useDiagnosticAlerts()
const { ref, dimensions } = useDivDimensions()
const headerDimensions = useDivDimensions()
const [filter, setFilter] = useState('all')
const duties = useRecoilValue(proposerDuties)

const setFilterValue = (value: FilterValue) => setFilter(value)
const isMobile = useMediaQuery('(max-width: 425px)')
Expand All @@ -29,7 +33,10 @@ const AlertInfo = () => {
return sortAlertMessagesBySeverity(baseAlerts)
}, [alerts, filter])

const isFiller = formattedAlerts.length < 6
const isFiller = formattedAlerts.length + (duties?.length || 0) < 6
const isAlerts = formattedAlerts.length > 0 || duties?.length > 0
const isProposerAlerts =
duties?.length > 0 && (filter === 'all' || filter === StatusColor.SUCCESS)

useEffect(() => {
const intervalId = setInterval(() => {
Expand Down Expand Up @@ -61,7 +68,7 @@ const AlertInfo = () => {
}
className='h-full w-full flex flex-col'
>
{formattedAlerts.length > 0 && (
{isAlerts && (
<div className={`overflow-scroll scrollbar-hide ${!isFiller ? 'flex-1' : ''}`}>
{formattedAlerts.map((alert) => {
const { severity, subText, message, id } = alert
Expand All @@ -78,6 +85,7 @@ const AlertInfo = () => {
/>
)
})}
{isProposerAlerts && <ProposerAlerts duties={duties} />}
</div>
)}
{isFiller && (
Expand Down
14 changes: 14 additions & 0 deletions src/components/Fallback/AppLoadFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner'

const AppLoadFallback = () => {
return (
<div className='relative w-screen h-screen bg-gradient-to-r from-primary to-tertiary'>
<div className='absolute top-0 left-0 w-full h-full bg-cover bg-lighthouse' />
<div className='absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2'>
<LoadingSpinner />
</div>
</div>
)
}

export default AppLoadFallback
76 changes: 76 additions & 0 deletions src/components/ProposerAlerts/AlertGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ProposerDuty, StatusColor } from '../../types'
import { FC, useState } from 'react'
import StatusBar from '../StatusBar/StatusBar'
import Typography from '../Typography/Typography'
import ProposalAlert from './ProposalAlert'
import getSlotTimeData from '../../utilities/getSlotTimeData'
import { useTranslation } from 'react-i18next'

export interface AlertGroupProps {
duties: ProposerDuty[]
onClick: (ids: string[]) => void
genesis: number
secondsPerSlot: number
}

const AlertGroup: FC<AlertGroupProps> = ({ duties, genesis, secondsPerSlot, onClick }) => {
const { t } = useTranslation()
const indices = duties.map(({ validator_index }) => validator_index)
const uuids = duties.map(({ uuid }) => uuid)
const isFullGroup = duties.length > 1
const [isExpand, toggleGroup] = useState(false)

const sortedDutiesBySlot = [...duties].sort((a, b) => Number(b.slot) - Number(a.slot))
const latestDuty = sortedDutiesBySlot[0]
const latestDutyTime = getSlotTimeData(Number(latestDuty.slot), genesis, secondsPerSlot)

const toggle = () => toggleGroup(!isExpand)
const removeGroup = () => onClick(uuids)

const renderMappedDuties = () =>
duties?.map((duty, index) => {
const { isFuture, shortHand } = getSlotTimeData(Number(duty.slot), genesis, secondsPerSlot)

return (
<ProposalAlert
onDelete={!isFullGroup ? removeGroup : undefined}
isFuture={isFuture}
key={index}
duty={duty}
time={shortHand}
/>
)
})

return (
<>
{isFullGroup ? (
<>
<div className='w-full @1540:h-22 group border-b-style500 flex justify-between items-center space-x-2 @1540:space-x-4 p-2'>
<StatusBar count={3} status={StatusColor.SUCCESS} />
<div onClick={toggle} className='cursor-pointer w-full max-w-tiny @1540:max-w-full'>
<Typography type='text-caption2'>
{t(
`alertMessages.groupedProposers.${latestDutyTime.isFuture ? 'future' : 'past'}`,
{ count: duties?.length, indices: indices.join(', ') },
)}
</Typography>
<Typography color='text-primary' darkMode='text-primary' type='text-caption2'>
{isExpand ? t('collapseInfo') : t('expandInfo')}
</Typography>
</div>
<i
onClick={removeGroup}
className='bi-trash-fill cursor-pointer opacity-0 group-hover:opacity-100 text-dark200 dark:text-dark300'
/>
</div>
{isExpand && renderMappedDuties()}
</>
) : (
renderMappedDuties()
)}
</>
)
}

export default AlertGroup
41 changes: 41 additions & 0 deletions src/components/ProposerAlerts/ProposalAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ProposerDuty, StatusColor } from '../../types'
import { FC } from 'react'
import StatusBar from '../StatusBar/StatusBar'
import Typography from '../Typography/Typography'
import { Trans } from 'react-i18next'

export interface ProposalAlertProps {
duty: ProposerDuty
time: string
isFuture?: boolean
onDelete?: (uuid: string[]) => void
}

const ProposalAlert: FC<ProposalAlertProps> = ({ duty, time, isFuture, onDelete }) => {
const { validator_index, slot, uuid } = duty

const removeAlert = () => onDelete?.([uuid])
return (
<div className='w-full @1540:h-22 group border-b-style500 flex justify-between items-center space-x-2 @1540:space-x-4 p-2'>
<StatusBar count={3} status={StatusColor.SUCCESS} />
<div className='w-full max-w-tiny @1540:max-w-full'>
<Typography type='text-caption2'>
<Trans
i18nKey={`alertMessages.proposerAlert.${isFuture ? 'future' : 'past'}`}
values={{ validator_index, slot, time }}
>
<span className='font-bold underline' />
</Trans>
</Typography>
</div>
{onDelete && (
<i
onClick={removeAlert}
className='bi-trash-fill cursor-pointer opacity-0 group-hover:opacity-100 text-dark200 dark:text-dark300'
/>
)}
</div>
)
}

export default ProposalAlert
58 changes: 58 additions & 0 deletions src/components/ProposerAlerts/ProposerAlerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { FC } from 'react'
import { ProposerDuty } from '../../types'
import groupArray from '../../utilities/groupArray'
import AlertGroup from './AlertGroup'
import ProposalAlert from './ProposalAlert'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { selectGenesisBlock } from '../../recoil/selectors/selectGenesisBlock'
import { selectBnSpec } from '../../recoil/selectors/selectBnSpec'
import { proposerDuties } from '../../recoil/atoms'
import getSlotTimeData from '../../utilities/getSlotTimeData'

export interface ProposerAlertsProps {
duties: ProposerDuty[]
}

const ProposerAlerts: FC<ProposerAlertsProps> = ({ duties }) => {
const { SECONDS_PER_SLOT } = useRecoilValue(selectBnSpec)
const genesis = useRecoilValue(selectGenesisBlock) as number
const setProposers = useSetRecoilState(proposerDuties)
const groups = groupArray(duties, 10)

const removeAlert = (uuids: string[]) => {
setProposers((prev) => prev.filter(({ uuid }) => !uuids.includes(uuid)))
}

return (
<>
{duties.length >= 10
? groups.map((group, index) => (
<AlertGroup
onClick={removeAlert}
genesis={genesis}
secondsPerSlot={SECONDS_PER_SLOT}
duties={group}
key={index}
/>
))
: duties.map((duty, index) => {
const { isFuture, shortHand } = getSlotTimeData(
Number(duty.slot),
genesis,
SECONDS_PER_SLOT,
)
return (
<ProposalAlert
onDelete={removeAlert}
isFuture={isFuture}
time={shortHand}
key={index}
duty={duty}
/>
)
})}
</>
)
}

export default ProposerAlerts
2 changes: 0 additions & 2 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ export const CLIENT_PROVIDERS = [
] as ClientProvider[]

export const initialEthDeposit = 32
export const secondsInSlot = 12
export const slotsInEpoc = 32
export const secondsInEpoch = secondsInSlot * 32
export const secondsInHour = 3600
export const secondsInDay = 86400
export const secondsInWeek = 604800
Expand Down
28 changes: 22 additions & 6 deletions src/hooks/__tests__/useEpochAprEstimate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import {
mockShortValidatorCache,
mockedRecentWithdrawalCash,
} from '../../mocks/validatorResults'
import { mockBeaconSpec } from '../../mocks/beaconSpec'
import useFilteredValidatorCacheData from '../useFilteredValidatorCacheData'

jest.mock('../useFilteredValidatorCacheData', () => jest.fn())

const mockedUseFilteredValidatorCacheData = useFilteredValidatorCacheData as jest.MockedFn<
typeof useFilteredValidatorCacheData
>

jest.mock('ethers/lib/utils', () => ({
formatUnits: jest.fn(),
Expand All @@ -21,22 +29,26 @@ describe('useEpochAprEstimate hook', () => {
mockedFormatUnits.mockImplementation((value) => value.toString())
})
it('should return default values', () => {
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(undefined)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: undefined,
textColor: 'text-dark500',
})
})
it('should return default values when not enough epoch data', () => {
mockedRecoilValue.mockReturnValue(mockShortValidatorCache)
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(mockShortValidatorCache)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: undefined,
textColor: 'text-dark500',
})
})
it('should return correct values', () => {
mockedRecoilValue.mockReturnValue(mockValidatorCache)
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(mockValidatorCache)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: 1.3438636363304557,
Expand All @@ -45,7 +57,8 @@ describe('useEpochAprEstimate hook', () => {
})

it('should return correct values when provided an array of indexes', () => {
mockedRecoilValue.mockReturnValue(mockValidatorCache)
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(mockValidatorCache)
const { result } = renderHook(() => useEpochAprEstimate(['1234567']))
expect(result.current).toStrictEqual({
estimatedApr: 1.3438636363304557,
Expand All @@ -54,7 +67,8 @@ describe('useEpochAprEstimate hook', () => {
})

it('should return correct when there is a withdrawal value', () => {
mockedRecoilValue.mockReturnValue(mockedWithdrawalCash)
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(mockedWithdrawalCash)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: 3.8495973450145105,
Expand All @@ -63,7 +77,8 @@ describe('useEpochAprEstimate hook', () => {
})

it('should return correct when there is a withdrawal values at a loss', () => {
mockedRecoilValue.mockReturnValue(mockedWithdrawalCashLoss)
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(mockedWithdrawalCashLoss)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: -0.1710932155095768,
Expand All @@ -72,7 +87,8 @@ describe('useEpochAprEstimate hook', () => {
})

it('should return correct values when last epoch was a withdrawal', () => {
mockedRecoilValue.mockReturnValue(mockedRecentWithdrawalCash)
mockedRecoilValue.mockReturnValue(mockBeaconSpec)
mockedUseFilteredValidatorCacheData.mockReturnValue(mockedRecentWithdrawalCash)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: 0,
Expand Down
Loading

0 comments on commit f16b391

Please sign in to comment.