Relay Chain - Coretime Overview UI Tab (#11016)

* exposing coretime wss endpoint to use on relay chain, adjusted hooks to accept api endpoint as a parameter

* adjusting hook calls to new parameters

* added Tag for coretime types, renamed occupancyType to coretimeType

* added cycle progress and cycle end date to summary, fixed the estimation dates

* coretime page draft, added hook for gathering all the coretime information in order to show it on relay chain

* lint

* bumped version

* updated coretime chain summary

* updated summary, added timeslice for the first sale start,

* hook updates to make sure the coretime data is displayed even if workloads are empty, fallback on coreAssignments

* removed unused files

* lint

* tsc

* merging parachainIds with renewalIds

* if there is no record in potential renewals, then the chain has renewed already

* added config for page-coretime

* removed hardcoded regionLength value

* improved function to find out type

* lint

* tsc

* Fix versioning

* fix comments

---------

Co-authored-by: tarikgul <tariksnow37@gmail.com>
This commit is contained in:
Daria 2024-10-22 21:38:00 +08:00 committed by GitHub
parent 9134c0e32b
commit 4df5c9a8d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 967 additions and 219 deletions

View File

@ -351,7 +351,7 @@ export const testParasPaseoCommon: EndpointOption[] = [
}
},
{
info: 'BridgeHub',
info: 'PaseoBridgeHub',
isPeopleForIdentity: true,
paraId: 1002,
providers: {
@ -367,7 +367,7 @@ export const testParasPaseoCommon: EndpointOption[] = [
}
},
{
info: 'Coretime',
info: 'PaseoCoretime',
isPeopleForIdentity: true,
paraId: 1005,
providers: {

View File

@ -0,0 +1,22 @@
// Copyright 2017-2024 @polkadot/apps-routing authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Route, TFunction } from './types.js';
import Component from '@polkadot/app-coretime';
export default function create (t: TFunction): Route {
return {
Component,
display: {
needsApi: [
'query.coretimeAssignmentProvider.coreDescriptors'
],
needsApiInstances: true
},
group: 'network',
icon: 'flask',
name: 'coretime',
text: t('nav.coretime', 'Coretime (Experimental)', { ns: 'apps-routing' })
};
}

View File

@ -14,6 +14,7 @@ import calendar from './calendar.js';
import claims from './claims.js';
import collator from './collator.js';
import contracts from './contracts.js';
import coretime from './coretime.js';
import council from './council.js';
import democracy from './democracy.js';
import explorer from './explorer.js';
@ -62,7 +63,9 @@ export default function create (t: TFunction): Routes {
// Legacy staking Pre v14 pallet version.
stakingLegacy(t),
collator(t),
// Coretime
broker(t),
coretime(t),
// governance v2
referenda(t),
membership(t),

View File

@ -11,12 +11,13 @@
{ "path": "../page-alliance/tsconfig.build.json" },
{ "path": "../page-ambassador/tsconfig.build.json" },
{ "path": "../page-assets/tsconfig.build.json" },
{ "path": "../page-broker/tsconfig.build.json" },
{ "path": "../page-bounties/tsconfig.build.json" },
{ "path": "../page-calendar/tsconfig.build.json" },
{ "path": "../page-claims/tsconfig.build.json" },
{ "path": "../page-broker/tsconfig.build.json" },
{ "path": "../page-collator/tsconfig.build.json" },
{ "path": "../page-contracts/tsconfig.build.json" },
{ "path": "../page-coretime/tsconfig.build.json" },
{ "path": "../page-council/tsconfig.build.json" },
{ "path": "../page-democracy/tsconfig.build.json" },
{ "path": "../page-explorer/tsconfig.build.json" },

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { PalletBrokerConfigRecord } from '@polkadot/react-hooks/types';
import React, { useRef } from 'react';
@ -14,11 +15,12 @@ import Workload from './Workload.js';
interface Props {
api: ApiPromise;
core: number;
config: PalletBrokerConfigRecord,
workload?: CoreWorkloadType[],
workplan?: CoreWorkplanType[],
}
function CoreTable ({ api, core, workload, workplan }: Props): React.ReactElement<Props> {
function CoreTable ({ api, config, core, workload, workplan }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?] | false)[]>([[t('core')]]);
const header: [React.ReactNode?, string?, number?, (() => void)?][] = [
@ -39,6 +41,7 @@ function CoreTable ({ api, core, workload, workplan }: Props): React.ReactElemen
>
<Workload
api={api}
config={config}
core={core}
key={core}
workload={workload}

View File

@ -6,20 +6,26 @@ import type { CoreInfo } from '../types.js';
import React from 'react';
import { useBrokerConfig } from '@polkadot/react-hooks';
import CoreTable from './CoreTable.js';
interface Props {
api: ApiPromise;
isApiReady: boolean;
data: CoreInfo[];
}
function CoresTable ({ api, data }: Props): React.ReactElement<Props> {
function CoresTable ({ api, data, isApiReady }: Props): React.ReactElement<Props> {
const config = useBrokerConfig(api, isApiReady);
return (
<>
{data?.map((coreData) => {
{config && data?.map((coreData) => {
return (
<CoreTable
api={api}
config={config}
core={coreData?.core}
key={coreData?.core}
workload={coreData?.workload}

View File

@ -4,16 +4,16 @@
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import type { statsType } from '../types.js';
import React from 'react';
import React, { useMemo } from 'react';
import { CardSummary, styled, SummaryBox, UsageBar } from '@polkadot/react-components';
import { defaultHighlight } from '@polkadot/react-components/styles';
import { useApi, useBrokerSalesInfo, useBrokerStatus } from '@polkadot/react-hooks';
import { useApi, useBrokerConfig, useBrokerSalesInfo, useBrokerStatus } from '@polkadot/react-hooks';
import { type CoreWorkload } from '@polkadot/react-hooks/types';
import { formatBalance } from '@polkadot/util';
import { BN, BN_ZERO } from '@polkadot/util';
import { useTranslation } from '../translate.js';
import { getStats } from '../utils.js';
import { estimateTime, getStats } from '../utils.js';
import RegionLength from './Summary/RegionLength.js';
import Timeslice from './Summary/Timeslice.js';
import TimeslicePeriod from './Summary/TimeslicePeriod.js';
@ -41,12 +41,15 @@ interface Props {
function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
const { t } = useTranslation();
const { api, apiEndpoint } = useApi();
const totalCores = useBrokerStatus('coreCount');
const { api, apiEndpoint, isApiReady } = useApi();
const uiHighlight = apiEndpoint?.ui.color || defaultHighlight;
const { idles, pools, tasks }: statsType = React.useMemo(() => getStats(totalCores, workloadInfos), [totalCores, workloadInfos]);
const { idles, pools, tasks }: statsType = React.useMemo(() => getStats(coreCount, workloadInfos), [coreCount, workloadInfos]);
const salesInfo = useBrokerSalesInfo();
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]);
return (
<SummaryBox>
@ -63,24 +66,31 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
<CardSummary label={t('region (ts)')}>
<RegionLength />
</CardSummary>
<CardSummary label={t('estimated bulk price')}>
<div className='ui--balance-value'>
{formatBalance(salesInfo?.endPrice) || '-'}
</div>
</CardSummary>
<CardSummary label={t('total cores')}>
{coreCount}
</CardSummary>
<CardSummary label={t('cores sold/offered')}>
<div>
{salesInfo?.coresSold} / {salesInfo?.coresOffered}
{saleInfo?.coresSold} / {saleInfo?.coresOffered}
</div>
</CardSummary>
<CardSummary
label={t('cycle progress')}
progress={{
isBlurred: false,
total: new BN(config?.regionLength || 0),
value: (config?.regionLength && currentRegionEnd && status && new BN(config?.regionLength - (currentRegionEnd - status?.lastTimeslice))) || BN_ZERO,
withTime: false
}}
/>
</StyledDiv>
</>
)}
<div style={{ marginLeft: '2rem' }}>
<div
className='media--1400'
style={{ marginLeft: '2rem' }}
>
<UsageBar
data={[
{ color: '#FFFFFF', label: 'Idle', value: idles },
@ -91,6 +101,32 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement {
</div>
</StyledSection>
<section>
{status && currentRegionStart && currentRegionEnd &&
(
<>
<CardSummary
className='media--1200'
label={t('cycle dates')}
>
<div>
<div style={{ fontSize: '14px' }}>{estimateTime(currentRegionStart, status?.lastTimeslice * 80)}</div>
<div style={{ fontSize: '14px' }}>{estimateTime(currentRegionEnd, status?.lastTimeslice * 80)}</div>
</div>
</CardSummary>
<CardSummary
className='media--1200'
label={t('cycle ts')}
>
<div>
<div style={{ fontSize: '14px' }}>{currentRegionStart}</div>
<div style={{ fontSize: '14px' }}>{currentRegionEnd}</div>
</div>
</CardSummary>
</>
)
}
</section>
</SummaryBox>
);
}

View File

@ -1,24 +0,0 @@
// Copyright 2017-2024 @polkadot/app-broker authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { BrokerStatus } from '@polkadot/react-query';
interface Props {
children?: React.ReactNode;
className?: string;
}
function Cores ({ children, className }: Props): React.ReactElement<Props> | null {
return (
<BrokerStatus
className={className}
query='coreCount'
>
{children}
</BrokerStatus>
);
}
export default React.memo(Cores);

View File

@ -1,26 +0,0 @@
// Copyright 2017-2024 @polkadot/app-broker authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { useBrokerSalesInfo } from '@polkadot/react-hooks';
import { formatBalance } from '@polkadot/util';
interface Props {
className?: string;
children?: React.ReactNode;
renewalBump?: string;
currentPrice?: string;
}
function RenewalPrice (): React.ReactElement<Props> | null {
const salesInfo = useBrokerSalesInfo();
return (
<div className='ui--balance-value'>
{formatBalance(salesInfo?.endPrice) || '-'}
</div>
);
}
export default React.memo(RenewalPrice);

View File

@ -3,7 +3,7 @@
import React from 'react';
import { BrokerStatus } from '@polkadot/react-query';
import { useApi, useBrokerStatus } from '@polkadot/react-hooks';
interface Props {
children?: React.ReactNode;
@ -11,14 +11,14 @@ interface Props {
}
function Timeslice ({ children, className }: Props): React.ReactElement<Props> | null {
const { api, isApiReady } = useApi();
const info = useBrokerStatus(api, isApiReady);
return (
<BrokerStatus
className={className}
query='lastTimeslice'
>
<div className={className}>
{info?.lastTimeslice || '-'}
{children}
</BrokerStatus>
);
</div>);
}
export default React.memo(Timeslice);

View File

@ -1,11 +1,20 @@
// Copyright 2017-2024 @polkadot/app-broker authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { FlagColor } from '@polkadot/react-components/types';
import React from 'react';
import { AddressMini, styled } from '@polkadot/react-components';
import { AddressMini, styled, Tag } from '@polkadot/react-components';
import { type InfoRow, Occupancy } from '../types.js';
import { CoreTimeTypes, type InfoRow } from '../types.js';
const colours: Record<string, string> = {
[CoreTimeTypes.Reservation]: 'orange',
[CoreTimeTypes.Lease]: 'blue',
[CoreTimeTypes['Bulk Coretime']]: 'pink',
[CoreTimeTypes['On Demand']]: 'green'
};
const StyledTableCol = styled.td<{ hide?: 'mobile' | 'tablet' | 'both' }>`
width: 150px;
@ -45,7 +54,7 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
}
switch (data.type) {
case (Occupancy.Reservation): {
case (CoreTimeTypes.Reservation): {
return (
<>
<TableCol
@ -56,16 +65,18 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
header='Blocks per timeslice'
value={data.maskBits}
/>
<TableCol
header='type'
value={'Reservation'}
/>
<td>
<Tag
color={colours[data.type] as FlagColor}
label={Object.values(CoreTimeTypes)[data.type]}
/>
</td>
<td colSpan={4} />
</>
);
}
case (Occupancy.Lease): {
case (CoreTimeTypes.Lease):
return (
<>
<TableCol
@ -90,10 +101,48 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
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='type'
value={'Legacy Lease'}
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} />
</>);
}
@ -123,23 +172,24 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement {
header='Last block'
value={data.endBlock}
/>
<TableCol
header='type'
value={'Bulk Coretime'}
/>
<StyledTableCol hide='mobile'>
<h5 style={{ opacity: '0.6' }}>{'Owner'}</h5>
{data.owner
? (
<AddressMini
isPadded={false}
key={data.owner}
value={data.owner}
/>
)
: <p>&nbsp;</p>}
<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>}
</>);
}
}

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { RegionInfo } from '@polkadot/react-hooks/types';
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';
@ -21,11 +21,12 @@ interface Props {
core: number;
workload: CoreWorkloadType[] | undefined
workplan?: CoreWorkplanType[] | undefined
config: PalletBrokerConfigRecord
}
function Workload ({ api, core, workload, workplan }: Props): React.ReactElement<Props> {
function Workload ({ api, config, core, workload, workplan }: Props): React.ReactElement<Props> {
const { isApiReady } = useApi();
const salesInfo = useBrokerSalesInfo();
const salesInfo = useBrokerSalesInfo(api, isApiReady);
const status = useCall<Option<PalletBrokerStatusRecord>>(isApiReady && api.query.broker?.status);
const [isExpanded, toggleIsExpanded] = useToggle(false);
@ -45,18 +46,19 @@ function Workload ({ api, core, workload, workplan }: Props): React.ReactElement
useEffect(() => {
if (!!workload?.length && !!salesInfo) {
setWorkloadData(formatRowInfo(workload, core, region, currentTimeSlice, salesInfo));
// 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, currentTimeSlice, { regionBegin: salesInfo.regionBegin - config.regionLength, regionEnd: salesInfo.regionEnd - config.regionLength }, config.regionLength));
} else {
return setWorkloadData([{ core }]);
}
}, [workload, region, currentTimeSlice, core, salesInfo]);
}, [workload, region, currentTimeSlice, core, salesInfo, config]);
useEffect(() => {
if (!!workplan?.length && !!salesInfo) {
setWorkplanData(formatRowInfo(workplan, core, region, currentTimeSlice, salesInfo));
setWorkplanData(formatRowInfo(workplan, core, region, currentTimeSlice, salesInfo, config.regionLength));
}
}
, [workplan, region, currentTimeSlice, core, salesInfo]);
, [workplan, region, currentTimeSlice, core, salesInfo, config]);
const hasWorkplan = workplan?.length;

View File

@ -25,7 +25,7 @@ const formatDataObject = (one: CoreWorkplan | CoreWorkload, leaseMap: LeaseMapTy
lastBlock: leaseMap[one?.info.task as number]?.until || 0,
maskBits: one.info.maskBits,
task: one.info.task,
type: getOccupancyType(leaseMap[one.info.task as number], reservationMap[one.info.task as number])
type: getOccupancyType(leaseMap[one.info.task as number], reservationMap[one.info.task as number], one.info.isPool)
});
const formatData = (coreCount: number, workplan: CoreWorkplan[], workload: CoreWorkload[], leaseMap: LeaseMapType, reservationMap: ReservationMapType): CoreInfo[] => {
@ -56,7 +56,7 @@ function Overview ({ className }: Props): React.ReactElement<Props> {
const [data, setData] = useState<CoreInfo[]>([]);
const [filtered, setFiltered] = useState<CoreInfo[]>();
const coreCount = useBrokerStatus('coreCount') || '-';
const status = useBrokerStatus(api, isApiReady);
const workloadInfos: CoreWorkload[] | undefined = useWorkloadInfos(api, isApiReady);
const workplanInfos: CoreWorkplan[] | undefined = useWorkplanInfos(api, isApiReady);
@ -67,15 +67,15 @@ function Overview ({ className }: Props): React.ReactElement<Props> {
const reservationMap: ReservationMapType = useMemo(() => reservations ? createTaskMap(reservations) : [], [reservations]);
useEffect(() => {
!!workplanInfos && !!workloadInfos && !!coreCount &&
setData(formatData(Number(coreCount), workplanInfos, workloadInfos, leaseMap, reservationMap));
}, [workplanInfos, workloadInfos, leaseMap, reservationMap, coreCount]);
!!workplanInfos && !!workloadInfos && !!status?.coreCount &&
setData(formatData(Number(status?.coreCount), workplanInfos, workloadInfos, leaseMap, reservationMap));
}, [workplanInfos, workloadInfos, leaseMap, reservationMap, status]);
return (
<div className={className}>
<Summary
apiEndpoint={apiEndpoint}
coreCount={coreCount}
coreCount={status?.coreCount.toString() || '-'}
workloadInfos={workloadInfos}
></Summary>
{!!data?.length &&
@ -88,6 +88,7 @@ function Overview ({ className }: Props): React.ReactElement<Props> {
<CoresTable
api={api}
data={filtered}
isApiReady={isApiReady}
/>
)}
</>)

View File

@ -13,7 +13,7 @@ export interface InfoRow {
owner?: string
leaseLength?: number
endBlock?: number
type?: Occupancy
type?: CoreTimeTypes
}
export interface CoreInfo {
@ -28,18 +28,19 @@ export interface statsType {
tasks: number
}
export enum Occupancy {
export enum CoreTimeTypes {
'Reservation',
'Lease',
'Bulk Coretime'
'Bulk Coretime',
'On Demand'
}
export interface CoreWorkplanType extends CoreWorkplan {
lastBlock: number,
type: Occupancy
type: CoreTimeTypes
}
export interface CoreWorkloadType extends CoreWorkload {
lastBlock: number,
type: Occupancy
type: CoreTimeTypes
}

View File

@ -6,12 +6,11 @@ import type { CoreWorkloadType, CoreWorkplanType, InfoRow } from './types.js';
import { BN } from '@polkadot/util';
import { Occupancy } from './types.js';
import { CoreTimeTypes } from './types.js';
export const CoreTimeConsts = {
BlockTime: 6000,
BlocksPerTimeslice: 80,
DefaultRegion: 5040
BlocksPerTimeslice: 80
};
function formatDate (date: Date) {
@ -23,18 +22,18 @@ function formatDate (date: Date) {
}
/**
* blockTime = 6000 ms
* BlocksPerTimeslice = 80
* Default Regoin = 5040 timeslices
* TargetBlock = TargetTimeslice * BlocksPerTimeslice
* Block Time Difference = |TargetBlock - latest Block| * blockTime
*
* Estimate timestamp =
* if targetBlock is before the latestBlock
* now minus block time difference
* else
* now plus block time difference
*/
* blockTime = 6000 ms
* BlocksPerTimeslice = 80
* Default Regoin = 5040 timeslices
* TargetBlock = TargetTimeslice * BlocksPerTimeslice
* Block Time Difference = |TargetBlock - latest Block| * blockTime
*
* Estimate timestamp =
* if targetBlock is before the latestBlock
* now minus block time difference
* else
* now plus block time difference
*/
export const estimateTime = (targetTimeslice: string | number, latestBlock: number): string | null => {
if (!latestBlock || !targetTimeslice) {
console.error('Invalid input: one or more inputs are missing');
@ -61,15 +60,24 @@ export const estimateTime = (targetTimeslice: string | number, latestBlock: numb
}
};
export function formatRowInfo (data: CoreWorkloadType[] | CoreWorkplanType[], core: number, currentRegion: RegionInfo | undefined, timeslice: number, { regionBegin, regionEnd }: { regionBegin: number, regionEnd: number }, regionLength = CoreTimeConsts.DefaultRegion): InfoRow[] {
/**
*
* @param data: CoreWorkloadType[]
* @param core: core number
* @param currentRegion
* @param timeslice
* @param param4
* @param regionLength
*/
export function formatRowInfo (data: CoreWorkloadType[] | CoreWorkplanType[], core: number, currentRegion: RegionInfo | undefined, currentTimeSlice: number, { regionBegin, regionEnd }: { regionBegin: number, regionEnd: number }, regionLength: number): InfoRow[] {
return data.map((one: CoreWorkloadType | CoreWorkplanType) => {
const item: InfoRow = { core, maskBits: one?.info?.maskBits, task: one?.info?.task, type: one?.type };
const blockNumber = timeslice * 80;
const blockNumberNow = currentTimeSlice * 80;
item.type = one.type;
let end; let start = null;
if (one.type === Occupancy.Lease) {
if (one.type === CoreTimeTypes.Lease) {
const period = Math.floor(one.lastBlock / regionLength);
end = period * regionLength;
@ -79,12 +87,12 @@ export function formatRowInfo (data: CoreWorkloadType[] | CoreWorkplanType[], co
}
item.owner = currentRegion?.owner.toString();
item.start = start ? estimateTime(start, blockNumber) : null;
item.end = end ? estimateTime(end, blockNumber) : null;
item.start = start ? estimateTime(start, blockNumberNow) : null;
item.end = end ? estimateTime(end, blockNumberNow) : null;
item.endBlock = end ? Number(end) * 80 : 0;
if ('timeslice' in one && !start) {
start = estimateTime(one.timeslice, blockNumber) ?? null;
start = estimateTime(one.timeslice, blockNumberNow) ?? null;
}
return item;
@ -121,6 +129,10 @@ export const createTaskMap = <T extends { task: string }>(items: T[]): Record<nu
}, {} as Record<number, T>);
};
export const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined): Occupancy => {
return reservation ? Occupancy.Reservation : lease ? Occupancy.Lease : Occupancy['Bulk Coretime'];
export const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined, isPool: boolean): CoreTimeTypes => {
if (isPool) {
return CoreTimeTypes['On Demand'];
}
return reservation ? CoreTimeTypes.Reservation : lease ? CoreTimeTypes.Lease : CoreTimeTypes['Bulk Coretime'];
};

View File

View File

View File

@ -0,0 +1 @@
# @polkadot/app-coretime

View File

@ -0,0 +1,27 @@
{
"bugs": "https://github.com/polkadot-js/apps/issues",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/polkadot-js/apps/tree/master/packages/page-coretime#readme",
"license": "Apache-2.0",
"name": "@polkadot/app-coretime",
"private": true,
"repository": {
"directory": "packages/page-coretime",
"type": "git",
"url": "https://github.com/polkadot-js/apps.git"
},
"sideEffects": false,
"type": "module",
"version": "0.145.2-1-x",
"dependencies": {
"@polkadot/react-components": "^0.145.2-1-x",
"@polkadot/react-query": "^0.145.2-1-x"
},
"peerDependencies": {
"react": "*",
"react-dom": "*",
"react-is": "*"
}
}

View File

@ -0,0 +1,90 @@
// Copyright 2017-2024 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { BrokerStatus, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@polkadot/react-hooks/types';
import React, { useMemo } from 'react';
import { CardSummary, SummaryBox } from '@polkadot/react-components';
import { useCall } from '@polkadot/react-hooks';
import { BN } from '@polkadot/util';
import { useTranslation } from '../translate.js';
import { estimateTime, FirstCycleStart } from '../utils.js';
interface Props {
api: ApiPromise | null,
coreDscriptors?: CoreDescription[];
saleInfo: PalletBrokerSaleInfoRecord
config: PalletBrokerConfigRecord,
region: RegionInfo[],
status: BrokerStatus,
parachainCount: number
}
function Summary ({ api, config, parachainCount, saleInfo, status }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const currentRegionEnd = saleInfo.regionEnd - config.regionLength;
const currentRegionStart = saleInfo.regionEnd - config.regionLength * 2;
const chainName = useCall<string>(api?.rpc.system.chain)?.toString().toLowerCase();
const cycleNumber = useMemo(() =>
chainName && currentRegionEnd && Math.floor((currentRegionEnd - FirstCycleStart[chainName]) / config.regionLength)
, [currentRegionEnd, chainName, config]);
return (
<SummaryBox>
<section>
<CardSummary label={t('timeslice')}>
{status?.lastTimeslice}
</CardSummary>
<CardSummary label={t('cores sold')}>
{`${saleInfo?.coresSold} / ${saleInfo?.coresOffered}`}
</CardSummary>
<CardSummary label={t('parachains')}>
{parachainCount && parachainCount}
</CardSummary>
{status &&
<CardSummary label={t('cycle number')}>
<div>
{cycleNumber}
</div>
</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
}}
/>
}
</section>
<section className='media--1200'>
{status &&
(<CardSummary label={t('cycle dates')}>
<div>
<div style={{ fontSize: '14px' }}>{estimateTime(currentRegionStart, status?.lastTimeslice * 80)}</div>
<div style={{ fontSize: '14px' }}>{estimateTime(currentRegionEnd, status?.lastTimeslice * 80)}</div>
</div>
</CardSummary>)
}
{status &&
<CardSummary label={t('cycle ts')}>
<div>
<div style={{ fontSize: '14px' }}>{currentRegionStart}</div>
<div style={{ fontSize: '14px' }}>{currentRegionEnd}</div>
</div>
</CardSummary>
}
</section>
</SummaryBox>
);
}
export default React.memo(Summary);

View File

@ -0,0 +1,79 @@
// Copyright 2017-2024 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { FlagColor } from '@polkadot/react-components/types';
import type { CoretimeInformation } from '@polkadot/react-hooks/types';
import React, { useRef } from 'react';
import { ParaLink, Table, Tag } from '@polkadot/react-components';
import { BN, formatBalance, formatNumber } from '@polkadot/util';
import { useTranslation } from './translate.js';
import { CoreTimeTypes } from './types.js';
import { estimateTime, getOccupancyType } from './utils.js';
interface Props {
coretimeInfo: CoretimeInformation
}
const colours: Record<string, string> = {
[CoreTimeTypes.Reservation]: 'orange',
[CoreTimeTypes.Lease]: 'blue',
[CoreTimeTypes['Bulk Coretime']]: 'pink'
};
function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('parachains'), 'start'],
[t('name'), 'start'],
[t('core number'), 'start'],
[t('type'), 'start'],
[t('last block'), 'start'],
[t('end'), 'start'],
[t('renewal'), 'start'],
[t('renewal price'), 'start']
]);
return (
<Table
emptySpinner={false}
header={headerRef.current}
isSplit={false}
>
{coretimeInfo?.taskIds?.map((taskId: number) => {
const chain = coretimeInfo.chainInfo[taskId];
const type = getOccupancyType(chain?.lease, chain?.reservation, !!chain.worklplan?.find((a) => a.info.isPool));
const targetTimeslice = chain?.lease?.until || coretimeInfo.salesInfo.regionEnd;
const showEsimates = !!targetTimeslice && type !== CoreTimeTypes.Reservation;
return (
<tr key={taskId}>
<td>{taskId}</td>
<td>
<ParaLink
id={new BN(taskId)}
/>
</td>
<td>{chain?.workload?.core}</td>
<td>
<Tag
color={colours[type] as FlagColor}
label={Object.values(CoreTimeTypes)[type]}
/>
</td>
<td>{showEsimates && formatNumber(targetTimeslice * 80).toString()}</td>
<td>{showEsimates && estimateTime(targetTimeslice, coretimeInfo.status.lastCommittedTimeslice * 80)}</td>
<td>{chain?.renewal ? 'eligible' : type === CoreTimeTypes['Bulk Coretime'] ? 'renewed' : ''}</td>
<td>{chain?.renewal ? formatBalance(chain.renewal?.price.toString()) : ''}</td>
</tr>
);
}
)}
</Table>
);
}
export default React.memo(ParachainsTable);

View File

@ -0,0 +1,62 @@
// Copyright 2017-2024 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { TabItem } from '@polkadot/react-components/types';
import React, { useRef } from 'react';
import { Tabs } from '@polkadot/react-components';
import { useApi, useCoretimeInformation } from '@polkadot/react-hooks';
import Summary from './Overview/Summary.js';
import ParachainsTable from './ParachainsTable.js';
import { useTranslation } from './translate.js';
interface Props {
basePath: string;
className?: string;
}
function createItemsRef (t: (key: string, options?: { replace: Record<string, unknown> }) => string): TabItem[] {
return [
{
isRoot: true,
name: 'overview',
text: t('Overview')
}
];
}
function CoretimeApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, isApiReady } = useApi();
const itemsRef = useRef(createItemsRef(t));
const coretimeInfo = useCoretimeInformation(api, isApiReady);
return (
<main className={className}>
<Tabs
basePath={basePath}
items={itemsRef.current}
/>
{coretimeInfo && (
<Summary
api={isApiReady ? api : null}
config={coretimeInfo?.config}
parachainCount={coretimeInfo.taskIds?.length || 0}
region={coretimeInfo?.region}
saleInfo={coretimeInfo?.salesInfo}
status={coretimeInfo?.status}
/>
)}
{!!coretimeInfo &&
<ParachainsTable
coretimeInfo={coretimeInfo}
/>
}
</main>
);
}
export default React.memo(CoretimeApp);

View File

@ -0,0 +1,8 @@
// Copyright 2017-2024 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useTranslation as useTranslationBase } from 'react-i18next';
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
return useTranslationBase('app-coretime');
}

View File

@ -0,0 +1,9 @@
// Copyright 2017-2024 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
export enum CoreTimeTypes {
'Reservation',
'Lease',
'Bulk Coretime',
'On Demand'
}

View File

@ -0,0 +1,73 @@
// Copyright 2017-2024 @polkadot/app-coretime authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LegacyLease, Reservation } from '@polkadot/react-hooks/types';
import { BN } from '@polkadot/util';
import { CoreTimeTypes } from './types.js';
export const CoreTimeConsts = {
BlockTime: 6000,
BlocksPerTimeslice: 80
};
export const FirstCycleStart: Record<string, number> = {
kusama: 285768,
polkadot: 282525
};
function formatDate (date: Date) {
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'short' });
const year = date.getFullYear();
return `${day} ${month} ${year}`;
}
/**
* blockTime = 6000 ms
* BlocksPerTimeslice = 80
* Default Regoin = 5040 timeslices
* TargetBlock = TargetTimeslice * BlocksPerTimeslice
* Block Time Difference = |TargetBlock - latest Block| * blockTime
*
* Estimate timestamp =
* if targetBlock is before the latestBlock
* now minus block time difference
* else
* now plus block time difference
*/
export const estimateTime = (targetTimeslice: string | number, latestBlock: number): string | null => {
if (!latestBlock || !targetTimeslice) {
console.error('Invalid input: one or more inputs are missing');
return null;
}
const now = new Date().getTime();
try {
const blockTime = new BN(CoreTimeConsts.BlockTime); // Average block time in milliseconds (6 seconds)
const timeSlice = new BN(CoreTimeConsts.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);
return formatDate(new Date(estTimestamp.toNumber()));
} catch (error) {
console.error('Error in calculation:', error);
return null;
}
};
export const getOccupancyType = (lease: LegacyLease | undefined, reservation: Reservation | undefined, isPool: boolean): CoreTimeTypes => {
if (isPool) {
return CoreTimeTypes['On Demand'];
}
return reservation ? CoreTimeTypes.Reservation : lease ? CoreTimeTypes.Lease : CoreTimeTypes['Bulk Coretime'];
};

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src"
},
"references": [
{ "path": "../react-api/tsconfig.xref.json" },
{ "path": "../react-hooks/tsconfig.build.json" },
{ "path": "../react-components/tsconfig.build.json" }
]
}

View File

@ -18,7 +18,7 @@ import { deriveMapCache, setDeriveCache } from '@polkadot/api-derive/util';
import { ethereumChains, typesBundle } from '@polkadot/apps-config';
import { web3Accounts, web3Enable } from '@polkadot/extension-dapp';
import { TokenUnit } from '@polkadot/react-components/InputConsts/units';
import { useApiUrl, useEndpoint, usePeopleEndpoint, useQueue } from '@polkadot/react-hooks';
import { useApiUrl, useCoretimeEndpoint, useEndpoint, usePeopleEndpoint, useQueue } from '@polkadot/react-hooks';
import { ApiCtx } from '@polkadot/react-hooks/ctx/Api';
import { ApiSigner } from '@polkadot/react-signer/signers';
import { keyring } from '@polkadot/ui-keyring';
@ -297,6 +297,7 @@ export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore
const [isLocalFork] = useState(store.get('localFork') === apiUrl);
const apiEndpoint = useEndpoint(apiUrl);
const peopleEndpoint = usePeopleEndpoint(apiEndpoint?.relayName || apiEndpoint?.info);
const coreTimeEndpoint = useCoretimeEndpoint(apiEndpoint?.relayName || apiEndpoint?.info);
const relayUrls = useMemo(
() => (apiEndpoint?.valueRelay && isNumber(apiEndpoint.paraId) && (apiEndpoint.paraId < 2000))
? apiEndpoint.valueRelay
@ -309,7 +310,14 @@ export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore
: null,
[apiEndpoint, peopleEndpoint]
);
const coretimeUrls = useMemo(
() => (coreTimeEndpoint?.providers)
? coreTimeEndpoint.providers
: null,
[coreTimeEndpoint]
);
const apiRelay = useApiUrl(relayUrls);
const apiCoretime = useApiUrl(coretimeUrls);
const apiSystemPeople = useApiUrl(peopleUrls);
const createLink = useMemo(
() => makeCreateLink(apiUrl, isElectron),
@ -317,8 +325,8 @@ export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore
);
const enableIdentity = apiEndpoint?.isPeople || (isNumber(apiEndpoint?.paraId) && (apiEndpoint?.paraId >= 2000)) || (typeof apiEndpoint?.isPeopleForIdentity === 'boolean' && !apiEndpoint?.isPeopleForIdentity);
const value = useMemo<ApiProps>(
() => objectSpread({}, state, { api: statics.api, apiEndpoint, apiError, apiIdentity: ((apiEndpoint?.isPeopleForIdentity && apiSystemPeople) || statics.api), apiRelay, apiSystemPeople, apiUrl, createLink, enableIdentity, extensions, isApiConnected, isApiInitialized, isElectron, isLocalFork, isWaitingInjected: !extensions }),
[apiError, createLink, extensions, isApiConnected, isApiInitialized, isElectron, isLocalFork, state, apiEndpoint, apiRelay, apiUrl, apiSystemPeople, enableIdentity]
() => objectSpread({}, state, { api: statics.api, apiCoretime, apiEndpoint, apiError, apiIdentity: ((apiEndpoint?.isPeopleForIdentity && apiSystemPeople) || statics.api), apiRelay, apiSystemPeople, apiUrl, createLink, enableIdentity, extensions, isApiConnected, isApiInitialized, isElectron, isLocalFork, isWaitingInjected: !extensions }),
[apiError, createLink, extensions, isApiConnected, isApiInitialized, isElectron, isLocalFork, state, apiEndpoint, apiCoretime, apiRelay, apiUrl, apiSystemPeople, enableIdentity]
);
// initial initialization

View File

@ -53,6 +53,7 @@ export interface ApiProps extends ApiState {
* Used for checking if tx.idenitity.* should be used. Can be used for other scenarios as well.
*/
enableIdentity: boolean;
apiCoretime: ApiPromise;
apiRelay: ApiPromise | null;
apiSystemPeople: ApiPromise | null;
apiUrl?: string;

View File

@ -31,6 +31,7 @@ export interface ApiProps extends ApiState {
apiEndpoint: LinkOption | null;
apiError: string | null;
apiIdentity: ApiPromise;
apiCoretime: ApiPromise;
enableIdentity: boolean;
apiRelay: ApiPromise | null;
apiSystemPeople: ApiPromise | null;

View File

@ -33,6 +33,8 @@ export { useCallMulti } from './useCallMulti.js';
export { useCollectiveInstance } from './useCollectiveInstance.js';
export { useCollectiveMembers } from './useCollectiveMembers.js';
export { useCoreDescriptor } from './useCoreDescriptor.js';
export { useCoretimeEndpoint } from './useCoretimeEndpoint.js';
export { useCoretimeInformation } from './useCoretimeInformation.js';
export { useDebounce } from './useDebounce.js';
export { useDelegations } from './useDelegations.js';
export { useDeriveAccountFlags } from './useDeriveAccountFlags.js';

View File

@ -220,6 +220,30 @@ export interface CoreDescription {
info: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[];
}
export interface CoreDescriptorAssignment {
task: string,
ratio: number,
remaining: number,
isTask: boolean,
isPool: boolean
}
export interface CoreDescriptor {
core: number,
info: {
currentWork: {
assignments: CoreDescriptorAssignment[],
endHint: BN | null,
pos: number,
step: number
},
queue: {
first: BN,
last: BN
}
}
}
export interface OnDemandQueueStatus {
traffic: u128;
nextIndex: u32;
@ -297,3 +321,38 @@ export interface PalletBrokerConfigRecord {
renewalBump: BN;
contributionTimeout: number;
}
export interface ChainInformation {
id: number,
workload: CoreWorkload | undefined,
renewal: PotentialRenewal | undefined,
worklplan: CoreWorkplan[] | undefined,
lease: LegacyLease | undefined,
reservation: Reservation| undefined
}
export interface CoretimeInformation {
chainInfo: Record<number, ChainInformation>,
salesInfo: PalletBrokerSaleInfoRecord,
status: BrokerStatus,
region: RegionInfo[],
config: PalletBrokerConfigRecord
taskIds: number[]
}
export interface BrokerStatus {
coreCount: number;
privatePoolSize: number;
systemPoolSize: number;
lastCommittedTimeslice: number;
lastTimeslice: number;
}
export interface PotentialRenewal {
core: number,
when: number,
price: BN,
completion: 'Complete' | 'Partial',
mask: string[]
maskBits: number,
task: string
}

View File

@ -1,13 +1,14 @@
// Copyright 2017-2024 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { Option } from '@polkadot/types';
import type { PalletBrokerConfigRecord } from '@polkadot/types/lookup';
import type { PalletBrokerConfigRecord as SimplifiedPalletBrokerConfigRecord } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import { createNamedHook, useCall } from '@polkadot/react-hooks';
function extractInfo (config: Option<PalletBrokerConfigRecord>): SimplifiedPalletBrokerConfigRecord {
const c = config.unwrap();
@ -24,10 +25,8 @@ function extractInfo (config: Option<PalletBrokerConfigRecord>): SimplifiedPalle
};
}
function useBrokerConfigImpl () {
const { api, isApiReady } = useApi();
const config = useCall<Option<PalletBrokerConfigRecord>>(isApiReady && api.query.broker.configuration);
function useBrokerConfigImpl (api: ApiPromise, ready: boolean) {
const config = useCall<Option<PalletBrokerConfigRecord>>(ready && api?.query.broker.configuration);
const [state, setState] = useState<SimplifiedPalletBrokerConfigRecord | undefined>();

View File

@ -11,7 +11,7 @@ import { useEffect, useState } from 'react';
import { createNamedHook, useCall } from '@polkadot/react-hooks';
function useBrokerLeasesImpl (api: ApiPromise, ready: boolean): LegacyLease[] | undefined {
const leases = useCall<Vec<PalletBrokerLeaseRecordItem>>(ready && api.query.broker.leases);
const leases = useCall<Vec<PalletBrokerLeaseRecordItem>>(ready && api?.query?.broker?.leases);
const [state, setState] = useState<LegacyLease[]>();
useEffect((): void => {

View File

@ -0,0 +1,75 @@
// Copyright 2017-2024 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { Option, StorageKey, u32 } from '@polkadot/types';
import type { PalletBrokerPotentialRenewalId, PalletBrokerPotentialRenewalRecord } from '@polkadot/types/lookup';
import type { PotentialRenewal } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { processHexMask } from './utils/dataProcessing.js';
function extractInfo (info: Option<PalletBrokerPotentialRenewalRecord>, item: PalletBrokerPotentialRenewalId): PotentialRenewal | undefined {
const unwrapped: PalletBrokerPotentialRenewalRecord | null = info.isSome ? info.unwrap() : null;
let mask: string[] = [];
let task = '';
if (!unwrapped) {
return;
}
const completion = unwrapped?.completion;
if (completion?.isComplete) {
const complete = completion?.asComplete[0];
task = complete.assignment.isTask ? complete?.assignment.asTask.toString() : complete?.assignment.isPool ? 'Pool' : 'Idle';
mask = processHexMask(complete.mask);
} else if (completion?.isPartial) {
mask = processHexMask(completion?.asPartial);
task = '';
} else {
mask = [];
}
return {
// How much of a core has been assigned or, if completely assigned, the workload itself.
completion: completion?.type,
core: item?.core.toNumber(),
mask,
maskBits: mask?.length,
price: unwrapped?.price.toBn() || BN_ZERO,
task,
when: item?.when.toNumber()
};
}
const OPT_KEY = {
transform: (keys: StorageKey<[u32]>[]): u32[] =>
keys.map(({ args: [id] }) => id)
};
function useBrokerPotentialRenewalsImpl (api: ApiPromise, ready: boolean): PotentialRenewal[] | undefined {
const keys = useMapKeys(ready && api?.query.broker.potentialRenewals, [], OPT_KEY);
const potentialRenewals = useCall<[[PalletBrokerPotentialRenewalId[]], Option<PalletBrokerPotentialRenewalRecord>[]]>(ready && api?.query.broker.potentialRenewals.multi, [keys], { withParams: true });
const [state, setState] = useState<PotentialRenewal[] | undefined>();
useEffect((): void => {
if (!potentialRenewals) {
return;
}
const renewals = potentialRenewals[0][0].map((info, index) => extractInfo(potentialRenewals[1][index], info));
setState(renewals.filter((renewal): renewal is PotentialRenewal => !!renewal));
}, [potentialRenewals]);
return state;
}
export const useBrokerPotentialRenewals = createNamedHook('useBrokerPotentialRenewals', useBrokerPotentialRenewalsImpl);

View File

@ -13,7 +13,7 @@ import { createNamedHook, useCall } from '@polkadot/react-hooks';
import { processHexMask } from './utils/dataProcessing.js';
function useBrokerReservationsImpl (api: ApiPromise, ready: boolean): Reservation[] | undefined {
const reservations = useCall<[any, Vec<Vec<PalletBrokerScheduleItem>>[]]>(ready && api.query.broker.reservations);
const reservations = useCall<[any, Vec<Vec<PalletBrokerScheduleItem>>[]]>(ready && api?.query.broker.reservations);
const [state, setState] = useState<Reservation[]>();
useEffect((): void => {

View File

@ -1,13 +1,14 @@
// Copyright 2017-2024 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { Option } from '@polkadot/types';
import type { PalletBrokerSaleInfoRecord } from '@polkadot/types/lookup';
import type { PalletBrokerSaleInfoRecord as SimplifiedPalletBrokerSaleInfoRecord } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import { createNamedHook, useCall } from '@polkadot/react-hooks';
import { BN } from '@polkadot/util';
function extractInfo (record: Option<PalletBrokerSaleInfoRecord>): SimplifiedPalletBrokerSaleInfoRecord {
@ -27,10 +28,8 @@ function extractInfo (record: Option<PalletBrokerSaleInfoRecord>): SimplifiedPal
};
}
function useBrokerSalesInfoImpl () {
const { api, isApiReady } = useApi();
const record = useCall<Option<PalletBrokerSaleInfoRecord>>(isApiReady && api.query.broker.saleInfo);
function useBrokerSalesInfoImpl (api: ApiPromise, ready: boolean) {
const record = useCall<Option<PalletBrokerSaleInfoRecord>>(ready && api?.query.broker.saleInfo);
const [state, setState] = useState<SimplifiedPalletBrokerSaleInfoRecord | undefined>();

View File

@ -1,25 +1,34 @@
// Copyright 2017-2024 @polkadot/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { Option } from '@polkadot/types';
import type { PalletBrokerStatusRecord } from '@polkadot/types/lookup';
import type { BrokerStatus } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
import { createNamedHook, useCall } from '@polkadot/react-hooks';
function useBrokerStatusImpl (query: string): string | undefined {
const { api } = useApi();
const status = useCall<PalletBrokerStatusRecord>(api.query.broker?.status);
const [state, setState] = useState<PalletBrokerStatusRecord | undefined>();
function useBrokerStatusImpl (api: ApiPromise, ready: boolean): BrokerStatus | undefined {
const status = useCall<Option<PalletBrokerStatusRecord>>(ready && api?.query.broker?.status);
const [state, setState] = useState<BrokerStatus | undefined>();
useEffect((): void => {
status &&
setState(
status
);
if (!!status && status.isSome) {
const s = status.unwrap();
setState({
coreCount: s.coreCount?.toNumber(),
lastCommittedTimeslice: s.lastCommittedTimeslice?.toNumber(),
lastTimeslice: s.lastTimeslice?.toNumber(),
privatePoolSize: s.privatePoolSize?.toNumber(),
systemPoolSize: s.systemPoolSize?.toNumber()
});
}
}, [status]);
return state?.toJSON()?.[query]?.toString();
return state;
}
export const useBrokerStatus = createNamedHook('useBrokerStatus', useBrokerStatusImpl);

View File

@ -2,18 +2,42 @@
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { StorageKey, u32, Vec } from '@polkadot/types';
import type { PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor } from '@polkadot/types/lookup';
import type { CoreDescription } from './types.js';
import type { StorageKey, u32 } from '@polkadot/types';
import type { PalletBrokerCoretimeInterfaceCoreAssignment, PolkadotRuntimeParachainsAssignerCoretimeAssignmentState, PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor, PolkadotRuntimeParachainsAssignerCoretimeQueueDescriptor, PolkadotRuntimeParachainsAssignerCoretimeWorkState } from '@polkadot/types/lookup';
import type { CoreDescriptor } from './types.js';
import { useEffect, useState } from 'react';
import { createNamedHook, useCall, useMapKeys } from '@polkadot/react-hooks';
import { BN } from '@polkadot/util';
function extractInfo (info: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor, core: number): CoreDescriptor {
const currentWork: PolkadotRuntimeParachainsAssignerCoretimeWorkState | null = info?.currentWork.isSome ? info.currentWork.unwrap() : null;
const queue: PolkadotRuntimeParachainsAssignerCoretimeQueueDescriptor | null = info?.queue.isSome ? info.queue.unwrap() : null;
const assignments = currentWork?.assignments || [];
function extractInfo (info: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[], core: number) {
return {
core,
info
info: {
currentWork: {
assignments: assignments?.map((one: [PalletBrokerCoretimeInterfaceCoreAssignment, PolkadotRuntimeParachainsAssignerCoretimeAssignmentState]) => {
return ({
isPool: one[0]?.isPool,
isTask: one[0]?.isTask,
ratio: one[1]?.ratio.toNumber(),
remaining: one[1]?.remaining.toNumber(),
task: one[0]?.isTask ? one[0]?.asTask.toString() : one[0]?.isPool ? 'Pool' : 'Idle'
});
}),
endHint: currentWork?.endHint.isSome ? currentWork?.endHint?.unwrap().toBn() : null,
pos: currentWork?.pos.toNumber() || 0,
step: currentWork?.step.toNumber() || 0
},
queue: {
first: queue?.first.toBn() || new BN(0),
last: queue?.last.toBn() || new BN(0)
}
}
};
}
@ -22,7 +46,7 @@ const OPT_KEY = {
keys.map(({ args: [id] }) => id)
};
function useCoreDescriptorImpl (api: ApiPromise, ready: boolean): CoreDescription[] | undefined {
function useCoreDescriptorImpl (api: ApiPromise, ready: boolean): CoreDescriptor[] | undefined {
const keys = useMapKeys(ready && api.query.coretimeAssignmentProvider.coreDescriptors, [], OPT_KEY);
const sanitizedKeys = keys?.map((_, index) => {
@ -31,17 +55,12 @@ function useCoreDescriptorImpl (api: ApiPromise, ready: boolean): CoreDescriptio
sanitizedKeys?.pop();
const coreDescriptors = useCall<[[number[]], Vec<PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor>[]]>(ready && api.query.coretimeAssignmentProvider.coreDescriptors.multi, [sanitizedKeys], { withParams: true });
const [state, setState] = useState<CoreDescription[] | undefined>();
const coreDescriptors = useCall<[[number[]], PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[]]>(ready && api.query.coretimeAssignmentProvider.coreDescriptors.multi, [sanitizedKeys], { withParams: true });
const [state, setState] = useState<CoreDescriptor[] | undefined>();
useEffect((): void => {
coreDescriptors &&
setState(
coreDescriptors[0][0].map((info, index) => {
return extractInfo(coreDescriptors[1][index], info);
}
)
setState(coreDescriptors[0][0].map((info, index) => extractInfo(coreDescriptors[1][index], info))
);
}, [coreDescriptors]);

View File

@ -0,0 +1,23 @@
// Copyright 2017-2024 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
import { useMemo } from 'react';
import { createWsEndpoints } from '@polkadot/apps-config';
import { isString } from '@polkadot/util';
import { createNamedHook } from './createNamedHook.js';
const endpoints = createWsEndpoints((k, v) => v?.toString() || k);
export function getCoretimeEndpoint (curApiInfo?: string): LinkOption | null {
return endpoints.find(({ info }) => isString(info) && isString(curApiInfo) && info.toLowerCase().includes('coretime') && info.toLowerCase().includes(curApiInfo.toLowerCase())) || null;
}
function useCoretimeEndpointImpl (relayInfo?: string): LinkOption | null {
return useMemo(() => getCoretimeEndpoint(relayInfo), [relayInfo]);
}
export const useCoretimeEndpoint = createNamedHook('useCoretimeEndpoint', useCoretimeEndpointImpl);

View File

@ -0,0 +1,115 @@
// Copyright 2017-2024 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { ParaId } from '@polkadot/types/interfaces';
import type { ChainInformation, CoretimeInformation, CoreWorkload, CoreWorkloadInfo, PotentialRenewal } from './types.js';
import { useEffect, useMemo, useState } from 'react';
import { createNamedHook, useApi, useBrokerConfig, useBrokerLeases, useBrokerReservations, useBrokerSalesInfo, useBrokerStatus, useCall, useCoreDescriptor, useRegions, useWorkloadInfos, useWorkplanInfos } from '@polkadot/react-hooks';
import { useBrokerPotentialRenewals } from './useBrokerPotentialRenewals.js';
function useCoretimeInformationImpl (api: ApiPromise, ready: boolean): CoretimeInformation | undefined {
const { apiCoretime, isApiReady } = useApi();
const [workloadData, setWorkloadData] = useState<CoreWorkload[]>([]);
const [taskIds, setTaskIds] = useState<number[]>([]);
/** coretime API calls */
const status = useBrokerStatus(apiCoretime, isApiReady);
const leases = useBrokerLeases(apiCoretime, isApiReady);
const reservations = useBrokerReservations(apiCoretime, isApiReady);
const salesInfo = useBrokerSalesInfo(apiCoretime, isApiReady);
const workloads = useWorkloadInfos(apiCoretime, isApiReady);
const workplans = useWorkplanInfos(apiCoretime, isApiReady);
const config = useBrokerConfig(apiCoretime, isApiReady);
const potentialRenewals = useBrokerPotentialRenewals(apiCoretime, isApiReady);
const region = useRegions(apiCoretime);
/** Other APIs */
const paraIds = useCall<ParaId[]>(api.query.paras.parachains);
const coreInfos = useCoreDescriptor(api, ready);
// when - The point in time that the renewable workload on core ends and a fresh renewal may begin.
const potentialRenewalsCurrentRegion = useMemo(() => config && salesInfo && potentialRenewals?.filter((renewal: PotentialRenewal) => renewal.when.toString() === salesInfo?.regionBegin.toString()), [potentialRenewals, salesInfo, config]);
const [state, setState] = useState<CoretimeInformation | undefined>();
useEffect(() => {
if (paraIds?.length && potentialRenewals?.length && !taskIds.length) {
const simpleIds = paraIds.map((p) => p.toNumber());
const renewalIds = potentialRenewals?.map((r) => Number(r.task));
const numbers = [...new Set(simpleIds.concat(renewalIds))];
if (numbers?.length > simpleIds.length) {
setTaskIds(numbers.sort((a, b) => a - b));
} else {
setTaskIds(simpleIds);
}
}
}, [potentialRenewals, paraIds, taskIds]);
useEffect(() => {
if (workloads?.length) {
setWorkloadData(workloads);
}
if (!!coreInfos?.length && !workloads?.length) {
const parsedCoreInfos = coreInfos?.map((coreInfo) => ({
core: -1,
info: coreInfo.info.currentWork.assignments.map((assignment) => (
{
isPool: assignment.isPool,
isTask: assignment.isTask,
mask: [],
maskBits: 0,
task: assignment.task
}
) as CoreWorkloadInfo)
}));
setWorkloadData(parsedCoreInfos as unknown as CoreWorkload[]);
}
}, [workloads, coreInfos]);
useEffect((): void => {
if (!workloadData?.length || !leases?.length || !reservations?.length) {
return;
}
const chainInfo: Record<string, ChainInformation> = {};
taskIds?.forEach((id) => {
const taskId = id.toString();
const lease = leases?.find((lease) => lease.task === taskId);
const reservation = reservations?.find((reservation) => reservation.task === taskId);
const workload = workloadData?.find((one) => one.info.task === taskId);
const renewal = potentialRenewalsCurrentRegion?.find((renewal) => renewal.task.toString() === taskId);
const worklplan = workplans?.filter((workplan) => workplan.core === workload?.core && workplan.info.task.toString() === taskId);
chainInfo[id.toString()] = {
id,
lease,
renewal,
reservation,
workload,
worklplan
};
});
if (chainInfo && config && region && salesInfo && status) {
setState({
chainInfo,
config,
region,
salesInfo,
status,
taskIds
});
}
}, [taskIds, workloadData, potentialRenewalsCurrentRegion, salesInfo, leases, reservations, region, status, config, workplans]);
return state;
}
export const useCoretimeInformation = createNamedHook('useCoretimeInformation', useCoretimeInformationImpl);

View File

@ -19,7 +19,7 @@ function extractInfo (value: OnDemandQueueStatus) {
function useQueueStatusImpl (): OnDemandQueueStatus | undefined {
const { api } = useApi();
const queue = useCall<OnDemandQueueStatus>(api.query.onDemandAssignmentProvider.queueStatus);
const queue = useCall<OnDemandQueueStatus>(api.query.onDemandAssignmentProvider?.queueStatus);
const [state, setState] = useState<OnDemandQueueStatus | undefined>();

View File

@ -27,9 +27,9 @@ const OPT_KEY = {
};
function useRegionsImpl (api: ApiPromise): RegionInfo[] | undefined {
const regionKeys = useMapKeys(api.query.broker.regions, [], OPT_KEY);
const regionKeys = useMapKeys(api?.query?.broker.regions, [], OPT_KEY);
const regionInfo = useCall<[[PalletBrokerRegionId[]], Option<PalletBrokerRegionRecord>[]]>(api.query.broker.regions.multi, [regionKeys], { withParams: true });
const regionInfo = useCall<[[PalletBrokerRegionId[]], Option<PalletBrokerRegionRecord>[]]>(api?.query?.broker.regions.multi, [regionKeys], { withParams: true });
const [state, setState] = useState<RegionInfo[] | undefined>();

View File

@ -34,7 +34,7 @@ function extractInfo (info: PalletBrokerScheduleItem[], core: number): CoreWorkl
isTask: assignment.isTask,
mask,
maskBits: mask.length,
task: assignment.isTask ? assignment.asTask.toString() : assignment.isPool ? 'Pool' : ''
task: assignment.isTask ? assignment.asTask.toString() : assignment.isPool ? 'Pool' : 'Idle'
}
};
}
@ -45,8 +45,8 @@ const OPT_KEY = {
};
function useWorkloadInfosImpl (api: ApiPromise, ready: boolean): CoreWorkload[] | undefined {
const cores = useMapKeys(ready && api.query.broker.workload, [], OPT_KEY);
const workloadInfo = useCall<[[BN[]], Vec<PalletBrokerScheduleItem>[]]>(ready && api.query.broker.workload.multi, [cores], { withParams: true });
const cores = useMapKeys(ready && api?.query.broker.workload, [], OPT_KEY);
const workloadInfo = useCall<[[BN[]], Vec<PalletBrokerScheduleItem>[]]>(ready && api?.query.broker.workload.multi, [cores], { withParams: true });
const [state, setState] = useState<CoreWorkload[] | undefined>();
useEffect((): void => {

View File

@ -34,7 +34,7 @@ function extractInfo (info: Vec<PalletBrokerScheduleItem>, core: number, timesli
isTask: assignment.isTask,
mask,
maskBits: mask.length,
task: assignment.isTask ? assignment.asTask.toString() : assignment.isPool ? 'Pool' : ''
task: assignment.isTask ? assignment.asTask.toString() : assignment.isPool ? 'Pool' : 'Idle'
},
timeslice
};
@ -46,13 +46,13 @@ const OPT_KEY = {
};
function useWorkplanInfosImpl (api: ApiPromise, ready: boolean): CoreWorkplan[] | undefined {
const workplanKeys = useMapKeys(ready && api.query.broker.workplan, [], OPT_KEY);
const workplanKeys = useMapKeys(ready && api?.query.broker.workplan, [], OPT_KEY);
const sanitizedKeys = workplanKeys?.map((value) => {
return value[0];
});
const workplanInfo = useCall<[[[u32, u16][]], Option<Vec<PalletBrokerScheduleItem>>[]]>(ready && api.query.broker.workplan.multi, [sanitizedKeys], { withParams: true });
const workplanInfo = useCall<[[[u32, u16][]], Option<Vec<PalletBrokerScheduleItem>>[]]>(ready && api?.query.broker.workplan.multi, [sanitizedKeys], { withParams: true });
const [state, setState] = useState<CoreWorkplan[] | undefined>();

View File

@ -1,25 +0,0 @@
// Copyright 2017-2024 @polkadot/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { useBrokerStatus } from '@polkadot/react-hooks';
interface Props {
children?: React.ReactNode;
className?: string;
query: string;
}
function BrokerStatus ({ children, className = '', query }: Props): React.ReactElement<Props> {
const info = useBrokerStatus(query) || '-';
return (
<div className={className}>
{info}
{children}
</div>
);
}
export default React.memo(BrokerStatus);

View File

@ -8,7 +8,6 @@ export { default as BestFinalized } from './BestFinalized.js';
export { default as BestNumber } from './BestNumber.js';
export { default as BlockToTime } from './BlockToTime.js';
export { default as Bonded } from './Bonded.js';
export { default as BrokerStatus } from './BrokerStatus.js';
export { default as Chain } from './Chain.js';
export { default as Elapsed } from './Elapsed.js';
export { default as FormatBalance } from './FormatBalance.js';

View File

@ -33,6 +33,7 @@
"@polkadot/app-collator": ["page-collator/src/index.tsx"],
"@polkadot/app-contracts": ["page-contracts/src/index.tsx"],
"@polkadot/app-council": ["page-council/src/index.tsx"],
"@polkadot/app-coretime": ["page-coretime/src/index.tsx"],
"@polkadot/app-democracy": ["page-democracy/src/index.tsx"],
"@polkadot/app-democracy/*": ["page-democracy/src/*.tsx"],
"@polkadot/app-extrinsics": ["page-extrinsics/src/index.tsx"],

View File

@ -28,6 +28,7 @@
{ "path": "./packages/page-collator/tsconfig.build.json" },
{ "path": "./packages/page-contracts/tsconfig.build.json" },
{ "path": "./packages/page-council/tsconfig.build.json" },
{ "path": "./packages/page-coretime/tsconfig.build.json" },
{ "path": "./packages/page-democracy/tsconfig.build.json" },
{ "path": "./packages/page-explorer/tsconfig.build.json" },
{ "path": "./packages/page-extrinsics/tsconfig.build.json" },

View File

@ -1427,6 +1427,19 @@ __metadata:
languageName: unknown
linkType: soft
"@polkadot/app-coretime@workspace:packages/page-coretime":
version: 0.0.0-use.local
resolution: "@polkadot/app-coretime@workspace:packages/page-coretime"
dependencies:
"@polkadot/react-components": "npm:^0.145.2-1-x"
"@polkadot/react-query": "npm:^0.145.2-1-x"
peerDependencies:
react: "*"
react-dom: "*"
react-is: "*"
languageName: unknown
linkType: soft
"@polkadot/app-council@workspace:packages/page-council":
version: 0.0.0-use.local
resolution: "@polkadot/app-council@workspace:packages/page-council"