From 68c45b91b6085933ea0c395f55cfd1fb569c4c01 Mon Sep 17 00:00:00 2001 From: Arjun Porwal <65214523+ap211unitech@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:00:06 +0530 Subject: [PATCH] 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 --- .../src/Referenda/Referendum.tsx | 7 +- packages/page-scheduler/src/Scheduled.tsx | 8 +- packages/page-scheduler/src/Scheduler.tsx | 4 +- .../page-staking-async/src/System/index.tsx | 15 ++- .../src/Validators/Summary.tsx | 2 +- .../src/Validators/SummarySession.tsx | 117 ++++++++++++++++++ packages/page-staking/src/Targets/index.tsx | 3 +- packages/page-staking/src/index.tsx | 1 - packages/react-components/src/CardSummary.tsx | 6 +- .../react-components/src/StakingUnbonding.tsx | 11 +- packages/react-hooks/src/ctx/StakingAsync.tsx | 10 +- 11 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 packages/page-staking-async/src/Validators/SummarySession.tsx diff --git a/packages/page-referenda/src/Referenda/Referendum.tsx b/packages/page-referenda/src/Referenda/Referendum.tsx index 3367b8f592..a867cf2b84 100644 --- a/packages/page-referenda/src/Referenda/Referendum.tsx +++ b/packages/page-referenda/src/Referenda/Referendum.tsx @@ -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 { 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); diff --git a/packages/page-scheduler/src/Scheduled.tsx b/packages/page-scheduler/src/Scheduled.tsx index c76a58d987..e9a2a4410d 100644 --- a/packages/page-scheduler/src/Scheduled.tsx +++ b/packages/page-scheduler/src/Scheduled.tsx @@ -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 { + 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 {bestNumber && ( <> - + #{formatNumber(blockNumber)} )} diff --git a/packages/page-scheduler/src/Scheduler.tsx b/packages/page-scheduler/src/Scheduler.tsx index 28781dcdeb..e9e27fe38c 100644 --- a/packages/page-scheduler/src/Scheduler.tsx +++ b/packages/page-scheduler/src/Scheduler.tsx @@ -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 { const { t } = useTranslation(); const { api } = useApi(); - const bestNumber = useBestNumber(); + const bestNumber = useBestNumberRelay(); const items = useCall(api.query.scheduler.agenda.entries, undefined, OPT_SCHED); const filtered = useMemo( diff --git a/packages/page-staking-async/src/System/index.tsx b/packages/page-staking-async/src/System/index.tsx index e9817ebe8e..3ed287a331 100644 --- a/packages/page-staking-async/src/System/index.tsx +++ b/packages/page-staking-async/src/System/index.tsx @@ -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(rcApi?.derive.staking.overview); + const ahStakingOverview = useCall(api?.derive.staking.overview); + const validatorsInfo = useCall(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} diff --git a/packages/page-staking-async/src/Validators/Summary.tsx b/packages/page-staking-async/src/Validators/Summary.tsx index defd614be4..ef4799aec4 100644 --- a/packages/page-staking-async/src/Validators/Summary.tsx +++ b/packages/page-staking-async/src/Validators/Summary.tsx @@ -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; diff --git a/packages/page-staking-async/src/Validators/SummarySession.tsx b/packages/page-staking-async/src/Validators/SummarySession.tsx new file mode 100644 index 0000000000..dcbd63b69b --- /dev/null +++ b/packages/page-staking-async/src/Validators/SummarySession.tsx @@ -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 { + const { t } = useTranslation(); + const { api } = useApi(); // Asset Hub API + const blockTime = useBlockInterval(); + const { rcApi } = useStakingAsyncApis(); + const sessionInfo = useCall(rcApi?.derive.session?.progress); + const ahSessionInfo = useCall(api?.derive.session?.progress); + const forcing = useCall(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 + ? ( + + ) + : ( + + #{sessionInfo + ? formatNumber(sessionInfo.currentIndex) + : 123} + {withEra && activeEraStart &&
 
} +
+ ) + )} + + + )} + + ); +} + +export default React.memo(SummarySession); diff --git a/packages/page-staking/src/Targets/index.tsx b/packages/page-staking/src/Targets/index.tsx index 622fd4f8e0..1a54a8bdeb 100644 --- a/packages/page-staking/src/Targets/index.tsx +++ b/packages/page-staking/src/Targets/index.tsx @@ -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; diff --git a/packages/page-staking/src/index.tsx b/packages/page-staking/src/index.tsx index ea959d07a2..2db74f4b0c 100644 --- a/packages/page-staking/src/index.tsx +++ b/packages/page-staking/src/index.tsx @@ -185,7 +185,6 @@ function StakingApp ({ basePath, className = '' }: Props): React.ReactElement | null { +function CardSummary ({ apiOverride, children, className = '', label, progress }: Props): React.ReactElement | 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 && ( @@ -88,6 +91,7 @@ function CardSummary ({ children, className = '', label, progress }: Props): Rea }` : ( diff --git a/packages/react-components/src/StakingUnbonding.tsx b/packages/react-components/src/StakingUnbonding.tsx index 889be1b79a..7183a6cfcc 100644 --- a/packages/react-components/src/StakingUnbonding.tsx +++ b/packages/react-components/src/StakingUnbonding.tsx @@ -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 | null { - const { api } = useApi(); - const progress = useCall(api.derive.session.progress); + const { api: connectedApi } = useApi(); + const { isStakingAsync, rcApi } = useStakingAsyncApis(); + const api = useMemo(() => (isStakingAsync ? rcApi : connectedApi), [connectedApi, isStakingAsync, rcApi]); + const progress = useCall(api?.derive.session.progress); const { t } = useTranslation(); const [mapped, total, isStalled] = useMemo( @@ -99,9 +101,10 @@ function StakingUnbonding ({ className = '', iconPosition = 'left', stakingInfo >
{t('Unbonding {{value}}', { replace: { value: formatBalance(value, { forceUnit: '-' }) } })}
- {api.consts.babe?.epochDuration + {api?.consts.babe?.epochDuration ? ( diff --git a/packages/react-hooks/src/ctx/StakingAsync.tsx b/packages/react-hooks/src/ctx/StakingAsync.tsx index c8a057d8b7..b1494415b8 100644 --- a/packages/react-hooks/src/ctx/StakingAsync.tsx +++ b/packages/react-hooks/src/ctx/StakingAsync.tsx @@ -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,