Files
pezkuwi-apps/packages/page-staking-async/src/Actions/Account/ListNominees.tsx
T
pezkuwichain d21bfb1320 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
2026-01-07 13:05:27 +03:00

148 lines
4.8 KiB
TypeScript

// Copyright 2017-2025 @pezkuwi/app-staking-async authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveEraExposure, DeriveSessionIndexes } from '@pezkuwi/api-derive/types';
import type { BN } from '@pezkuwi/util';
import React, { useMemo } from 'react';
import { AddressMini, ExpanderScroll, MarkWarning, Spinner } from '@pezkuwi/react-components';
import { useApi, useCall } from '@pezkuwi/react-hooks';
import { isToBn } from '@pezkuwi/util';
import { useTranslation } from '../../translate.js';
import useInactives from '../useInactives.js';
interface Props {
nominating?: string[];
stashId: string;
}
const EMPTY_MAP = {};
function mapExposure (stashId: string, all: string[], eraExposure?: DeriveEraExposure): Record<string, BN> {
if (!eraExposure?.validators) {
return EMPTY_MAP;
}
const nomBalanceMap: Record<string, BN> = {};
// for every active nominee
all.forEach((nom) => {
// cycle through its nominator to find our current stash
eraExposure.validators[nom]?.others.some((o) => {
// NOTE Some chains have non-standard implementations, without value
if (o.who.eq(stashId) && isToBn(o.value)) {
nomBalanceMap[nom] = o.value.toBn();
return true;
}
return false;
});
});
return nomBalanceMap;
}
function renderNominators (stashId: string, all: string[] = [], eraExposure?: DeriveEraExposure): null | [number, () => React.ReactNode[]] {
return all.length
? [
all.length,
(): React.ReactNode[] => {
const nomBalanceMap = mapExposure(stashId, all, eraExposure);
return all.map((nomineeId, index): React.ReactNode => (
<AddressMini
balance={nomBalanceMap[nomineeId]}
key={index}
value={nomineeId}
withBalance={!!eraExposure && !!nomBalanceMap[nomineeId]}
/>
));
}
]
: null;
}
function ListNominees ({ nominating, stashId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
// 1. Fetch session info first. It's the primary dependency.
const sessionInfo = useCall<DeriveSessionIndexes>(api.query.staking && api.derive.session?.indexes);
// 2. CORRECTED: Fetch eraExposure only when sessionInfo.activeEra is available.
// Removed the broken check for 'erasStakers'.
const eraExposure = useCall<DeriveEraExposure>(sessionInfo && api.derive.staking.eraExposure, [sessionInfo?.activeEra]);
// 3. The useInactives hook is self-contained and fetches its own data.
const { nomsActive, nomsAtRisk, nomsInactive, nomsOver, nomsWaiting } = useInactives(stashId, nominating);
const [renActive, renAtRisk, renInactive, renOver, renWaiting] = useMemo(
() => [
renderNominators(stashId, nomsActive, eraExposure),
// eraExposure is not needed for at-risk, as it's not about rewards
renderNominators(stashId, nomsAtRisk),
renderNominators(stashId, nomsInactive),
renderNominators(stashId, nomsOver),
renderNominators(stashId, nomsWaiting)
],
[eraExposure, nomsActive, nomsAtRisk, nomsInactive, nomsOver, nomsWaiting, stashId]
);
const isLoading = useMemo(() =>
!eraExposure || !nominating || nomsActive === undefined,
[eraExposure, nominating, nomsActive]);
if (isLoading) {
return (
<Spinner
label='Checking validators'
variant='app'
/>
);
}
return (
<>
{renOver && (
<ExpanderScroll
className='stakeOver'
renderChildren={renOver[1]}
summary={t('Oversubscribed nominations ({{count}})', { replace: { count: renOver[0] } })}
/>
)}
{renActive && (
<ExpanderScroll
renderChildren={renActive[1]}
summary={t('Active nominations ({{count}})', { replace: { count: renActive[0] } })}
/>
)}
{renInactive && (
<ExpanderScroll
renderChildren={renInactive[1]}
summary={t('Inactive nominations ({{count}})', { replace: { count: renInactive[0] } })}
/>
)}
{renAtRisk && (
<ExpanderScroll
renderChildren={renAtRisk[1]}
summary={t('Renomination required ({{count}})', { replace: { count: renAtRisk[0] } })}
/>
)}
{renWaiting && (
<ExpanderScroll
renderChildren={renWaiting[1]}
summary={t('Waiting nominations ({{count}})', { replace: { count: renWaiting[0] } })}
/>
)}
{nomsActive && nomsInactive && (nomsActive.length === 0) && (nomsInactive.length !== 0) && (
<MarkWarning content={t('This could mean your nomination has not been applied to any validator in the active set by the election algorithm or it has been applied against a validator who is either oversubscribed or chilled.')} />
)}
</>
);
}
export default React.memo(ListNominees);