Validator-only payout toggle (#3386)

* Validator-only payout toggle

* ToggleButton consistency
This commit is contained in:
Jaco Greeff 2020-08-11 13:31:42 +02:00 committed by GitHub
parent 46734ee56d
commit e16eb134aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 129 additions and 27 deletions

View File

@ -77,6 +77,7 @@ function Actions ({ className = '', isInElection, ownStashes, targets }: Props):
return (
<div className={className}>
<ElectionBanner isInElection={isInElection} />
<Button.Group>
<NewNominator
isInElection={isInElection}
@ -85,7 +86,6 @@ function Actions ({ className = '', isInElection, ownStashes, targets }: Props):
<NewValidator isInElection={isInElection} />
<NewStash />
</Button.Group>
<ElectionBanner isInElection={isInElection} />
<Table
empty={foundStashes && t<string>('No funds staked yet. Bond funds to validate or nominate a validator')}
footer={footer}

View File

@ -9,27 +9,31 @@ import { Button } from '@polkadot/react-components';
interface Props {
className?: string;
onChange: (index: number) => void;
options: { text: string, value: number }[];
options: { isDisabled?: boolean, text: string, value: number }[];
selected: number;
}
interface ToggleProps {
index: number;
isDisabled?: boolean;
isSelected: boolean;
onChange: (index: number) => void;
text: string;
}
function ToggleIndex ({ index, isSelected, onChange, text }: ToggleProps): React.ReactElement<ToggleProps> {
function ToggleIndex ({ index, isDisabled, isSelected, onChange, text }: ToggleProps): React.ReactElement<ToggleProps> {
const _onClick = useCallback(
() => onChange(index),
[index, onChange]
(): void => {
!isDisabled && onChange(index);
},
[isDisabled, index, onChange]
);
return (
<Button
icon={isSelected ? 'check' : 'circle'}
isBasic
isDisabled={isDisabled}
isSelected={isSelected}
key={text}
label={text}
@ -47,9 +51,10 @@ function PayToggle ({ className = '', onChange, options, selected }: Props): Rea
return (
<div className={`ui--ToggleButton ${className}`}>
{options.map(({ text }, index): React.ReactNode => (
{options.map(({ isDisabled, text }, index): React.ReactNode => (
<ToggleIndexMemo
index={index}
isDisabled={isDisabled}
isSelected={selected === index}
key={index}
onChange={onChange}
@ -76,5 +81,9 @@ export default React.memo(styled(PayToggle)`
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.ui--Icon {
width: 1em;
}
}
`);

View File

@ -25,7 +25,7 @@ import Validator from './Validator';
interface Props {
className?: string;
isInElection?: boolean;
ownValidators: StakerState;
ownValidators: StakerState[];
}
interface Available {
@ -99,16 +99,18 @@ function extractStashes (allRewards: Record<string, DeriveStakerReward[]>): Payo
.sort((a, b) => b.available.cmp(a.available));
}
function Payouts ({ className = '', isInElection }: Props): React.ReactElement<Props> {
function Payouts ({ className = '', isInElection, ownValidators }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [{ stashTotal, stashes, validators }, setPayouts] = useState<Available>({});
const [eraSelection, setEraSelection] = useState<EraSelection[]>([{ text: '', value: 0 }]);
const [hasOwnValidators] = useState(ownValidators.length !== 0);
const [myStashesIndex, setMyStashesIndex] = useState((api.tx.staking.payoutStakers && hasOwnValidators) ? 0 : 1);
const [eraSelectionIndex, setEraSelectionIndex] = useState(0);
const eraLength = useCall<BN>(api.derive.session.eraLength, []);
const historyDepth = useCall<BN>(api.query.staking.historyDepth, []);
const stakerPayoutsAfter = useStakerPayouts();
const { allRewards, isLoadingRewards } = useOwnEraRewards(eraSelection[eraSelectionIndex].value);
const { allRewards, isLoadingRewards } = useOwnEraRewards(eraSelection[eraSelectionIndex].value, myStashesIndex ? undefined : ownValidators);
const isDisabled = isInElection || !isFunction(api.tx.utility?.batch);
useEffect((): void => {
@ -154,16 +156,18 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
stashes,
validators: groupByValidator(allRewards, stakerPayoutsAfter)
});
} else {
setPayouts({});
}
}, [allRewards, stakerPayoutsAfter]);
const headerStashes = useMemo(() => [
[t('payout/stash'), 'start', 2],
[myStashesIndex ? t('payout/stash') : t('overall/validator'), 'start', 2],
[t('eras'), 'start'],
[t('available')],
[('remaining')],
[undefined, undefined, 3]
], [t]);
], [myStashesIndex, t]);
const headerValidators = useMemo(() => [
[t('payout/validator'), 'start', 2],
@ -173,6 +177,11 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
[undefined, undefined, 3]
], [t]);
const valOptions = useMemo(() => [
{ isDisabled: !hasOwnValidators, text: t('My validators'), value: 'val' },
{ text: t('My stashes'), value: 'all' }
], [hasOwnValidators, t]);
const footer = useMemo(() => (
<tr>
<td colSpan={3} />
@ -185,8 +194,14 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
return (
<div className={className}>
<ElectionBanner isInElection={isInElection} />
{api.tx.staking.payoutStakers && (
<Button.Group>
<PayToggle
onChange={setMyStashesIndex}
options={valOptions}
selected={myStashesIndex}
/>
<PayToggle
onChange={setEraSelectionIndex}
options={eraSelection}
@ -199,7 +214,6 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
/>
</Button.Group>
)}
<ElectionBanner isInElection={isInElection} />
<Table
empty={!isLoadingRewards && stashes && t<string>('No pending payouts for your stashes')}
emptySpinner={t<string>('Retrieving info for the selected eras, this will take some time')}
@ -216,7 +230,7 @@ function Payouts ({ className = '', isInElection }: Props): React.ReactElement<P
/>
))}
</Table>
{api.tx.staking.payoutStakers && !isLoadingRewards && validators && (validators.length !== 0) && (
{api.tx.staking.payoutStakers && (myStashesIndex === 1) && !isLoadingRewards && validators && (validators.length !== 0) && (
<Table
header={headerValidators}
isFixed

View File

@ -177,6 +177,7 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { calcWit
numValidators={validators?.length}
totalStaked={totalStaked}
/>
<ElectionBanner isInElection={isInElection} />
<Button.Group>
<Button
icon='check'
@ -190,7 +191,6 @@ function Targets ({ className = '', isInElection, ownStashes, targets: { calcWit
targets={selected}
/>
</Button.Group>
<ElectionBanner isInElection={isInElection} />
<Table
empty={sorted && t<string>('No active validators to check')}
filter={filter}

View File

@ -2,10 +2,13 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveStakerReward } from '@polkadot/api-derive/types';
import { DeriveEraPoints, DeriveEraRewards, DeriveStakerReward } from '@polkadot/api-derive/types';
import { EraIndex } from '@polkadot/types/interfaces';
import { StakerState } from './types';
import { useEffect, useState } from 'react';
import { registry } from '@polkadot/react-api';
import { BN_ZERO } from '@polkadot/util';
import useApi from './useApi';
import useCall from './useCall';
@ -13,11 +16,21 @@ import useIsMountedRef from './useIsMountedRef';
import { useOwnStashIds } from './useOwnStashes';
interface OwnRewards {
allRewards?: Record<string, DeriveStakerReward[]>;
allRewards?: Record<string, DeriveStakerReward[]> | null;
isLoadingRewards: boolean;
rewardCount: number;
}
interface ValidatorWithEras {
eras: EraIndex[];
stashId: string;
}
interface Filtered {
filteredEras: EraIndex[];
validatorEras: ValidatorWithEras[];
}
function getRewards ([[stashIds], available]: [[string[]], DeriveStakerReward[][]]): OwnRewards {
const allRewards: Record<string, DeriveStakerReward[]> = {};
@ -32,30 +45,96 @@ function getRewards ([[stashIds], available]: [[string[]], DeriveStakerReward[][
};
}
export default function useOwnEraRewards (maxEras?: number): OwnRewards {
function getValRewards (validatorEras: ValidatorWithEras[], erasPoints: DeriveEraPoints[], erasRewards: DeriveEraRewards[]): OwnRewards {
const allRewards: Record<string, DeriveStakerReward[]> = {};
validatorEras.forEach(({ eras, stashId }): void => {
eras.forEach((era): void => {
const eraPoints = erasPoints.find((p) => p.era.eq(era));
const eraRewards = erasRewards.find((r) => r.era.eq(era));
if (eraPoints?.eraPoints.gt(BN_ZERO) && eraPoints?.validators[stashId] && eraRewards) {
const reward = eraPoints.validators[stashId].mul(eraRewards.eraReward).div(eraPoints.eraPoints);
if (!reward.isZero()) {
const total = registry.createType('Balance', reward);
if (!allRewards[stashId]) {
allRewards[stashId] = [];
}
allRewards[stashId].push({
era,
eraReward: eraRewards.eraReward,
isEmpty: false,
isValidator: true,
nominating: [],
validators: {
[stashId]: {
total,
value: total
}
}
});
}
}
});
});
return {
allRewards,
isLoadingRewards: false,
rewardCount: Object.values(allRewards).filter((rewards) => rewards.length !== 0).length
};
}
export default function useOwnEraRewards (maxEras?: number, ownValidators?: StakerState[]): OwnRewards {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const stashIds = useOwnStashIds();
const allEras = useCall<EraIndex[]>(api.derive.staking?.erasHistoric, []);
const [filteredEras, setFilteredEras] = useState<EraIndex[]>([]);
const available = useCall<[[string[]], DeriveStakerReward[][]]>(!!filteredEras.length && stashIds && api.derive.staking?.stakerRewardsMultiEras, [stashIds, filteredEras], { withParams: true });
const [{ filteredEras, validatorEras }, setFiltered] = useState<Filtered>({ filteredEras: [], validatorEras: [] });
const [state, setState] = useState<OwnRewards>({ isLoadingRewards: true, rewardCount: 0 });
const stakerRewards = useCall<[[string[]], DeriveStakerReward[][]]>(!ownValidators?.length && !!filteredEras.length && stashIds && api.derive.staking?.stakerRewardsMultiEras, [stashIds, filteredEras], { withParams: true });
const erasPoints = useCall<DeriveEraPoints[]>(!!validatorEras.length && !!filteredEras.length && api.derive.staking._erasPoints, [filteredEras, false]);
const erasRewards = useCall<DeriveEraRewards[]>(!!validatorEras.length && !!filteredEras.length && api.derive.staking._erasRewards, [filteredEras, false]);
useEffect((): void => {
setState({ isLoadingRewards: true, rewardCount: 0 });
}, [maxEras]);
setState({ allRewards: null, isLoadingRewards: true, rewardCount: 0 });
}, [maxEras, ownValidators]);
useEffect((): void => {
allEras && maxEras && setFilteredEras(
allEras.slice(-1 * maxEras)
if (allEras && maxEras) {
const filteredEras = allEras.slice(-1 * maxEras);
const validatorEras: ValidatorWithEras[] = [];
if (ownValidators?.length) {
ownValidators.forEach(({ stakingLedger, stashId }): void => {
if (stakingLedger) {
const eras = filteredEras.filter((era) => !stakingLedger.claimedRewards.some((c) => era.eq(c)));
if (eras.length) {
validatorEras.push({ eras, stashId });
}
}
});
}
setFiltered({ filteredEras, validatorEras });
}
}, [allEras, maxEras, ownValidators]);
useEffect((): void => {
mountedRef.current && stakerRewards && !ownValidators && setState(
getRewards(stakerRewards)
);
}, [allEras, maxEras]);
}, [mountedRef, ownValidators, stakerRewards]);
useEffect((): void => {
mountedRef.current && available && setState(
getRewards(available)
mountedRef && erasPoints && erasRewards && ownValidators && setState(
getValRewards(validatorEras, erasPoints, erasRewards)
);
}, [available, mountedRef]);
}, [erasPoints, erasRewards, mountedRef, ownValidators, validatorEras]);
return state;
}