Allow validator removal of nominators (#4747)

* Allow validator removal of nominators

* Updates

* Blocking

* Revert Dropdown

* Expand blocked

* typo

* Kick modal

* kicking operational

* Retrieve exposure
This commit is contained in:
Jaco Greeff 2021-03-01 13:45:03 +01:00 committed by GitHub
parent abcc3d4516
commit 8226471cdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 255 additions and 67 deletions

View File

@ -0,0 +1,93 @@
// Copyright 2017-2021 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { DeriveStakingQuery } from '@polkadot/api-derive/types';
import React, { useEffect, useMemo, useState } from 'react';
import { InputAddressMulti, Modal, Spinner, TxButton } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useTranslation } from '../../translate';
import SenderInfo from '../partials/SenderInfo';
interface Props {
className?: string;
controllerId: string;
nominating?: string[];
onClose: () => void;
stashId: string;
}
const MAX_KICK = 128;
const accountOpts = {
withExposure: true
};
function KickNominees ({ className = '', controllerId, nominating, onClose, stashId }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [selected, setSelected] = useState<string[]>([]);
const [{ kickTx }, setTx] = useState<{ kickTx?: null | SubmittableExtrinsic<'promise'> }>({});
const queryInfo = useCall<DeriveStakingQuery>(api.derive.staking.query, [stashId, accountOpts]);
const nominators = useMemo(
() => queryInfo?.exposure?.others.map(({ who }) => who.toString()),
[queryInfo]
);
useEffect((): void => {
try {
setTx({
kickTx: selected.length
? api.tx.staking.kick(selected)
: null
});
} catch {
setTx({ kickTx: null });
}
}, [api, selected]);
return (
<Modal
className={className}
header={t<string>('Remove nominees')}
size='large'
>
<Modal.Content>
<SenderInfo
controllerId={controllerId}
stashId={stashId}
/>
{nominators
? (
<InputAddressMulti
available={nominators}
availableLabel={t<string>('existing/active nominators')}
defaultValue={nominating}
help={t<string>('Filter available nominators based on name, address or short account index.')}
maxCount={MAX_KICK}
onChange={setSelected}
valueLabel={t<string>('nominators to be removed')}
/>
)
: <Spinner label={t<string>('Retrieving active nominators')} />
}
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<TxButton
accountId={controllerId}
extrinsic={kickTx}
icon='user-slash'
isDisabled={!kickTx}
label={t<string>('Remove')}
onStart={onClose}
/>
</Modal.Actions>
</Modal>
);
}
export default React.memo(KickNominees);

View File

