Feedback/#11253 (#11304)

* countdown component

* filtering by parachain id in workplan and workload

* lint

* rename chainName to relayName, fixed subscan block link

* always showing price even if the cores are sold out

* moved some calculations to the coretime context

* added broker context for page-broker, fix for wrong dates

* showing time with the estimated date in places

* removed extra code when calculating renewal status

* tsc for page-broker

* page-coretime tsc

* fix showing correct dates for fixed price phase in sale history

* minified workinforow as all rows show the same information now

* default values for fn input

* showing start price of the sale on the sale tab

* replaced custom Countdown element with existing implementation

* adjusted estimateTime function for broker

* adjusted estimateTime function for coretime page

* lint

* added Renewal link for RegionX
This commit is contained in:
Daria 2025-04-04 12:01:20 +13:00 committed by GitHub
parent bd71a8e9b8
commit 468a3d5107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 449 additions and 359 deletions

View File

@ -0,0 +1,83 @@
// Copyright 2017-2025 @polkadot/app-broker authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ReactNode } from 'react';
import type { ApiPromise } from '@polkadot/api';
import type { BrokerStatus, ChainConstants, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord } from '@polkadot/react-hooks/types';
import type { CurrentRegion } from './types.js';
import React, { createContext, useContext, useMemo } from 'react';
import { useBrokerConfig, useBrokerSalesInfo, useBrokerStatus, useCoretimeConsts } from '@polkadot/react-hooks';
import { estimateTime } from './utils.js';
interface BrokerProviderProps {
children: ReactNode;
api: ApiPromise;
isApiReady: boolean;
}
interface BrokerContextProps {
config: PalletBrokerConfigRecord | null,
coretimeConsts: ChainConstants | null,
currentRegion: CurrentRegion,
saleInfo: PalletBrokerSaleInfoRecord | null,
status: BrokerStatus | null,
}
const initialState = {
config: null,
coretimeConsts: null,
currentRegion: {
begin: null,
beginDate: null,
end: null,
endDate: null
},
saleInfo: null,
status: null
};
export const BrokerContext = createContext<BrokerContextProps>(initialState);
export const BrokerProvider = ({ api, children, isApiReady }: BrokerProviderProps) => {
const coretimeConsts = useCoretimeConsts();
const config = useBrokerConfig(api, isApiReady);
const saleInfo = useBrokerSalesInfo(api, isApiReady);
const status = useBrokerStatus(api, isApiReady);
const currentRegionEnd = useMemo(() => saleInfo?.regionBegin, [saleInfo]);
const currentRegionBegin = useMemo(() => saleInfo && config && saleInfo?.regionBegin - config?.regionLength, [saleInfo, config]);
const lastBlock = useMemo(() => status && coretimeConsts && status.lastTimeslice * coretimeConsts.relay.blocksPerTimeslice, [status, coretimeConsts]);
const currentRegionEndDate = useMemo(() => currentRegionEnd && lastBlock && estimateTime(Number(currentRegionEnd), lastBlock)?.formattedDate, [currentRegionEnd, lastBlock]);
const currentRegionBeginDate = useMemo(() => currentRegionBegin && lastBlock && estimateTime(Number(currentRegionBegin), lastBlock)?.formattedDate, [currentRegionBegin, lastBlock]);
const value = useMemo(() => {
if (!config || !saleInfo || !status || !currentRegionBegin || !currentRegionBeginDate || !currentRegionEnd || !currentRegionEndDate || !coretimeConsts) {
return initialState;
}
return ({
config,
coretimeConsts,
currentRegion: {
begin: currentRegionBegin,
beginDate: currentRegionBeginDate,
end: currentRegionEnd,
endDate: currentRegionEndDate
},
saleInfo,
status
});
}, [currentRegionBegin, currentRegionEnd, currentRegionBeginDate, currentRegionEndDate, config, saleInfo, status, coretimeConsts]);
return (
<BrokerContext.Provider value={value}>
{children}
</BrokerContext.Provider>
);
};
export const useBrokerContext = () => useContext(BrokerContext);

View File

@ -27,7 +27,7 @@ function CoreTable ({ api, config, core, workload, workplan }: Props): React.Rea
[
<div key={`header${core}`}>{headerRef.current} {core} <span></span></div>,
'core',
8,
9,
undefined
]
];

View File

