diff --git a/packages/apps-config/src/links/subscan.ts b/packages/apps-config/src/links/subscan.ts index a5e3939dfb93..7d911bfe115f 100644 --- a/packages/apps-config/src/links/subscan.ts +++ b/packages/apps-config/src/links/subscan.ts @@ -10,28 +10,40 @@ export const Subscan: ExternalDef = { chains: { Acala: 'acala', 'Acala Mandala TC5': 'acala-testnet', + Ajuna: 'ajuna', 'Ajuna Polkadot': 'ajuna', 'Aleph Zero': 'alephzero', 'Aleph Zero Testnet': 'alephzero-testnet', Altair: 'altair', + AssetHub: 'assethub-polkadot', Astar: 'astar', + Autonomys: 'autonomys', 'Bajun Kusama': 'bajun', Basilisk: 'basilisk', Bifrost: 'bifrost-kusama', 'Bifrost Polkadot': 'bifrost', + BridgeHub: 'bridgehub-polkadot', 'Calamari Parachain': 'calamari', Centrifuge: 'centrifuge', ChainX: 'chainx', + Clover: 'clover', + Collectives: 'collectives-polkadot', 'Composable Finance': 'composable', + Continuum: 'continuum', 'Continuum Network': 'continuum', + Coretime: 'coretime-polkadot', Crab2: 'crab', Creditcoin: 'creditcoin', 'Creditcoin3 Testnet': 'creditcoin3-testnet', Crust: 'crust', + 'Crust Network': 'crust-parachain', 'Crust Shadow': 'shadow', + Darwinia: 'darwinia', Darwinia2: 'darwinia', Dock: 'dock', 'Dolphin Parachain Testnet': 'dolphin', + 'Energy Web X': 'energywebx', + Humanode: 'humanode', 'Humanode Mainnet': 'humanode', Hydration: 'hydration', 'Integritee Network (Kusama)': 'integritee', @@ -44,21 +56,30 @@ export const Subscan: ExternalDef = { Kusama: 'kusama', 'Kusama Asset Hub': 'assethub-kusama', 'Mangata Kusama Mainnet': 'mangatax', + Manta: 'manta', 'Moonbase Alpha': 'moonbase', Moonbeam: 'moonbeam', Moonriver: 'moonriver', + Mythos: 'mythos', NeuroWeb: 'neuroweb', 'NeuroWeb Testnet': 'neuroweb-testnet', + Nodle: 'nodle', 'Nodle Parachain': 'nodle', 'OPAL by UNIQUE': 'opal', 'Paseo Testnet': 'paseo', + Peaq: 'peaq', + Pendulum: 'pendulum', + People: 'people-polkadot', Phala: 'phala', + 'Phala Network': 'phala', Picasso: 'picasso', 'Pioneer Network': 'pioneer', - Polkadex: 'polkadex', + Polimec: 'polimec', + Polkadex: 'polkadex-parachain', Polkadot: 'polkadot', 'Polkadot Asset Hub': 'assethub-polkadot', Polymesh: 'polymesh', + 'Polymesh Mainnet': 'polymesh', 'Polymesh Testnet': 'polymesh-testnet', 'QUARTZ by UNIQUE': 'quartz', Robonomics: 'robonomics', @@ -72,6 +93,8 @@ export const Subscan: ExternalDef = { Stafi: 'stafi', 'Turing Network': 'turing', UNIQUE: 'unique', + Unique: 'unique', + Vara: 'vara', 'Vara Network': 'vara', Westend: 'westend', Zeitgeist: 'zeitgeist', diff --git a/packages/page-coretime/src/Overview/Filters.tsx b/packages/page-coretime/src/Overview/Filters.tsx new file mode 100644 index 000000000000..d96f69e68076 --- /dev/null +++ b/packages/page-coretime/src/Overview/Filters.tsx @@ -0,0 +1,126 @@ +// Copyright 2017-2025 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ChainInformation } from '@polkadot/react-hooks/types'; +import type { ActiveFilters } from '../types.js'; + +import React, { useCallback, useState } from 'react'; + +import { Button, Dropdown, Input } from '@polkadot/react-components'; + +import { useTranslation } from '../translate.js'; +import { FilterType, useBlocksSort, useSearchFilter, useTypeFilter } from './filters/index.js'; + +interface Props { + chainInfo: Record; + data: number[]; + onFilter: (data: number[]) => void; +} + +function Filters ({ chainInfo, data: initialData, onFilter }: Props): React.ReactElement { + const { t } = useTranslation(); + const [activeFilters, setActiveFilters] = useState({ + search: [], + type: [] + }); + + const { apply: applyBlocksSort, direction, onApply: onApplySort, reset: resetSort } = useBlocksSort({ + chainInfo, + data: initialData, + onFilter: (data) => handleFilter(data, FilterType.BLOCKS) + }); + + const { apply: applySearchFilter, onApply: onApplySearch, reset: resetSearch, searchValue } = useSearchFilter({ + data: initialData, + onFilter: (data) => handleFilter(data, FilterType.SEARCH) + }); + + const { apply: applyTypeFilter, onApply: onApplyType, reset: resetType, selectedType, typeOptions } = useTypeFilter({ + chainInfo, + data: initialData, + onFilter: (data) => handleFilter(data, FilterType.TYPE) + }); + + /** + * 1. Applies additional filtering already present in the filters + * 2. Performs filtering based on the filter type + */ + const handleFilter = useCallback(( + filteredData: number[], + filterType: FilterType + ): void => { + let resultData = filteredData; + + if (filterType !== FilterType.SEARCH) { + resultData = applySearchFilter(resultData, activeFilters.search); + } + + if (filterType !== FilterType.TYPE) { + resultData = applyTypeFilter(resultData, activeFilters.type); + } + + if (filterType !== FilterType.BLOCKS && direction) { + resultData = applyBlocksSort(resultData, direction); + } + + if (filterType !== FilterType.BLOCKS) { + setActiveFilters((prev) => ({ + ...prev, + [filterType]: filteredData.length === initialData.length ? [] : filteredData + })); + } + + onFilter(resultData); + }, [initialData, onFilter, activeFilters, direction, applyBlocksSort, applyTypeFilter, applySearchFilter]); + + const resetAllFilters = useCallback(() => { + resetSearch(); + resetType(); + resetSort(); + setActiveFilters({ search: [], type: [] }); + onFilter(initialData); + }, [initialData, onFilter, resetSearch, resetType, resetSort]); + + const hasActiveFilters = searchValue || selectedType || direction; + + return ( +
+
+ +
+ +
+
+ {hasActiveFilters && ( +
+
+ )} +
+ ); +} + +export default Filters; diff --git a/packages/page-coretime/src/Overview/filters/index.ts b/packages/page-coretime/src/Overview/filters/index.ts new file mode 100644 index 000000000000..369ff70d6ec6 --- /dev/null +++ b/packages/page-coretime/src/Overview/filters/index.ts @@ -0,0 +1,7 @@ +// Copyright 2017-2025 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from '../../types.js'; +export * from './useBlockSort.js'; +export * from './useSearchFilter.js'; +export * from './useTypeFilter.js'; diff --git a/packages/page-coretime/src/Overview/filters/useBlockSort.tsx b/packages/page-coretime/src/Overview/filters/useBlockSort.tsx new file mode 100644 index 000000000000..5a26d7f5ab12 --- /dev/null +++ b/packages/page-coretime/src/Overview/filters/useBlockSort.tsx @@ -0,0 +1,56 @@ +// Copyright 2017-2025 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ChainInformation } from '@polkadot/react-hooks/types'; +import type { ChainInfoFilterProps, SortDirection } from '../../types.js'; + +import { useCallback, useState } from 'react'; + +export function sortByBlocks (data: number[], chainInfo: Record, direction: SortDirection): number[] { + if (!data || !chainInfo || !direction) { + return data || []; + } + + const filteredData = data.filter((block) => !!chainInfo[block]?.workTaskInfo[0]); + + return [...filteredData].sort((a, b) => { + const aInfo = chainInfo[a]?.workTaskInfo[0]; + const bInfo = chainInfo[b]?.workTaskInfo[0]; + + return direction === 'DESC' + ? bInfo.lastBlock - aInfo.lastBlock + : aInfo.lastBlock - bInfo.lastBlock; + }); +} + +const getNextSortState = (current: SortDirection): SortDirection => + ({ '': 'DESC', ASC: '', DESC: 'ASC' } as const)[current]; + +export function useBlocksSort ({ chainInfo, data, onFilter }: ChainInfoFilterProps) { + const [direction, setDirection] = useState(''); + + const apply = useCallback((data: number[], sort: SortDirection): number[] => { + return sort + ? sortByBlocks(data, chainInfo, sort) + : data; + }, [chainInfo]); + + const onApply = useCallback(() => { + const nextDirection = getNextSortState(direction); + + setDirection(nextDirection); + onFilter(nextDirection ? sortByBlocks(data, chainInfo, nextDirection) : data); + }, [data, chainInfo, onFilter, direction]); + + const reset = useCallback(() => { + setDirection(''); + onFilter(data || []); + }, [data, onFilter]); + + return { + apply, + direction, + onApply, + reset + }; +} diff --git a/packages/page-coretime/src/Overview/filters/useSearchFilter.tsx b/packages/page-coretime/src/Overview/filters/useSearchFilter.tsx new file mode 100644 index 000000000000..000bde5d8f13 --- /dev/null +++ b/packages/page-coretime/src/Overview/filters/useSearchFilter.tsx @@ -0,0 +1,78 @@ +// Copyright 2017-2025 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useCallback, useMemo, useState } from 'react'; + +import { useRelayEndpoints } from '@polkadot/react-hooks/useParaEndpoints'; + +interface UseSearchFilterProps { + data: number[]; + onFilter: (data: number[]) => void; +} + +export function useSearchFilter ({ data, onFilter }: UseSearchFilterProps) { + const [searchValue, setSearchValue] = useState(''); + const endpoints = useRelayEndpoints(); + const endPointsMap = useMemo(() => + Object.fromEntries( + endpoints + .filter((e) => e?.text && e.paraId) + .map((e) => [ + React.isValidElement(e.text) ? '' : String(e.text), + e.paraId + ]) + ), + [endpoints] + ); + + const apply = useCallback((data: number[], activeSearch: number[]): number[] => { + return activeSearch.length > 0 + ? data.filter((id) => activeSearch.includes(id)) + : data; + }, []); + + const reset = useCallback(() => { + setSearchValue(''); + onFilter(data); + }, [data, onFilter]); + + const onInputChange = useCallback((v: string) => { + setSearchValue(v); + const searchLower = v.trim().toLowerCase(); + + if (!searchLower) { + onFilter(data); + + return; + } + + const matchingIds = new Set(); + + for (const item of data) { + const itemStr = item.toString().toLowerCase(); + + if (itemStr.includes(searchLower)) { + matchingIds.add(item); + continue; + } + + for (const [key, value] of Object.entries(endPointsMap)) { + if (key.toLowerCase().includes(searchLower) && value === item) { + matchingIds.add(item); + break; + } + } + } + + const filteredData = Array.from(matchingIds); + + onFilter(apply(data, filteredData)); + }, [data, endPointsMap, onFilter, apply]); + + return { + apply, + onApply: onInputChange, + reset, + searchValue + }; +} diff --git a/packages/page-coretime/src/Overview/filters/useTypeFilter.tsx b/packages/page-coretime/src/Overview/filters/useTypeFilter.tsx new file mode 100644 index 000000000000..4c62b98306cc --- /dev/null +++ b/packages/page-coretime/src/Overview/filters/useTypeFilter.tsx @@ -0,0 +1,76 @@ +// Copyright 2017-2025 @polkadot/app-coretime authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { FlagColor } from '@polkadot/react-components/types'; +import type { ChainInfoFilterProps } from '../../types.js'; + +import React, { useCallback, useState } from 'react'; + +import { Tag } from '@polkadot/react-components'; +import { CoreTimeTypes } from '@polkadot/react-hooks/constants'; + +import { coretimeTypeColours } from '../../utils/index.js'; + +const coretimeTypes = Object.keys(CoreTimeTypes).filter((key) => isNaN(Number(key))); + +const typeOptions = [ + { + text: 'All', + value: 'All' + }, + ...coretimeTypes.map((type) => ({ + text: ( + + ), + value: CoreTimeTypes[type as keyof typeof CoreTimeTypes].toString() + })) +]; + +export function useTypeFilter ({ chainInfo, data, onFilter }: ChainInfoFilterProps) { + const [selectedType, setSelectedType] = useState(''); + const [activeType, setActiveType] = useState([]); + + const apply = useCallback((data: number[], activeType: number[]): number[] => { + return activeType.length > 0 + ? data.filter((id) => activeType.includes(id)) + : data; + }, []); + + const reset = useCallback(() => { + setSelectedType(''); + setActiveType([]); + onFilter(data); + }, [data, onFilter]); + + const onDropDownChange = useCallback((v: string) => { + setSelectedType(v); + + if (!v || v === 'All') { + setActiveType([]); + onFilter(data); + + return; + } + + const filteredData = data.filter((paraId) => { + const taskInfo = chainInfo[paraId]?.workTaskInfo; + + return taskInfo?.length > 0 && taskInfo[0].type.toString() === v; + }); + + setActiveType(filteredData); + onFilter(apply(data, filteredData)); + }, [chainInfo, data, onFilter, apply]); + + return { + activeType, + apply, + onApply: onDropDownChange, + reset, + selectedType, + typeOptions + }; +} diff --git a/packages/page-coretime/src/ParachainsTable.tsx b/packages/page-coretime/src/ParachainsTable.tsx index 27a56081537e..1524eb906195 100644 --- a/packages/page-coretime/src/ParachainsTable.tsx +++ b/packages/page-coretime/src/ParachainsTable.tsx @@ -1,11 +1,12 @@ // Copyright 2017-2025 @polkadot/app-coretime authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Table } from '@polkadot/react-components'; import { type CoretimeInformation } from '@polkadot/react-hooks/types'; +import Filters from './Overview/Filters.js'; import ParachainTableRow from './ParachainTableRow.js'; import { useTranslation } from './translate.js'; @@ -24,31 +25,54 @@ function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement { [t('end'), 'start media--1000'], [t('renewal'), 'start media--1200'], [t('renewal price'), 'start media--1200'], + [t('links'), 'start media--800'], [t('other cores'), 'end'] ]); + const [taskIds, setTaskIds] = useState([]); + + const onFilter = useCallback((filteredData: number[]) => { + setTaskIds(filteredData); + }, []); + + useEffect(() => { + if (coretimeInfo?.taskIds) { + setTaskIds(coretimeInfo?.taskIds); + } + }, [coretimeInfo?.taskIds]); + return ( - - - {coretimeInfo?.taskIds?.map((taskId: number) => { - const chain = coretimeInfo.chainInfo[taskId]; - - return ( - - ); - })} - -
+ <> + + + {taskIds?.map((taskId: number) => { + const chain = coretimeInfo.chainInfo[taskId]; + + if (!chain) { + return null; + } + + return ( + + ); + })} + +
+ ); } diff --git a/packages/page-coretime/src/Row.tsx b/packages/page-coretime/src/Row.tsx index f88a14b9ae69..d8fac0ca26eb 100644 --- a/packages/page-coretime/src/Row.tsx +++ b/packages/page-coretime/src/Row.tsx @@ -6,11 +6,12 @@ import type { ChainWorkTaskInformation, LegacyLease } from '@polkadot/react-hook import React from 'react'; -import { ParaLink, styled, Tag } from '@polkadot/react-components'; +import { MarkWarning, ParaLink, styled, Tag } from '@polkadot/react-components'; +import { ParaLinkType } from '@polkadot/react-components/ParaLink'; import { ChainRenewalStatus, CoreTimeTypes } from '@polkadot/react-hooks/constants'; import { BN, formatBalance, formatNumber } from '@polkadot/util'; -import { estimateTime } from './utils/index.js'; +import { coretimeTypeColours, estimateTime } from './utils/index.js'; import { useCoretimeContext } from './CoretimeContext.js'; interface Props { @@ -23,47 +24,87 @@ interface Props { highlight?: boolean } -const colours: Record = { - [CoreTimeTypes.Reservation]: 'orange', - [CoreTimeTypes.Lease]: 'blue', - [CoreTimeTypes['Bulk Coretime']]: 'pink' -}; +interface StyledCellProps { + $p: boolean; + $width?: string; +} -const StyledCell = styled.td<{ $p: boolean }>` +const StyledCell = styled.td` && { background-color: ${({ $p }) => ($p ? '#F9FAFB' : undefined)}; + width: ${({ $width }) => $width}; + } + height: 55px; +`; + +const StyledMarkWarning = styled(MarkWarning)` + width: fit-content; + margin: 0; + display: inline-block; + vertical-align: middle; + &.mark { + margin: 0 0 0 1rem; + display: inline; } `; +const EXPIRES_IN_DAYS = 7; + function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, lease, regionBegin, regionEnd }: Props): React.ReactElement { const chainRegionEnd = (chainRecord.renewalStatus === ChainRenewalStatus.Renewed ? regionEnd : regionBegin); const targetTimeslice = lease?.until || chainRegionEnd; const showEstimates = !!targetTimeslice && Object.values(CoreTimeTypes)[chainRecord.type] !== CoreTimeTypes.Reservation; - const { coretimeInfo, get } = useCoretimeContext(); + const estimatedTime = showEstimates && get && coretimeInfo && + estimateTime(targetTimeslice, get.blocks.relay(lastCommittedTimeslice), coretimeInfo?.constants?.relay); + + const isWithinWeek = estimatedTime && new Date(estimatedTime).getTime() - Date.now() < EXPIRES_IN_DAYS * 24 * 60 * 60 * 1000; + const isReservation = chainRecord.type === CoreTimeTypes.Reservation; + return ( - {id} {id} + {} {chainRecord?.workload?.core} {showEstimates && get && formatNumber(get.blocks.relay(targetTimeslice)).toString()} + >{showEstimates && chainRecord?.lastBlock && + {formatNumber(chainRecord?.lastBlock)} + } + {showEstimates && get && coretimeInfo && estimateTime(targetTimeslice, get.blocks.relay(lastCommittedTimeslice), coretimeInfo?.constants?.relay)} + style={{ whiteSpace: 'nowrap' }} + > + + {estimatedTime} + {!!isWithinWeek && !isReservation && ( + + )} + + {chainRecord?.renewal ? formatBalance(chainRecord.renewal?.price.toString()) : ''} + {
+ +
+ +
+
} +
{highlight && }
diff --git a/packages/page-coretime/src/types.ts b/packages/page-coretime/src/types.ts index 00aacf6065ff..13ee82c0632d 100644 --- a/packages/page-coretime/src/types.ts +++ b/packages/page-coretime/src/types.ts @@ -1,6 +1,7 @@ // Copyright 2017-2025 @polkadot/app-coretime authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { ChainInformation } from '@polkadot/react-hooks/types'; import type { PhaseName } from './constants.js'; export interface PhaseInfo { @@ -94,3 +95,25 @@ export interface GetResponse { relay: (blocks: number) => number; }; } + +export interface BaseFilterProps { + data: number[]; + onFilter: (data: number[]) => void; +} + +export interface ChainInfoFilterProps extends BaseFilterProps { + chainInfo: Record; +} + +export type SortDirection = 'DESC' | 'ASC' | ''; + +export enum FilterType { + BLOCKS = 'blocks', + SEARCH = 'search', + TYPE = 'type' +} + +export interface ActiveFilters { + search: number[]; + type: number[]; +} diff --git a/packages/page-coretime/src/utils/index.ts b/packages/page-coretime/src/utils/index.ts index c49a4accdcb3..76ee14d537e8 100644 --- a/packages/page-coretime/src/utils/index.ts +++ b/packages/page-coretime/src/utils/index.ts @@ -4,6 +4,7 @@ import type { ChainBlockConstants, ChainConstants, CoretimeInformation } from '@polkadot/react-hooks/types'; import type { ChainName, GetResponse, RegionInfo } from '../types.js'; +import { CoreTimeTypes } from '@polkadot/react-hooks/constants'; import { BN } from '@polkadot/util'; type FirstCycleStartType = Record< @@ -34,6 +35,12 @@ export const FirstCycleStart: FirstCycleStartType = { } }; +export const coretimeTypeColours: Record = { + [CoreTimeTypes.Reservation]: 'orange', + [CoreTimeTypes.Lease]: 'blue', + [CoreTimeTypes['Bulk Coretime']]: 'pink' +}; + export function formatDate (date: Date) { const day = date.getDate(); const month = date.toLocaleString('default', { month: 'short' }); diff --git a/packages/react-components/src/ParaLink.tsx b/packages/react-components/src/ParaLink.tsx index b78f1fd3f45e..635f184721e6 100644 --- a/packages/react-components/src/ParaLink.tsx +++ b/packages/react-components/src/ParaLink.tsx @@ -5,17 +5,27 @@ import type { BN } from '@polkadot/util'; import React, { useMemo } from 'react'; +import { Subscan } from '@polkadot/apps-config/links/subscan'; import { useParaEndpoints } from '@polkadot/react-hooks'; import ChainImg from './ChainImg.js'; +import Icon from './Icon.js'; import { styled } from './styled.js'; +export enum ParaLinkType { + PJS = 'pjs', + HOME = 'home', + SUBSCAN = 'subscan' +} + interface Props { className?: string; id: BN; + showLogo?: boolean; + type?: ParaLinkType; } -function ParaLink ({ className, id }: Props): React.ReactElement | null { +function ParaLink ({ className, id, showLogo = true, type = ParaLinkType.PJS }: Props): React.ReactElement | null { const endpoints = useParaEndpoints(id); const links = useMemo( () => endpoints.filter(({ isDisabled, isUnreachable }) => !isDisabled && !isUnreachable), @@ -26,25 +36,63 @@ function ParaLink ({ className, id }: Props): React.ReactElement | null { return null; } - const { text, ui, value } = links.length + const { homepage, text, ui, value } = links.length ? links[links.length - 1] : endpoints[0]; + const subscanUrl = text && + typeof text === 'string' && + Subscan.chains[text] && + Subscan.create(Subscan.chains[text], '', '').toString(); + return ( - + {showLogo && ( + + )} {links.length ? ( - {text} + <> + {type === ParaLinkType.SUBSCAN && !!subscanUrl && ( + + Subscan + + )} + {type === ParaLinkType.HOME && homepage && ( + + + + )} + {type === ParaLinkType.PJS && ( + + {typeof text === 'string' ? text : text?.toString()} + + )} + ) - : text + : type === ParaLinkType.PJS ? (typeof text === 'string' ? text : text?.toString()) : null } ); diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts index 730148efbb3b..5986822aee95 100644 --- a/packages/react-hooks/src/types.ts +++ b/packages/react-hooks/src/types.ts @@ -332,6 +332,7 @@ export interface PalletBrokerConfigRecord { } export interface ChainWorkTaskInformation { + lastBlock: number renewal: PotentialRenewal | undefined renewalStatus: string type: CoreTimeTypes diff --git a/packages/react-hooks/src/useCoretimeInformation.ts b/packages/react-hooks/src/useCoretimeInformation.ts index 9b1c5da84be8..30beedd59f38 100644 --- a/packages/react-hooks/src/useCoretimeInformation.ts +++ b/packages/react-hooks/src/useCoretimeInformation.ts @@ -119,7 +119,7 @@ function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeI }, [workloads, coreInfos]); useEffect((): void => { - if (!workloadData?.length || !reservations?.length) { + if (!workloadData?.length || !reservations?.length || !coretimeConstants) { return; } @@ -135,9 +135,15 @@ function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeI // parachain can be renewed on a different core const workplan = workplans?.filter((workplan) => workplan.info.task.toString() === taskId); const type = getOccupancyType(lease, reservation, workload?.info.isPool ?? false); + const potentialRenewal = potentialRenewalsCurrentRegion?.find((renewal) => renewal.task.toString() === taskId); let renewalStatus = potentialRenewal ? ChainRenewalStatus.Eligible : ChainRenewalStatus.None; + const chainRegionEnd = (renewalStatus === ChainRenewalStatus.Renewed ? salesInfo?.regionEnd : salesInfo?.regionBegin); + const targetTimeslice = lease?.until || chainRegionEnd; + + const lastBlock = targetTimeslice ? targetTimeslice * coretimeConstants?.relay.blocksPerTimeslice : 0; + const chainRenewedCore = type === CoreTimeTypes['Bulk Coretime'] && !!workplan?.length; if (chainRenewedCore) { @@ -146,6 +152,7 @@ function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeI return { chainRenewedCore, + lastBlock, renewal: potentialRenewal, renewalStatus, type,