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:
parent
4ca711d2c1
commit
0de8a8d2ff
@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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(
|
||||
|
||||
57
packages/page-staking2/src/Validators/ListActive.tsx
Normal file
57
packages/page-staking2/src/Validators/ListActive.tsx
Normal 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);
|
||||
57
packages/page-staking2/src/Validators/ListWaiting.tsx
Normal file
57
packages/page-staking2/src/Validators/ListWaiting.tsx
Normal 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);
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
31
packages/page-staking2/src/Validators/types.ts
Normal file
31
packages/page-staking2/src/Validators/types.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
20
packages/react-hooks/src/useNextTick.ts
Normal file
20
packages/react-hooks/src/useNextTick.ts
Normal 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);
|
||||
@ -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 } };
|
||||
|
||||
@ -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', () => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user