diff --git a/packages/page-staking/src/Actions/index.tsx b/packages/page-staking/src/Actions/index.tsx index 3356a936ce..0b7cc087cd 100644 --- a/packages/page-staking/src/Actions/index.tsx +++ b/packages/page-staking/src/Actions/index.tsx @@ -35,6 +35,21 @@ interface State { foundStashes?: StakerState[]; } +function sortStashes (a: StakerState, b: StakerState): number { + return (a.isStashValidating ? 1 : (a.isStashNominating ? 5 : 99)) - (b.isStashValidating ? 1 : (b.isStashNominating ? 5 : 99)); +} + +function extractState (ownStashes: StakerState[]): State { + return { + bondedTotal: ownStashes.reduce((total: BN, { stakingLedger }) => + stakingLedger + ? total.add(stakingLedger.total.unwrap()) + : total, + BN_ZERO), + foundStashes: ownStashes.sort(sortStashes) + }; +} + function Actions ({ className = '', isInElection, next, ownStashes, targets, validators }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); @@ -44,16 +59,7 @@ function Actions ({ className = '', isInElection, next, ownStashes, targets, val const [{ bondedTotal, foundStashes }, setState] = useState({}); useEffect((): void => { - ownStashes && setState({ - bondedTotal: ownStashes.reduce((total: BN, { stakingLedger }) => - stakingLedger - ? total.add(stakingLedger.total.unwrap()) - : total, - BN_ZERO), - foundStashes: ownStashes.sort((a, b) => - (a.isStashValidating ? 1 : (a.isStashNominating ? 5 : 99)) - (b.isStashValidating ? 1 : (b.isStashNominating ? 5 : 99)) - ) - }); + ownStashes && setState(extractState(ownStashes)); }, [ownStashes]); const header = useMemo(() => [ diff --git a/packages/page-staking/src/Actions/partials/Nominate.tsx b/packages/page-staking/src/Actions/partials/Nominate.tsx index 3ed56ea890..b4fa21c518 100644 --- a/packages/page-staking/src/Actions/partials/Nominate.tsx +++ b/packages/page-staking/src/Actions/partials/Nominate.tsx @@ -31,9 +31,9 @@ interface Selected { } function autoPick (targets: SortedTargets): string[] { - return (targets.validators || []).reduce((result: string[], { key, numNominators }): string[] => { + return (targets.validators || []).reduce((result: string[], { key, numNominators, rewardPayout }): string[] => { if (result.length < MAX_NOMINATIONS) { - if (numNominators && (numNominators < MAX_PAYOUTS)) { + if (numNominators && (numNominators < MAX_PAYOUTS) && !rewardPayout.isZero()) { result.push(key); } } @@ -137,6 +137,7 @@ function Nominate ({ className = '', controllerId, next, nominating, onChange, s value={ selected.map((validatorId) => ( diff --git a/packages/page-staking/src/Overview/Address/index.tsx b/packages/page-staking/src/Overview/Address/index.tsx index caf6bcd3b4..a88c216067 100644 --- a/packages/page-staking/src/Overview/Address/index.tsx +++ b/packages/page-staking/src/Overview/Address/index.tsx @@ -32,8 +32,8 @@ interface Props { onlineCount?: false | number; onlineMessage?: boolean; points?: string; - setNominators?: false | ((nominators: string[]) => void); toggleFavorite: (accountId: string) => void; + withoutName: boolean; } interface StakingState { @@ -44,9 +44,7 @@ interface StakingState { stakeOwn?: BN; } -/* stylelint-disable */ const PERBILL_PERCENT = 10_000_000; -/* stylelint-enable */ function expandInfo ({ exposure, validatorPrefs }: DeriveStakingQuery): StakingState { let nominators: [string, Balance][] = []; @@ -74,15 +72,15 @@ function expandInfo ({ exposure, validatorPrefs }: DeriveStakingQuery): StakingS }; } -function checkVisibility (api: ApiPromise, address: string, filterName: string, accountInfo?: DeriveAccountInfo): boolean { +function checkVisibility (api: ApiPromise, address: string, filterName: string, withoutName: boolean, accountInfo?: DeriveAccountInfo): boolean { let isVisible = false; const filterLower = filterName.toLowerCase(); - if (filterLower) { + if (filterLower || !withoutName) { if (accountInfo) { const { accountId, accountIndex, identity, nickname } = accountInfo; - if (accountId?.toString().includes(filterName) || accountIndex?.toString().includes(filterName)) { + if (withoutName && (accountId?.toString().includes(filterName) || accountIndex?.toString().includes(filterName))) { isVisible = true; } else if (api.query.identity && api.query.identity.identityOf) { isVisible = (!!identity?.display && identity.display.toLowerCase().includes(filterLower)) || @@ -106,7 +104,7 @@ function checkVisibility (api: ApiPromise, address: string, filterName: string, return isVisible; } -function Address ({ address, className = '', filterName, hasQueries, isAuthor, isElected, isFavorite, isMain, lastBlock, nominatedBy, onlineCount, onlineMessage, points, setNominators, toggleFavorite }: Props): React.ReactElement | null { +function Address ({ address, className = '', filterName, hasQueries, isAuthor, isElected, isFavorite, isMain, lastBlock, nominatedBy, onlineCount, onlineMessage, points, toggleFavorite, withoutName }: Props): React.ReactElement | null { const { api } = useApi(); const { allAccounts } = useAccounts(); const accountInfo = useCall(api.derive.accounts.info, [address]); @@ -116,19 +114,14 @@ function Address ({ address, className = '', filterName, hasQueries, isAuthor, i const [isNominating, setIsNominating] = useState(false); useEffect((): void => { - if (stakingInfo) { - const info = expandInfo(stakingInfo); - - setNominators && setNominators(info.nominators.map(([who]): string => who.toString())); - setStakingState(info); - } - }, [setNominators, stakingInfo]); + stakingInfo && setStakingState(expandInfo(stakingInfo)); + }, [stakingInfo]); useEffect((): void => { setIsVisible( - checkVisibility(api, address, filterName, accountInfo) + checkVisibility(api, address, filterName, withoutName, accountInfo) ); - }, [api, accountInfo, address, filterName]); + }, [api, accountInfo, address, filterName, withoutName]); useEffect((): void => { !isMain && setIsNominating( @@ -144,8 +137,12 @@ function Address ({ address, className = '', filterName, hasQueries, isAuthor, i [address] ); + if (!isVisible) { + return null; + } + return ( - + ][]): R }, {}); } -function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOverview }: Props): React.ReactElement | null { +function CurrentList ({ hasQueries, isIntentions, next, stakingOverview }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); const { byAuthor, eraPoints, lastBlockAuthors } = useContext(isIntentions ? EmptyAuthorsContext : BlockAuthorsContext); @@ -98,6 +98,7 @@ function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOv const [{ elected, validators, waiting }, setFiltered] = useState({}); const [nameFilter, setNameFilter] = useState(''); const [nominatedBy, setNominatedBy] = useState | null>(); + const [withoutName, setWithoutName] = useState(true); useEffect((): void => { stakingOverview && setFiltered( @@ -129,14 +130,30 @@ function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOv ], [t]); const filter = useMemo(() => ( - ('filter by name, address or index')} - onChange={setNameFilter} - value={nameFilter} - /> - ), [nameFilter, t]); +
+ ('filter by name, address or index')} + onChange={setNameFilter} + value={nameFilter} + /> +
+ {api.query.identity && ( + ('with/without identity') + : t('only with an identity') + } + onChange={setWithoutName} + value={withoutName} + /> + )} +
+
+ ), [api, nameFilter, t, withoutName]); const _renderRows = useCallback( (addresses?: AccountExtend[], isMain?: boolean): React.ReactNode[] => @@ -155,11 +172,11 @@ function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOv onlineCount={recentlyOnline?.[address]?.blockCount.toNumber()} onlineMessage={recentlyOnline?.[address]?.hasMessage} points={eraPoints[address]} - setNominators={setNominators} toggleFavorite={toggleFavorite} + withoutName={withoutName} /> )), - [byAuthor, eraPoints, hasQueries, lastBlockAuthors, nameFilter, nominatedBy, recentlyOnline, setNominators, toggleFavorite] + [byAuthor, eraPoints, hasQueries, lastBlockAuthors, nameFilter, nominatedBy, recentlyOnline, toggleFavorite, withoutName] ); return isIntentions diff --git a/packages/page-staking/src/Overview/Summary.tsx b/packages/page-staking/src/Overview/Summary.tsx index 8657b0ba51..9ed623d96e 100644 --- a/packages/page-staking/src/Overview/Summary.tsx +++ b/packages/page-staking/src/Overview/Summary.tsx @@ -16,7 +16,7 @@ interface Props { className?: string; isVisible: boolean; next?: string[]; - nominators: string[]; + nominators?: string[]; stakingOverview?: DeriveStakingOverview; } @@ -37,7 +37,7 @@ function Summary ({ className = '', isVisible, next, nominators, stakingOverview {next.length} )} - {!!nominators.length && ( + {!!nominators?.length && ( ('nominators')}> {nominators.length} diff --git a/packages/page-staking/src/Overview/index.tsx b/packages/page-staking/src/Overview/index.tsx index e17b4509e1..be7c318993 100644 --- a/packages/page-staking/src/Overview/index.tsx +++ b/packages/page-staking/src/Overview/index.tsx @@ -13,18 +13,16 @@ interface Props extends BareProps { hasQueries: boolean; isIntentions?: boolean; next?: string[]; - setNominators?: (nominators: string[]) => void; stakingOverview?: DeriveStakingOverview; } -function Overview ({ className = '', hasQueries, isIntentions, next, setNominators, stakingOverview }: Props): React.ReactElement { +function Overview ({ className = '', hasQueries, isIntentions, next, stakingOverview }: Props): React.ReactElement { return (
diff --git a/packages/page-staking/src/Targets/Validator.tsx b/packages/page-staking/src/Targets/Validator.tsx index cf973a8084..3c0a5c3948 100644 --- a/packages/page-staking/src/Targets/Validator.tsx +++ b/packages/page-staking/src/Targets/Validator.tsx @@ -2,12 +2,16 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { DeriveAccountInfo } from '@polkadot/api-derive/types'; import { ValidatorInfo } from '../types'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { ApiPromise } from '@polkadot/api'; import { AddressSmall, Icon, Toggle } from '@polkadot/react-components'; import { FormatBalance } from '@polkadot/react-query'; +import { useApi, useCall } from '@polkadot/react-hooks'; import { formatNumber } from '@polkadot/util'; +import keyring from '@polkadot/ui-keyring'; import MaxBadge from '../MaxBadge'; import Favorite from '../Overview/Address/Favorite'; @@ -18,21 +22,61 @@ interface Props { isSelected: boolean; toggleFavorite: (accountId: string) => void; toggleSelected: (accountId: string) => void; + withoutName: boolean; } -function Validator ({ canSelect, info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment }, isSelected, toggleFavorite, toggleSelected }: Props): React.ReactElement { +function checkVisibility (api: ApiPromise, accountInfo: DeriveAccountInfo): boolean { + let isVisible = false; + + const { accountId, identity, nickname } = accountInfo; + + if (api.query.identity && api.query.identity.identityOf) { + isVisible = !!(identity?.display && identity.display.toString()); + } else if (nickname) { + isVisible = !!nickname.toString(); + } + + if (!isVisible && accountId) { + const account = keyring.getAddress(accountId.toString()); + + isVisible = !!account?.meta?.name; + } + + return isVisible; +} + +function Validator ({ canSelect, info, isSelected, toggleFavorite, toggleSelected, withoutName }: Props): React.ReactElement | null { + const { api } = useApi(); + const accountInfo = useCall(api.derive.accounts.info, [info.accountId]); + const [hasName, setHasName] = useState(true); + + useEffect((): void => { + if (accountInfo) { + const hasIdentity = checkVisibility(api, accountInfo); + + info.hasIdentity = hasIdentity; + setHasName(hasIdentity); + } + }, [accountInfo, api, info]); + const _onQueryStats = useCallback( (): void => { - window.location.hash = `/staking/query/${key}`; + window.location.hash = `/staking/query/${info.key}`; }, - [key] + [info.key] ); const _toggleSelected = useCallback( - () => toggleSelected(key), - [key, toggleSelected] + () => toggleSelected(info.key), + [info.key, toggleSelected] ); + if (!hasName && !withoutName) { + return null; + } + + const { accountId, bondOther, bondOwn, bondTotal, commissionPer, isCommission, isFavorite, isNominating, key, numNominators, rankOverall, rewardPayout, validatorPayment } = info; + return ( + {numNominators || ''} { isCommission @@ -62,7 +107,7 @@ function Validator ({ canSelect, info: { accountId, bondOther, bondOwn, bondTota value={bondOther} /> - + {!rewardPayout.isZero() && } {(canSelect || isSelected) && ( { const { t } = useTranslation(); + const { api } = useApi(); const ownNominators = useOwnNominators(ownStashes); const [selected, setSelected] = useState([]); const [sorted, setSorted] = useState(); + const [withoutName, setWithoutName] = useState(true); const [{ sortBy, sortFromMax }, setSortBy] = useState({ sortBy: 'rankOverall', sortFromMax: true }); useEffect((): void => { @@ -75,11 +81,15 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward, const _selectProfitable = useCallback( () => setSelected( - (validators || []) - .filter((_, index) => index < MAX_NOMINATIONS) - .map(({ key }) => key) + (validators || []).reduce((result: string[], { hasIdentity, key, rewardPayout }): string[] => { + if ((result.length < MAX_NOMINATIONS) && (withoutName || hasIdentity) && !rewardPayout.isZero()) { + result.push(key); + } + + return result; + }, []) ), - [validators] + [validators, withoutName] ); const labels = useMemo( @@ -87,15 +97,16 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward, rankBondOther: t('other stake'), rankBondOwn: t('own stake'), rankBondTotal: t('total stake'), - rankComm: t('commission'), - rankOverall: t('profit/era est') + rankComm: t('comm.'), + rankNumNominators: t('nominators'), + rankOverall: t('profit/era') }), [t] ); const header = useMemo(() => [ [t('validators'), 'start', 4], - ...['rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'].map((header) => [ + ...['rankNumNominators', 'rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'].map((header) => [ <>{labels[header]}, sorted ? `isClickable ${sortBy === header ? 'ui--highlight--border' : ''} number` : 'number', 1, @@ -106,17 +117,33 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward, const filter = useMemo(() => ( sorted && ( - ('The amount that will be used on a per-validator basis to calculate profits for that validator.')} - isFull - isZeroable={false} - label={t('amount to use for estimation')} - onChange={setCalcWith} - value={calcWith} - /> +
+ ('The amount that will be used on a per-validator basis to calculate profits for that validator.')} + isFull + isZeroable={false} + label={t('amount to use for estimation')} + onChange={setCalcWith} + value={calcWith} + /> +
+ {api.query.identity && ( + ('with/without identity') + : t('only with an identity') + } + onChange={setWithoutName} + value={withoutName} + /> + )} +
+
) - ), [calcWith, setCalcWith, sorted, t]); + ), [api, calcWith, setCalcWith, sorted, t, withoutName]); return (
@@ -151,6 +178,7 @@ function Targets ({ className = '', ownStashes, targets: { calcWith, lastReward, key={validators[index].key} toggleFavorite={toggleFavorite} toggleSelected={_toggleSelected} + withoutName={withoutName} /> )} @@ -166,6 +194,7 @@ export default React.memo(styled(Targets)` margin-left: 0.5rem; } } + .ui--Table { overflow-x: auto; } diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index 19c0719ff9..bf2f64d0ad 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -6,7 +6,7 @@ import { DeriveStakingOverview } from '@polkadot/api-derive/types'; import { AppProps as Props } from '@polkadot/react-components/types'; import { ElectionStatus } from '@polkadot/types/interfaces'; -import React, { useEffect, useMemo, useReducer, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Route, Switch } from 'react-router'; import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; @@ -30,10 +30,6 @@ interface Validators { validators?: string[]; } -function reduceNominators (nominators: string[], additional: string[]): string[] { - return nominators.concat(...additional.filter((nominator): boolean => !nominators.includes(nominator))); -} - function StakingApp ({ basePath, className = '' }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); @@ -47,7 +43,6 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement(api.query.staking?.eraElectionStatus, [], { transform: (status: ElectionStatus) => status.isOpen }); - const [nominators, dispatchNominators] = useReducer(reduceNominators, [] as string[]); const hasQueries = useMemo( (): boolean => hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra), @@ -114,7 +109,7 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement @@ -126,7 +121,9 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement @@ -152,7 +149,6 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement @@ -175,4 +171,13 @@ export default React.memo(styled(StakingApp)` margin: 2.5rem auto; } } + + .staking--optionsBar { + text-align: right; + + .staking--buttonToggle { + margin-left: 1.5rem; + margin-top: 0.5rem; + } + } `); diff --git a/packages/page-staking/src/types.ts b/packages/page-staking/src/types.ts index 6c95b9d39f..c2b9d7e9a8 100644 --- a/packages/page-staking/src/types.ts +++ b/packages/page-staking/src/types.ts @@ -33,6 +33,7 @@ interface ValidatorInfoRank { rankBondOwn: number; rankBondTotal: number; rankComm: number; + rankNumNominators: number; rankOverall: number; rankPayment: number; rankReward: number; @@ -45,6 +46,7 @@ export interface ValidatorInfo extends ValidatorInfoRank { bondShare: number; bondTotal: Balance; commissionPer: number; + hasIdentity: boolean; isCommission: boolean; isFavorite: boolean; isNominating: boolean; diff --git a/packages/page-staking/src/useSortedTargets.ts b/packages/page-staking/src/useSortedTargets.ts index 202b5afd60..ac928677ed 100644 --- a/packages/page-staking/src/useSortedTargets.ts +++ b/packages/page-staking/src/useSortedTargets.ts @@ -44,6 +44,8 @@ function sortValidators (list: ValidatorInfo[]): ValidatorInfo[] { .map(mapIndex('rankPayment')) .sort((a, b) => a.rewardSplit.cmp(b.rewardSplit)) .map(mapIndex('rankReward')) + .sort((a, b) => b.numNominators - a.numNominators) + .map(mapIndex('rankNumNominators')) .sort((a, b): number => { const cmp = b.rewardPayout.cmp(a.rewardPayout); @@ -106,6 +108,7 @@ function extractInfo (allAccounts: string[], amount: BN = baseBalance(), elected bondShare: 0, bondTotal, commissionPer: (((prefs as ValidatorPrefs).commission?.unwrap() || BN_ZERO).toNumber() / 10_000_000), + hasIdentity: false, isCommission: !!(prefs as ValidatorPrefs).commission, isFavorite: favorites.includes(key), isNominating, @@ -115,6 +118,7 @@ function extractInfo (allAccounts: string[], amount: BN = baseBalance(), elected rankBondOwn: 0, rankBondTotal: 0, rankComm: 0, + rankNumNominators: 0, rankOverall: 0, rankPayment: 0, rankReward: 0, diff --git a/packages/react-components/src/AddressMini.tsx b/packages/react-components/src/AddressMini.tsx index 4b4bfc964e..687716dec4 100644 --- a/packages/react-components/src/AddressMini.tsx +++ b/packages/react-components/src/AddressMini.tsx @@ -22,6 +22,7 @@ interface Props extends BareProps { bonded?: BN | BN[]; children?: React.ReactNode; iconInfo?: React.ReactNode; + isHighlight?: boolean; isPadded?: boolean; isShort?: boolean; label?: React.ReactNode; @@ -39,13 +40,13 @@ interface Props extends BareProps { withShrink?: boolean; } -function AddressMini ({ balance, bonded, children, className = '', iconInfo, isPadded = true, label, labelBalance, noLookup, summary, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false, withSidebar = true }: Props): React.ReactElement | null { +function AddressMini ({ balance, bonded, children, className = '', iconInfo, isHighlight, isPadded = true, label, labelBalance, noLookup, summary, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false, withSidebar = true }: Props): React.ReactElement | null { if (!value) { return null; } return ( -
+
{label && ( )}