Payout all stakers (#2540)
This commit is contained in:
parent
34e254a5ff
commit
2ff155bea5
@ -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={[
|
||||
|
||||
27
packages/page-staking/src/ElectionBanner.tsx
Normal file
27
packages/page-staking/src/ElectionBanner.tsx
Normal 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);
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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={[
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user