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: detail apr #146

Merged
merged 20 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3a8de2d
feat: added util to calculate apr with unit test
rickimoore Mar 15, 2023
e9ac255
feat: add apr util to hook
rickimoore Mar 15, 2023
1a2a835
feat: added apr calcualtion to table
rickimoore Mar 15, 2023
ace83fc
fix: typogrphy fix
rickimoore Mar 15, 2023
a0ffdd2
feat: added tooltip text for annual apr
rickimoore Mar 15, 2023
29884be
feat: created hook to return filtered cache data by index
rickimoore Mar 27, 2023
27b324f
fix: format validator cache atom to remove info key
rickimoore Mar 27, 2023
2cb68e3
feat: created a hook to calculate the estimated apr based on the rewa…
rickimoore Mar 27, 2023
3564ec3
fix: refactor components to use the estimateApr hook
rickimoore Mar 27, 2023
c912c42
fix: refactored to use filtered cache data hook
rickimoore Mar 27, 2023
fb61714
fix: allow empty prop
rickimoore Mar 27, 2023
56a111e
fix: removed uneeded math pow
rickimoore Mar 27, 2023
bbbaba1
chore: removed uneeded map
rickimoore Mar 27, 2023
f48d0e9
fix: add tooltip to entire apr estimate text and updated text
rickimoore Mar 27, 2023
82a5934
fix: use intial balance instead of current for projected
rickimoore Mar 28, 2023
db48b9d
fix: updated hook to account for partial eth deposits
rickimoore Mar 28, 2023
ebc85c4
fix: variable name change and unit test created
rickimoore Mar 29, 2023
a79a235
chore: updated and added new unit tests for useEpochAprEstimate
rickimoore Mar 29, 2023
707057c
chore: added test for index param
rickimoore Mar 29, 2023
411deca
chore: added unit test for validatorCacheData filter
rickimoore Mar 29, 2023
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
37 changes: 19 additions & 18 deletions src/components/AccountEarnings/AccountEarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import Spinner from '../Spinner/Spinner'
import { useRecoilValue } from 'recoil'
import { selectEthExchangeRates } from '../../recoil/selectors/selectEthExchangeRates'
import EarningsLayout from './EarningsLayout'
import formatBalanceColor from '../../utilities/formatBalanceColor'
import { selectCurrencyPrefix } from '../../recoil/selectors/selectCurrencyPrefix'
import { activeCurrency } from '../../recoil/atoms'
import CurrencySelect from '../CurrencySelect/CurrencySelect'
import useEarningsEstimate from '../../hooks/useEarningsEstimate'
import Tooltip from '../ToolTip/Tooltip'
import useEpochAprEstimate from '../../hooks/useEpochAprEstimate'

