mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-21 21: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,142 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
||||
import type { SlashEra } from './types.js';
|
||||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { Button, Table, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCollectiveInstance } from '@pezkuwi/react-hooks';
|
||||
import { BN_ONE, isFunction } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Row from './Row.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
buttons: React.ReactNode;
|
||||
councilId: string | null;
|
||||
councilThreshold: number;
|
||||
slash: SlashEra;
|
||||
}
|
||||
|
||||
interface Proposal {
|
||||
length: number;
|
||||
proposal: SubmittableExtrinsic<'promise'>
|
||||
}
|
||||
|
||||
interface Selected {
|
||||
selected: number[];
|
||||
txAll: Proposal | null;
|
||||
txSome: Proposal | null;
|
||||
}
|
||||
|
||||
function Slashes ({ buttons, councilId, councilThreshold, slash }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const councilMod = useCollectiveInstance('council');
|
||||
const [{ selected, txAll, txSome }, setSelected] = useState<Selected>((): Selected => {
|
||||
const proposal = api.tx.staking.cancelDeferredSlash(slash.era, slash.slashes.map((_, index) => index));
|
||||
|
||||
return {
|
||||
selected: [],
|
||||
txAll: councilMod
|
||||
? { length: proposal.encodedLength, proposal }
|
||||
: null,
|
||||
txSome: null
|
||||
};
|
||||
});
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('era {{era}}/unapplied', {
|
||||
replace: {
|
||||
era: api.query.staking.earliestUnappliedSlash || !api.consts.staking.slashDeferDuration
|
||||
? slash.era.toString()
|
||||
: slash.era.sub(api.consts.staking.slashDeferDuration).sub(BN_ONE).toString()
|
||||
}
|
||||
}), 'start', 3],
|
||||
[t('reporters'), 'address'],
|
||||
[t('own')],
|
||||
[t('other')],
|
||||
[t('total')],
|
||||
[t('payout')],
|
||||
!api.query.staking.earliestUnappliedSlash && !!api.consts.staking.slashDeferDuration &&
|
||||
[t('apply')],
|
||||
[]
|
||||
]);
|
||||
|
||||
const _onSelect = useCallback(
|
||||
(index: number) => setSelected((state): Selected => {
|
||||
const selected = state.selected.includes(index)
|
||||
? state.selected.filter((i) => i !== index)
|
||||
: state.selected.concat(index).sort((a, b) => a - b);
|
||||
const proposal = selected.length
|
||||
? api.tx.staking.cancelDeferredSlash(slash.era, selected)
|
||||
: null;
|
||||
|
||||
return {
|
||||
selected,
|
||||
txAll: state.txAll,
|
||||
txSome: proposal && councilMod && isFunction(api.tx[councilMod].propose)
|
||||
? { length: proposal.encodedLength, proposal }
|
||||
: null
|
||||
};
|
||||
}),
|
||||
[api, councilMod, slash]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Summary slash={slash} />
|
||||
<Button.Group>
|
||||
{buttons}
|
||||
{councilMod && (
|
||||
<>
|
||||
<TxButton
|
||||
accountId={councilId}
|
||||
isDisabled={!txSome}
|
||||
isToplevel
|
||||
label={t('Cancel selected')}
|
||||
params={txSome && (
|
||||
api.tx[councilMod].propose.meta.args.length === 3
|
||||
? [councilThreshold, txSome.proposal, txSome.length]
|
||||
: [councilThreshold, txSome.proposal]
|
||||
)}
|
||||
tx={api.tx[councilMod].propose}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={councilId}
|
||||
isDisabled={!txAll}
|
||||
isToplevel
|
||||
label={t('Cancel all')}
|
||||
params={txAll && (
|
||||
api.tx[councilMod].propose.meta.args.length === 3
|
||||
? [councilThreshold, txAll.proposal, txAll.length]
|
||||
: [councilThreshold, txAll.proposal]
|
||||
)}
|
||||
tx={api.tx[councilMod].propose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Button.Group>
|
||||
<Table header={headerRef.current}>
|
||||
{slash.slashes.map((slash, index): React.ReactNode => (
|
||||
<Row
|
||||
index={index}
|
||||
isSelected={selected.includes(index)}
|
||||
key={index}
|
||||
onSelect={
|
||||
councilId
|
||||
? _onSelect
|
||||
: undefined
|
||||
}
|
||||
slash={slash}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Slashes);
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Slash } from './types.js';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AddressMini, AddressSmall, Badge, Checkbox, ExpanderScroll } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
onSelect?: (index: number) => void;
|
||||
slash: Slash;
|
||||
}
|
||||
|
||||
function Row ({ index, isSelected, onSelect, slash: { era, isMine, slash: { others, own, payout, reporters, validator }, total, totalOther } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
|
||||
const _onSelect = useCallback(
|
||||
() => onSelect && onSelect(index),
|
||||
[index, onSelect]
|
||||
);
|
||||
|
||||
const renderOthers = useCallback(
|
||||
() => others.map(([accountId, balance], index): React.ReactNode => (
|
||||
<AddressMini
|
||||
balance={balance}
|
||||
key={index}
|
||||
value={accountId}
|
||||
withBalance
|
||||
/>
|
||||
)),
|
||||
[others]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className='badge'>
|
||||
{isMine && (
|
||||
<Badge
|
||||
color='red'
|
||||
icon='skull-crossbones'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='address'>
|
||||
<AddressSmall value={validator} />
|
||||
</td>
|
||||
<td className='expand all'>
|
||||
{!!others.length && (
|
||||
<ExpanderScroll
|
||||
renderChildren={renderOthers}
|
||||
summary={t('Nominators ({{count}})', { replace: { count: formatNumber(others.length) } })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='address'>
|
||||
{reporters.map((reporter, index): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={index}
|
||||
value={reporter}
|
||||
/>
|
||||
))}
|
||||
</td>
|
||||
<td className='number together'>
|
||||
<FormatBalance value={own} />
|
||||
</td>
|
||||
<td className='number together'>
|
||||
<FormatBalance value={totalOther} />
|
||||
</td>
|
||||
<td className='number together'>
|
||||
<FormatBalance value={total} />
|
||||
</td>
|
||||
<td className='number together'>
|
||||
<FormatBalance value={payout} />
|
||||
</td>
|
||||
{!api.query.staking.earliestUnappliedSlash && !!api.consts.staking.slashDeferDuration && (
|
||||
<td className='number together'>
|
||||
{formatNumber(era)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<Checkbox
|
||||
isDisabled={!onSelect}
|
||||
onChange={_onSelect}
|
||||
value={isSelected}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Row);
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSessionProgress } from '@pezkuwi/api-derive/types';
|
||||
import type { SlashEra } from './types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { BN, BN_ONE, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
slash: SlashEra;
|
||||
}
|
||||
|
||||
function Summary ({ slash: { era, nominators, reporters, total, validators } }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const sessionInfo = useCall<DeriveSessionProgress>(api.derive.session?.progress);
|
||||
|
||||
const [blockProgress, blockEnd] = useMemo(
|
||||
() => sessionInfo
|
||||
? [
|
||||
sessionInfo.activeEra.sub(era).isub(BN_ONE).imul(sessionInfo.eraLength).iadd(sessionInfo.eraProgress),
|
||||
api.consts.staking.slashDeferDuration.mul(sessionInfo.eraLength)
|
||||
]
|
||||
: [new BN(0), new BN(0)],
|
||||
[api, era, sessionInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<section>
|
||||
<CardSummary label={t('validators')}>
|
||||
{formatNumber(validators.length)}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('nominators')}>
|
||||
{formatNumber(nominators.length)}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('reporters')}>
|
||||
{formatNumber(reporters.length)}
|
||||
</CardSummary>
|
||||
</section>
|
||||
{blockProgress.gtn(0) && (
|
||||
<CardSummary
|
||||
label={t('defer')}
|
||||
progress={{
|
||||
total: blockEnd,
|
||||
value: blockProgress,
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CardSummary label={t('total')}>
|
||||
<FormatBalance value={total} />
|
||||
</CardSummary>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StakerState } from '@pezkuwi/react-hooks/types';
|
||||
import type { UnappliedSlash } from '@pezkuwi/types/interfaces';
|
||||
import type { Slash, SlashEra } from './types.js';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { getSlashProposalThreshold } from '@pezkuwi/apps-config';
|
||||
import { Table, ToggleGroup } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useCollectiveMembers } from '@pezkuwi/react-hooks';
|
||||
import { BN, BN_ONE, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Era from './Era.js';
|
||||
|
||||
interface Props {
|
||||
ownStashes?: StakerState[];
|
||||
slashes: [BN, UnappliedSlash[]][];
|
||||
}
|
||||
|
||||
function calcSlashEras (slashes: [BN, UnappliedSlash[]][], ownStashes: StakerState[]): SlashEra[] {
|
||||
const slashEras: SlashEra[] = [];
|
||||
|
||||
slashes
|
||||
.reduce((rows: Slash[], [era, slashes]): Slash[] => {
|
||||
return slashes.reduce((rows: Slash[], slash): Slash[] => {
|
||||
const totalOther = slash.others.reduce((total: BN, [, value]): BN => {
|
||||
return total.add(value);
|
||||
}, new BN(0));
|
||||
|
||||
const isMine = ownStashes.some(({ stashId }): boolean => {
|
||||
return slash.validator.eq(stashId) || slash.others.some(([nominatorId]) => nominatorId.eq(stashId));
|
||||
});
|
||||
|
||||
rows.push({ era, isMine, slash, total: slash.own.add(totalOther), totalOther });
|
||||
|
||||
return rows;
|
||||
}, rows);
|
||||
}, [])
|
||||
.forEach((slash): void => {
|
||||
let slashEra = slashEras.find(({ era }) => era.eq(slash.era));
|
||||
|
||||
if (!slashEra) {
|
||||
slashEra = {
|
||||
era: slash.era,
|
||||
nominators: [],
|
||||
payout: new BN(0),
|
||||
reporters: [],
|
||||
slashes: [],
|
||||
total: new BN(0),
|
||||
validators: []
|
||||
};
|
||||
slashEras.push(slashEra);
|
||||
}
|
||||
|
||||
slashEra.payout.iadd(slash.slash.payout);
|
||||
slashEra.total.iadd(slash.total);
|
||||
slashEra.slashes.push(slash);
|
||||
|
||||
const validatorId = slash.slash.validator.toString();
|
||||
|
||||
if (!slashEra.validators.includes(validatorId)) {
|
||||
slashEra.validators.push(validatorId);
|
||||
}
|
||||
|
||||
slash.slash.others.forEach(([accountId]): void => {
|
||||
const nominatorId = accountId.toString();
|
||||
|
||||
if (slashEra && !slashEra.nominators.includes(nominatorId)) {
|
||||
slashEra.nominators.push(nominatorId);
|
||||
}
|
||||
});
|
||||
|
||||
slash.slash.reporters.forEach((accountId): void => {
|
||||
const reporterId = accountId.toString();
|
||||
|
||||
if (slashEra && !slashEra.reporters.includes(reporterId)) {
|
||||
slashEra.reporters.push(reporterId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return slashEras.sort((a, b) => b.era.cmp(a.era));
|
||||
}
|
||||
|
||||
function Slashes ({ ownStashes = [], slashes }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const { members } = useCollectiveMembers('council');
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const rows = useMemo(
|
||||
() => calcSlashEras(slashes, ownStashes),
|
||||
[ownStashes, slashes]
|
||||
);
|
||||
|
||||
const eraOpts = useMemo(
|
||||
() => rows
|
||||
.map(({ era }) =>
|
||||
api.query.staking.earliestUnappliedSlash || !api.consts.staking.slashDeferDuration
|
||||
? era
|
||||
: era.sub(api.consts.staking.slashDeferDuration).sub(BN_ONE)
|
||||
)
|
||||
.map((era) => ({
|
||||
text: t('era {{era}}', { replace: { era: formatNumber(era) } }),
|
||||
value: era.toString()
|
||||
})),
|
||||
[api, rows, t]
|
||||
);
|
||||
|
||||
const councilId = useMemo(
|
||||
() => allAccounts.find((accountId) => members.includes(accountId)) || null,
|
||||
[allAccounts, members]
|
||||
);
|
||||
|
||||
const emptyHeader = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('unapplied'), 'start']
|
||||
]);
|
||||
|
||||
if (!rows.length) {
|
||||
return (
|
||||
<Table
|
||||
empty={t('There are no unapplied/pending slashes')}
|
||||
header={emptyHeader.current}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const councilThreshold = Math.ceil((members.length || 0) * getSlashProposalThreshold(api));
|
||||
|
||||
return (
|
||||
<Era
|
||||
buttons={
|
||||
<ToggleGroup
|
||||
onChange={setSelectedIndex}
|
||||
options={eraOpts}
|
||||
value={selectedIndex}
|
||||
/>
|
||||
}
|
||||
councilId={councilId}
|
||||
councilThreshold={councilThreshold}
|
||||
key={rows[selectedIndex].era.toString()}
|
||||
slash={rows[selectedIndex]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Slashes);
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { UnappliedSlash } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export interface Slash {
|
||||
era: BN;
|
||||
isMine: boolean;
|
||||
slash: UnappliedSlash;
|
||||
total: BN;
|
||||
totalOther: BN;
|
||||
}
|
||||
|
||||
export interface SlashEra {
|
||||
era: BN;
|
||||
nominators: string[];
|
||||
payout: BN;
|
||||
reporters: string[];
|
||||
slashes: Slash[];
|
||||
validators: string[];
|
||||
total: BN;
|
||||
}
|
||||
Reference in New Issue
Block a user