diff --git a/packages/apps-config/src/endpoints/testingRelayPaseo.ts b/packages/apps-config/src/endpoints/testingRelayPaseo.ts index 3a9f2702e0..489082371f 100644 --- a/packages/apps-config/src/endpoints/testingRelayPaseo.ts +++ b/packages/apps-config/src/endpoints/testingRelayPaseo.ts @@ -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: { diff --git a/packages/apps-routing/src/coretime.ts b/packages/apps-routing/src/coretime.ts new file mode 100644 index 0000000000..37b45a48d8 --- /dev/null +++ b/packages/apps-routing/src/coretime.ts @@ -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' }) + }; +} diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index 60d3a739c2..713b0b9bcc 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -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), diff --git a/packages/apps-routing/tsconfig.build.json b/packages/apps-routing/tsconfig.build.json index 32327a5d91..b1f8b42f35 100644 --- a/packages/apps-routing/tsconfig.build.json +++ b/packages/apps-routing/tsconfig.build.json @@ -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" }, diff --git a/packages/page-broker/src/Overview/CoreTable.tsx b/packages/page-broker/src/Overview/CoreTable.tsx index 1fb9bba2ea..dd7d2fd7ca 100644 --- a/packages/page-broker/src/Overview/CoreTable.tsx +++ b/packages/page-broker/src/Overview/CoreTable.tsx @@ -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 { +function CoreTable ({ api, config, core, workload, workplan }: Props): React.ReactElement { 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 > { +function CoresTable ({ api, data, isApiReady }: Props): React.ReactElement { + const config = useBrokerConfig(api, isApiReady); + return ( <> - {data?.map((coreData) => { + {config && data?.map((coreData) => { return ( 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 ( @@ -63,24 +66,31 @@ function Summary ({ coreCount, workloadInfos }: Props): React.ReactElement { - -
- {formatBalance(salesInfo?.endPrice) || '-'} -
-
{coreCount}
- {salesInfo?.coresSold} / {salesInfo?.coresOffered} + {saleInfo?.coresSold} / {saleInfo?.coresOffered}
+ )} -
+
+
+ {status && currentRegionStart && currentRegionEnd && + ( + <> + +
+
{estimateTime(currentRegionStart, status?.lastTimeslice * 80)}
+
{estimateTime(currentRegionEnd, status?.lastTimeslice * 80)}
+
+
+ +
+
{currentRegionStart}
+
{currentRegionEnd}
+
+
+ + ) + } +
); } diff --git a/packages/page-broker/src/Overview/Summary/Cores.tsx b/packages/page-broker/src/Overview/Summary/Cores.tsx deleted file mode 100644 index 2e15228a74..0000000000 --- a/packages/page-broker/src/Overview/Summary/Cores.tsx +++ /dev/null @@ -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 | null { - return ( - - {children} - - ); -} - -export default React.memo(Cores); diff --git a/packages/page-broker/src/Overview/Summary/RenewalPrice.tsx b/packages/page-broker/src/Overview/Summary/RenewalPrice.tsx deleted file mode 100644 index a355ad030b..0000000000 --- a/packages/page-broker/src/Overview/Summary/RenewalPrice.tsx +++ /dev/null @@ -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 | null { - const salesInfo = useBrokerSalesInfo(); - - return ( -
- {formatBalance(salesInfo?.endPrice) || '-'} -
- ); -} - -export default React.memo(RenewalPrice); diff --git a/packages/page-broker/src/Overview/Summary/Timeslice.tsx b/packages/page-broker/src/Overview/Summary/Timeslice.tsx index a8f3546d3b..9bdba0878f 100644 --- a/packages/page-broker/src/Overview/Summary/Timeslice.tsx +++ b/packages/page-broker/src/Overview/Summary/Timeslice.tsx @@ -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 | null { + const { api, isApiReady } = useApi(); + const info = useBrokerStatus(api, isApiReady); + return ( - +
+ {info?.lastTimeslice || '-'} {children} - - ); +
); } export default React.memo(Timeslice); diff --git a/packages/page-broker/src/Overview/WorkInfoRow.tsx b/packages/page-broker/src/Overview/WorkInfoRow.tsx index 09f55ba882..a56f22deff 100644 --- a/packages/page-broker/src/Overview/WorkInfoRow.tsx +++ b/packages/page-broker/src/Overview/WorkInfoRow.tsx @@ -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 = { + [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 ( <> - + + + ); } - case (Occupancy.Lease): { + case (CoreTimeTypes.Lease): return ( <> + +
type
+ +
+ + ); + + case (CoreTimeTypes['On Demand']): { + return ( + <> + + + + + +
type
+ +
); } @@ -123,23 +172,24 @@ function WorkInfoRow ({ data }: { data: InfoRow }): React.ReactElement { header='Last block' value={data.endBlock} /> - - -
{'Owner'}
- {data.owner - ? ( - - ) - - :

 

} + +
type
+
+ {data.owner + ? +
{'Owner'}
+ + +
+ : } ); } } diff --git a/packages/page-broker/src/Overview/Workload.tsx b/packages/page-broker/src/Overview/Workload.tsx index 028c84ee29..ce9a897340 100644 --- a/packages/page-broker/src/Overview/Workload.tsx +++ b/packages/page-broker/src/Overview/Workload.tsx @@ -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 { +function Workload ({ api, config, core, workload, workplan }: Props): React.ReactElement { const { isApiReady } = useApi(); - const salesInfo = useBrokerSalesInfo(); + const salesInfo = useBrokerSalesInfo(api, isApiReady); const status = useCall>(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; diff --git a/packages/page-broker/src/Overview/index.tsx b/packages/page-broker/src/Overview/index.tsx index 9506e2080e..46b90b6456 100644 --- a/packages/page-broker/src/Overview/index.tsx +++ b/packages/page-broker/src/Overview/index.tsx @@ -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 { const [data, setData] = useState([]); const [filtered, setFiltered] = useState(); - 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 { 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 (
{!!data?.length && @@ -88,6 +88,7 @@ function Overview ({ className }: Props): React.ReactElement { )} ) diff --git a/packages/page-broker/src/types.ts b/packages/page-broker/src/types.ts index 69d25e21c6..28547c792e 100644 --- a/packages/page-broker/src/types.ts +++ b/packages/page-broker/src/types.ts @@ -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 } diff --git a/packages/page-broker/src/utils.ts b/packages/page-broker/src/utils.ts index 90104c4afa..773e363af8 100644 --- a/packages/page-broker/src/utils.ts +++ b/packages/page-broker/src/utils.ts @@ -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 = (items: T[]): Record); }; -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']; }; diff --git a/packages/page-coretime/.skip-build b/packages/page-coretime/.skip-build new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/page-coretime/.skip-npm b/packages/page-coretime/.skip-npm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/page-coretime/README.md b/packages/page-coretime/README.md new file mode 100644 index 0000000000..d9c23d4732 --- /dev/null +++ b/packages/page-coretime/README.md @@ -0,0 +1 @@ +# @polkadot/app-coretime diff --git a/packages/page-coretime/package.json b/packages/page-coretime/package.json new file mode 100644 index 0000000000..221ac86026 --- /dev/null +++ b/packages/page-coretime/package.json @@ -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": "*" + } +} diff --git a/packages/page-coretime/src/Overview/Summary.tsx b/packages/page-coretime/src/Overview/Summary.tsx new file mode 100644 index 0000000000..fa57045494 --- /dev/null +++ b/packages/page-coretime/src/Overview/Summary.tsx @@ -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 { + const { t } = useTranslation(); + const currentRegionEnd = saleInfo.regionEnd - config.regionLength; + const currentRegionStart = saleInfo.regionEnd - config.regionLength * 2; + const chainName = useCall(api?.rpc.system.chain)?.toString().toLowerCase(); + + const cycleNumber = useMemo(() => + chainName && currentRegionEnd && Math.floor((currentRegionEnd - FirstCycleStart[chainName]) / config.regionLength) + , [currentRegionEnd, chainName, config]); + + return ( + +
+ + {status?.lastTimeslice} + + + {`${saleInfo?.coresSold} / ${saleInfo?.coresOffered}`} + + + {parachainCount && parachainCount} + + {status && + +
+ {cycleNumber} +
+
+ } + {config && status && + + } +
+
+ {status && + ( +
+
{estimateTime(currentRegionStart, status?.lastTimeslice * 80)}
+
{estimateTime(currentRegionEnd, status?.lastTimeslice * 80)}
+
+
) + } + {status && + +
+
{currentRegionStart}
+
{currentRegionEnd}
+
+
+ } +
+
+ ); +} + +export default React.memo(Summary); diff --git a/packages/page-coretime/src/ParachainsTable.tsx b/packages/page-coretime/src/ParachainsTable.tsx new file mode 100644 index 0000000000..950e2aba4a --- /dev/null +++ b/packages/page-coretime/src/ParachainsTable.tsx @@ -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 = { + [CoreTimeTypes.Reservation]: 'orange', + [CoreTimeTypes.Lease]: 'blue', + [CoreTimeTypes['Bulk Coretime']]: 'pink' +}; + +function ParachainsTable ({ coretimeInfo }: Props): React.ReactElement { + 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 ( + + {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 ( + + + + + + + + + + + ); + } + )} + +
{taskId} + + {chain?.workload?.core} + + {showEsimates && formatNumber(targetTimeslice * 80).toString()}{showEsimates && estimateTime(targetTimeslice, coretimeInfo.status.lastCommittedTimeslice * 80)}{chain?.renewal ? 'eligible' : type === CoreTimeTypes['Bulk Coretime'] ? 'renewed' : ''}{chain?.renewal ? formatBalance(chain.renewal?.price.toString()) : ''}
+ ); +} + +export default React.memo(ParachainsTable); diff --git a/packages/page-coretime/src/index.tsx b/packages/page-coretime/src/index.tsx new file mode 100644 index 0000000000..3125e2b327 --- /dev/null +++ b/packages/page-coretime/src/index.tsx @@ -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): TabItem[] { + return [ + { + isRoot: true, + name: 'overview', + text: t('Overview') + } + ]; +} + +function CoretimeApp ({ basePath, className }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api, isApiReady } = useApi(); + const itemsRef = useRef(createItemsRef(t)); + const coretimeInfo = useCoretimeInformation(api, isApiReady); + + return ( +
+ + {coretimeInfo && ( + + )} + {!!coretimeInfo && + + } + +
+ ); +} + +export default React.memo(CoretimeApp); diff --git a/packages/page-coretime/src/translate.ts b/packages/page-coretime/src/translate.ts new file mode 100644 index 0000000000..125dda4ab0 --- /dev/null +++ b/packages/page-coretime/src/translate.ts @@ -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 } { + return useTranslationBase('app-coretime'); +} diff --git a/packages/page-coretime/src/types.ts b/packages/page-coretime/src/types.ts new file mode 100644 index 0000000000..b5afab7d6a --- /dev/null +++ b/packages/page-coretime/src/types.ts @@ -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' +} diff --git a/packages/page-coretime/src/utils.ts b/packages/page-coretime/src/utils.ts new file mode 100644 index 0000000000..5b7800b7da --- /dev/null +++ b/packages/page-coretime/src/utils.ts @@ -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 = { + 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']; +}; diff --git a/packages/page-coretime/tsconfig.build.json b/packages/page-coretime/tsconfig.build.json new file mode 100644 index 0000000000..0292e5e4c9 --- /dev/null +++ b/packages/page-coretime/tsconfig.build.json @@ -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" } + ] +} diff --git a/packages/react-api/src/Api.tsx b/packages/react-api/src/Api.tsx index a5c644731d..011fbf5cff 100644 --- a/packages/react-api/src/Api.tsx +++ b/packages/react-api/src/Api.tsx @@ -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( - () => 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 diff --git a/packages/react-api/src/types.ts b/packages/react-api/src/types.ts index d7873857e5..e86868034e 100644 --- a/packages/react-api/src/types.ts +++ b/packages/react-api/src/types.ts @@ -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; diff --git a/packages/react-hooks/src/ctx/types.ts b/packages/react-hooks/src/ctx/types.ts index c236701bec..227fa46a6f 100644 --- a/packages/react-hooks/src/ctx/types.ts +++ b/packages/react-hooks/src/ctx/types.ts @@ -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; diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 25db135781..83f3695e1b 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -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'; diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts index ad387f2fa9..c450071257 100644 --- a/packages/react-hooks/src/types.ts +++ b/packages/react-hooks/src/types.ts @@ -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, + 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 +} diff --git a/packages/react-hooks/src/useBrokerConfig.ts b/packages/react-hooks/src/useBrokerConfig.ts index 61423945bf..da46006905 100644 --- a/packages/react-hooks/src/useBrokerConfig.ts +++ b/packages/react-hooks/src/useBrokerConfig.ts @@ -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): SimplifiedPalletBrokerConfigRecord { const c = config.unwrap(); @@ -24,10 +25,8 @@ function extractInfo (config: Option): SimplifiedPalle }; } -function useBrokerConfigImpl () { - const { api, isApiReady } = useApi(); - - const config = useCall>(isApiReady && api.query.broker.configuration); +function useBrokerConfigImpl (api: ApiPromise, ready: boolean) { + const config = useCall>(ready && api?.query.broker.configuration); const [state, setState] = useState(); diff --git a/packages/react-hooks/src/useBrokerLeases.ts b/packages/react-hooks/src/useBrokerLeases.ts index 441bbd066f..075dbfff15 100644 --- a/packages/react-hooks/src/useBrokerLeases.ts +++ b/packages/react-hooks/src/useBrokerLeases.ts @@ -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>(ready && api.query.broker.leases); + const leases = useCall>(ready && api?.query?.broker?.leases); const [state, setState] = useState(); useEffect((): void => { diff --git a/packages/react-hooks/src/useBrokerPotentialRenewals.ts b/packages/react-hooks/src/useBrokerPotentialRenewals.ts new file mode 100644 index 0000000000..ed5cebbdfb --- /dev/null +++ b/packages/react-hooks/src/useBrokerPotentialRenewals.ts @@ -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, 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[]]>(ready && api?.query.broker.potentialRenewals.multi, [keys], { withParams: true }); + + const [state, setState] = useState(); + + 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); diff --git a/packages/react-hooks/src/useBrokerReservations.ts b/packages/react-hooks/src/useBrokerReservations.ts index cd5e37e085..6d3d16faa0 100644 --- a/packages/react-hooks/src/useBrokerReservations.ts +++ b/packages/react-hooks/src/useBrokerReservations.ts @@ -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>[]]>(ready && api.query.broker.reservations); + const reservations = useCall<[any, Vec>[]]>(ready && api?.query.broker.reservations); const [state, setState] = useState(); useEffect((): void => { diff --git a/packages/react-hooks/src/useBrokerSalesInfo.ts b/packages/react-hooks/src/useBrokerSalesInfo.ts index fcc67e517e..7d28a07e2c 100644 --- a/packages/react-hooks/src/useBrokerSalesInfo.ts +++ b/packages/react-hooks/src/useBrokerSalesInfo.ts @@ -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): SimplifiedPalletBrokerSaleInfoRecord { @@ -27,10 +28,8 @@ function extractInfo (record: Option): SimplifiedPal }; } -function useBrokerSalesInfoImpl () { - const { api, isApiReady } = useApi(); - - const record = useCall>(isApiReady && api.query.broker.saleInfo); +function useBrokerSalesInfoImpl (api: ApiPromise, ready: boolean) { + const record = useCall>(ready && api?.query.broker.saleInfo); const [state, setState] = useState(); diff --git a/packages/react-hooks/src/useBrokerStatus.ts b/packages/react-hooks/src/useBrokerStatus.ts index 708dece93f..d1f757a024 100644 --- a/packages/react-hooks/src/useBrokerStatus.ts +++ b/packages/react-hooks/src/useBrokerStatus.ts @@ -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(api.query.broker?.status); - const [state, setState] = useState(); +function useBrokerStatusImpl (api: ApiPromise, ready: boolean): BrokerStatus | undefined { + const status = useCall>(ready && api?.query.broker?.status); + const [state, setState] = useState(); 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); diff --git a/packages/react-hooks/src/useCoreDescriptor.ts b/packages/react-hooks/src/useCoreDescriptor.ts index c7eca80396..7fca588d81 100644 --- a/packages/react-hooks/src/useCoreDescriptor.ts +++ b/packages/react-hooks/src/useCoreDescriptor.ts @@ -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[]]>(ready && api.query.coretimeAssignmentProvider.coreDescriptors.multi, [sanitizedKeys], { withParams: true }); - - const [state, setState] = useState(); + const coreDescriptors = useCall<[[number[]], PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor[]]>(ready && api.query.coretimeAssignmentProvider.coreDescriptors.multi, [sanitizedKeys], { withParams: true }); + const [state, setState] = useState(); 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]); diff --git a/packages/react-hooks/src/useCoretimeEndpoint.ts b/packages/react-hooks/src/useCoretimeEndpoint.ts new file mode 100644 index 0000000000..2e63072aaf --- /dev/null +++ b/packages/react-hooks/src/useCoretimeEndpoint.ts @@ -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); diff --git a/packages/react-hooks/src/useCoretimeInformation.ts b/packages/react-hooks/src/useCoretimeInformation.ts new file mode 100644 index 0000000000..5fa788f55b --- /dev/null +++ b/packages/react-hooks/src/useCoretimeInformation.ts @@ -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([]); + const [taskIds, setTaskIds] = useState([]); + + /** 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(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(); + + 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 = {}; + + 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); diff --git a/packages/react-hooks/src/useQueueStatus.ts b/packages/react-hooks/src/useQueueStatus.ts index fa4271f6b6..e132fcf6ad 100644 --- a/packages/react-hooks/src/useQueueStatus.ts +++ b/packages/react-hooks/src/useQueueStatus.ts @@ -19,7 +19,7 @@ function extractInfo (value: OnDemandQueueStatus) { function useQueueStatusImpl (): OnDemandQueueStatus | undefined { const { api } = useApi(); - const queue = useCall(api.query.onDemandAssignmentProvider.queueStatus); + const queue = useCall(api.query.onDemandAssignmentProvider?.queueStatus); const [state, setState] = useState(); diff --git a/packages/react-hooks/src/useRegions.ts b/packages/react-hooks/src/useRegions.ts index 4570c3c39c..9e115ec0e7 100644 --- a/packages/react-hooks/src/useRegions.ts +++ b/packages/react-hooks/src/useRegions.ts @@ -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[]]>(api.query.broker.regions.multi, [regionKeys], { withParams: true }); + const regionInfo = useCall<[[PalletBrokerRegionId[]], Option[]]>(api?.query?.broker.regions.multi, [regionKeys], { withParams: true }); const [state, setState] = useState(); diff --git a/packages/react-hooks/src/useWorkloadInfos.ts b/packages/react-hooks/src/useWorkloadInfos.ts index 32dd71389e..4a66a63f70 100644 --- a/packages/react-hooks/src/useWorkloadInfos.ts +++ b/packages/react-hooks/src/useWorkloadInfos.ts @@ -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[]]>(ready && api.query.broker.workload.multi, [cores], { withParams: true }); + const cores = useMapKeys(ready && api?.query.broker.workload, [], OPT_KEY); + const workloadInfo = useCall<[[BN[]], Vec[]]>(ready && api?.query.broker.workload.multi, [cores], { withParams: true }); const [state, setState] = useState(); useEffect((): void => { diff --git a/packages/react-hooks/src/useWorkplanInfos.ts b/packages/react-hooks/src/useWorkplanInfos.ts index 13bd487fac..f804700f15 100644 --- a/packages/react-hooks/src/useWorkplanInfos.ts +++ b/packages/react-hooks/src/useWorkplanInfos.ts @@ -34,7 +34,7 @@ function extractInfo (info: Vec, 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>[]]>(ready && api.query.broker.workplan.multi, [sanitizedKeys], { withParams: true }); + const workplanInfo = useCall<[[[u32, u16][]], Option>[]]>(ready && api?.query.broker.workplan.multi, [sanitizedKeys], { withParams: true }); const [state, setState] = useState(); diff --git a/packages/react-query/src/BrokerStatus.tsx b/packages/react-query/src/BrokerStatus.tsx deleted file mode 100644 index a85ed52d94..0000000000 --- a/packages/react-query/src/BrokerStatus.tsx +++ /dev/null @@ -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 { - const info = useBrokerStatus(query) || '-'; - - return ( -
- {info} - {children} -
- ); -} - -export default React.memo(BrokerStatus); diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index 53d94bf456..6d29a47722 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -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'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 662448d98d..158c1f661e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"], diff --git a/tsconfig.build.json b/tsconfig.build.json index bea32db314..53428eb1a1 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -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" }, diff --git a/yarn.lock b/yarn.lock index d664284036..49c4e6e6f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"