export const AccountEarningFallback = () => {
return (
Expand All @@ -28,20 +28,19 @@ export const AccountEarningFallback = () => {
const AccountEarning = () => {
const { t } = useTranslation()
const currency = useRecoilValue(activeCurrency)
const { estimate, totalEarnings, annualizedEarningsPercent, estimateSelection, selectEstimate } =
useEarningsEstimate()
const { estimate, totalEarnings, estimateSelection, selectEstimate } = useEarningsEstimate()
const { formattedPrefix } = useRecoilValue(selectCurrencyPrefix)
const { rates } = useRecoilValue(selectEthExchangeRates)

const { estimatedApr, textColor } = useEpochAprEstimate()

const activeRate = rates[currency]
const formattedRate = activeRate ? Number(activeRate) : 0
const totalBalance = formattedRate * totalEarnings
const estimatedRateConversion = formattedRate * estimate
const isEstimate = estimateSelection !== undefined
const timeFrame = isEstimate ? EARNINGS_OPTIONS[estimateSelection]?.title : undefined

const annualizedTextColor = formatBalanceColor(annualizedEarningsPercent)

const viewEarnings = async (value: number) => selectEstimate(value)

return (
Expand Down Expand Up @@ -184,20 +183,22 @@ const AccountEarning = () => {
</Tooltip>
</div>
<div>
<div className='flex space-x-2'>
<Typography type='text-caption1' className='capitalize' color='text-dark400'>
{t('annualized')}
<Tooltip id='overallApr' maxWidth={200} text={t('tooltip.annualApr')}>
<div className='flex space-x-2'>
<Typography type='text-caption1' isCapitalize color='text-dark400'>
{t('annualized')}
</Typography>
<i className='bi bi-info-circle text-caption1 text-dark400' />
</div>
<Typography
type='text-subtitle3'
color={textColor}
darkMode={textColor}
family='font-roboto'
>
{estimatedApr ? estimatedApr.toFixed(2) : '---'}%
</Typography>
<i className='bi bi-info-circle text-caption1 text-dark400' />
</div>
<Typography
type='text-subtitle3'
color={annualizedTextColor}
darkMode={annualizedTextColor}
family='font-roboto'
>
{annualizedEarningsPercent.toFixed(2)}%
</Typography>
</Tooltip>
</div>
</div>
</div>
Expand Down
10 changes: 6 additions & 4 deletions src/components/ValidatorDetailTable/ValidatorDetailTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { FC } from 'react'
import { ValidatorInfo } from '../../types/validator'
import formatBalanceColor from '../../utilities/formatBalanceColor'
import { useTranslation } from 'react-i18next'
import useEpochAprEstimate from '../../hooks/useEpochAprEstimate'

export interface ValidatorDetailTableProps {
validator: ValidatorInfo
}

export const ValidatorDetailTable: FC<ValidatorDetailTableProps> = ({ validator }) => {
const { t } = useTranslation()
const { balance } = validator
const { balance, index } = validator
const income = balance ? balance - 32 : 0
const incomeColor = formatBalanceColor(income)
const { estimatedApr, textColor } = useEpochAprEstimate([String(index)])
return (
<>
<div className='w-full lg:hidden'>
Expand Down Expand Up @@ -172,9 +174,9 @@ export const ValidatorDetailTable: FC<ValidatorDetailTableProps> = ({ validator
-
</Typography>
</div>
<div className='w-20 py-4 px-6 opacity-20'>
<Typography color='text-dark400' type='text-caption1'>
-
<div className='py-4 px-6'>
<Typography darkMode={`dark:${textColor}`} color={textColor} type='text-caption1'>
{`${estimatedApr ? estimatedApr.toFixed(2) : '---'} %`}
</Typography>
</div>
</div>
Expand Down
82 changes: 82 additions & 0 deletions src/hooks/__tests__/useEpochAprEstimate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import useEpochAprEstimate from '../useEpochAprEstimate'
import { renderHook } from '@testing-library/react-hooks'
import { mockedRecoilValue } from '../../../test.helpers'
import { formatUnits } from 'ethers/lib/utils'
import {
mockValidatorCache,
mockedWithdrawalCash,
mockedWithdrawalCashLoss,
mockShortValidatorCache,
mockedRecentWithdrawalCash,
} from '../../mocks/validatorResults'

jest.mock('ethers/lib/utils', () => ({
formatUnits: jest.fn(),
}))

const mockedFormatUnits = formatUnits as jest.MockedFn<typeof formatUnits>

describe('useEpochAprEstimate hook', () => {
beforeEach(() => {
mockedFormatUnits.mockImplementation((value) => value.toString())
})
it('should return default values', () => {
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)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: undefined,
textColor: 'text-dark500',
})
})
it('should return correct values', () => {
mockedRecoilValue.mockReturnValue(mockValidatorCache)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: 1.3438636363304557,
textColor: 'text-success',
})
})

it('should return correct values when provided an array of indexes', () => {
mockedRecoilValue.mockReturnValue(mockValidatorCache)
const { result } = renderHook(() => useEpochAprEstimate(['1234567']))
expect(result.current).toStrictEqual({
estimatedApr: 1.3438636363304557,
textColor: 'text-success',
})
})

it('should return correct when there is a withdrawal value', () => {
mockedRecoilValue.mockReturnValue(mockedWithdrawalCash)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: 3.8495973450145105,
textColor: 'text-success',
})
})

it('should return correct when there is a withdrawal values at a loss', () => {
mockedRecoilValue.mockReturnValue(mockedWithdrawalCashLoss)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: -0.1710932155095768,
textColor: 'text-error',
})
})

it('should return correct values when last epoch was a withdrawal', () => {
mockedRecoilValue.mockReturnValue(mockedRecentWithdrawalCash)
const { result } = renderHook(() => useEpochAprEstimate())
expect(result.current).toStrictEqual({
estimatedApr: 0,
textColor: 'text-dark500',
})
})
})
32 changes: 32 additions & 0 deletions src/hooks/__tests__/useFilteredValidatorCacheData.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { renderHook } from '@testing-library/react-hooks'
import useFilteredValidatorCacheData from '../useFilteredValidatorCacheData'
import { mockedRecoilValue } from '../../../test.helpers'
import { mockValidatorCache } from '../../mocks/validatorResults'
import clearAllMocks = jest.clearAllMocks

describe('useFilteredValidatorCacheData hook', () => {
beforeEach(() => {
clearAllMocks()
})

it('should return undefined if no validator cache data', () => {
mockedRecoilValue.mockReturnValue(undefined)
const { result } = renderHook(() => useFilteredValidatorCacheData())

expect(result.current).toBe(undefined)
})

it('should return unfiltered data when no provided indices', () => {
mockedRecoilValue.mockReturnValue(mockValidatorCache)
const { result } = renderHook(() => useFilteredValidatorCacheData())

expect(result.current).toStrictEqual(mockValidatorCache)
})

it('should return filtered data', () => {
mockedRecoilValue.mockReturnValue(mockValidatorCache)
const { result } = renderHook(() => useFilteredValidatorCacheData(['1234567']))

expect(result.current).toStrictEqual({ '1234567': mockValidatorCache['1234567'] })
})
})
56 changes: 56 additions & 0 deletions src/hooks/useEpochAprEstimate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import useFilteredValidatorCacheData from './useFilteredValidatorCacheData'
import { useMemo } from 'react'
import { formatUnits } from 'ethers/lib/utils'
import { secondsInDay, secondsInEpoch } from '../constants/constants'
import calculateAprPercentage from '../utilities/calculateAprPercentage'
import formatBalanceColor from '../utilities/formatBalanceColor'

const useEpochAprEstimate = (indices?: string[]) => {
const filteredValidatorCache = useFilteredValidatorCacheData(indices)

const formattedCache = useMemo(() => {
if (!filteredValidatorCache) return
return Object.values(filteredValidatorCache).map((cache) =>
cache.map(({ total_balance }) => total_balance),
)
}, [filteredValidatorCache])

const isValidEpochCount = formattedCache?.every((subArr) => subArr.length > 1)

const formatForWithdrawal = (arr: number[]) => {
const foundIndex = arr.findIndex((value) => value > 32 && value < 32.001)
return foundIndex === -1 ? arr : [arr[foundIndex], ...arr.slice(foundIndex + 1)]
}

const mappedTotalApr = useMemo(() => {
return formattedCache?.map((cache) => {
const formattedValues = cache.map((value) => Number(formatUnits(value, 'gwei')))
const formattedWithdrawalCache = formatForWithdrawal(formattedValues)

const initialBalance = formattedWithdrawalCache[0]
const currentBalance = formattedWithdrawalCache[formattedWithdrawalCache.length - 1]
const rewards = currentBalance - initialBalance
const multiplier = (secondsInDay * 365) / secondsInEpoch / formattedWithdrawalCache.length

const rewardsMultiplied = rewards * multiplier
const projectedBalance = rewardsMultiplied + initialBalance

return calculateAprPercentage(projectedBalance, initialBalance)
})
}, [formattedCache])

return useMemo(() => {
const estimatedApr =
mappedTotalApr && isValidEpochCount
? mappedTotalApr.reduce((acc, a) => acc + a, 0) / mappedTotalApr.length
: undefined
const textColor = formatBalanceColor(estimatedApr)

return {
estimatedApr,
textColor,
}
}, [mappedTotalApr, isValidEpochCount])
}

export default useEpochAprEstimate
24 changes: 24 additions & 0 deletions src/hooks/useFilteredValidatorCacheData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useRecoilValue } from 'recoil'
import { validatorCacheBalanceResult } from '../recoil/atoms'
import { useMemo } from 'react'
import { ValidatorCache } from '../types/validator'

