From dad40a45dad33e485b3b831e71aca2df9ed9714e Mon Sep 17 00:00:00 2001 From: Dominique Date: Fri, 7 Aug 2026 22:35:11 +0200 Subject: [PATCH] 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 --- .../page-staking-legacy/src/Payouts/index.tsx | 11 ++++++++--- packages/page-staking/src/Payouts/index.tsx | 11 ++++++++--- packages/react-hooks/src/useAccountInfo.ts | 11 +++++++---- packages/react-hooks/src/useOwnStashInfos.ts | 5 +++-- packages/react-hooks/src/utils/isEmpty.ts | 16 ---------------- 5 files changed, 26 insertions(+), 28 deletions(-) delete mode 100644 packages/react-hooks/src/utils/isEmpty.ts diff --git a/packages/page-staking-legacy/src/Payouts/index.tsx b/packages/page-staking-legacy/src/Payouts/index.tsx index e91bfeafc7..1ba949697d 100644 --- a/packages/page-staking-legacy/src/Payouts/index.tsx +++ b/packages/page-staking-legacy/src/Payouts/index.tsx @@ -159,8 +159,13 @@ function getOptions (blockTime: BN, eraLength: BN | undefined, historyDepth: BN function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownValidators }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const [hasOwnValidators] = useState(() => ownValidators.length !== 0); - const [myStashesIndex, setMyStashesIndex] = useState(() => hasOwnValidators ? 0 : 1); + const [selectedIndex, setSelectedIndex] = useState(); + 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(api.derive.session.eraLength); const blockTime = useBlockInterval(); @@ -226,7 +231,7 @@ function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownVal diff --git a/packages/page-staking/src/Payouts/index.tsx b/packages/page-staking/src/Payouts/index.tsx index 9cc50457db..f35a605338 100644 --- a/packages/page-staking/src/Payouts/index.tsx +++ b/packages/page-staking/src/Payouts/index.tsx @@ -162,8 +162,13 @@ function getOptions (blockTime: BN, eraLength: BN | undefined, historyDepth: BN function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownValidators }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const [hasOwnValidators] = useState(() => ownValidators.length !== 0); - const [myStashesIndex, setMyStashesIndex] = useState(() => hasOwnValidators ? 0 : 1); + const [selectedIndex, setSelectedIndex] = useState(); + 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(api.derive.session.eraLength); const blockTime = useBlockInterval(); @@ -229,7 +234,7 @@ function Payouts ({ className = '', historyDepth, isInElection, ownPools, ownVal diff --git a/packages/react-hooks/src/useAccountInfo.ts b/packages/react-hooks/src/useAccountInfo.ts index e87e6c7c30..f7965e8be9 100644 --- a/packages/react-hooks/src/useAccountInfo.ts +++ b/packages/react-hooks/src/useAccountInfo.ts @@ -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(api.query.staking?.nominators, [value]); + const nominator = useCall>(api.query.staking?.nominators, [value]); const validator = useCall(api.query.staking?.validators, [value]); const [accountIndex, setAccountIndex] = useState(undefined); const [tags, setSortedTags] = useState([]); @@ -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]); diff --git a/packages/react-hooks/src/useOwnStashInfos.ts b/packages/react-hooks/src/useOwnStashInfos.ts index cc7d770d99..f5f0e2629a 100644 --- a/packages/react-hooks/src/useOwnStashInfos.ts +++ b/packages/react-hooks/src/useOwnStashInfos.ts @@ -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; diff --git a/packages/react-hooks/src/utils/isEmpty.ts b/packages/react-hooks/src/utils/isEmpty.ts deleted file mode 100644 index dd63468f79..0000000000 --- a/packages/react-hooks/src/utils/isEmpty.ts +++ /dev/null @@ -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; -};