Files
pezkuwi-apps/packages/page-staking/src/Actions/useInactives.ts
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

256 lines
9.5 KiB
TypeScript

// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { QueryableStorageMultiArg } from '@pezkuwi/api/types';
import type { DeriveEraExposure, DeriveEraNominatorExposure, DeriveEraValidatorExposurePaged, DeriveSessionIndexes } from '@pezkuwi/api-derive/types';
import type { Option, u16, u32 } from '@pezkuwi/types';
import type { EraIndex, Exposure, Nominations, SlashingSpans } from '@pezkuwi/types/interfaces';
import { useEffect, useState } from 'react';
import { createNamedHook, useApi, useCall, useIsMountedRef } from '@pezkuwi/react-hooks';
import { BN, BN_ZERO } from '@pezkuwi/util';
interface Inactives {
nomsActive?: string[];
nomsChilled?: string[];
nomsInactive?: string[];
nomsOver?: string[];
nomsWaiting?: string[];
}
interface ExtractStateParams {
api: ApiPromise;
stashId: string;
slashes: Option<SlashingSpans>[];
nominees: string[];
activeEra: EraIndex| undefined;
submittedIn: EraIndex;
exposures: Exposure[];
version: number | undefined;
allNominators?: DeriveEraNominatorExposure;
activeValidators?: DeriveEraValidatorExposurePaged;
}
function extractState (params: ExtractStateParams): Inactives {
const { activeEra, activeValidators, allNominators, api, exposures, nominees, slashes, stashId, submittedIn, version } = params;
if (((version && version >= 14) && !allNominators && !activeValidators) || !activeEra || !version) {
return { nomsActive: [], nomsChilled: [], nomsInactive: [], nomsOver: [], nomsWaiting: [] };
}
// / For older non-paged exposure, a reward payout was restricted to the top
// / `MaxExposurePageSize` nominators. This is to limit the i/o cost for the
// / nominator payout.
const max = api.consts.staking?.maxNominatorRewardedPerValidator as u32 || new BN(512);
/**
* NOTE With the introduction of the SlashReported event, nominators are not auto-chilled on validator slash
*
* Chilled validators / nominations
* - Chilling is the act of stepping back from any nominating or validating
* To be chilled, we have a slash era and it is later than the submission era
* (if submitted in the same, the nomination will only take effect after the era)
*/
const nomsChilled = !api.events.staking.SlashReported
? nominees.filter((_, index) => slashes[index].isNone ? false : slashes[index].unwrap().lastNonzeroSlash.gt(submittedIn))
: [];
/**
* Oversubscribed validators / nominations
* - validators that have been nominated by more than max accounts
*/
const nomsOver = exposures
.map(({ others }) =>
others.sort((a, b) => (b.value?.unwrap() || BN_ZERO).cmp(a.value?.unwrap() || BN_ZERO))
)
.map((others, index) =>
!max || max.gtn(others.map(({ who }) => who.toString()).indexOf(stashId))
? null
: nominees[index]
)
.filter((nominee): nominee is string => !!nominee && !nomsChilled.includes(nominee));
// first a blanket find of nominations not in the active set
const inactiveValidators = exposures.map((exposure, index) => exposure.others.some(({ who }) => who.eq(stashId)) ? null : nominees[index])
.filter((nominee): nominee is string => !!nominee);
/**
* Waiting validator / nomination
* - the validator is not active, not producing blocks in this era.
*/
let nomsWaiting: string[] = [];
/**
* Active validator / nomination
* - the validator your funds are bonded to,
* - they are earning rewards in the current era (they were selected to be part of the current validators set in the current era)
*/
let nomsActive: string[] = [];
/**
* Inactive validator / nomination
* - A set of nominations will be inactive when none of those nominees are participating in the current validator set
* (the set of validators currently elected to validate the network).
*/
let nomsInactive: string[] = [];
/**
* When you first nominate validators, all of them will be "waiting" in the current era.
* The nominations will take effect in the next era. One will only see active validators (and begin earning staking rewards) after two eras,
* so on the third day earliest.
*/
if (submittedIn.eq(activeEra)) {
return { nomsActive: [], nomsChilled, nomsInactive: [], nomsOver, nomsWaiting: nominees };
}
if (version >= 14) {
nomsWaiting = inactiveValidators.filter((inactive) => !activeValidators?.[inactive] && !nomsChilled.includes(inactive) && !nomsOver.includes(inactive));
nomsActive = allNominators?.[stashId] ? [allNominators?.[stashId][0].validatorId] : [];
nomsInactive = inactiveValidators.filter((nominee) => !nomsWaiting.includes(nominee) && !nomsChilled.includes(nominee) && !nomsOver.includes(nominee) && !nomsActive.includes(nominee));
return { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting };
}
/**
* Keeping this for backwards compatibility *
* For staking pallet lower than version 14
*/
nomsWaiting = exposures.map((exposure, index) =>
exposure.total?.unwrap().isZero() || (
inactiveValidators.includes(nominees[index]) &&
// it could be activeEra + 1 (currentEra for last session)
submittedIn.gte(activeEra)
)
? nominees[index]
: null
)
.filter((nominee): nominee is string => !!nominee)
.filter((nominee) => !nomsChilled.includes(nominee) && !nomsOver.includes(nominee));
nomsActive = nominees.filter((nominee) => !nomsInactive.includes(nominee) && !nomsChilled.includes(nominee) && !nomsOver.includes(nominee));
// inactive also contains waiting, remove those
nomsInactive = inactiveValidators.filter((nominee) => !nomsWaiting.includes(nominee) && !nomsChilled.includes(nominee) && !nomsOver.includes(nominee) && !nomsActive.includes(nominee));
return { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting };
}
/**
*
* @param stashId - address of the account that is performing staking
* @param nominees - the validators that the given account has nominated
* @returns
*/
function useInactivesImpl (stashId: string, nominees?: string[], eraExposure?: DeriveEraExposure): Inactives {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const [state, setState] = useState<Inactives>({});
const [exposures, setExposures] = useState<Exposure[]>([]);
const [slashes, setSlashes] = useState<Option<SlashingSpans>[]>([]);
const [submittedIn, setSubmittedIn] = useState<EraIndex>();
const indexes = useCall<DeriveSessionIndexes>(api.derive.session.indexes);
const version = useCall<u16>(api.query.staking.palletVersion)?.toNumber();
/**
* pallet updates v14 introduces ErasStakersPaged which is used by the derive `staking.eraExposure`
*/
useEffect(() => {
if (version && version >= 14 && !eraExposure) {
return;
}
const exposuresData = nominees?.map((id) => eraExposure?.validators?.[id]).filter((val) => val) as Exposure[];
mountedRef.current && exposuresData?.length && nominees?.length && !!submittedIn && setState(
extractState({
activeEra: indexes?.activeEra,
activeValidators: eraExposure?.validators,
allNominators: eraExposure?.nominators,
api,
exposures: exposuresData,
nominees,
slashes,
stashId,
submittedIn,
version
})
);
}, [api, stashId, slashes, nominees, indexes, submittedIn, eraExposure, version, mountedRef]);
/**
* These calls are used by both staking pallet before v14 and after
*/
useEffect((): () => void => {
let unsub: (() => void) | undefined;
if (mountedRef.current && nominees?.length && indexes) {
api.queryMulti(
[[api.query.staking.nominators, stashId] as QueryableStorageMultiArg<'promise'>]
.concat(
nominees.map((id) => [api.query.staking.slashingSpans, id]))
, ([optNominators, ...slashingSpans]: [Option<Nominations>, ...(Option<SlashingSpans>)[]]): void => {
setSubmittedIn(optNominators.unwrapOrDefault().submittedIn);
setSlashes(slashingSpans);
})
.then((_unsub): void => {
unsub = _unsub;
})
.catch(console.error);
}
return (): void => {
unsub && unsub();
};
}, [api, indexes, mountedRef, nominees, stashId]);
/**
* Deprecated calls for exposure
* - erasStakers - deprecated in v14
* - stakers - deprecated earlier
*/
useEffect((): () => void => {
let unsub: (() => void) | undefined;
if (version && version < 14 && mountedRef.current && nominees?.length && indexes) {
api.queryMulti(
api.query.staking.erasStakers
? nominees.map((id) => [api.query.staking.erasStakers, [indexes?.activeEra, id]])
: nominees.map((id) => [api.query.staking.stakers, id])
, (exposures: Exposure[]): void => setExposures(exposures))
.then((_unsub): void => {
unsub = _unsub;
})
.catch(console.error);
}
return (): void => {
unsub && unsub();
};
}, [api, indexes, mountedRef, nominees, stashId, version]);
/**
* Extracting state for deprecated calls
*/
useEffect(() => {
if (exposures.length && slashes.length && nominees?.length && !!submittedIn) {
mountedRef.current && setState(
extractState({
activeEra: indexes?.activeEra,
api,
exposures,
nominees,
slashes,
stashId,
submittedIn,
version
})
);
}
}, [api, stashId, slashes, nominees, indexes, submittedIn, exposures, version, mountedRef]);
return state;
}
export default createNamedHook('useInactives', useInactivesImpl);