diff --git a/packages/page-staking/src/Actions/Account/BondExtra.tsx b/packages/page-staking/src/Actions/Account/BondExtra.tsx index c052326c33..c4e1872e21 100644 --- a/packages/page-staking/src/Actions/Account/BondExtra.tsx +++ b/packages/page-staking/src/Actions/Account/BondExtra.tsx @@ -6,7 +6,8 @@ import { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/ty import { AmountValidateState } from '../types'; import BN from 'bn.js'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; +import { ApiPromise } from '@polkadot/api'; import { InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { BalanceFree } from '@polkadot/react-query'; @@ -22,32 +23,35 @@ interface Props { stashId: string; } +function calcBalance (api: ApiPromise, stakingInfo?: DeriveStakingAccount, stashBalance?: DeriveBalancesAll): BN | null { + if (stakingInfo && stakingInfo.stakingLedger && stashBalance) { + const sumUnlocking = (stakingInfo.unlocking || []).reduce((acc, { value }) => acc.iadd(value), new BN(0)); + const redeemable = stakingInfo.redeemable || BN_ZERO; + const available = stashBalance.freeBalance.sub(stakingInfo.stakingLedger.active.unwrap()).sub(sumUnlocking).sub(redeemable); + + return available.gt(api.consts.balances.existentialDeposit) + ? available.sub(api.consts.balances.existentialDeposit) + : BN_ZERO; + } + + return null; +} + function BondExtra ({ controllerId, onClose, stakingInfo, stashId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const [amountError, setAmountError] = useState(null); const [maxAdditional, setMaxAdditional] = useState(); - const [maxBalance] = useState(); - const [startBalance, setStartBalance] = useState(null); const stashBalance = useCall(api.derive.balances.all, [stashId]); const currentAmount = useMemo( () => stakingInfo && stakingInfo.stakingLedger?.active.unwrap(), [stakingInfo] ); - useEffect((): void => { - if (stakingInfo && stakingInfo.stakingLedger && stashBalance) { - const sumUnlocking = stakingInfo.unlocking?.reduce((acc, { value }) => acc.add(value), BN_ZERO) || BN_ZERO; - const redeemable = stakingInfo.redeemable || BN_ZERO; - const available = stashBalance.freeBalance.sub(stakingInfo.stakingLedger.active.unwrap()).sub(sumUnlocking).sub(redeemable); - - setStartBalance( - available.gt(api.consts.balances.existentialDeposit) - ? available.sub(api.consts.balances.existentialDeposit) - : BN_ZERO - ); - } - }, [api, stakingInfo, stashBalance]); + const startBalance = useMemo( + () => calcBalance(api, stakingInfo, stashBalance), + [api, stakingInfo, stashBalance] + ); return ( } - maxValue={maxBalance} onChange={setMaxAdditional} /> ({ + era, + slashes: all.filter(({ others, validator }) => + validator.eq(stashId) || others.some(([nominatorId]) => nominatorId.eq(stashId)) + ) + })) + .filter(({ slashes }) => slashes.length); +} + function Account ({ allSlashes, className = '', info: { controllerId, destination, destinationId, hexSessionIdNext, hexSessionIdQueue, isLoading, isOwnController, isOwnStash, isStashNominating, isStashValidating, nominating, sessionIds, stakingLedger, stashId }, isDisabled, targets }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); @@ -59,27 +70,11 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio const [isSettingsOpen, toggleSettings] = useToggle(); const [isUnbondOpen, toggleUnbond] = useToggle(); const [isValidateOpen, toggleValidate] = useToggle(); - const [hasBonded, setHasBonded] = useState(false); - const [slashes, setSlashes] = useState([]); - useEffect((): void => { - stakingAccount?.stakingLedger && setHasBonded( - !stakingAccount.stakingLedger.active.isEmpty - ); - }, [stakingAccount]); - - useEffect((): void => { - allSlashes && setSlashes( - allSlashes - .map(([era, all]) => ({ - era, - slashes: all.filter(({ others, validator }) => - validator.eq(stashId) || others.some(([nominatorId]) => nominatorId.eq(stashId)) - ) - })) - .filter(({ slashes }) => slashes.length) - ); - }, [allSlashes, stashId]); + const slashes = useMemo( + () => extractSlashes(stashId, allSlashes), + [allSlashes, stashId] + ); const withdrawFunds = useCallback( () => { @@ -95,6 +90,8 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio [api, controllerId, queueExtrinsic, spanCount] ); + const hasBonded = !!stakingAccount?.stakingLedger && !stakingAccount.stakingLedger.active.isEmpty; + return ( diff --git a/packages/page-staking/src/Actions/index.tsx b/packages/page-staking/src/Actions/index.tsx index a68817fd12..168ffc1e99 100644 --- a/packages/page-staking/src/Actions/index.tsx +++ b/packages/page-staking/src/Actions/index.tsx @@ -6,7 +6,7 @@ import { StakerState } from '@polkadot/react-hooks/types'; import { SortedTargets } from '../types'; import BN from 'bn.js'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Button, Table } from '@polkadot/react-components'; import { useAvailableSlashes } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; @@ -37,7 +37,11 @@ function sortStashes (a: StakerState, b: StakerState): number { return (a.isStashValidating ? 1 : (a.isStashNominating ? 5 : 99)) - (b.isStashValidating ? 1 : (b.isStashNominating ? 5 : 99)); } -function extractState (ownStashes: StakerState[]): State { +function extractState (ownStashes?: StakerState[]): State { + if (!ownStashes) { + return {}; + } + return { bondedTotal: ownStashes.reduce((total: BN, { stakingLedger }) => stakingLedger @@ -51,11 +55,11 @@ function extractState (ownStashes: StakerState[]): State { function Actions ({ className = '', isInElection, ownStashes, targets }: Props): React.ReactElement { const { t } = useTranslation(); const allSlashes = useAvailableSlashes(); - const [{ bondedTotal, foundStashes }, setState] = useState({}); - useEffect((): void => { - ownStashes && setState(extractState(ownStashes)); - }, [ownStashes]); + const { bondedTotal, foundStashes } = useMemo( + () => extractState(ownStashes), + [ownStashes] + ); const header = useMemo(() => [ [t('stashes'), 'start', 2], diff --git a/packages/page-staking/src/Actions/useUnbondDuration.ts b/packages/page-staking/src/Actions/useUnbondDuration.ts index 9786365291..c6b07e8535 100644 --- a/packages/page-staking/src/Actions/useUnbondDuration.ts +++ b/packages/page-staking/src/Actions/useUnbondDuration.ts @@ -5,22 +5,18 @@ import { DeriveSessionInfo } from '@polkadot/api-derive/types'; import BN from 'bn.js'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useApi, useCall } from '@polkadot/react-hooks'; import { BN_ONE } from '@polkadot/util'; export default function useUnbondDuration (): BN | undefined { const { api } = useApi(); const sessionInfo = useCall(api.derive.session.info, []); - const [duration, setDuration] = useState(); - useEffect((): void => { - // We check sessionLength, for aura this is not properly calculated (not exposed), - // so the actual real block value would be invalid here - sessionInfo && sessionInfo.sessionLength.gt(BN_ONE) && setDuration( - sessionInfo.eraLength.mul(api.consts.staking.bondingDuration) - ); - }, [api, sessionInfo]); - - return duration; + return useMemo( + () => (sessionInfo && sessionInfo.sessionLength.gt(BN_ONE)) + ? sessionInfo.eraLength.mul(api.consts.staking.bondingDuration) + : undefined, + [api, sessionInfo] + ); } diff --git a/packages/page-staking/src/Overview/Address/Favorite.tsx b/packages/page-staking/src/Overview/Address/Favorite.tsx index 094d0a404b..a13dbadd38 100644 --- a/packages/page-staking/src/Overview/Address/Favorite.tsx +++ b/packages/page-staking/src/Overview/Address/Favorite.tsx @@ -15,7 +15,7 @@ interface Props { function Favorite ({ address, className, isFavorite, toggleFavorite }: Props): React.ReactElement { const _onFavorite = useCallback( - (): void => toggleFavorite(address), + () => toggleFavorite(address), [address, toggleFavorite] ); diff --git a/packages/page-staking/src/Overview/Address/NominatedBy.tsx b/packages/page-staking/src/Overview/Address/NominatedBy.tsx index b42d9a4925..f6e6f076a9 100644 --- a/packages/page-staking/src/Overview/Address/NominatedBy.tsx +++ b/packages/page-staking/src/Overview/Address/NominatedBy.tsx @@ -4,7 +4,7 @@ import { EraIndex, SlashingSpans } from '@polkadot/types/interfaces'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import { AddressMini, Expander, Spinner } from '@polkadot/react-components'; import { formatNumber } from '@polkadot/util'; @@ -20,24 +20,26 @@ interface Chilled { chilled: string[]; } +function extractChilled (nominators: [string, EraIndex, number][] = [], slashingSpans?: SlashingSpans | null): Chilled { + const chilled = slashingSpans + ? nominators + .filter(([, submittedIn]) => !slashingSpans.lastNonzeroSlash.isZero() && slashingSpans.lastNonzeroSlash.gte(submittedIn)) + .map(([who]) => who) + : []; + const active = nominators + .filter(([who]) => !chilled.includes(who)) + .map(([who]) => who); + + return { active, chilled }; +} + function NominatedBy ({ nominators, slashingSpans }: Props): React.ReactElement { const { t } = useTranslation(); - const [{ active, chilled }, setChilled] = useState({ active: [], chilled: [] }); - useEffect((): void => { - if (nominators) { - const chilled = slashingSpans - ? nominators - .filter(([, submittedIn]) => !slashingSpans.lastNonzeroSlash.isZero() && slashingSpans.lastNonzeroSlash.gte(submittedIn)) - .map(([who]) => who) - : []; - const active = nominators - .filter(([who]) => !chilled.includes(who)) - .map(([who]) => who); - - setChilled({ active, chilled }); - } - }, [nominators, slashingSpans]); + const { active, chilled } = useMemo( + () => extractChilled(nominators, slashingSpans), + [nominators, slashingSpans] + ); return ( diff --git a/packages/page-staking/src/Overview/Address/index.tsx b/packages/page-staking/src/Overview/Address/index.tsx index 2509813f83..d38ee70577 100644 --- a/packages/page-staking/src/Overview/Address/index.tsx +++ b/packages/page-staking/src/Overview/Address/index.tsx @@ -6,7 +6,7 @@ import { Balance, EraIndex, SlashingSpans } from '@polkadot/types/interfaces'; import { DeriveAccountInfo, DeriveStakingQuery } from '@polkadot/api-derive/types'; import BN from 'bn.js'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { AddressSmall, Icon } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; import { useApi, useCall } from '@polkadot/react-hooks'; @@ -78,18 +78,16 @@ function Address ({ address, className = '', filterName, hasQueries, isElected, const slashingSpans = useCall(!isMain && api.query.staking.slashingSpans, [address], { transform: (opt: Option) => opt.unwrapOr(null) }); - const [{ commission, nominators, stakeOther, stakeOwn }, setStakingState] = useState({ nominators: [] }); - const [isVisible, setIsVisible] = useState(true); - useEffect((): void => { - stakingInfo && setStakingState(expandInfo(stakingInfo)); - }, [stakingInfo]); + const { commission, nominators, stakeOther, stakeOwn } = useMemo( + () => stakingInfo ? expandInfo(stakingInfo) : { nominators: [] }, + [stakingInfo] + ); - useEffect((): void => { - accountInfo && setIsVisible( - checkVisibility(api, address, accountInfo, filterName, withIdentity) - ); - }, [api, accountInfo, address, filterName, withIdentity]); + const isVisible = useMemo( + () => accountInfo ? checkVisibility(api, address, accountInfo, filterName, withIdentity) : true, + [api, accountInfo, address, filterName, withIdentity] + ); const _onQueryStats = useCallback( (): void => { diff --git a/packages/page-staking/src/Overview/CurrentList.tsx b/packages/page-staking/src/Overview/CurrentList.tsx index 0ea1025841..6495dd0f23 100644 --- a/packages/page-staking/src/Overview/CurrentList.tsx +++ b/packages/page-staking/src/Overview/CurrentList.tsx @@ -6,7 +6,7 @@ import { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/ty import { AccountId, EraIndex, Nominations } from '@polkadot/types/interfaces'; import { Authors } from '@polkadot/react-query/BlockAuthors'; -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import { Table } from '@polkadot/react-components'; import { useApi, useCall, useLoadingDelay } from '@polkadot/react-hooks'; import { BlockAuthorsContext } from '@polkadot/react-query'; @@ -97,25 +97,21 @@ function CurrentList ({ favorites, hasQueries, isIntentions, next, stakingOvervi const { byAuthor, eraPoints } = useContext(isIntentions ? EmptyAuthorsContext : BlockAuthorsContext); const recentlyOnline = useCall(!isIntentions && api.derive.imOnline?.receivedHeartbeats, []); const nominators = useCall<[StorageKey, Option][]>(isIntentions && api.query.staking.nominators.entries as any, []); - const [{ elected, validators, waiting }, setFiltered] = useState({}); const [nameFilter, setNameFilter] = useState(''); - const [nominatedBy, setNominatedBy] = useState | null>(); const [withIdentity, setWithIdentity] = useState(false); // we have a very large list, so we use a loading delay const isLoading = useLoadingDelay(); - useEffect((): void => { - stakingOverview && setFiltered( - getFiltered(stakingOverview, favorites, next) - ); - }, [favorites, next, stakingOverview]); + const { elected, validators, waiting } = useMemo( + () => stakingOverview ? getFiltered(stakingOverview, favorites, next) : {}, + [favorites, next, stakingOverview] + ); - useEffect((): void => { - nominators && setNominatedBy( - extractNominators(nominators) - ); - }, [nominators]); + const nominatedBy = useMemo( + () => nominators ? extractNominators(nominators) : null, + [nominators] + ); const headerWaiting = useMemo(() => [ [t('intentions'), 'start', 2], diff --git a/packages/page-staking/src/Payouts/Validator.tsx b/packages/page-staking/src/Payouts/Validator.tsx index b340b98898..6dfae8af41 100644 --- a/packages/page-staking/src/Payouts/Validator.tsx +++ b/packages/page-staking/src/Payouts/Validator.tsx @@ -5,7 +5,7 @@ import { PayoutValidator } from './types'; import BN from 'bn.js'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import { AddressMini, AddressSmall, Expander } from '@polkadot/react-components'; import { BlockToTime, FormatBalance } from '@polkadot/react-query'; @@ -27,32 +27,33 @@ interface State { oldestEra?: BN; } +function extractState (payout: PayoutValidator): State { + const eraStr = createErasString(payout.eras.map(({ era }) => era)); + const nominators = payout.eras.reduce((nominators: Record, { stashes }): Record => { + Object.entries(stashes).forEach(([stashId, value]): void => { + if (nominators[stashId]) { + nominators[stashId] = nominators[stashId].add(value); + } else { + nominators[stashId] = value; + } + }); + + return nominators; + }, {}); + + return { eraStr, nominators, numNominators: Object.keys(nominators).length, oldestEra: payout.eras[0]?.era }; +} + function Validator ({ className = '', isDisabled, payout }: Props): React.ReactElement { const { t } = useTranslation(); - const [{ eraStr, nominators, numNominators, oldestEra }, setState] = useState({ - eraStr: '', - nominators: {}, - numNominators: 0 - }); + + const { eraStr, nominators, numNominators, oldestEra } = useMemo( + () => extractState(payout), + [payout] + ); + const eraBlocks = useEraBlocks(oldestEra); - useEffect((): void => { - const eraStr = createErasString(payout.eras.map(({ era }) => era)); - const nominators = payout.eras.reduce((nominators: Record, { stashes }): Record => { - Object.entries(stashes).forEach(([stashId, value]): void => { - if (nominators[stashId]) { - nominators[stashId] = nominators[stashId].add(value); - } else { - nominators[stashId] = value; - } - }); - - return nominators; - }, {}); - - setState({ eraStr, nominators, numNominators: Object.keys(nominators).length, oldestEra: payout.eras[0]?.era }); - }, [payout]); - return ( (api.query.staking.historyDepth, []); const progress = useCall(api.derive.session.progress, []); const forcing = useCall(api.query.staking.forceEra, []); - const [duration, setDuration] = useState(); - useEffect((): void => { - depth && era && forcing && progress && progress.sessionLength.gt(BN_ONE) && setDuration( - ( + return useMemo( + () => (depth && era && forcing && progress && progress.sessionLength.gt(BN_ONE)) + ? ( forcing.isForceAlways ? progress.sessionLength : progress.eraLength @@ -33,8 +32,7 @@ export default function useEraBlocks (era?: BN): BN | undefined { ? progress.sessionProgress : progress.eraProgress ) - ); - }, [api, depth, era, forcing, progress]); - - return duration; + : undefined, + [depth, era, forcing, progress] + ); } diff --git a/packages/page-staking/src/Payouts/useStakerPayouts.ts b/packages/page-staking/src/Payouts/useStakerPayouts.ts index 47424adbaa..aea9c453a1 100644 --- a/packages/page-staking/src/Payouts/useStakerPayouts.ts +++ b/packages/page-staking/src/Payouts/useStakerPayouts.ts @@ -5,23 +5,21 @@ import { EraIndex } from '@polkadot/types/interfaces'; import BN from 'bn.js'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useApi, useCall } from '@polkadot/react-hooks'; import { Option } from '@polkadot/types'; import { isFunction } from '@polkadot/util'; export default function useStakerPayouts (): BN { const { api } = useApi(); - const [stakerPayoutAfter, setState] = useState( - isFunction(api.tx.staking.payoutStakers) - ? new BN(0) - : new BN(1_000_000_000) - ); const migrateEraOpt = useCall>(api.query.staking?.migrateEra, []); - useEffect((): void => { - migrateEraOpt && migrateEraOpt.isSome && setState(migrateEraOpt.unwrap()); - }, [migrateEraOpt]); - - return stakerPayoutAfter; + return useMemo( + () => (migrateEraOpt && migrateEraOpt.isSome && migrateEraOpt.unwrap()) || ( + isFunction(api.tx.staking.payoutStakers) + ? new BN(0) + : new BN(1_000_000_000) + ), + [api, migrateEraOpt] + ); } diff --git a/packages/page-staking/src/Query/ChartPoints.tsx b/packages/page-staking/src/Query/ChartPoints.tsx index d3673788b5..ce792a77a3 100644 --- a/packages/page-staking/src/Query/ChartPoints.tsx +++ b/packages/page-staking/src/Query/ChartPoints.tsx @@ -5,7 +5,7 @@ import { DeriveStakerPoints } from '@polkadot/api-derive/types'; import { ChartInfo, LineDataEntry, Props } from './types'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Chart, Spinner } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; @@ -41,19 +41,20 @@ function extractPoints (points: DeriveStakerPoints[]): ChartInfo { function ChartPoints ({ validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const [{ chart, labels }, setChart] = useState({ chart: [], labels: [] }); const stakerPoints = useCall(api.derive.staking.stakerPoints, [validatorId, true]); + + const { chart, labels } = useMemo( + () => stakerPoints + ? extractPoints(stakerPoints) + : { chart: [], labels: [] }, + [stakerPoints] + ); + const legends = useMemo(() => [ t('points'), t('average') ], [t]); - useEffect((): void => { - stakerPoints && setChart( - extractPoints(stakerPoints) - ); - }, [stakerPoints]); - return (

{t('era points')}

diff --git a/packages/page-staking/src/Query/ChartRewards.tsx b/packages/page-staking/src/Query/ChartRewards.tsx index 6faef52c5d..1ff038f6e2 100644 --- a/packages/page-staking/src/Query/ChartRewards.tsx +++ b/packages/page-staking/src/Query/ChartRewards.tsx @@ -6,7 +6,7 @@ import { DeriveEraRewards, DeriveOwnSlashes, DeriveStakerPoints } from '@polkado import { ChartInfo, LineDataEntry, Props } from './types'; import BN from 'bn.js'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Chart, Spinner } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { formatBalance } from '@polkadot/util'; @@ -55,26 +55,28 @@ function extractRewards (erasRewards: DeriveEraRewards[], ownSlashes: DeriveOwnS function ChartRewards ({ validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const [{ chart, labels }, setChart] = useState({ chart: [], labels: [] }); const ownSlashes = useCall(api.derive.staking.ownSlashes, [validatorId, true]); const erasRewards = useCall(api.derive.staking.erasRewards, []); const stakerPoints = useCall(api.derive.staking.stakerPoints, [validatorId, true]); + const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({ currency: formatBalance.getDefaults().unit, divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')) }), []); + + const { chart, labels } = useMemo( + () => (erasRewards && ownSlashes && stakerPoints) + ? extractRewards(erasRewards, ownSlashes, stakerPoints, divisor) + : { chart: [], labels: [] }, + [divisor, erasRewards, ownSlashes, stakerPoints] + ); + const legends = useMemo(() => [ t('{{currency}} slashed', { replace: { currency } }), t('{{currency}} rewards', { replace: { currency } }), t('{{currency}} average', { replace: { currency } }) ], [currency, t]); - useEffect((): void => { - erasRewards && ownSlashes && stakerPoints && setChart( - extractRewards(erasRewards, ownSlashes, stakerPoints, divisor) - ); - }, [divisor, erasRewards, ownSlashes, stakerPoints]); - return (

{t('rewards & slashes')}

diff --git a/packages/page-staking/src/Query/ChartStake.tsx b/packages/page-staking/src/Query/ChartStake.tsx index a3020a6c82..d8c462a6d9 100644 --- a/packages/page-staking/src/Query/ChartStake.tsx +++ b/packages/page-staking/src/Query/ChartStake.tsx @@ -6,7 +6,7 @@ import { DeriveOwnExposure } from '@polkadot/api-derive/types'; import { ChartInfo, LineDataEntry, Props } from './types'; import BN from 'bn.js'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Chart, Spinner } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { formatBalance } from '@polkadot/util'; @@ -49,24 +49,26 @@ function extractStake (exposures: DeriveOwnExposure[], divisor: BN): ChartInfo { function ChartStake ({ validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const [{ chart, labels }, setChart] = useState({ chart: [], labels: [] }); const ownExposures = useCall(api.derive.staking.ownExposures, [validatorId, true]); + const { currency, divisor } = useMemo((): { currency: string; divisor: BN } => ({ currency: formatBalance.getDefaults().unit, divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')) }), []); + + const { chart, labels } = useMemo( + () => ownExposures + ? extractStake(ownExposures, divisor) + : { chart: [], labels: [] }, + [divisor, ownExposures] + ); + const legends = useMemo(() => [ t('{{currency}} clipped', { replace: { currency } }), t('{{currency}} total', { replace: { currency } }), t('{{currency}} average', { replace: { currency } }) ], [currency, t]); - useEffect((): void => { - ownExposures && setChart( - extractStake(ownExposures, divisor) - ); - }, [divisor, ownExposures]); - return (

{t('elected stake')}

diff --git a/packages/page-staking/src/Slashes/Summary.tsx b/packages/page-staking/src/Slashes/Summary.tsx index 6e72644400..6c30d8348d 100644 --- a/packages/page-staking/src/Slashes/Summary.tsx +++ b/packages/page-staking/src/Slashes/Summary.tsx @@ -6,7 +6,7 @@ import { DeriveSessionProgress } from '@polkadot/api-derive/types'; import { SlashEra } from './types'; import BN from 'bn.js'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import { CardSummary, SummaryBox } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; @@ -18,24 +18,20 @@ interface Props { slash: SlashEra; } -function calcBlocks (slashDeferDuration: BN, era: BN, { activeEra, eraLength, eraProgress }: DeriveSessionProgress): [BN, BN] { - return [ - activeEra.sub(era).subn(1).mul(eraLength).add(eraProgress), - slashDeferDuration.mul(eraLength) - ]; -} - function Header ({ slash: { era, nominators, reporters, total, validators } }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const sessionInfo = useCall(api.derive.session?.progress, []); - const [[blockProgress, blockEnd], setBlocks] = useState([new BN(0), new BN(0)]); - useEffect((): void => { - sessionInfo && setBlocks( - calcBlocks(api.consts.staking.slashDeferDuration, era, sessionInfo) - ); - }, [api, era, sessionInfo]); + const [blockProgress, blockEnd] = useMemo( + () => sessionInfo + ? [ + sessionInfo.activeEra.sub(era).subn(1).mul(sessionInfo.eraLength).add(sessionInfo.eraProgress), + api.consts.staking.slashDeferDuration.mul(sessionInfo.eraLength) + ] + : [new BN(0), new BN(0)], + [api, era, sessionInfo] + ); return ( diff --git a/packages/page-staking/src/Targets/Nominate.tsx b/packages/page-staking/src/Targets/Nominate.tsx index 36ce04ea9e..8120211f36 100644 --- a/packages/page-staking/src/Targets/Nominate.tsx +++ b/packages/page-staking/src/Targets/Nominate.tsx @@ -4,7 +4,7 @@ import { StakerState } from '@polkadot/react-hooks/types'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { AddressMini, Button, InputAddress, Modal, Static, TxButton } from '@polkadot/react-components'; import { useToggle } from '@polkadot/react-hooks'; @@ -26,14 +26,12 @@ interface IdState { function Nominate ({ className = '', isDisabled, ownNominators, targets }: Props): React.ReactElement { const { t } = useTranslation(); const [ids, setIds] = useState(null); - const [filter, setFilter] = useState([]); const [isOpen, toggleOpen] = useToggle(); - useEffect((): void => { - ownNominators && setFilter( - ownNominators.map(({ stashId }) => stashId) - ); - }, [ownNominators]); + const filter = useMemo( + () => (ownNominators || []).map(({ stashId }) => stashId), + [ownNominators] + ); const _onChangeStash = useCallback( (accountId?: string | null): void => { diff --git a/packages/page-staking/src/Targets/Summary.tsx b/packages/page-staking/src/Targets/Summary.tsx index 24ac4ff6ef..d439bca886 100644 --- a/packages/page-staking/src/Targets/Summary.tsx +++ b/packages/page-staking/src/Targets/Summary.tsx @@ -5,7 +5,7 @@ import { Balance } from '@polkadot/types/interfaces'; import BN from 'bn.js'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import { SummaryBox, CardSummary } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; @@ -19,25 +19,21 @@ interface Props { totalStaked?: BN; } -interface Progress { - hideValue: true; - total: BN; - value: BN; -} - function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const totalIssuance = useCall(api.query.balances?.totalIssuance, []); - const [progressStake, setProgressStake] = useState(); - useEffect((): void => { - totalIssuance && totalStaked && totalStaked.gtn(0) && setProgressStake({ - hideValue: true, - total: totalIssuance, - value: totalStaked - }); - }, [totalIssuance, totalStaked]); + const progressStake = useMemo( + () => totalIssuance && totalStaked && totalStaked.gtn(0) + ? { + hideValue: true, + total: totalIssuance, + value: totalStaked + } + : undefined, + [totalIssuance, totalStaked] + ); return ( diff --git a/packages/page-staking/src/Targets/Validator.tsx b/packages/page-staking/src/Targets/Validator.tsx index 4242d09dd5..00bc99f659 100644 --- a/packages/page-staking/src/Targets/Validator.tsx +++ b/packages/page-staking/src/Targets/Validator.tsx @@ -7,7 +7,7 @@ import { UnappliedSlash } from '@polkadot/types/interfaces'; import { ValidatorInfo } from '../types'; import BN from 'bn.js'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { ApiPromise } from '@polkadot/api'; import { AddressSmall, Badge, Checkbox, Icon } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; @@ -33,11 +33,6 @@ interface Props { withIdentity: boolean; } -interface Slash { - era: BN; - slashes: UnappliedSlash[]; -} - function checkIdentity (api: ApiPromise, accountInfo: DeriveAccountInfo): boolean { let hasIdentity = false; @@ -62,26 +57,24 @@ function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSe const { t } = useTranslation(); const { api } = useApi(); const accountInfo = useCall(api.derive.accounts.info, [info.accountId]); - const [isVisible, setVisibility] = useState(true); - const [slashes, setSlashes] = useState([]); useEffect((): void => { if (accountInfo) { info.hasIdentity = checkIdentity(api, accountInfo); - setVisibility(checkVisibility(api, info.key, accountInfo, filterName, withIdentity)); } - }, [accountInfo, api, filterName, info, withIdentity]); + }, [api, accountInfo, info]); - useEffect((): void => { - allSlashes && setSlashes( - allSlashes - .map(([era, slashes]) => ({ - era, - slashes: slashes.filter(({ validator }) => validator.eq(info.accountId)) - })) - .filter(({ slashes }) => slashes.length) - ); - }, [allSlashes, info]); + const isVisible = useMemo( + () => accountInfo ? checkVisibility(api, info.key, accountInfo, filterName, withIdentity) : true, + [accountInfo, api, filterName, info, withIdentity] + ); + + const slashes = useMemo( + () => (allSlashes || []) + .map(([era, all]) => ({ era, slashes: all.filter(({ validator }) => validator.eq(info.accountId)) })) + .filter(({ slashes }) => slashes.length), + [allSlashes, info] + ); const _onQueryStats = useCallback( (): void => { diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index ed6a2b004e..0b15fd31cc 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -34,6 +34,11 @@ interface SortState { sortFromMax: boolean; } +const CLASSES: Record = { + rankBondOther: 'ui--media-1600', + rankNumNominators: 'ui--media-1200' +}; + function sort (sortBy: TargetSortBy, sortFromMax: boolean, validators: ValidatorInfo[]): number[] { return [...Array(validators.length).keys()] .sort((a, b) => @@ -48,34 +53,40 @@ function sort (sortBy: TargetSortBy, sortFromMax: boolean, validators: Validator ); } +function extractNominees (ownNominators: StakerState[] = []): string[] { + const myNominees: string[] = []; + + ownNominators.forEach(({ nominating = [] }: StakerState): void => { + nominating.forEach((nominee: string): void => { + !myNominees.includes(nominee) && myNominees.push(nominee); + }); + }); + + return myNominees; +} + function Targets ({ className = '', isInElection, ownStashes, targets: { calcWith, lastReward, nominators, setCalcWith, totalStaked, validators }, toggleFavorite }: Props): React.ReactElement { const { t } = useTranslation(); const allSlashes = useAvailableSlashes(); const ownNominators = useOwnNominators(ownStashes); const [selected, setSelected] = useState([]); - const [sorted, setSorted] = useState(); - const [myNominees, setMyNominees] = useState([]); const [nameFilter, setNameFilter] = useState(''); const [withElected, setWithElected] = useState(false); const [withIdentity, setWithIdentity] = useState(false); const [{ sortBy, sortFromMax }, setSortBy] = useState({ sortBy: 'rankOverall', sortFromMax: true }); + const [sorted, setSorted] = useState(); + // We are using an effect here to get this async. Sorting will have a double-render, however it allows + // the page to immediately display (with loading), whereas useMemo would have a laggy interface + // (the same applies for changing the sort order, state here is more effective) useEffect((): void => { - validators && setSorted( - sort(sortBy, sortFromMax, validators) - ); + validators && setSorted(sort(sortBy, sortFromMax, validators)); }, [sortBy, sortFromMax, validators]); - useEffect((): void => { - ownNominators && setMyNominees( - ownNominators.reduce((myNominees: string[], nominator): string[] => - (nominator.nominating || []).reduce((myNominees: string[], nominee): string[] => { - !myNominees.includes(nominee) && myNominees.push(nominee); - - return myNominees; - }, myNominees), []) - ); - }, [ownNominators]); + const myNominees = useMemo( + () => extractNominees(ownNominators), + [ownNominators] + ); const _sort = useCallback( (newSortBy: TargetSortBy) => setSortBy(({ sortBy, sortFromMax }) => ({ @@ -121,24 +132,16 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { calcWit [t] ); - const classes = useMemo( - (): Record => ({ - rankBondOther: 'ui--media-1600', - rankNumNominators: 'ui--media-1200' - }), - [] - ); - const header = useMemo(() => [ [t('validators'), 'start', 3], ...['rankNumNominators', 'rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'].map((header) => [ <>{labels[header]}, - `${sorted ? `isClickable ${sortBy === header ? 'ui--highlight--border' : ''} number` : 'number'} ${classes[header] || ''}`, + `${sorted ? `isClickable ${sortBy === header ? 'ui--highlight--border' : ''} number` : 'number'} ${CLASSES[header] || ''}`, 1, (): void => _sort(header as 'rankComm') ]), [] - ], [_sort, classes, labels, sortBy, sorted, sortFromMax, t]); + ], [_sort, labels, sortBy, sorted, sortFromMax, t]); const filter = useMemo(() => ( sorted && ( diff --git a/packages/page-staking/src/Targets/useOwnNominators.ts b/packages/page-staking/src/Targets/useOwnNominators.ts index a297eb1759..22942e1bf5 100644 --- a/packages/page-staking/src/Targets/useOwnNominators.ts +++ b/packages/page-staking/src/Targets/useOwnNominators.ts @@ -4,16 +4,11 @@ import { StakerState } from '@polkadot/react-hooks/types'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; export default function useOwnNominators (ownStashes?: StakerState[]): StakerState[] | undefined { - const [state, setState] = useState(); - - useEffect((): void => { - ownStashes && setState( - ownStashes.filter(({ isOwnController, isStashValidating }) => isOwnController && !isStashValidating) - ); - }, [ownStashes]); - - return state; + return useMemo( + () => ownStashes ? ownStashes.filter(({ isOwnController, isStashValidating }) => isOwnController && !isStashValidating) : undefined, + [ownStashes] + ); } diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index 56b52b7f76..e236cc2da7 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -4,10 +4,9 @@ import { DeriveStakingOverview } from '@polkadot/api-derive/types'; import { AppProps as Props } from '@polkadot/react-components/types'; -import { StakerState } from '@polkadot/react-hooks/types'; import { ElectionStatus } from '@polkadot/types/interfaces'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { Route, Switch } from 'react-router'; import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; @@ -28,18 +27,11 @@ import { STORE_FAVS_BASE } from './constants'; import { useTranslation } from './translate'; import useSortedTargets from './useSortedTargets'; -interface Validators { - next?: string[]; - validators?: string[]; -} - function StakingApp ({ basePath, className = '' }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const { hasAccounts } = useAccounts(); const { pathname } = useLocation(); - const [{ next }, setValidators] = useState({}); - const [ownValidators, setOwnValidators] = useState([]); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); const allStashes = useStashIds(); const ownStashes = useOwnStashInfos(); @@ -49,10 +41,24 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement(api.query.staking?.eraElectionStatus, [], { transform: (status: ElectionStatus) => status.isOpen }); + const hasQueries = useMemo( () => hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra), [api, hasAccounts] ); + + const next = useMemo( + () => (allStashes && stakingOverview) + ? allStashes.filter((address) => !stakingOverview.validators.includes(address as any)) + : undefined, + [allStashes, stakingOverview] + ); + + const ownValidators = useMemo( + () => (ownStashes || []).filter(({ isStashValidating }) => isStashValidating), + [ownStashes] + ); + const items = useMemo(() => [ { isRoot: true, @@ -88,30 +94,16 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement('Validator stats') } ].filter((q): q is { name: string; text: string } => !!q), [api, slashes, t]); + const hiddenTabs = useMemo( - (): string[] => { - return !hasAccounts - ? ['actions', 'payouts', 'query'] - : !hasQueries - ? ['returns', 'query'] - : []; - }, + (): string[] => !hasAccounts + ? ['actions', 'payouts', 'query'] + : !hasQueries + ? ['returns', 'query'] + : [], [hasAccounts, hasQueries] ); - useEffect((): void => { - allStashes && stakingOverview && setValidators({ - next: allStashes.filter((address) => !stakingOverview.validators.includes(address as any)), - validators: stakingOverview.validators.map((a) => a.toString()) - }); - }, [allStashes, stakingOverview]); - - useEffect((): void => { - ownStashes && setOwnValidators( - ownStashes.filter(({ isStashValidating }) => isStashValidating) - ); - }, [ownStashes]); - return (