From e498669d50b05c25604111fba20f6e43d7919f04 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 7 Aug 2020 12:21:47 +0200 Subject: [PATCH] Displayed chilled nominators in waiting (#3361) --- .../src/Actions/Account/ListNominees.tsx | 7 +-- .../src/Actions/Account/index.tsx | 1 - .../page-staking/src/Actions/useInactives.ts | 40 ++++++------ .../src/Overview/Address/NominatedBy.tsx | 62 +++++++++++++++---- .../src/Overview/Address/index.tsx | 15 ++++- .../page-staking/src/Overview/CurrentList.tsx | 13 ++-- 6 files changed, 90 insertions(+), 48 deletions(-) diff --git a/packages/page-staking/src/Actions/Account/ListNominees.tsx b/packages/page-staking/src/Actions/Account/ListNominees.tsx index 67280c7ff2..219756ebe6 100644 --- a/packages/page-staking/src/Actions/Account/ListNominees.tsx +++ b/packages/page-staking/src/Actions/Account/ListNominees.tsx @@ -2,8 +2,6 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { Slash } from '../types'; - import React from 'react'; import { AddressMini, Expander } from '@polkadot/react-components'; @@ -13,12 +11,11 @@ import useInactives from '../useInactives'; interface Props { nominating?: string[]; stashId: string; - slashes: Slash[]; } -function ListNominees ({ nominating, slashes, stashId }: Props): React.ReactElement { +function ListNominees ({ nominating, stashId }: Props): React.ReactElement { const { t } = useTranslation(); - const { nomsActive, nomsChilled, nomsInactive, nomsWaiting } = useInactives(stashId, slashes, nominating); + const { nomsActive, nomsChilled, nomsInactive, nomsWaiting } = useInactives(stashId, nominating); return ( <> diff --git a/packages/page-staking/src/Actions/Account/index.tsx b/packages/page-staking/src/Actions/Account/index.tsx index 35beecf89a..7c062971d0 100644 --- a/packages/page-staking/src/Actions/Account/index.tsx +++ b/packages/page-staking/src/Actions/Account/index.tsx @@ -202,7 +202,6 @@ function Account ({ allSlashes, className = '', info: { controllerId, destinatio {isStashNominating && ( )} diff --git a/packages/page-staking/src/Actions/useInactives.ts b/packages/page-staking/src/Actions/useInactives.ts index 5eb786b9fb..4d2e5de98d 100644 --- a/packages/page-staking/src/Actions/useInactives.ts +++ b/packages/page-staking/src/Actions/useInactives.ts @@ -3,8 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { DeriveSessionIndexes } from '@polkadot/api-derive/types'; -import { EraIndex, Exposure, Nominations } from '@polkadot/types/interfaces'; -import { Slash } from './types'; +import { EraIndex, Exposure, Nominations, SlashingSpans } from '@polkadot/types/interfaces'; import { useEffect, useState } from 'react'; import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks'; @@ -17,18 +16,10 @@ interface Inactives { nomsWaiting?: string[]; } -function extractState (stashId: string, slashes: Slash[], nominees: string[], activeEra: EraIndex, submittedIn: EraIndex, exposures: Exposure[]): Inactives { +function extractState (stashId: string, slashes: Option[], nominees: string[], activeEra: EraIndex, submittedIn: EraIndex, exposures: Exposure[]): Inactives { // chilled - const nomsChilled = nominees.filter((nominee) => - slashes.some(({ era, slashes }) => - submittedIn.lt(era) && ( - slashes - .filter(({ validator }) => validator.eq(nominee)) - .some(({ others }) => - others.some(([accountId]) => accountId.eq(stashId)) - ) - ) - ) + const nomsChilled = nominees.filter((_, index) => + slashes[index].isSome && slashes[index].unwrap().lastNonzeroSlash.gte(submittedIn) ); // first a blanket find of nominations not in the active set @@ -64,7 +55,7 @@ function extractState (stashId: string, slashes: Slash[], nominees: string[], ac }; } -export default function useInactives (stashId: string, slashes: Slash[], nominees?: string[]): Inactives { +export default function useInactives (stashId: string, nominees?: string[]): Inactives { const { api } = useApi(); const mountedRef = useIsMountedRef(); const [state, setState] = useState({}); @@ -76,12 +67,19 @@ export default function useInactives (stashId: string, slashes: Slash[], nominee if (mountedRef.current && nominees && nominees.length && indexes) { api .queryMulti( - [[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 => { + [[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]) + ) + .concat( + nominees.map((id) => [api.query.staking.slashingSpans, id]) + ), + ([optNominators, ...exposuresAndSpans]: [Option, ...(Exposure | Option)[]]): void => { + const exposures = exposuresAndSpans.slice(0, nominees.length) as Exposure[]; + const slashes = exposuresAndSpans.slice(nominees.length) as Option[]; + mountedRef.current && setState( extractState(stashId, slashes, nominees, indexes.activeEra, optNominators.unwrapOrDefault().submittedIn, exposures) ); @@ -95,7 +93,7 @@ export default function useInactives (stashId: string, slashes: Slash[], nominee return (): void => { unsub && unsub(); }; - }, [api, indexes, mountedRef, nominees, slashes, stashId]); + }, [api, indexes, mountedRef, nominees, stashId]); return state; } diff --git a/packages/page-staking/src/Overview/Address/NominatedBy.tsx b/packages/page-staking/src/Overview/Address/NominatedBy.tsx index 80de931a9f..1e7b2de243 100644 --- a/packages/page-staking/src/Overview/Address/NominatedBy.tsx +++ b/packages/page-staking/src/Overview/Address/NominatedBy.tsx @@ -2,32 +2,70 @@ // 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 { EraIndex, SlashingSpans } from '@polkadot/types/interfaces'; + +import React, { useEffect, useState } from 'react'; import { AddressMini, Expander, Spinner } from '@polkadot/react-components'; import { formatNumber } from '@polkadot/util'; import { useTranslation } from '../../translate'; interface Props { - nominators?: [string, number][]; + nominators?: [string, EraIndex, number][]; + slashingSpans?: SlashingSpans | null; } -function NominatedBy ({ nominators }: Props): React.ReactElement { +interface Chilled { + active: string[]; + chilled: string[]; +} + +function NominatedBy ({ nominators, slashingSpans }: Props): React.ReactElement { const { t } = useTranslation(); + const [{ active, chilled }, setChilled] = useState({ active: [], chilled: [] }); + + useEffect((): void => { + if (nominators) { + const chilled = slashingSpans + ? nominators + .filter(([, submittedIn]) => submittedIn.lte(slashingSpans.lastNonzeroSlash)) + .map(([who]) => who) + : []; + const active = nominators + .filter(([who]) => !chilled.includes(who)) + .map(([who]) => who); + + setChilled({ active, chilled }); + } + }, [nominators, slashingSpans]); return ( {nominators - ? (nominators.length !== 0 && ( - ('Nominations ({{count}})', { replace: { count: formatNumber(nominators.length) } })}> - {nominators.map(([who]): React.ReactNode => - + ? ( + <> + {active.length !== 0 && ( + ('Nominations ({{count}})', { replace: { count: formatNumber(active.length) } })}> + {active.map((who): React.ReactNode => + + )} + )} - - )) + {chilled.length !== 0 && ( + ('Chilled ({{count}})', { replace: { count: formatNumber(chilled.length) } })}> + {chilled.map((who): React.ReactNode => + + )} + + )} + + ) : } diff --git a/packages/page-staking/src/Overview/Address/index.tsx b/packages/page-staking/src/Overview/Address/index.tsx index bab35d00fd..2509813f83 100644 --- a/packages/page-staking/src/Overview/Address/index.tsx +++ b/packages/page-staking/src/Overview/Address/index.tsx @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { Balance } from '@polkadot/types/interfaces'; +import { Balance, EraIndex, SlashingSpans } from '@polkadot/types/interfaces'; import { DeriveAccountInfo, DeriveStakingQuery } from '@polkadot/api-derive/types'; import BN from 'bn.js'; @@ -11,6 +11,7 @@ import { AddressSmall, Icon } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; import { useApi, useCall } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; +import { Option } from '@polkadot/types'; import Favorite from './Favorite'; import NominatedBy from './NominatedBy'; @@ -26,7 +27,7 @@ interface Props { isFavorite: boolean; isMain?: boolean; lastBlock?: string; - nominatedBy?: [string, number][]; + nominatedBy?: [string, EraIndex, number][]; onlineCount?: false | number; onlineMessage?: boolean; points?: string; @@ -74,6 +75,9 @@ function Address ({ address, className = '', filterName, hasQueries, isElected, const { api } = useApi(); const accountInfo = useCall(api.derive.accounts.info, [address]); const stakingInfo = useCall(api.derive.staking.query, [address]); + const slashingSpans = useCall(!isMain && api.query.staking.slashingSpans, [address], { + transform: (opt: Option) => opt.unwrapOr(null) + }); const [{ commission, nominators, stakeOther, stakeOwn }, setStakingState] = useState({ nominators: [] }); const [isVisible, setIsVisible] = useState(true); @@ -123,7 +127,12 @@ function Address ({ address, className = '', filterName, hasQueries, isElected, stakeOther={stakeOther} /> ) - : + : ( + + ) } {stakeOwn?.gtn(0) && ( diff --git a/packages/page-staking/src/Overview/CurrentList.tsx b/packages/page-staking/src/Overview/CurrentList.tsx index 1a018685eb..0ea1025841 100644 --- a/packages/page-staking/src/Overview/CurrentList.tsx +++ b/packages/page-staking/src/Overview/CurrentList.tsx @@ -3,7 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types'; -import { AccountId, Nominations } from '@polkadot/types/interfaces'; +import { AccountId, EraIndex, Nominations } from '@polkadot/types/interfaces'; import { Authors } from '@polkadot/react-query/BlockAuthors'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; @@ -69,14 +69,15 @@ function getFiltered (stakingOverview: DeriveStakingOverview, favorites: string[ }; } -function extractNominators (nominations: [StorageKey, Option][]): Record { - return nominations.reduce((mapped: Record, [key, optNoms]) => { +function extractNominators (nominations: [StorageKey, Option][]): Record { + return nominations.reduce((mapped: Record, [key, optNoms]) => { if (optNoms.isSome) { const nominatorId = key.args[0].toString(); + const { submittedIn, targets } = optNoms.unwrap(); - optNoms.unwrap().targets.forEach((_validatorId, index): void => { + targets.forEach((_validatorId, index): void => { const validatorId = _validatorId.toString(); - const info: [string, number] = [nominatorId, index + 1]; + const info: [string, EraIndex, number] = [nominatorId, submittedIn, index + 1]; if (!mapped[validatorId]) { mapped[validatorId] = [info]; @@ -98,7 +99,7 @@ function CurrentList ({ favorites, hasQueries, isIntentions, next, stakingOvervi const nominators = useCall<[StorageKey, Option][]>(isIntentions && api.query.staking.nominators.entries as any, []); const [{ elected, validators, waiting }, setFiltered] = useState({}); const [nameFilter, setNameFilter] = useState(''); - const [nominatedBy, setNominatedBy] = useState | null>(); + const [nominatedBy, setNominatedBy] = useState | null>(); const [withIdentity, setWithIdentity] = useState(false); // we have a very large list, so we use a loading delay