mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-19 06:45:41 +00:00
Add Pezkuwi SDK UI - Polkadot.js Apps clone
- Clone Polkadot.js Apps repository - Update package.json with Pezkuwi branding - Add Pezkuwi endpoint to production chains (wss://pezkuwichain.app:9944) - Create comprehensive README for SDK UI - Set up project structure with all packages Next steps: - Apply Kurdistan colors (Kesk, Sor, Zer, Spi + Black) to UI theme - Replace logos with Pezkuwi branding - Test build and deployment
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { MarkWarning } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
function ActionsBanner (): React.ReactElement<null> | null {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MarkWarning
|
||||
className='warning centered'
|
||||
content={t('Use the account actions to create a new validator/nominator stash and bond it to participate in staking. Do not send funds directly via a transfer to a validator.')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ActionsBanner);
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { NominatedBy as NominatedByType } from '@polkadot/app-staking/types';
|
||||
import type { SlashingSpans } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressMini, ExpanderScroll } from '@polkadot/react-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
|
||||
interface Props {
|
||||
nominators?: NominatedByType[];
|
||||
slashingSpans?: SlashingSpans | null;
|
||||
}
|
||||
|
||||
interface Chilled {
|
||||
active: null | [number, () => React.ReactNode[]];
|
||||
chilled: null | [number, () => React.ReactNode[]];
|
||||
}
|
||||
|
||||
function extractFunction (all: string[]): null | [number, () => React.ReactNode[]] {
|
||||
return all.length
|
||||
? [
|
||||
all.length,
|
||||
() => all.map((value): React.ReactNode =>
|
||||
<AddressMini
|
||||
key={value}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
]
|
||||
: null;
|
||||
}
|
||||
|
||||
function extractChilled (api: ApiPromise, nominators: NominatedByType[] = [], slashingSpans?: SlashingSpans | null): Chilled {
|
||||
// NOTE With the introduction of the SlashReported event,
|
||||
// nominators are not auto-chilled on validator slash
|
||||
const chilled = slashingSpans && !api.events.staking.SlashReported
|
||||
? nominators
|
||||
.filter(({ submittedIn }) =>
|
||||
slashingSpans.lastNonzeroSlash.gt(submittedIn)
|
||||
)
|
||||
.map(({ nominatorId }) => nominatorId)
|
||||
: [];
|
||||
|
||||
return {
|
||||
active: extractFunction(
|
||||
nominators
|
||||
.filter(({ nominatorId }) => !chilled.includes(nominatorId))
|
||||
.map(({ nominatorId }) => nominatorId)
|
||||
),
|
||||
chilled: extractFunction(chilled)
|
||||
};
|
||||
}
|
||||
|
||||
function NominatedBy ({ nominators, slashingSpans }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
|
||||
const { active, chilled } = useMemo(
|
||||
() => extractChilled(api, nominators, slashingSpans),
|
||||
[api, nominators, slashingSpans]
|
||||
);
|
||||
|
||||
return (
|
||||
<td className='expand all'>
|
||||
{active && (
|
||||
<ExpanderScroll
|
||||
renderChildren={active[1]}
|
||||
summary={t('Nominations ({{count}})', { replace: { count: formatNumber(active[0]) } })}
|
||||
/>
|
||||
)}
|
||||
{chilled && (
|
||||
<ExpanderScroll
|
||||
renderChildren={chilled[1]}
|
||||
summary={t('Renomination required ({{count}})', { replace: { count: formatNumber(chilled[0]) } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(NominatedBy);
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@polkadot/types';
|
||||
import type { NominatorValue } from './types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressMini, ExpanderScroll } from '@polkadot/react-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { BN, BN_ZERO } from '@polkadot/util';
|
||||
|
||||
interface Props {
|
||||
stakeOther?: BN;
|
||||
nominators?: NominatorValue[];
|
||||
}
|
||||
|
||||
function extractFunction (all: NominatorValue[]): null | [number, () => React.ReactNode[]] {
|
||||
return [
|
||||
all.length,
|
||||
() => all.map(({ nominatorId, value }): React.ReactNode =>
|
||||
<AddressMini
|
||||
bonded={value}
|
||||
key={nominatorId}
|
||||
value={nominatorId}
|
||||
withBonded
|
||||
/>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
function sumValue (all: { value: BN }[]): BN {
|
||||
const total = new BN(0);
|
||||
|
||||
for (let i = 0, count = all.length; i < count; 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] {
|
||||
if (!nominators) {
|
||||
return [null, BN_ZERO, null, BN_ZERO];
|
||||
}
|
||||
|
||||
const sorted = nominators.sort((a, b) => b.value.cmp(a.value));
|
||||
|
||||
if (!maxPaid || maxPaid.gtn(sorted.length)) {
|
||||
return [extractFunction(sorted), stakeOther || BN_ZERO, null, BN_ZERO];
|
||||
}
|
||||
|
||||
const max = maxPaid.toNumber();
|
||||
const rewarded = sorted.slice(0, max);
|
||||
const rewardedTotal = sumValue(rewarded);
|
||||
const unrewarded = sorted.slice(max);
|
||||
const unrewardedTotal = sumValue(unrewarded);
|
||||
|
||||
return [extractFunction(rewarded), rewardedTotal, extractFunction(unrewarded), unrewardedTotal];
|
||||
}
|
||||
|
||||
function StakeOther ({ nominators, stakeOther }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApi();
|
||||
|
||||
const [rewarded, rewardedTotal, unrewarded, unrewardedTotal] = useMemo(
|
||||
() => extractTotals(api.consts.staking?.maxNominatorRewardedPerValidator as u32, nominators, stakeOther),
|
||||
[api, nominators, stakeOther]
|
||||
);
|
||||
|
||||
return (
|
||||
<td className='expand all'>
|
||||
{(!rewarded || rewarded[0] !== 0) && (
|
||||
<ExpanderScroll
|
||||
className={rewarded ? '' : '--tmp'}
|
||||
renderChildren={rewarded?.[1]}
|
||||
summary={
|
||||
<FormatBalance
|
||||
labelPost={` (${rewarded ? rewarded[0] : '0'})`}
|
||||
value={rewardedTotal}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{unrewarded && (
|
||||
<ExpanderScroll
|
||||
className='stakeOver'
|
||||
renderChildren={unrewarded[1]}
|
||||
summary={
|
||||
<FormatBalance
|
||||
labelPost={` (${unrewarded[0]})`}
|
||||
value={unrewardedTotal}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(StakeOther);
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import MaxBadge from '@polkadot/app-staking/MaxBadge';
|
||||
import { Badge } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
interface Props {
|
||||
isChilled?: boolean;
|
||||
isElected: boolean;
|
||||
isMain?: boolean;
|
||||
isPara?: boolean;
|
||||
isRelay?: boolean;
|
||||
nominators?: { nominatorId: string }[];
|
||||
onlineCount?: false | BN;
|
||||
onlineMessage?: boolean;
|
||||
}
|
||||
|
||||
const NO_NOMS: { nominatorId: string }[] = [];
|
||||
|
||||
function Status ({ isChilled, isElected, isMain, isPara, isRelay, nominators = NO_NOMS, onlineCount, onlineMessage }: Props): React.ReactElement<Props> {
|
||||
const { allAccounts } = useAccounts();
|
||||
const blockCount = onlineCount && onlineCount.toNumber();
|
||||
|
||||
const isNominating = useMemo(
|
||||
() => nominators.some(({ nominatorId }) => allAccounts.includes(nominatorId)),
|
||||
[allAccounts, nominators]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNominating
|
||||
? (
|
||||
<Badge
|
||||
className='media--1100'
|
||||
color='green'
|
||||
icon='hand-paper'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Badge
|
||||
className='media--1100'
|
||||
color='transparent'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{isRelay && (
|
||||
isPara
|
||||
? (
|
||||
<Badge
|
||||
className='media--1100'
|
||||
color='purple'
|
||||
icon='vector-square'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Badge
|
||||
className='media--1100'
|
||||
color='transparent'
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{isChilled
|
||||
? (
|
||||
<Badge
|
||||
className='media--1000'
|
||||
color='red'
|
||||
icon='cancel'
|
||||
/>
|
||||
)
|
||||
: isElected
|
||||
? (
|
||||
<Badge
|
||||
className='media--1000'
|
||||
color='blue'
|
||||
icon='chevron-right'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Badge
|
||||
className='media--1000'
|
||||
color='transparent'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{isMain && (
|
||||
blockCount
|
||||
? (
|
||||
<Badge
|
||||
className='media--900'
|
||||
color='green'
|
||||
info={blockCount}
|
||||
/>
|
||||
)
|
||||
: onlineMessage
|
||||
? (
|
||||
<Badge
|
||||
className='media--900'
|
||||
color='green'
|
||||
icon='envelope'
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Badge
|
||||
className='media--900'
|
||||
color='transparent'
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<MaxBadge numNominators={nominators.length} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Status);
|
||||
@@ -0,0 +1,236 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { DeriveHeartbeatAuthor } from '@polkadot/api-derive/types';
|
||||
import type { NominatedBy as NominatedByType, ValidatorInfo } from '@polkadot/app-staking/types';
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { SlashingSpans, ValidatorPrefs } from '@polkadot/types/interfaces';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { NominatorValue } from './types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Columar, Icon, LinkExternal, Table, Tag } from '@polkadot/react-components';
|
||||
import { checkVisibility } from '@polkadot/react-components/util';
|
||||
import { useApi, useCall, useDeriveAccountInfo, useToggle } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import NominatedBy from './NominatedBy.js';
|
||||
import StakeOther from './StakeOther.js';
|
||||
import Status from './Status.js';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
className?: string;
|
||||
filterName: string;
|
||||
hasQueries: boolean;
|
||||
isElected: boolean;
|
||||
isFavorite: boolean;
|
||||
isMain?: boolean;
|
||||
isPara?: boolean;
|
||||
lastBlock?: string;
|
||||
minCommission?: BN;
|
||||
nominatedBy?: NominatedByType[];
|
||||
points?: string;
|
||||
recentlyOnline?: DeriveHeartbeatAuthor;
|
||||
toggleFavorite: (accountId: string) => void;
|
||||
validatorInfo?: ValidatorInfo;
|
||||
withIdentity?: boolean;
|
||||
}
|
||||
|
||||
interface StakingState {
|
||||
isChilled?: boolean;
|
||||
commission?: string;
|
||||
nominators?: NominatorValue[];
|
||||
stakeTotal?: BN;
|
||||
stakeOther?: BN;
|
||||
stakeOwn?: BN;
|
||||
}
|
||||
|
||||
function expandInfo ({ exposureMeta, exposurePaged, validatorPrefs }: ValidatorInfo, minCommission?: BN): StakingState {
|
||||
let nominators: NominatorValue[] | undefined;
|
||||
let stakeTotal: BN | undefined;
|
||||
let stakeOther: BN | undefined;
|
||||
let stakeOwn: BN | undefined;
|
||||
|
||||
if (exposureMeta?.total) {
|
||||
nominators = exposurePaged.others.map(({ value, who }) => ({
|
||||
nominatorId: who.toString(),
|
||||
value: value.unwrap()
|
||||
}));
|
||||
stakeTotal = exposureMeta.total?.unwrap() || BN_ZERO;
|
||||
stakeOwn = exposureMeta.own.unwrap();
|
||||
stakeOther = stakeTotal.sub(stakeOwn);
|
||||
}
|
||||
|
||||
const commission = (validatorPrefs as ValidatorPrefs)?.commission?.unwrap();
|
||||
|
||||
return {
|
||||
commission: commission?.toHuman(),
|
||||
isChilled: commission && minCommission && commission.isZero() && commission.lt(minCommission),
|
||||
nominators,
|
||||
stakeOther,
|
||||
stakeOwn,
|
||||
stakeTotal
|
||||
};
|
||||
}
|
||||
|
||||
const transformSlashes = {
|
||||
transform: (opt: Option<SlashingSpans>) => opt.unwrapOr(null)
|
||||
};
|
||||
|
||||
function useAddressCalls (api: ApiPromise, address: string, isMain?: boolean) {
|
||||
const params = useMemo(() => [address], [address]);
|
||||
const accountInfo = useDeriveAccountInfo(address);
|
||||
const slashingSpans = useCall<SlashingSpans | null>(!isMain && api.query.staking.slashingSpans, params, transformSlashes);
|
||||
|
||||
return { accountInfo, slashingSpans };
|
||||
}
|
||||
|
||||
function Address ({ address, className = '', filterName, hasQueries, isElected, isFavorite, isMain, isPara, lastBlock, minCommission, nominatedBy, points, recentlyOnline, toggleFavorite, validatorInfo, withIdentity }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api, apiIdentity } = useApi();
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
const { accountInfo, slashingSpans } = useAddressCalls(api, address, isMain);
|
||||
|
||||
const { commission, isChilled, nominators, stakeOther, stakeOwn } = useMemo(
|
||||
() => validatorInfo
|
||||
? expandInfo(validatorInfo, minCommission)
|
||||
: {},
|
||||
[minCommission, validatorInfo]
|
||||
);
|
||||
|
||||
const isVisible = useMemo(
|
||||
() => accountInfo ? checkVisibility(apiIdentity, address, accountInfo, filterName, withIdentity) : true,
|
||||
[accountInfo, address, filterName, apiIdentity, withIdentity]
|
||||
);
|
||||
|
||||
const statsLink = useMemo(
|
||||
() => `#/staking/query/${address}`,
|
||||
[address]
|
||||
);
|
||||
|
||||
const pointsAnimClass = useMemo(
|
||||
() => points && `greyAnim-${Date.now() % 25}`,
|
||||
[points]
|
||||
);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={`${className} isExpanded isFirst ${isExpanded ? 'packedBottom' : 'isLast'}`}>
|
||||
<Table.Column.Favorite
|
||||
address={address}
|
||||
isFavorite={isFavorite}
|
||||
toggle={toggleFavorite}
|
||||
/>
|
||||
<td className='badge together'>
|
||||
<Status
|
||||
isChilled={isChilled}
|
||||
isElected={isElected}
|
||||
isMain={isMain}
|
||||
isPara={isPara}
|
||||
isRelay={!!(api.query.parasShared || api.query.shared)?.activeValidatorIndices}
|
||||
nominators={isMain ? nominators : nominatedBy}
|
||||
onlineCount={recentlyOnline?.blockCount}
|
||||
onlineMessage={recentlyOnline?.hasMessage}
|
||||
/>
|
||||
</td>
|
||||
<td className='address all relative'>
|
||||
<AddressSmall value={address} />
|
||||
{isMain && pointsAnimClass && (
|
||||
<Tag
|
||||
className={`${pointsAnimClass} absolute`}
|
||||
color='lightgrey'
|
||||
label={points}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
{isMain
|
||||
? (
|
||||
<StakeOther
|
||||
nominators={nominators}
|
||||
stakeOther={stakeOther}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<NominatedBy
|
||||
nominators={nominatedBy}
|
||||
slashingSpans={slashingSpans}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<td className='number'>
|
||||
{commission || <span className='--tmp'>50.00%</span>}
|
||||
</td>
|
||||
{isMain && (
|
||||
<td className='number'>
|
||||
{lastBlock}
|
||||
</td>
|
||||
)}
|
||||
<Table.Column.Expand
|
||||
isExpanded={isExpanded}
|
||||
toggle={toggleIsExpanded}
|
||||
/>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className={`${className} ${isExpanded ? 'isExpanded isLast' : 'isCollapsed'} packedTop`}>
|
||||
<td colSpan={2} />
|
||||
<td
|
||||
className='columar'
|
||||
colSpan={
|
||||
isMain
|
||||
? 4
|
||||
: 3
|
||||
}
|
||||
>
|
||||
<Columar size='small'>
|
||||
<Columar.Column>
|
||||
{isMain && stakeOwn?.gtn(0) && (
|
||||
<>
|
||||
<h5>{t('own stake')}</h5>
|
||||
<FormatBalance
|
||||
value={stakeOwn}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Columar.Column>
|
||||
<Columar.Column>
|
||||
{hasQueries && (
|
||||
<>
|
||||
<h5>{t('graphs')}</h5>
|
||||
<a href={statsLink}>
|
||||
<Icon
|
||||
className='highlight--color'
|
||||
icon='chart-line'
|
||||
/>
|
||||
{t('historic results')}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
<Columar is100>
|
||||
<Columar.Column>
|
||||
<LinkExternal
|
||||
data={address}
|
||||
type='validator' // {isMain ? 'validator' : 'intention'}
|
||||
withTitle
|
||||
/>
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Address);
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Balance } from '@polkadot/types/interfaces';
|
||||
|
||||
export interface NominatorValue {
|
||||
nominatorId: string;
|
||||
value: Balance;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types';
|
||||
import type { NominatedByMap, SortedTargets, ValidatorInfo } from '@polkadot/app-staking/types';
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Filtering from '@polkadot/app-staking/Filtering';
|
||||
import Legend from '@polkadot/app-staking2/Legend';
|
||||
import { Table } from '@polkadot/react-components';
|
||||
import { useApi, useBlockAuthors, useNextTick } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Address from './Address/index.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
byAuthor: Record<string, string>;
|
||||
eraPoints: Record<string, string>;
|
||||
favorites: string[];
|
||||
hasQueries: boolean;
|
||||
isIntentions?: boolean;
|
||||
isIntentionsTrigger?: boolean;
|
||||
isOwn: boolean;
|
||||
minCommission?: BN;
|
||||
nominatedBy?: NominatedByMap;
|
||||
ownStashIds?: string[];
|
||||
paraValidators: Record<string, boolean>;
|
||||
recentlyOnline?: DeriveHeartbeats;
|
||||
setNominators?: (nominators: string[]) => void;
|
||||
stakingOverview?: DeriveStakingOverview;
|
||||
targets: SortedTargets;
|
||||
toggleFavorite: (address: string) => void;
|
||||
}
|
||||
|
||||
type AccountExtend = [string, boolean, boolean];
|
||||
|
||||
interface Filtered {
|
||||
validators?: AccountExtend[];
|
||||
waiting?: AccountExtend[];
|
||||
}
|
||||
|
||||
function filterAccounts (isOwn: boolean, accounts: string[] = [], ownStashIds: string[] = [], elected: string[], favorites: string[], without: string[]): AccountExtend[] {
|
||||
return accounts
|
||||
.filter((accountId) =>
|
||||
!without.includes(accountId) && (
|
||||
!isOwn ||
|
||||
ownStashIds.includes(accountId)
|
||||
)
|
||||
)
|
||||
.map((accountId): AccountExtend => [
|
||||
accountId,
|
||||
elected.includes(accountId),
|
||||
favorites.includes(accountId)
|
||||
])
|
||||
.sort(([accA,, isFavA]: AccountExtend, [accB,, isFavB]: AccountExtend): number => {
|
||||
const isStashA = ownStashIds.includes(accA);
|
||||
const isStashB = ownStashIds.includes(accB);
|
||||
|
||||
return isFavA === isFavB
|
||||
? isStashA === isStashB
|
||||
? 0
|
||||
: (isStashA ? -1 : 1)
|
||||
: (isFavA ? -1 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
function accountsToString (accounts: AccountId[]): string[] {
|
||||
const result = new Array<string>(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 {
|
||||
if (!stakingOverview) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const allElected = accountsToString(stakingOverview.nextElected);
|
||||
const validatorIds = accountsToString(stakingOverview.validators);
|
||||
|
||||
return {
|
||||
validators: filterAccounts(isOwn, validatorIds, ownStashIds, allElected, favorites, []),
|
||||
waiting: filterAccounts(isOwn, allElected, ownStashIds, allElected, favorites, validatorIds).concat(
|
||||
filterAccounts(isOwn, next, ownStashIds, [], favorites, allElected)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function mapValidators (infos: ValidatorInfo[]): Record<string, ValidatorInfo> {
|
||||
const result: Record<string, ValidatorInfo> = {};
|
||||
|
||||
for (let i = 0, count = infos.length; i < count; 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<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { byAuthor, eraPoints } = useBlockAuthors();
|
||||
const [nameFilter, setNameFilter] = useState<string>('');
|
||||
const isNextTick = useNextTick();
|
||||
|
||||
const { validators, waiting } = useMemo(
|
||||
() => getFiltered(isOwn, stakingOverview, favorites, targets.waitingIds, ownStashIds),
|
||||
[favorites, isOwn, ownStashIds, stakingOverview, targets]
|
||||
);
|
||||
|
||||
const list = useMemo(
|
||||
() => isNextTick
|
||||
? isIntentions
|
||||
? nominatedBy && waiting
|
||||
: validators
|
||||
: undefined,
|
||||
[isIntentions, isNextTick, nominatedBy, validators, waiting]
|
||||
);
|
||||
|
||||
const infoMap = useMemo(
|
||||
() => targets.validators && mapValidators(targets.validators),
|
||||
[targets]
|
||||
);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>(
|
||||
isIntentions
|
||||
? [
|
||||
[t('intentions'), 'start', 3],
|
||||
[t('nominators'), 'expand'],
|
||||
[t('commission'), 'number'],
|
||||
[]
|
||||
]
|
||||
: [
|
||||
[t('validators'), 'start', 3],
|
||||
[t('other stake'), 'expand'],
|
||||
[t('commission')],
|
||||
[t('last #')],
|
||||
[]
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={
|
||||
isIntentions
|
||||
? list && t('No waiting validators found')
|
||||
: list && recentlyOnline && infoMap && t('No active validators found')
|
||||
}
|
||||
emptySpinner={
|
||||
<>
|
||||
{!waiting && <div>{t('Retrieving validators')}</div>}
|
||||
{!infoMap && <div>{t('Retrieving validator info')}</div>}
|
||||
{isIntentions
|
||||
? !nominatedBy && <div>{t('Retrieving nominators')}</div>
|
||||
: !recentlyOnline && <div>{t('Retrieving online status')}</div>
|
||||
}
|
||||
{!list && <div>{t('Preparing validator list')}</div>}
|
||||
</>
|
||||
}
|
||||
filter={
|
||||
<Filtering
|
||||
nameFilter={nameFilter}
|
||||
setNameFilter={setNameFilter}
|
||||
/>
|
||||
}
|
||||
header={headerRef.current}
|
||||
legend={
|
||||
<Legend
|
||||
isRelay={!isIntentions && !!(api.query.parasShared || api.query.shared)?.activeValidatorIndices}
|
||||
minCommission={minCommission}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{list?.map(([address, isElected, isFavorite]): React.ReactNode => (
|
||||
<Address
|
||||
address={address}
|
||||
filterName={nameFilter}
|
||||
hasQueries={hasQueries}
|
||||
isElected={isElected}
|
||||
isFavorite={isFavorite}
|
||||
isMain={!isIntentions}
|
||||
isPara={paraValidators[address]}
|
||||
key={address}
|
||||
lastBlock={byAuthor[address]}
|
||||
minCommission={minCommission}
|
||||
nominatedBy={nominatedBy?.[address]}
|
||||
points={eraPoints[address]}
|
||||
recentlyOnline={recentlyOnline?.[address]}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validatorInfo={infoMap?.[address]}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CurrentList);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, styled } from '@polkadot/react-components';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
withIcon?: boolean;
|
||||
}
|
||||
|
||||
function StakingAsyncOverview ({ children, className = '', withIcon = true }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<StyledArticle className={`${className} mark warning`}>
|
||||
{withIcon && <Icon icon='exclamation-triangle' />}
|
||||
<strong>Staking Async is Polkadot's staking system</strong> which elects validators <em>for the Relay Chain</em>, <em>on AssetHub</em>.
|
||||
The actual collators of the AssetHub parachain are managed by the collator-selection system. To nominate a Relay Chain validator, please use this page and everything works as before. To setup a validator, please see{' '}
|
||||
<a
|
||||
href='https://docs.google.com/document/d/1X4EjL-7he70vtUumNhEqnUs7XdTCDj8TQpGJhNuAklY/edit?tab=t.0#heading=h.xh97bpw96bkk'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this guide
|
||||
</a>.
|
||||
<br />
|
||||
<br />
|
||||
For more information about Staking Async and AssetHub migration, please see the{' '}
|
||||
<a
|
||||
href='https://docs.google.com/document/d/1XR3vL2p4QV0wC7FrlC8eN-q62BqNFTFElbj21wEmMGg/edit?tab=t.tyioldyxov9u'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Asset Hub Migration FAQ
|
||||
</a>.
|
||||
{children}
|
||||
</StyledArticle>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
const StyledArticle = styled.article`
|
||||
max-width: 50vw;
|
||||
margin-inline: auto !important;
|
||||
|
||||
.ui--Icon {
|
||||
color: rgba(255, 196, 12, 1);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(StakingAsyncOverview);
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveStakingOverview } from '@polkadot/api-derive/types';
|
||||
import type { SortedTargets } from '@polkadot/app-staking/types';
|
||||
import type { Option, u32 } from '@polkadot/types-codec';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { CardSummary, styled, SummaryBox } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import SummarySession from './SummarySession.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
nominators?: string[];
|
||||
stakingOverview?: DeriveStakingOverview;
|
||||
targets: SortedTargets;
|
||||
}
|
||||
|
||||
const OPT_CURRENTERA = {
|
||||
transform: (currentEra: Option<u32>): BN | null =>
|
||||
currentEra.unwrapOr(null)
|
||||
};
|
||||
|
||||
const useActiveNominators = () => {
|
||||
const { api } = useApi();
|
||||
const currentEra = useCall(api.query.staking.currentEra, undefined, OPT_CURRENTERA);
|
||||
const [exposedNominators, setExposedNominators] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getExposedNominators = async () => {
|
||||
const exposedNominators = (await api.query.staking.erasStakersPaged.entries(currentEra)).map(([_, value]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
return (value).unwrap().others.map((x) => x.who.toString());
|
||||
}).flat();
|
||||
|
||||
setExposedNominators([...new Set(exposedNominators)]);
|
||||
};
|
||||
|
||||
getExposedNominators().catch(console.log);
|
||||
}, [api.query.staking.erasStakersPaged, currentEra]);
|
||||
|
||||
return exposedNominators;
|
||||
};
|
||||
|
||||
function Summary ({ className = '', stakingOverview, targets: { counterForNominators, inflation: { idealStake, inflation, stakedFraction }, waitingIds } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const activeNominators = useActiveNominators();
|
||||
|
||||
const percent = <span className='percent'>%</span>;
|
||||
|
||||
return (
|
||||
<StyledSummaryBox className={className}>
|
||||
<section>
|
||||
<CardSummary label={t('validators')}>
|
||||
{stakingOverview
|
||||
? <>{formatNumber(stakingOverview.validators.length)} / {formatNumber(stakingOverview.validatorCount)}</>
|
||||
: <span className='--tmp'>999 / 999</span>
|
||||
}
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
className='media--900'
|
||||
label={t('waiting')}
|
||||
>
|
||||
{waitingIds
|
||||
? formatNumber(waitingIds.length)
|
||||
: <span className='--tmp'>99</span>
|
||||
}
|
||||
</CardSummary>
|
||||
<CardSummary
|
||||
className='media--1000'
|
||||
label={
|
||||
counterForNominators
|
||||
? t('active / nominators')
|
||||
: t('nominators')
|
||||
}
|
||||
>
|
||||
{activeNominators
|
||||
? (
|
||||
<>
|
||||
{formatNumber(activeNominators.length)}
|
||||
{counterForNominators && (
|
||||
<> / {formatNumber(counterForNominators)}</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: <span className='--tmp'>999 / 999</span>
|
||||
}
|
||||
</CardSummary>
|
||||
</section>
|
||||
<section>
|
||||
{(idealStake > 0) && Number.isFinite(idealStake) && (
|
||||
<CardSummary
|
||||
className='media--1400'
|
||||
label={t('ideal staked')}
|
||||
>
|
||||
<>{(idealStake * 100).toFixed(1)}{percent}</>
|
||||
</CardSummary>
|
||||
)}
|
||||
{(stakedFraction > 0) && (
|
||||
<CardSummary
|
||||
className='media--1300'
|
||||
label={t('staked')}
|
||||
>
|
||||
<>{(stakedFraction * 100).toFixed(1)}{percent}</>
|
||||
</CardSummary>
|
||||
)}
|
||||
{(inflation > 0) && Number.isFinite(inflation) && (
|
||||
<CardSummary
|
||||
className='media--1200'
|
||||
label={t('inflation')}
|
||||
>
|
||||
<>{inflation.toFixed(1)}{percent}</>
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<SummarySession />
|
||||
</section>
|
||||
</StyledSummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledSummaryBox = styled(SummaryBox)`
|
||||
.validator--Account-block-icon {
|
||||
display: inline-block;
|
||||
margin-right: 0.75rem;
|
||||
margin-top: -0.25rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.validator--Summary-authors {
|
||||
.validator--Account-block-icon+.validator--Account-block-icon {
|
||||
margin-left: -1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.percent {
|
||||
font-size: var(--font-percent-tiny);
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSessionProgress } from '@polkadot/api-derive/types';
|
||||
import type { Forcing } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { CardSummary } from '@polkadot/react-components';
|
||||
import { useApi, useBlockInterval, useCall, useStakingAsyncApis } from '@polkadot/react-hooks';
|
||||
import { BN, BN_THREE, BN_TWO, BN_ZERO, formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
withEra?: boolean;
|
||||
withSession?: boolean;
|
||||
}
|
||||
|
||||
function SummarySession ({ className, withEra = true, withSession = true }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi(); // Asset Hub API
|
||||
const blockTime = useBlockInterval();
|
||||
const { rcApi } = useStakingAsyncApis();
|
||||
const sessionInfo = useCall<DeriveSessionProgress>(rcApi?.derive.session?.progress);
|
||||
const ahSessionInfo = useCall<DeriveSessionProgress>(api?.derive.session?.progress);
|
||||
const forcing = useCall<Forcing>(rcApi?.query.staking?.forceEra);
|
||||
|
||||
const eraLabel = t('era');
|
||||
const sessionLabel = rcApi?.query.babe
|
||||
? t('epoch')
|
||||
: t('session');
|
||||
const activeEraStart = sessionInfo?.activeEraStart.unwrapOr(null);
|
||||
|
||||
const eraProgress = useMemo(() => {
|
||||
if (!ahSessionInfo) {
|
||||
return BN_ZERO;
|
||||
}
|
||||
|
||||
const currentEraStart = ahSessionInfo.activeEraStart.unwrapOrDefault();
|
||||
|
||||
if (currentEraStart.isZero()) {
|
||||
return BN_ZERO;
|
||||
}
|
||||
|
||||
const currentTimestamp = new BN(Math.floor(Date.now()));
|
||||
|
||||
const elapsed = currentTimestamp.sub(currentEraStart);
|
||||
|
||||
return elapsed.div(blockTime);
|
||||
}, [ahSessionInfo, blockTime]);
|
||||
|
||||
const eraDuration = useMemo(
|
||||
() => {
|
||||
const epochDuration = rcApi?.consts.babe?.epochDuration;
|
||||
const sessionsPerEra = api?.consts.staking?.sessionsPerEra;
|
||||
|
||||
return epochDuration?.mul(sessionsPerEra);
|
||||
},
|
||||
[api?.consts.staking?.sessionsPerEra, rcApi?.consts.babe?.epochDuration]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rcApi?.derive.session && (
|
||||
<>
|
||||
{withSession && (
|
||||
rcApi?.query.babe
|
||||
? (
|
||||
<CardSummary
|
||||
apiOverride={rcApi}
|
||||
className={className}
|
||||
label={sessionLabel}
|
||||
progress={{
|
||||
isBlurred: !sessionInfo,
|
||||
total: sessionInfo?.sessionLength || BN_THREE,
|
||||
value: sessionInfo?.sessionProgress || BN_TWO,
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<CardSummary
|
||||
apiOverride={rcApi}
|
||||
label={sessionLabel}
|
||||
>
|
||||
#{sessionInfo
|
||||
? formatNumber(sessionInfo.currentIndex)
|
||||
: <span className='--tmp'>123</span>}
|
||||
{withEra && activeEraStart && <div className='isSecondary'> </div>}
|
||||
</CardSummary>
|
||||
)
|
||||
)}
|
||||
<CardSummary
|
||||
apiOverride={rcApi}
|
||||
className={className}
|
||||
label={eraLabel}
|
||||
progress={{
|
||||
isBlurred: !(sessionInfo && forcing),
|
||||
total: sessionInfo && forcing
|
||||
? forcing.isForceAlways
|
||||
? sessionInfo.sessionLength
|
||||
: eraDuration
|
||||
: BN_THREE,
|
||||
value: sessionInfo && forcing
|
||||
? forcing.isForceAlways
|
||||
? sessionInfo.sessionProgress
|
||||
: eraProgress
|
||||
: BN_TWO,
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(SummarySession);
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types';
|
||||
import type { NominatedByMap, SortedTargets } from '@polkadot/app-staking/types';
|
||||
import type { StakerState } from '@polkadot/react-hooks/types';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Button, ToggleGroup } from '@polkadot/react-components';
|
||||
import { useApi, useBlockAuthors, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import ActionsBanner from './ActionsBanner.js';
|
||||
import CurrentList from './CurrentList.js';
|
||||
import StakingAsyncOverview from './StakingAsyncOverview.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
favorites: string[];
|
||||
hasAccounts: boolean;
|
||||
hasQueries: boolean;
|
||||
minCommission?: BN;
|
||||
nominatedBy?: NominatedByMap;
|
||||
ownStashes?: StakerState[];
|
||||
paraValidators?: Record<string, boolean>;
|
||||
stakingOverview?: DeriveStakingOverview;
|
||||
targets: SortedTargets;
|
||||
toggleFavorite: (address: string) => void;
|
||||
toggleLedger?: () => void;
|
||||
toggleNominatedBy: () => void;
|
||||
}
|
||||
|
||||
const EMPTY_PARA_VALS: Record<string, boolean> = {};
|
||||
const EMPTY_BY_AUTHOR: Record<string, string> = {};
|
||||
const EMPTY_ERA_POINTS: Record<string, string> = {};
|
||||
|
||||
function Overview ({ className = '', favorites, hasAccounts, hasQueries, minCommission, nominatedBy, ownStashes, paraValidators, stakingOverview, targets, toggleFavorite, toggleLedger, toggleNominatedBy }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { byAuthor, eraPoints } = useBlockAuthors();
|
||||
const [intentIndex, _setIntentIndex] = useState(0);
|
||||
const [typeIndex, setTypeIndex] = useState(1);
|
||||
const recentlyOnline = useCall<DeriveHeartbeats>(api.derive.imOnline?.receivedHeartbeats);
|
||||
|
||||
const setIntentIndex = useCallback(
|
||||
(index: number): void => {
|
||||
index && toggleNominatedBy();
|
||||
_setIntentIndex(index);
|
||||
},
|
||||
[toggleNominatedBy]
|
||||
);
|
||||
|
||||
const filterOptions = useRef([
|
||||
{ text: t('Own validators'), value: 'mine' },
|
||||
{ text: t('All validators'), value: 'all' }
|
||||
]);
|
||||
|
||||
const intentOptions = useRef([
|
||||
{ text: t('Active'), value: 'active' },
|
||||
{ text: t('Waiting'), value: 'waiting' }
|
||||
]);
|
||||
|
||||
const ownStashIds = useMemo(
|
||||
() => ownStashes?.map(({ stashId }) => stashId),
|
||||
[ownStashes]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
toggleLedger && toggleLedger();
|
||||
}, [toggleLedger]);
|
||||
|
||||
const isOwn = typeIndex === 0;
|
||||
|
||||
return (
|
||||
<div className={`${className} staking--Overview`}>
|
||||
<StakingAsyncOverview withIcon />
|
||||
<Summary
|
||||
stakingOverview={stakingOverview}
|
||||
targets={targets}
|
||||
/>
|
||||
{hasAccounts && (ownStashes?.length === 0) && (
|
||||
<ActionsBanner />
|
||||
)}
|
||||
<Button.Group>
|
||||
<ToggleGroup
|
||||
onChange={setTypeIndex}
|
||||
options={filterOptions.current}
|
||||
value={typeIndex}
|
||||
/>
|
||||
<ToggleGroup
|
||||
onChange={setIntentIndex}
|
||||
options={intentOptions.current}
|
||||
value={intentIndex}
|
||||
/>
|
||||
</Button.Group>
|
||||
<CurrentList
|
||||
byAuthor={intentIndex === 0 ? byAuthor : EMPTY_BY_AUTHOR}
|
||||
eraPoints={intentIndex === 0 ? eraPoints : EMPTY_ERA_POINTS}
|
||||
favorites={favorites}
|
||||
hasQueries={hasQueries}
|
||||
isIntentions={intentIndex === 1}
|
||||
isOwn={isOwn}
|
||||
key={intentIndex}
|
||||
minCommission={intentIndex === 0 ? minCommission : undefined}
|
||||
nominatedBy={intentIndex === 1 ? nominatedBy : undefined}
|
||||
ownStashIds={ownStashIds}
|
||||
paraValidators={(intentIndex === 0 && paraValidators) || EMPTY_PARA_VALS}
|
||||
recentlyOnline={intentIndex === 0 ? recentlyOnline : undefined}
|
||||
stakingOverview={stakingOverview}
|
||||
targets={targets}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
Reference in New Issue
Block a user