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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -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'
/>
&nbsp;{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;
}