Files
pezkuwi-apps/packages/page-staking-async/src/System/index.tsx
T
Satoshi Qazi Muhammed 405fb9ce30 Read the active validator set from the relay session on Asset Hub staking
Connected to Asset Hub, the staking page asked the relay for
derive.staking.validators. The api only offers the staking derives on a
chain with the staking pallet, and the Pezkuwi relay carries only
stakingAhClient, so rcApi.derive.staking is undefined: the page rendered,
then crashed with "reading 'validators'" once the relay api connected.

Neither chain can answer that derive alone. Take the active set from the
relay's session.validators (Asset Hub's session set is its collators) and
the next elected set from Asset Hub, where staking lives, in the shape the
derive returns.
2026-10-12 12:15:57 +04:00

279 lines
9.0 KiB
TypeScript

// Copyright 2017-2026 @pezkuwi/app-staking-async authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { DeriveStakingOverview, DeriveStakingValidators } from '@pezkuwi/api-derive/types';
import type { AppProps } from '@pezkuwi/react-components/types';
import type { AccountId, ElectionStatus, ParaValidatorIndex, ValidatorId } from '@pezkuwi/types/interfaces';
import type { BN } from '@pezkuwi/util';
import React, { useCallback, useMemo, useState } from 'react';
import { Route, Routes } from 'react-router';
import Bags from '@pezkuwi/app-staking/Bags';
import Payouts from '@pezkuwi/app-staking/Payouts';
import Query from '@pezkuwi/app-staking/Query';
import Slashes from '@pezkuwi/app-staking/Slashes';
import Targets from '@pezkuwi/app-staking/Targets';
import useNominations from '@pezkuwi/app-staking/useNominations';
import useSortedTargets from '@pezkuwi/app-staking/useSortedTargets';
import Pools from '@pezkuwi/app-staking2/Pools';
import useOwnPools from '@pezkuwi/app-staking2/Pools/useOwnPools';
import { Tabs } from '@pezkuwi/react-components';
import { useAccounts, useApi, useAvailableSlashes, useCall, useCallMulti, useFavorites, useOwnStashInfos } from '@pezkuwi/react-hooks';
import { isFunction } from '@pezkuwi/util';
import Actions from '../Actions/index.js';
import CommandCenter from '../CommandCenter/index.js';
import { STORE_FAVS_BASE } from '../constants.js';
import { useTranslation } from '../translate.js';
import Validators from '../Validators/index.js';
const HIDDEN_ACC = ['actions', 'payout'];
const OPT_MULTI = {
defaultValue: [false, undefined, {}] as [boolean, BN | undefined, Record<string, boolean>],
transform: ([eraElectionStatus, minValidatorBond, validators, activeValidatorIndices]: [ElectionStatus | null, BN | undefined, ValidatorId[] | null, ParaValidatorIndex[] | null]): [boolean, BN | undefined, Record<string, boolean>] => [
!!eraElectionStatus && eraElectionStatus.isOpen,
minValidatorBond && !minValidatorBond.isZero()
? minValidatorBond
: undefined,
validators && activeValidatorIndices
? activeValidatorIndices.reduce((all, index) => ({ ...all, [validators[index.toNumber()].toString()]: true }), {})
: {}
]
};
interface Props extends AppProps {
ahApi?: ApiPromise
rcApi?: ApiPromise
isRelayChain: boolean
rcEndPoints: string[]
ahEndPoints: string[]
}
function StakingApp ({ ahApi, ahEndPoints, basePath, isRelayChain, rcApi, rcEndPoints }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [withLedger, setWithLedger] = useState(false);
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [loadNominations, setLoadNominations] = useState(false);
const { areAccountsLoaded, hasAccounts } = useAccounts();
const ownStashes = useOwnStashInfos();
const slashes = useAvailableSlashes();
const targets = useSortedTargets(favorites, withLedger);
const [isInElection, minCommission, paraValidators] = useCallMulti<[boolean, BN | undefined, Record<string, boolean>]>([
api.query.staking.eraElectionStatus,
api.query.staking.minCommission,
api.query.session.validators,
(api.query.parasShared || api.query.shared)?.activeValidatorIndices
], OPT_MULTI);
const nominatedBy = useNominations(loadNominations);
const ownPools = useOwnPools();
const ahStakingOverview = useCall<DeriveStakingOverview>(api?.derive.staking.overview);
// derive.staking.validators pairs the session set with the next elected set, and the api
// only offers it on a chain that has the staking pallet. Here staking lives on Asset Hub and
// the relay carries only stakingAhClient, so neither chain can answer it alone: the active
// set is the relay's session set (Asset Hub's session set is its collators), the next set is
// elected on Asset Hub.
const rcValidators = useCall<AccountId[]>(rcApi?.query.session.validators);
const nextElected = useCall<AccountId[]>(api.derive.staking.nextElected);
const validatorsInfo = useMemo((): DeriveStakingValidators | undefined => (
rcValidators && nextElected
? { nextElected, validators: rcValidators }
: undefined
), [nextElected, rcValidators]);
const stakingOverview = useMemo(() => (
!!ahStakingOverview && !!validatorsInfo
? {
...ahStakingOverview,
...validatorsInfo
}
: undefined
), [ahStakingOverview, validatorsInfo]);
const toggleNominatedBy = useCallback(
() => setLoadNominations(true),
[]
);
const toggleLedger = useCallback(
() => setWithLedger(true),
[]
);
const hasQueries = useMemo(
() => hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra),
[api, hasAccounts]
);
const hasStashes = useMemo(
() => hasAccounts && !!ownStashes && (ownStashes.length !== 0),
[hasAccounts, ownStashes]
);
const ownValidators = useMemo(
() => (ownStashes || []).filter(({ isStashValidating }) => isStashValidating),
[ownStashes]
);
const items = useMemo(() => [
{
isRoot: true,
name: 'active-validators',
text: t('Active Validators')
},
{
name: 'actions',
text: t('Accounts')
},
hasStashes && isFunction(api.query.staking.activeEra) && {
name: 'payout',
text: t('Payouts')
},
isFunction(api.query.nominationPools?.minCreateBond) && {
name: 'pools',
text: t('Pools')
},
{
alias: 'returns',
name: 'all-validators',
text: t('All Validators')
},
hasStashes && isFunction((api.query.voterBagsList || api.query.bagsList || api.query.voterList)?.counterForListNodes) && {
name: 'bags',
text: t('Bags')
},
{
count: slashes.reduce((count, [, unapplied]) => count + unapplied.length, 0),
name: 'slashes',
text: t('Slashes')
},
{
hasParams: true,
name: 'query',
text: t('Validator stats')
},
{
name: 'command-center',
text: t('Command Center')
}
].filter((q): q is { name: string; text: string } => !!q), [api, hasStashes, slashes, t]);
return (
<>
<Tabs
basePath={basePath}
hidden={
areAccountsLoaded && !hasAccounts
? HIDDEN_ACC
: undefined
}
items={items}
/>
<Routes>
<Route path={basePath}>
<Route
element={
<Bags ownStashes={ownStashes} />
}
path='bags'
/>
<Route
element={
<Payouts
historyDepth={targets.historyDepth}
isInElection={isInElection}
ownPools={ownPools}
ownValidators={ownValidators}
/>
}
path='payout'
/>
<Route
element={
<Pools ownPools={ownPools} />
}
path='pools'
/>
<Route
element={
<Query basePath={basePath} />
}
path='query/:value?'
/>
<Route
element={
<Actions
isInElection={isInElection}
minCommission={minCommission}
ownPools={ownPools}
ownStashes={ownStashes}
targets={targets}
/>
}
path='actions'
/>
<Route
element={
<Targets
isInElection={isInElection}
nominatedBy={nominatedBy}
ownStashes={ownStashes}
targets={targets}
toggleFavorite={toggleFavorite}
toggleLedger={toggleLedger}
toggleNominatedBy={toggleNominatedBy}
/>
}
path='all-validators'
/>
<Route
element={
<Slashes
ownStashes={ownStashes}
slashes={slashes}
/>
}
path='slashes'
/>
<Route
element={
<CommandCenter
ahApi={ahApi}
ahEndPoints={ahEndPoints}
isRelayChain={isRelayChain}
rcApi={rcApi}
rcEndPoints={rcEndPoints}
/>
}
path='command-center'
/>
<Route
element={
<Validators
favorites={favorites}
hasAccounts={hasAccounts}
hasQueries={hasQueries}
minCommission={minCommission}
nominatedBy={nominatedBy}
ownStashes={ownStashes}
paraValidators={paraValidators}
stakingOverview={stakingOverview}
targets={targets}
toggleFavorite={toggleFavorite}
toggleNominatedBy={toggleNominatedBy}
/>
}
index
/>
</Route>
</Routes>
</>
);
}
export default React.memo(StakingApp);