const useFilteredValidatorCacheData = (indices?: string[]): ValidatorCache | undefined => {
const validatorCacheData = useRecoilValue(validatorCacheBalanceResult)

return useMemo(() => {
if (!validatorCacheData) return undefined

if (!indices) return validatorCacheData

return Object.keys(validatorCacheData)
.filter((key) => indices.includes(key))
.reduce((obj, key: string) => {
return Object.assign(obj, {
[key]: validatorCacheData[Number(key)],
})
}, {})
}, [validatorCacheData, indices])
}

export default useFilteredValidatorCacheData
7 changes: 5 additions & 2 deletions src/hooks/useValidatorCachePolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { beaconEpochInterval, validatorCacheBalanceResult } from '../recoil/atom
import { selectActiveValidators } from '../recoil/selectors/selectActiveValidators'
import usePollApi from './usePollApi'
import { selectBeaconUrl } from '../recoil/selectors/selectBeaconUrl'
import { ValidatorCacheResults } from '../types/validator'

const useValidatorCachePolling = () => {
const beaconUrl = useRecoilValue(selectBeaconUrl)
Expand All @@ -25,9 +26,11 @@ const useValidatorCachePolling = () => {
})

useEffect(() => {
const data = response?.data.data.validators
const data = response?.data.data.validators as ValidatorCacheResults
if (data) {
setValidatorCache(data)
setValidatorCache(
Object.fromEntries(Object.entries(data).map(([key, { info }]) => [Number(key), info])),
)
}
}, [response])

