diff --git a/packages/page-staking/src/Overview/SummaryGeneral.tsx b/packages/page-staking/src/Overview/SummaryGeneral.tsx index 2d2b92f25dc6..4c1e8c9d3109 100644 --- a/packages/page-staking/src/Overview/SummaryGeneral.tsx +++ b/packages/page-staking/src/Overview/SummaryGeneral.tsx @@ -85,11 +85,17 @@ totalStaked } }: Props) { >{inflation.toFixed(1)}% ('returns')}> - 0) && Number.isFinite(stakedReturn)} - > - {stakedReturn.toFixed(1)}% - + {stakedReturn !== null + ? 0) && + Number.isFinite(stakedReturn)} + // eslint-disable-next-line @typescript-eslint/indent + > + {stakedReturn.toFixed(1)}% + + : '0%'} diff --git a/packages/page-staking/src/Overview/SummaryNominators.tsx b/packages/page-staking/src/Overview/SummaryNominators.tsx index e0b2398f3f0d..23904aeabd81 100644 --- a/packages/page-staking/src/Overview/SummaryNominators.tsx +++ b/packages/page-staking/src/Overview/SummaryNominators.tsx @@ -64,9 +64,11 @@ function SummaryNominators ({ targets: { maxNominatorsCount, help={t('Number of nominators backing active validators in the current era.')} label={t('active')} > - - {formatNumber(nominatorActiveCount)} - + {nominatorActiveCount !== null + ? + {formatNumber(nominatorActiveCount)} + + : '-'} @@ -91,9 +93,11 @@ function SummaryNominators ({ targets: { maxNominatorsCount, help={t('Minimum threshold stake among active nominators.')} label={t('min active thrs')} > - - {nominatorMinActiveThreshold} - + {nominatorMinActiveThreshold !== null + ? + {nominatorMinActiveThreshold} + + : '-'}
diff --git a/packages/page-staking/src/Overview/SummaryValidators.tsx b/packages/page-staking/src/Overview/SummaryValidators.tsx index b2517cca25e2..6e31baf1ea29 100644 --- a/packages/page-staking/src/Overview/SummaryValidators.tsx +++ b/packages/page-staking/src/Overview/SummaryValidators.tsx @@ -61,9 +61,11 @@ function SummaryValidators ({ targets: help={t('Count of active validators.')} label={t('active')} > - - {validatorActiveCount} - + {validatorActiveCount !== null + ? + {validatorActiveCount} + + : '-'}
@@ -88,9 +90,11 @@ function SummaryValidators ({ targets: help={t('Minimum threshold stake among active validators.')} label={t('min active thrs')} > - - {validatorMinActiveThreshold} - + {validatorMinActiveThreshold !== null + ? + {validatorMinActiveThreshold} + + : '-'}
diff --git a/packages/page-staking/src/types.ts b/packages/page-staking/src/types.ts index d3c96ae13ec6..b51dbfe25ec1 100644 --- a/packages/page-staking/src/types.ts +++ b/packages/page-staking/src/types.ts @@ -66,7 +66,7 @@ export interface ValidatorInfo extends ValidatorInfoRank { numNominators: number; numRecentPayouts: number; skipRewards: boolean; - stakedReturn: number; + stakedReturn: number | null; stakedReturnCmp: number; validatorPrefs?: ValidatorPrefs | ValidatorPrefsTo196; withReturns?: boolean; @@ -96,15 +96,15 @@ export interface SortedTargets { validators?: ValidatorInfo[]; validatorIds?: string[]; waitingIds?: string[]; - nominatorActiveCount?: number; - nominatorElectingCount?: number; + nominatorActiveCount?: number | null; + nominatorElectingCount?: number | null; nominatorIntentionCount?: number; - validatorActiveCount?: number; + validatorActiveCount?: number | null; validatorIntentionCount?: number; validatorWaitingCount?: number; - nominatorMinActiveThreshold?: string; + nominatorMinActiveThreshold?: string | null; nominatorMaxElectingCount?: u32 | null; - validatorMinActiveThreshold?: string; + validatorMinActiveThreshold?: string | null; } export interface PoolAccounts { diff --git a/packages/page-staking/src/useSortedTargets.ts b/packages/page-staking/src/useSortedTargets.ts index a6151a1f9b9c..e02a5f242990 100644 --- a/packages/page-staking/src/useSortedTargets.ts +++ b/packages/page-staking/src/useSortedTargets.ts @@ -9,7 +9,7 @@ import type { SortedTargets, TargetSortBy, ValidatorInfo } from './types'; import { useEffect, useMemo, useState } from 'react'; -import { createNamedHook, useAccounts, useApi, useCall, useCallMulti, useInflation } from '@polkadot/react-hooks'; +import { createNamedHook, useAccounts, useApi, useCall, useCallMulti, useInflation, usePrevious } from '@polkadot/react-hooks'; import { AccountId32 } from '@polkadot/types/interfaces'; import { PalletStakingExposure, PalletStakingIndividualExposure } from '@polkadot/types/lookup'; import { arrayFlatten, BN, BN_HUNDRED, BN_MAX_INTEGER, BN_ONE, BN_ZERO, formatBalance } from '@polkadot/util'; @@ -57,10 +57,10 @@ const OPT_MULTI = { historyDepth, maxNominatorsCount: optMaxNominatorsCount && optMaxNominatorsCount.isSome ? optMaxNominatorsCount.unwrap() - : undefined, + : new BN(0), maxValidatorsCount: optMaxValidatorsCount && optMaxValidatorsCount.isSome ? optMaxValidatorsCount.unwrap() - : undefined, + : new BN(0), minNominatorBond, minValidatorBond, totalIssuance @@ -192,7 +192,7 @@ function extractSingle (api: ApiPromise, allAccounts: string[], derive: DeriveSt rankOverall: 0, rankReward: 0, skipRewards, - stakedReturn: 0, + stakedReturn: null, stakedReturnCmp: 0, validatorPrefs, withReturns @@ -212,7 +212,7 @@ function addReturns (inflation: Inflation, baseInfo: Partial): Pa avgStaked && !avgStaked.isZero() && validators.forEach((v): void => { if (!v.skipRewards && v.withReturns) { - const adjusted = avgStaked.mul(BN_HUNDRED).imuln(inflation.stakedReturn).div(v.bondTotal); + const adjusted = avgStaked.mul(BN_HUNDRED).imuln(inflation.stakedReturn || 0).div(v.bondTotal); // in some cases, we may have overflows... protect against those v.stakedReturn = (adjusted.gt(BN_MAX_INTEGER) ? BN_MAX_INTEGER : adjusted).toNumber() / BN_HUNDRED.toNumber(); @@ -293,23 +293,43 @@ function useSortedTargetsImpl (favorites: string[], withLedger: boolean): Sorted const waitingInfo = useCall(api.derive.staking.waitingInfo, [{ ...DEFAULT_FLAGS_WAITING, withLedger }]); const lastEraInfo = useCall(api.derive.session.info, undefined, OPT_ERA); const [stakers, setStakers] = useState<[StorageKey<[u32, AccountId32]>, PalletStakingExposure][]>([]); - const [stakersTotal, setStakersTotal] = useState(); - const [nominatorMinActiveThreshold, setNominatorMinActiveThreshold] = useState(''); + const [stakersTotal, setStakersTotal] = useState(); + const [nominatorMinActiveThreshold, setNominatorMinActiveThreshold] = useState(''); const [nominatorMaxElectingCount, setNominatorMaxElectingCount] = useState(); - const [nominatorElectingCount, setNominatorElectingCount] = useState(); - const [nominatorActiveCount, setNominatorActiveCount] = useState(); - const [validatorActiveCount, setValidatorActiveCount] = useState(); + const [nominatorElectingCount, setNominatorElectingCount] = useState(); + const [nominatorActiveCount, setNominatorActiveCount] = useState(); + const [validatorActiveCount, setValidatorActiveCount] = useState(); - const [calcStakers, setCalcStakers] = useState(false); + const [timerDone, setTimerDone] = useState(false); + + const prevStakersLength = usePrevious(stakers.length); + + useEffect(() => { + // This timer is set to wait for 10 seconds in order to identify + // if api has finished loading the staking info. If at the end of this timer + // the values from API are not yet loaded, then it is assumed that the + // API is not returning any values (meaning probable misconfiguration). + // This is for covering edge cases (e.g. staking pallet is included + // in apps but not used - stakers = 0). + const apiTimer = setTimeout(() => setTimerDone(true), 10000); + + return () => { + clearTimeout(apiTimer); + }; + }, []); useEffect(() => { - if (stakers[0] && stakers[0][1]) { + if (prevStakersLength !== stakers.length && stakers[0] && stakers[0][1]) { setStakersTotal(stakers[0][1].total.toBn()); + } else if (stakers.length === 0) { + if (timerDone) { + setStakersTotal(null); + } } - }, [stakers]); + }, [prevStakersLength, stakers, timerDone]); useEffect(() => { - if (stakers.length && !calcStakers) { + if (stakers.length !== prevStakersLength) { const assignments: Map = new Map(); stakers.sort((a, b) => a[1].total.toBn().cmp(b[1].total.toBn())).map((x) => x[1].others).flat(1).forEach((x) => { @@ -319,20 +339,24 @@ function useSortedTargetsImpl (favorites: string[], withLedger: boolean): Sorted assignments.set(nominator, val ? amount.toBn().add(val) : amount.toBn()); }); - const nominatorStakes = Array.from(assignments); nominatorStakes.sort((a, b) => a[1].cmp(b[1])); - setNominatorMaxElectingCount(api.consts.electionProviderMultiPhase?.maxElectingVoters); - setNominatorElectingCount(assignments.size); setNominatorActiveCount(assignments.size); setNominatorMinActiveThreshold(nominatorStakes[0] ? b(nominatorStakes[0][1], api) : ''); setValidatorActiveCount(stakers.length); - setCalcStakers(true); + } else if (stakers.length === 0) { + if (timerDone) { + setNominatorMaxElectingCount(api.consts.electionProviderMultiPhase?.maxElectingVoters); + setNominatorElectingCount(null); + setNominatorActiveCount(null); + setNominatorMinActiveThreshold(null); + setValidatorActiveCount(null); + } } - }, [api, calcStakers, stakers]); + }, [api, prevStakersLength, stakers, timerDone]); const baseInfo = useMemo( () => electedInfo && lastEraInfo && totalIssuance && waitingInfo @@ -343,6 +367,12 @@ function useSortedTargetsImpl (favorites: string[], withLedger: boolean): Sorted const inflation = useInflation(baseInfo?.totalStaked); + useEffect(() => { + if (!inflation.stakedReturn && timerDone) { + inflation.stakedReturn = null; + } + }, [inflation, inflation.stakedReturn, timerDone]); + const curEra = useCall>(api.query.staking.currentEra); const getStakers = useMemo(() => async (currentEra: u32) => { @@ -351,7 +381,7 @@ function useSortedTargetsImpl (favorites: string[], withLedger: boolean): Sorted curEra && getStakers(curEra?.unwrap()); - const validatorMinActiveThreshold = stakersTotal ? b(stakersTotal, api) : ''; + const validatorMinActiveThreshold = stakersTotal ? b(stakersTotal, api) : null; return useMemo( (): SortedTargets => ({ @@ -372,7 +402,7 @@ function useSortedTargetsImpl (favorites: string[], withLedger: boolean): Sorted validatorActiveCount, validatorMinActiveThreshold, ...( - inflation && inflation.stakedReturn + inflation && (inflation.stakedReturn !== null && inflation.stakedReturn) ? addReturns(inflation, baseInfo) : baseInfo ) diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 8c4306889bff..ad66a4d62f5a 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -53,6 +53,7 @@ export { usePopupWindow } from './usePopupWindow'; export { useProxies } from './useProxies'; export { useIsParasLinked, useParaEndpoints } from './useParaEndpoints'; export { usePassword } from './usePassword'; +export { usePrevious } from './usePrevious'; export { useRegistrars } from './useRegistrars'; export { useSavedFlags } from './useSavedFlags'; export { useScroll } from './useScroll'; diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts index 4ed597978654..a1d9cd3e95ab 100644 --- a/packages/react-hooks/src/types.ts +++ b/packages/react-hooks/src/types.ts @@ -40,7 +40,7 @@ export interface Inflation { idealInterest: number; inflation: number; stakedFraction: number; - stakedReturn: number; + stakedReturn: number | null; } export interface Slash { diff --git a/packages/react-hooks/src/usePrevious.ts b/packages/react-hooks/src/usePrevious.ts new file mode 100644 index 000000000000..fece6e3b9b3e --- /dev/null +++ b/packages/react-hooks/src/usePrevious.ts @@ -0,0 +1,19 @@ +// Copyright 2017-2022 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useRef } from 'react'; + +import { createNamedHook } from './createNamedHook'; + +function usePreviousImpl (v: any) { + const ref = useRef(); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ref.current = v; + }, [v]); + + return ref.current; +} + +export const usePrevious = createNamedHook('usePopupWindow', usePreviousImpl);