fix: validators reported as idle in staking accounts (#12391)

* fix: validators reported as idle in staking accounts

* refactor: detect validators via isStorageFallback in useOwnStashInfos

* refactor: rename setMyStashesIndex to setSelectedIndex in Payouts

* refactor: detect validators via isStorageFallback in useAccountInfo

* updated comments to be more succinct

* fix: reset Payouts toggle when own validators empty
This commit is contained in:
Dominique 2026-08-07 22:35:11 +02:00 committed by GitHub
parent 65dd9cba45
commit dad40a45da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 26 additions and 28 deletions

View File

@ -159,8 +159,13 @@ function getOptions (blockTime: BN, eraLength: BN | undefined, historyDepth: BN
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 [selectedIndex, setSelectedIndex] = useState<number | undefined>();
const hasOwnValidators = ownValidators.length !== 0;
// "Own validators" (index 0) is disabled when there are none. Testing that before
// `selectedIndex` means we neither default to it, nor stay on it if the list empties.
const myStashesIndex = hasOwnValidators
? (selectedIndex ?? 0)
: 1;
const [eraSelectionIndex, setEraSelectionIndex] = useState(0);
const eraLength = useCall<BN>(api.derive.session.eraLength);
const blockTime = useBlockInterval();
@ -226,7 +231,7 @@ function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownVal
<StyledDiv className={className}>
<Button.Group>
<ToggleGroup
onChange={setMyStashesIndex}
onChange={setSelectedIndex}
options={valOptions}
value={myStashesIndex}
/>

View File

@ -162,8 +162,13 @@ function getOptions (blockTime: BN, eraLength: BN | undefined, historyDepth: BN
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 [selectedIndex, setSelectedIndex] = useState<number | undefined>();
const hasOwnValidators = ownValidators.length !== 0;
// "Own validators" (index 0) is disabled when there are none. Testing that before
// `selectedIndex` means we neither default to it, nor stay on it if the list empties.
const myStashesIndex = hasOwnValidators
? (selectedIndex ?? 0)
: 1;
const [eraSelectionIndex, setEraSelectionIndex] = useState(0);
const eraLength = useCall<BN>(api.derive.session.eraLength);
const blockTime = useBlockInterval();
@ -229,7 +234,7 @@ function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownVal
<StyledDiv className={className}>
<Button.Group>
<ToggleGroup
onChange={setMyStashesIndex}
onChange={setSelectedIndex}
options={valOptions}
value={myStashesIndex}
/>

View File

@ -1,6 +1,7 @@
// Copyright 2017-2026 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '@polkadot/types';
import type { Nominations, ValidatorPrefs } from '@polkadot/types/interfaces';
import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types';
import type { HexString } from '@polkadot/util/types';
@ -11,7 +12,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { keyring } from '@polkadot/ui-keyring';
import { isFunction, isHex } from '@polkadot/util';
import { isEmpty } from './utils/isEmpty.js';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
@ -45,7 +45,7 @@ function useAccountInfoImpl (value: string | null, isContract = false): UseAccou
const { accounts: { isAccount }, addresses: { isAddress } } = useKeyring();
const accountInfo = useDeriveAccountInfo(value);
const accountFlags = useDeriveAccountFlags(value);
const nominator = useCall<Nominations>(api.query.staking?.nominators, [value]);
const nominator = useCall<Option<Nominations>>(api.query.staking?.nominators, [value]);
const validator = useCall<ValidatorPrefs>(api.query.staking?.validators, [value]);
const [accountIndex, setAccountIndex] = useState<string | undefined>(undefined);
const [tags, setSortedTags] = useState<string[]>([]);
@ -57,17 +57,20 @@ function useAccountInfoImpl (value: string | null, isContract = false): UseAccou
const [isEditingName, toggleIsEditingName, setIsEditingName] = useToggle();
const [isEditingTags, toggleIsEditingTags, setIsEditingTags] = useToggle();
// `staking.validators` is a ValueQuery map, so a non-validator reads as the default
// prefs - only `isStorageFallback` separates them (`undefined` when the entry exists)
useEffect((): void => {
validator && setFlags((flags) => ({
...flags,
isValidator: !isEmpty(validator)
isValidator: !validator.isStorageFallback
}));
}, [validator]);
// `staking.nominators` is an OptionQuery map, so absence shows on the value itself
useEffect((): void => {
nominator && setFlags((flags) => ({
...flags,
isNominator: !isEmpty(nominator)
isNominator: !nominator.isNone
}));
}, [nominator]);

View File

@ -12,7 +12,6 @@ import { useEffect, useMemo, useState } from 'react';
import { u8aConcat, u8aToHex } from '@polkadot/util';
import { isEmpty } from './utils/isEmpty.js';
import { createNamedHook } from './createNamedHook.js';
import { useAccounts } from './useAccounts.js';
import { useApi } from './useApi.js';
@ -38,7 +37,9 @@ const QUERY_OPTS = {
function getStakerState (stashId: string, allAccounts: string[], [isOwnStash, { claimedRewardsEras, controllerId: _controllerId, exposureMeta, exposurePaged, nextSessionIds: _nextSessionIds, nominators, rewardDestination, sessionIds: _sessionIds, stakingLedger, validatorPrefs }, validateInfo]: [boolean, DeriveStakingAccount, ValidatorInfo]): StakerState {
const isStashNominating = !!(nominators?.length);
const isStashValidating = !(Array.isArray(validateInfo) ? isEmpty(validateInfo[1] as ValidatorPrefs) : isEmpty(validateInfo));
// `staking.validators` is a ValueQuery map, so a non-validator reads as the default
// prefs - only `isStorageFallback` separates them (`undefined` when the entry exists)
const isStashValidating = !validateInfo.isStorageFallback;
const nextSessionIds = _nextSessionIds instanceof Map
? [..._nextSessionIds.values()]
: _nextSessionIds;

View File

@ -1,16 +0,0 @@
// Copyright 2017-2026 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Struct } from '@polkadot/types-codec';
export const isEmpty = (struct: Struct) => {
if (struct.values) {
for (const v of struct.values()) {
if (!v.isEmpty) {
return false;
}
}
}
return true;
};