mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-13 17:31:07 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { NominatedBy as NominatedByType } from '@pezkuwi/app-staking/types';
|
||||
import type { SlashingSpans } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressMini, ExpanderScroll } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { formatNumber } from '@pezkuwi/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 @pezkuwi/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@pezkuwi/types';
|
||||
import type { NominatorValue } from './types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressMini, ExpanderScroll } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN, BN_ZERO } from '@pezkuwi/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 @pezkuwi/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import MaxBadge from '@pezkuwi/app-staking/MaxBadge';
|
||||
import { Badge } from '@pezkuwi/react-components';
|
||||
import { useAccounts } from '@pezkuwi/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 @pezkuwi/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { DeriveHeartbeatAuthor } from '@pezkuwi/api-derive/types';
|
||||
import type { NominatedBy as NominatedByType, ValidatorInfo } from '@pezkuwi/app-staking/types';
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { SlashingSpans, ValidatorPrefs } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { NominatorValue } from './types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Columar, Icon, LinkExternal, Table, Tag } from '@pezkuwi/react-components';
|
||||
import { checkVisibility } from '@pezkuwi/react-components/util';
|
||||
import { useApi, useCall, useDeriveAccountInfo, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN_ZERO } from '@pezkuwi/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 @pezkuwi/app-staking-async authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Balance } from '@pezkuwi/types/interfaces';
|
||||
|
||||
export interface NominatorValue {
|
||||
nominatorId: string;
|
||||
value: Balance;
|
||||
}
|
||||
Reference in New Issue
Block a user