@ -20,6 +20,7 @@ import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../../translate';
import BondExtra from './BondExtra';
import InjectKeys from './InjectKeys';
import KickNominees from './KickNominees';
import ListNominees from './ListNominees';
import Nominate from './Nominate';
import SetControllerAccount from './SetControllerAccount';
@ -72,6 +73,7 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
const { queueExtrinsic } = useContext(StatusContext);
const [isBondExtraOpen, toggleBondExtra] = useToggle();
const [isInjectOpen, toggleInject] = useToggle();
const [isKickOpen, toggleKick] = useToggle();
const [isNominateOpen, toggleNominate] = useToggle();
const [isRewardDestinationOpen, toggleRewardDestination] = useToggle();
const [isSetControllerOpen, toggleSetController] = useToggle();
@ -130,6 +132,13 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
{isInjectOpen && (
<InjectKeys onClose={toggleInject} />
)}
{isKickOpen && controllerId && (
<KickNominees
controllerId={controllerId}
onClose={toggleKick}
stashId={stashId}
/>
)}
{isNominateOpen && controllerId && (
<Nominate
controllerId={controllerId}
@ -307,14 +316,24 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio
>
{t<string>('Change reward destination')}
</Menu.Item>
{isStashValidating &&
<Menu.Item
disabled={!isOwnController}
onClick={toggleValidate}
>
{t<string>('Change validator preferences')}
</Menu.Item>
}
{isStashValidating && (
<>
<Menu.Item
disabled={!isOwnController}
onClick={toggleValidate}
>
{t<string>('Change validator preferences')}
</Menu.Item>
{api.tx.staking.kick && (
<Menu.Item
disabled={!isOwnController}
onClick={toggleKick}
>
{t<string>('Remove nominees')}
</Menu.Item>
)}
</>
)}
<Menu.Divider />
{!isStashNominating &&
<Menu.Item

View File

@ -26,6 +26,14 @@ interface Props {
onChange: (info: BondInfo) => void;
}
const EMPTY_INFO = {
bondOwnTx: null,
bondTx: null,
controllerId: null,
controllerTx: null,
stashId: null
};
function Bond ({ className = '', isNominating, minNomination, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
@ -76,13 +84,7 @@ function Bond ({ className = '', isNominating, minNomination, onChange }: Props)
controllerTx: api.tx.staking.setController(controllerId),
stashId
}
: {
bondOwnTx: null,
bondTx: null,
controllerId: null,
controllerTx: null,
stashId: null
}
: EMPTY_INFO
);
}, [api, amount, amountError, controllerError, controllerId, destination, destAccount, stashId, onChange]);

View File

@ -7,11 +7,12 @@ import type { NominateInfo } from './types';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { InputAddress, InputAddressMulti, MarkWarning, Modal } from '@polkadot/react-components';
import { InputAddressMulti, MarkWarning, Modal } from '@polkadot/react-components';
import { useApi, useFavorites } from '@polkadot/react-hooks';
import { MAX_NOMINATIONS, STORE_FAVS_BASE } from '../../constants';
import { useTranslation } from '../../translate';
import SenderInfo from './SenderInfo';
interface Props {
className?: string;
@ -41,33 +42,24 @@ function Nominate ({ className = '', controllerId, nominating, onChange, stashId
});
useEffect((): void => {
onChange({
nominateTx: selected && selected.length
? api.tx.staking.nominate(selected)
: null
});
try {
onChange({
nominateTx: selected && selected.length
? api.tx.staking.nominate(selected)
: null
});
} catch {
onChange({ nominateTx: null });
}
}, [api, onChange, selected]);
return (
<div className={className}>
{withSenders && (
<Modal.Columns>
<Modal.Column>
<InputAddress
defaultValue={stashId}
isDisabled
label={t<string>('stash account')}
/>
<InputAddress
defaultValue={controllerId}
isDisabled
label={t<string>('controller account')}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The stash that is to be affected. The transaction will be sent from the associated controller account.')}</p>
</Modal.Column>
</Modal.Columns>
<SenderInfo
controllerId={controllerId}
stashId={stashId}
/>
)}
<Modal.Columns>
<Modal.Column>

View File

@ -0,0 +1,40 @@
// Copyright 2017-2021 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { InputAddress, Modal } from '@polkadot/react-components';
import { useTranslation } from '../../translate';
interface Props {
className?: string;
controllerId: string;
stashId: string;
}
function SenderInfo ({ className = '', controllerId, stashId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<Modal.Columns className={className}>
<Modal.Column>
<InputAddress
defaultValue={stashId}
isDisabled
label={t<string>('stash account')}
/>
<InputAddress
defaultValue={controllerId}
isDisabled
label={t<string>('controller account')}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The stash that is to be affected. The transaction will be sent from the associated controller account.')}</p>
</Modal.Column>
</Modal.Columns>
);
}
export default React.memo(SenderInfo);

View File

@ -27,12 +27,16 @@ function SessionKey ({ className = '', controllerId, onChange, stashId, withSend
const [keys, setKeys] = useState<string | null>(null);
useEffect((): void => {
onChange({
sessionTx: isHex(keys)
// this is weird... :(
? api.tx.session.setKeys(keys as any, EMPTY_PROOF)
: null
});
try {
onChange({
sessionTx: isHex(keys)
// this is weird... :(
? api.tx.session.setKeys(keys as any, EMPTY_PROOF)
: null
});
} catch {
onChange({ sessionTx: null });
}
}, [api, keys, onChange]);
return (

View File

@ -4,9 +4,9 @@
import type { ValidateInfo } from './types';
import BN from 'bn.js';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { InputAddress, InputNumber, Modal } from '@polkadot/react-components';
import { Dropdown, InputAddress, InputNumber, Modal } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { BN_HUNDRED as MAX_COMM } from '@polkadot/util';
@ -25,16 +25,34 @@ const COMM_MUL = new BN(1e7);
function Validate ({ className = '', controllerId, onChange, stashId, withSenders }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [commission, setCommission] = useState<BN | number>(1);
const [allowNoms, setAllowNoms] = useState(true);
const blockedOptions = useRef([
{ text: t('Yes, allow nominations'), value: true },
{ text: t('No, block all nominations'), value: false }
]);
useEffect((): void => {
try {
onChange({
validateTx: api.tx.staking.validate({
blocked: !allowNoms,
commission
})
});
} catch {
onChange({ validateTx: null });
}
}, [api, allowNoms, commission, onChange]);
const _setCommission = useCallback(
(value?: BN) => onChange({
validateTx: value && api.tx.staking.validate({
commission: value.isZero()
? 1 // small non-zero set to avoid isEmpty
: value.mul(COMM_MUL)
})
}),
[api, onChange]
(value?: BN) => value && setCommission(
value.isZero()
? 1 // small non-zero set to avoid isEmpty
: value.mul(COMM_MUL)
),
[]
);
return (
@ -72,6 +90,22 @@ function Validate ({ className = '', controllerId, onChange, stashId, withSender
<p>{t<string>('The commission is deducted from all rewards before the remainder is split with nominators.')}</p>
</Modal.Column>
</Modal.Columns>
{api.tx.staking.kick && (
<Modal.Columns>
<Modal.Column>
<Dropdown
defaultValue={true}
help={t<string>('Does this validator allow nominations or is it blocked for all')}
label={t<string>('allows new nominations')}
onChange={setAllowNoms}
options={blockedOptions.current}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The validator can block any new nominations. By default it is set to allow all nominations.')}</p>
</Modal.Column>
</Modal.Columns>
)}
</div>
);
}

View File

@ -18,7 +18,7 @@ interface Inactives {
nomsWaiting?: string[];
}
function extractState (api: ApiPromise, stashId: string, slashes: Option<SlashingSpans>[], nominees: string[], activeEra: EraIndex, submittedIn: EraIndex, exposures: Exposure[]): Inactives {
function extractState (api: ApiPromise, stashId: string, slashes: Option<SlashingSpans>[], nominees: string[], { activeEra }: DeriveSessionIndexes, submittedIn: EraIndex, exposures: Exposure[]): Inactives {
const max = api.consts.staking?.maxNominatorRewardedPerValidator;
// chilled
@ -54,7 +54,11 @@ function extractState (api: ApiPromise, stashId: string, slashes: Option<Slashin
// waiting if validator is inactive or we have not submitted long enough ago
const nomsWaiting = exposures
.map((exposure, index) =>
exposure.total.unwrap().isZero() || (nomsInactive.includes(nominees[index]) && submittedIn.eq(activeEra))
exposure.total.unwrap().isZero() || (
nomsInactive.includes(nominees[index]) &&
// it could be activeEra + 1 (currentEra for last session)
submittedIn.gte(activeEra)
)
? nominees[index]
: null
)
@ -102,7 +106,7 @@ export default function useInactives (stashId: string, nominees?: string[]): Ina
const slashes = exposuresAndSpans.slice(nominees.length) as Option<SlashingSpans>[];
mountedRef.current && setState(
extractState(api, stashId, slashes, nominees, indexes.activeEra, optNominators.unwrapOrDefault().submittedIn, exposures)
extractState(api, stashId, slashes, nominees, indexes, optNominators.unwrapOrDefault().submittedIn, exposures)
);
}
)

View File

@ -3,9 +3,10 @@
import type { Props } from '../types';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { Dropdown } from '@polkadot/react-components';
import { isBoolean } from '@polkadot/util';
import { useTranslation } from '../translate';
import Bare from './Bare';
@ -15,16 +16,15 @@ function BoolParam ({ className = '', defaultValue: { value }, isDisabled, isErr
const [defaultValue] = useState(
value instanceof Boolean
? value.valueOf()
: value as boolean
: isBoolean(value)
? value
: false
);
const options = useMemo(
() => [
{ text: t<string>('No'), value: false },
{ text: t<string>('Yes'), value: true }
],
[t]
);
const options = useRef([
{ text: t<string>('No'), value: false },
{ text: t<string>('Yes'), value: true }
]);
const _onChange = useCallback(
(value: boolean) =>
@ -44,7 +44,7 @@ function BoolParam ({ className = '', defaultValue: { value }, isDisabled, isErr
isError={isError}
label={label}
onChange={_onChange}
options={options}
options={options.current}
withEllipsis
withLabel={withLabel}
/>