mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-10-20 18:56:30 +00:00
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.
279 lines
9.0 KiB
TypeScript
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);
|