diff --git a/packages/page-staking/src/Actions/Account/KickNominees.tsx b/packages/page-staking/src/Actions/Account/KickNominees.tsx new file mode 100644 index 0000000000..5e99e914d9 --- /dev/null +++ b/packages/page-staking/src/Actions/Account/KickNominees.tsx @@ -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 | null { + const { t } = useTranslation(); + const { api } = useApi(); + const [selected, setSelected] = useState([]); + const [{ kickTx }, setTx] = useState<{ kickTx?: null | SubmittableExtrinsic<'promise'> }>({}); + const queryInfo = useCall(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 ( + ('Remove nominees')} + size='large' + > + + + {nominators + ? ( + ('existing/active nominators')} + defaultValue={nominating} + help={t('Filter available nominators based on name, address or short account index.')} + maxCount={MAX_KICK} + onChange={setSelected} + valueLabel={t('nominators to be removed')} + /> + ) + : ('Retrieving active nominators')} /> + } + + + ('Remove')} + onStart={onClose} + /> + + + ); +} + +export default React.memo(KickNominees); diff --git a/packages/page-staking/src/Actions/Account/index.tsx b/packages/page-staking/src/Actions/Account/index.tsx index 76b3a55468..255e0846a9 100644 --- a/packages/page-staking/src/Actions/Account/index.tsx +++ b/packages/page-staking/src/Actions/Account/index.tsx @@ -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 && ( )} + {isKickOpen && controllerId && ( + + )} {isNominateOpen && controllerId && ( {t('Change reward destination')} - {isStashValidating && - - {t('Change validator preferences')} - - } + {isStashValidating && ( + <> + + {t('Change validator preferences')} + + {api.tx.staking.kick && ( + + {t('Remove nominees')} + + )} + + )} {!isStashNominating && void; } +const EMPTY_INFO = { + bondOwnTx: null, + bondTx: null, + controllerId: null, + controllerTx: null, + stashId: null +}; + function Bond ({ className = '', isNominating, minNomination, onChange }: Props): React.ReactElement { 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]); diff --git a/packages/page-staking/src/Actions/partials/Nominate.tsx b/packages/page-staking/src/Actions/partials/Nominate.tsx index a07820d0e0..e6ea568d7f 100644 --- a/packages/page-staking/src/Actions/partials/Nominate.tsx +++ b/packages/page-staking/src/Actions/partials/Nominate.tsx @@ -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 (
{withSenders && ( - - - ('stash account')} - /> - ('controller account')} - /> - - -

{t('The stash that is to be affected. The transaction will be sent from the associated controller account.')}

-
-
+ )} diff --git a/packages/page-staking/src/Actions/partials/SenderInfo.tsx b/packages/page-staking/src/Actions/partials/SenderInfo.tsx new file mode 100644 index 0000000000..4a92546d00 --- /dev/null +++ b/packages/page-staking/src/Actions/partials/SenderInfo.tsx @@ -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 { + const { t } = useTranslation(); + + return ( + + + ('stash account')} + /> + ('controller account')} + /> + + +

{t('The stash that is to be affected. The transaction will be sent from the associated controller account.')}

+
+
+ ); +} + +export default React.memo(SenderInfo); diff --git a/packages/page-staking/src/Actions/partials/SessionKey.tsx b/packages/page-staking/src/Actions/partials/SessionKey.tsx index bbece18625..f49b7927ce 100644 --- a/packages/page-staking/src/Actions/partials/SessionKey.tsx +++ b/packages/page-staking/src/Actions/partials/SessionKey.tsx @@ -27,12 +27,16 @@ function SessionKey ({ className = '', controllerId, onChange, stashId, withSend const [keys, setKeys] = useState(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 ( diff --git a/packages/page-staking/src/Actions/partials/Validate.tsx b/packages/page-staking/src/Actions/partials/Validate.tsx index 7e105f18a3..af2c3990db 100644 --- a/packages/page-staking/src/Actions/partials/Validate.tsx +++ b/packages/page-staking/src/Actions/partials/Validate.tsx @@ -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 { const { t } = useTranslation(); const { api } = useApi(); + const [commission, setCommission] = useState(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

{t('The commission is deducted from all rewards before the remainder is split with nominators.')}

+ {api.tx.staking.kick && ( + + + ('Does this validator allow nominations or is it blocked for all')} + label={t('allows new nominations')} + onChange={setAllowNoms} + options={blockedOptions.current} + /> + + +

{t('The validator can block any new nominations. By default it is set to allow all nominations.')}

+
+
+ )}
); } diff --git a/packages/page-staking/src/Actions/useInactives.ts b/packages/page-staking/src/Actions/useInactives.ts index 7bcb8d1806..e645991b4d 100644 --- a/packages/page-staking/src/Actions/useInactives.ts +++ b/packages/page-staking/src/Actions/useInactives.ts @@ -18,7 +18,7 @@ interface Inactives { nomsWaiting?: string[]; } -function extractState (api: ApiPromise, stashId: string, slashes: Option[], nominees: string[], activeEra: EraIndex, submittedIn: EraIndex, exposures: Exposure[]): Inactives { +function extractState (api: ApiPromise, stashId: string, slashes: Option[], 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 - 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[]; mountedRef.current && setState( - extractState(api, stashId, slashes, nominees, indexes.activeEra, optNominators.unwrapOrDefault().submittedIn, exposures) + extractState(api, stashId, slashes, nominees, indexes, optNominators.unwrapOrDefault().submittedIn, exposures) ); } ) diff --git a/packages/react-params/src/Param/Bool.tsx b/packages/react-params/src/Param/Bool.tsx index fde5e96acf..ba140ad834 100644 --- a/packages/react-params/src/Param/Bool.tsx +++ b/packages/react-params/src/Param/Bool.tsx @@ -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('No'), value: false }, - { text: t('Yes'), value: true } - ], - [t] - ); + const options = useRef([ + { text: t('No'), value: false }, + { text: t('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} />