Expand Down
21 changes: 4 additions & 17 deletions src/hooks/useValidatorEarnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,14 @@ import {
secondsInWeek,
} from '../constants/constants'
import calculateEpochEstimate from '../utilities/calculateEpochEstimate'
import { validatorCacheBalanceResult } from '../recoil/atoms'
import { selectValidatorInfos } from '../recoil/selectors/selectValidatorInfos'
import calculateAprPercentage from '../utilities/calculateAprPercentage'
import useFilteredValidatorCacheData from './useFilteredValidatorCacheData'

const useValidatorEarnings = (indices?: string[]) => {
const validators = useRecoilValue(selectValidatorInfos)
const validatorCacheData = useRecoilValue(validatorCacheBalanceResult)

const filteredCacheData = useMemo(() => {
if (!validatorCacheData) return undefined

if (!indices) return validatorCacheData

return Object.keys(validatorCacheData)
.filter((key) => indices.includes(key))
.reduce((obj, key: string) => {
return Object.assign(obj, {
[key]: validatorCacheData[Number(key)],
})
}, {})
}, [validatorCacheData, indices])
const filteredCacheData = useFilteredValidatorCacheData(indices)
const filteredValidators = useMemo(() => {
return indices ? validators.filter(({ index }) => indices.includes(String(index))) : validators
}, [validators, indices])
Expand All @@ -36,7 +24,6 @@ const useValidatorEarnings = (indices?: string[]) => {
if (!filteredCacheData) return undefined

return Object.values(filteredCacheData)
.map((cache) => cache.info)
.flat()
.reduce(function (r, a) {
r[a.epoch] = r[a.epoch] || []
Expand Down Expand Up @@ -82,7 +69,7 @@ const useValidatorEarnings = (indices?: string[]) => {
)

const initialEth = filteredValidators.length * initialEthDeposit
const annualizedEarningsPercent = (Math.pow(total / initialEth, 1) - 1) * 100
const annualizedEarningsPercent = calculateAprPercentage(total, initialEth)

return {
total,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useValidatorEpochBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const useValidatorEpochBalance = () => {
return validatorCacheData && activeValidators.length && Object.values(validatorCacheData).length
? activeValidators
.map(({ index, name }) => {
const data = validatorCacheData[index as any]?.info
const data = validatorCacheData[index as any]
return {
index,
name,
Expand All @@ -35,7 +35,7 @@ const useValidatorEpochBalance = () => {
const formattedTimestamps = useMemo(() => {
const data = validatorCacheData && Object.values(validatorCacheData)[0]
return data
? data.info.map(({ epoch }) => {
? data.map(({ epoch }) => {
const slot = epoch * slotsInEpoc

return moment((genesisBlock + slot * secondsInSlot) * 1000).format('HH:mm')
Expand Down
Loading