diff --git a/packages/extension-polkagate/src/hooks/useValidatorApy.ts b/packages/extension-polkagate/src/hooks/useValidatorApy.ts index b5906a795..afa613829 100644 --- a/packages/extension-polkagate/src/hooks/useValidatorApy.ts +++ b/packages/extension-polkagate/src/hooks/useValidatorApy.ts @@ -10,6 +10,11 @@ import { useCallback, useEffect, useState } from 'react'; import { BN, BN_HUNDRED, BN_ZERO } from '@polkadot/util'; +interface ValidatorEraInfo { + netReward: BN; + total: BN; +} + export default function useValidatorApy (api: ApiPromise | undefined, validatorAddress: string, isElected?: boolean): string | undefined | null { const [apy, setApy] = useState(); @@ -20,27 +25,25 @@ export default function useValidatorApy (api: ApiPromise | undefined, validatorA // Define the number of past eras you want to check (e.g., last 10 eras) const eraDepth = 10; - let depth = 0; - let totalDepth = 0; const decimal = new BN(10 ** api.registry.chainDecimals[0]); let totalRewards = BN_ZERO; let totalPoints = BN_ZERO; let validatorPoints = BN_ZERO; let totalStaked = BN_ZERO; + const validatorEraInfo: ValidatorEraInfo[] = []; const currentEra = ((await api.query['staking']['activeEra']()) as Option).unwrap().index.toNumber(); const { commission } = await api.query['staking']['validators'](validatorAddress) as PalletStakingValidatorPrefs; // Loop over the past eras to calculate rewards for the validator for (let eraIndex = currentEra - eraDepth; eraIndex <= currentEra; eraIndex++) { + let netReward; const eraReward = await api.query['staking']['erasValidatorReward'](eraIndex) as Option; if (eraReward.isNone) { continue; } - depth++; - const eraPoints = await api.query['staking']['erasRewardPoints'](eraIndex) as PalletStakingEraRewardPoints; let validatorEraPoints; @@ -58,7 +61,9 @@ export default function useValidatorApy (api: ApiPromise | undefined, validatorA totalPoints = totalPoints.add(eraPoints.total); const _eraReward = eraReward.unwrap(); - totalRewards = totalRewards.add(_eraReward.mul(validatorPoints).div(totalPoints).muln(100 - (commission.toNumber() / 1e7)).div(BN_HUNDRED)); + netReward = _eraReward.mul(validatorPoints).div(totalPoints).muln(100 - (commission.toNumber() / 1e7)).div(BN_HUNDRED); + } else { + continue; } // Fetch the validator's stake in this era @@ -72,29 +77,50 @@ export default function useValidatorApy (api: ApiPromise | undefined, validatorA continue; } - totalDepth++; - totalStaked = totalStaked.add(totalAsBN); + validatorEraInfo.push( + { + netReward, + total: totalAsBN + }); } } - // Average the total staked amount over the eras - totalStaked = totalStaked.div(decimal).divn(totalDepth); + if (!validatorEraInfo.length) { + setApy(null); + } + + validatorEraInfo.forEach(({ netReward, total }) => { + totalRewards = totalRewards.add(netReward); + totalStaked = totalStaked.add(total); + }); - const dailyReward = totalRewards.div(decimal).divn(depth); + const actualDepth = validatorEraInfo.length; + + totalStaked = totalStaked.div(decimal).divn(actualDepth); + + const dailyReward = totalRewards.div(decimal).divn(actualDepth); // Calculate daily return as a fraction of the staked amount const dailyReturn = dailyReward.toNumber() / totalStaked.toNumber(); const APY = (dailyReturn * 365 * 100).toFixed(2); + if (!isFinite(+APY) || isNaN(+APY)) { + setApy(null); + } + setApy(APY); }, [api]); useEffect(() => { - if (!api || !isElected) { + if (!api || isElected === undefined) { return; } + if (isElected === false) { + return setApy(null); + } + calculateValidatorAPY(validatorAddress).catch(console.error); }, [api, calculateValidatorAPY, isElected, validatorAddress]);