Hooks reworks with mounted refs (#2417)

This commit is contained in:
Jaco Greeff 2020-03-18 13:51:33 +01:00 committed by GitHub
parent e8f6da1041
commit b720cd1443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 170 additions and 440 deletions

View File

@ -16,13 +16,13 @@ export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] {
const { api } = useApi();
const indexes = useCall<DeriveSessionIndexes>(api.derive.session?.indexes, []);
const earliestSlash = useCall<Option<EraIndex>>(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<void> => {
unsub = await api.query.staking.unappliedSlashes.multi<Vec<UnappliedSlash>>(range, (values): void =>
setSlashes(
unsub = await api.query.staking.unappliedSlashes.multi<Vec<UnappliedSlash>>(range, (values): void => {
mountedRef.current && setSlashes(
values
.map((value, index): [BN, UnappliedSlash[]] => [from.addn(index), value])
.filter(([, slashes]): boolean => slashes.length !== 0)
)
);
);
});
})();
}
}

View File

@ -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<Result>({ 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<Result>(api.query.council.members, [], {
transform: transform(allAccounts)
}) || { isMember: false, members: [] };
return members;
}

View File

@ -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<DerivedCollectiveProposals>(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<number>(isApiReady && api.derive.council?.proposals, [], {
transform
}) || 0;
return counter;
}

View File

@ -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<any[]>(isApiReady && api.derive.democracy?.proposals, []);
const referenda = useCall<any[]>(isApiReady && api.derive.democracy?.referendums, []);
const [counter, setCounter] = useState(0);
useEffect((): void => {
setCounter(
mountedRef.current && setCounter(
(proposals?.length || 0) +
(referenda?.length || 0)
);

View File

@ -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<DeriveSocietyMember[]>(api.derive.society?.members, []);
const [ownState, setOwnState] = useState<OwnMembers>({ allMembers: [], isMember: false, ownMembers: [] });
const state = useCall<OwnMembers>(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;
}

View File

@ -32,7 +32,7 @@ export default function useInactives (stashId: string, nominees?: string[]): str
}
});
setInactives(inactives);
mountedRef.current && setInactives(inactives);
}
)
.then((_unsub): void => {

View File

@ -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<Props> | null {
function Address ({ address, className, filterName, hasQueries, heartbeat, isAuthor, isElected, isFavorite, isMain, lastBlock, points, setNominators, toggleFavorite, withNominations }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const info = useCall<DeriveAccountInfo>(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
)}
</td>
<td className='number'>
{points?.gtn(0) && (
{points && points.gtn(0) && (
<><label>{t('points')}</label>{formatNumber(points)}</>
)}
</td>

View File

@ -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<string, AddressDetails>, _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<Props> | 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<Filtered>({ elected: [], validators: [] });
const [nameFilter, setNameFilter] = useState<string>('');
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}
/>
));

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const blockCounts = useBlockCounts(validatorId, sessionRewards);
const [blocksLabels, setBlocksLabels] = useState<string[]>([]);
const [blocksChart, setBlocksChart] = useState<LineData | null>(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<void> => {
const values = await getHistoric<Exposure>(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 (
<Columar className={className}>
<Column emptyText={t('Loading block data')}>
{(rewardsChart || blocksChart) && (
<>
{blocksChart && (
<div className='staking--Chart'>
<h1>{t('blocks produced')}</h1>
<Chart.Line
colors={COLORS_BLOCKS}
labels={blocksLabels}
legends={[t('blocks'), t('average')]}
values={blocksChart}
/>
</div>
)}
{rewardsChart && (
<div className='staking--Chart'>
<h1>{t('rewards & slashes')}</h1>
<Chart.Line
colors={COLORS_REWARD}
labels={rewardsLabels}
legends={[t('{{currency}} slashed', { replace: { currency } }), t('{{currency}} rewards (est.)', { replace: { currency } }), t('{{currency}} average', { replace: { currency } })]}
values={rewardsChart}
/>
</div>
)}
</>
)}
</Column>
<Column emptyText={t('Loading staker data')}>
{(stakeChart || splitChart) && (
<>
{stakeChart && stakeChart[0].length !== 0 && (
<div className='staking--Chart'>
<h1>{t('elected stake')}</h1>
<Chart.Line
labels={stakeLabels}
legends={[t('{{currency}} total', { replace: { currency } }), t('{{currency}} own', { replace: { currency } }), t('{{currency}} other', { replace: { currency } })]}
values={stakeChart}
/>
</div>
)}
{splitChart && (
<div className='staking--Chart'>
<h1>{t('staker percentages')}</h1>
<Chart.HorizBar
aspectRatio={2}
max={splitMax}
values={splitChart}
/>
</div>
)}
</>
)}
</Column>
</Columar>
);
}

View File

@ -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<DeriveSessionIndexes>(api.derive.session?.indexes, []);
const current = useCall<u32>(api.query.imOnline?.authoredBlocks, [indexes?.currentIndex, accountId]);
const [counts, setCounts] = useState<u32[]>([]);
@ -27,7 +27,7 @@ export default function useBlockCounts (accountId: string, sessionRewards: Sessi
api.query.imOnline.authoredBlocks.at(parentHash, sessionIndex.subn(1), accountId) as Promise<u32>
))
.then((historic): void => {
mounted.current && setHistoric(historic);
mountedRef.current && setHistoric(historic);
});
}
}

View File

@ -26,14 +26,14 @@ interface StakeInfo {
function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const totalInsurance = useCall<Balance>(api.query.balances.totalIssuance, []);
const totalIssuance = useCall<Balance>(api.query.balances.totalIssuance, []);
const [{ percentage }, setStakeInfo] = useState<StakeInfo>({});
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 (
<SummaryBox>
@ -46,13 +46,13 @@ function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Pro
/>
</CardSummary>
)}
{totalStaked && totalInsurance && (
{totalStaked && totalIssuance && (
<CardSummary label=''>/</CardSummary>
)}
{totalStaked && (
{totalIssuance && (
<CardSummary label={t('total issuance')}>
<FormatBalance
value={totalInsurance}
value={totalIssuance}
withSi
/>
</CardSummary>

View File

@ -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<Props> {
transform: (optBalance: Option<Balance>) =>
optBalance.unwrapOrDefault()
});
const [_amount, setAmount] = useState<BN | undefined>(new BN(1000));
const [_amount, setAmount] = useState<BN | undefined>(new BN(1_000));
const electedInfo = useCall<DerivedStakingElected>(api.derive.staking.electedInfo, []);
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [{ nominators, validators, totalStaked }, setWorkable] = useState<AllInfo>({ nominators: [], validators: [] });
@ -190,9 +190,7 @@ function Targets ({ className }: Props): React.ReactElement<Props> {
)
.sort((a, b): number => a.isFavorite === b.isFavorite
? 0
: a.isFavorite
? -1
: 1
: (a.isFavorite ? -1 : 1)
)
});
},

View File

@ -42,23 +42,17 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
const recentlyOnline = useCall<DerivedHeartbeats>(api.derive.imOnline?.receivedHeartbeats, []);
const stakingOverview = useCall<DerivedStakingOverview>(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<Props>
}
})
},
{
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 (
<main className={`staking--App ${className}`}>

View File

@ -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<Hash[]>(isApiReady && api.query.technicalCommittee?.proposals, []);
const [counter, setCounter] = useState(0);
useEffect((): void => {
setCounter(proposals?.length || 0);
}, [proposals]);
const counter = useCall<number>(isApiReady && api.query.technicalCommittee?.proposals, [], {
transform
}) || 0;
return counter;
}

View File

@ -65,13 +65,13 @@ function unsubscribe (tracker: TrackerRef): void {
}
// subscribe, trying to play nice with the browser threads
function subscribe <T> (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity, withParams }: CallOptions<T>): void {
function subscribe <T> (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity, withParams }: CallOptions<T>): 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 <T> (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 <T> (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 <T> (fn: TrackFn | undefined | null | false, params: CallParams = [], options: CallOptions<T> = {}): T | undefined {
const mounted = useIsMountedRef();
const mountedRef = useIsMountedRef();
const tracker = useRef<Tracker>({ isActive: false, count: 0, serialized: null, subscriber: null });
const [value, setValue] = useState<T | undefined>(options.defaultValue);
@ -116,13 +116,13 @@ export default function useCall <T> (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]);

View File

@ -8,12 +8,12 @@ import useIsMountedRef from './useIsMountedRef';
// Debounces inputs
export default function useDebounce <T> (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

View File

@ -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<Result>(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;
}

View File

@ -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<DerivedStakingQuery[]>(isApiReady && stashIds && api.derive.staking?.queryMulti, stashIds);
const indexes = useCall<DeriveSessionIndexes>(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<OwnRewards>({ rewardCount: 0 });
useEffect((): void => {
available && nextPayouts && setState(
mountedRef.current && available && nextPayouts && setState(
getRewards(available, nextPayouts)
);
}, [available, nextPayouts]);

View File

@ -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<AccountId>[], ownL
export default function useOwnStashes (): [string, IsInKeyring][] | undefined {
const { allAccounts } = useAccounts();
const mountedRef = useIsMountedRef();
const { api } = useApi();
const ownBonded = useCall<Option<AccountId>[]>(api.query.staking?.bonded.multi as any, [allAccounts]);
const ownLedger = useCall<Option<StakingLedger>[]>(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<string[] | undefined>();
useEffect((): void => {
ownStashes && setStashIds(
mountedRef.current && ownStashes && setStashIds(
ownStashes.map(([stashId]) => stashId)
);
}, [ownStashes]);