Skip to content

Commit

Permalink
Showing waiting, active and inactive nominations on the Accounts page (
Browse files Browse the repository at this point in the history
…#10998)

* fix: showing waiting, active and inactive nominations, staking pallet changes applied

* setting a defualt number of max nominations per validator

* refactored to split the deprecated calls from new calls

* added spinner for a long call to exposure derive

* changed type
  • Loading branch information
piggydoughnut authored Nov 4, 2024
1 parent dcd7479 commit 3ec308f
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 71 deletions.
13 changes: 11 additions & 2 deletions packages/page-staking/src/Actions/Account/ListNominees.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { BN } from '@polkadot/util';

import React, { useMemo } from 'react';

import { AddressMini, ExpanderScroll, MarkWarning } from '@polkadot/react-components';
import { AddressMini, ExpanderScroll, MarkWarning, Spinner } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { isFunction, isToBn } from '@polkadot/util';

Expand Down Expand Up @@ -68,15 +68,24 @@ function renderNominators (stashId: string, all: string[] = [], eraExposure?: De
function ListNominees ({ nominating, stashId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting } = useInactives(stashId, nominating);
const sessionInfo = useCall<DeriveSessionIndexes>(api.query.staking && api.derive.session?.indexes);
const eraExposure = useCall<DeriveEraExposure>(isFunction(api.query.staking.erasStakers) && api.derive.staking.eraExposure, [sessionInfo?.activeEra]);
const { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting } = useInactives(stashId, nominating, eraExposure);

const [renActive, renChilled, renInactive, renOver, renWaiting] = useMemo(
() => [renderNominators(stashId, nomsActive, eraExposure), renderNominators(stashId, nomsChilled), renderNominators(stashId, nomsInactive), renderNominators(stashId, nomsOver), renderNominators(stashId, nomsWaiting)],
[eraExposure, nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting, stashId]
);

if (!nomsInactive && !nomsWaiting) {
return (
<Spinner
label='Checking validators'
variant='app'
/>
);
}

return (
<>
{renOver && (
Expand Down
259 changes: 190 additions & 69 deletions packages/page-staking/src/Actions/useInactives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

import type { ApiPromise } from '@polkadot/api';
import type { QueryableStorageMultiArg } from '@polkadot/api/types';
import type { DeriveSessionIndexes } from '@polkadot/api-derive/types';
import type { Option, u32 } from '@polkadot/types';
import type { DeriveEraExposure, DeriveEraNominatorExposure, DeriveEraValidatorExposurePaged, DeriveSessionIndexes } from '@polkadot/api-derive/types';
import type { Option, u16, u32 } from '@polkadot/types';
import type { EraIndex, Exposure, Nominations, SlashingSpans } from '@polkadot/types/interfaces';

import { useEffect, useState } from 'react';

import { createNamedHook, useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { BN, BN_ZERO } from '@polkadot/util';

interface Inactives {
nomsActive?: string[];
Expand All @@ -20,28 +20,50 @@ interface Inactives {
nomsWaiting?: string[];
}

function extractState (api: ApiPromise, stashId: string, slashes: Option<SlashingSpans>[], nominees: string[], { activeEra }: DeriveSessionIndexes, submittedIn: EraIndex, exposures: Exposure[]): Inactives {
const max = api.consts.staking?.maxNominatorRewardedPerValidator as u32;
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;
}

// chilled
// NOTE With the introduction of the SlashReported event,
// nominators are not auto-chilled on validator slash
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
// 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)
: slashes[index].unwrap().lastNonzeroSlash.gt(submittedIn)
)
? nominees.filter((_, index) => slashes[index].isNone ? false : slashes[index].unwrap().lastNonzeroSlash.gt(submittedIn))
: [];

// all nominations that are oversubscribed
/**
* 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)
)
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))
Expand All @@ -51,83 +73,182 @@ function extractState (api: ApiPromise, stashId: string, slashes: Option<Slashin
.filter((nominee): nominee is string => !!nominee && !nomsChilled.includes(nominee));

// first a blanket find of nominations not in the active set
let nomsInactive = exposures
.map((exposure, index) =>
exposure.others.some(({ who }) => who.eq(stashId))
? null
: nominees[index]
)
const inactiveValidators = exposures.map((exposure, index) => exposure.others.some(({ who }) => who.eq(stashId)) ? null : nominees[index])
.filter((nominee): nominee is string => !!nominee);

// waiting if validator is inactive or we have not submitted long enough ago
const nomsWaiting = exposures
.map((exposure, index) =>
exposure.total?.unwrap().isZero() || (
nomsInactive.includes(nominees[index]) &&
// it could be activeEra + 1 (currentEra for last session)
submittedIn.gte(activeEra)
)
? nominees[index]
: null
/**
* 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));

// filter based on all inactives
const nomsActive = nominees.filter((nominee) => !nomsInactive.includes(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 = nomsInactive.filter((nominee) => !nomsWaiting.includes(nominee) && !nomsChilled.includes(nominee) && !nomsOver.includes(nominee));

return {
nomsActive,
nomsChilled,
nomsInactive,
nomsOver,
nomsWaiting
};
nomsInactive = inactiveValidators.filter((nominee) => !nomsWaiting.includes(nominee) && !nomsChilled.includes(nominee) && !nomsOver.includes(nominee) && !nomsActive.includes(nominee));

return { nomsActive, nomsChilled, nomsInactive, nomsOver, nomsWaiting };
}

function useInactivesImpl (stashId: string, nominees?: string[]): Inactives {
/**
*
* @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(
api.query.staking.erasStakers
? nominees.map((id) => [api.query.staking.erasStakers, [indexes.activeEra, id]])
: nominees.map((id) => [api.query.staking.stakers, id])
)
.concat(
nominees.map((id) => [api.query.staking.slashingSpans, id])
),
([optNominators, ...exposuresAndSpans]: [Option<Nominations>, ...(Exposure | Option<SlashingSpans>)[]]): void => {
const exposures = exposuresAndSpans.slice(0, nominees.length) as Exposure[];
const slashes = exposuresAndSpans.slice(nominees.length) as Option<SlashingSpans>[];

mountedRef.current && setState(
extractState(api, stashId, slashes, nominees, indexes, optNominators.unwrapOrDefault().submittedIn, exposures)
);
}
)
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);
})
.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;
}

Expand Down

0 comments on commit 3ec308f

Please sign in to comment.