diff --git a/packages/page-staking/src/Targets/Validator.tsx b/packages/page-staking/src/Targets/Validator.tsx index 5c810c4119..838cd22740 100644 --- a/packages/page-staking/src/Targets/Validator.tsx +++ b/packages/page-staking/src/Targets/Validator.tsx @@ -33,42 +33,40 @@ function queryAddress (address: string): void { window.location.hash = `/staking/query/${address}`; } -function Validator ({ allSlashes, canSelect, filterName, info, isNominated, isSelected, nominatedBy = [], toggleFavorite, toggleSelected }: Props): React.ReactElement | null { +function Validator ({ allSlashes, canSelect, filterName, info: { accountId, bondOther, bondOwn, bondTotal, commissionPer, isBlocking, isElected, isFavorite, key, lastPayout, numNominators, rankOverall, stakedReturnCmp }, isNominated, isSelected, nominatedBy = [], toggleFavorite, toggleSelected }: Props): React.ReactElement | null { const { t } = useTranslation(); const { api } = useApi(); - const accountInfo = useDeriveAccountInfo(info.accountId); - const [,, time] = useBlockTime(info.lastPayout); + const accountInfo = useDeriveAccountInfo(accountId); + const [,, time] = useBlockTime(lastPayout); const isVisible = useMemo( () => accountInfo - ? checkVisibility(api, info.key, accountInfo, filterName) + ? checkVisibility(api, key, accountInfo, filterName) : true, - [accountInfo, api, filterName, info] + [accountInfo, api, filterName, key] ); const slashes = useMemo( () => (allSlashes || []) - .map(([era, all]) => ({ era, slashes: all.filter(({ validator }) => validator.eq(info.accountId)) })) + .map(([era, all]) => ({ era, slashes: all.filter(({ validator }) => validator.eq(accountId)) })) .filter(({ slashes }) => slashes.length), - [allSlashes, info] + [allSlashes, accountId] ); const _onQueryStats = useCallback( - () => queryAddress(info.key), - [info.key] + () => queryAddress(key), + [key] ); const _toggleSelected = useCallback( - () => toggleSelected(info.key), - [info.key, toggleSelected] + () => toggleSelected(key), + [key, toggleSelected] ); if (!isVisible) { return null; } - const { accountId, bondOther, bondOwn, bondTotal, commissionPer, isBlocking, isElected, isFavorite, key, lastPayout, numNominators, rankOverall, stakedReturnCmp } = info; - return ( diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index c369e70df3..2f90ff20ca 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -62,7 +62,7 @@ const CLASSES: Record = { rankBondOwn: 'media--900' }; const MAX_CAP_PERCENT = 100; // 75 if only using numNominators -const MAX_COMM_PERCENT = 20; // -1 for median +const MAX_COMM_PERCENT = 10; // -1 for median const MAX_DAYS = 7; const SORT_KEYS = ['rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall']; @@ -95,7 +95,7 @@ function applyFilter (validators: ValidatorInfo[], medianComm: number, allIdenti (!withPayout || !isBabe || (!!lastPayout && daysPayout.gte(lastPayout))) && (!withoutComm || ( MAX_COMM_PERCENT > 0 - ? (commissionPer < MAX_COMM_PERCENT) + ? (commissionPer <= MAX_COMM_PERCENT) : (!medianComm || (commissionPer <= medianComm))) ) && (!withoutOver || !maxPaid || maxPaid.muln(MAX_CAP_PERCENT).div(BN_HUNDRED).gten(nomCount)) @@ -143,13 +143,15 @@ function sort (sortBy: TargetSortBy, sortFromMax: boolean, validators: Validator // Use slice to create new array, so that sorting triggers component render return validators .slice(0) - .sort((a, b) => sortFromMax - ? a[sortBy] - b[sortBy] - : b[sortBy] - a[sortBy] + .sort((a, b) => + sortFromMax + ? a[sortBy] - b[sortBy] + : b[sortBy] - a[sortBy] ) - .sort((a, b) => a.isFavorite === b.isFavorite - ? 0 - : (a.isFavorite ? -1 : 1) + .sort((a, b) => + a.isFavorite === b.isFavorite + ? 0 + : (a.isFavorite ? -1 : 1) ); } @@ -319,8 +321,8 @@ function Targets ({ className = '', isInElection, nominatedBy, ownStashes, targe className='staking--buttonToggle' label={ MAX_COMM_PERCENT > 0 - ? t('comm. < {{maxComm}}%', { replace: { maxComm: MAX_COMM_PERCENT } }) - : t('comm. < median') + ? t('comm. <= {{maxComm}}%', { replace: { maxComm: MAX_COMM_PERCENT } }) + : t('comm. <= median') } onChange={setToggle.withoutComm} value={toggles.withoutComm} @@ -357,6 +359,7 @@ function Targets ({ className = '', isInElection, nominatedBy, ownStashes, targe const displayList = isQueryFiltered ? validators : sorted; + const canSelect = selected.length < maxNominations; return (
@@ -402,7 +405,7 @@ function Targets ({ className = '', isInElection, nominatedBy, ownStashes, targe {displayList && displayList.map((info): React.ReactNode => ownStashes && ownStashes.filter(({ isOwnController, isStashValidating }) => isOwnController && !isStashValidating), + () => ownStashes && ownStashes.filter(({ isOwnController, isStashValidating }) => + isOwnController && + !isStashValidating + ), [ownStashes] ); } diff --git a/packages/page-staking/src/Validators/Address/NominatedBy.tsx b/packages/page-staking/src/Validators/Address/NominatedBy.tsx index 04a15e97dc..670c08af0e 100644 --- a/packages/page-staking/src/Validators/Address/NominatedBy.tsx +++ b/packages/page-staking/src/Validators/Address/NominatedBy.tsx @@ -25,10 +25,10 @@ function extractFunction (all: string[]): null | [number, () => React.ReactNode[ return all.length ? [ all.length, - () => all.map((who): React.ReactNode => + () => all.map((value): React.ReactNode => ) ] @@ -38,7 +38,10 @@ function extractFunction (all: string[]): null | [number, () => React.ReactNode[ function extractChilled (nominators: NominatedByType[] = [], slashingSpans?: SlashingSpans | null): Chilled { const chilled = slashingSpans ? nominators - .filter(({ submittedIn }) => !slashingSpans.lastNonzeroSlash.isZero() && slashingSpans.lastNonzeroSlash.gte(submittedIn)) + .filter(({ submittedIn }) => + !slashingSpans.lastNonzeroSlash.isZero() && + slashingSpans.lastNonzeroSlash.gte(submittedIn) + ) .map(({ nominatorId }) => nominatorId) : []; diff --git a/packages/page-staking/src/Validators/Address/StakeOther.tsx b/packages/page-staking/src/Validators/Address/StakeOther.tsx index e1056c54cc..d2dac654e2 100644 --- a/packages/page-staking/src/Validators/Address/StakeOther.tsx +++ b/packages/page-staking/src/Validators/Address/StakeOther.tsx @@ -31,6 +31,16 @@ function extractFunction (all: NominatorValue[]): null | [number, () => React.Re : null; } +function sumValue (all: { value: BN }[]): BN { + const total = new BN(0); + + for (let i = 0; i < all.length; i++) { + total.iadd(all[i].value); + } + + return total; +} + function extractTotals (maxPaid: BN | undefined, nominators: NominatorValue[], stakeOther?: BN): [null | [number, () => React.ReactNode[]], BN, null | [number, () => React.ReactNode[]], BN] { const sorted = nominators.sort((a, b) => b.value.cmp(a.value)); @@ -40,9 +50,9 @@ function extractTotals (maxPaid: BN | undefined, nominators: NominatorValue[], s const max = maxPaid.toNumber(); const rewarded = sorted.slice(0, max); - const rewardedTotal = rewarded.reduce((total, { value }) => total.iadd(value), new BN(0)); + const rewardedTotal = sumValue(rewarded); const unrewarded = sorted.slice(max); - const unrewardedTotal = unrewarded.reduce((total, { value }) => total.iadd(value), new BN(0)); + const unrewardedTotal = sumValue(unrewarded); return [extractFunction(rewarded), rewardedTotal, extractFunction(unrewarded), unrewardedTotal]; } diff --git a/packages/page-staking/src/Validators/Address/index.tsx b/packages/page-staking/src/Validators/Address/index.tsx index c876d1a218..e7425769a5 100644 --- a/packages/page-staking/src/Validators/Address/index.tsx +++ b/packages/page-staking/src/Validators/Address/index.tsx @@ -57,7 +57,10 @@ function expandInfo ({ exposure, validatorPrefs }: ValidatorInfo, minCommission? let stakeOwn: BN | undefined; if (exposure && exposure.total) { - nominators = exposure.others.map(({ value, who }) => ({ nominatorId: who.toString(), value: value.unwrap() })); + nominators = exposure.others.map(({ value, who }) => ({ + nominatorId: who.toString(), + value: value.unwrap() + })); stakeTotal = exposure.total?.unwrap() || BN_ZERO; stakeOwn = exposure.own.unwrap(); stakeOther = stakeTotal.sub(stakeOwn); diff --git a/packages/page-staking/src/Validators/CurrentList.tsx b/packages/page-staking/src/Validators/CurrentList.tsx index 3d7a4ef5fb..422f5a3441 100644 --- a/packages/page-staking/src/Validators/CurrentList.tsx +++ b/packages/page-staking/src/Validators/CurrentList.tsx @@ -71,7 +71,13 @@ function filterAccounts (isOwn: boolean, accounts: string[] = [], ownStashIds: s } function accountsToString (accounts: AccountId[]): string[] { - return accounts.map((a) => a.toString()); + const result = new Array(accounts.length); + + for (let i = 0; i < accounts.length; i++) { + result[i] = accounts[i].toString(); + } + + return result; } function getFiltered (isOwn: boolean, stakingOverview: DeriveStakingOverview | undefined, favorites: string[], next?: string[], ownStashIds?: string[]): Filtered { @@ -90,6 +96,18 @@ function getFiltered (isOwn: boolean, stakingOverview: DeriveStakingOverview | u }; } +function mapValidators (infos: ValidatorInfo[]): Record { + const result: Record = {}; + + for (let i = 0; i < infos.length; i++) { + const info = infos[i]; + + result[info.key] = info; + } + + return result; +} + const DEFAULT_PARAS = {}; function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, minCommission, nominatedBy, ownStashIds, paraValidators = DEFAULT_PARAS, recentlyOnline, stakingOverview, targets, toggleFavorite }: Props): React.ReactElement | null { @@ -116,11 +134,7 @@ function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, m ); const infoMap = useMemo( - () => targets.validators && targets.validators.reduce>((result, info) => { - result[info.key] = info; - - return result; - }, {}), + () => targets.validators && mapValidators(targets.validators), [targets] ); diff --git a/packages/page-staking/src/useIdentities.ts b/packages/page-staking/src/useIdentities.ts index 96d388a082..70b1cead4b 100644 --- a/packages/page-staking/src/useIdentities.ts +++ b/packages/page-staking/src/useIdentities.ts @@ -9,11 +9,13 @@ type Result = Record; const OPT_CALL = { transform: ([[validatorIds], hasIdentities]: [[string[]], DeriveHasIdentity[]]): Record => { - return validatorIds.reduce((result: Record, validatorId, index): Record => { - result[validatorId] = hasIdentities[index]; + const result: Record = {}; - return result; - }, {}); + for (let i = 0; i < validatorIds.length; i++) { + result[validatorIds[i]] = hasIdentities[i]; + } + + return result; }, withParamsTransform: true }; diff --git a/packages/page-staking/src/useNominations.ts b/packages/page-staking/src/useNominations.ts index 87862a46b2..f7588fbaf5 100644 --- a/packages/page-staking/src/useNominations.ts +++ b/packages/page-staking/src/useNominations.ts @@ -10,25 +10,28 @@ import { useMemo } from 'react'; import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks'; function extractNominators (nominations: [StorageKey, Option][]): NominatedByMap { - return nominations.reduce((mapped: NominatedByMap, [key, optNoms]) => { + const mapped: NominatedByMap = {}; + + for (let i = 0; i < nominations.length; i++) { + const [key, optNoms] = nominations[i]; + if (optNoms.isSome && key.args.length) { const nominatorId = key.args[0].toString(); const { submittedIn, targets } = optNoms.unwrap(); - targets.forEach((_validatorId, index): void => { - const validatorId = _validatorId.toString(); - const info = { index: index + 1, nominatorId, submittedIn }; + for (let j = 0; j < targets.length; j++) { + const validatorId = targets[j].toString(); if (!mapped[validatorId]) { - mapped[validatorId] = [info]; - } else { - mapped[validatorId].push(info); + mapped[validatorId] = []; } - }); - } - return mapped; - }, {}); + mapped[validatorId].push({ index: j + 1, nominatorId, submittedIn }); + } + } + } + + return mapped; } function useNominationsImpl (isActive = true): NominatedByMap | undefined { diff --git a/packages/page-staking/src/useOwnPools.ts b/packages/page-staking/src/useOwnPools.ts index 6394bbd43b..015e257786 100644 --- a/packages/page-staking/src/useOwnPools.ts +++ b/packages/page-staking/src/useOwnPools.ts @@ -12,10 +12,12 @@ import { createNamedHook, useAccounts, useApi, useCall } from '@polkadot/react-h import { createAccounts } from './usePoolAccounts'; const OPT_MULTI = { - transform: ([[ids], opts]: [[string[]], Option[]]): OwnPoolBase[] => - ids.reduce((pools: OwnPoolBase[], accountId, index): OwnPoolBase[] => { - if (opts[index].isSome) { - const member = opts[index].unwrap(); + transform: ([[ids], opts]: [[string[]], Option[]]): OwnPoolBase[] => { + const pools: OwnPoolBase[] = []; + + for (let i = 0; i < ids.length; i++) { + if (opts[i].isSome) { + const member = opts[i].unwrap(); let entry = pools.find(({ poolId }) => poolId.eq(member.poolId)); if (!entry) { @@ -24,11 +26,12 @@ const OPT_MULTI = { pools.push(entry); } - entry.members[accountId] = member; + entry.members[ids[i]] = member; } + } - return pools; - }, []), + return pools; + }, withParamsTransform: true }; diff --git a/packages/page-staking/src/useSortedTargets.ts b/packages/page-staking/src/useSortedTargets.ts index 697d6a5077..f3b3d68eb9 100644 --- a/packages/page-staking/src/useSortedTargets.ts +++ b/packages/page-staking/src/useSortedTargets.ts @@ -34,6 +34,35 @@ const EMPTY_PARTIAL: Partial = {}; const DEFAULT_FLAGS_ELECTED = { withController: true, withExposure: true, withPrefs: true }; const DEFAULT_FLAGS_WAITING = { withController: true, withPrefs: true }; +const OPT_ERA = { + transform: ({ activeEra, eraLength, sessionLength }: DeriveSessionInfo): LastEra => ({ + activeEra, + eraLength, + lastEra: activeEra.isZero() + ? BN_ZERO + : activeEra.sub(BN_ONE), + sessionLength + }) +}; + +const OPT_MULTI = { + defaultValue: {}, + transform: ([historyDepth, counterForNominators, counterForValidators, optMaxNominatorsCount, optMaxValidatorsCount, minNominatorBond, minValidatorBond, totalIssuance]: [BN, BN?, BN?, Option?, Option?, BN?, BN?, BN?]): MultiResult => ({ + counterForNominators, + counterForValidators, + historyDepth, + maxNominatorsCount: optMaxNominatorsCount && optMaxNominatorsCount.isSome + ? optMaxNominatorsCount.unwrap() + : undefined, + maxValidatorsCount: optMaxValidatorsCount && optMaxValidatorsCount.isSome + ? optMaxValidatorsCount.unwrap() + : undefined, + minNominatorBond, + minValidatorBond, + totalIssuance + }) +}; + function mapIndex (mapBy: TargetSortBy): (info: ValidatorInfo, index: number) => ValidatorInfo { return (info, index): ValidatorInfo => { info[mapBy] = index + 1; @@ -50,19 +79,17 @@ function sortValidators (list: ValidatorInfo[]): ValidatorInfo[] { const existing: string[] = []; return list - .filter((a): boolean => { - const s = a.accountId.toString(); + .filter(({ accountId }): boolean => { + const key = accountId.toString(); - if (!existing.includes(s)) { - existing.push(s); + if (existing.includes(key)) { + return false; + } else { + existing.push(key); return true; } - - return false; }) - // .filter((a) => a.bondTotal.gtn(0)) - // ignored, not used atm // .sort((a, b) => b.commissionPer - a.commissionPer) // .map(mapIndex('rankComm')) .sort((a, b) => b.bondOther.cmp(a.bondOther)) @@ -75,7 +102,6 @@ function sortValidators (list: ValidatorInfo[]): ValidatorInfo[] { // .map(mapIndex('rankPayment')) .sort((a, b) => a.stakedReturnCmp - b.stakedReturnCmp) .map(mapIndex('rankReward')) - // ignored, not used atm // .sort((a, b) => b.numNominators - a.numNominators) // .map(mapIndex('rankNumNominators')) .sort((a, b) => @@ -95,12 +121,22 @@ function extractSingle (api: ApiPromise, allAccounts: string[], derive: DeriveSt const nominators: Record = {}; const emptyExposure = api.createType('Exposure'); const earliestEra = historyDepth && lastEra.sub(historyDepth).iadd(BN_ONE); - const list = derive.info.map(({ accountId, exposure = emptyExposure, stakingLedger, validatorPrefs }): ValidatorInfo => { + const list = new Array(derive.info.length); + + for (let i = 0; i < derive.info.length; i++) { + const { accountId, exposure = emptyExposure, stakingLedger, validatorPrefs } = derive.info[i]; + // some overrides (e.g. Darwinia Crab) does not have the own/total field in Exposure let [bondOwn, bondTotal] = exposure.total ? [exposure.own.unwrap(), exposure.total.unwrap()] : [BN_ZERO, BN_ZERO]; + + if (bondTotal.isZero()) { + bondTotal = bondOwn = stakingLedger.total?.unwrap() || BN_ZERO; + } + const skipRewards = bondTotal.isZero(); + // some overrides (e.g. Darwinia Crab) does not have the value field in IndividualExposure const minNominated = (exposure.others || []).reduce((min: BN, { value = api.createType('Compact') }): BN => { const actual = value.unwrap(); @@ -110,25 +146,12 @@ function extractSingle (api: ApiPromise, allAccounts: string[], derive: DeriveSt : min; }, BN_ZERO); - if (bondTotal.isZero()) { - bondTotal = bondOwn = stakingLedger.total?.unwrap() || BN_ZERO; - } - const key = accountId.toString(); const lastEraPayout = !lastEra.isZero() ? stakingLedger.claimedRewards[stakingLedger.claimedRewards.length - 1] : undefined; - // only use if it is more recent than historyDepth - let lastPayout: BN | undefined = earliestEra && lastEraPayout && lastEraPayout.gt(earliestEra) - ? lastEraPayout - : undefined; - - if (lastPayout && !sessionLength.eq(BN_ONE)) { - lastPayout = lastEra.sub(lastPayout).mul(eraLength); - } - - return { + list[i] = { accountId, bondOther: bondTotal.sub(bondOwn), bondOwn, @@ -149,7 +172,10 @@ function extractSingle (api: ApiPromise, allAccounts: string[], derive: DeriveSt }, allAccounts.includes(key)), key, knownLength: activeEra.sub(stakingLedger.claimedRewards[0] || activeEra), - lastPayout, + // only use if it is more recent than historyDepth + lastPayout: earliestEra && lastEraPayout && lastEraPayout.gt(earliestEra) && !sessionLength.eq(BN_ONE) + ? lastEra.sub(lastEraPayout).mul(eraLength) + : undefined, minNominated, numNominators: (exposure.others || []).length, numRecentPayouts: earliestEra @@ -167,7 +193,7 @@ function extractSingle (api: ApiPromise, allAccounts: string[], derive: DeriveSt validatorPrefs, withReturns }; - }); + } return [list, nominators]; } @@ -245,35 +271,6 @@ function extractBaseInfo (api: ApiPromise, allAccounts: string[], electedDerive: }; } -const OPT_ERA = { - transform: ({ activeEra, eraLength, sessionLength }: DeriveSessionInfo): LastEra => ({ - activeEra, - eraLength, - lastEra: activeEra.isZero() - ? BN_ZERO - : activeEra.sub(BN_ONE), - sessionLength - }) -}; - -const OPT_MULTI = { - defaultValue: {}, - transform: ([historyDepth, counterForNominators, counterForValidators, optMaxNominatorsCount, optMaxValidatorsCount, minNominatorBond, minValidatorBond, totalIssuance]: [BN, BN?, BN?, Option?, Option?, BN?, BN?, BN?]): MultiResult => ({ - counterForNominators, - counterForValidators, - historyDepth, - maxNominatorsCount: optMaxNominatorsCount && optMaxNominatorsCount.isSome - ? optMaxNominatorsCount.unwrap() - : undefined, - maxValidatorsCount: optMaxValidatorsCount && optMaxValidatorsCount.isSome - ? optMaxValidatorsCount.unwrap() - : undefined, - minNominatorBond, - minValidatorBond, - totalIssuance - }) -}; - function useSortedTargetsImpl (favorites: string[], withLedger: boolean): SortedTargets { const { api } = useApi(); const { allAccounts } = useAccounts(); @@ -300,26 +297,26 @@ function useSortedTargetsImpl (favorites: string[], withLedger: boolean): Sorted const inflation = useInflation(baseInfo?.totalStaked); - const partial = useMemo( - () => inflation && inflation.stakedReturn - ? addReturns(inflation, baseInfo) - : baseInfo, - [baseInfo, inflation] + return useMemo( + (): SortedTargets => ({ + counterForNominators, + counterForValidators, + historyDepth, + inflation, + maxNominatorsCount, + maxValidatorsCount, + medianComm: 0, + minNominated: BN_ZERO, + minNominatorBond, + minValidatorBond, + ...( + inflation && inflation.stakedReturn + ? addReturns(inflation, baseInfo) + : baseInfo + ) + }), + [baseInfo, counterForNominators, counterForValidators, historyDepth, inflation, maxNominatorsCount, maxValidatorsCount, minNominatorBond, minValidatorBond] ); - - return { - counterForNominators, - counterForValidators, - historyDepth, - inflation, - maxNominatorsCount, - maxValidatorsCount, - medianComm: 0, - minNominated: BN_ZERO, - minNominatorBond, - minValidatorBond, - ...partial - }; } export default createNamedHook('useSortedTargets', useSortedTargetsImpl);