mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-11-06 23:41:09 +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:
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);
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user