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:
parent
abcc3d4516
commit
8226471cdd
93
packages/page-staking/src/Actions/Account/KickNominees.tsx
Normal file
93
packages/page-staking/src/Actions/Account/KickNominees.tsx
Normal 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);
|
||||
@ -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
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
40
packages/page-staking/src/Actions/partials/SenderInfo.tsx
Normal file
40
packages/page-staking/src/Actions/partials/SenderInfo.tsx
Normal 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);
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user