@ -6,8 +6,7 @@ import type { CoreInfo } from '../types.js';
import React from 'react';
import { useBrokerConfig } from '@polkadot/react-hooks';
import { useBrokerContext } from '../BrokerContext.js';
import CoreTable from './CoreTable.js';
interface Props {
@ -16,8 +15,8 @@ interface Props {
data: CoreInfo[];
}
function CoresTable ({ api, data, isApiReady }: Props): React.ReactElement<Props> {
const config = useBrokerConfig(api, isApiReady);
function CoresTable ({ api, data }: Props): React.ReactElement<Props> {
const { config } = useBrokerContext();
return (
<>

View File

@ -23,7 +23,7 @@ interface Props {
const filterLoad = (parachainId: string, data: CoreInfo[], workloadCoreSelected: number): CoreInfo[] => {
if (parachainId) {
return data.filter(({ workload }) => !!workload?.filter(({ info }) => info.task === parachainId).length);
return data.filter(({ workload, workplan }) => !!workload?.filter(({ info }) => info.task === parachainId).length || !!workplan?.filter(({ info }) => info.task === parachainId).length);
}
if (workloadCoreSelected === -1) {

View File

@ -4,17 +4,17 @@
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { statsType } from '../types.js';
import React, { useMemo } from 'react';
import React from 'react';
import { CardSummary, styled, SummaryBox, UsageBar } from '@polkadot/react-components';
import { defaultHighlight } from '@polkadot/react-components/styles';
import { useApi, useBrokerConfig, useBrokerSalesInfo, useBrokerStatus } from '@polkadot/react-hooks';
import { useApi } from '@polkadot/react-hooks';
import { type CoreWorkload } from '@polkadot/react-hooks/types';
import { useCoretimeConsts } from '@polkadot/react-hooks/useCoretimeConsts';
import { BN, BN_ZERO } from '@polkadot/util';
import { useBrokerContext } from '../BrokerContext.js';
import { useTranslation } from '../translate.js';
import { estimateTime, getStats } from '../utils.js';
import { getStats } from '../utils.js';
import RegionLength from './Summary/RegionLength.js';
import Timeslice from './Summary/Timeslice.js';
import TimeslicePeriod from './Summary/TimeslicePeriod.js';
@ -35,23 +35,17 @@ const StyledSection = styled.section`
`;
interface Props {
coreCount?: string
apiEndpoint?: LinkOption | null;
coreCount?: string
workloadInfos?: CoreWorkload[]
}
function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
const { t } = useTranslation();
const { api, apiEndpoint, isApiReady } = useApi();
const coretimeConstants = useCoretimeConsts();
const { api, apiEndpoint } = useApi();
const uiHighlight = apiEndpoint?.ui.color || defaultHighlight;
const { idles, pools, tasks }: statsType = React.useMemo(() => getStats(coreCount, workloadInfos), [coreCount, workloadInfos]);
const saleInfo = useBrokerSalesInfo(api, isApiReady);
const config = useBrokerConfig(api, isApiReady);
const status = useBrokerStatus(api, isApiReady);
const currentRegionEnd = useMemo(() => saleInfo && config && saleInfo?.regionEnd - config?.regionLength, [saleInfo, config]);
const currentRegionStart = useMemo(() => currentRegionEnd && config && currentRegionEnd - config?.regionLength, [currentRegionEnd, config]);
const { config, currentRegion, saleInfo, status } = useBrokerContext();
return (
<SummaryBox>
@ -81,7 +75,7 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
progress={{
isBlurred: false,
total: new BN(config?.regionLength || 0),
value: (config?.regionLength && currentRegionEnd && status && new BN(config?.regionLength - (currentRegionEnd - status?.lastTimeslice))) || BN_ZERO,
value: (config?.regionLength && currentRegion.end && status && new BN(config?.regionLength - (currentRegion.end - status?.lastTimeslice))) || BN_ZERO,
withTime: false
}}
/>
@ -104,7 +98,7 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
</div>
</StyledSection>
<section>
{status && currentRegionStart && currentRegionEnd &&
{currentRegion.begin && currentRegion.end &&
(
<>
<CardSummary
@ -112,8 +106,8 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
label={t('sale dates')}
>
<div>
<div style={{ fontSize: '14px' }}>{estimateTime(currentRegionStart, status?.lastTimeslice * 80, coretimeConstants?.relay)}</div>
<div style={{ fontSize: '14px' }}>{estimateTime(currentRegionEnd, status?.lastTimeslice * 80, coretimeConstants?.relay)}</div>
<div style={{ fontSize: '14px' }}>{currentRegion.beginDate}</div>
<div style={{ fontSize: '14px' }}>{currentRegion.endDate}</div>
</div>
</CardSummary>
<CardSummary
@ -121,8 +115,8 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
label={t('sale ts')}
>
<div>
<div style={{ fontSize: '14px' }}>{currentRegionStart}</div>
<div style={{ fontSize: '14px' }}>{currentRegionEnd}</div>
<div style={{ fontSize: '14px' }}>{currentRegion.begin}</div>
<div style={{ fontSize: '14px' }}>{currentRegion.end}</div>
</div>
</CardSummary>
</>

View File

@ -50,150 +50,62 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
return (
<>
<td style={{ width: 200 }}>no task</td>
<td colSpan={6} />
<td colSpan={7} />
</>);
}
switch (data.type) {
case (CoreTimeTypes.Reservation): {
return (
<>
<TableCol
header='Task'
value={data.task}
/>
<TableCol
header='Blocks per timeslice'
value={data.maskBits}
/>
<td>
return (
<>
<TableCol
header='Task'
value={data.task}
/>
<TableCol
header='Blocks per timeslice'
value={data.maskBits}
/>
<TableCol
header='Start ts'
hide='both'
value={data.startTimeslice}
/>
<TableCol
header='Start date'
hide='both'
value={data.start}
/>
<TableCol
header='End date'
hide='both'
value={data.end}
/>
<TableCol
header='Last block (relay)'
value={data.endBlock}
/>
<StyledTableCol hide={'mobile'}>
<h5 style={{ opacity: '0.6' }}>type</h5>
{data.type &&
(
<Tag
color={colours[data.type] as FlagColor}
label={Object.values(CoreTimeTypes)[data.type]}
/>
</td>
<td colSpan={4} />
</>
);
}
)
}
</StyledTableCol>
{data.owner
? <StyledTableCol hide='mobile'>
<h5 style={{ opacity: '0.6' }}>{'Owner'}</h5>
<AddressMini
isPadded={false}
key={data.owner}
value={data.owner}
/>
case (CoreTimeTypes.Lease):
return (
<>
<TableCol
header='Task'
value={data.task}
/>
<TableCol
header='Blocks per timeslice'
value={data.maskBits}
/>
<TableCol
header='Start'
hide='both'
value={data.start}
/>
<TableCol
header='End'
hide='both'
value={data.end}
/>
<TableCol
header='Last block'
value={data.endBlock}
/>
<StyledTableCol hide={'mobile'}>
<h5 style={{ opacity: '0.6' }}>type</h5>
<Tag
color={colours[data.type] as FlagColor}
label={Object.values(CoreTimeTypes)[data.type]}
/>
</StyledTableCol>
<td colSpan={1} />
</>);
case (CoreTimeTypes['On Demand']): {
return (
<>
<TableCol
header='Task'
value={data.task}
/>
<TableCol
header='Blocks per timeslice'
value={data.maskBits}
/>
<TableCol
header='Start'
hide='both'
value={data.start}
/>
<TableCol
header='End'
hide='both'
value={data.end}
/>
<TableCol
header='Last block'
value={data.endBlock}
/>
<StyledTableCol hide={'mobile'}>
<h5 style={{ opacity: '0.6' }}>type</h5>
<Tag
color={colours[data.type] as FlagColor}
label={Object.values(CoreTimeTypes)[data.type]}
/>
</StyledTableCol>
<td colSpan={1} />
</>);
}
default: {
return (
<>
<TableCol
header='Task'
value={data.task}
/>
<TableCol
header='Blocks per timeslice'
value={data.maskBits}
/>
<TableCol
header='Start'
hide='both'
value={data.start}
/>
<TableCol
header='End'
hide='both'
value={data.end}
/>
<TableCol
header='Last block'
value={data.endBlock}
/>
<StyledTableCol hide={'mobile'}>
<h5 style={{ opacity: '0.6' }}>type</h5>
<Tag
color={colours[CoreTimeTypes['Bulk Coretime']] as FlagColor}
label={'Bulk Coretime'}
/>
</StyledTableCol>
{data.owner
? <StyledTableCol hide='mobile'>
<h5 style={{ opacity: '0.6' }}>{'Owner'}</h5>
<AddressMini
isPadded={false}
key={data.owner}
value={data.owner}
/>
</StyledTableCol>
: <td></td>}
</>);
}
}
</StyledTableCol>
: <td></td>}
</>);
}
export default React.memo(WorkInfoRow);

View File

@ -3,17 +3,16 @@
import type { ApiPromise } from '@polkadot/api';
import type { PalletBrokerConfigRecord, RegionInfo } from '@polkadot/react-hooks/types';
import type { Option } from '@polkadot/types';
import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup';
import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from '../types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { ExpandButton } from '@polkadot/react-components';
import { useApi, useBrokerSalesInfo, useCall, useRegions, useToggle } from '@polkadot/react-hooks';
import { useRegions, useToggle } from '@polkadot/react-hooks';
import { useCoretimeConsts } from '@polkadot/react-hooks/useCoretimeConsts';
import { formatRowInfo } from '../utils.js';
import { useBrokerContext } from '../BrokerContext.js';
import { estimateTime, formatRowInfo } from '../utils.js';
import WorkInfoRow from './WorkInfoRow.js';
import Workplan from './Workplan.js';
@ -26,57 +25,65 @@ interface Props {
}
function Workload ({ api, config, core, workload, workplan }: Props): React.ReactElement<Props> {
const { isApiReady } = useApi();
const salesInfo = useBrokerSalesInfo(api, isApiReady);
const coretimeConstants = useCoretimeConsts();
const status = useCall<Option<PalletBrokerStatusRecord>>(isApiReady && api.query.broker?.status);
const [isExpanded, toggleIsExpanded] = useToggle(false);
const [workloadData, setWorkloadData] = useState<InfoRow[]>();
const [workplanData, setWorkplanData] = useState<InfoRow[]>();
const currentTimeSlice = useMemo(() => {
if (status?.isSome) {
return status.unwrap().lastCommittedTimeslice.toNumber();
}
const { currentRegion, status } = useBrokerContext();
return 0;
}, [status]);
const currentTimeSlice = useMemo(() =>
status?.lastTimeslice ?? 0
, [status]);
const regionInfo = useRegions(api);
const region: RegionInfo | undefined = useMemo(() => regionInfo?.find((v) => v.core === core && v.start <= currentTimeSlice && v.end > currentTimeSlice), [regionInfo, core, currentTimeSlice]);
const regionOwnerInfo: RegionInfo | undefined = useMemo(() => regionInfo?.find((v) => v.core === core && v.start <= currentTimeSlice && v.end > currentTimeSlice), [regionInfo, core, currentTimeSlice]);
useEffect(() => {
if (!!workload?.length && !!salesInfo) {
if (!!workload?.length && currentTimeSlice > 0) {
// saleInfo points to a regionEnd and regionBeing in the next cycle, but we want the start and end of the current cycle
setWorkloadData(formatRowInfo(
workload,
core,
region,
regionOwnerInfo,
currentTimeSlice,
{ regionBegin: salesInfo.regionBegin - config.regionLength, regionEnd: salesInfo.regionEnd - config.regionLength },
{
begin: currentRegion.begin || 0,
beginDate: currentRegion.beginDate || '',
end: currentRegion.end || 0,
endDate: currentRegion.endDate || ''
},
config.regionLength,
coretimeConstants?.relay
));
} else {
return setWorkloadData([{ core }]);
}
}, [workload, region, currentTimeSlice, core, salesInfo, config, coretimeConstants]);
}, [workload, regionOwnerInfo, currentTimeSlice, core, config, coretimeConstants, currentRegion]);
useEffect(() => {
if (!!workplan?.length && !!salesInfo) {
if (workplan?.length && status && coretimeConstants && currentRegion.endDate) {
const futureRegionStart = currentRegion.end || 0;
const futureRegionEnd = futureRegionStart + config.regionLength;
const lastBlock = status.lastTimeslice * coretimeConstants?.relay.blocksPerTimeslice;
setWorkplanData(formatRowInfo(
workplan,
core,
region,
currentTimeSlice,
{ regionBegin: salesInfo.regionBegin - config.regionLength, regionEnd: salesInfo.regionEnd - config.regionLength },
regionOwnerInfo,
status.lastTimeslice,
{
begin: futureRegionStart,
beginDate: currentRegion.endDate,
end: futureRegionEnd,
endDate: estimateTime(futureRegionEnd, lastBlock)?.formattedDate ?? ''
},
config.regionLength,
coretimeConstants?.relay
));
}
}
, [workplan, region, currentTimeSlice, core, salesInfo, config, coretimeConstants]);
}, [workplan, regionOwnerInfo, status, core, config, coretimeConstants, currentRegion]);
const hasWorkplan = workplan?.length;
@ -113,14 +120,12 @@ function Workload ({ api, config, core, workload, workplan }: Props): React.Reac
<>
<tr>
<td style={{ fontWeight: 700, paddingTop: '2rem', width: 150 }}>workplans</td>
<td colSpan={7}></td>
<td colSpan={8}></td>
</tr>
{workplanData?.map((workplanInfo) => (
<Workplan
currentTimeSlice={currentTimeSlice}
isExpanded={isExpanded}
key={workplanInfo.core}
region={region}
workplanData={workplanInfo}
/>
))}

View File

@ -1,7 +1,6 @@
// Copyright 2017-2025 @polkadot/app-broker authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { RegionInfo } from '@polkadot/react-hooks/types';
import type { InfoRow } from '../types.js';
import React from 'react';
@ -13,9 +12,7 @@ import WorkInfoRow from './WorkInfoRow.js';
interface Props {
className?: string;
workplanData: InfoRow;
currentTimeSlice: number
isExpanded: boolean
region: RegionInfo | undefined
}
function Workplan ({ isExpanded, workplanData }: Props): React.ReactElement<Props> {
@ -26,7 +23,7 @@ function Workplan ({ isExpanded, workplanData }: Props): React.ReactElement<Prop
style={{ minHeight: '100px' }}
>
<td> <Spinner /> </td>
<td colSpan={6}></td>
<td colSpan={7}></td>
</tr>
);
}

View File

@ -6,8 +6,9 @@ import type { CoreInfo } from '../types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { useApi, useBrokerLeases, useBrokerReservations, useBrokerStatus, useWorkloadInfos, useWorkplanInfos } from '@polkadot/react-hooks';
import { useApi, useBrokerLeases, useBrokerReservations, useWorkloadInfos, useWorkplanInfos } from '@polkadot/react-hooks';
import { useBrokerContext } from '../BrokerContext.js';
import { createTaskMap, getOccupancyType } from '../utils.js';
import CoresTable from './CoresTables.js';
import Filters from './Filters.js';
@ -53,10 +54,10 @@ const formatData = (coreCount: number, workplan: CoreWorkplan[], workload: CoreW
function Overview ({ className }: Props): React.ReactElement<Props> {
const { api, apiEndpoint, isApiReady } = useApi();
const [data, setData] = useState<CoreInfo[]>([]);
const { currentRegion, status } = useBrokerContext();
const [data, setData] = useState<CoreInfo[]>([]);
const [filtered, setFiltered] = useState<CoreInfo[]>();
const status = useBrokerStatus(api, isApiReady);
const workloadInfos: CoreWorkload[] | undefined = useWorkloadInfos(api, isApiReady);
const workplanInfos: CoreWorkplan[] | undefined = useWorkplanInfos(api, isApiReady);
@ -73,11 +74,12 @@ function Overview ({ className }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Summary
apiEndpoint={apiEndpoint}
coreCount={status?.coreCount.toString() || '-'}
workloadInfos={workloadInfos}
></Summary>
{!!currentRegion.beginDate && !!currentRegion.endDate &&
<Summary
apiEndpoint={apiEndpoint}
coreCount={status?.coreCount.toString() || '-'}
workloadInfos={workloadInfos}
/>}
{!!data?.length &&
(<>
<Filters

View File

@ -6,8 +6,10 @@ import type { TabItem } from '@polkadot/react-components/types';
import React, { useRef } from 'react';
import { Tabs } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import Overview from './Overview/index.js';
import { BrokerProvider } from './BrokerContext.js';
import { useTranslation } from './translate.js';
interface Props {
@ -28,14 +30,20 @@ function createItemsRef (t: (key: string, options?: { replace: Record<string, un
function BrokerApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const itemsRef = useRef(createItemsRef(t));
const { api, isApiReady } = useApi();
return (
<main className={className}>
<Tabs
basePath={basePath}
items={itemsRef.current}
/>
<Overview />
<BrokerProvider
api={api}
isApiReady={isApiReady}
>
<Tabs
basePath={basePath}
items={itemsRef.current}
/>
<Overview />
</BrokerProvider>
</main>
);
}

View File

@ -10,6 +10,7 @@ export interface InfoRow {
core: number
mask?: string
start?: string | null,
startTimeslice?: number | null
end?: string | null
owner?: string
leaseLength?: number
@ -38,3 +39,10 @@ export interface CoreWorkloadType extends CoreWorkload {
lastBlock: number,
type: CoreTimeTypes
}
export interface CurrentRegion {
begin: number | null,
beginDate: string | null,
end: number | null,
endDate: string | null
}

View File

@ -16,9 +16,11 @@ function formatDate (date: Date) {
}
/**
* Calculation on the Relay chain
*
* blockTime = 6000 ms
* BlocksPerTimeslice = 80
* Default Regoin = 5040 timeslices
* Default Region = 5040 timeslices
* TargetBlock = TargetTimeslice * BlocksPerTimeslice
* Block Time Difference = |TargetBlock - latest Block| * blockTime
*
@ -31,26 +33,29 @@ function formatDate (date: Date) {
export const estimateTime = (
targetTimeslice: string | number,
latestBlock: number,
{ blocksPerTimeslice: blocksPerTs, blocktimeMs }: ChainBlockConstants = { blocksPerTimeslice: 0, blocktimeMs: 0 }
): string | null => {
{ blocksPerTimeslice: blocksPerTs, blocktimeMs }: ChainBlockConstants = { blocksPerTimeslice: 80, blocktimeMs: 6000 }
): { timestamp: number, formattedDate: string } | null => {
if (!latestBlock || !targetTimeslice) {
console.error('Invalid input: one or more inputs are missing');
return null;
}
const now = new Date().getTime();
const now = new BN(Date.now());
try {
const blockTime = new BN(blocktimeMs); // Average block time in milliseconds (6 seconds)
const timeSlice = new BN(blocksPerTs);
const blocksPerTimeslice = new BN(blocksPerTs);
const targetBlock = new BN(Number(targetTimeslice)).mul(blocksPerTimeslice);
const latestBlockBN = new BN(latestBlock);
const timestampBN = new BN(now);
const targetBlockBN = new BN(targetTimeslice).mul(timeSlice);
const blockTimeDifference = targetBlockBN.sub(latestBlockBN).mul(blockTime);
const estTimestamp = timestampBN.add(blockTimeDifference);
const blockDifference = targetBlock.sub(latestBlockBN);
const timeDifference = blockDifference.mul(blockTime);
const estTimestamp = now.add(timeDifference);
return formatDate(new Date(estTimestamp.toNumber()));
return {
formattedDate: formatDate(new Date(estTimestamp.toNumber())),
timestamp: estTimestamp.toNumber()
};
} catch (error) {
console.error('Error in calculation:', error);
@ -70,36 +75,38 @@ export const estimateTime = (
export function formatRowInfo (
data: CoreWorkloadType[] | CoreWorkplanType[],
core: number,
currentRegion: RegionInfo | undefined,
regionOwnerInfo: RegionInfo | undefined,
currentTimeSlice: number,
{ regionBegin, regionEnd }: { regionBegin: number, regionEnd: number },
currentRegion: { begin: number, beginDate: string, end: number, endDate: string },
regionLength: number,
coretimeRelayConstants: ChainBlockConstants = { blocksPerTimeslice: 0, blocktimeMs: 0 }
): InfoRow[] {
const blockNumberNow = currentTimeSlice * coretimeRelayConstants.blocksPerTimeslice;
return data.map((one: CoreWorkloadType | CoreWorkplanType) => {
const item: InfoRow = { core, maskBits: one?.info?.maskBits, task: one?.info?.task, type: one?.type };
const blockNumberNow = currentTimeSlice * 80;
const item: InfoRow = {
core,
maskBits: one?.info?.maskBits,
task: one?.info?.task,
type: one?.type
};
item.type = one.type;
let end; let start = null;
if (one.type === CoreTimeTypes.Lease) {
// For region-based types, use the provided dates
if ([CoreTimeTypes['Bulk Coretime'], CoreTimeTypes.Reservation, CoreTimeTypes['On Demand']].includes(one.type)) {
item.start = currentRegion.beginDate;
item.end = currentRegion.endDate;
item.startTimeslice = currentRegion.begin;
item.endBlock = currentRegion.end * coretimeRelayConstants.blocksPerTimeslice;
} else if (one.type === CoreTimeTypes.Lease) { // For lease type, calculate the end
const period = Math.floor(one.lastBlock / regionLength);
const endTs = period * regionLength;
const endEstimate = estimateTime(endTs, blockNumberNow, coretimeRelayConstants);
end = period * regionLength;
} else {
start = currentRegion?.start?.toString() ?? regionBegin;
end = currentRegion?.end?.toString() ?? regionEnd;
item.end = endEstimate?.formattedDate ?? null;
item.endBlock = endTs * coretimeRelayConstants.blocksPerTimeslice;
}
item.owner = currentRegion?.owner.toString();
item.start = start ? estimateTime(start, blockNumberNow, coretimeRelayConstants) : null;
item.end = end ? estimateTime(end, blockNumberNow, coretimeRelayConstants) : null;
item.endBlock = end ? Number(end) * 80 : 0;
if ('timeslice' in one && !start) {
start = estimateTime(one.timeslice, blockNumberNow, coretimeRelayConstants) ?? null;
}
item.owner = regionOwnerInfo?.owner.toString();
return item;
});

View File

@ -9,17 +9,27 @@ import React, { createContext, useContext, useMemo } from 'react';
import { useCoretimeInformation } from '@polkadot/react-hooks';
import { createGet } from './utils/index.js';
import { createGet, estimateTime } from './utils/index.js';
interface CoretimeContextProps {
coretimeInfo: CoretimeInformation | null;
get: ReturnType<typeof createGet> | null;
saleStartDate: string | null
saleEndDate: string | null
currentRegionEnd: number | null
currentRegionStart: number | null
}
const CoretimeContext = createContext<CoretimeContextProps>({
const initState = {
coretimeInfo: null,
get: null
});
currentRegionEnd: null,
currentRegionStart: null,
get: null,
saleEndDate: null,
saleStartDate: null
};
const CoretimeContext = createContext<CoretimeContextProps>(initState);
export const CoretimeProvider = ({ api,
children,
@ -37,7 +47,26 @@ export const CoretimeProvider = ({ api,
return null;
}, [coretimeInfo?.constants]);
const value = useMemo(() => ({ coretimeInfo: coretimeInfo ?? null, get }), [coretimeInfo, get]);
const currentRegionEnd = useMemo(() => coretimeInfo ? coretimeInfo?.salesInfo.regionEnd - coretimeInfo?.config.regionLength : 0, [coretimeInfo]);
const currentRegionStart = useMemo(() => coretimeInfo ? coretimeInfo.salesInfo.regionEnd - coretimeInfo?.config.regionLength * 2 : 0, [coretimeInfo]);
const saleStartDate = useMemo(() => get && coretimeInfo && estimateTime(currentRegionStart, get.blocks.relay(coretimeInfo?.status?.lastTimeslice), coretimeInfo.constants.relay)?.formattedDate, [currentRegionStart, coretimeInfo, get]);
const saleEndDate = useMemo(() => get && coretimeInfo && estimateTime(currentRegionEnd, get.blocks.relay(coretimeInfo?.status?.lastTimeslice), coretimeInfo.constants.relay)?.formattedDate, [currentRegionEnd, coretimeInfo, get]);
const value = useMemo(() => {
if (!coretimeInfo || !currentRegionEnd || !currentRegionStart || !get || !saleEndDate || !saleStartDate) {
return initState;
}
return {
coretimeInfo: coretimeInfo ?? null,
currentRegionEnd,
currentRegionStart,
get,
saleEndDate,
saleStartDate
};
}, [coretimeInfo, currentRegionEnd, currentRegionStart, get, saleEndDate, saleStartDate]);
return (
<CoretimeContext.Provider value={value}>

View File

@ -3,7 +3,7 @@
import type { ApiPromise } from '@polkadot/api';
import type { TabItem } from '@polkadot/react-components/types';
import type { ChainName } from './types.js';
import type { RelayName } from './types.js';
import React, { useRef } from 'react';
import { Route, Routes } from 'react-router-dom';
@ -39,7 +39,7 @@ function createItemsRef (t: (key: string, options?: { replace: Record<string, un
function CoretimePage ({ api, basePath, className, isApiReady }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const itemsRef = useRef(createItemsRef(t));
const chainName = useCall<string>(isApiReady && api?.rpc.system.chain)?.toString().toLowerCase() as ChainName;
const relayName = useCall<string>(isApiReady && api?.rpc.system.chain)?.toString().toLowerCase() as RelayName;
return (
<main className={className}>
@ -47,16 +47,16 @@ function CoretimePage ({ api, basePath, className, isApiReady }: Props): React.R
basePath={basePath}
items={itemsRef.current}
/>
{!chainName
{!relayName
? <div><Spinner /></div>
: <Routes>
<Route path={basePath}>
<Route
element={<Overview chainName={chainName} />}
element={<Overview relayName={relayName} />}
index
/>
<Route
element={<Sale chainName={chainName} />}
element={<Sale relayName={relayName} />}
path='sale'
/>
</Route>

View File

@ -1,8 +1,8 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BrokerStatus, ChainConstants, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@polkadot/react-hooks/types';
import type { ChainName } from '../types.js';
import type { BrokerStatus, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@polkadot/react-hooks/types';
import type { RelayName } from '../types.js';
import React, { useMemo } from 'react';
@ -11,7 +11,7 @@ import { BN } from '@polkadot/util';
import { useCoretimeContext } from '../CoretimeContext.js';
import { useTranslation } from '../translate.js';
import { estimateTime, FirstCycleStart } from '../utils/index.js';
import { FirstCycleStart } from '../utils/index.js';
interface Props {
coreDscriptors?: CoreDescription[];
@ -20,25 +20,24 @@ interface Props {
region: RegionInfo[],
status: BrokerStatus,
parachainCount: number
chainName: ChainName,
constants: ChainConstants
relayName: RelayName,
}
function Summary ({ chainName, config, constants, parachainCount, saleInfo, status }: Props): React.ReactElement<Props> {
function Summary ({ config, parachainCount, relayName, status }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const currentRegionEnd = saleInfo.regionEnd - config.regionLength;
const currentRegionStart = saleInfo.regionEnd - config.regionLength * 2;
const { get } = useCoretimeContext();
const { coretimeInfo, currentRegionEnd, currentRegionStart, saleEndDate, saleStartDate } = useCoretimeContext();
const saleNumber = useMemo(() => {
if (chainName && currentRegionEnd) {
if (relayName && currentRegionEnd) {
return Math.floor(
(currentRegionEnd - FirstCycleStart.timeslice.coretime[chainName]) / config.regionLength
(currentRegionEnd - FirstCycleStart.timeslice.coretime[relayName]) / config.regionLength
);
}
return undefined;
}, [currentRegionEnd, chainName, config]);
}, [currentRegionEnd, relayName, config]);
const timeslicesSinceCycleStart = useMemo(() => currentRegionEnd && new BN(config?.regionLength).sub((new BN(currentRegionEnd)).sub(new BN(status.lastTimeslice))), [status, config, currentRegionEnd]);
return (
<SummaryBox>
@ -56,36 +55,46 @@ function Summary ({ chainName, config, constants, parachainCount, saleInfo, stat
<CardSummary label={t('parachains')}>
{parachainCount && parachainCount}
</CardSummary>
{config && status &&
<CardSummary
className='media--800'
label={t('cycle progress')}
progress={{
isBlurred: false,
total: new BN(config?.regionLength),
value: new BN(config?.regionLength - (currentRegionEnd - status.lastTimeslice)),
withTime: false
}}
/>
{config && status && currentRegionEnd && saleEndDate && saleStartDate && timeslicesSinceCycleStart && coretimeInfo?.constants &&
<>
<CardSummary
className='media--800'
label={t('timeslice progress')}
progress={{
hideGraph: true,
hideValue: false,
isBlurred: false,
total: new BN(config?.regionLength),
value: timeslicesSinceCycleStart,
withTime: false
}}
/>
<CardSummary
label={t('cycle')}
progress={{
total: new BN(config.regionLength).mul(new BN(coretimeInfo?.constants.relay.blocksPerTimeslice)),
value: timeslicesSinceCycleStart.mul(new BN(coretimeInfo?.constants.relay.blocksPerTimeslice)),
withTime: true
}}
/>
</>
}
</section>
<section className='media--1200'>
{status &&
(<CardSummary label={t('sale dates')}>
<div>
<div style={{ fontSize: '14px' }}>{get && estimateTime(currentRegionStart, get.blocks.relay(status?.lastTimeslice), constants.relay)}</div>
<div style={{ fontSize: '14px' }}>{get && estimateTime(currentRegionEnd, get.blocks.relay(status?.lastTimeslice), constants.relay)}</div>
</div>
</CardSummary>)
}
{status &&
<CardSummary label={t('sale ts')}>
<div>
<div style={{ fontSize: '14px' }}>{currentRegionStart}</div>
<div style={{ fontSize: '14px' }}>{currentRegionEnd}</div>
</div>
</CardSummary>
}
<CardSummary label={t('sale dates')}>
<div>
<div style={{ fontSize: '14px' }}>{saleStartDate}</div>
<div style={{ fontSize: '14px' }}>{saleEndDate}</div>
</div>
</CardSummary>
<CardSummary label={t('sale ts')}>
<div>
<div style={{ fontSize: '14px' }}>{currentRegionStart}</div>
<div style={{ fontSize: '14px' }}>{currentRegionEnd}</div>
</div>
</CardSummary>
</section>
</SummaryBox>
);

View File

@ -1,7 +1,7 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainName } from '../types.js';
import type { RelayName } from '../types.js';
import React from 'react';
@ -11,21 +11,20 @@ import Summary from './Summary.js';
interface Props {
className?: string;
chainName: ChainName
relayName: RelayName
}
function Overview ({ chainName, className }: Props): React.ReactElement<Props> {
function Overview ({ className, relayName }: Props): React.ReactElement<Props> {
const { coretimeInfo } = useCoretimeContext();
return (
<main className={className}>
{coretimeInfo && (
<Summary
chainName={chainName}
config={coretimeInfo?.config}
constants={coretimeInfo?.constants}
parachainCount={coretimeInfo.taskIds?.length || 0}
region={coretimeInfo?.region}
relayName={relayName}
saleInfo={coretimeInfo?.salesInfo}
status={coretimeInfo?.status}
/>
@ -33,6 +32,7 @@ function Overview ({ chainName, className }: Props): React.ReactElement<Props> {
{!!coretimeInfo &&
<ParachainsTable
coretimeInfo={coretimeInfo}
relayName={relayName}
/>
}
</main>

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ChainInformation, ChainWorkTaskInformation } from '@polkadot/react-hooks/types';
import type { RelayName } from './types.js';
import React from 'react';
@ -15,9 +16,10 @@ interface Props {
regionEnd: number
regionBegin: number
lastCommittedTimeslice: number
relayName: RelayName
}
function ParachainTableRow ({ chain, lastCommittedTimeslice, regionBegin, regionEnd }: Props): React.ReactElement<Props> {
function ParachainTableRow ({ chain, lastCommittedTimeslice, regionBegin, regionEnd, relayName }: Props): React.ReactElement<Props> {
const [isExpanded, toggleIsExpanded] = useToggle(false);
const info = chain.workTaskInfo;
const firstRecord = chain.workTaskInfo[0];
@ -38,6 +40,7 @@ function ParachainTableRow ({ chain, lastCommittedTimeslice, regionBegin, region
lease={chain.lease}
regionBegin={regionBegin}
regionEnd={regionEnd}
relayName={relayName}
/>
{idx === 0 && <td style={{ paddingRight: '2rem', textAlign: 'right', verticalAlign: 'top' }}>

View File

@ -1,6 +1,8 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { RelayName } from './types.js';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Table } from '@polkadot/react-components';
@ -12,9 +14,10 @@ import { useTranslation } from './translate.js';
interface Props {
coretimeInfo: CoretimeInformation
relayName: RelayName
}
function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement<Props> {
function ParachainsTable ({ coretimeInfo, relayName }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('parachains'), 'start'],
@ -22,7 +25,7 @@ function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement<Props> {
[t('core number'), 'start'],
[t('type'), 'start'],
[t('last block'), 'start media--800'],
[t('end'), 'start media--1000'],
[t('end date (approx)'), 'start media--1000'],
[t('renewal'), 'start media--1200'],
[t('renewal price'), 'start media--1200'],
[t('links'), 'start media--800'],
@ -67,6 +70,7 @@ function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement<Props> {
lastCommittedTimeslice={coretimeInfo.status.lastCommittedTimeslice}
regionBegin={coretimeInfo.salesInfo.regionBegin}
regionEnd={coretimeInfo.salesInfo.regionEnd}
relayName={relayName}
/>
);
})}

View File

@ -3,6 +3,7 @@
import type { FlagColor } from '@polkadot/react-components/types';
import type { ChainWorkTaskInformation, LegacyLease } from '@polkadot/react-hooks/types';
import type { RelayName } from './types.js';
import React from 'react';
@ -22,6 +23,7 @@ interface Props {
lastCommittedTimeslice: number
lease: LegacyLease | undefined
highlight?: boolean
relayName: RelayName
}
interface StyledCellProps {
@ -50,16 +52,34 @@ const StyledMarkWarning = styled(MarkWarning)`
const EXPIRES_IN_DAYS = 7;
function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, lease, regionBegin, regionEnd }: Props): React.ReactElement<Props> {
const chainRegionEnd = (chainRecord.renewalStatus === ChainRenewalStatus.Renewed ? regionEnd : regionBegin);
function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, lease, regionBegin, regionEnd, relayName }: Props): React.ReactElement<Props> {
// Group status checks
const { renewalStatus } = chainRecord;
const isRenewed = renewalStatus === ChainRenewalStatus.Renewed;
const isEligible = renewalStatus === ChainRenewalStatus.Eligible;
const chainRegionEnd = isRenewed ? regionEnd : regionBegin;
const renewalLink = isEligible && (
<a
href={`https://app.regionx.tech/renew?network=${relayName}&paraId=${id}&core=${chainRecord?.workload?.core}`}
rel='noopener noreferrer'
target='_blank'
>
Renew on RegionX
</a>
);
const renewalValue = isRenewed
? chainRecord.renewalStatusMessage
: (isEligible ? renewalLink : '-');
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 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 isWithinWeek = !!estimatedTime && new Date(estimatedTime.timestamp).getTime() - Date.now() < EXPIRES_IN_DAYS * 24 * 60 * 60 * 1000;
const isReservation = chainRecord.type === CoreTimeTypes.Reservation;
return (
@ -83,9 +103,9 @@ function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, leas
<StyledCell
$p={highlight}
className='media--800'
>{showEstimates && chainRecord?.lastBlock &&
>{showEstimates && chainRecord?.lastBlock && relayName &&
<a
href={`https://polkadot.subscan.io/block/${chainRecord?.lastBlock}`}
href={`https://${relayName.split(' ')[0]}.subscan.io/block/${chainRecord?.lastBlock}`}
rel='noreferrer'
target='_blank'
>{formatNumber(chainRecord?.lastBlock)}
@ -97,7 +117,7 @@ function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, leas
style={{ whiteSpace: 'nowrap' }}
>
<span>
{estimatedTime}
{estimatedTime && estimatedTime?.formattedDate}
{!!isWithinWeek && !isReservation && chainRecord?.renewalStatus !== ChainRenewalStatus.Renewed && (
<StyledMarkWarning
content='Expires Soon'
@ -108,7 +128,7 @@ function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, leas
<StyledCell
$p={highlight}
className='media--1200'
>{chainRecord?.renewalStatus === ChainRenewalStatus.Renewed ? chainRecord.renewalStatusMessage : chainRecord.renewalStatus}</StyledCell>
>{renewalValue}</StyledCell>
<StyledCell
$p={highlight}
className='media--1200'

View File

@ -1,7 +1,7 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainName, SaleParameters } from '../types.js';
import type { RelayName, SaleParameters } from '../types.js';
import React, { useMemo } from 'react';
@ -80,12 +80,12 @@ const phases = {
const dotLakeUrl = 'https://data.parity.io/coretime';
const SaleDetailsView = ({ chainName, chosenSaleNumber, saleParams }: { saleParams: SaleParameters, chosenSaleNumber: number, chainName: ChainName }) => {
const SaleDetailsView = ({ chosenSaleNumber, relayName, saleParams }: { saleParams: SaleParameters, chosenSaleNumber: number, relayName: RelayName }) => {
const { t } = useTranslation();
const subscanPriceGraphUrl = useMemo(() =>
`https://coretime-${chainName}.subscan.io/coretime_dashboard`
, [chainName]);
`https://coretime-${relayName}.subscan.io/coretime_dashboard`
, [relayName]);
if (chosenSaleNumber === -1 || !saleParams) {
return null;
@ -127,14 +127,14 @@ const SaleDetailsView = ({ chainName, chosenSaleNumber, saleParams }: { salePara
target='_blank'
>Sale Purchase Graph</a>
<SubscanModuleCallUrl
chainName={chainName}
chainName={relayName}
chosenSaleNumber={chosenSaleNumber}
currentRegion={saleParams.currentRegion}
urlTitle='Sale Purchase Transactions'
/>
<SubscanModuleCallUrl
call={'renew'}
chainName={chainName}
chainName={relayName}
chosenSaleNumber={chosenSaleNumber}
currentRegion={saleParams.currentRegion}
urlTitle='Sale Renewal Transactions'
@ -151,7 +151,7 @@ const SaleDetailsView = ({ chainName, chosenSaleNumber, saleParams }: { salePara
{Object.entries(providers).map(([provider, { alt, href, logo }]) => (
<LinkWithLogo
alt={alt}
href={href(chainName)}
href={href(relayName)}
key={provider}
logo={logo}
/>

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { BrokerStatus, ChainConstants, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@polkadot/react-hooks/types';
import type { BrokerStatus, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@polkadot/react-hooks/types';
import React from 'react';
@ -11,7 +11,7 @@ import { BN, formatNumber } from '@polkadot/util';
import { useCoretimeContext } from '../CoretimeContext.js';
import { useTranslation } from '../translate.js';
import { estimateTime, getCurrentRegionStartEndTs } from '../utils/index.js';
import { getCurrentRegionStartEndTs } from '../utils/index.js';
interface Props {
api: ApiPromise | null,
@ -21,15 +21,12 @@ interface Props {
region: RegionInfo[],
status: BrokerStatus,
saleNumber: number,
constants: ChainConstants
}
function Summary ({ config, constants, saleInfo, saleNumber, status }: Props): React.ReactElement<Props> {
function Summary ({ config, saleInfo, saleNumber, status }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { currentRegionEndTs, currentRegionStartTs } = getCurrentRegionStartEndTs(saleInfo, config.regionLength);
const { get } = useCoretimeContext();
const cycleEnd = get && estimateTime(currentRegionEndTs, get.blocks.relay(status?.lastTimeslice), constants.relay);
const cycleStart = get && estimateTime(currentRegionStartTs, get.blocks.relay(status?.lastTimeslice), constants.relay);
const { get, saleEndDate, saleStartDate } = useCoretimeContext();
return (
<SummaryBox>
@ -45,7 +42,7 @@ function Summary ({ config, constants, saleInfo, saleNumber, status }: Props): R
{`${saleInfo?.coresSold} / ${saleInfo?.coresOffered}`}
</CardSummary>
<CardSummary label={t('sale end')}>
<div>{cycleEnd}</div>
<div>{saleEndDate}</div>
</CardSummary>
<CardSummary label={t('last block')}>
<div>{get && formatNumber(get.blocks.relay(currentRegionEndTs))}</div>
@ -70,8 +67,8 @@ function Summary ({ config, constants, saleInfo, saleNumber, status }: Props): R
{status &&
(<CardSummary label={t('current region dates')}>
<div>
<div style={{ fontSize: '14px' }}>{cycleStart}</div>
<div style={{ fontSize: '14px' }}>{cycleEnd}</div>
<div style={{ fontSize: '14px' }}>{saleStartDate}</div>
<div style={{ fontSize: '14px' }}>{saleEndDate}</div>
</div>
</CardSummary>)
}

View File

@ -19,8 +19,7 @@ export const Cores = ({ color, phaseName, salesInfo }: { phaseName: string, sale
const { t } = useTranslation();
const { apiCoretime } = useApi();
const bestNumberFinalized = useCall<BlockNumber>(apiCoretime?.derive.chain.bestNumberFinalized);
const soldOut = useMemo(() => salesInfo.coresOffered === salesInfo.coresSold, [salesInfo.coresOffered, salesInfo.coresSold]);
const coretimePrice = useMemo(() => bestNumberFinalized && getCorePriceAt(bestNumberFinalized.toNumber(), salesInfo), [salesInfo, bestNumberFinalized]);
const coretimePrice = useMemo(() => bestNumberFinalized && salesInfo && getCorePriceAt(bestNumberFinalized.toNumber(), salesInfo), [salesInfo, bestNumberFinalized]);
const CoresWrapper = styled(WhiteBox)`
justify-self: flex-end;
@ -39,10 +38,7 @@ export const Cores = ({ color, phaseName, salesInfo }: { phaseName: string, sale
)
: (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{soldOut && (
<h4>{t('All cores are sold out')}</h4>
)}
{!soldOut && (
{
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<p style={{ fontSize: '14px', marginBottom: '0.15rem', opacity: '0.8' }}>{t('current price')}</p>
@ -53,7 +49,7 @@ export const Cores = ({ color, phaseName, salesInfo }: { phaseName: string, sale
<p style={{ fontSize: '20px' }}> {salesInfo.coresOffered - salesInfo.coresSold}</p>
</div>
</div>
)}
}
</div>
)}

View File

@ -11,7 +11,7 @@ import { CardSummary, ProgressBar, styled, SummaryBox } from '@polkadot/react-co
import { formatBalance, formatNumber } from '@polkadot/util';
import { useTranslation } from '../../translate.js';
import { getSaleProgress } from '../../utils/sale.js';
import { getCorePriceAt, getSaleProgress } from '../../utils/sale.js';
import { WhiteBox } from '../../WhiteBox.js';
const TimelineWrapper = styled(WhiteBox)`
@ -43,6 +43,12 @@ export const Timeline = ({ color, coretimeInfo: { salesInfo, status }, phaseName
salesInfo.regionBegin),
[saleParams, status.lastTimeslice, salesInfo.regionBegin]);
const coretimePriceStart = useMemo(() => salesInfo && getCorePriceAt(salesInfo.saleStart, salesInfo), [salesInfo]);
const endPrice = useMemo(() => salesInfo.endPrice.toNumber() < 10 ** formatBalance.getDefaults().decimals
? `${salesInfo.endPrice.toNumber() / 10 ** formatBalance.getDefaults().decimals} ${formatBalance.getDefaults().unit}`
: formatBalance(salesInfo.endPrice), [salesInfo.endPrice]);
return (
<TimelineWrapper>
<p style={{ fontSize: '16px', fontWeight: 'bold' }}>{t('Sale timeline')}</p>
@ -53,7 +59,8 @@ export const Timeline = ({ color, coretimeInfo: { salesInfo, status }, phaseName
<CardSummary label='current phase end'>{saleParams?.phaseConfig?.config[phaseName as keyof typeof saleParams.phaseConfig.config].end.date}</CardSummary>
<CardSummary label='last phase block'>{formatNumber(saleParams?.phaseConfig?.config[phaseName as keyof typeof saleParams.phaseConfig.config].end.blocks.relay)}</CardSummary>
</>}
<CardSummary label='fixed price'>{formatBalance(salesInfo.endPrice)}</CardSummary>
<CardSummary label='start price'>{formatBalance(coretimePriceStart)}</CardSummary>
<CardSummary label='fixed price'>{endPrice}</CardSummary>
</section>
<section>

View File

@ -1,7 +1,7 @@
// Copyright 2017-2025 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ChainName, SaleParameters } from '../types.js';
import type { RelayName, SaleParameters } from '../types.js';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@ -19,7 +19,7 @@ import SaleDetailsView from './SaleDetailsView.js';
import Summary from './Summary.js';
interface Props {
chainName: ChainName
relayName: RelayName
}
const ResponsiveGrid = styled.div`
@ -46,7 +46,7 @@ const ResponsiveGrid = styled.div`
}
`;
function Sale ({ chainName }: Props): React.ReactElement<Props> {
function Sale ({ relayName }: Props): React.ReactElement<Props> {
const { coretimeInfo } = useCoretimeContext();
const { api, apiEndpoint, isApiReady } = useApi();
const { t } = useTranslation();
@ -71,11 +71,11 @@ function Sale ({ chainName }: Props): React.ReactElement<Props> {
if (coretimeInfo && !saleParams) {
setSaleParams(getSaleParameters(
coretimeInfo,
chainName,
relayName,
lastCommittedTimeslice ?? 0
));
}
}, [coretimeInfo, saleParams, lastCommittedTimeslice, chainName]);
}, [coretimeInfo, saleParams, lastCommittedTimeslice, relayName]);
const phaseName = useMemo(() => saleParams?.phaseConfig?.currentPhaseName, [saleParams]);
@ -87,9 +87,9 @@ function Sale ({ chainName }: Props): React.ReactElement<Props> {
return;
}
setSelectedSaleParams(getSaleParameters(coretimeInfo, chainName, lastCommittedTimeslice ?? 0, value));
setSelectedSaleParams(getSaleParameters(coretimeInfo, relayName, lastCommittedTimeslice ?? 0, value));
}
}, [coretimeInfo, chainName, lastCommittedTimeslice]);
}, [coretimeInfo, relayName, lastCommittedTimeslice]);
return (
<div>
@ -97,7 +97,6 @@ function Sale ({ chainName }: Props): React.ReactElement<Props> {
<Summary
api={isApiReady ? api : null}
config={coretimeInfo?.config}
constants={coretimeInfo?.constants}
region={coretimeInfo?.region}
saleInfo={coretimeInfo?.salesInfo}
saleNumber={saleParams?.saleNumber}
@ -132,8 +131,8 @@ function Sale ({ chainName }: Props): React.ReactElement<Props> {
</div>
{saleParams && selectedSaleParams &&
<SaleDetailsView
chainName={chainName}
chosenSaleNumber={chosenSaleNumber}
relayName={relayName}
saleParams={selectedSaleParams}
/>}
</div>

View File

@ -83,7 +83,7 @@ export interface RegionInfo {
regionEnd: number;
}
export type ChainName = 'kusama' | 'polkadot' | 'paseo testnet' | 'westend'
export type RelayName = 'kusama' | 'polkadot' | 'paseo testnet' | 'westend'
export interface GetResponse {
blocks: {

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ChainBlockConstants, ChainConstants, CoretimeInformation } from '@polkadot/react-hooks/types';
import type { ChainName, GetResponse, RegionInfo } from '../types.js';
import type { GetResponse, RegionInfo, RelayName } from '../types.js';
import { CoreTimeTypes } from '@polkadot/react-hooks/constants';
import { BN } from '@polkadot/util';
@ -11,7 +11,7 @@ type FirstCycleStartType = Record<
'block' | 'timeslice',
Record<
'coretime',
Record<ChainName, number>
Record<RelayName, number>
>
>;
@ -41,11 +41,18 @@ export const coretimeTypeColours: Record<string, string> = {
[CoreTimeTypes['Bulk Coretime']]: 'pink'
};
export function formatDate (date: Date) {
export function formatDate (date: Date, time = false) {
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'short' });
const year = date.getFullYear();
if (time) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${day} ${month} ${year}, ${hours}:${minutes}`;
}
return `${day} ${month} ${year}`;
}
@ -71,7 +78,7 @@ export const estimateTime = (
targetTimeslice: string | number,
latestBlock: number,
{ blocksPerTimeslice: blocksPerTs, blocktimeMs }: ChainBlockConstants
): string | null => {
): { timestamp: number, formattedDate: string } | null => {
if (!latestBlock || !targetTimeslice) {
console.error('Invalid input: one or more inputs are missing');
@ -88,7 +95,10 @@ export const estimateTime = (
const timeDifference = blockDifference.mul(blockTime);
const estTimestamp = now.add(timeDifference);
return formatDate(new Date(estTimestamp.toNumber()));
return {
formattedDate: formatDate(new Date(estTimestamp.toNumber())),
timestamp: estTimestamp.toNumber()
};
} catch (error) {
console.error('Error in calculation:', error);

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ChainConstants, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord } from '@polkadot/react-hooks/types';
import type { ChainName, GetResponse, PhaseConfig, RegionInfo, SaleParameters } from '../types.js';
import type { GetResponse, PhaseConfig, RegionInfo, RelayName, SaleParameters } from '../types.js';
import { type ProgressBarSection } from '@polkadot/react-components/types';
import { BN } from '@polkadot/util';
@ -69,14 +69,14 @@ export const getCorePriceAt = (blockNow: number | null, saleInfo: PalletBrokerSa
*/
export const getCurrentSaleNumber = (
currentRegionEnd: number,
chainName: ChainName,
relayName: RelayName,
config: Pick<PalletBrokerConfigRecord, 'interludeLength' | 'leadinLength' | 'regionLength'>
): number => {
if (!chainName || !currentRegionEnd) {
if (!relayName || !currentRegionEnd) {
return -1;
}
return Math.ceil((currentRegionEnd - FirstCycleStart.timeslice.coretime[chainName]) / config.regionLength);
return Math.ceil((currentRegionEnd - FirstCycleStart.timeslice.coretime[relayName]) / config.regionLength);
};
export const determinePhaseName = (
@ -164,11 +164,11 @@ const getPhaseConfiguration = (
const fixedPriceLengthTs = regionLength - interludeLengthTs - leadInLengthTs;
const fixedPriceEndTs = priceDiscoveryEndTs + fixedPriceLengthTs;
const get = createGet(constants);
const getDate = (ts: number) => estimateTime(ts, get.blocks.relay(lastCommittedTimeslice) ?? 0, constants.relay);
const getDate = (ts: number) => estimateTime(ts, get.blocks.relay(lastCommittedTimeslice), constants.relay)?.formattedDate ?? null;
return {
config: {
[PhaseName.FixedPrice]: makeConfig(fixedPriceEndTs, fixedPriceEndTs, get, getDate, PhaseName.FixedPrice),
[PhaseName.FixedPrice]: makeConfig(priceDiscoveryEndTs, fixedPriceEndTs, get, getDate, PhaseName.FixedPrice),
[PhaseName.PriceDiscovery]: makeConfig(renewalsEndTs, priceDiscoveryEndTs, get, getDate, PhaseName.PriceDiscovery),
[PhaseName.Renewals]: makeConfig(currentRegionStart, renewalsEndTs, get, getDate, PhaseName.Renewals)
},
@ -178,7 +178,7 @@ const getPhaseConfiguration = (
export const getSaleParameters = (
{ config, constants, salesInfo }: {salesInfo: RegionInfo, config: Pick<PalletBrokerConfigRecord, 'interludeLength' | 'leadinLength' | 'regionLength'>, constants: ChainConstants},
chainName: ChainName,
relayName: RelayName,
lastCommittedTimeslice: number,
chosenSaleNumber = -1
): SaleParameters => {
@ -186,30 +186,30 @@ export const getSaleParameters = (
const interludeLengthTs = get.timeslices.coretime(config.interludeLength);
const leadInLengthTs = get.timeslices.coretime(config.leadinLength);
let { currentRegionEndTs, currentRegionStartTs } = getCurrentRegionStartEndTs(salesInfo, config.regionLength);
const getDate = (ts: number) => estimateTime(ts, get.blocks.relay(lastCommittedTimeslice), constants.relay) || '';
const saleNumber = getCurrentSaleNumber(currentRegionEndTs, chainName, config);
const getDate = (ts: number) => estimateTime(ts, get.blocks.relay(lastCommittedTimeslice), constants.relay)?.formattedDate ?? null;
const saleNumber = getCurrentSaleNumber(currentRegionEndTs, relayName, config);
let currentRegionInfo: SaleParameters['currentRegion'];
if (chosenSaleNumber !== -1) {
// A hack for Kusama as one of the sales had an unusual length
// checked against Subscan historical sales
if (chainName === 'kusama') {
if (relayName === 'kusama') {
const irregularRegionLength = 848;
if (chosenSaleNumber === 0) {
currentRegionStartTs = FirstCycleStart.timeslice.coretime[chainName];
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName];
currentRegionEndTs = currentRegionStartTs + config.regionLength;
} else if (chosenSaleNumber === 1) {
currentRegionStartTs = FirstCycleStart.timeslice.coretime[chainName] + config.regionLength;
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName] + config.regionLength;
// that particular sale #2 was only 848 blocks long
currentRegionEndTs = currentRegionStartTs + irregularRegionLength;
} else {
currentRegionStartTs = FirstCycleStart.timeslice.coretime[chainName] + config.regionLength * (chosenSaleNumber - 1) + irregularRegionLength;
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName] + config.regionLength * (chosenSaleNumber - 1) + irregularRegionLength;
currentRegionEndTs = currentRegionStartTs + config.regionLength;
}
} else {
currentRegionStartTs = FirstCycleStart.timeslice.coretime[chainName] + config.regionLength * chosenSaleNumber;
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName] + config.regionLength * chosenSaleNumber;
currentRegionEndTs = currentRegionStartTs + config.regionLength;
}
@ -220,7 +220,7 @@ export const getSaleParameters = (
coretime: 0,
relay: get.blocks.relay(currentRegionEndTs)
},
date: getDate(currentRegionEndTs),
date: getDate(currentRegionEndTs) ?? '',
ts: currentRegionEndTs
},
start: {
@ -229,7 +229,7 @@ export const getSaleParameters = (
coretime: 0,
relay: get.blocks.relay(currentRegionStartTs)
},
date: getDate(currentRegionStartTs),
date: getDate(currentRegionStartTs) ?? '',
ts: currentRegionStartTs
}
};
@ -267,7 +267,7 @@ export const getSaleParameters = (
coretime: 0,
relay: currentRegionInfo.end.blocks.relay + get.blocks.relay(config.regionLength)
},
date: estimateTime(currentRegionInfo.end.ts + config.regionLength, get.blocks.relay(lastCommittedTimeslice), constants.relay),
date: estimateTime(currentRegionInfo.end.ts + config.regionLength, get.blocks.relay(lastCommittedTimeslice), constants.relay)?.formattedDate ?? null,
ts: currentRegionInfo.end.ts + config.regionLength
},
start: {

View File

@ -35,6 +35,7 @@ export { useCallMulti } from './useCallMulti.js';
export { useCollectiveInstance } from './useCollectiveInstance.js';
export { useCollectiveMembers } from './useCollectiveMembers.js';
export { useCoreDescriptor } from './useCoreDescriptor.js';
export { useCoretimeConsts } from './useCoretimeConsts.js';
export { useCoretimeEndpoint } from './useCoretimeEndpoint.js';
export { useCoretimeInformation } from './useCoretimeInformation.js';
export { useDebounce } from './useDebounce.js';