Validator-only payout toggle (#3386)
* Validator-only payout toggle * ToggleButton consistency
This commit is contained in:
parent
46734ee56d
commit
e16eb134aa
@ -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}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user