Fix: Staking summary and Schedular remaining time (#11903)

* feat: enhance staking overview by combining data from multiple sources and add SummarySession component

* feat: integrate staking async APIs into Scheduled and Scheduler components

* fix: build

* feat: integrate staking async APIs into StakingUnbonding component

* feat: enhance SummarySession component with era progress calculation and improved API integration

* fix: chart timeline

* chore: comment
This commit is contained in:
Arjun Porwal 2025-10-13 22:00:06 +05:30 committed by GitHub
parent 9b671cb7c2
commit 68c45b91b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 162 additions and 22 deletions

View File

@ -9,7 +9,7 @@ import type { CurveGraph, ReferendumProps as Props } from '../types.js';
import React, { useMemo } from 'react';
import { Chart, Columar, LinkExternal, styled, Table } from '@polkadot/react-components';
import { useBestNumber, useBlockInterval, useToggle } from '@polkadot/react-hooks';
import { useBestNumberRelay, useBlockInterval, useStakingAsyncApis, useToggle } from '@polkadot/react-hooks';
import { calcBlockTime } from '@polkadot/react-hooks/useBlockTime';
import { BN_MILLION, BN_THOUSAND, bnMax, bnToBn, formatNumber, objectSpread } from '@polkadot/util';
@ -347,8 +347,9 @@ function extractInfo (info: PalletReferendaReferendumInfoConvictionVotingTally |
function Referendum (props: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const bestNumber = useBestNumber();
const blockInterval = useBlockInterval();
const { isStakingAsync, rcApi } = useStakingAsyncApis();
const bestNumber = useBestNumberRelay();
const blockInterval = useBlockInterval(isStakingAsync ? rcApi : undefined);
const { activeIssuance, className = '', palletReferenda, value: { id, info, isConvictionVote, track, trackGraph } } = props;
const [isExpanded, toggleExpanded] = useToggle(false);

View File

@ -7,7 +7,7 @@ import type { ScheduledExt } from './types.js';
import React from 'react';
import Hash from '@polkadot/app-preimages/Preimages/Hash';
import { usePreimage } from '@polkadot/react-hooks';
import { usePreimage, useStakingAsyncApis } from '@polkadot/react-hooks';
import { CallExpander } from '@polkadot/react-params';
import { BlockToTime } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
@ -19,6 +19,7 @@ interface Props {
}
function Scheduled ({ bestNumber, className = '', value: { blockNumber, call, maybeId, maybePeriodic, preimageHash } }: Props): React.ReactElement<Props> {
const { isStakingAsync, rcApi } = useStakingAsyncApis();
const preimage = usePreimage(preimageHash);
const period = maybePeriodic.unwrapOr(null);
const name = maybeId.unwrapOr(null);
@ -37,7 +38,10 @@ function Scheduled ({ bestNumber, className = '', value: { blockNumber, call, ma
<td className='number together'>
{bestNumber && (
<>
<BlockToTime value={blockNumber.sub(bestNumber)} />
<BlockToTime
api={isStakingAsync ? rcApi : undefined}
value={blockNumber.sub(bestNumber)}
/>
#{formatNumber(blockNumber)}
</>
)}

View File

@ -11,7 +11,7 @@ import type { ScheduledExt } from './types.js';
import React, { useMemo, useRef } from 'react';
import { Table } from '@polkadot/react-components';
import { useApi, useBestNumber, useCall } from '@polkadot/react-hooks';
import { useApi, useBestNumberRelay, useCall } from '@polkadot/react-hooks';
import ScheduledView from './Scheduled.js';
import { useTranslation } from './translate.js';
@ -81,7 +81,7 @@ const OPT_SCHED = {
function Schedule ({ className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const bestNumber = useBestNumber();
const bestNumber = useBestNumberRelay();
const items = useCall<ScheduledExt[]>(api.query.scheduler.agenda.entries, undefined, OPT_SCHED);
const filtered = useMemo(

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { DeriveStakingOverview } from '@polkadot/api-derive/types';
import type { DeriveStakingOverview, DeriveStakingValidators } from '@polkadot/api-derive/types';
import type { AppProps } from '@polkadot/react-components/types';
import type { ElectionStatus, ParaValidatorIndex, ValidatorId } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
@ -71,7 +71,17 @@ function StakingApp ({ ahApi, ahEndPoints, basePath, isRelayChain, rcApi, rcEndP
], OPT_MULTI);
const nominatedBy = useNominations(loadNominations);
const ownPools = useOwnPools();
const stakingOverview = useCall<DeriveStakingOverview>(rcApi?.derive.staking.overview);
const ahStakingOverview = useCall<DeriveStakingOverview>(api?.derive.staking.overview);
const validatorsInfo = useCall<DeriveStakingValidators>(rcApi?.derive.staking.validators);
const stakingOverview = useMemo(() => (
!!ahStakingOverview && !!validatorsInfo
? {
...ahStakingOverview,
...validatorsInfo
}
: undefined
), [ahStakingOverview, validatorsInfo]);
const toggleNominatedBy = useCallback(
() => setLoadNominations(true),
@ -201,7 +211,6 @@ function StakingApp ({ ahApi, ahEndPoints, basePath, isRelayChain, rcApi, rcEndP
isInElection={isInElection}
nominatedBy={nominatedBy}
ownStashes={ownStashes}
stakingOverview={stakingOverview}
targets={targets}
toggleFavorite={toggleFavorite}
toggleLedger={toggleLedger}

View File

@ -8,12 +8,12 @@ import type { BN } from '@polkadot/util';
import React, { useEffect, useState } from 'react';
import SummarySession from '@polkadot/app-explorer/SummarySession';
import { CardSummary, styled, SummaryBox } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate.js';
import SummarySession from './SummarySession.js';
interface Props {
className?: string;

View File

@ -0,0 +1,117 @@
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveSessionProgress } from '@polkadot/api-derive/types';
import type { Forcing } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import { CardSummary } from '@polkadot/react-components';
import { useApi, useBlockInterval, useCall, useStakingAsyncApis } from '@polkadot/react-hooks';
import { BN, BN_THREE, BN_TWO, BN_ZERO, formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate.js';
interface Props {
className?: string;
withEra?: boolean;
withSession?: boolean;
}
function SummarySession ({ className, withEra = true, withSession = true }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi(); // Asset Hub API
const blockTime = useBlockInterval();
const { rcApi } = useStakingAsyncApis();
const sessionInfo = useCall<DeriveSessionProgress>(rcApi?.derive.session?.progress);
const ahSessionInfo = useCall<DeriveSessionProgress>(api?.derive.session?.progress);
const forcing = useCall<Forcing>(rcApi?.query.staking?.forceEra);
const eraLabel = t('era');
const sessionLabel = rcApi?.query.babe
? t('epoch')
: t('session');
const activeEraStart = sessionInfo?.activeEraStart.unwrapOr(null);
const eraProgress = useMemo(() => {
if (!ahSessionInfo) {
return BN_ZERO;
}
const currentEraStart = ahSessionInfo.activeEraStart.unwrapOrDefault();
if (currentEraStart.isZero()) {
return BN_ZERO;
}
const currentTimestamp = new BN(Math.floor(Date.now()));
const elapsed = currentTimestamp.sub(currentEraStart);
return elapsed.div(blockTime);
}, [ahSessionInfo, blockTime]);
const eraDuration = useMemo(
() => {
// default to 1800 blocks i.e. 6 hours
return new BN(6 * 60 * 60 * 1000).div(blockTime);
},
[blockTime]
);
return (
<>
{rcApi?.derive.session && (
<>
{withSession && (
rcApi?.query.babe
? (
<CardSummary
apiOverride={rcApi}
className={className}
label={sessionLabel}
progress={{
isBlurred: !sessionInfo,
total: sessionInfo?.sessionLength || BN_THREE,
value: sessionInfo?.sessionProgress || BN_TWO,
withTime: true
}}
/>
)
: (
<CardSummary
apiOverride={rcApi}
label={sessionLabel}
>
#{sessionInfo
? formatNumber(sessionInfo.currentIndex)
: <span className='--tmp'>123</span>}
{withEra && activeEraStart && <div className='isSecondary'>&nbsp;</div>}
</CardSummary>
)
)}
<CardSummary
className={className}
label={eraLabel}
progress={{
isBlurred: !(sessionInfo && forcing),
total: sessionInfo && forcing
? forcing.isForceAlways
? sessionInfo.sessionLength
: eraDuration
: BN_THREE,
value: sessionInfo && forcing
? forcing.isForceAlways
? sessionInfo.sessionProgress
: eraProgress
: BN_TWO,
withTime: true
}}
/>
</>
)}
</>
);
}
export default React.memo(SummarySession);

View File

@ -1,7 +1,7 @@
// Copyright 2017-2025 @polkadot/app-staking authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveHasIdentity, DeriveStakingOverview } from '@polkadot/api-derive/types';
import type { DeriveHasIdentity } from '@polkadot/api-derive/types';
import type { StakerState } from '@polkadot/react-hooks/types';
import type { u32 } from '@polkadot/types-codec';
import type { BN } from '@polkadot/util';
@ -29,7 +29,6 @@ interface Props {
isInElection: boolean;
nominatedBy?: NominatedByMap;
ownStashes?: StakerState[];
stakingOverview?: DeriveStakingOverview;
targets: SortedTargets;
toggleFavorite: (address: string) => void;
toggleLedger: () => void;

View File

@ -185,7 +185,6 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement<Pr
isInElection={isInElection}
nominatedBy={nominatedBy}
ownStashes={ownStashes}
stakingOverview={stakingOverview}
targets={targets}
toggleFavorite={toggleFavorite}
toggleLedger={toggleLedger}

View File

@ -1,6 +1,7 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { UInt } from '@polkadot/types';
import type { BN } from '@polkadot/util';
@ -28,9 +29,10 @@ interface Props {
className?: string;
label: React.ReactNode;
progress?: ProgressProps;
apiOverride?: ApiPromise;
}
function CardSummary ({ children, className = '', label, progress }: Props): React.ReactElement<Props> | null {
function CardSummary ({ apiOverride, children, className = '', label, progress }: Props): React.ReactElement<Props> | null {
const value = progress?.value;
const total = progress?.total;
const left = progress && !isUndefined(value) && !isUndefined(total) && value.gten(0) && total.gtn(0)
@ -73,6 +75,7 @@ function CardSummary ({ children, className = '', label, progress }: Props): Rea
<>
{isTimed && !children && (
<BlockToTime
api={apiOverride}
className={progress.isBlurred ? '--tmp' : ''}
value={progress.total}
/>
@ -88,6 +91,7 @@ function CardSummary ({ children, className = '', label, progress }: Props): Rea
}`
: (
<BlockToTime
api={apiOverride}
className={`${progress.isBlurred ? '--tmp' : ''} timer`}
value={progress.total.sub(progress.value)}
/>

View File

@ -5,7 +5,7 @@ import type { DeriveSessionProgress, DeriveStakingAccount } from '@polkadot/api-
import React, { useMemo } from 'react';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useApi, useCall, useStakingAsyncApis } from '@polkadot/react-hooks';
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { BN, BN_ONE, BN_ZERO, formatBalance, formatNumber } from '@polkadot/util';
@ -66,8 +66,10 @@ function extractTotals (stakingInfo?: DeriveStakingAccountPartial, progress?: De
}
function StakingUnbonding ({ className = '', iconPosition = 'left', stakingInfo }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const progress = useCall<DeriveSessionProgress>(api.derive.session.progress);
const { api: connectedApi } = useApi();
const { isStakingAsync, rcApi } = useStakingAsyncApis();
const api = useMemo(() => (isStakingAsync ? rcApi : connectedApi), [connectedApi, isStakingAsync, rcApi]);
const progress = useCall<DeriveSessionProgress>(api?.derive.session.progress);
const { t } = useTranslation();
const [mapped, total, isStalled] = useMemo(
@ -99,9 +101,10 @@ function StakingUnbonding ({ className = '', iconPosition = 'left', stakingInfo
>
<div>{t('Unbonding {{value}}', { replace: { value: formatBalance(value, { forceUnit: '-' }) } })}</div>
<div className='faded'>
{api.consts.babe?.epochDuration
{api?.consts.babe?.epochDuration
? (
<BlockToTime
api={api}
label={`${t('{{blocks}} blocks', { replace: { blocks: formatNumber(blocks) } })}, `}
value={blocks}
/>

View File

@ -56,7 +56,7 @@ export const StakingAsyncProvider = ({ children }: PropsWithChildren) => {
const rcEndPoints = useMemo(() => {
return (isRelayChain
? apiEndpoint?.providers
: apiEndpoint?.valueRelay) || [];
: apiEndpoint?.valueRelay)?.filter((e) => e.startsWith('wss://')) || [];
}, [apiEndpoint, isRelayChain]);
const ahEndPoints: string[] = useMemo(() => {
@ -66,20 +66,24 @@ export const StakingAsyncProvider = ({ children }: PropsWithChildren) => {
)?.providers || [];
}
return apiEndpoint?.providers || [];
return apiEndpoint?.providers?.filter((e) => e.startsWith('wss://')) || [];
}, [api, apiEndpoint, isRelayChain]);
useEffect(() => {
if (isRelayChain) {
const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length));
setRcApi(api);
!!ahUrl && getApi(ahUrl).then(setAhApi).catch(console.error);
} else {
const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length));
setAhApi(api);
!!rcUrl && getApi(rcUrl).then(setRcApi).catch(console.error);
}
}, [ahEndPoints, isRelayChain, rcEndPoints]);
}, [ahEndPoints, api, isRelayChain, rcEndPoints]);
const state = useMemo(() => ({
ahApi,