diff --git a/packages/page-staking/src/Actions/Account/ListNominees.tsx b/packages/page-staking/src/Actions/Account/ListNominees.tsx new file mode 100644 index 0000000000..fb4bff8165 --- /dev/null +++ b/packages/page-staking/src/Actions/Account/ListNominees.tsx @@ -0,0 +1,62 @@ +// 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 { AddressMini, Expander } from '@polkadot/react-components'; + +import { useTranslation } from '../../translate'; +import useInactives from '../useInactives'; + +interface Props { + nominating?: string[]; + stashId: string; +} + +function ListNominees ({ nominating, stashId }: Props): React.ReactElement { + const { t } = useTranslation(); + const { nomsActive, nomsInactive, nomsWaiting } = useInactives(stashId, nominating); + + return ( + <> + {nomsActive && nomsActive.length !== 0 && ( + + {nomsActive.map((nomineeId, index): React.ReactNode => ( + + ))} + + )} + {nomsInactive && nomsInactive.length !== 0 && ( + + {nomsInactive.map((nomineeId, index): React.ReactNode => ( + + ))} + + )} + {nomsWaiting && nomsWaiting.length !== 0 && ( + + {nomsWaiting.map((nomineeId, index): React.ReactNode => ( + + ))} + + )} + + ); +} + +export default React.memo(ListNominees); diff --git a/packages/page-staking/src/Actions/Account/index.tsx b/packages/page-staking/src/Actions/Account/index.tsx index 8928ba0e68..622a202313 100644 --- a/packages/page-staking/src/Actions/Account/index.tsx +++ b/packages/page-staking/src/Actions/Account/index.tsx @@ -7,15 +7,15 @@ import { EraIndex } from '@polkadot/types/interfaces'; import { StakerState } from '@polkadot/react-hooks/types'; import { SortedTargets } from '../../types'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; -import { AddressInfo, AddressMini, AddressSmall, Button, Expander, Menu, Popup, StakingBonded, StakingRedeemable, StakingUnbonding, TxButton } from '@polkadot/react-components'; +import { AddressInfo, AddressMini, AddressSmall, Button, Menu, Popup, StakingBonded, StakingRedeemable, StakingUnbonding, TxButton } from '@polkadot/react-components'; import { useApi, useCall, useToggle } from '@polkadot/react-hooks'; import { useTranslation } from '../../translate'; -import useInactives from '../useInactives'; import BondExtra from './BondExtra'; import InjectKeys from './InjectKeys'; +import ListNominees from './ListNominees'; import Nominate from './Nominate'; import SetControllerAccount from './SetControllerAccount'; import SetRewardDestination from './SetRewardDestination'; @@ -39,8 +39,6 @@ function Account ({ className, info: { controllerId, destination, destinationId, const { api } = useApi(); const balancesAll = useCall(api.derive.balances.all, [stashId]); const stakingAccount = useCall(api.derive.staking.account, [stashId]); - const [activeNoms, setActiveNoms] = useState([]); - const inactiveNoms = useInactives(stashId, nominating); const [isBondExtraOpen, toggleBondExtra] = useToggle(); const [isInjectOpen, toggleInject] = useToggle(); const [isNominateOpen, toggleNominate] = useToggle(); @@ -51,12 +49,6 @@ function Account ({ className, info: { controllerId, destination, destinationId, const [isUnbondOpen, toggleUnbond] = useToggle(); const [isValidateOpen, toggleValidate] = useToggle(); - useEffect((): void => { - nominating && setActiveNoms( - nominating.filter((id): boolean => !inactiveNoms.includes(id)) - ); - }, [inactiveNoms, nominating]); - return ( @@ -141,32 +133,10 @@ function Account ({ className, info: { controllerId, destination, destinationId, : ( {isStashNominating && ( - <> - {activeNoms.length !== 0 && ( - - {activeNoms.map((nomineeId, index): React.ReactNode => ( - - ))} - - )} - {inactiveNoms.length !== 0 && ( - - {inactiveNoms.map((nomineeId, index): React.ReactNode => ( - - ))} - - )} - + )} ) diff --git a/packages/page-staking/src/Actions/useInactives.ts b/packages/page-staking/src/Actions/useInactives.ts index 8e61592d3e..b8c2cfcb71 100644 --- a/packages/page-staking/src/Actions/useInactives.ts +++ b/packages/page-staking/src/Actions/useInactives.ts @@ -3,15 +3,54 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { DeriveSessionIndexes } from '@polkadot/api-derive/types'; -import { Exposure } from '@polkadot/types/interfaces'; +import { EraIndex, Exposure, Nominations } from '@polkadot/types/interfaces'; import { useEffect, useState } from 'react'; import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks'; +import { Option } from '@polkadot/types'; -export default function useInactives (stashId: string, nominees?: string[]): string[] { +interface Inactives { + nomsActive?: string[]; + nomsInactive?: string[]; + nomsWaiting?: string[]; +} + +function extractState (stashId: string, nominees: string[], activeEra: EraIndex, submittedIn: EraIndex, exposures: Exposure[]): Inactives { + // first a blanket find of nominations not in the active set + let nomsInactive = exposures + .map((exposure, index) => + exposure.others.some(({ who }) => who.eq(stashId)) + ? null + : nominees[index] + ) + .filter((inactiveId): inactiveId is string => !!inactiveId); + + // 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]) && activeEra.sub(submittedIn).lten(2)) + ? nominees[index] + : null + ) + .filter((waitingId): waitingId is string => !!waitingId); + + // filter based on all inactives + const nomsActive = nominees.filter((nominee) => !nomsInactive.includes(nominee)); + + // inactive also contains waiting, remove those + nomsInactive = nomsInactive.filter((inactiveId) => !nomsWaiting.includes(inactiveId)); + + return { + nomsActive, + nomsInactive, + nomsWaiting + }; +} + +export default function useInactives (stashId: string, nominees?: string[]): Inactives { const { api } = useApi(); const mountedRef = useIsMountedRef(); - const [inactives, setInactives] = useState([]); + const [state, setState] = useState({}); const indexes = useCall(api.derive.session.indexes, []); useEffect((): () => void => { @@ -20,18 +59,14 @@ export default function useInactives (stashId: string, nominees?: string[]): str if (mountedRef.current && nominees?.length && indexes) { api .queryMulti( - api.query.staking.erasStakers - ? nominees.map((id) => [api.query.staking.erasStakers, [indexes.activeEra, id]]) - : nominees.map((id) => [api.query.staking.stakers, id]), - (exposures: Exposure[]): void => { - mountedRef.current && setInactives( - exposures - .map((exposure, index) => - exposure.others.some(({ who }) => who.eq(stashId)) - ? null - : nominees[index].toString() - ) - .filter((inactiveId): inactiveId is string => !!inactiveId) + [[api.query.staking.nominators, stashId] as any].concat( + api.query.staking.erasStakers + ? nominees.map((id) => [api.query.staking.erasStakers, [indexes.activeEra, id]]) + : nominees.map((id) => [api.query.staking.stakers, id]) + ), + ([optNominators, ...exposures]: [Option, ...Exposure[]]): void => { + mountedRef.current && setState( + extractState(stashId, nominees, indexes.activeEra, optNominators.unwrapOrDefault().submittedIn, exposures) ); } ) @@ -45,5 +80,5 @@ export default function useInactives (stashId: string, nominees?: string[]): str }; }, [api, indexes, mountedRef, nominees, stashId]); - return inactives; + return state; }