Add waiting list to next-gen staking (#8896)

* Add waiting list to next-gen staking

* Extract use* types

* Adjust useHeartbeat checks

* Adjust with useNextTick

* Adjust for useNextTick

* Points anim via ref

* Adjust
This commit is contained in:
Jaco 2023-01-31 11:56:37 +02:00 committed by GitHub
parent 4ca711d2c1
commit 0de8a8d2ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 441 additions and 188 deletions

View File

@ -12,7 +12,7 @@ import styled from 'styled-components';
import { Button, FilterInput, SortDropdown, SummaryBox, Table } from '@polkadot/react-components';
import { getAccountCryptoType } from '@polkadot/react-components/util';
import { useAccounts, useApi, useDelegations, useFavorites, useIpfs, useLedger, useLoadingDelay, useProxies, useToggle } from '@polkadot/react-hooks';
import { useAccounts, useApi, useDelegations, useFavorites, useIpfs, useLedger, useNextTick, useProxies, useToggle } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { settings } from '@polkadot/ui-settings';
import { BN_ZERO, isFunction } from '@polkadot/util';
@ -107,7 +107,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
const [{ sortBy, sortFromMax }, setSortBy] = useState<SortControls>(DEFAULT_SORT_CONTROLS);
const delegations = useDelegations();
const proxies = useProxies();
const isLoading = useLoadingDelay();
const isNextTick = useNextTick();
const onSortChange = useCallback(
(sortBy: SortCategory) => setSortBy(({ sortFromMax }) => ({ sortBy, sortFromMax })),
@ -374,10 +374,10 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
/>
</Button.Group>
</SummaryBox>
{isLoading || !sortedAccounts.length
{!isNextTick || !sortedAccounts.length
? (
<Table
empty={!isLoading && sortedAccounts && t<string>("You don't have any accounts. Some features are currently hidden and will only become available once you have accounts.")}
empty={isNextTick && sortedAccounts && t<string>("You don't have any accounts. Some features are currently hidden and will only become available once you have accounts.")}
header={header.accounts}
/>
)

View File

@ -7,7 +7,7 @@ import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { Button, FilterInput, SummaryBox, Table } from '@polkadot/react-components';
import { useAddresses, useFavorites, useLoadingDelay, useToggle } from '@polkadot/react-hooks';
import { useAddresses, useFavorites, useNextTick, useToggle } from '@polkadot/react-hooks';
import CreateModal from '../modals/Create';
import { useTranslation } from '../translate';
@ -29,7 +29,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
const [sortedAddresses, setSortedAddresses] = useState<SortedAddress[] | undefined>();
const [filterOn, setFilter] = useState<string>('');
const isLoading = useLoadingDelay();
const isNextTick = useNextTick();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('contacts'), 'start', 4]
@ -75,11 +75,11 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
</Button.Group>
</SummaryBox>
<Table
empty={!isLoading && sortedAddresses && t<string>('no addresses saved yet, add any existing address')}
empty={isNextTick && sortedAddresses && t<string>('no addresses saved yet, add any existing address')}
header={headerRef.current}
isSplit
>
{!isLoading && sortedAddresses?.map(({ address, isFavorite }): React.ReactNode => (
{isNextTick && sortedAddresses?.map(({ address, isFavorite }): React.ReactNode => (
<Address
address={address}
filter={filterOn}

View File

@ -9,7 +9,7 @@ import type { NominatedByMap, SortedTargets, ValidatorInfo } from '../types';
import React, { useMemo, useRef, useState } from 'react';
import { Table } from '@polkadot/react-components';
import { useApi, useBlockAuthors, useLoadingDelay } from '@polkadot/react-hooks';
import { useApi, useBlockAuthors, useNextTick } from '@polkadot/react-hooks';
import Filtering from '../Filtering';
import Legend from '../Legend';
@ -113,9 +113,7 @@ function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, m
const { api } = useApi();
const { byAuthor, eraPoints } = useBlockAuthors();
const [nameFilter, setNameFilter] = useState<string>('');
// we have a very large list, so we use a loading delay
const isLoading = useLoadingDelay();
const isNextTick = useNextTick();
const { validators, waiting } = useMemo(
() => getFiltered(isOwn, stakingOverview, favorites, targets.waitingIds, ownStashIds),
@ -123,12 +121,12 @@ function CurrentList ({ className, favorites, hasQueries, isIntentions, isOwn, m
);
const list = useMemo(
() => isLoading
? undefined
: isIntentions
() => isNextTick
? isIntentions
? nominatedBy && waiting
: validators,
[isIntentions, isLoading, nominatedBy, validators, waiting]
: validators
: undefined,
[isIntentions, isNextTick, nominatedBy, validators, waiting]
);
const infoMap = useMemo(

View File

@ -0,0 +1,57 @@
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SessionInfo, Validator } from '../types';
import type { UsePoints } from './types';
import React, { useRef } from 'react';
import Legend from '@polkadot/app-staking/Legend';
import { Table } from '@polkadot/react-components';
import { useNextTick } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import ValidatorRow from './Validator';
interface Props {
className?: string;
isRelay: boolean;
points?: UsePoints;
sessionInfo: SessionInfo;
toggleFavorite: (stashId: string) => void;
validatorsActive?: Validator[];
}
function ListActive ({ className = '', isRelay, points, sessionInfo, toggleFavorite, validatorsActive }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const isNextTick = useNextTick();
const headerActive = useRef<[string?, string?, number?][]>([
// favorite, badges, details, expand
[t<string>('validators'), 'start', 4]
]);
return (
<Table
className={className}
empty={isNextTick && validatorsActive && t<string>('No session validators found')}
emptySpinner={t<string>('Retrieving session validators')}
header={headerActive.current}
isSplit
legend={<Legend isRelay={isRelay} />}
>
{isNextTick && validatorsActive?.map((v) => (
<ValidatorRow
isRelay={isRelay}
key={v.key}
points={points?.[v.stashId]}
sessionInfo={sessionInfo}
toggleFavorite={toggleFavorite}
validator={v}
/>
))}
</Table>
);
}
export default React.memo(ListActive);

View File

@ -0,0 +1,57 @@
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SessionInfo, Validator } from '../types';
import React, { useRef } from 'react';
import Legend from '@polkadot/app-staking/Legend';
import { Table } from '@polkadot/react-components';
import { useNextTick } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import useValidatorsWaiting from '../useValidatorsWaiting';
import ValidatorRow from './Validator';
interface Props {
className?: string;
favorites: string[];
isRelay: boolean;
sessionInfo: SessionInfo;
toggleFavorite: (stashId: string) => void;
validatorsActive?: Validator[];
}
function ListActive ({ className = '', favorites, isRelay, sessionInfo, toggleFavorite, validatorsActive }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const isNextTick = useNextTick();
const validatorsWaiting = useValidatorsWaiting(favorites, sessionInfo, validatorsActive);
const headerWaiting = useRef<[string?, string?, number?][]>([
// favorite, badges, details, expand
[t<string>('waiting'), 'start', 4]
]);
return (
<Table
className={className}
empty={isNextTick && validatorsWaiting && t<string>('No waiting validators found')}
emptySpinner={t<string>('Retrieving waiting validators')}
header={headerWaiting.current}
isSplit
legend={<Legend isRelay={isRelay} />}
>
{isNextTick && validatorsWaiting?.map((v) => (
<ValidatorRow
isRelay={isRelay}
key={v.key}
sessionInfo={sessionInfo}
toggleFavorite={toggleFavorite}
validator={v}
/>
))}
</Table>
);
}
export default React.memo(ListActive);

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { Validator } from '../../types';
import type { Heartbeat } from './types';
import type { UseHeartbeat } from '../types';
import React, { useMemo } from 'react';
@ -10,7 +10,7 @@ import { Badge } from '@polkadot/react-components';
import { useAccounts } from '@polkadot/react-hooks';
interface Props {
heartbeat: Heartbeat;
heartbeat: UseHeartbeat;
isChilled?: boolean;
isPara?: boolean;
isRelay?: boolean;

View File

@ -3,7 +3,7 @@
import type { SessionInfo, Validator } from '../../types';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import { AddressSmall, Table, Tag } from '@polkadot/react-components';
import { useToggle } from '@polkadot/react-hooks';
@ -41,12 +41,22 @@ function ValidatorExpanded ({ className = '' }: PropsExpanded): React.ReactEleme
function Validator ({ className = '', isRelay, points, sessionInfo, toggleFavorite, validator }: Props): React.ReactElement<Props> {
const [isExpanded, toggleExpanded] = useToggle();
const pointsRef = useRef<{ counter: number, points: number }>({ counter: 0, points: 0 });
const exposure = useExposure(validator, sessionInfo);
const heartbeat = useHeartbeat(validator, sessionInfo);
const pointsAnimClass = useMemo(
() => (points && `greyAnim-${Date.now() % 25}`) || '',
[points]
(): string => {
if (!points || pointsRef.current.points === points) {
return '';
}
pointsRef.current.points = points;
pointsRef.current.counter = (pointsRef.current.counter + 1) % 25;
return `greyAnim-${pointsRef.current.counter}`;
},
[points, pointsRef]
);
return (

View File

@ -1,7 +0,0 @@
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
export interface Heartbeat {
authoredBlocks?: number;
isOnline: boolean;
}

View File

@ -3,34 +3,21 @@
import type { PalletStakingExposure } from '@polkadot/types/lookup';
import type { SessionInfo, Validator } from '../../types';
import type { UseExposure, UseExposureExposure } from '../types';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import { BN } from '@polkadot/util';
import { BN, objectSpread } from '@polkadot/util';
interface ExposureEntry {
who: string,
value: BN
interface CacheEntry extends UseExposure {
activeEra: BN;
}
interface Exposure {
others: ExposureEntry[];
own: BN;
total: BN;
}
interface Result {
clipped?: Exposure;
exposure?: Exposure;
waiting?: {
others: ExposureEntry[],
total: BN
};
}
type Cache = Record<string, CacheEntry>;
const OPT_EXPOSURE = {
transform: ({ others, own, total }: PalletStakingExposure): Exposure => ({
transform: ({ others, own, total }: PalletStakingExposure): UseExposureExposure => ({
others: others
.map(({ value, who }) => ({
value: value.unwrap(),
@ -42,28 +29,28 @@ const OPT_EXPOSURE = {
})
};
function getResult (exposure?: Exposure, clipped?: Exposure): Result {
let waiting: Result['waiting'];
function getResult (exposure: UseExposureExposure, clipped: UseExposureExposure): UseExposure {
let waiting: UseExposure['waiting'];
if (exposure && clipped) {
const others = exposure.others.filter(({ who }) =>
!clipped.others.find((c) =>
who === c.who
)
);
const others = exposure.others.filter(({ who }) =>
!clipped.others.find((c) =>
who === c.who
)
);
if (others.length) {
waiting = {
others,
total: others.reduce((total, { value }) => total.iadd(value), new BN(0))
};
}
if (others.length) {
waiting = {
others,
total: others.reduce((total, { value }) => total.iadd(value), new BN(0))
};
}
return { clipped, exposure, waiting };
}
function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): Result | undefined {
const cache: Cache = {};
function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): UseExposure | undefined {
const { api } = useApi();
const params = useMemo(
@ -74,10 +61,26 @@ function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): R
const fullExposure = useCall(params && api.query.staking.erasStakers, params, OPT_EXPOSURE);
const clipExposure = useCall(params && api.query.staking.erasStakersClipped, params, OPT_EXPOSURE);
return useMemo(
() => getResult(fullExposure, clipExposure),
const result = useMemo(
() => fullExposure && clipExposure && getResult(fullExposure, clipExposure),
[clipExposure, fullExposure]
);
// clear the cached result on era changes
useEffect((): void => {
if (activeEra && (!cache[stashId] || !activeEra.eq(cache[stashId].activeEra))) {
cache[stashId] = { activeEra };
}
}, [activeEra, stashId]);
// update the cached result on exposure changes
useEffect((): void => {
if (result && activeEra && cache[stashId] && activeEra.eq(cache[stashId].activeEra)) {
cache[stashId] = objectSpread<CacheEntry>({ activeEra }, result);
}
}, [activeEra, result, stashId]);
return result || cache[stashId];
}
export default createNamedHook('useExposure', useExposureImpl);

View File

@ -3,12 +3,22 @@
import type { Option, u32 } from '@polkadot/types';
import type { Codec } from '@polkadot/types/types';
import type { BN } from '@polkadot/util';
import type { SessionInfo, Validator } from '../../types';
import type { Heartbeat } from './types';
import type { UseHeartbeat } from '../types';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import { isBoolean, isNumber, objectSpread } from '@polkadot/util';
interface CacheEntry extends UseHeartbeat {
currentSession: BN;
}
type Cache = Record<string, CacheEntry>;
const EMPTY: UseHeartbeat = {};
const OPT_BLOCKS = {
transform: (authoredBlocks: u32): number =>
@ -21,27 +31,47 @@ const OPT_BEATS = {
receivedHeartbeats.isSome
};
function useHeartbeatImpl ({ stashId, stashIndex }: Validator, { currentSession }: SessionInfo): Heartbeat {
const cache: Cache = {};
function useHeartbeatImpl ({ stashId, stashIndex }: Validator, { currentSession }: SessionInfo): UseHeartbeat {
const { api } = useApi();
const params = useMemo(
() => currentSession && ({
authoredBlocks: [currentSession, stashId],
receivedHeartbeats: [currentSession, stashIndex]
}),
() => stashIndex === -1
? undefined
: currentSession && ({
authoredBlocks: [currentSession, stashId],
receivedHeartbeats: [currentSession, stashIndex]
}),
[currentSession, stashId, stashIndex]
);
const authoredBlocks = useCall(params && api.query.imOnline.authoredBlocks, params?.authoredBlocks, OPT_BLOCKS);
const receivedHeartbeats = useCall(params && api.query.imOnline.receivedHeartbeats, params?.receivedHeartbeats, OPT_BEATS);
return useMemo(
() => ({
const result = useMemo(
() => isNumber(authoredBlocks) && isBoolean(receivedHeartbeats) && ({
authoredBlocks,
isOnline: !!(authoredBlocks || receivedHeartbeats)
}),
[authoredBlocks, receivedHeartbeats]
);
// when the session changes, empty the cache
useEffect((): void => {
if (currentSession && (!cache[stashId] || !currentSession.eq(cache[stashId].currentSession))) {
cache[stashId] = { currentSession };
}
}, [currentSession, stashId]);
// set the new heartbeat info based on retrieved
useEffect((): void => {
if (result && currentSession && currentSession.eq(cache[stashId].currentSession)) {
cache[stashId] = objectSpread<CacheEntry>({ currentSession }, result);
}
}, [currentSession, result, stashId]);
return result || cache[stashId] || EMPTY;
}
export default createNamedHook('useHeartbeat', useHeartbeatImpl);

View File

@ -1,83 +1,94 @@
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SessionInfo, Validator } from '../types';
import type { SessionInfo } from '../types';
import React, { useRef } from 'react';
import React, { useRef, useState } from 'react';
import styled from 'styled-components';
import Legend from '@polkadot/app-staking/Legend';
import { Table } from '@polkadot/react-components';
import { Button, ToggleGroup } from '@polkadot/react-components';
import { useTranslation } from '../translate';
import useValidatorsActive from '../useValidatorsActive';
import ListActive from './ListActive';
import ListWaiting from './ListWaiting';
import usePoints from './usePoints';
import ValidatorRow from './Validator';
interface Props {
className?: string;
favorites: string[];
isRelay: boolean;
sessionInfo: SessionInfo;
toggleFavorite: (stashId: string) => void;
validatorsSession?: Validator[];
}
function Validators ({ className = '', isRelay, sessionInfo, toggleFavorite, validatorsSession }: Props): React.ReactElement<Props> {
function Validators ({ className = '', favorites, isRelay, sessionInfo, toggleFavorite }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [intentIndex, setIntentIndex] = useState(0);
const validatorsActive = useValidatorsActive(favorites, sessionInfo);
const points = usePoints(sessionInfo);
const headerActive = useRef<[string?, string?, number?][]>([
// favorite, badges, details, expand
[t<string>('validators'), 'start', 4]
const intentOptions = useRef([
{ text: t('Active'), value: 'active' },
{ text: t('Waiting'), value: 'waiting' }
]);
return (
<StyledTable
className={className}
empty={validatorsSession && t<string>('No session validators found')}
emptySpinner={t<string>('Retrieving session validators')}
header={headerActive.current}
isSplit
legend={
<Legend
isRelay={isRelay}
<StyledDiv className={className}>
<Button.Group>
<ToggleGroup
onChange={setIntentIndex}
options={intentOptions.current}
value={intentIndex}
/>
</Button.Group>
{intentIndex === 0
? (
<ListActive
isRelay={isRelay}
points={points}
sessionInfo={sessionInfo}
toggleFavorite={toggleFavorite}
validatorsActive={validatorsActive}
/>
)
: (
<ListWaiting
favorites={favorites}
isRelay={isRelay}
sessionInfo={sessionInfo}
toggleFavorite={toggleFavorite}
validatorsActive={validatorsActive}
/>
)
}
>
{validatorsSession?.map((v) => (
<ValidatorRow
isRelay={isRelay}
key={v.key}
points={points?.[v.stashId]}
sessionInfo={sessionInfo}
toggleFavorite={toggleFavorite}
validator={v}
/>
))}
</StyledTable>
</StyledDiv>
);
}
const StyledTable = styled(Table)`
td.statusInfo {
padding: 0 0 0 0.5rem;
vertical-align: middle;
const StyledDiv = styled.div`
.ui--Table table {
td.statusInfo {
padding: 0 0 0 0.5rem;
vertical-align: middle;
> div {
display: inline-block;
max-width: 3.6rem;
min-width: 3.6rem;
> div {
display: inline-block;
max-width: 3.6rem;
min-width: 3.6rem;
.ui--Badge {
margin: 0.125rem;
.ui--Badge {
margin: 0.125rem;
&.opaque {
opacity: var(--opacity-gray);
&.opaque {
opacity: var(--opacity-gray);
}
}
}
}
+ td.address {
padding-left: 0.5rem;
+ td.address {
padding-left: 0.5rem;
}
}
}
`;

View File

@ -0,0 +1,31 @@
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
export type UsePoints = Record<string, number>;
export interface UseHeartbeat {
authoredBlocks?: number;
isOnline?: boolean;
}
export interface UseExposureExposureEntry {
who: string,
value: BN
}
export interface UseExposureExposure {
others: UseExposureExposureEntry[];
own: BN;
total: BN;
}
export interface UseExposure {
clipped?: UseExposureExposure;
exposure?: UseExposureExposure;
waiting?: {
others: UseExposureExposureEntry[];
total: BN;
};
}

View File

@ -2,27 +2,34 @@
// SPDX-License-Identifier: Apache-2.0
import type { PalletStakingEraRewardPoints } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { SessionInfo } from '../types';
import type { UsePoints } from './types';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
type Result = Record<string, number>;
interface Cache {
activeEra: BN;
points?: UsePoints;
}
const OPT_POINTS = {
transform: ({ individual }: PalletStakingEraRewardPoints): Result =>
transform: ({ individual }: PalletStakingEraRewardPoints): UsePoints =>
[...individual.entries()]
.filter(([, points]) => points.gt(BN_ZERO))
.reduce((result: Result, [stashId, points]): Result => {
.reduce((result: UsePoints, [stashId, points]): UsePoints => {
result[stashId.toString()] = points.toNumber();
return result;
}, {})
};
function usePointsImpl ({ activeEra }: SessionInfo): Result | undefined {
let cache: Cache | undefined;
function usePointsImpl ({ activeEra }: SessionInfo): UsePoints | undefined {
const { api } = useApi();
const queryParams = useMemo(
@ -30,7 +37,19 @@ function usePointsImpl ({ activeEra }: SessionInfo): Result | undefined {
[activeEra]
);
return useCall(queryParams && api.query.staking.erasRewardPoints, queryParams, OPT_POINTS);
const points = useCall(queryParams && api.query.staking.erasRewardPoints, queryParams, OPT_POINTS);
useEffect((): void => {
if (activeEra && points) {
cache = { activeEra, points };
}
}, [activeEra, points]);
return points || (
cache &&
activeEra?.eq(cache.activeEra) &&
cache.points
) || undefined;
}
export default createNamedHook('usePoints', usePointsImpl);

View File

@ -12,7 +12,6 @@ import { useApi, useFavorites } from '@polkadot/react-hooks';
import { STORE_FAVS_BASE } from './constants';
import { useTranslation } from './translate';
import useSessionInfo from './useSessionInfo';
import useValidatorsActive from './useValidatorsActive';
import Validators from './Validators';
function StakingApp ({ basePath }: Props): React.ReactElement<Props> {
@ -20,7 +19,6 @@ function StakingApp ({ basePath }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const sessionInfo = useSessionInfo();
const validatorsActive = useValidatorsActive(favorites, sessionInfo);
const isRelay = useMemo(
() => !!(api.query.parasShared || api.query.shared)?.activeValidatorIndices,
@ -44,10 +42,10 @@ function StakingApp ({ basePath }: Props): React.ReactElement<Props> {
<Switch>
<Route>
<Validators
favorites={favorites}
isRelay={isRelay}
sessionInfo={sessionInfo}
toggleFavorite={toggleFavorite}
validatorsSession={validatorsActive}
/>
</Route>
</Switch>

View File

@ -3,6 +3,7 @@
import type { StorageKey, u32 } from '@polkadot/types';
import type { AccountId32 } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { SessionInfo } from './types';
import { useEffect, useMemo } from 'react';
@ -10,8 +11,8 @@ import { useEffect, useMemo } from 'react';
import { createNamedHook, useApi, useMapKeys } from '@polkadot/react-hooks';
interface Cache {
currentEra: SessionInfo['currentEra'];
elected?: string[];
currentEra: BN;
elected: string[];
}
const OPT_ELECTED = {
@ -23,15 +24,13 @@ const OPT_ELECTED = {
// since this would only change once per era for the full runtime,
// we just store it globally to not have expensive map-key lookups
const cached: Cache = {
currentEra: null
};
let cache: Cache | undefined;
function useElectedValidatorsImpl ({ currentEra }: SessionInfo): string[] | undefined {
const { api } = useApi();
const electedParams = useMemo(
() => currentEra && (!cached.currentEra || !cached.currentEra.eq(currentEra))
() => cache && currentEra && !cache.currentEra.eq(currentEra)
? [currentEra]
: null,
[currentEra]
@ -41,20 +40,15 @@ function useElectedValidatorsImpl ({ currentEra }: SessionInfo): string[] | unde
useEffect((): void => {
if (currentEra && elected) {
cached.currentEra = currentEra;
cached.elected = elected;
cache = { currentEra, elected };
}
}, [currentEra, elected]);
return useMemo(
() => (
currentEra &&
cached.currentEra &&
cached.currentEra.eq(currentEra) &&
cached.elected
) || elected,
[currentEra, elected]
);
return elected || (
cache &&
currentEra?.eq(cache.currentEra) &&
cache.elected
) || undefined;
}
export default createNamedHook('useElectedValidators', useElectedValidatorsImpl);

View File

@ -2,12 +2,20 @@
// SPDX-License-Identifier: Apache-2.0
import type { AccountId32 } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { SessionInfo, Validator } from './types';
import { useEffect } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import useTaggedValidators from './useTaggedValidators';
interface Cache {
activeEra: BN;
validators: Validator[];
}
const OPT_VALIDATORS = {
transform: (validators: AccountId32[]): Validator[] =>
validators.map((a, stashIndex) => {
@ -24,11 +32,24 @@ const OPT_VALIDATORS = {
})
};
let cache: Cache | undefined;
function useValidatorsActiveImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined {
const { api } = useApi();
const validators = useCall(api.query.session.validators, undefined, OPT_VALIDATORS);
const sessionValidators = useCall(api.query.session.validators, undefined, OPT_VALIDATORS);
const validators = useTaggedValidators(favorites, sessionInfo, sessionValidators);
return useTaggedValidators(favorites, sessionInfo, validators);
useEffect((): void => {
if (validators && sessionInfo.activeEra) {
cache = { activeEra: sessionInfo.activeEra, validators };
}
}, [sessionInfo, validators]);
return validators || (
cache &&
sessionInfo.activeEra?.eq(cache.activeEra) &&
cache.validators
) || undefined;
}
export default createNamedHook('useValidatorsActive', useValidatorsActiveImpl);

View File

@ -6,7 +6,7 @@ import type { StorageKey } from '@polkadot/types';
import type { AccountId32, EventRecord } from '@polkadot/types/interfaces';
import type { SessionInfo, Validator } from './types';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@polkadot/react-hooks';
@ -49,6 +49,8 @@ function mapValidators (validators?: AccountId32[]): Validator[] | undefined {
});
}
let cache: Validator[] | undefined;
function useValidatorsAllImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined {
const { api } = useApi();
const startValue = useMapKeys(api.query.staking.validators, EMPTY_PARAMS, OPT_VALIDATORS);
@ -62,7 +64,15 @@ function useValidatorsAllImpl (favorites: string[], sessionInfo: SessionInfo): V
[validators]
);
return useTaggedValidators(favorites, sessionInfo, validatorsIndexed);
const result = useTaggedValidators(favorites, sessionInfo, validatorsIndexed);
useEffect((): void => {
if (result) {
cache = result;
}
}, [result]);
return result || cache;
}
export default createNamedHook('useValidatorsAll', useValidatorsAllImpl);

View File

@ -1,31 +1,49 @@
// Copyright 2017-2023 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
import type { SessionInfo, Validator } from './types';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { createNamedHook } from '@polkadot/react-hooks';
import useValidatorsActive from './useValidatorsActive';
import useValidatorsAll from './useValidatorsAll';
interface Cache {
activeEra: BN;
validators: Validator[];
}
function excludeValidators (from?: Validator[], exclude?: Validator[]): Validator[] | undefined {
return from && exclude && from.filter(({ stashId }) =>
!exclude.some((v) => v.stashId === stashId)
);
}
function useValidatorsWaitingImpl (favorites: string[], sessionInfo: SessionInfo): Validator[] | undefined {
const activeValidators = useValidatorsActive(favorites, sessionInfo);
let cache: Cache | undefined;
function useValidatorsWaitingImpl (favorites: string[], sessionInfo: SessionInfo, activeValidators?: Validator[]): Validator[] | undefined {
const allValidators = useValidatorsAll(favorites, sessionInfo);
// both active and all is already sorted and tagged, so we don't
// need to re-sort the waiting list
return useMemo(
const validators = useMemo(
() => excludeValidators(allValidators, activeValidators),
[activeValidators, allValidators]
);
useEffect((): void => {
if (validators && sessionInfo.activeEra) {
cache = { activeEra: sessionInfo.activeEra, validators };
}
}, [sessionInfo, validators]);
return validators || (
cache &&
sessionInfo.activeEra?.eq(cache.activeEra) &&
cache.validators
) || undefined;
}
export default createNamedHook('useValidatorsWaiting', useValidatorsWaitingImpl);

View File

@ -4,7 +4,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { useDebounce, useLoadingDelay } from '@polkadot/react-hooks';
import { useDebounce, useNextTick } from '@polkadot/react-hooks';
import Input from '../Input';
import Spinner from '../Spinner';
@ -39,7 +39,7 @@ function InputAddressMulti ({ available, availableLabel, className = '', default
const [_filter, setFilter] = useState<string>('');
const [selected, setSelected] = useState<string[]>([]);
const filter = useDebounce(_filter);
const isLoading = useLoadingDelay();
const isNextTick = useNextTick();
useEffect((): void => {
defaultValue && setSelected(defaultValue);
@ -86,9 +86,8 @@ function InputAddressMulti ({ available, availableLabel, className = '', default
<div className='ui--InputAddressMulti-column'>
<label>{availableLabel}</label>
<div className='ui--InputAddressMulti-items'>
{isLoading
? <Spinner />
: available.map((address) => (
{isNextTick
? available.map((address) => (
<Available
address={address}
filter={filter}
@ -97,6 +96,7 @@ function InputAddressMulti ({ available, availableLabel, className = '', default
onSelect={onSelect}
/>
))
: <Spinner />
}
</div>
</div>

View File

@ -45,11 +45,11 @@ export { useIsMountedRef } from './useIsMountedRef';
export { useJudgements } from './useJudgements';
export { useKeyring } from './useKeyring';
export { useLedger } from './useLedger';
export { useLoadingDelay } from './useLoadingDelay';
export { useMapEntries } from './useMapEntries';
export { useMapKeys } from './useMapKeys';
export { useMemoValue } from './useMemoValue';
export { useModal } from './useModal';
export { useNextTick } from './useNextTick';
export { useNonEmptyString } from './useNonEmptyString';
export { useNonZeroBn } from './useNonZeroBn';
export { useOwnEraRewards } from './useOwnEraRewards';

View File

@ -1,24 +0,0 @@
// Copyright 2017-2023 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useState } from 'react';
import { createNamedHook } from './createNamedHook';
import { useIsMountedRef } from './useIsMountedRef';
function useLoadingDelayImpl (delay = 0): boolean {
const mountedRef = useIsMountedRef();
const [isLoading, setIsLoading] = useState(true);
useEffect((): void => {
setTimeout((): void => {
mountedRef.current && setIsLoading(false);
}, delay);
// Ignore, this is for the initial setup
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return isLoading;
}
export const useLoadingDelay = createNamedHook('useLoadingDelay', useLoadingDelayImpl);

View File

@ -0,0 +1,20 @@
// Copyright 2017-2023 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useState } from 'react';
import { nextTick } from '@polkadot/util';
import { createNamedHook } from './createNamedHook';
function useNextTickImpl (): boolean {
const [isNextTick, setIsNextTick] = useState(false);
useEffect((): void => {
nextTick(() => setIsNextTick(true));
}, []);
return isNextTick;
}
export const useNextTick = createNamedHook('useNextTick', useNextTickImpl);

View File

@ -45,11 +45,18 @@ const EMPTY_STATE: Partial<Result> = {
// return both v1 & v2 weight structures (would depend on actual use)
export function convertWeight (weight: V1Weight | V2Weight): { v1Weight: BN, v2Weight: V2WeightConstruct } {
if ((weight as V2Weight).proofSize) {
// V2 weight
const refTime = (weight as V2Weight).refTime.toBn();
return { v1Weight: refTime, v2Weight: weight as V2Weight };
} else if ((weight as V2Weight).refTime) {
// V1.5 weight (when not converted)
const refTime = (weight as V2Weight).refTime.toBn();
return { v1Weight: refTime, v2Weight: { refTime } };
}
// V1 weight
const refTime = (weight as V1Weight).toBn();
return { v1Weight: refTime, v2Weight: { refTime } };

View File

@ -60,8 +60,8 @@ jest.mock('@polkadot/react-hooks/useAccountInfo', () => {
});
});
jest.mock('@polkadot/react-hooks/useLoadingDelay', () => ({
useLoadingDelay: () => false
jest.mock('@polkadot/react-hooks/useNextTick', () => ({
useNextTick: () => true
}));
jest.mock('@polkadot/react-hooks/useBalancesAll', () => ({