Staking render opt. useEffect/useState -> useMemo (#3408)

* Staking render opt. useEffect/useState -> useMemo

* Targets opt

* Simplify

* Couple more

* Targets useEffect

* Update packages/page-staking/src/Actions/Account/BondExtra.tsx

* Additional

* Cleanup

* Unused state

* Update packages/page-staking/src/index.tsx

* Update packages/page-staking/src/index.tsx
This commit is contained in:
Jaco Greeff 2020-08-13 11:04:10 +02:00 committed by GitHub
parent 3ccb6ae98a
commit 8c07ccc83f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 254 additions and 283 deletions

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [amountError, setAmountError] = useState<AmountValidateState | null>(null);
const [maxAdditional, setMaxAdditional] = useState<BN | undefined>();
const [maxBalance] = useState<BN | undefined>();
const [startBalance, setStartBalance] = useState<BN | null>(null);
const stashBalance = useCall<DeriveBalancesAll>(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 (
<Modal
@ -83,7 +87,6 @@ function BondExtra ({ controllerId, onClose, stakingInfo, stashId }: Props): Rea
params={stashId}
/>
}
maxValue={maxBalance}
onChange={setMaxAdditional}
/>
<ValidateAmount

View File

@ -9,7 +9,7 @@ import { SortedTargets } from '../../types';
import { Slash } from '../types';
import BN from 'bn.js';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import styled from 'styled-components';
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, Menu, Popup, StakingBonded, StakingRedeemable, StakingUnbonding, StatusContext, TxButton } from '@polkadot/react-components';
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
@ -38,6 +38,17 @@ interface Props {
validators?: string[];
}
function extractSlashes (stashId: string, allSlashes: [BN, UnappliedSlash[]][] = []): Slash[] {
return allSlashes
.map(([era, all]) => ({
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<Props> {
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<Slash[]>([]);
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 (
<tr className={className}>
<td className='badge together'>

View File

@ -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<Props> {
const { t } = useTranslation();
const allSlashes = useAvailableSlashes();
const [{ bondedTotal, foundStashes }, setState] = useState<State>({});
useEffect((): void => {
ownStashes && setState(extractState(ownStashes));
}, [ownStashes]);
const { bondedTotal, foundStashes } = useMemo(
() => extractState(ownStashes),
[ownStashes]
);
const header = useMemo(() => [
[t('stashes'), 'start', 2],

View File

@ -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<DeriveSessionInfo>(api.derive.session.info, []);
const [duration, setDuration] = useState<BN | undefined>();
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]
);
}

View File

@ -15,7 +15,7 @@ interface Props {
function Favorite ({ address, className, isFavorite, toggleFavorite }: Props): React.ReactElement<Props> {
const _onFavorite = useCallback(
(): void => toggleFavorite(address),
() => toggleFavorite(address),
[address, toggleFavorite]
);

View File

@ -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<Props> {
const { t } = useTranslation();
const [{ active, chilled }, setChilled] = useState<Chilled>({ 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 (
<td className='start all'>

View File

@ -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<SlashingSpans | null>(!isMain && api.query.staking.slashingSpans, [address], {
transform: (opt: Option<SlashingSpans>) => opt.unwrapOr(null)
});
const [{ commission, nominators, stakeOther, stakeOwn }, setStakingState] = useState<StakingState>({ 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 => {

View File

@ -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<DeriveHeartbeats>(!isIntentions && api.derive.imOnline?.receivedHeartbeats, []);
const nominators = useCall<[StorageKey, Option<Nominations>][]>(isIntentions && api.query.staking.nominators.entries as any, []);
const [{ elected, validators, waiting }, setFiltered] = useState<Filtered>({});
const [nameFilter, setNameFilter] = useState<string>('');
const [nominatedBy, setNominatedBy] = useState<Record<string, [string, EraIndex, number][]> | 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],

View File

@ -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<string, BN>, { stashes }): Record<string, BN> => {
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<Props> {
const { t } = useTranslation();
const [{ eraStr, nominators, numNominators, oldestEra }, setState] = useState<State>({
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<string, BN>, { stashes }): Record<string, BN> => {
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 (
<tr className={className}>
<td

View File

@ -6,7 +6,7 @@ import { Forcing } from '@polkadot/types/interfaces';
import { DeriveSessionProgress } 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';
@ -15,11 +15,10 @@ export default function useEraBlocks (era?: BN): BN | undefined {
const depth = useCall<BN>(api.query.staking.historyDepth, []);
const progress = useCall<DeriveSessionProgress>(api.derive.session.progress, []);
const forcing = useCall<Forcing>(api.query.staking.forceEra, []);
const [duration, setDuration] = useState<BN | undefined>();
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]
);
}

View File

@ -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<BN>(
isFunction(api.tx.staking.payoutStakers)
? new BN(0)
: new BN(1_000_000_000)
);
const migrateEraOpt = useCall<Option<EraIndex>>(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]
);
}

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [{ chart, labels }, setChart] = useState<ChartInfo>({ chart: [], labels: [] });
const stakerPoints = useCall<DeriveStakerPoints[]>(api.derive.staking.stakerPoints, [validatorId, true]);
const { chart, labels } = useMemo(
() => stakerPoints
? extractPoints(stakerPoints)
: { chart: [], labels: [] },
[stakerPoints]
);
const legends = useMemo(() => [
t<string>('points'),
t<string>('average')
], [t]);
useEffect((): void => {
stakerPoints && setChart(
extractPoints(stakerPoints)
);
}, [stakerPoints]);
return (
<div className='staking--Chart'>
<h1>{t<string>('era points')}</h1>

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [{ chart, labels }, setChart] = useState<ChartInfo>({ chart: [], labels: [] });
const ownSlashes = useCall<DeriveOwnSlashes[]>(api.derive.staking.ownSlashes, [validatorId, true]);
const erasRewards = useCall<DeriveEraRewards[]>(api.derive.staking.erasRewards, []);
const stakerPoints = useCall<DeriveStakerPoints[]>(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<string>('{{currency}} slashed', { replace: { currency } }),
t<string>('{{currency}} rewards', { replace: { currency } }),
t<string>('{{currency}} average', { replace: { currency } })
], [currency, t]);
useEffect((): void => {
erasRewards && ownSlashes && stakerPoints && setChart(
extractRewards(erasRewards, ownSlashes, stakerPoints, divisor)
);
}, [divisor, erasRewards, ownSlashes, stakerPoints]);
return (
<div className='staking--Chart'>
<h1>{t<string>('rewards & slashes')}</h1>

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [{ chart, labels }, setChart] = useState<ChartInfo>({ chart: [], labels: [] });
const ownExposures = useCall<DeriveOwnExposure[]>(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<string>('{{currency}} clipped', { replace: { currency } }),
t<string>('{{currency}} total', { replace: { currency } }),
t<string>('{{currency}} average', { replace: { currency } })
], [currency, t]);
useEffect((): void => {
ownExposures && setChart(
extractStake(ownExposures, divisor)
);
}, [divisor, ownExposures]);
return (
<div className='staking--Chart'>
<h1>{t<string>('elected stake')}</h1>

View File

@ -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<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const sessionInfo = useCall<DeriveSessionProgress>(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 (
<tr>

View File

@ -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<Props> {
const { t } = useTranslation();
const [ids, setIds] = useState<IdState | null>(null);
const [filter, setFilter] = useState<string[]>([]);
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 => {

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const totalIssuance = useCall<Balance>(api.query.balances?.totalIssuance, []);
const [progressStake, setProgressStake] = useState<Progress | undefined>();
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 (
<SummaryBox>

View File

@ -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<DeriveAccountInfo>(api.derive.accounts.info, [info.accountId]);
const [isVisible, setVisibility] = useState(true);
const [slashes, setSlashes] = useState<Slash[]>([]);
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 => {

View File

@ -34,6 +34,11 @@ interface SortState {
sortFromMax: boolean;
}
const CLASSES: Record<string, string> = {
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<Props> {
const { t } = useTranslation();
const allSlashes = useAvailableSlashes();
const ownNominators = useOwnNominators(ownStashes);
const [selected, setSelected] = useState<string[]>([]);
const [sorted, setSorted] = useState<number[] | undefined>();
const [myNominees, setMyNominees] = useState<string[]>([]);
const [nameFilter, setNameFilter] = useState<string>('');
const [withElected, setWithElected] = useState(false);
const [withIdentity, setWithIdentity] = useState(false);
const [{ sortBy, sortFromMax }, setSortBy] = useState<SortState>({ sortBy: 'rankOverall', sortFromMax: true });
const [sorted, setSorted] = useState<number[] | undefined>();
// 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<string, string> => ({
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]}<Icon icon={sortBy === header ? (sortFromMax ? 'chevron-down' : 'chevron-up') : 'minus'} /></>,
`${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 && (

View File

@ -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<StakerState[] | undefined>();
useEffect((): void => {
ownStashes && setState(
ownStashes.filter(({ isOwnController, isStashValidating }) => isOwnController && !isStashValidating)
);
}, [ownStashes]);
return state;
return useMemo(
() => ownStashes ? ownStashes.filter(({ isOwnController, isStashValidating }) => isOwnController && !isStashValidating) : undefined,
[ownStashes]
);
}

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { hasAccounts } = useAccounts();
const { pathname } = useLocation();
const [{ next }, setValidators] = useState<Validators>({});
const [ownValidators, setOwnValidators] = useState<StakerState[]>([]);
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const allStashes = useStashIds();
const ownStashes = useOwnStashInfos();
@ -49,10 +41,24 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement<Pr
const isInElection = useCall<boolean>(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<Pr
text: t<string>('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 (
<main className={`staking--App ${className}`}>
<HelpOverlay md={basicMd as string} />