From 3564a43ea441031cccf2cf9f465cae75bb800a94 Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Mon, 6 Apr 2026 16:33:21 +0200 Subject: [PATCH 1/8] feat(explorer): bring back inactive validators --- .../hooks/stake/useGetInactiveValidator.ts | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/core/src/hooks/stake/useGetInactiveValidator.ts b/apps/core/src/hooks/stake/useGetInactiveValidator.ts index 5e16864e954..4c753b977d5 100644 --- a/apps/core/src/hooks/stake/useGetInactiveValidator.ts +++ b/apps/core/src/hooks/stake/useGetInactiveValidator.ts @@ -5,29 +5,51 @@ import { useQuery } from '@tanstack/react-query'; import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; import { getInactiveValidatorsMetadata } from '../../utils'; +import type { InactiveValidatorData } from '../../types'; export function useGetInactiveValidator(validatorAddress: string) { const iotaClient = useIotaClient(); const { data } = useIotaClientQuery('getLatestIotaSystemState'); const inactivePoolsId = data?.inactivePoolsId; + const validatorCandidatesId = data?.validatorCandidatesId; return useQuery({ - queryKey: ['inactive-validators', inactivePoolsId], + queryKey: ['inactive-validators', inactivePoolsId, validatorCandidatesId], async queryFn() { - if (!inactivePoolsId) { + if (!inactivePoolsId && !validatorCandidatesId) { throw Error('Missing params'); } - const inactiveValidators = await iotaClient.getDynamicFields({ - parentId: normalizeIotaAddress(inactivePoolsId), - }); - return Promise.all( - inactiveValidators.data.map((validator) => - getInactiveValidatorsMetadata(iotaClient, validator.objectId), - ), - ); + + const results: (InactiveValidatorData | null)[] = []; + + if (inactivePoolsId) { + const inactiveValidators = await iotaClient.getDynamicFields({ + parentId: normalizeIotaAddress(inactivePoolsId), + }); + const inactiveData = await Promise.all( + inactiveValidators.data.map((validator) => + getInactiveValidatorsMetadata(iotaClient, validator.objectId), + ), + ); + results.push(...inactiveData); + } + + if (validatorCandidatesId) { + const candidateValidators = await iotaClient.getDynamicFields({ + parentId: normalizeIotaAddress(validatorCandidatesId), + }); + const candidateData = await Promise.all( + candidateValidators.data.map((validator) => + getInactiveValidatorsMetadata(iotaClient, validator.objectId), + ), + ); + results.push(...candidateData); + } + + return results; }, select(data) { return data.find((v) => v?.validatorAddress === validatorAddress) ?? null; }, - enabled: !!inactivePoolsId, + enabled: !!inactivePoolsId || !!validatorCandidatesId, }); } From 0e6e6acd54c5bbdac338e3a3aedbd7f0151209c0 Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Wed, 8 Apr 2026 11:36:40 +0200 Subject: [PATCH 2/8] feat(explorer): resolve validator candidate --- apps/core/src/hooks/stake/index.ts | 1 + .../hooks/stake/useGetInactiveValidator.ts | 43 +++++-------------- .../hooks/stake/useGetValidatorCandidate.ts | 34 +++++++++++++++ .../src/pages/validator/ValidatorDetails.tsx | 31 ++++++++++++- 4 files changed, 75 insertions(+), 34 deletions(-) create mode 100644 apps/core/src/hooks/stake/useGetValidatorCandidate.ts diff --git a/apps/core/src/hooks/stake/index.ts b/apps/core/src/hooks/stake/index.ts index bf3cfb32f4a..6f9217aa2a1 100644 --- a/apps/core/src/hooks/stake/index.ts +++ b/apps/core/src/hooks/stake/index.ts @@ -9,3 +9,4 @@ export * from './useStakeTxnInfo'; export * from './useNewStakeTransaction'; export * from './useNewUnstakeTransaction'; export * from './useGetInactiveValidator'; +export * from './useGetValidatorCandidate'; diff --git a/apps/core/src/hooks/stake/useGetInactiveValidator.ts b/apps/core/src/hooks/stake/useGetInactiveValidator.ts index 4c753b977d5..3b8fa361b51 100644 --- a/apps/core/src/hooks/stake/useGetInactiveValidator.ts +++ b/apps/core/src/hooks/stake/useGetInactiveValidator.ts @@ -5,51 +5,30 @@ import { useQuery } from '@tanstack/react-query'; import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; import { getInactiveValidatorsMetadata } from '../../utils'; -import type { InactiveValidatorData } from '../../types'; export function useGetInactiveValidator(validatorAddress: string) { const iotaClient = useIotaClient(); const { data } = useIotaClientQuery('getLatestIotaSystemState'); const inactivePoolsId = data?.inactivePoolsId; - const validatorCandidatesId = data?.validatorCandidatesId; return useQuery({ - queryKey: ['inactive-validators', inactivePoolsId, validatorCandidatesId], + queryKey: ['inactive-validators', inactivePoolsId], async queryFn() { - if (!inactivePoolsId && !validatorCandidatesId) { + if (!inactivePoolsId) { throw Error('Missing params'); } - const results: (InactiveValidatorData | null)[] = []; - - if (inactivePoolsId) { - const inactiveValidators = await iotaClient.getDynamicFields({ - parentId: normalizeIotaAddress(inactivePoolsId), - }); - const inactiveData = await Promise.all( - inactiveValidators.data.map((validator) => - getInactiveValidatorsMetadata(iotaClient, validator.objectId), - ), - ); - results.push(...inactiveData); - } - - if (validatorCandidatesId) { - const candidateValidators = await iotaClient.getDynamicFields({ - parentId: normalizeIotaAddress(validatorCandidatesId), - }); - const candidateData = await Promise.all( - candidateValidators.data.map((validator) => - getInactiveValidatorsMetadata(iotaClient, validator.objectId), - ), - ); - results.push(...candidateData); - } - - return results; + const inactiveValidators = await iotaClient.getDynamicFields({ + parentId: normalizeIotaAddress(inactivePoolsId), + }); + return Promise.all( + inactiveValidators.data.map((validator) => + getInactiveValidatorsMetadata(iotaClient, validator.objectId), + ), + ); }, select(data) { return data.find((v) => v?.validatorAddress === validatorAddress) ?? null; }, - enabled: !!inactivePoolsId || !!validatorCandidatesId, + enabled: !!inactivePoolsId, }); } diff --git a/apps/core/src/hooks/stake/useGetValidatorCandidate.ts b/apps/core/src/hooks/stake/useGetValidatorCandidate.ts new file mode 100644 index 00000000000..25fd32ab829 --- /dev/null +++ b/apps/core/src/hooks/stake/useGetValidatorCandidate.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useQuery } from '@tanstack/react-query'; +import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; +import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; +import { getInactiveValidatorsMetadata } from '../../utils'; + +export function useGetValidatorCandidate(validatorAddress: string) { + const iotaClient = useIotaClient(); + const { data } = useIotaClientQuery('getLatestIotaSystemState'); + const validatorCandidatesId = data?.validatorCandidatesId; + return useQuery({ + queryKey: ['validator-candidates', validatorCandidatesId], + async queryFn() { + if (!validatorCandidatesId) { + throw Error('Missing params'); + } + + const candidateValidators = await iotaClient.getDynamicFields({ + parentId: normalizeIotaAddress(validatorCandidatesId), + }); + return Promise.all( + candidateValidators.data.map((validator) => + getInactiveValidatorsMetadata(iotaClient, validator.objectId), + ), + ); + }, + select(data) { + return data.find((v) => v?.validatorAddress === validatorAddress) ?? null; + }, + enabled: !!validatorCandidatesId, + }); +} diff --git a/apps/explorer/src/pages/validator/ValidatorDetails.tsx b/apps/explorer/src/pages/validator/ValidatorDetails.tsx index 02de8c4dbd4..268355b3e57 100644 --- a/apps/explorer/src/pages/validator/ValidatorDetails.tsx +++ b/apps/explorer/src/pages/validator/ValidatorDetails.tsx @@ -2,7 +2,12 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useGetInactiveValidator, useGetValidatorsApy, useGetValidatorsEvents } from '@iota/core'; +import { + useGetInactiveValidator, + useGetValidatorCandidate, + useGetValidatorsApy, + useGetValidatorsEvents, +} from '@iota/core'; import { useParams } from 'react-router-dom'; import { InactiveValidators, PageLayout, ValidatorMeta, ValidatorStats } from '~/components'; import { VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib/constants'; @@ -30,6 +35,9 @@ function ValidatorDetails(): JSX.Element { const { data: inactiveValidatorData, isLoading: isInactiveValidatorLoading } = useGetInactiveValidator(id || ''); + const { data: validatorCandidateData, isLoading: isValidatorCandidateLoading } = + useGetValidatorCandidate(id || ''); + const numberOfValidators = systemStateData?.activeValidators.length ?? null; const { data: rollingAverageApys, isLoading: isValidatorsApysLoading } = useGetValidatorsApy(); const { data: validatorEvents, isLoading: isValidatorsEventsLoading } = useGetValidatorsEvents({ @@ -56,7 +64,8 @@ function ValidatorDetails(): JSX.Element { isLoadingSystemState || isValidatorsEventsLoading || isValidatorsApysLoading || - isInactiveValidatorLoading + isInactiveValidatorLoading || + isValidatorCandidateLoading ) { return } />; } @@ -81,6 +90,24 @@ function ValidatorDetails(): JSX.Element { ); } + if (validatorCandidateData && !activeValidatorData) { + return ( + + } + type={InfoBoxType.Default} + style={InfoBoxStyle.Elevated} + /> + + + } + /> + ); + } + if (!activeValidatorData || !systemStateData || !validatorEvents || !id) { return ( Date: Wed, 8 Apr 2026 11:46:52 +0200 Subject: [PATCH 3/8] fmt --- apps/core/src/hooks/stake/useGetInactiveValidator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/core/src/hooks/stake/useGetInactiveValidator.ts b/apps/core/src/hooks/stake/useGetInactiveValidator.ts index 3b8fa361b51..5e16864e954 100644 --- a/apps/core/src/hooks/stake/useGetInactiveValidator.ts +++ b/apps/core/src/hooks/stake/useGetInactiveValidator.ts @@ -16,7 +16,6 @@ export function useGetInactiveValidator(validatorAddress: string) { if (!inactivePoolsId) { throw Error('Missing params'); } - const inactiveValidators = await iotaClient.getDynamicFields({ parentId: normalizeIotaAddress(inactivePoolsId), }); From bd1d421648f879c8a0c82034cdf41c47e40b9314 Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Wed, 8 Apr 2026 12:10:50 +0200 Subject: [PATCH 4/8] rename to align --- apps/core/src/hooks/stake/useGetInactiveValidator.ts | 4 ++-- apps/core/src/hooks/stake/useGetValidatorCandidate.ts | 4 ++-- apps/core/src/types/validators.ts | 2 +- ...activeValidatorsMetadata.ts => getValidatorsMetadata.ts} | 6 +++--- apps/core/src/utils/stake/index.ts | 2 +- apps/explorer/src/components/validator/ValidatorMeta.tsx | 6 +++--- apps/explorer/src/pages/validator/ValidatorDetails.tsx | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) rename apps/core/src/utils/stake/{getInactiveValidatorsMetadata.ts => getValidatorsMetadata.ts} (91%) diff --git a/apps/core/src/hooks/stake/useGetInactiveValidator.ts b/apps/core/src/hooks/stake/useGetInactiveValidator.ts index 5e16864e954..78b7589816a 100644 --- a/apps/core/src/hooks/stake/useGetInactiveValidator.ts +++ b/apps/core/src/hooks/stake/useGetInactiveValidator.ts @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; -import { getInactiveValidatorsMetadata } from '../../utils'; +import { getValidatorsMetadata } from '../../utils'; export function useGetInactiveValidator(validatorAddress: string) { const iotaClient = useIotaClient(); @@ -21,7 +21,7 @@ export function useGetInactiveValidator(validatorAddress: string) { }); return Promise.all( inactiveValidators.data.map((validator) => - getInactiveValidatorsMetadata(iotaClient, validator.objectId), + getValidatorsMetadata(iotaClient, validator.objectId), ), ); }, diff --git a/apps/core/src/hooks/stake/useGetValidatorCandidate.ts b/apps/core/src/hooks/stake/useGetValidatorCandidate.ts index 25fd32ab829..73b6ab62725 100644 --- a/apps/core/src/hooks/stake/useGetValidatorCandidate.ts +++ b/apps/core/src/hooks/stake/useGetValidatorCandidate.ts @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; -import { getInactiveValidatorsMetadata } from '../../utils'; +import { getValidatorsMetadata } from '../../utils'; export function useGetValidatorCandidate(validatorAddress: string) { const iotaClient = useIotaClient(); @@ -22,7 +22,7 @@ export function useGetValidatorCandidate(validatorAddress: string) { }); return Promise.all( candidateValidators.data.map((validator) => - getInactiveValidatorsMetadata(iotaClient, validator.objectId), + getValidatorsMetadata(iotaClient, validator.objectId), ), ); }, diff --git a/apps/core/src/types/validators.ts b/apps/core/src/types/validators.ts index df0eca1bc36..95de4438b49 100644 --- a/apps/core/src/types/validators.ts +++ b/apps/core/src/types/validators.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export type InactiveValidatorData = { +export type ValidatorOverviewData = { imageUrl: string; name: string; description: string; diff --git a/apps/core/src/utils/stake/getInactiveValidatorsMetadata.ts b/apps/core/src/utils/stake/getValidatorsMetadata.ts similarity index 91% rename from apps/core/src/utils/stake/getInactiveValidatorsMetadata.ts rename to apps/core/src/utils/stake/getValidatorsMetadata.ts index a2f43b519d8..a221418292b 100644 --- a/apps/core/src/utils/stake/getInactiveValidatorsMetadata.ts +++ b/apps/core/src/utils/stake/getValidatorsMetadata.ts @@ -3,12 +3,12 @@ import type { IotaClient } from '@iota/iota-sdk/client'; import { normalizeIotaAddress, toBase64 } from '@iota/iota-sdk/utils'; -import { InactiveValidatorData, ValidatorSchema, DynamicFieldObjectSchema } from '../../types'; +import { ValidatorOverviewData, ValidatorSchema, DynamicFieldObjectSchema } from '../../types'; -export async function getInactiveValidatorsMetadata( +export async function getValidatorsMetadata( client: IotaClient, validatorObjectId: string, -): Promise { +): Promise { const validatorObject = await client.getObject({ id: normalizeIotaAddress(validatorObjectId), options: { diff --git a/apps/core/src/utils/stake/index.ts b/apps/core/src/utils/stake/index.ts index 076346afc95..16768da82bf 100644 --- a/apps/core/src/utils/stake/index.ts +++ b/apps/core/src/utils/stake/index.ts @@ -12,5 +12,5 @@ export * from './checkIfIsTimelockedStaking'; export * from './getUnstakeDetailsFromEvents'; export * from './getTransactionAmountForTimelocked'; export * from './getValidatorEffectiveCommission'; -export * from './getInactiveValidatorsMetadata'; +export * from './getValidatorsMetadata'; export * from './types'; diff --git a/apps/explorer/src/components/validator/ValidatorMeta.tsx b/apps/explorer/src/components/validator/ValidatorMeta.tsx index 034675b63e7..e2da5f9b2cd 100644 --- a/apps/explorer/src/components/validator/ValidatorMeta.tsx +++ b/apps/explorer/src/components/validator/ValidatorMeta.tsx @@ -6,14 +6,14 @@ import { type IotaValidatorSummary } from '@iota/iota-sdk/client'; import { ArrowTopRight } from '@iota/apps-ui-icons'; import { AddressLink } from '~/components/ui'; import { ImageIcon, ImageIconSize } from '@iota/core'; -import type { InactiveValidatorData } from '@iota/core/src/types'; +import type { ValidatorOverviewData } from '@iota/core/src/types'; import { onCopySuccess } from '~/lib/utils'; type ValidatorMetaProps = { validatorData: IotaValidatorSummary; }; -export function InactiveValidators({ +export function ValidatorOverview({ validatorData: { imageUrl, name, @@ -24,7 +24,7 @@ export function InactiveValidators({ validatorStakingPoolId, }, }: { - validatorData: InactiveValidatorData; + validatorData: ValidatorOverviewData; }): JSX.Element { return (
diff --git a/apps/explorer/src/pages/validator/ValidatorDetails.tsx b/apps/explorer/src/pages/validator/ValidatorDetails.tsx index 268355b3e57..ef7c135aed0 100644 --- a/apps/explorer/src/pages/validator/ValidatorDetails.tsx +++ b/apps/explorer/src/pages/validator/ValidatorDetails.tsx @@ -9,7 +9,7 @@ import { useGetValidatorsEvents, } from '@iota/core'; import { useParams } from 'react-router-dom'; -import { InactiveValidators, PageLayout, ValidatorMeta, ValidatorStats } from '~/components'; +import { ValidatorOverview, PageLayout, ValidatorMeta, ValidatorStats } from '~/components'; import { VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib/constants'; import { getValidatorMoveEvent } from '~/lib/utils'; import { InfoBox, InfoBoxStyle, InfoBoxType, LoadingIndicator } from '@iota/apps-ui-kit'; @@ -82,7 +82,7 @@ function ValidatorDetails(): JSX.Element { style={InfoBoxStyle.Elevated} /> {inactiveValidatorData && ( - + )}
} @@ -101,7 +101,7 @@ function ValidatorDetails(): JSX.Element { type={InfoBoxType.Default} style={InfoBoxStyle.Elevated} /> - + } /> From 33823f7f3bf80b01ed6bd429445bdfce33a81e58 Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Wed, 8 Apr 2026 17:12:29 +0200 Subject: [PATCH 5/8] add candidates to table and cleanup --- apps/core/src/hooks/stake/index.ts | 1 - .../hooks/stake/useGetValidatorCandidate.ts | 34 ----- apps/explorer/src/hooks/index.ts | 1 + .../src/hooks/useGetValidatorCandidates.ts | 45 +++++++ .../explorer/src/lib/types/validator.types.ts | 5 +- .../utils/generateValidatorsTableColumns.tsx | 111 +++++++++++++--- .../lib/utils/getValidatorCandidateObjects.ts | 120 ++++++++++++++++++ apps/explorer/src/lib/utils/index.ts | 3 +- ...idators.ts => sanitizeValidatorObjects.ts} | 9 +- .../src/pages/validator/ValidatorDetails.tsx | 29 +++-- .../src/pages/validators/Validators.tsx | 42 ++++-- 11 files changed, 315 insertions(+), 85 deletions(-) delete mode 100644 apps/core/src/hooks/stake/useGetValidatorCandidate.ts create mode 100644 apps/explorer/src/hooks/useGetValidatorCandidates.ts create mode 100644 apps/explorer/src/lib/utils/getValidatorCandidateObjects.ts rename apps/explorer/src/lib/utils/{sanitizePendingValidators.ts => sanitizeValidatorObjects.ts} (93%) diff --git a/apps/core/src/hooks/stake/index.ts b/apps/core/src/hooks/stake/index.ts index 6f9217aa2a1..bf3cfb32f4a 100644 --- a/apps/core/src/hooks/stake/index.ts +++ b/apps/core/src/hooks/stake/index.ts @@ -9,4 +9,3 @@ export * from './useStakeTxnInfo'; export * from './useNewStakeTransaction'; export * from './useNewUnstakeTransaction'; export * from './useGetInactiveValidator'; -export * from './useGetValidatorCandidate'; diff --git a/apps/core/src/hooks/stake/useGetValidatorCandidate.ts b/apps/core/src/hooks/stake/useGetValidatorCandidate.ts deleted file mode 100644 index 73b6ab62725..00000000000 --- a/apps/core/src/hooks/stake/useGetValidatorCandidate.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2025 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useQuery } from '@tanstack/react-query'; -import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; -import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; -import { getValidatorsMetadata } from '../../utils'; - -export function useGetValidatorCandidate(validatorAddress: string) { - const iotaClient = useIotaClient(); - const { data } = useIotaClientQuery('getLatestIotaSystemState'); - const validatorCandidatesId = data?.validatorCandidatesId; - return useQuery({ - queryKey: ['validator-candidates', validatorCandidatesId], - async queryFn() { - if (!validatorCandidatesId) { - throw Error('Missing params'); - } - - const candidateValidators = await iotaClient.getDynamicFields({ - parentId: normalizeIotaAddress(validatorCandidatesId), - }); - return Promise.all( - candidateValidators.data.map((validator) => - getValidatorsMetadata(iotaClient, validator.objectId), - ), - ); - }, - select(data) { - return data.find((v) => v?.validatorAddress === validatorAddress) ?? null; - }, - enabled: !!validatorCandidatesId, - }); -} diff --git a/apps/explorer/src/hooks/index.ts b/apps/explorer/src/hooks/index.ts index 677281d9abe..18bbb4faed1 100644 --- a/apps/explorer/src/hooks/index.ts +++ b/apps/explorer/src/hooks/index.ts @@ -18,3 +18,4 @@ export * from './useNormalizedMoveModule'; export * from './useSearch'; export * from './useVerifiedSourceCode'; export * from './useEndOfEpochTransactionFromCheckpoint'; +export * from './useGetValidatorCandidates'; diff --git a/apps/explorer/src/hooks/useGetValidatorCandidates.ts b/apps/explorer/src/hooks/useGetValidatorCandidates.ts new file mode 100644 index 00000000000..6687d685f62 --- /dev/null +++ b/apps/explorer/src/hooks/useGetValidatorCandidates.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useIotaClient, useIotaClientQuery } from '@iota/dapp-kit'; +import { useQuery } from '@tanstack/react-query'; +import { getValidatorCandidateObjects, sanitizeValidatorObjects } from '~/lib'; +import type { IotaValidatorSummaryExtended } from '~/lib/types'; + +export function useGetValidatorCandidates(validatorAddress?: string) { + const iotaClient = useIotaClient(); + const { data: systemState } = useIotaClientQuery('getLatestIotaSystemState'); + const validatorCandidatesId = systemState?.validatorCandidatesId; + + const { + data: candidateObjects, + isPending, + isLoading, + isError, + } = useQuery({ + queryKey: ['validator-candidate-objects', validatorCandidatesId, iotaClient], + async queryFn() { + if (!validatorCandidatesId) { + throw Error('Missing validatorCandidatesId'); + } + return getValidatorCandidateObjects(iotaClient, validatorCandidatesId); + }, + enabled: !!validatorCandidatesId, + }); + + const allCandidates: IotaValidatorSummaryExtended[] = sanitizeValidatorObjects( + candidateObjects, + { isCandidate: true }, + ); + + const data = validatorAddress + ? allCandidates.filter((v) => v.iotaAddress === validatorAddress) + : allCandidates; + + return { + data, + isPending, + isLoading, + isError, + }; +} diff --git a/apps/explorer/src/lib/types/validator.types.ts b/apps/explorer/src/lib/types/validator.types.ts index 82f38e761e3..e971e71602d 100644 --- a/apps/explorer/src/lib/types/validator.types.ts +++ b/apps/explorer/src/lib/types/validator.types.ts @@ -3,4 +3,7 @@ import { type IotaValidatorSummary } from '@iota/iota-sdk/client'; -export type IotaValidatorSummaryExtended = IotaValidatorSummary & { isPending?: boolean }; +export type IotaValidatorSummaryExtended = IotaValidatorSummary & { + isPending?: boolean; + isCandidate?: boolean; +}; diff --git a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx index ede4bc08b6c..604451134f6 100644 --- a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx +++ b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx @@ -152,7 +152,11 @@ export function generateValidatorsTableColumns({ accessorKey: 'stakingPoolIotaBalance', enableSorting: true, sortingFn: (rowA, rowB, columnId) => - BigInt(rowA.getValue(columnId)) - BigInt(rowB.getValue(columnId)) > 0 ? 1 : -1, + parseBigIntSafe(rowA.getValue(columnId)) - + parseBigIntSafe(rowB.getValue(columnId)) > + 0 + ? 1 + : -1, cell({ getValue }) { const stakingPoolIotaBalance = getValue(); return ( @@ -176,7 +180,15 @@ export function generateValidatorsTableColumns({ return apyA - apyB; }, - cell({ getValue }) { + cell({ getValue, row }) { + const validator = row.original as IotaValidatorSummaryExtended; + if (validator.isCandidate || validator.isPending) { + return ( + + -- + + ); + } const iotaAddress = getValue(); const { apy, isApyApproxZero } = rollingAverageApys?.[iotaAddress] ?? { apy: null, @@ -211,10 +223,17 @@ export function generateValidatorsTableColumns({ accessorKey: 'nextEpochCommissionRate', enableSorting: true, sortingFn: sortByNumber, - cell({ getValue }) { + cell({ getValue, row }) { + const validator = row.original as IotaValidatorSummaryExtended; + const value = getValue(); + const commission = Number(value); return ( - {`${Number(getValue()) / 100}%`} + + {validator.isCandidate || validator.isPending || isNaN(commission) + ? '--' + : `${commission / 100}%`} + ); }, @@ -225,12 +244,21 @@ export function generateValidatorsTableColumns({ id: 'nextEpochStake', enableSorting: true, sortingFn: (rowA, rowB, columnId) => - BigInt(rowA.getValue(columnId)) - BigInt(rowB.getValue(columnId)) > 0 ? 1 : -1, + parseBigIntSafe(rowA.getValue(columnId)) - + parseBigIntSafe(rowB.getValue(columnId)) > + 0 + ? 1 + : -1, cell({ getValue }) { const nextEpochStake = getValue(); + const isValid = nextEpochStake && !isNaN(Number(nextEpochStake)); return ( - + {isValid ? ( + + ) : ( + -- + )} ); }, @@ -266,12 +294,19 @@ export function generateValidatorsTableColumns({ accessorKey: 'votingPower', enableSorting: true, sortingFn: sortByNumber, - cell({ getValue }) { + cell({ getValue, row }) { + const validator = row.original as IotaValidatorSummaryExtended; const votingPower = getValue(); + const numericPower = Number(votingPower); return ( - {votingPower ? Number(votingPower) / 100 + '%' : '--'} + {validator.isCandidate || + validator.isPending || + !votingPower || + isNaN(numericPower) + ? '--' + : numericPower / 100 + '%'} ); @@ -288,13 +323,13 @@ export function generateValidatorsTableColumns({ return sortByString(labelA, labelB); }, cell({ row }) { - const { atRisk, label, isPending } = determineRisk( + const { atRisk, label, isPending, isCandidate } = determineRisk( committeeMembers, atRiskValidators, row, ); - if (isPending) { + if (isPending || isCandidate) { return ( @@ -353,6 +388,17 @@ export function generateValidatorsTableColumns({ id: 'isEarningNext', enableSorting: true, sortingFn: (rowA, rowB) => { + const valA = rowA.original as IotaValidatorSummaryExtended; + const valB = rowB.original as IotaValidatorSummaryExtended; + + // Candidates and pending validators never earn + if (valA.isCandidate || valA.isPending) { + return valB.isCandidate || valB.isPending ? 0 : -1; + } + if (valB.isCandidate || valB.isPending) { + return 1; + } + const { atRisk: atRiskA } = determineRisk(committeeMembers, atRiskValidators, rowA); const { atRisk: atRiskB } = determineRisk(committeeMembers, atRiskValidators, rowB); @@ -369,14 +415,25 @@ export function generateValidatorsTableColumns({ return sortByBoolean(isEarningNextA, isEarningNextB); }, cell({ row }) { + const validator = row.original as IotaValidatorSummaryExtended; + + // Candidates and pending validators are not part of the active set + // and cannot earn rewards in the next epoch. + if (validator.isCandidate || validator.isPending) { + return ( + + + + ); + } + const { atRisk } = determineRisk(committeeMembers, atRiskValidators, row); const isInTopStakers = !!topValidators.find( (v) => v.iotaAddress === row.original.iotaAddress, ); - // if its active or pending validator (all validators in this context are either active or pending), - // not at high risk (high risk, not normal risk), + // if its active validator, not at high risk, // and is part of the top X stakers, // it will generate rewards in the next epoch, otherwise not. const isEarningNext = (atRisk === null || atRisk > 1) && isInTopStakers; @@ -417,7 +474,17 @@ function sortByNumber( return Number(rowA.getValue(columnId)) - Number(rowB.getValue(columnId)) > 0 ? 1 : -1; } function sortByStakingBalanceDesc(left: IotaValidatorSummary, right: IotaValidatorSummary) { - return BigInt(left.stakingPoolIotaBalance) > BigInt(right.stakingPoolIotaBalance) ? -1 : 1; + const leftBalance = parseBigIntSafe(left.stakingPoolIotaBalance); + const rightBalance = parseBigIntSafe(right.stakingPoolIotaBalance); + return leftBalance > rightBalance ? -1 : leftBalance < rightBalance ? 1 : 0; +} + +function parseBigIntSafe(value: string | undefined | null): bigint { + try { + return BigInt(value ?? 0); + } catch { + return 0n; + } } function getLastReward( validatorEvents: IotaEvent[], @@ -443,17 +510,21 @@ function determineRisk( const isAtRisk = !!atRiskValidator; const atRisk = isAtRisk ? VALIDATOR_LOW_STAKE_GRACE_PERIOD - Number(atRiskValidator[1]) : null; const isPending = validator.isPending; - const label = isPending - ? 'Pending' - : atRisk === null - ? 'Active' - : atRisk > 1 - ? `At Risk in ${atRisk} epochs` - : 'At Risk next epoch'; + const isCandidate = validator.isCandidate; + const label = isCandidate + ? 'Candidate' + : isPending + ? 'Pending' + : atRisk === null + ? 'Active' + : atRisk > 1 + ? `At Risk in ${atRisk} epochs` + : 'At Risk next epoch'; return { label, atRisk, isPending, + isCandidate, isCommitteeMember, }; } diff --git a/apps/explorer/src/lib/utils/getValidatorCandidateObjects.ts b/apps/explorer/src/lib/utils/getValidatorCandidateObjects.ts new file mode 100644 index 00000000000..64796b1426f --- /dev/null +++ b/apps/explorer/src/lib/utils/getValidatorCandidateObjects.ts @@ -0,0 +1,120 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { + DynamicFieldInfo, + IotaClient, + IotaObjectDataOptions, + IotaObjectResponse, +} from '@iota/iota-sdk/client'; +import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; + +const MULTI_GET_OBJECTS_LIMIT = 50; + +async function fetchAllDynamicFields( + client: IotaClient, + parentId: string, +): Promise { + const allEntries: DynamicFieldInfo[] = []; + let cursor: string | null | undefined = null; + let hasNextPage = true; + + while (hasNextPage) { + const page = await client.getDynamicFields({ + parentId, + ...(cursor ? { cursor } : {}), + }); + allEntries.push(...page.data); + cursor = page.nextCursor; + hasNextPage = page.hasNextPage; + } + + return allEntries; +} + +/** + * Batch wrapper around `client.multiGetObjects` that respects the + * RPC limit of 50 objects per request. + */ +async function batchMultiGetObjects( + client: IotaClient, + ids: string[], + options?: IotaObjectDataOptions, +): Promise { + const results: IotaObjectResponse[] = []; + + for (let i = 0; i < ids.length; i += MULTI_GET_OBJECTS_LIMIT) { + const batch = ids.slice(i, i + MULTI_GET_OBJECTS_LIMIT); + const batchResults = await client.multiGetObjects({ ids: batch, options }); + results.push(...batchResults); + } + + return results; +} + +export async function getValidatorCandidateObjects( + client: IotaClient, + validatorCandidatesId: string, +): Promise { + const candidateEntries = await fetchAllDynamicFields( + client, + normalizeIotaAddress(validatorCandidatesId), + ); + + if (candidateEntries.length === 0) { + return []; + } + + // Fetch wrapper objects to get the inner Versioned IDs + const wrapperObjects = await batchMultiGetObjects( + client, + candidateEntries.map((entry) => normalizeIotaAddress(entry.objectId)), + { showContent: true }, + ); + + // Extract the inner Versioned object IDs from each wrapper + const innerIds = wrapperObjects + .map((obj) => { + const content = obj.data?.content; + if (content?.dataType !== 'moveObject') return null; + const fields = content.fields as Record; + const value = fields?.value as Record | undefined; + const inner = (value?.fields as Record)?.inner as + | Record + | undefined; + const innerFields = inner?.fields as Record | undefined; + const id = innerFields?.id as Record | undefined; + return id?.id ?? null; + }) + .filter((id): id is string => id !== null); + + if (innerIds.length === 0) { + return []; + } + + // Fetch the ValidatorV1 dynamic field objects from each Versioned object + // getDynamicFieldObject only returns the object reference (id, version, digest), + // so we first resolve the object IDs then fetch with showContent. + const dynamicFieldRefs = await Promise.all( + innerIds.map((innerId) => + client.getDynamicFieldObject({ + parentObjectId: normalizeIotaAddress(innerId), + name: { type: 'u64', value: '1' }, + }), + ), + ); + + const validatorV1ObjectIds = dynamicFieldRefs + .map((ref) => ref.data?.objectId) + .filter((id): id is string => !!id); + + if (validatorV1ObjectIds.length === 0) { + return []; + } + + const validatorV1Objects = await batchMultiGetObjects(client, validatorV1ObjectIds, { + showContent: true, + }); + + return validatorV1Objects; +} diff --git a/apps/explorer/src/lib/utils/index.ts b/apps/explorer/src/lib/utils/index.ts index 023768934c0..3fa30dca592 100644 --- a/apps/explorer/src/lib/utils/index.ts +++ b/apps/explorer/src/lib/utils/index.ts @@ -14,5 +14,6 @@ export * from './sentry'; export * from './stringUtils'; export * from './iotaMoveTypeConverters'; export * from './getSupplyChangeAfterEpochEnd'; -export * from './sanitizePendingValidators'; +export * from './sanitizeValidatorObjects'; +export * from './getValidatorCandidateObjects'; export * from './onCopySuccess'; diff --git a/apps/explorer/src/lib/utils/sanitizePendingValidators.ts b/apps/explorer/src/lib/utils/sanitizeValidatorObjects.ts similarity index 93% rename from apps/explorer/src/lib/utils/sanitizePendingValidators.ts rename to apps/explorer/src/lib/utils/sanitizeValidatorObjects.ts index 0bded56d548..ed332150f47 100644 --- a/apps/explorer/src/lib/utils/sanitizePendingValidators.ts +++ b/apps/explorer/src/lib/utils/sanitizeValidatorObjects.ts @@ -27,11 +27,12 @@ interface MoveStructFields { fields: { [key: string]: MoveValue }; } -export function sanitizePendingValidators( - allPendings: IotaObjectResponse[] | undefined, +export function sanitizeValidatorObjects( + objects: IotaObjectResponse[] | undefined, + flags: Pick, ): IotaValidatorSummaryExtended[] { return ( - allPendings?.map(({ data }) => { + objects?.map(({ data }) => { const fields = (data && data.content && @@ -45,7 +46,7 @@ export function sanitizePendingValidators( const exchangeRates = (stakingPool.exchange_rates as MoveStructFields)?.fields || {}; return { - isPending: true, + ...flags, authorityPubkeyBytes: '', commissionRate: String(value?.fields.commission_rate), description: String(metadata.description), diff --git a/apps/explorer/src/pages/validator/ValidatorDetails.tsx b/apps/explorer/src/pages/validator/ValidatorDetails.tsx index ef7c135aed0..0f87e79b367 100644 --- a/apps/explorer/src/pages/validator/ValidatorDetails.tsx +++ b/apps/explorer/src/pages/validator/ValidatorDetails.tsx @@ -2,12 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { - useGetInactiveValidator, - useGetValidatorCandidate, - useGetValidatorsApy, - useGetValidatorsEvents, -} from '@iota/core'; +import { useGetInactiveValidator, useGetValidatorsApy, useGetValidatorsEvents } from '@iota/core'; import { useParams } from 'react-router-dom'; import { ValidatorOverview, PageLayout, ValidatorMeta, ValidatorStats } from '~/components'; import { VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib/constants'; @@ -16,6 +11,7 @@ import { InfoBox, InfoBoxStyle, InfoBoxType, LoadingIndicator } from '@iota/apps import { Warning } from '@iota/apps-ui-icons'; import type { LatestIotaSystemStateSummary } from '@iota/iota-sdk/client'; import { useIotaClientQuery } from '@iota/dapp-kit'; +import { useGetValidatorCandidates } from '~/hooks'; const getAtRiskRemainingEpochs = ( data: LatestIotaSystemStateSummary | undefined, @@ -35,13 +31,15 @@ function ValidatorDetails(): JSX.Element { const { data: inactiveValidatorData, isLoading: isInactiveValidatorLoading } = useGetInactiveValidator(id || ''); - const { data: validatorCandidateData, isLoading: isValidatorCandidateLoading } = - useGetValidatorCandidate(id || ''); + const { data: candidateMatches, isLoading: isValidatorCandidateLoading } = + useGetValidatorCandidates(id || ''); - const numberOfValidators = systemStateData?.activeValidators.length ?? null; + const validatorCandidateData = candidateMatches?.[0] ?? null; + + const numberOfActiveValidators = systemStateData?.activeValidators.length ?? null; const { data: rollingAverageApys, isLoading: isValidatorsApysLoading } = useGetValidatorsApy(); const { data: validatorEvents, isLoading: isValidatorsEventsLoading } = useGetValidatorsEvents({ - limit: numberOfValidators, + limit: numberOfActiveValidators, order: 'descending', }); const epochId = systemStateData?.epoch; @@ -91,6 +89,15 @@ function ValidatorDetails(): JSX.Element { } if (validatorCandidateData && !activeValidatorData) { + const candidateOverview = { + imageUrl: validatorCandidateData.imageUrl, + name: validatorCandidateData.name, + description: validatorCandidateData.description, + projectUrl: validatorCandidateData.projectUrl, + validatorPublicKey: validatorCandidateData.protocolPubkeyBytes, + validatorAddress: validatorCandidateData.iotaAddress, + validatorStakingPoolId: validatorCandidateData.stakingPoolId, + }; return ( - + } /> diff --git a/apps/explorer/src/pages/validators/Validators.tsx b/apps/explorer/src/pages/validators/Validators.tsx index 20cc48f2e95..43f43cc0d3d 100644 --- a/apps/explorer/src/pages/validators/Validators.tsx +++ b/apps/explorer/src/pages/validators/Validators.tsx @@ -31,8 +31,8 @@ import { ErrorBoundary, PageLayout, PlaceholderTable, TableCard } from '~/compon import { generateValidatorsTableColumns } from '~/lib/ui'; import { Warning } from '@iota/apps-ui-icons'; import { useQuery } from '@tanstack/react-query'; -import { useEnhancedRpcClient } from '~/hooks'; -import { sanitizePendingValidators } from '~/lib'; +import { useEnhancedRpcClient, useGetValidatorCandidates } from '~/hooks'; +import { sanitizeValidatorObjects } from '~/lib'; import { IOTA_TYPE_ARG, normalizeIotaAddress } from '@iota/iota-sdk/utils'; function ValidatorPageResult(): JSX.Element { @@ -68,7 +68,11 @@ function ValidatorPageResult(): JSX.Element { showContent: true, }); - const sanitizedPendingValidatorsData = sanitizePendingValidators(pendingValidatorsData); + const sanitizedPendingValidatorsData = sanitizeValidatorObjects(pendingValidatorsData, { + isPending: true, + }); + + const { data: sanitizedCandidateValidatorsData } = useGetValidatorCandidates(); const { data: validatorsApy } = useGetValidatorsApy(); const { data: totalSupplyData } = useIotaClientQuery('getTotalSupply', { @@ -129,11 +133,23 @@ function ValidatorPageResult(): JSX.Element { return formatPercentageDisplay(ratio); })(); - const activeAndPendingValidators = data - ? Number(data.pendingActiveValidatorsSize) > 0 - ? activeValidators?.concat(sanitizedPendingValidatorsData) - : activeValidators - : []; + const allValidators = useMemo( + () => [ + ...(activeValidators || []), + ...(Number(data?.pendingActiveValidatorsSize) > 0 + ? sanitizedPendingValidatorsData + : []), + ...(sanitizedCandidateValidatorsData.length > 0 + ? sanitizedCandidateValidatorsData + : []), + ], + [ + activeValidators, + data?.pendingActiveValidatorsSize, + sanitizedPendingValidatorsData, + sanitizedCandidateValidatorsData, + ], + ); const tableColumns = useMemo(() => { if (!data || !maxCommitteeSize || !validatorEvents) return null; @@ -152,7 +168,7 @@ function ValidatorPageResult(): JSX.Element { ]; return generateValidatorsTableColumns({ - allValidators: activeAndPendingValidators, + allValidators, committeeMembers: data.committeeMembers.map((validator) => validator.iotaAddress), atRiskValidators: data.atRiskValidators, maxCommitteeSize, @@ -162,7 +178,7 @@ function ValidatorPageResult(): JSX.Element { includeColumns, currentEpoch: data.epoch, }); - }, [data, activeAndPendingValidators, validatorEvents, validatorsApy, maxCommitteeSize]); + }, [data, allValidators, validatorEvents, validatorsApy, maxCommitteeSize]); const [formattedTotalStakedAmount, totalStakedSymbol] = useFormatCoin({ balance: totalStaked }); const [formattedlastEpochRewardOnAllValidatorsAmount, lastEpochRewardOnAllValidatorsSymbol] = @@ -243,7 +259,7 @@ function ValidatorPageResult(): JSX.Element { } @@ -272,14 +288,14 @@ function ValidatorPageResult(): JSX.Element { )} {isSuccess && isMaxCommitteeSizeSuccess && - activeAndPendingValidators && + allValidators && tableColumns && ( From eba959235f32d6795248dfe09e61edac0c5b1c6d Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Thu, 9 Apr 2026 17:58:56 +0200 Subject: [PATCH 6/8] suggestions --- .../src/hooks/useGetValidatorCandidates.ts | 37 +++++++++++-------- .../utils/generateValidatorsTableColumns.tsx | 24 ++---------- .../src/pages/validator/ValidatorDetails.tsx | 4 +- .../src/pages/validators/Validators.tsx | 24 +++--------- 4 files changed, 32 insertions(+), 57 deletions(-) diff --git a/apps/explorer/src/hooks/useGetValidatorCandidates.ts b/apps/explorer/src/hooks/useGetValidatorCandidates.ts index 6687d685f62..063eab99a6e 100644 --- a/apps/explorer/src/hooks/useGetValidatorCandidates.ts +++ b/apps/explorer/src/hooks/useGetValidatorCandidates.ts @@ -6,17 +6,25 @@ import { useQuery } from '@tanstack/react-query'; import { getValidatorCandidateObjects, sanitizeValidatorObjects } from '~/lib'; import type { IotaValidatorSummaryExtended } from '~/lib/types'; +interface ValidatorCandidatesResult { + data: T; + isPending: boolean; + isLoading: boolean; + isError: boolean; +} + +export function useGetValidatorCandidates( + validatorAddress: string, +): ValidatorCandidatesResult; +export function useGetValidatorCandidates(): ValidatorCandidatesResult< + IotaValidatorSummaryExtended[] +>; export function useGetValidatorCandidates(validatorAddress?: string) { const iotaClient = useIotaClient(); const { data: systemState } = useIotaClientQuery('getLatestIotaSystemState'); const validatorCandidatesId = systemState?.validatorCandidatesId; - const { - data: candidateObjects, - isPending, - isLoading, - isError, - } = useQuery({ + const { data, isPending, isLoading, isError } = useQuery({ queryKey: ['validator-candidate-objects', validatorCandidatesId, iotaClient], async queryFn() { if (!validatorCandidatesId) { @@ -25,17 +33,16 @@ export function useGetValidatorCandidates(validatorAddress?: string) { return getValidatorCandidateObjects(iotaClient, validatorCandidatesId); }, enabled: !!validatorCandidatesId, + select(candidateObjects) { + const allCandidates = sanitizeValidatorObjects(candidateObjects, { + isCandidate: true, + }); + return validatorAddress + ? (allCandidates.find((v) => v.iotaAddress === validatorAddress) ?? null) + : allCandidates; + }, }); - const allCandidates: IotaValidatorSummaryExtended[] = sanitizeValidatorObjects( - candidateObjects, - { isCandidate: true }, - ); - - const data = validatorAddress - ? allCandidates.filter((v) => v.iotaAddress === validatorAddress) - : allCandidates; - return { data, isPending, diff --git a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx index 604451134f6..540a359610e 100644 --- a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx +++ b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx @@ -152,11 +152,7 @@ export function generateValidatorsTableColumns({ accessorKey: 'stakingPoolIotaBalance', enableSorting: true, sortingFn: (rowA, rowB, columnId) => - parseBigIntSafe(rowA.getValue(columnId)) - - parseBigIntSafe(rowB.getValue(columnId)) > - 0 - ? 1 - : -1, + BigInt(rowA.getValue(columnId)) - BigInt(rowB.getValue(columnId)) > 0 ? 1 : -1, cell({ getValue }) { const stakingPoolIotaBalance = getValue(); return ( @@ -244,11 +240,7 @@ export function generateValidatorsTableColumns({ id: 'nextEpochStake', enableSorting: true, sortingFn: (rowA, rowB, columnId) => - parseBigIntSafe(rowA.getValue(columnId)) - - parseBigIntSafe(rowB.getValue(columnId)) > - 0 - ? 1 - : -1, + BigInt(rowA.getValue(columnId)) - BigInt(rowB.getValue(columnId)) > 0 ? 1 : -1, cell({ getValue }) { const nextEpochStake = getValue(); const isValid = nextEpochStake && !isNaN(Number(nextEpochStake)); @@ -474,17 +466,7 @@ function sortByNumber( return Number(rowA.getValue(columnId)) - Number(rowB.getValue(columnId)) > 0 ? 1 : -1; } function sortByStakingBalanceDesc(left: IotaValidatorSummary, right: IotaValidatorSummary) { - const leftBalance = parseBigIntSafe(left.stakingPoolIotaBalance); - const rightBalance = parseBigIntSafe(right.stakingPoolIotaBalance); - return leftBalance > rightBalance ? -1 : leftBalance < rightBalance ? 1 : 0; -} - -function parseBigIntSafe(value: string | undefined | null): bigint { - try { - return BigInt(value ?? 0); - } catch { - return 0n; - } + return BigInt(left.stakingPoolIotaBalance) > BigInt(right.stakingPoolIotaBalance) ? -1 : 1; } function getLastReward( validatorEvents: IotaEvent[], diff --git a/apps/explorer/src/pages/validator/ValidatorDetails.tsx b/apps/explorer/src/pages/validator/ValidatorDetails.tsx index 0f87e79b367..eead910f18f 100644 --- a/apps/explorer/src/pages/validator/ValidatorDetails.tsx +++ b/apps/explorer/src/pages/validator/ValidatorDetails.tsx @@ -31,11 +31,9 @@ function ValidatorDetails(): JSX.Element { const { data: inactiveValidatorData, isLoading: isInactiveValidatorLoading } = useGetInactiveValidator(id || ''); - const { data: candidateMatches, isLoading: isValidatorCandidateLoading } = + const { data: validatorCandidateData, isLoading: isValidatorCandidateLoading } = useGetValidatorCandidates(id || ''); - const validatorCandidateData = candidateMatches?.[0] ?? null; - const numberOfActiveValidators = systemStateData?.activeValidators.length ?? null; const { data: rollingAverageApys, isLoading: isValidatorsApysLoading } = useGetValidatorsApy(); const { data: validatorEvents, isLoading: isValidatorsEventsLoading } = useGetValidatorsEvents({ diff --git a/apps/explorer/src/pages/validators/Validators.tsx b/apps/explorer/src/pages/validators/Validators.tsx index 43f43cc0d3d..d697910405a 100644 --- a/apps/explorer/src/pages/validators/Validators.tsx +++ b/apps/explorer/src/pages/validators/Validators.tsx @@ -133,23 +133,11 @@ function ValidatorPageResult(): JSX.Element { return formatPercentageDisplay(ratio); })(); - const allValidators = useMemo( - () => [ - ...(activeValidators || []), - ...(Number(data?.pendingActiveValidatorsSize) > 0 - ? sanitizedPendingValidatorsData - : []), - ...(sanitizedCandidateValidatorsData.length > 0 - ? sanitizedCandidateValidatorsData - : []), - ], - [ - activeValidators, - data?.pendingActiveValidatorsSize, - sanitizedPendingValidatorsData, - sanitizedCandidateValidatorsData, - ], - ); + const allValidators = [ + ...(activeValidators || []), + ...(Number(data?.pendingActiveValidatorsSize) > 0 ? sanitizedPendingValidatorsData : []), + ...(sanitizedCandidateValidatorsData ?? []), + ]; const tableColumns = useMemo(() => { if (!data || !maxCommitteeSize || !validatorEvents) return null; @@ -235,7 +223,7 @@ function ValidatorPageResult(): JSX.Element { /> ) : (
-
+
Validators
From 3bf6cfc4e6bbb3eed02fa19342291cdaa94319e7 Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Fri, 10 Apr 2026 08:53:14 +0200 Subject: [PATCH 7/8] lint --- apps/explorer/src/pages/validators/Validators.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/src/pages/validators/Validators.tsx b/apps/explorer/src/pages/validators/Validators.tsx index d697910405a..2dccc3ff395 100644 --- a/apps/explorer/src/pages/validators/Validators.tsx +++ b/apps/explorer/src/pages/validators/Validators.tsx @@ -223,7 +223,7 @@ function ValidatorPageResult(): JSX.Element { /> ) : (
-
+
Validators
From c6b328699925101968deb086ce3261956f8d261f Mon Sep 17 00:00:00 2001 From: Jose Luis Torres Corbalan Date: Mon, 13 Apr 2026 17:17:19 +0200 Subject: [PATCH 8/8] chore: adapt to candidate validators --- .../components/validator/ValidatorFilters.tsx | 4 +++- .../utils/generateValidatorsTableColumns.tsx | 23 ------------------- .../src/pages/validators/Validators.tsx | 4 +--- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/apps/explorer/src/components/validator/ValidatorFilters.tsx b/apps/explorer/src/components/validator/ValidatorFilters.tsx index 7c4350871ed..6beac9141f4 100644 --- a/apps/explorer/src/components/validator/ValidatorFilters.tsx +++ b/apps/explorer/src/components/validator/ValidatorFilters.tsx @@ -3,7 +3,7 @@ import { ButtonSegment, SegmentedButton } from '@iota/apps-ui-kit'; -export type ValidatorStatus = 'All' | 'Committee' | 'Active' | 'Pending' | 'At Risk'; +export type ValidatorStatus = 'All' | 'Committee' | 'Active' | 'Pending' | 'Candidate' | 'At Risk'; interface ValidatorFiltersProps { selectedStatus: ValidatorStatus; @@ -13,6 +13,7 @@ interface ValidatorFiltersProps { committee: number; active: number; pending: number; + candidate: number; atRisk: number; }; } @@ -27,6 +28,7 @@ export function ValidatorFilters({ { status: 'Committee', count: validatorCounts.committee }, { status: 'Active', count: validatorCounts.active }, { status: 'Pending', count: validatorCounts.pending }, + { status: 'Candidate', count: validatorCounts.candidate }, { status: 'At Risk', count: validatorCounts.atRisk }, ]; diff --git a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx index e6a91fd0d26..1840937ec32 100644 --- a/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx +++ b/apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx @@ -330,29 +330,6 @@ export function generateValidatorsTableColumns({ ); }, }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - enableSorting: true, - sortingFn: sortByNumber, - cell({ getValue, row }) { - const validator = row.original as IotaValidatorSummaryExtended; - const votingPower = getValue(); - const numericPower = Number(votingPower); - return ( - - - {validator.isCandidate || - validator.isPending || - !votingPower || - isNaN(numericPower) - ? '--' - : numericPower / 100 + '%'} - - - ); - }, - }, { header: 'Status', accessorKey: 'status', diff --git a/apps/explorer/src/pages/validators/Validators.tsx b/apps/explorer/src/pages/validators/Validators.tsx index ee82b8c50b5..69261ba75d6 100644 --- a/apps/explorer/src/pages/validators/Validators.tsx +++ b/apps/explorer/src/pages/validators/Validators.tsx @@ -153,9 +153,7 @@ function ValidatorPageResult(): JSX.Element { ? (activeValidators?.concat(sanitizedPendingValidatorsData) ?? []) : (activeValidators ?? []); const candidateValidators = - Number(sanitizedCandidateValidatorsData) > 0 - ? (sanitizedCandidateValidatorsData ?? []) - : []; + Number(sanitizedCandidateValidatorsData) > 0 ? sanitizedCandidateValidatorsData : []; return [...pendingActiveValidators, ...candidateValidators]; }, [data, activeValidators, sanitizedPendingValidatorsData, sanitizedCandidateValidatorsData]);