Payout all stakers (#2540)

This commit is contained in:
Jaco Greeff 2020-04-09 13:18:16 +02:00 committed by GitHub
parent 34e254a5ff
commit 2ff155bea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 113 additions and 44 deletions

View File

@ -2,13 +2,14 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { ActiveEraInfo, ElectionStatus, EraIndex } from '@polkadot/types/interfaces';
import { ActiveEraInfo, EraIndex } from '@polkadot/types/interfaces';
import React, { useCallback, useEffect, useState } from 'react';
import { Table } from '@polkadot/react-components';
import { useCall, useApi, useOwnStashes } from '@polkadot/react-hooks';
import { Option } from '@polkadot/types';
import ElectionBanner from '../ElectionBanner';
import { useTranslation } from '../translate';
import Account from './Account';
import NewStake from './NewStake';
@ -16,19 +17,17 @@ import NewStake from './NewStake';
interface Props {
allStashes?: string[];
className?: string;
isInElection?: boolean;
next?: string[];
validators?: string[];
}
function Actions ({ allStashes, className, next, validators }: Props): React.ReactElement<Props> {
function Actions ({ allStashes, className, isInElection, next, validators }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const activeEra = useCall<EraIndex | undefined>(api.query.staking?.activeEra, [], {
transform: (activeEra: Option<ActiveEraInfo>) => activeEra.unwrapOr({ index: undefined }).index
});
const isInElection = useCall<boolean>(api.query.staking?.eraElectionStatus, [], {
transform: (status: ElectionStatus) => status.isOpen
});
const ownStashes = useOwnStashes();
const [foundStashes, setFoundStashes] = useState<[string, boolean][] | null>(null);
const [stashTypes, setStashTypes] = useState<Record<string, number>>({});
@ -57,11 +56,7 @@ function Actions ({ allStashes, className, next, validators }: Props): React.Rea
return (
<div className={className}>
<NewStake isInElection={isInElection} />
{isInElection && (
<article className='warning nomargin'>
{t('There is currently an ongoing election for new validator candidates. As such staking operations are not permitted.')}
</article>
)}
<ElectionBanner isInElection={isInElection} />
<Table
empty={t('No funds staked yet. Bond funds to validate or nominate a validator')}
header={[

View File

@ -0,0 +1,27 @@
// 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 React from 'react';
import { useTranslation } from './translate';
interface Props {
isInElection?: boolean;
}
function ElectionBanner ({ isInElection }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
if (!isInElection) {
return null;
}
return (
<article className='warning nomargin'>
{t('There is currently an ongoing election for new validator candidates. As such staking operations are not permitted.')}
</article>
);
}
export default React.memo(ElectionBanner);

View File

@ -3,21 +3,44 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { PayoutValidator } from './types';
import BN from 'bn.js';
import React, { useState, useEffect } from 'react';
import { Button, Modal, InputAddress, TxButton } from '@polkadot/react-components';
import { ApiPromise } from '@polkadot/api';
import { AddressMini, Button, Modal, InputAddress, Static, TxButton } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
interface Props {
eras: BN[];
isInElection?: boolean;
validatorId: string;
payout?: PayoutValidator | PayoutValidator[];
}
function PayButton ({ eras, isInElection, validatorId }: Props): React.ReactElement<Props> {
function createExtrinsic (api: ApiPromise, payout: PayoutValidator | PayoutValidator[]): SubmittableExtrinsic<'promise'> {
if (Array.isArray(payout)) {
if (payout.length === 1) {
return createExtrinsic(api, payout[0]);
}
return api.tx.utility.batch(
payout.reduce((calls: SubmittableExtrinsic<'promise'>[], { eras, validatorId }): SubmittableExtrinsic<'promise'>[] =>
calls.concat(
...eras.map(({ era }) => api.tx.staking.payoutStakers(validatorId, era))
), [])
);
}
const { eras, validatorId } = payout;
return eras.length === 1
? api.tx.staking.payoutStakers(validatorId, eras[0].era)
: api.tx.utility.batch(
eras.map(({ era }) => api.tx.staking.payoutStakers(validatorId, era))
);
}
function PayButton ({ isInElection, payout }: Props): React.ReactElement<Props> {
const { api } = useApi();
const { t } = useTranslation();
const [isVisible, togglePayout] = useToggle();
@ -25,29 +48,42 @@ function PayButton ({ eras, isInElection, validatorId }: Props): React.ReactElem
const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic<'promise'> | null>(null);
useEffect((): void => {
setExtrinsic(
() => eras.length === 1
? api.tx.staking.payoutStakers(validatorId, eras[0])
: api.tx.utility.batch(
eras.map((era): SubmittableExtrinsic<'promise'> =>
api.tx.staking.payoutStakers(validatorId, era)
)
)
payout && setExtrinsic(
() => createExtrinsic(api, payout)
);
}, [api, eras, validatorId]);
}, [api, payout]);
const isPayoutEmpty = !payout || (Array.isArray(payout) && payout.length === 0);
return (
<>
{isVisible && (
{payout && isVisible && (
<Modal header={t('Payout all stakers')}>
<Modal.Content>
{Array.isArray(payout)
? (
<Static
label={t('payout stakers for (multiple)')}
value={
payout.map(({ validatorId }) => (
<AddressMini
key={validatorId}
value={validatorId}
/>
))
}
/>
)
: (
<InputAddress
defaultValue={payout.validatorId}
isDisabled
label={t('payout stakers for (single)')}
/>
)
}
<InputAddress
isDisabled
label={t('payout stakers for')}
value={validatorId}
/>
<InputAddress
label={t('request from')}
label={t('request payout from')}
onChange={setAccount}
type='account'
value={accountId}
@ -67,8 +103,8 @@ function PayButton ({ eras, isInElection, validatorId }: Props): React.ReactElem
)}
<Button
icon='percent'
isDisabled={isInElection}
label={t('Payout')}
isDisabled={isInElection || isPayoutEmpty}
label={Array.isArray(payout) ? t('Payout all') : t('Payout')}
onClick={togglePayout}
/>
</>

View File

@ -21,23 +21,20 @@ interface Props {
interface State {
eraStr: string;
eras: BN[];
nominators: Record<string, BN>;
numNominators: number;
}
function Payout ({ className, isInElection, payout }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [{ eraStr, eras, nominators, numNominators }, setState] = useState<State>({
const [{ eraStr, nominators, numNominators }, setState] = useState<State>({
eraStr: '',
eras: [],
nominators: {},
numNominators: 0
});
useEffect((): void => {
const eras = payout.eras.map(({ era }) => era);
const eraStr = createErasString(eras);
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]) {
@ -50,7 +47,7 @@ function Payout ({ className, isInElection, payout }: Props): React.ReactElement
return nominators;
}, {});
setState({ eraStr, eras, nominators, numNominators: Object.keys(nominators).length });
setState({ eraStr, nominators, numNominators: Object.keys(nominators).length });
}, [payout]);
return (
@ -82,9 +79,8 @@ function Payout ({ className, isInElection, payout }: Props): React.ReactElement
</td>
<td className='button'>
<PayButton
eras={eras}
isInElection={isInElection}
validatorId={payout.validatorId}
payout={payout}
/>
</td>
</tr>

View File

@ -8,11 +8,13 @@ import { PayoutStash, PayoutValidator } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Table } from '@polkadot/react-components';
import { Button, Table } from '@polkadot/react-components';
import { useApi, useOwnEraRewards } from '@polkadot/react-hooks';
import ElectionBanner from '../ElectionBanner';
import { useTranslation } from '../translate';
import useStakerPayouts from './useStakerPayouts';
import PayButton from './PayButton';
import Stash from './Stash';
import Validator from './Validator';
@ -96,6 +98,15 @@ function Payouts ({ className, isInElection }: Props): React.ReactElement<Props>
return (
<div className={className}>
{api.tx.staking.payoutStakers && (
<Button.Group>
<PayButton
isInElection={isInElection}
payout={validators}
/>
</Button.Group>
)}
<ElectionBanner isInElection={isInElection} />
<Table
empty={stashes && t('No pending payouts for your stashes')}
header={[

View File

@ -4,7 +4,7 @@
import { DeriveStakingOverview } from '@polkadot/api-derive/types';
import { AppProps as Props } from '@polkadot/react-components/types';
import { AccountId } from '@polkadot/types/interfaces';
import { AccountId, ElectionStatus } from '@polkadot/types/interfaces';
import React, { useEffect, useMemo, useReducer, useState } from 'react';
import { Route, Switch } from 'react-router';
@ -43,6 +43,9 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
stashes.map((accountId): string => accountId.toString())
});
const stakingOverview = useCall<DeriveStakingOverview>(api.derive.staking.overview, []);
const isInElection = useCall<boolean>(api.query.staking?.eraElectionStatus, [], {
transform: (status: ElectionStatus) => status.isOpen
});
const [nominators, dispatchNominators] = useReducer(reduceNominators, [] as string[]);
const hasQueries = useMemo(
(): boolean =>
@ -117,7 +120,7 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
<Targets />
</Route>
<Route path={`${basePath}/payout`}>
<Payouts />
<Payouts isInElection={isInElection} />
</Route>
<Route path={[`${basePath}/query/:value`, `${basePath}/query`]}>
<Query />
@ -135,6 +138,7 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
<Actions
allStashes={allStashes}
className={pathname === `${basePath}/actions` ? '' : 'staking--hidden'}
isInElection={isInElection}
next={next}
validators={validators}
/>