From b720cd1443ba1b85dc73027f3a197517e677fe4d Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 18 Mar 2020 13:51:33 +0100 Subject: [PATCH] Hooks reworks with mounted refs (#2417) --- .../src/Motions/useAvailableSlashes.ts | 12 +- .../page-council/src/useCouncilMembers.ts | 27 +- packages/page-council/src/useCounter.ts | 18 +- packages/page-democracy/src/useCounter.ts | 5 +- packages/page-society/src/useMembers.ts | 32 +-- .../src/Actions/Account/useInactives.ts | 2 +- .../page-staking/src/Overview/Address.tsx | 15 +- .../page-staking/src/Overview/CurrentList.tsx | 102 +++---- .../page-staking/src/Query/ValidatorPrev.tsx | 271 ------------------ .../page-staking/src/Query/useBlockCounts.tsx | 4 +- packages/page-staking/src/Targets/Summary.tsx | 14 +- packages/page-staking/src/Targets/index.tsx | 8 +- packages/page-staking/src/index.tsx | 50 ++-- packages/page-tech-comm/src/useCounter.ts | 14 +- packages/react-hooks/src/useCall.ts | 14 +- packages/react-hooks/src/useDebounce.ts | 4 +- packages/react-hooks/src/useMembers.ts | 4 +- packages/react-hooks/src/useOwnEraRewards.ts | 7 +- packages/react-hooks/src/useOwnStashes.ts | 7 +- 19 files changed, 170 insertions(+), 440 deletions(-) delete mode 100644 packages/page-staking/src/Query/ValidatorPrev.tsx diff --git a/packages/page-council/src/Motions/useAvailableSlashes.ts b/packages/page-council/src/Motions/useAvailableSlashes.ts index aba145fd50..3026b4b821 100644 --- a/packages/page-council/src/Motions/useAvailableSlashes.ts +++ b/packages/page-council/src/Motions/useAvailableSlashes.ts @@ -16,13 +16,13 @@ export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] { const { api } = useApi(); const indexes = useCall(api.derive.session?.indexes, []); const earliestSlash = useCall>(api.query.staking?.earliestUnappliedSlash, []); - const mounted = useIsMountedRef(); + const mountedRef = useIsMountedRef(); const [slashes, setSlashes] = useState<[BN, UnappliedSlash[]][]>([]); useEffect((): Unsub => { let unsub: Unsub | undefined; - if (mounted.current && indexes && earliestSlash?.isSome) { + if (mountedRef.current && indexes && earliestSlash?.isSome) { const from = earliestSlash.unwrap(); const range: BN[] = []; let start = new BN(from); @@ -34,13 +34,13 @@ export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] { if (range.length) { (async (): Promise => { - unsub = await api.query.staking.unappliedSlashes.multi>(range, (values): void => - setSlashes( + unsub = await api.query.staking.unappliedSlashes.multi>(range, (values): void => { + mountedRef.current && setSlashes( values .map((value, index): [BN, UnappliedSlash[]] => [from.addn(index), value]) .filter(([, slashes]): boolean => slashes.length !== 0) - ) - ); + ); + }); })(); } } diff --git a/packages/page-council/src/useCouncilMembers.ts b/packages/page-council/src/useCouncilMembers.ts index 4a0346caac..56e5d88294 100644 --- a/packages/page-council/src/useCouncilMembers.ts +++ b/packages/page-council/src/useCouncilMembers.ts @@ -4,30 +4,29 @@ import { AccountId, Balance } from '@polkadot/types/interfaces'; -import { useState, useEffect } from 'react'; import { useAccounts, useApi, useCall } from '@polkadot/react-hooks'; interface Result { isMember: boolean; members: string[]; } +function transform (allAccounts: string[]): (queryMembers: [AccountId, Balance][]) => Result { + return (queryMembers: [AccountId, Balance][]): Result => { + const members = queryMembers.map((accountId): string => accountId.toString()); + + return { + members, + isMember: members.some((accountId): boolean => allAccounts.includes(accountId)) + }; + }; +} export default function useCouncilMembers (): Result { const { api } = useApi(); const { allAccounts } = useAccounts(); - const queryMembers = useCall<[AccountId, Balance][]>(api.query.council.members, []); - const [members, setMembers] = useState({ isMember: false, members: [] }); - - useEffect((): void => { - if (allAccounts && queryMembers) { - const members = queryMembers.map((accountId): string => accountId.toString()); - - setMembers({ - members, - isMember: members.some((accountId): boolean => allAccounts.includes(accountId)) - }); - } - }, [allAccounts, queryMembers]); + const members = useCall(api.query.council.members, [], { + transform: transform(allAccounts) + }) || { isMember: false, members: [] }; return members; } diff --git a/packages/page-council/src/useCounter.ts b/packages/page-council/src/useCounter.ts index a3973a0d37..1a07f67dfb 100644 --- a/packages/page-council/src/useCounter.ts +++ b/packages/page-council/src/useCounter.ts @@ -4,21 +4,17 @@ import { DerivedCollectiveProposals } from '@polkadot/api-derive/types'; -import { useState, useEffect } from 'react'; import { useApi, useCall } from '@polkadot/react-hooks'; +function transform (motions: DerivedCollectiveProposals): number { + return motions.filter(({ votes }): boolean => !!votes).length; +} + export default function useCounter (): number { const { api, isApiReady } = useApi(); - const motions = useCall(isApiReady && api.derive.council?.proposals, []); - const [counter, setCounter] = useState(0); - - useEffect((): void => { - setCounter( - motions - ? motions.filter(({ votes }): boolean => !!votes).length - : 0 - ); - }, [motions]); + const counter = useCall(isApiReady && api.derive.council?.proposals, [], { + transform + }) || 0; return counter; } diff --git a/packages/page-democracy/src/useCounter.ts b/packages/page-democracy/src/useCounter.ts index 6ddad0c803..34f17c854b 100644 --- a/packages/page-democracy/src/useCounter.ts +++ b/packages/page-democracy/src/useCounter.ts @@ -3,16 +3,17 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { useState, useEffect } from 'react'; -import { useApi, useCall } from '@polkadot/react-hooks'; +import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks'; export default function useCounter (): number { const { api, isApiReady } = useApi(); + const mountedRef = useIsMountedRef(); const proposals = useCall(isApiReady && api.derive.democracy?.proposals, []); const referenda = useCall(isApiReady && api.derive.democracy?.referendums, []); const [counter, setCounter] = useState(0); useEffect((): void => { - setCounter( + mountedRef.current && setCounter( (proposals?.length || 0) + (referenda?.length || 0) ); diff --git a/packages/page-society/src/useMembers.ts b/packages/page-society/src/useMembers.ts index 48629ec9c2..5544773fd0 100644 --- a/packages/page-society/src/useMembers.ts +++ b/packages/page-society/src/useMembers.ts @@ -5,26 +5,26 @@ import { DeriveSocietyMember } from '@polkadot/api-derive/types'; import { OwnMembers } from './types'; -import { useEffect, useState } from 'react'; import { useAccounts, useApi, useCall } from '@polkadot/react-hooks'; +function transform (allAccounts: string[]): (members: DeriveSocietyMember[]) => OwnMembers { + return (members: DeriveSocietyMember[]): OwnMembers => { + const allMembers = members + .filter((member): boolean => !member.isSuspended) + .map((member): string => member.accountId.toString()); + const ownMembers = allMembers + .filter((address): boolean => allAccounts.includes(address)); + + return { allMembers, isMember: ownMembers.length !== 0, ownMembers }; + }; +} + export default function useMembers (): OwnMembers { const { api } = useApi(); const { allAccounts } = useAccounts(); - const members = useCall(api.derive.society?.members, []); - const [ownState, setOwnState] = useState({ allMembers: [], isMember: false, ownMembers: [] }); + const state = useCall(api.derive.society?.members, [], { + transform: transform(allAccounts) + }) || { allMembers: [], isMember: false, ownMembers: [] }; - useEffect((): void => { - if (allAccounts && members) { - const allMembers = members - .filter((member): boolean => !member.isSuspended) - .map((member): string => member.accountId.toString()); - const ownMembers = allMembers - .filter((address): boolean => allAccounts.includes(address)); - - setOwnState({ allMembers, isMember: ownMembers.length !== 0, ownMembers }); - } - }, [allAccounts, members]); - - return ownState; + return state; } diff --git a/packages/page-staking/src/Actions/Account/useInactives.ts b/packages/page-staking/src/Actions/Account/useInactives.ts index c5fa3348f8..5e9e8b572c 100644 --- a/packages/page-staking/src/Actions/Account/useInactives.ts +++ b/packages/page-staking/src/Actions/Account/useInactives.ts @@ -32,7 +32,7 @@ export default function useInactives (stashId: string, nominees?: string[]): str } }); - setInactives(inactives); + mountedRef.current && setInactives(inactives); } ) .then((_unsub): void => { diff --git a/packages/page-staking/src/Overview/Address.tsx b/packages/page-staking/src/Overview/Address.tsx index bb575f145a..cf922f85ab 100644 --- a/packages/page-staking/src/Overview/Address.tsx +++ b/packages/page-staking/src/Overview/Address.tsx @@ -22,15 +22,14 @@ interface Props { defaultName: string; filterName: string; hasQueries: boolean; - heartbeat?: DerivedHeartbeatAuthor; + heartbeat?: false | DerivedHeartbeatAuthor; isAuthor?: boolean; isElected: boolean; isFavorite: boolean; isMain?: boolean; lastBlock?: string; - myAccounts: string[]; - points?: RewardPoint; - setNominators?: (nominators: string[]) => void; + points?: false | RewardPoint; + setNominators?: false | ((nominators: string[]) => void); toggleFavorite: (accountId: string) => void; withNominations?: boolean; } @@ -45,7 +44,7 @@ interface StakingState { stakeOwn?: BN; } -function expandInfo ({ controllerId, exposure, nextSessionIds, validatorPrefs }: DerivedStakingQuery, myAccounts: string[], withNominations = true): StakingState { +function expandInfo ({ controllerId, exposure, nextSessionIds, validatorPrefs }: DerivedStakingQuery, withNominations = true): StakingState { const nominators = withNominations && exposure ? exposure.others.map(({ who, value }): [AccountId, Balance] => [who, value.unwrap()]) : []; @@ -98,7 +97,7 @@ function checkVisibility (api: ApiPromise, address: string, filterName: string, return isVisible; } -function Address ({ address, className, filterName, hasQueries, heartbeat, isAuthor, isElected, isFavorite, isMain, lastBlock, myAccounts, points, setNominators, toggleFavorite, withNominations }: Props): React.ReactElement | null { +function Address ({ address, className, filterName, hasQueries, heartbeat, isAuthor, isElected, isFavorite, isMain, lastBlock, points, setNominators, toggleFavorite, withNominations }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const info = useCall(api.derive.accounts.info as any, [address]); @@ -109,7 +108,7 @@ function Address ({ address, className, filterName, hasQueries, heartbeat, isAut useEffect((): void => { if (stakingInfo) { - const info = expandInfo(stakingInfo, myAccounts, withNominations); + const info = expandInfo(stakingInfo, withNominations); setNominators && setNominators(info.nominators.map(([who]): string => who.toString())); setStakingState(info); @@ -220,7 +219,7 @@ function Address ({ address, className, filterName, hasQueries, heartbeat, isAut )} - {points?.gtn(0) && ( + {points && points.gtn(0) && ( <>{formatNumber(points)} )} diff --git a/packages/page-staking/src/Overview/CurrentList.tsx b/packages/page-staking/src/Overview/CurrentList.tsx index 89bfcaad62..35afda6d4d 100644 --- a/packages/page-staking/src/Overview/CurrentList.tsx +++ b/packages/page-staking/src/Overview/CurrentList.tsx @@ -8,7 +8,7 @@ import { AddressDetails } from './types'; import React, { useEffect, useReducer, useState } from 'react'; import { Input, Spinner, Table } from '@polkadot/react-components'; -import { useAccounts, useFavorites } from '@polkadot/react-hooks'; +import { useFavorites } from '@polkadot/react-hooks'; import { STORE_FAVS_BASE } from '../constants'; import { useTranslation } from '../translate'; @@ -28,10 +28,10 @@ interface Props { type AccountExtend = [string, boolean, boolean]; -function sortByFav ([,, isFavA]: AccountExtend, [,, isFavB]: AccountExtend): number { - return isFavA === isFavB - ? 0 - : (isFavA ? -1 : 1); +interface Filtered { + elected: AccountExtend[]; + validators: AccountExtend[]; + waiting?: AccountExtend[]; } function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[]): AccountExtend[] { @@ -42,7 +42,11 @@ function filterAccounts (accounts: string[] = [], elected: string[], favorites: elected.includes(accountId), favorites.includes(accountId) ]) - .sort(sortByFav); + .sort(([,, isFavA]: AccountExtend, [,, isFavB]: AccountExtend): number => + isFavA === isFavB + ? 0 + : (isFavA ? -1 : 1) + ); } function accountsToString (accounts: AccountId[]): string[] { @@ -64,48 +68,53 @@ function reduceDetails (state: Record, _details: Address }, { ...state }); } +function getDetails (stakingOverview: DerivedStakingOverview, validators: AccountExtend[]): AddressDetails[] { + const allPoints = [...stakingOverview.eraPoints.individual.entries()]; + + return validators.map(([address]): AddressDetails => { + const points = allPoints.find(([accountId]): boolean => accountId.eq(address)); + + return { + address, + points: points + ? points[1] + : undefined + }; + }); +} + +function getFiltered (stakingOverview: DerivedStakingOverview, favorites: string[], next?: string[]): Filtered { + const allElected = accountsToString(stakingOverview.nextElected); + const validatorIds = accountsToString(stakingOverview.validators); + const validators = filterAccounts(validatorIds, allElected, favorites, []); + const elected = filterAccounts(allElected, allElected, favorites, validatorIds); + const waiting = filterAccounts(next, [], favorites, allElected); + + return { + elected, + validators, + waiting + }; +} + function CurrentList ({ authorsMap, hasQueries, isIntentions, isVisible, lastAuthors, next, recentlyOnline, setNominators, stakingOverview }: Props): React.ReactElement | null { const { t } = useTranslation(); - const { allAccounts } = useAccounts(); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); - const [{ allElected, elected, validators, waiting }, setFiltered] = useState<{ allElected: string[]; elected: AccountExtend[]; validators: AccountExtend[]; waiting?: AccountExtend[] }>({ allElected: [], elected: [], validators: [] }); + const [{ elected, validators, waiting }, setFiltered] = useState({ elected: [], validators: [] }); const [nameFilter, setNameFilter] = useState(''); const [addressDetails, dispatchDetails] = useReducer(reduceDetails, {}); useEffect((): void => { - if (isVisible && stakingOverview) { - const allElected = accountsToString(stakingOverview.nextElected); - const _validators = accountsToString(stakingOverview.validators); - const validators = filterAccounts(_validators, allElected, favorites, []); - const elected = filterAccounts(allElected, allElected, favorites, _validators); - - setFiltered({ - allElected, - elected, - validators, - waiting: filterAccounts(next, [], favorites, allElected) - }); - } - }, [favorites, isVisible, next, stakingOverview?.nextElected, stakingOverview?.validators]); + isVisible && stakingOverview && setFiltered( + getFiltered(stakingOverview, favorites, next) + ); + }, [favorites, isVisible, next, stakingOverview]); useEffect((): void => { - if (stakingOverview?.eraPoints) { - const allPoints = stakingOverview.eraPoints - ? [...stakingOverview.eraPoints.individual.entries()] - : []; - - dispatchDetails(validators.map(([address]): AddressDetails => { - const points = allPoints.find(([accountId]): boolean => accountId.eq(address)); - - return { - address, - points: points - ? points[1] - : undefined - }; - })); - } - }, [stakingOverview?.eraPoints, allElected, validators]); + stakingOverview && dispatchDetails( + getDetails(stakingOverview, validators) + ); + }, [stakingOverview, validators]); const _renderRows = (addresses: AccountExtend[], defaultName: string, isMain: boolean): React.ReactNode => addresses.map(([address, isElected, isFavorite]): React.ReactNode => ( @@ -114,24 +123,15 @@ function CurrentList ({ authorsMap, hasQueries, isIntentions, isVisible, lastAut defaultName={defaultName} filterName={nameFilter} hasQueries={hasQueries} - heartbeat={ - isMain && recentlyOnline - ? recentlyOnline[address] - : undefined - } + heartbeat={isMain && recentlyOnline && recentlyOnline[address]} isAuthor={lastAuthors && lastAuthors.includes(address)} isElected={isElected} isFavorite={isFavorite} isMain={isMain} key={address} lastBlock={authorsMap[address]} - myAccounts={allAccounts} - points={ - isMain - ? addressDetails[address] && addressDetails[address].points - : undefined - } - setNominators={isIntentions ? undefined : setNominators} + points={isMain && addressDetails[address] && addressDetails[address].points} + setNominators={isIntentions && setNominators} toggleFavorite={toggleFavorite} /> )); diff --git a/packages/page-staking/src/Query/ValidatorPrev.tsx b/packages/page-staking/src/Query/ValidatorPrev.tsx deleted file mode 100644 index 428b644c70..0000000000 --- a/packages/page-staking/src/Query/ValidatorPrev.tsx +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2017-2020 @polkadot/app-staking authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { Balance, Hash, Exposure } from '@polkadot/types/interfaces'; -import { SessionRewards, Slash } from '../types'; - -import BN from 'bn.js'; -import React, { useEffect, useState } from 'react'; -import { Chart, Columar, Column } from '@polkadot/react-components'; -import { toShortAddress } from '@polkadot/react-components/util'; -import { getHistoric } from '@polkadot/react-api/util'; -import { useApi } from '@polkadot/react-hooks'; -import { formatBalance, formatNumber } from '@polkadot/util'; - -import { useTranslation } from '../translate'; -import useBlockCounts from './useBlockCounts'; - -interface Props { - className?: string; - sessionRewards: SessionRewards[]; - validatorId: string; -} - -type LineDataEntry = (BN | number)[]; - -type LineData = LineDataEntry[]; - -interface SplitEntry { - colors: string[]; - label: string; - tooltip: string; - value: number; -} - -type SplitData = SplitEntry[]; - -const COLORS_MINE = ['#ff8c00']; -const COLORS_OTHER = ['#acacac']; -const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac']; -const COLORS_BLOCKS = [undefined, '#acacac']; - -function balanceToNumber (amount: BN, divisor: BN): number { - return amount.muln(1000).div(divisor).toNumber() / 1000; -} - -function extractStake (values: [Hash, Exposure][], divisor: BN): LineData { - return [ - values.map(([, { total }]): number => - balanceToNumber(total.unwrap(), divisor)) - // exposures.map(({ own }): number => - // balanceToNumber(own.unwrap(), divisor)), - // exposures.map(({ others }): number => - // balanceToNumber( - // others.reduce((total, { value }): number => - // total.add(value.unwrap()), new BN(0) - // ), divisor)) - ]; -} - -function extractSplit (values: [Hash, Exposure][], validatorId: string): SplitData | null { - if (!values.length) { - return null; - } - - const last = values[values.length - 1][1]; - const total = last.total.unwrap(); - - if (total.eqn(0)) { - return null; - } - - const currency = formatBalance.getDefaults().unit; - - return [{ accountId: validatorId, isOwn: true, value: last.own.unwrap() }] - .concat(last.others.map(({ who, value }): { accountId: string; isOwn: boolean; value: Balance } => ({ - accountId: who.toString(), isOwn: false, value: value.unwrap() - }))) - .sort((a, b): number => b.value.cmp(a.value)) - .map(({ accountId, isOwn, value }): SplitEntry => { - const label = toShortAddress(accountId); - const percentage = value.muln(10000).div(total).toNumber() / 100; - const tooltip = `${formatBalance(value, { forceUnit: '-', withSi: false })} ${currency} (${percentage.toFixed(2)}%)`; - - return { - colors: isOwn ? COLORS_MINE : COLORS_OTHER, - label, - tooltip, - value: percentage - }; - }); -} - -function extractEraSlash (validatorId: string, slashes: Slash[]): BN { - return slashes.reduce((total: BN, { accountId, amount }): BN => { - return accountId.eq(validatorId) - ? total.sub(amount) - : total; - }, new BN(0)); -} - -export default function Validator ({ className, sessionRewards, validatorId }: Props): React.ReactElement { - const { t } = useTranslation(); - const { api } = useApi(); - const blockCounts = useBlockCounts(validatorId, sessionRewards); - const [blocksLabels, setBlocksLabels] = useState([]); - const [blocksChart, setBlocksChart] = useState(null); - const [{ currency, divisor }] = useState<{ currency: string; divisor: BN }>({ - currency: formatBalance.getDefaults().unit, - divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0')) - }); - const [{ rewardsChart, rewardsLabels }, setRewardsInfo] = useState<{ rewardsChart: LineData | null; rewardsLabels: string[] }>({ rewardsChart: null, rewardsLabels: [] }); - const [{ splitChart, splitMax }, setSplitInfo] = useState<{ splitChart: SplitData | null; splitMax: number }>({ splitChart: null, splitMax: 100 }); - const [{ stakeChart, stakeLabels }, setStakeInfo] = useState<{ stakeChart: LineData | null; stakeLabels: string[]}>({ stakeChart: null, stakeLabels: [] }); - - useEffect((): void => { - if (!splitChart) { - const hashes = sessionRewards.map(({ blockHash }): Hash => blockHash); - const stakeLabels = sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex)); - - api.isReady.then(async (): Promise => { - const values = await getHistoric(api.query.staking.stakers.at, [validatorId], hashes); - const stakeChart = extractStake(values, divisor); - const splitChart = extractSplit(values, validatorId); - const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100; - - setStakeInfo({ stakeChart, stakeLabels }); - setSplitInfo({ splitChart, splitMax }); - }); - } - }, [sessionRewards, splitChart]); - - useEffect((): void => { - if (blockCounts) { - const rewardsLabels: string[] = []; - const rewardsChart: LineData = [[], [], []]; - let total = new BN(0); - let lastRewardIndex = 0; - let rewardCount = 0; - - // we only work from the second position, the first deemed incomplete - sessionRewards.forEach(({ blockNumber, reward, sessionIndex, slashes }, index): void => { - // we are trying to find the first index where rewards are allocated, this is our start - // since we only want complete eras, this means we drop some at the start - if (lastRewardIndex === 0) { - if (index && reward.gtn(0)) { - lastRewardIndex = index; - } - - return; - } - - // slash is extracted from the available slashes - const neg = extractEraSlash(validatorId, slashes); - - // start of a new session, use the counts for the previous - const totalBlocks = blockCounts - .filter((count, countIndex): boolean => - !!count && countIndex >= lastRewardIndex && countIndex < index) - .reduce((total, count): BN => total.add(count), new BN(0)); - - // calculate the rewards based on our total share - const pos = reward - .mul(totalBlocks) - .div(blockNumber.sub(sessionRewards[lastRewardIndex].blockNumber)); - - // add this to the total - total = total.add(neg).add(pos); - rewardCount++; - - // if we have a reward here, set the reward index for the next iteration - if (reward.gtn(0)) { - lastRewardIndex = index; - } - - // this shows the start of the new era, however rewards are for previous - rewardsLabels.push(formatNumber(sessionIndex.subn(1))); - - // calculate and format to 3 decimals - rewardsChart[0].push(balanceToNumber(neg, divisor)); - rewardsChart[1].push(balanceToNumber(pos, divisor)); - rewardsChart[2].push(balanceToNumber(total.divn(rewardCount), divisor)); - }); - - setRewardsInfo({ rewardsChart, rewardsLabels }); - } - }, [blockCounts, sessionRewards, validatorId]); - - useEffect((): void => { - setBlocksLabels( - sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex)) - ); - }, [sessionRewards]); - - useEffect((): void => { - if (blockCounts) { - const avgSet: number[] = []; - const idxSet: BN[] = []; - - blockCounts.reduce((total: BN, value, index): BN => { - total = total.add(value); - - avgSet.push(total.muln(100).divn(index + 1).toNumber() / 100); - idxSet.push(value); - - return total; - }, new BN(0)); - - setBlocksChart([idxSet, avgSet]); - } - }, [blockCounts]); - - return ( - - - {(rewardsChart || blocksChart) && ( - <> - {blocksChart && ( -
-

{t('blocks produced')}

- -
- )} - {rewardsChart && ( -
-

{t('rewards & slashes')}

- -
- )} - - )} -
- - {(stakeChart || splitChart) && ( - <> - {stakeChart && stakeChart[0].length !== 0 && ( -
-

{t('elected stake')}

- -
- )} - {splitChart && ( -
-

{t('staker percentages')}

- -
- )} - - )} -
-
- ); -} diff --git a/packages/page-staking/src/Query/useBlockCounts.tsx b/packages/page-staking/src/Query/useBlockCounts.tsx index ef9d2f2f37..8e9b131fd5 100644 --- a/packages/page-staking/src/Query/useBlockCounts.tsx +++ b/packages/page-staking/src/Query/useBlockCounts.tsx @@ -11,7 +11,7 @@ import { u32 } from '@polkadot/types'; export default function useBlockCounts (accountId: string, sessionRewards: SessionRewards[]): u32[] { const { api } = useApi(); - const mounted = useIsMountedRef(); + const mountedRef = useIsMountedRef(); const indexes = useCall(api.derive.session?.indexes, []); const current = useCall(api.query.imOnline?.authoredBlocks, [indexes?.currentIndex, accountId]); const [counts, setCounts] = useState([]); @@ -27,7 +27,7 @@ export default function useBlockCounts (accountId: string, sessionRewards: Sessi api.query.imOnline.authoredBlocks.at(parentHash, sessionIndex.subn(1), accountId) as Promise )) .then((historic): void => { - mounted.current && setHistoric(historic); + mountedRef.current && setHistoric(historic); }); } } diff --git a/packages/page-staking/src/Targets/Summary.tsx b/packages/page-staking/src/Targets/Summary.tsx index 920e0a94ad..28bc2f94e8 100644 --- a/packages/page-staking/src/Targets/Summary.tsx +++ b/packages/page-staking/src/Targets/Summary.tsx @@ -26,14 +26,14 @@ interface StakeInfo { function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const totalInsurance = useCall(api.query.balances.totalIssuance, []); + const totalIssuance = useCall(api.query.balances.totalIssuance, []); const [{ percentage }, setStakeInfo] = useState({}); useEffect((): void => { - totalInsurance && totalStaked?.gtn(0) && setStakeInfo({ - percentage: `${(totalStaked.muln(10000).div(totalInsurance).toNumber() / 100).toFixed(2)}%` + totalIssuance && totalStaked?.gtn(0) && setStakeInfo({ + percentage: `${(totalStaked.muln(10000).div(totalIssuance).toNumber() / 100).toFixed(2)}%` }); - }, [totalInsurance, totalStaked]); + }, [totalIssuance, totalStaked]); return ( @@ -46,13 +46,13 @@ function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Pro /> )} - {totalStaked && totalInsurance && ( + {totalStaked && totalIssuance && ( / )} - {totalStaked && ( + {totalIssuance && ( diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index 5b8e91abbc..1c423d4327 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -19,7 +19,7 @@ import { useTranslation } from '../translate'; import Summary from './Summary'; import Validator from './Validator'; -const PERBILL = new BN(1000000000); +const PERBILL = new BN(1_000_000_000); interface Props { className?: string; @@ -167,7 +167,7 @@ function Targets ({ className }: Props): React.ReactElement { transform: (optBalance: Option) => optBalance.unwrapOrDefault() }); - const [_amount, setAmount] = useState(new BN(1000)); + const [_amount, setAmount] = useState(new BN(1_000)); const electedInfo = useCall(api.derive.staking.electedInfo, []); const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE); const [{ nominators, validators, totalStaked }, setWorkable] = useState({ nominators: [], validators: [] }); @@ -190,9 +190,7 @@ function Targets ({ className }: Props): React.ReactElement { ) .sort((a, b): number => a.isFavorite === b.isFavorite ? 0 - : a.isFavorite - ? -1 - : 1 + : (a.isFavorite ? -1 : 1) ) }); }, diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index 62e20ef931..55cd609f05 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -42,23 +42,17 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement const recentlyOnline = useCall(api.derive.imOnline?.receivedHeartbeats, []); const stakingOverview = useCall(api.derive.staking.overview, []); const [nominators, dispatchNominators] = useReducer(reduceNominators, [] as string[]); - const hasQueries = useMemo((): boolean => { - return hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra); - }, [api, hasAccounts]); + const hasQueries = useMemo( + (): boolean => + hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra), + [api, hasAccounts] + ); const items = useMemo(() => [ { isRoot: true, name: 'overview', text: t('Staking overview') }, - { - name: 'waiting', - text: t('Waiting') - }, - { - name: 'returns', - text: t('Returns') - }, { name: 'actions', text: t('Account actions{{count}}', { @@ -69,29 +63,37 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement } }) }, + { + name: 'waiting', + text: t('Waiting') + }, + { + name: 'returns', + text: t('Returns') + }, { hasParams: true, name: 'query', text: t('Validator stats') } ], [rewardCount, t]); - const hiddenTabs = useMemo((): string[] => { - const result = next ? [] : ['waiting']; - - if (!hasAccounts) { - result.push('actions', 'query'); - } else if (!hasQueries) { - result.push('returns', 'query'); - } - - return result; - }, [hasAccounts, hasQueries, next]); + const hiddenTabs = useMemo( + (): string[] => + !hasAccounts + ? ['actions', 'query'] + : !hasQueries + ? ['returns', 'query'] + : [], + [hasAccounts, hasQueries] + ); useEffect((): void => { allStashes && stakingOverview && setNext( - allStashes.filter((address): boolean => !stakingOverview.validators.includes(address as any)) + allStashes.filter((address): boolean => + !stakingOverview.validators.includes(address as any) + ) ); - }, [allStashes, stakingOverview?.validators]); + }, [allStashes, stakingOverview]); return (
diff --git a/packages/page-tech-comm/src/useCounter.ts b/packages/page-tech-comm/src/useCounter.ts index 74ca2f7e33..15fb8e4d4d 100644 --- a/packages/page-tech-comm/src/useCounter.ts +++ b/packages/page-tech-comm/src/useCounter.ts @@ -4,17 +4,17 @@ import { Hash } from '@polkadot/types/interfaces'; -import { useState, useEffect } from 'react'; import { useApi, useCall } from '@polkadot/react-hooks'; +function transform (proposals: Hash[]): number { + return proposals.length; +} + export default function useCounter (): number { const { api, isApiReady } = useApi(); - const proposals = useCall(isApiReady && api.query.technicalCommittee?.proposals, []); - const [counter, setCounter] = useState(0); - - useEffect((): void => { - setCounter(proposals?.length || 0); - }, [proposals]); + const counter = useCall(isApiReady && api.query.technicalCommittee?.proposals, [], { + transform + }) || 0; return counter; } diff --git a/packages/react-hooks/src/useCall.ts b/packages/react-hooks/src/useCall.ts index 9aff2cac8c..22d9322c4f 100644 --- a/packages/react-hooks/src/useCall.ts +++ b/packages/react-hooks/src/useCall.ts @@ -65,13 +65,13 @@ function unsubscribe (tracker: TrackerRef): void { } // subscribe, trying to play nice with the browser threads -function subscribe (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity, withParams }: CallOptions): void { +function subscribe (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity, withParams }: CallOptions): void { const validParams = params.filter((p): boolean => !isUndefined(p)); unsubscribe(tracker); setTimeout((): void => { - if (mounted.current) { + if (mountedRef.current) { if (fn && (!fn.meta || !fn.meta.type?.isDoubleMap || validParams.length === 2)) { // swap to acive mode and reset our count tracker.current.isActive = true; @@ -82,10 +82,10 @@ function subscribe (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn | tracker.current.subscriber = fn(...params, (value: any): void => { // when we don't have an active sub, or single-shot, ignore (we use the isActive flag here // since .subscriber may not be set on immeditae callback) - if (mounted.current && tracker.current.isActive && (!isSingle || !tracker.current.count)) { + if (mountedRef.current && tracker.current.isActive && (!isSingle || !tracker.current.count)) { tracker.current.count++; - mounted.current && tracker.current.isActive && setValue( + mountedRef.current && tracker.current.isActive && setValue( withParams ? [params, transform(value)] as any : transform(value) @@ -104,7 +104,7 @@ function subscribe (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn | // - has a callback to set the value // FIXME The typings here need some serious TLC export default function useCall (fn: TrackFn | undefined | null | false, params: CallParams = [], options: CallOptions = {}): T | undefined { - const mounted = useIsMountedRef(); + const mountedRef = useIsMountedRef(); const tracker = useRef({ isActive: false, count: 0, serialized: null, subscriber: null }); const [value, setValue] = useState(options.defaultValue); @@ -116,13 +116,13 @@ export default function useCall (fn: TrackFn | undefined | null | false, par // on changes, re-subscribe useEffect((): void => { // check if we have a function & that we are mounted - if (mounted.current && fn) { + if (mountedRef.current && fn) { const [serialized, mappedParams] = extractParams(fn, params, options.paramMap || transformIdentity); if (mappedParams && serialized !== tracker.current.serialized) { tracker.current.serialized = serialized; - subscribe(mounted, tracker, fn, mappedParams, setValue, options); + subscribe(mountedRef, tracker, fn, mappedParams, setValue, options); } } }, [fn, params]); diff --git a/packages/react-hooks/src/useDebounce.ts b/packages/react-hooks/src/useDebounce.ts index ccf93590f8..13a05931d6 100644 --- a/packages/react-hooks/src/useDebounce.ts +++ b/packages/react-hooks/src/useDebounce.ts @@ -8,12 +8,12 @@ import useIsMountedRef from './useIsMountedRef'; // Debounces inputs export default function useDebounce (value: T, delay = 250): T { - const mounted = useIsMountedRef(); + const mountedRef = useIsMountedRef(); const [debouncedValue, setDebouncedValue] = useState(value); useEffect((): () => void => { const handler = setTimeout(() => { - mounted.current && setDebouncedValue(value); + mountedRef.current && setDebouncedValue(value); }, delay); // each time it renders, it clears diff --git a/packages/react-hooks/src/useMembers.ts b/packages/react-hooks/src/useMembers.ts index 9de417699d..acf0f827d9 100644 --- a/packages/react-hooks/src/useMembers.ts +++ b/packages/react-hooks/src/useMembers.ts @@ -21,7 +21,7 @@ function getResult (allAccounts: string[], members: string[]): Result { export default function useMembers (collective: 'council' | 'technicalCommittee'): Result { const { api } = useApi(); const { allAccounts } = useAccounts(); - const { isMember, members } = ( + const state = ( collective === 'council' ? useCall(api.query.electionsPhragmen?.members || api.query.elections.members, [], { transform: (accounts: [AccountId, Balance][]): Result => @@ -33,5 +33,5 @@ export default function useMembers (collective: 'council' | 'technicalCommittee' }) ) || { isMember: false, members: [] }; - return { isMember, members }; + return state; } diff --git a/packages/react-hooks/src/useOwnEraRewards.ts b/packages/react-hooks/src/useOwnEraRewards.ts index aa7221d271..7f8c009c54 100644 --- a/packages/react-hooks/src/useOwnEraRewards.ts +++ b/packages/react-hooks/src/useOwnEraRewards.ts @@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'; import BN from 'bn.js'; import useApi from './useApi'; import useCall from './useCall'; +import useIsMountedRef from './useIsMountedRef'; import { useOwnStashIds } from './useOwnStashes'; interface OwnRewards { @@ -19,13 +20,14 @@ interface OwnRewards { function useNextPayouts (onlyLatest?: boolean): [string, BN][] | undefined { const { api, isApiReady } = useApi(); + const mountedRef = useIsMountedRef(); const stashIds = useOwnStashIds(); const allInfo = useCall(isApiReady && stashIds && api.derive.staking?.queryMulti, stashIds); const indexes = useCall(isApiReady && api.derive.session?.indexes, []); const [nextPayouts, setNextPayouts] = useState<[string, BN][] | undefined>(); useEffect((): void => { - if (stashIds && allInfo && indexes) { + if (mountedRef.current && stashIds && allInfo && indexes) { const prevEra = indexes.activeEra.subn(1); const lastPayouts = allInfo .map(({ stakingLedger }, index) => [stashIds[index], stakingLedger?.lastReward?.unwrapOr(new BN(-1)).addn(1)]) @@ -64,12 +66,13 @@ function getRewards ([thesePayouts, theseRewards]: [[string, EraIndex][], Derive export default function useOwnEraRewards (onlyLatest?: boolean): OwnRewards { const { api } = useApi(); + const mountedRef = useIsMountedRef(); const nextPayouts = useNextPayouts(onlyLatest); const available = useCall<[[string, EraIndex][], DeriveStakerReward[][]]>(nextPayouts && api.derive.staking?.stakerRewardsMulti as any, nextPayouts, { withParams: true }); const [state, setState] = useState({ rewardCount: 0 }); useEffect((): void => { - available && nextPayouts && setState( + mountedRef.current && available && nextPayouts && setState( getRewards(available, nextPayouts) ); }, [available, nextPayouts]); diff --git a/packages/react-hooks/src/useOwnStashes.ts b/packages/react-hooks/src/useOwnStashes.ts index b1223842a5..7e35d77f70 100644 --- a/packages/react-hooks/src/useOwnStashes.ts +++ b/packages/react-hooks/src/useOwnStashes.ts @@ -10,6 +10,7 @@ import { Option } from '@polkadot/types'; import useAccounts from './useAccounts'; import useApi from './useApi'; import useCall from './useCall'; +import useIsMountedRef from './useIsMountedRef'; type IsInKeyring = boolean; @@ -33,13 +34,14 @@ function getStashes (allAccounts: string[], ownBonded: Option[], ownL export default function useOwnStashes (): [string, IsInKeyring][] | undefined { const { allAccounts } = useAccounts(); + const mountedRef = useIsMountedRef(); const { api } = useApi(); const ownBonded = useCall[]>(api.query.staking?.bonded.multi as any, [allAccounts]); const ownLedger = useCall[]>(api.query.staking?.ledger.multi as any, [allAccounts]); const [state, setState] = useState<[string, IsInKeyring][] | undefined>(); useEffect((): void => { - allAccounts && ownBonded && ownLedger && setState( + mountedRef.current && allAccounts && ownBonded && ownLedger && setState( getStashes(allAccounts, ownBonded, ownLedger) ); }, [allAccounts, ownBonded, ownLedger]); @@ -48,11 +50,12 @@ export default function useOwnStashes (): [string, IsInKeyring][] | undefined { } export function useOwnStashIds (): string[] | undefined { + const mountedRef = useIsMountedRef(); const ownStashes = useOwnStashes(); const [stashIds, setStashIds] = useState(); useEffect((): void => { - ownStashes && setStashIds( + mountedRef.current && ownStashes && setStashIds( ownStashes.map(([stashId]) => stashId) ); }, [ownStashes]);