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:
Kurdistan Tech Ministry committed 2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 files changed
+329019

No files matched your search

@@ -0,0 +1,184 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { BatchOptions } from '@pezkuwi/react-hooks/types';
import type { u32 } from '@pezkuwi/types';
import type { EraIndex } from '@pezkuwi/types/interfaces';
import type { PayoutValidator } from './types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { AddressMini, Button, InputAddress, Modal, Static, styled, TxButton } from '@pezkuwi/react-components';
import { useApi, useToggle, useTxBatch } from '@pezkuwi/react-hooks';
import { useTranslation } from '../translate.js';
interface Props {
className?: string;
isAll?: boolean;
isDisabled?: boolean;
payout?: PayoutValidator | PayoutValidator[];
}
interface SinglePayout {
era: EraIndex;
validatorId: string;
}
function createStream (api: ApiPromise, payouts: SinglePayout[]): SubmittableExtrinsic<'promise'>[] {
return payouts
.sort((a, b) => a.era.cmp(b.era))
.map(({ era, validatorId }) =>
api.tx.staking.payoutStakers(validatorId, era)
);
}
function createExtrinsics (api: ApiPromise, payout: PayoutValidator | PayoutValidator[]): SubmittableExtrinsic<'promise'>[] | null {
if (!Array.isArray(payout)) {
const { eras, validatorId } = payout;
if (eras.every((e) => e.isClaimed)) {
return null;
}
return eras.length === 1
? [api.tx.staking.payoutStakers(validatorId, eras[0].era)]
: createStream(api, eras.filter((era) => !era.isClaimed).map((era): SinglePayout => ({ era: era.era, validatorId })));
} else if (payout.length === 1) {
if (payout[0].eras.every((e) => e.isClaimed)) {
return null;
}
return createExtrinsics(api, payout[0]);
}
if (!payout.some((p) => p.eras.some((e) => !e.isClaimed))) {
return null;
}
return createStream(api, payout.reduce((payouts: SinglePayout[], { eras, validatorId }): SinglePayout[] => {
eras.forEach(({ era, isClaimed }): void => {
if (!isClaimed) {
payouts.push({ era, validatorId });
}
});
return payouts;
}, []));
}
function PayButton ({ className, isAll, isDisabled, payout }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [isVisible, togglePayout] = useToggle();
const [accountId, setAccount] = useState<string | null>(null);
const [txs, setTxs] = useState<SubmittableExtrinsic<'promise'>[] | null>(null);
const batchOpts = useMemo<BatchOptions>(
() => ({
max: 36 * 64 / ((api.consts.staking.maxNominatorRewardedPerValidator as u32)?.toNumber() || 64),
type: 'force'
}),
[api]
);
const extrinsics = useTxBatch(txs, batchOpts);
useEffect((): void => {
payout && setTxs(
() => createExtrinsics(api, payout)
);
}, [api, payout]);
const isPayoutEmpty = !payout || (!Array.isArray(payout) && !payout.eras.some((e) => !e.isClaimed)) || (Array.isArray(payout) && payout.some((p) => !p.eras.some((e) => !e.isClaimed))) || (Array.isArray(payout) && payout.length === 0);
return (
<>
{payout && isVisible && (
<StyledModal
className={className}
header={t('Payout all stakers')}
onClose={togglePayout}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t('Any account can request payout for stakers, this is not limited to accounts that will be rewarded.')}>
<InputAddress
label={t('request payout from')}
onChange={setAccount}
type='account'
value={accountId}
/>
</Modal.Columns>
<Modal.Columns
hint={
<>
<p>{t('All the listed validators and all their nominators will receive their rewards.')}</p>
<p>{t('The UI puts a limit of 40 payouts at a time, where each payout is a single validator for a single era.')}</p>
</>
}
>
{Array.isArray(payout)
? (
<Static
label={t('payout stakers for (multiple)')}
value={
payout.map(({ validatorId }) => (
<AddressMini
className='addressStatic'
key={validatorId}
value={validatorId}
/>
))
}
/>
)
: (
<InputAddress
defaultValue={payout.validatorId}
isDisabled
label={t('payout stakers for (single)')}
/>
)
}
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={accountId}
extrinsic={extrinsics}
icon='credit-card'
isDisabled={!extrinsics?.length || !accountId}
label={t('Payout')}
onStart={togglePayout}
/>
</Modal.Actions>
</StyledModal>
)}
<Button
icon='credit-card'
isDisabled={isDisabled || isPayoutEmpty}
label={
(isAll || Array.isArray(payout))
? t('Payout all')
: t('Payout')
}
onClick={togglePayout}
/>
</>
);
}
const StyledModal = styled(Modal)`
.ui--AddressMini.padded.addressStatic {
display: inline-block;
padding-top: 0.5rem;
.ui--AddressMini-info {
min-width: 10rem;
max-width: 10rem;
}
}
`;
export default React.memo(PayButton);
@@ -0,0 +1,64 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import type { PayoutStash } from './types.js';
import React, { useEffect, useState } from 'react';
import { AddressSmall, Table } from '@pezkuwi/react-components';
import { BlockToTime } from '@pezkuwi/react-query';
import { BN_MILLION } from '@pezkuwi/util';
import useEraBlocks from './useEraBlocks.js';
import { createErasString } from './util.js';
interface Props {
className?: string;
historyDepth?: BN;
payout: PayoutStash;
}
interface EraInfo {
eraStr: React.ReactNode;
oldestEra?: BN;
}
function Stash ({ className = '', historyDepth, payout: { available, rewards, stashId } }: Props): React.ReactElement<Props> {
const [{ eraStr, oldestEra }, setEraInfo] = useState<EraInfo>({ eraStr: '' });
const eraBlocks = useEraBlocks(historyDepth, oldestEra);
useEffect((): void => {
rewards && setEraInfo({
eraStr: createErasString(rewards.filter(({ isClaimed }) => !isClaimed).map(({ era }) => era)),
oldestEra: rewards[0]?.era
});
}, [rewards]);
return (
<tr className={className}>
<td
className='address'
colSpan={2}
>
<AddressSmall value={stashId} />
</td>
<td className='start'>
<span className='payout-eras'>{eraStr}</span>
</td>
<Table.Column.Balance value={available} />
<td className='number'>
<BlockToTime
className={eraBlocks ? '' : '--tmp'}
value={eraBlocks || BN_MILLION}
/>
</td>
<td
className='button'
colSpan={3}
/>
</tr>
);
}
export default React.memo(Stash);
@@ -0,0 +1,96 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import type { PayoutValidator } from './types.js';
import React, { useMemo } from 'react';
import { AddressMini, AddressSmall, Expander, Table } from '@pezkuwi/react-components';
import { BlockToTime } from '@pezkuwi/react-query';
import { useTranslation } from '../translate.js';
import PayButton from './PayButton.js';
import useEraBlocks from './useEraBlocks.js';
import { createErasString } from './util.js';
interface Props {
className?: string;
historyDepth?: BN;
isDisabled?: boolean;
payout: PayoutValidator;
}
interface State {
eraStr: React.ReactNode;
nominators: Record<string, BN>;
numNominators: number;
oldestEra?: BN;
}
function extractState (payout: PayoutValidator): State {
const eraStr = createErasString(payout.eras.filter(({ isClaimed }) => !isClaimed).map(({ era }) => era));
const nominators = payout.eras.reduce((nominators: Record<string, BN>, { stashes }): Record<string, BN> => {
Object.entries(stashes).forEach(([stashId, value]): void => {
if (nominators[stashId]) {
nominators[stashId] = nominators[stashId].add(value);
} else {
nominators[stashId] = value;
}
});
return nominators;
}, {});
return { eraStr, nominators, numNominators: Object.keys(nominators).length, oldestEra: payout.eras[0]?.era };
}
function Validator ({ className = '', historyDepth, isDisabled, payout }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { eraStr, nominators, numNominators, oldestEra } = useMemo(
() => extractState(payout),
[payout]
);
const eraBlocks = useEraBlocks(historyDepth, oldestEra);
return (
<tr className={className}>
<td
className='address'
colSpan={2}
>
<AddressSmall value={payout.validatorId} />
</td>
<td className='start'>
<span className='payout-eras'>{eraStr}</span>
</td>
<Table.Column.Balance value={payout.available} />
<td className='number'>{eraBlocks && <BlockToTime value={eraBlocks} />}</td>
<td
className='expand'
colSpan={2}
>
<Expander summary={t('{{count}} own stashes', { replace: { count: numNominators } })}>
{Object.entries(nominators).map(([stashId, balance]) =>
<AddressMini
balance={balance}
key={stashId}
value={stashId}
withBalance
/>
)}
</Expander>
</td>
<td className='button'>
<PayButton
isDisabled={isDisabled}
payout={payout}
/>
</td>
</tr>
);
}
export default React.memo(Validator);
+307
View File
@@ -0,0 +1,307 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveStakerReward } from '@pezkuwi/api-derive/types';
import type { OwnPool } from '@pezkuwi/app-staking2/Pools/types';
import type { StakerState } from '@pezkuwi/react-hooks/types';
import type { PayoutStash, PayoutValidator } from './types.js';
import React, { useMemo, useRef, useState } from 'react';
import { Button, MarkWarning, styled, Table, ToggleGroup } from '@pezkuwi/react-components';
import { useApi, useBlockInterval, useCall, useOwnEraRewards } from '@pezkuwi/react-hooks';
import { BN, BN_THREE } from '@pezkuwi/util';
import ElectionBanner from '../ElectionBanner.js';
import { useTranslation } from '../translate.js';
import PayButton from './PayButton.js';
import Stash from './Stash.js';
import Validator from './Validator.js';
interface Props {
className?: string;
historyDepth?: BN;
isInElection?: boolean;
ownPools?: OwnPool[];
ownValidators: StakerState[];
}
interface Available {
stashAvail?: BN | null;
stashes?: PayoutStash[];
valAvail?: BN | null;
valTotal?: BN | null;
validators?: PayoutValidator[];
}
interface EraSelection {
value: number;
text: string;
}
const DAY_SECS = new BN(1000 * 60 * 60 * 24);
function groupByValidator (allRewards: Record<string, DeriveStakerReward[]>): PayoutValidator[] {
return Object
.entries(allRewards)
.reduce((grouped: PayoutValidator[], [stashId, rewards]): PayoutValidator[] => {
rewards
.forEach((reward): void => {
Object
.entries(reward.validators)
.forEach(([validatorId, { total, value }]): void => {
const entry = grouped.find((entry) => entry.validatorId === validatorId);
if (entry) {
const eraEntry = entry.eras.find((entry) => entry.era.eq(reward.era));
if (eraEntry) {
eraEntry.stashes[stashId] = value;
} else {
entry.eras.push({
era: reward.era,
isClaimed: reward.isClaimed,
stashes: { [stashId]: value }
});
}
entry.available = entry.available.add(value);
entry.total = entry.total.add(total);
} else {
grouped.push({
available: value,
eras: [{
era: reward.era,
isClaimed: reward.isClaimed,
stashes: { [stashId]: value }
}],
total,
validatorId
});
}
});
});
return grouped;
}, [])
.sort((a, b) => b.available.cmp(a.available));
}
function extractStashes (allRewards: Record<string, DeriveStakerReward[]>): PayoutStash[] {
return Object
.entries(allRewards)
.map(([stashId, rewards]): PayoutStash => ({
available: rewards.reduce((result, { validators }) =>
Object.values(validators).reduce((result, { value }) =>
result.iadd(value), result), new BN(0)
),
rewards,
stashId
}))
.filter(({ available }) => !available.isZero())
.filter(({ rewards }) => rewards.some((r) => !r.isClaimed))
.sort((a, b) => b.available.cmp(a.available));
}
function getAvailable (allRewards: Record<string, DeriveStakerReward[]> | null | undefined): Available {
if (allRewards) {
const stashes = extractStashes(allRewards);
const validators = groupByValidator(allRewards);
const stashAvail = stashes.length
? stashes.reduce<BN>((a, { available }) => a.iadd(available), new BN(0))
: null;
const [valAvail, valTotal] = validators.length
? validators.reduce<[BN, BN]>(([a, t], { available, total }) => [a.iadd(available), t.iadd(total)], [new BN(0), new BN(0)])
: [null, null];
return {
stashAvail,
stashes,
valAvail,
valTotal,
validators
};
}
return {};
}
function getOptions (blockTime: BN, eraLength: BN | undefined, historyDepth: BN | undefined, t: (key: string, options?: { replace: Record<string, unknown> }) => string): EraSelection[] {
if (!eraLength || !historyDepth) {
return [{ text: '', value: 0 }];
}
const blocksPerDay = DAY_SECS.div(blockTime);
const maxBlocks = eraLength.mul(historyDepth);
const eraSelection: EraSelection[] = [];
const days = new BN(2);
while (true) {
const dayBlocks = blocksPerDay.mul(days);
if (dayBlocks.gte(maxBlocks)) {
break;
}
eraSelection.push({
text: t('{{days}} days', { replace: { days: days.toString() } }),
value: dayBlocks.div(eraLength).toNumber()
});
days.imul(BN_THREE);
}
eraSelection.push({
text: t('Max, {{eras}} eras', { replace: { eras: historyDepth.toNumber() } }),
value: historyDepth.toNumber()
});
return eraSelection;
}
function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownValidators }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [hasOwnValidators] = useState(() => ownValidators.length !== 0);
const [myStashesIndex, setMyStashesIndex] = useState(() => hasOwnValidators ? 0 : 1);
const [eraSelectionIndex, setEraSelectionIndex] = useState(0);
const eraLength = useCall<BN>(api.derive.session.eraLength);
const blockTime = useBlockInterval();
const poolStashes = useMemo(
() => ownPools?.map(({ stashId }) => stashId),
[ownPools]
);
const eraSelection = useMemo(
() => getOptions(blockTime, eraLength, historyDepth, t),
[blockTime, eraLength, historyDepth, t]
);
const { allRewards, isLoadingRewards } = useOwnEraRewards(eraSelection[eraSelectionIndex].value, myStashesIndex ? undefined : ownValidators, poolStashes);
const { stashAvail, stashes, valAvail, validators } = useMemo(
() => getAvailable(allRewards),
[allRewards]
);
const headerStashes = useMemo<[React.ReactNode?, string?, number?][]>(
() => [
[myStashesIndex ? t('payout/stash') : t('overall/validator'), 'start', 2],
[t('eras'), 'start'],
[myStashesIndex ? t('own') : t('total')],
[('remaining')],
[undefined, undefined, 3]
],
[myStashesIndex, t]
);
const headerValidatorsRef = useRef<[React.ReactNode?, string?, number?][]>([
[t('payout/validator'), 'start', 2],
[t('eras'), 'start'],
[t('own')],
[('remaining')],
[undefined, undefined, 3]
]);
const valOptions = useMemo(() => [
{ isDisabled: !hasOwnValidators, text: t('Own validators'), value: 'val' },
{ text: t('Own stashes'), value: 'all' }
], [hasOwnValidators, t]);
const footerStash = useMemo(() => (
<tr>
<td colSpan={3} />
<Table.Column.Balance value={stashAvail} />
<td colSpan={4} />
</tr>
), [stashAvail]);
const footerVal = useMemo(() => (
<tr>
<td colSpan={3} />
<Table.Column.Balance value={valAvail} />
<td colSpan={4} />
</tr>
), [valAvail]);
return (
<StyledDiv className={className}>
<Button.Group>
<ToggleGroup
onChange={setMyStashesIndex}
options={valOptions}
value={myStashesIndex}
/>
<ToggleGroup
onChange={setEraSelectionIndex}
options={eraSelection}
value={eraSelectionIndex}
/>
<PayButton
isAll
isDisabled={isInElection}
payout={validators}
/>
</Button.Group>
<ElectionBanner isInElection={isInElection} />
{!isLoadingRewards && !stashes?.length && (
<MarkWarning
className='warning centered'
withIcon={false}
>
<p>{t('Payouts of rewards for a validator can be initiated by any account. This means that as soon as a validator or nominator requests a payout for an era, all the nominators for that validator will be rewarded. Each user does not need to claim individually and the suggestion is that validators should claim rewards for everybody as soon as an era ends.')}</p>
<p>{t('If you have not claimed rewards straight after the end of the era, the validator is in the active set and you are seeing no rewards, this would mean that the reward payout transaction was made by another account on your behalf. Always check your favorite explorer to see any historic payouts made to your accounts.')}</p>
</MarkWarning>
)}
<Table
empty={!isLoadingRewards && stashes && (
myStashesIndex
? t('No pending payouts for your stashes')
: t('No pending payouts for your validators')
)}
emptySpinner={t('Retrieving info for the selected eras, this will take some time')}
footer={footerStash}
header={headerStashes}
isFixed
>
{!isLoadingRewards && stashes?.map((payout): React.ReactNode => (
<Stash
historyDepth={historyDepth}
key={payout.stashId}
payout={payout}
/>
))}
</Table>
{(myStashesIndex === 1) && !isLoadingRewards && validators && (validators.length !== 0) && validators.filter(({ eras }) => eras.some((e) => !e.isClaimed)).length > 0 && (
<Table
footer={footerVal}
header={headerValidatorsRef.current}
isFixed
>
{!isLoadingRewards && validators.filter(({ available }) => !available.isZero()).map((payout): React.ReactNode => (
<Validator
historyDepth={historyDepth}
isDisabled={isInElection}
key={payout.validatorId}
payout={payout}
/>
))}
</Table>
)}
</StyledDiv>
);
}
const StyledDiv = styled.div`
.payout-eras {
padding-left: 0.25rem;
vertical-align: middle;
span {
white-space: nowrap;
}
}
`;
export default React.memo(Payouts);
@@ -0,0 +1,25 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveStakerReward } from '@pezkuwi/api-derive/types';
import type { Balance, EraIndex } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
export interface PayoutEraValidator {
era: EraIndex;
stashes: Record<string, Balance>;
isClaimed: boolean;
}
export interface PayoutValidator {
available: BN;
eras: PayoutEraValidator[];
validatorId: string;
total: BN;
}
export interface PayoutStash {
available: BN;
rewards: DeriveStakerReward[];
stashId: string;
}
@@ -0,0 +1,39 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveSessionProgress } from '@pezkuwi/api-derive/types';
import type { Forcing } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
import { useMemo } from 'react';
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
import { BN_ONE } from '@pezkuwi/util';
function useEraBlocksImpl (historyDepth?: BN, era?: BN): BN | undefined {
const { api } = useApi();
const progress = useCall<DeriveSessionProgress>(api.derive.session.progress);
const forcing = useCall<Forcing>(api.query.staking.forceEra);
return useMemo(
() => (historyDepth && era && forcing && progress && progress.sessionLength.gt(BN_ONE))
? (
forcing.isForceAlways
? progress.sessionLength
: progress.eraLength
).mul(
historyDepth
.sub(progress.activeEra)
.iadd(era)
.iadd(BN_ONE)
).isub(
forcing.isForceAlways
? progress.sessionProgress
: progress.eraProgress
)
: undefined,
[era, forcing, historyDepth, progress]
);
}
export default createNamedHook('useEraBlocks', useEraBlocksImpl);
@@ -0,0 +1,60 @@
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import React from 'react';
import { BN_ONE, formatNumber } from '@pezkuwi/util';
function isSingle (entry: BN | [BN, BN]): entry is BN {
return !Array.isArray(entry);
}
export function createErasString (eras: BN[]): React.ReactNode {
if (!eras.length) {
return '';
}
const parts = eras
.sort((a, b) => a.cmp(b))
.reduce((result: (BN | [BN, BN])[], era): (BN | [BN, BN])[] => {
if (result.length === 0) {
return [era];
} else {
const last = result[result.length - 1];
if (isSingle(last)) {
if (last.add(BN_ONE).eq(era)) {
result[result.length - 1] = [last, era];
} else {
result.push(era);
}
} else {
if (last[1].add(BN_ONE).eq(era)) {
last[1] = era;
} else {
result.push(era);
}
}
}
return result;
}, [])
.map((entry) =>
isSingle(entry)
? formatNumber(entry)
: `${formatNumber(entry[0])}-${formatNumber(entry[1])}`
);
return (
<>
{parts.map((section, index) => (
<React.Fragment key={section}>
{index !== 0 && ', '}
<span>{section}</span>
</React.Fragment>
))}
</>
);
}