diff --git a/docs/src/openapi-v1.yaml b/docs/src/openapi-v1.yaml
index 36ea29961..7dd10cd12 100755
--- a/docs/src/openapi-v1.yaml
+++ b/docs/src/openapi-v1.yaml
@@ -24,6 +24,7 @@ tags:
- name: accounts
- name: blocks
- name: contracts
+- name: coretime
- name: node
description: node connected to sidecar
- name: pallets
@@ -985,6 +986,142 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
+ /coretime/leases:
+ get:
+ tags:
+ - coretime
+ summary: Get all the leases currently registered on coretime chain.
+ description: ''
+ operationId: getCoretimeLeases
+ parameters:
+ - name: blockId
+ in: query
+ description: Block identifier, as the block height or block hash.
+ required: false
+ schema:
+ pattern: '^0[xX][0-9a-fA-F]{1,64}$|[0-9]{1,12}'
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CoretimeLeasesResponse'
+ /coretime/regions:
+ get:
+ tags:
+ - coretime
+ summary: Get all the regions currently registered on coretime chain.
+ description: ''
+ operationId: getCoretimeRegions
+ parameters:
+ - name: blockId
+ in: query
+ description: Block identifier, as the block height or block hash.
+ required: false
+ schema:
+ pattern: '^0[xX][0-9a-fA-F]{1,64}$|[0-9]{1,12}'
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CoretimeRegionsResponse'
+ /coretime/renewals:
+ get:
+ tags:
+ - coretime
+ summary: Get all the potential renewals currently registered on coretime chain.
+ description: ''
+ operationId: getCoretimeRenewals
+ parameters:
+ - name: blockId
+ in: query
+ description: Block identifier, as the block height or block hash.
+ required: false
+ schema:
+ pattern: '^0[xX][0-9a-fA-F]{1,64}$|[0-9]{1,12}'
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CoretimeRenewalsResponse'
+ /coretime/reservations:
+ get:
+ tags:
+ - coretime
+ summary: Get all the reservations currently registered on coretime chain.
+ description: ''
+ operationId: getCoretimeReservations
+ parameters:
+ - name: blockId
+ in: query
+ description: Block identifier, as the block height or block hash.
+ required: false
+ schema:
+ pattern: '^0[xX][0-9a-fA-F]{1,64}$|[0-9]{1,12}'
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CoretimeReservationsResponse'
+ /coretime/info:
+ get:
+ tags:
+ - coretime
+ summary: Get the generic information about coretime, either on coretime chain or relay chain.
+ description: ''
+ parameters:
+ - name: blockId
+ in: query
+ description: Block identifier, as the block height or block hash.
+ required: false
+ schema:
+ pattern: '^0[xX][0-9a-fA-F]{1,64}$|[0-9]{1,12}'
+ type: string
+ operationId: getCoretimeInfo
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - $ref: '#/components/schemas/CoretimeChainInfoResponse'
+ - $ref: '#/components/schemas/CoretimeRelayInfoResponse'
+ /coretime/overview:
+ get:
+ tags:
+ - coretime
+ summary: Get all the cores information either on coretime chain or relay chain.
+ description: ''
+ operationId: getCoretimeCores
+ parameters:
+ - name: blockId
+ in: query
+ description: Block identifier, as the block height or block hash.
+ required: false
+ schema:
+ pattern: '^0[xX][0-9a-fA-F]{1,64}$|[0-9]{1,12}'
+ type: string
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - $ref: '#/components/schemas/CoretimeChainCoresResponse'
+ - $ref: '#/components/schemas/CoretimeRelayCoresResponse'
/node/network:
get:
tags:
@@ -3036,6 +3173,359 @@ components:
type: array
items:
$ref: '#/components/schemas/Operation'
+ CoretimeRegionsResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ regions:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeRegion'
+ CoretimeLeasesResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ leases:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeLease'
+ CoretimeReservationsResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ reservations:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeReservation'
+ CoretimeRenewalsResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ renewals:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeRenewal'
+ CoretimeChainInfoResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ configuration:
+ $ref: '#/components/schemas/CoretimeConfig'
+ currentRegion:
+ type: object
+ properties:
+ start:
+ type: string
+ description: The start time.
+ end:
+ type: string
+ description: The end time.
+ cores:
+ type: object
+ properties:
+ total:
+ type: string
+ description: The total number of cores.
+ available:
+ type: string
+ description: The number of free cores.
+ sold:
+ type: string
+ description: The number of reserved cores.
+ currentCorePrice:
+ type: string
+ description: The current core price.
+ selloutPrice:
+ type: string
+ description: The sellout price.
+ firstCore:
+ type: string
+ description: The first core id.
+ CoretimeRelayInfoResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ brokerId:
+ type: string
+ description: The broker parachain id.
+ palletVersion:
+ type: string
+ description: The pallet version.
+ maxHistoricalRevenue:
+ type: string
+ description: The maximum historical revenue.
+ CoretimeChainCoresResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ cores:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeCore'
+ CoretimeRelayCoresResponse:
+ type: object
+ properties:
+ at:
+ $ref: '#/components/schemas/BlockIdentifiers'
+ cores:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeRelayCoreDescriptor'
+ CoretimeRelayCoreDescriptor:
+ type: object
+ properties:
+ paraId:
+ type: string
+ description: The parachain id.
+ type:
+ type: string
+ description: The parachain type.
+ info:
+ type: object
+ properties:
+ currentWork:
+ type: object
+ properties:
+ assignments:
+ type: array
+ items:
+ type: object
+ properties:
+ isPool:
+ type: boolean
+ description: Whether the workload is a pool.
+ isTask:
+ type: boolean
+ description: Whether the workload is a task.
+ ratio:
+ type: string
+ description: The ratio of the workload.
+ remaining:
+ type: string
+ description: The remaining workload.
+ task:
+ type: string
+ description: The parachain id.
+ endHint:
+ type: string
+ description: The end hint.
+ pos:
+ type: string
+ description: The position.
+ step:
+ type: string
+ description: The step.
+ queue:
+ type: object
+ properties:
+ first:
+ type: string
+ description: The first assignment in queue.
+ last:
+ type: string
+ description: The last assignment in queue.
+ CoretimeCore:
+ type: object
+ properties:
+ coreId:
+ type: string
+ description: The core id.
+ paraId:
+ type: string
+ description: The parachain core.
+ workload:
+ type: object
+ $ref: '#/components/schemas/CoretimeWorkload'
+ workplan:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeWorkplan'
+ type:
+ description: The paid price.
+ type: object
+ properties:
+ condition:
+ type: string
+ description: Type of assignment.
+ details:
+ type: object
+ oneOf:
+ - $ref: '#/components/schemas/CoretimeUntil'
+ - $ref: '#/components/schemas/CoretimeMask'
+ regions:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeRegion'
+ CoretimeConfig:
+ type: object
+ properties:
+ interludeLength:
+ type: string
+ description: The interlude length.
+ leadinLength:
+ type: string
+ description: The leadin length.
+ regionLength:
+ type: string
+ description: The region length.
+ relayBlocksPerTimeslice:
+ type: string
+ description: The number of relay chain blocks per timeslice.
+ CoretimeSaleInfo:
+ type: object
+ properties:
+ phase:
+ type: string
+ description: The phase of the sale.
+ saleStart:
+ type: string
+ description: The sale start.
+ leadinLength:
+ type: string
+ description: The leading length.
+ endPrice:
+ type: string
+ description: The end price.
+ regionBegin:
+ type: string
+ description: The region start time.
+ regionEnd:
+ type: string
+ description: The region end time.
+ idealCoresSold:
+ type: string
+ description: The ideal number of cores sold.
+ coresOffered:
+ type: string
+ description: The number of cores on sale.
+ firstCore:
+ type: string
+ description: The first core id.
+ selloutPrice:
+ type: string
+ description: The sellout price.
+ coresSold:
+ type: string
+ description: The number of cores sold.
+ CoretimeMask:
+ type: string
+ description: The mask.
+ CoretimeUntil:
+ type: string
+ description: The lease expiry time.
+ CoretimeWorkplan:
+ type: object
+ properties:
+ core:
+ type: string
+ description: The core id.
+ timeslice:
+ type: string
+ description: The timeslice.
+ info:
+ type: array
+ items:
+ $ref: '#/components/schemas/CoretimeWorkplanInfo'
+ CoretimeWorkplanInfo:
+ type: object
+ properties:
+ isPool:
+ type: boolean
+ description: Whether the workload is a pool.
+ isTask:
+ type: boolean
+ description: Whether the workload is a task.
+ mask:
+ type: string
+ description: The mask.
+ task:
+ type: string
+ description: The parachain id.
+ CoretimeWorkload:
+ type: object
+ properties:
+ isPool:
+ type: boolean
+ description: Whether the workload is a pool.
+ isTask:
+ type: boolean
+ description: Whether the workload is a task.
+ mask:
+ type: string
+ description: The mask.
+ task:
+ type: string
+ description: The parachain id.
+ CoretimeRegion:
+ type: object
+ properties:
+ core:
+ type: string
+ description: The core id.
+ begin:
+ type: string
+ description: The begin time.
+ end:
+ type: string
+ description: The end time.
+ owner:
+ type: string
+ description: The owner of the region.
+ paid:
+ type: string
+ description: The paid price.
+ mask:
+ type: string
+ description: The mask.
+ CoretimeLease:
+ type: object
+ properties:
+ task:
+ type: string
+ description: The parachain id.
+ until:
+ type: string
+ description: The lease expiry time.
+ core:
+ type: string
+ description: The core id.
+ CoretimeReservation:
+ type: object
+ properties:
+ task:
+ type: string
+ description: The parachain id.
+ mask:
+ type: string
+ description: The mask.
+ CoretimeRenewal:
+ type: object
+ properties:
+ completion:
+ type: string
+ description: The completion status.
+ core:
+ type: string
+ description: The core id.
+ mask:
+ type: string
+ description: The mask.
+ price:
+ type: string
+ description: The renewal price.
+ task:
+ type: string
+ description: The parachain id.
+ when:
+ type: string
+ description: The renewal time.
BlockWithDecodedXcmMsgs:
allOf:
- $ref: "#/components/schemas/Block"
diff --git a/src/chains-config/coretimeControllers.ts b/src/chains-config/coretimeControllers.ts
new file mode 100644
index 000000000..009b298e4
--- /dev/null
+++ b/src/chains-config/coretimeControllers.ts
@@ -0,0 +1,54 @@
+// Copyright 2017-2024 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import { ControllerConfig } from '../types/chains-config';
+import { initLRUCache, QueryFeeDetailsCache } from './cache';
+
+/**
+ * Polkadot configuration for Sidecar.
+ */
+export const coretimeControllers: ControllerConfig = {
+ controllers: [
+ 'AccountsBalanceInfo',
+ 'AccountsConvert',
+ 'AccountsProxyInfo',
+ 'Blocks',
+ 'BlocksExtrinsics',
+ 'BlocksTrace',
+ 'BlocksRawExtrinsics',
+ 'NodeNetwork',
+ 'NodeVersion',
+ 'PalletsConsts',
+ 'PalletsErrors',
+ 'PalletsEvents',
+ 'PalletsNominationPools',
+ 'PalletsOnGoingReferenda',
+ 'PalletsStakingProgress',
+ 'PalletsStakingValidators',
+ 'PalletsStorage',
+ 'RuntimeCode',
+ 'RuntimeMetadata',
+ 'RuntimeSpec',
+ 'CoretimeGeneric',
+ 'CoretimeChain',
+ ],
+ options: {
+ finalizes: true,
+ minCalcFeeRuntime: 0,
+ blockStore: initLRUCache(),
+ hasQueryFeeApi: new QueryFeeDetailsCache(27, 28),
+ },
+};
diff --git a/src/chains-config/index.ts b/src/chains-config/index.ts
index 323ec254a..0ca8da767 100644
--- a/src/chains-config/index.ts
+++ b/src/chains-config/index.ts
@@ -28,6 +28,7 @@ import { astarControllers } from './astarControllers';
import { bifrostControllers } from './bifrostControllers';
import { bifrostPolkadotControllers } from './bifrostPolkadotControllers';
import { calamariControllers } from './calamariControllers';
+import { coretimeControllers } from './coretimeControllers';
import { crustControllers } from './crustControllers';
import { defaultControllers } from './defaultControllers';
import { dockMainnetControllers } from './dockMainnetControllers';
@@ -77,6 +78,9 @@ const specToControllerMap: { [x: string]: ControllerConfig } = {
bifrost_polkadot: bifrostPolkadotControllers,
heiko: heikoControllers,
parallel: parallelControllers,
+ 'coretime-westend': coretimeControllers,
+ 'coretime-polkadot': coretimeControllers,
+ 'coretime-kusama': coretimeControllers,
};
/**
diff --git a/src/chains-config/kusamaControllers.ts b/src/chains-config/kusamaControllers.ts
index adf588156..cfb78fb22 100644
--- a/src/chains-config/kusamaControllers.ts
+++ b/src/chains-config/kusamaControllers.ts
@@ -53,6 +53,7 @@ export const kusamaControllers: ControllerConfig = {
'TransactionFeeEstimate',
'TransactionMaterial',
'TransactionSubmit',
+ 'CoretimeGeneric',
],
options: {
finalizes: true,
diff --git a/src/chains-config/westendControllers.ts b/src/chains-config/westendControllers.ts
index 656a12df7..13c26a5ef 100644
--- a/src/chains-config/westendControllers.ts
+++ b/src/chains-config/westendControllers.ts
@@ -52,6 +52,7 @@ export const westendControllers: ControllerConfig = {
'TransactionFeeEstimate',
'TransactionMaterial',
'TransactionSubmit',
+ 'CoretimeGeneric',
],
options: {
finalizes: true,
diff --git a/src/controllers/coretime/CoretimeChainController.ts b/src/controllers/coretime/CoretimeChainController.ts
new file mode 100644
index 000000000..121724729
--- /dev/null
+++ b/src/controllers/coretime/CoretimeChainController.ts
@@ -0,0 +1,61 @@
+// Copyright 2017-2024 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import { ApiPromise } from '@polkadot/api';
+import { RequestHandler } from 'express';
+
+import { CoretimeService } from '../../services';
+import AbstractController from '../AbstractController';
+
+export default class CoretimeChainController extends AbstractController {
+ constructor(api: ApiPromise) {
+ super(api, '/coretime', new CoretimeService(api));
+ this.initRoutes();
+ }
+
+ protected initRoutes(): void {
+ this.safeMountAsyncGetHandlers([
+ ['/leases', this.getLeases], // :taskId
+ ['/regions', this.getRegions],
+ ['/renewals', this.getRenewals],
+ ['/reservations', this.getReservations],
+ ]);
+ }
+
+ private getLeases: RequestHandler = async ({ query: { at } }, res): Promise => {
+ const hash = await this.getHashFromAt(at);
+
+ CoretimeChainController.sanitizedSend(res, await this.service.getCoretimeLeases(hash));
+ };
+
+ private getRegions: RequestHandler = async ({ query: { at } }, res): Promise => {
+ const hash = await this.getHashFromAt(at);
+
+ CoretimeChainController.sanitizedSend(res, await this.service.getCoretimeRegions(hash));
+ };
+
+ private getReservations: RequestHandler = async ({ query: { at } }, res): Promise => {
+ const hash = await this.getHashFromAt(at);
+
+ CoretimeChainController.sanitizedSend(res, await this.service.getCoretimeReservations(hash));
+ };
+
+ private getRenewals: RequestHandler = async ({ query: { at } }, res): Promise => {
+ const hash = await this.getHashFromAt(at);
+
+ CoretimeChainController.sanitizedSend(res, await this.service.getCoretimeRenewals(hash));
+ };
+}
diff --git a/src/controllers/coretime/CoretimeGenericController.ts b/src/controllers/coretime/CoretimeGenericController.ts
new file mode 100644
index 000000000..0ef9eb22a
--- /dev/null
+++ b/src/controllers/coretime/CoretimeGenericController.ts
@@ -0,0 +1,47 @@
+// Copyright 2017-2024 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import { ApiPromise } from '@polkadot/api';
+import { RequestHandler } from 'express';
+
+import { CoretimeService } from '../../services';
+import AbstractController from '../AbstractController';
+
+export default class CoretimeGenericController extends AbstractController {
+ constructor(api: ApiPromise) {
+ super(api, '/coretime', new CoretimeService(api));
+ this.initRoutes();
+ }
+
+ protected initRoutes(): void {
+ this.safeMountAsyncGetHandlers([
+ ['/info', this.getCoretimeOverview],
+ ['/overview', this.getCoretimeCores],
+ ]);
+ }
+
+ private getCoretimeOverview: RequestHandler = async ({ query: { at } }, res): Promise => {
+ const hash = await this.getHashFromAt(at);
+
+ CoretimeGenericController.sanitizedSend(res, await this.service.getCoretimeInfo(hash));
+ };
+
+ private getCoretimeCores: RequestHandler = async ({ query: { at } }, res): Promise => {
+ const hash = await this.getHashFromAt(at);
+
+ CoretimeGenericController.sanitizedSend(res, await this.service.getCoretimeCores(hash));
+ };
+}
diff --git a/src/controllers/coretime/index.ts b/src/controllers/coretime/index.ts
new file mode 100644
index 000000000..8b8b9df73
--- /dev/null
+++ b/src/controllers/coretime/index.ts
@@ -0,0 +1,18 @@
+// Copyright 2017-2024 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+export { default as CoretimeChain } from './CoretimeChainController';
+export { default as CoretimeGeneric } from './CoretimeGenericController';
diff --git a/src/controllers/index.ts b/src/controllers/index.ts
index 1df8f5292..636162e6a 100644
--- a/src/controllers/index.ts
+++ b/src/controllers/index.ts
@@ -27,6 +27,7 @@ import {
} from './accounts';
import { Blocks, BlocksExtrinsics, BlocksRawExtrinsics, BlocksTrace } from './blocks';
import { ContractsInk } from './contracts';
+import { CoretimeChain, CoretimeGeneric } from './coretime';
import { NodeNetwork, NodeTransactionPool, NodeVersion } from './node';
import {
PalletsAssetConversion,
@@ -89,4 +90,6 @@ export const controllers = {
TransactionMaterial,
TransactionSubmit,
Paras,
+ CoretimeGeneric,
+ CoretimeChain,
};
diff --git a/src/services/coretime/CoretimeService.spec.ts b/src/services/coretime/CoretimeService.spec.ts
new file mode 100644
index 000000000..12f052346
--- /dev/null
+++ b/src/services/coretime/CoretimeService.spec.ts
@@ -0,0 +1,397 @@
+// Copyright 2017-2024 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import type { ApiPromise } from '@polkadot/api';
+import type { Hash } from '@polkadot/types/interfaces';
+
+import { kusamaCoretimeMetadata } from '../../test-helpers/metadata/coretimeKusamaMetadata';
+import { kusamaMetadataV1003003 } from '../../test-helpers/metadata/kusamaMetadataV1003003';
+import { createApiWithAugmentations, TypeFactory } from '../../test-helpers/typeFactory';
+import { blockHash22887036 } from '../test-helpers/mock';
+import {
+ mockCoreDescriptors,
+ mockLeases,
+ mockParasLifeCycles,
+ mockRegions,
+ mockReservations,
+ mockWorkloads,
+ mockWorkplans,
+ potentialRenewalsMocks,
+} from '../test-helpers/mock/coretime';
+import { blockHash26187139 } from '../test-helpers/mock/mockBlock26187139';
+import { mockKusamaCoretimeApiBlock26187139 } from '../test-helpers/mock/mockCoretimeChainApi';
+import { mockKusamaApiBlock26187139 } from '../test-helpers/mock/mockKusamaApiBlock26187139';
+import { CoretimeService } from './CoretimeService';
+
+const coretimeApi = createApiWithAugmentations(kusamaCoretimeMetadata);
+const kusamaApi = createApiWithAugmentations(kusamaMetadataV1003003);
+
+const coretimeTypeFactory = new TypeFactory(coretimeApi);
+const kusamaTypeFactory = new TypeFactory(kusamaApi);
+
+const regionsEntries = () =>
+ Promise.resolve().then(() =>
+ mockRegions.map((region) => {
+ const storageEntry = coretimeApi.query.broker.regions;
+ const key = coretimeTypeFactory.storageKey(region.key, 'PalletBrokerRegionId', storageEntry);
+ return [
+ key,
+ mockKusamaCoretimeApiBlock26187139.registry.createType('Option', region.value),
+ ];
+ }),
+ );
+
+const leases = () =>
+ Promise.resolve().then(() =>
+ mockLeases.map((lease) => {
+ return mockKusamaCoretimeApiBlock26187139.registry.createType('PalletBrokerLeaseRecordItem', lease);
+ }),
+ );
+
+const potentialRenewalsEntries = () =>
+ Promise.resolve().then(() =>
+ potentialRenewalsMocks.map((renewal) => {
+ const storageEntry = coretimeApi.query.broker.potentialRenewals;
+ const key = coretimeTypeFactory.storageKey(renewal.key, 'PalletBrokerPotentialRenewalId', storageEntry);
+ return [
+ key,
+ mockKusamaCoretimeApiBlock26187139.registry.createType(
+ 'Option',
+ renewal.value,
+ ),
+ ];
+ }),
+ );
+
+const workloadsEntries = () =>
+ Promise.resolve().then(() =>
+ mockWorkloads.map((workload) => {
+ const storageEntry = coretimeApi.query.broker.workload;
+ const key = coretimeTypeFactory.storageKey(workload.key, 'U32', storageEntry);
+ return [key, [mockKusamaCoretimeApiBlock26187139.registry.createType('PalletBrokerScheduleItem', workload)]];
+ }),
+ );
+
+const parasLifeCyclesEntries = () =>
+ Promise.resolve().then(() =>
+ mockParasLifeCycles.map((parasLifeCycle) => {
+ const storageEntry = kusamaApi.query.paras.paraLifecycles;
+ const key = kusamaTypeFactory.storageKey(parasLifeCycle.key, 'U32', storageEntry);
+ return [
+ key,
+ mockKusamaApiBlock26187139.registry.createType(
+ 'Option',
+ parasLifeCycle.value,
+ ),
+ ];
+ }),
+ );
+
+const coreDescriptorsEntries = () =>
+ Promise.resolve().then(() => {
+ return mockCoreDescriptors.map((coreDescriptor) => {
+ const storageEntry = kusamaApi.query.coretimeAssignmentProvider.coreDescriptors;
+ const key = kusamaTypeFactory.storageKey(coreDescriptor.key, 'U32', storageEntry);
+
+ const currentWork = mockKusamaApiBlock26187139.registry.createType(
+ 'Option',
+ coreDescriptor.value.currentWork,
+ );
+
+ const queue = mockKusamaApiBlock26187139.registry.createType(
+ 'Option',
+ coreDescriptor.value.queue,
+ );
+
+ return [
+ key,
+ mockKusamaApiBlock26187139.registry.createType('PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor', {
+ ...coreDescriptor.value,
+ currentWork,
+ queue,
+ }),
+ ];
+ });
+ });
+
+const coreSchedulesEntries = () =>
+ Promise.resolve().then(() => {
+ return [];
+ });
+
+const workplanEntries = () =>
+ Promise.resolve().then(() =>
+ mockWorkplans.map((workplan) => {
+ const storageEntry = coretimeApi.query.broker.workplan;
+ const key = coretimeTypeFactory.storageKey(workplan.key, 'StorageKey', storageEntry);
+ return [
+ key,
+ mockKusamaCoretimeApiBlock26187139.registry.createType('Option>', workplan.value),
+ ];
+ }),
+ );
+
+const workplanMultiEntries = () =>
+ Promise.resolve().then(() => {
+ const storageEntry = coretimeApi.query.broker.workplan;
+ const key = coretimeTypeFactory.storageKey(mockWorkplans[0].key, 'StorageKey', storageEntry);
+ return [
+ key,
+ mockKusamaCoretimeApiBlock26187139.registry.createType(
+ 'Option>',
+ mockWorkplans[0].value,
+ ),
+ ];
+ });
+const mockKusamaApi = {
+ ...mockKusamaApiBlock26187139,
+ at: (_hash: Hash) => mockKusamaApi,
+ consts: {
+ ...mockKusamaApiBlock26187139.consts,
+ coretime: {
+ brokerId: 1,
+ },
+ onDemandAssignmentProvider: {
+ maxHistoricalRevenue: '50',
+ },
+ },
+ query: {
+ coretimeAssignmentProvider: {
+ coreSchedules: {
+ entries: coreSchedulesEntries,
+ },
+ coreDescriptors: {
+ entries: coreDescriptorsEntries,
+ },
+ palletVersion: () => Promise.resolve().then(() => '1'),
+ },
+ onDemandAssignmentProvider: {},
+ paras: {
+ paraLifecycles: {
+ entries: parasLifeCyclesEntries,
+ },
+ },
+ },
+} as unknown as ApiPromise;
+
+const mockCoretimeApi = {
+ ...mockKusamaCoretimeApiBlock26187139,
+ at: (_hash: Hash) => mockCoretimeApi,
+ consts: {
+ ...mockKusamaApiBlock26187139.consts,
+ broker: {
+ timeslicePeriod: mockKusamaCoretimeApiBlock26187139.registry.createType('U32', '80'),
+ },
+ },
+ query: {
+ broker: {
+ status: () =>
+ Promise.resolve().then(() =>
+ mockKusamaCoretimeApiBlock26187139.registry.createType('PalletBrokerStatusRecord', {
+ coreCount: 100,
+ privatePoolSize: 0,
+ systemPoolSize: 80,
+ lastCommittedTimeslice: 328585,
+ lastTimeslice: 328585,
+ }),
+ ),
+ configuration: () =>
+ Promise.resolve().then(() =>
+ mockKusamaCoretimeApiBlock26187139.registry.createType('Option', {
+ advanceNotice: 10,
+ interludeLength: 50400,
+ leadinLength: 50400,
+ regionLength: 5040,
+ idealBulkProportion: 1000000000,
+ limitCoresOffered: null,
+ renewalBump: 30000000,
+ contributionTimeout: 5040,
+ }),
+ ),
+ potentialRenewals: {
+ entries: potentialRenewalsEntries,
+ },
+ reservations: () =>
+ Promise.resolve().then(() =>
+ mockReservations.map((reservation) => {
+ return [mockKusamaCoretimeApiBlock26187139.registry.createType('PalletBrokerScheduleItem', reservation)];
+ }),
+ ),
+ leases: leases,
+ saleInfo: () =>
+ Promise.resolve().then(() =>
+ mockKusamaCoretimeApiBlock26187139.registry.createType('Option', {
+ saleStart: 1705849,
+ leadinLength: 50400,
+ endPrice: 776775851,
+ regionBegin: 331128,
+ regionEnd: 336168,
+ idealCoresSold: 81,
+ coresOffered: 81,
+ firstCore: 19,
+ selloutPrice: 32205681617,
+ coresSold: 69,
+ }),
+ ),
+ workplan: {
+ entries: workplanEntries,
+ },
+ workload: {
+ multi: workplanMultiEntries,
+ entries: workloadsEntries,
+ },
+ regions: {
+ entries: regionsEntries,
+ },
+ },
+ },
+} as unknown as ApiPromise;
+
+const CoretimeServiceAtCoretimeChain = new CoretimeService(mockCoretimeApi);
+
+const CoretimeServiceAtRelayChain = new CoretimeService(mockKusamaApi);
+
+describe('CoretimeService', () => {
+ describe('getRegions', () => {
+ it('should error with an invalid chain', async () => {
+ await expect(CoretimeServiceAtRelayChain.getCoretimeRegions(blockHash22887036)).rejects.toThrow(
+ 'This endpoint is only available on coretime chains.',
+ );
+ });
+ it('should return regions', async () => {
+ const regions = await CoretimeServiceAtCoretimeChain.getCoretimeRegions(blockHash26187139);
+ expect(regions.regions).toHaveLength(2);
+ expect(regions.at).toHaveProperty('hash');
+ expect(regions.regions[0]).toHaveProperty('begin');
+ expect(regions.regions[0]).toHaveProperty('end');
+ expect(regions.regions[0]).toHaveProperty('core');
+ expect(regions.regions[0]).toHaveProperty('owner');
+ expect(regions.regions[0]).toHaveProperty('paid');
+ });
+
+ it('should return empty array if no regions', () => {
+ return;
+ });
+ });
+
+ describe('getLeases', () => {
+ it('should error with an invalid chain', async () => {
+ await expect(CoretimeServiceAtRelayChain.getCoretimeRegions(blockHash22887036)).rejects.toThrow(
+ 'This endpoint is only available on coretime chains.',
+ );
+ });
+
+ it('should return leases', async () => {
+ const leases = await CoretimeServiceAtCoretimeChain.getCoretimeLeases(blockHash26187139);
+
+ expect(leases.leases).toHaveLength(2);
+ expect(leases.at).toHaveProperty('hash');
+ expect(leases.leases[0]).toHaveProperty('task');
+ expect(leases.leases[0]).toHaveProperty('until');
+ });
+ });
+
+ describe('getReservations', () => {
+ it('should error with an invalid chain', async () => {
+ await expect(CoretimeServiceAtRelayChain.getCoretimeRegions(blockHash22887036)).rejects.toThrow(
+ 'This endpoint is only available on coretime chains.',
+ );
+ });
+
+ it('should return reservations', async () => {
+ const reservations = await CoretimeServiceAtCoretimeChain.getCoretimeReservations(blockHash26187139);
+ expect(reservations.reservations).toHaveLength(3);
+ expect(reservations.at).toHaveProperty('hash');
+ expect(reservations.reservations[0]).toHaveProperty('mask');
+ expect(reservations.reservations[0]).toHaveProperty('task');
+ });
+ });
+
+ describe('getRenewals', () => {
+ it('should error with an invalid chain', async () => {
+ await expect(CoretimeServiceAtRelayChain.getCoretimeRegions(blockHash22887036)).rejects.toThrow(
+ 'This endpoint is only available on coretime chains.',
+ );
+ });
+
+ it('should return renewals', async () => {
+ const renewals = await CoretimeServiceAtCoretimeChain.getCoretimeRenewals(blockHash26187139);
+ expect(renewals.renewals).toHaveLength(2);
+ expect(renewals.at).toHaveProperty('hash');
+ expect(renewals.renewals[0]).toHaveProperty('core');
+ expect(renewals.renewals[0]).toHaveProperty('price');
+ expect(renewals.renewals[0]).toHaveProperty('task');
+ expect(renewals.renewals[0]).toHaveProperty('when');
+ });
+ });
+
+ describe('getInfo', () => {
+ it('should return info data for relay chain coretime', async () => {
+ const info = await CoretimeServiceAtRelayChain.getCoretimeInfo(blockHash22887036);
+ expect(info).toHaveProperty('at');
+ expect(info).toHaveProperty('brokerId');
+
+ if ('brokerId' in info) {
+ expect(info.brokerId).not.toBeNull();
+ expect(info).toHaveProperty('palletVersion');
+ expect(info.palletVersion).not.toBeNull();
+ } else {
+ throw new Error('BrokerId is not present in the info object');
+ }
+ });
+
+ it('should return info data for coretime chain coretime', async () => {
+ const info = await CoretimeServiceAtCoretimeChain.getCoretimeInfo(blockHash26187139);
+ expect(info).toHaveProperty('at');
+ expect(info).toHaveProperty('configuration');
+ if ('configuration' in info) {
+ expect(info.configuration).not.toBeNull();
+ expect(info.configuration?.leadinLength).toBe(50400);
+ expect(info).toHaveProperty('currentRegion');
+ expect(info).toHaveProperty('cores');
+ expect(info).toHaveProperty('phase');
+ expect(info.currentRegion).not.toBeNull();
+ } else {
+ throw new Error('Configuration is not present in the info object');
+ }
+ });
+ });
+
+ describe('getCores', () => {
+ it('should get cores for coretime chain', async () => {
+ const cores = await CoretimeServiceAtCoretimeChain.getCoretimeCores(blockHash26187139);
+ expect(cores.cores).toHaveLength(2);
+ expect(cores.at).toHaveProperty('hash');
+ expect(cores.cores && cores.cores[0]).toHaveProperty('coreId');
+ expect(cores.cores && cores.cores[0]).toHaveProperty('regions');
+ expect(cores.cores && cores.cores[0]).toHaveProperty('paraId');
+ });
+
+ it('should get cores for relay chain', async () => {
+ const cores = await CoretimeServiceAtRelayChain.getCoretimeCores(blockHash26187139);
+ expect(cores.cores).toHaveLength(2);
+ expect(cores.at).toHaveProperty('hash');
+ expect(cores.cores && cores.cores[0]).toHaveProperty('paraId');
+ expect(cores.cores && cores.cores[0]).toHaveProperty('type');
+ expect(cores.cores && cores.cores[0]).toHaveProperty('info');
+ const coresData = cores.cores;
+ if (coresData && 'info' in coresData[0]) {
+ expect(coresData[0].info).toHaveProperty('currentWork');
+ expect(coresData[0].info.currentWork).toHaveProperty('assignments');
+ }
+ });
+ });
+});
diff --git a/src/services/coretime/CoretimeService.ts b/src/services/coretime/CoretimeService.ts
new file mode 100644
index 000000000..0e709df47
--- /dev/null
+++ b/src/services/coretime/CoretimeService.ts
@@ -0,0 +1,595 @@
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-unsafe-return */
+// Copyright 2017-2025 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import type { ApiDecoration, QueryableModuleStorage } from '@polkadot/api/types';
+import type { Option, StorageKey, U32 } from '@polkadot/types';
+import type { BlockHash, ParaId } from '@polkadot/types/interfaces';
+import type {
+ PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor,
+ PolkadotRuntimeParachainsParasParaLifecycle,
+} from '@polkadot/types/lookup';
+import { BN } from '@polkadot/util';
+
+import type {
+ ICoretimeChainInfo,
+ ICoretimeCores,
+ ICoretimeLeases,
+ ICoretimeRegions,
+ ICoretimeRelayInfo,
+ ICoretimeRenewals,
+ ICoretimeReservations,
+ LeaseWithCore,
+ TConfigInfo,
+ TCoreDescriptor,
+ TLeaseInfo,
+ TParaLifecycle,
+ TPotentialRenewalInfo,
+ TRegionInfo,
+ TReservationInfo,
+ TSaleInfo,
+ TStatusInfo,
+ TWorkloadInfo,
+ TWorkplanInfo,
+} from '../../types/responses';
+import { AbstractService } from '../AbstractService';
+import {
+ extractConfigInfo,
+ extractCoreDescriptorInfo,
+ extractLeaseInfo,
+ extractParachainLifecycleInfo,
+ extractPotentialRenewalInfo,
+ extractRegionInfo,
+ extractReservationInfo,
+ extractSaleInfo,
+ extractStatusInfo,
+ extractWorkloadInfo,
+ extractWorkplanInfo,
+ sortByCore,
+} from './util';
+
+enum ChainType {
+ Relay = 'Relay',
+ Parachain = 'Parachain',
+}
+
+const SCALE = new BN(10000);
+
+export class CoretimeService extends AbstractService {
+ private getAndDecodeRegions = async (api: ApiDecoration<'promise'>): Promise => {
+ const regions = await api.query.broker.regions.entries();
+ const regionsInfo = regions.map((region) => {
+ return extractRegionInfo([region[0], region[1]]);
+ });
+
+ return regionsInfo;
+ };
+
+ private getAndDecodeLeases = async (api: ApiDecoration<'promise'>): Promise => {
+ const leases = await api.query.broker.leases();
+ return leases.map((lease) => extractLeaseInfo(lease));
+ };
+
+ private getAndDecodeWorkload = async (api: ApiDecoration<'promise'>): Promise => {
+ const workloads = await api.query.broker.workload.entries();
+
+ return sortByCore(
+ workloads.map((workload) => {
+ return extractWorkloadInfo(workload[1], workload[0].args[0].toNumber());
+ }),
+ );
+ };
+
+ private getAndDecodeWorkplan = async (api: ApiDecoration<'promise'>): Promise => {
+ const workplans = await api.query.broker.workplan.entries();
+
+ const wplsInfo = sortByCore(
+ workplans.map(([key, val]) => {
+ const [timeslice, core] = key.args[0].map((a) => a.toNumber());
+ return extractWorkplanInfo(val, core, timeslice);
+ }),
+ );
+
+ return wplsInfo;
+ };
+
+ private getAndDecodeSaleInfo = async (api: ApiDecoration<'promise'>): Promise => {
+ const saleInfo = await api.query.broker.saleInfo();
+ return saleInfo.isSome ? extractSaleInfo(saleInfo.unwrap()) : null;
+ };
+
+ private getAndDecodeStatus = async (api: ApiDecoration<'promise'>): Promise => {
+ const status = await api.query.broker.status();
+
+ return extractStatusInfo(status);
+ };
+
+ private getAndDecodeConfiguration = async (api: ApiDecoration<'promise'>): Promise => {
+ const configuration = await api.query.broker.configuration();
+
+ return extractConfigInfo(configuration);
+ };
+
+ private getAndDecodePotentialRenewals = async (api: ApiDecoration<'promise'>): Promise => {
+ const potentialRenewals = await api.query.broker.potentialRenewals.entries();
+
+ const potentialRenewalsInfo = sortByCore(
+ potentialRenewals.map((renewal) => extractPotentialRenewalInfo(renewal[1], renewal[0])),
+ );
+
+ return potentialRenewalsInfo;
+ };
+
+ private getAndDecodeReservations = async (api: ApiDecoration<'promise'>): Promise => {
+ const reservations = await api.query.broker.reservations();
+
+ return reservations.map((res) => extractReservationInfo(res));
+ };
+
+ private getAndDecodeCoreSchedules = async (api: ApiDecoration<'promise'>): Promise[]> => {
+ const coreSchedules = await api.query.coretimeAssignmentProvider.coreSchedules.entries();
+ return coreSchedules as unknown as Record[];
+ };
+
+ private getAndDecodeCoreDescriptors = async (api: ApiDecoration<'promise'>): Promise => {
+ const coreDescriptors = await api.query.coretimeAssignmentProvider.coreDescriptors.entries();
+ const descriptors = coreDescriptors as unknown as [
+ StorageKey<[U32]>,
+ PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor,
+ ][];
+
+ return descriptors.map((descriptor) => extractCoreDescriptorInfo(descriptor[0], descriptor[1]));
+ };
+
+ private getAndDecodeParachainsLifecycle = async (api: ApiDecoration<'promise'>): Promise => {
+ const parachains = await api.query.paras.paraLifecycles.entries();
+ return parachains.map((para) =>
+ extractParachainLifecycleInfo(
+ para[0] as unknown as StorageKey<[ParaId]>,
+ para[1] as unknown as Option,
+ ),
+ );
+ };
+
+ private leadinFactorAt = (scaledWhen: BN): BN => {
+ const scaledHalf = SCALE.div(new BN(2)); // 0.5 scaled to 10000
+
+ if (scaledWhen.lte(scaledHalf)) {
+ // First half of the graph, steeper slope
+ return SCALE.mul(new BN(100)).sub(scaledWhen.mul(new BN(180)));
+ } else {
+ // Second half of the graph, flatter slope
+ return SCALE.mul(new BN(19)).sub(scaledWhen.mul(new BN(18)));
+ }
+ };
+
+ private getCorePriceAt = (blockNumber: number, saleInfo: TSaleInfo) => {
+ const { endPrice, leadinLength, saleStart } = saleInfo;
+ // Explicit conversion to BN
+ const blockNowBn = new BN(blockNumber);
+ const saleStartBn = new BN(saleStart);
+ const leadinLengthBn = new BN(leadinLength);
+
+ // Elapsed time since the start of the sale, constrained to not exceed the total lead-in period
+ const elapsedTimeSinceSaleStart = blockNowBn.sub(saleStartBn);
+ const cappedElapsedTime = elapsedTimeSinceSaleStart.lt(leadinLengthBn) ? elapsedTimeSinceSaleStart : leadinLengthBn;
+
+ const scaledProgress = cappedElapsedTime.mul(new BN(10000)).div(leadinLengthBn);
+
+ /**
+ * Progress is a normalized value between 0 and 1, where:
+ *
+ * 0 means the sale just started.
+ * 1 means the sale is at the end of the lead-in period.
+ *
+ * We are scaling it to avoid floating point precision issues.
+ */
+ const leadinFactor = this.leadinFactorAt(scaledProgress);
+ const scaledPrice = leadinFactor.mul(endPrice).div(SCALE);
+
+ return scaledPrice;
+ };
+
+ private getPhaseConfiguration = (
+ currentRegionStart: number,
+ regionLength: number,
+ interludeLengthTs: number,
+ leadInLengthTs: number,
+ lastCommittedTimeslice: number,
+ ): {
+ config: {
+ phaseName: string;
+ lastTimeslice: number;
+ }[];
+ currentPhaseName: string;
+ } => {
+ const renewalsEndTs = currentRegionStart + interludeLengthTs;
+ const priceDiscoveryEndTs = renewalsEndTs + leadInLengthTs;
+ const fixedPriceLenght = regionLength - interludeLengthTs - leadInLengthTs;
+ const fixedPriceEndTs = priceDiscoveryEndTs + fixedPriceLenght;
+
+ const progress = lastCommittedTimeslice - currentRegionStart;
+ let phaseName = 'fixedPrice';
+
+ if (progress < interludeLengthTs) {
+ phaseName = 'renewals';
+ }
+
+ if (progress < interludeLengthTs + leadInLengthTs) {
+ phaseName = 'priceDiscovery';
+ }
+
+ return {
+ config: [
+ {
+ phaseName: 'renewals',
+ lastTimeslice: renewalsEndTs,
+ },
+ {
+ phaseName: 'priceDiscovery',
+ lastTimeslice: priceDiscoveryEndTs,
+ },
+ {
+ phaseName: 'fixedPrice',
+ lastTimeslice: fixedPriceEndTs,
+ },
+ ],
+ currentPhaseName: phaseName,
+ };
+ };
+
+ private getCurrentRegionStartEndTs = (saleInfo: TSaleInfo, regionLength: number) => {
+ return {
+ currentRegionEnd: saleInfo.regionBegin,
+ currentRegionStart: saleInfo.regionBegin - regionLength,
+ };
+ };
+
+ async getCoretimeInfo(hash: BlockHash): Promise {
+ const { api } = this;
+
+ const [{ specName }, { number }, historicApi] = await Promise.all([
+ api.rpc.state.getRuntimeVersion(hash),
+ api.rpc.chain.getHeader(hash),
+ api.at(hash),
+ ]);
+
+ const blockNumber = number.unwrap();
+
+ if (this.getChainType(specName.toString()) === ChainType.Relay) {
+ this.assertCoretimeModule(historicApi, ChainType.Relay);
+
+ const [brokerId, maxHistoricalRevenue, palletVersion] = await Promise.all([
+ historicApi.consts.coretime.brokerId,
+ historicApi.consts.onDemandAssignmentProvider.maxHistoricalRevenue,
+ historicApi.query.coretimeAssignmentProvider.palletVersion(),
+ ]);
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ brokerId: brokerId as unknown as number,
+ palletVersion: palletVersion as unknown as number,
+ maxHistoricalRevenue: maxHistoricalRevenue as unknown as number,
+ };
+ } else {
+ this.assertCoretimeModule(historicApi, ChainType.Parachain);
+ const [config, saleInfo, timeslicePeriod, status] = await Promise.all([
+ this.getAndDecodeConfiguration(historicApi),
+ this.getAndDecodeSaleInfo(historicApi),
+ historicApi.consts.broker.timeslicePeriod,
+ this.getAndDecodeStatus(historicApi),
+ ]);
+
+ const blocksPerTimeslice = timeslicePeriod as unknown as U32;
+ const currentRegionStats = saleInfo && this.getCurrentRegionStartEndTs(saleInfo, config.regionLength);
+ const phaseConfig = this.getPhaseConfiguration(
+ currentRegionStats?.currentRegionStart || 0,
+ config.regionLength,
+ config.interludeLength,
+ saleInfo?.leadinLength || 0,
+ status.lastCommittedTimeslice || 0,
+ );
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ configuration: {
+ regionLength: config.regionLength,
+ interludeLength: config.interludeLength,
+ leadinLength: saleInfo?.leadinLength || 0,
+ relayBlocksPerTimeslice: blocksPerTimeslice.toNumber(),
+ },
+ currentRegion: {
+ start: currentRegionStats?.currentRegionStart || null,
+ end: currentRegionStats?.currentRegionEnd || null,
+ },
+ cores: {
+ available: Number(saleInfo?.coresOffered) - Number(saleInfo?.coresSold),
+ sold: Number(saleInfo?.coresSold),
+ total: Number(saleInfo?.coresOffered),
+ currentCorePrice: this.getCorePriceAt(blockNumber.toNumber(), saleInfo!),
+ selloutPrice: saleInfo?.selloutPrice,
+ firstCore: saleInfo?.firstCore,
+ },
+ phase: {
+ currentPhase: phaseConfig.currentPhaseName,
+ config: phaseConfig.config.map((c) => ({
+ phaseName: c.phaseName,
+ lastRelayBlock: c.lastTimeslice * blocksPerTimeslice.toNumber(),
+ lastTimeslice: c.lastTimeslice,
+ })),
+ },
+ };
+ }
+ }
+
+ async getCoretimeLeases(hash: BlockHash): Promise {
+ const { api } = this;
+
+ const [{ specName }, { number }, historicApi] = await Promise.all([
+ api.rpc.state.getRuntimeVersion(hash),
+ api.rpc.chain.getHeader(hash),
+ api.at(hash),
+ ]);
+
+ const blockNumber = number.unwrap();
+
+ if (this.getChainType(specName.toString()) === ChainType.Relay) {
+ throw new Error('This endpoint is only available on coretime chains.');
+ } else {
+ this.assertCoretimeModule(historicApi, ChainType.Parachain);
+ const [leases, workload] = await Promise.all([
+ this.getAndDecodeLeases(historicApi),
+ this.getAndDecodeWorkload(historicApi),
+ ]);
+
+ const leasesWithCore: LeaseWithCore[] = leases.reduce((acc: LeaseWithCore[], lease) => {
+ const core = workload.find((wl) => wl.info.task.includes(lease.task))?.core;
+ return [
+ ...acc,
+ {
+ ...lease,
+ core: core!,
+ },
+ ];
+ }, []);
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ leases: sortByCore(leasesWithCore),
+ };
+ }
+ }
+
+ async getCoretimeRegions(hash: BlockHash): Promise {
+ const { api } = this;
+
+ const [{ specName }, { number }, historicApi] = await Promise.all([
+ api.rpc.state.getRuntimeVersion(hash),
+ api.rpc.chain.getHeader(hash),
+ api.at(hash),
+ ]);
+
+ const blockNumber = number.unwrap();
+
+ if (this.getChainType(specName.toString()) === ChainType.Relay) {
+ throw new Error('This endpoint is only available on coretime chains.');
+ } else {
+ this.assertCoretimeModule(historicApi, ChainType.Parachain);
+
+ const regions = await this.getAndDecodeRegions(historicApi);
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ regions: sortByCore(regions),
+ };
+ }
+ }
+
+ async getCoretimeReservations(hash: BlockHash): Promise {
+ const { api } = this;
+
+ const [{ specName }, { number }, historicApi] = await Promise.all([
+ api.rpc.state.getRuntimeVersion(hash),
+ api.rpc.chain.getHeader(hash),
+ api.at(hash),
+ ]);
+
+ const blockNumber = number.unwrap();
+
+ if (this.getChainType(specName.toString()) === ChainType.Relay) {
+ throw new Error('This endpoint is only available on coretime chains.');
+ } else {
+ // coretime chain or parachain
+ this.assertCoretimeModule(historicApi, ChainType.Parachain);
+
+ const reservations = await this.getAndDecodeReservations(historicApi);
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ reservations,
+ };
+ }
+ }
+
+ async getCoretimeRenewals(hash: BlockHash): Promise {
+ const { api } = this;
+
+ const [{ specName }, { number }, historicApi] = await Promise.all([
+ api.rpc.state.getRuntimeVersion(hash),
+ api.rpc.chain.getHeader(hash),
+ api.at(hash),
+ ]);
+
+ const blockNumber = number.unwrap();
+
+ if (this.getChainType(specName.toString()) === ChainType.Relay) {
+ throw new Error('This endpoint is only available on coretime chains.');
+ } else {
+ this.assertCoretimeModule(historicApi, ChainType.Parachain);
+
+ const renewals = await this.getAndDecodePotentialRenewals(historicApi);
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ renewals: sortByCore(renewals),
+ };
+ }
+ }
+
+ async getCoretimeCores(hash: BlockHash): Promise {
+ const { api } = this;
+
+ const [{ specName }, { number }, historicApi] = await Promise.all([
+ api.rpc.state.getRuntimeVersion(hash),
+ api.rpc.chain.getHeader(hash),
+ api.at(hash),
+ ]);
+
+ const blockNumber = number.unwrap();
+
+ if (this.getChainType(specName.toString()) === ChainType.Relay) {
+ this.assertCoretimeModule(historicApi, ChainType.Relay);
+ const [parachains, schedules, descriptors] = await Promise.all([
+ this.getAndDecodeParachainsLifecycle(historicApi),
+ this.getAndDecodeCoreSchedules(historicApi),
+ this.getAndDecodeCoreDescriptors(historicApi),
+ ]);
+
+ const descriptorsWithParas = parachains.reduce<(TParaLifecycle & TCoreDescriptor)[]>((acc, para) => {
+ const core = descriptors.find((f) => {
+ const assignments = f.info.currentWork.assignments.find((assgn) => assgn.task === para.paraId.toString());
+ return !!assignments;
+ });
+ if (core) {
+ acc.push({
+ ...para,
+ ...core,
+ });
+ }
+ return acc;
+ }, []);
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ cores: descriptorsWithParas,
+ coreSchedules: schedules,
+ };
+ } else {
+ this.assertCoretimeModule(historicApi, ChainType.Parachain);
+ const [workload, workplan, leases, reservations, regions] = await Promise.all([
+ this.getAndDecodeWorkload(historicApi),
+ this.getAndDecodeWorkplan(historicApi),
+ this.getAndDecodeLeases(historicApi),
+ this.getAndDecodeReservations(historicApi),
+ this.getAndDecodeRegions(historicApi),
+ ]);
+
+ const systemParas = reservations.map((el) => el.task);
+ const cores = workload.map((wl) => {
+ const coreType = systemParas.includes(wl.info.task)
+ ? wl.info.task === 'Pool'
+ ? 'ondemand'
+ : 'reservation'
+ : leases.map((f) => f.task).includes(wl.info.task)
+ ? 'lease'
+ : 'bulk';
+
+ let details = undefined;
+
+ if (coreType === 'reservation') {
+ details = { mask: reservations.find((f) => f.task === wl.info.task)?.mask };
+ } else if (coreType === 'lease') {
+ details = { until: leases.find((f) => f.task === wl.info.task)?.until };
+ }
+
+ const coreRegions = regions.filter((region) => region.core === wl.core);
+ return {
+ coreId: wl.core,
+ paraId: wl.info.task,
+ workload: wl.info,
+ workplan: workplan.filter((f) => f.core === wl.core),
+ type: { condition: coreType, details },
+ regions: coreRegions,
+ };
+ });
+
+ return {
+ at: {
+ hash,
+ height: blockNumber.toString(10),
+ },
+ cores,
+ };
+ }
+ }
+
+ /**
+ * Coretime pallets and modules are not available on all runtimes. This
+ * verifies that by checking if the module exists. If it doesnt it will throw an error
+ *
+ * @param queryFn The QueryModuleStorage key that we want to check exists
+ * @param mod Module we are checking
+ */
+ private assertQueryModule(queryFn: QueryableModuleStorage<'promise'>, mod: string): void {
+ if (!queryFn) {
+ throw Error(`The runtime does not include the ${mod} module at this block`);
+ }
+ }
+
+ private getChainType(specName: string): ChainType {
+ const relay = ['polkadot', 'kusama', 'westend', 'paseo'];
+ if (relay.includes(specName.toLowerCase())) {
+ return ChainType.Relay;
+ } else {
+ return ChainType.Parachain;
+ }
+ }
+
+ private assertCoretimeModule = (api: ApiDecoration<'promise'>, chainType: ChainType) => {
+ if (chainType === ChainType.Relay) {
+ this.assertQueryModule(api.query.onDemandAssignmentProvider, 'onDemandAssignmentProvider');
+ this.assertQueryModule(api.query.coretimeAssignmentProvider, 'coretimeAssignmentProvider');
+ return;
+ } else if (chainType === ChainType.Parachain) {
+ this.assertQueryModule(api.query.broker, 'broker');
+ return;
+ }
+ throw new Error('Unsupported network type.');
+ };
+}
diff --git a/src/services/coretime/index.ts b/src/services/coretime/index.ts
new file mode 100644
index 000000000..2571e92cd
--- /dev/null
+++ b/src/services/coretime/index.ts
@@ -0,0 +1,17 @@
+// Copyright 2017-2024 Parity Technologies (UK) Ltd.
+// This file is part of Substrate API Sidecar.
+//
+// Substrate API Sidecar is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+export * from './CoretimeService';
diff --git a/src/services/coretime/util.ts b/src/services/coretime/util.ts
new file mode 100644
index 000000000..102f4e12f
--- /dev/null
+++ b/src/services/coretime/util.ts
@@ -0,0 +1,262 @@
+import { StorageKey } from '@polkadot/types';
+import { ParaId } from '@polkadot/types/interfaces';
+import {
+ PalletBrokerConfigRecord,
+ PalletBrokerCoretimeInterfaceCoreAssignment,
+ PalletBrokerLeaseRecordItem,
+ PalletBrokerPotentialRenewalId,
+ PalletBrokerPotentialRenewalRecord,
+ PalletBrokerRegionId,
+ PalletBrokerRegionRecord,
+ PalletBrokerSaleInfoRecord,
+ PalletBrokerScheduleItem,
+ PalletBrokerStatusRecord,
+ PolkadotRuntimeParachainsAssignerCoretimeAssignmentState,
+ PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor,
+ PolkadotRuntimeParachainsAssignerCoretimeQueueDescriptor,
+ PolkadotRuntimeParachainsAssignerCoretimeWorkState,
+ PolkadotRuntimeParachainsParasParaLifecycle,
+} from '@polkadot/types/lookup';
+import { Option, Vec } from '@polkadot/types-codec';
+import { AnyTuple } from '@polkadot/types-codec/types';
+import { BN } from '@polkadot/util';
+
+import {
+ TConfigInfo,
+ TCoreDescriptor,
+ TLeaseInfo,
+ TParaLifecycle,
+ TPotentialRenewalInfo,
+ TRegionInfo,
+ TReservationInfo,
+ TSaleInfo,
+ TStatusInfo,
+ TWorkloadInfo,
+ TWorkplanInfo,
+} from '../../types/responses';
+
+export function sortByCore(dataArray?: T | T[]): T[] {
+ if (!dataArray) {
+ return [];
+ }
+
+ const sanitized = Array.isArray(dataArray) ? dataArray : [dataArray];
+
+ return sanitized.sort((a, b) => a.core - b.core);
+}
+
+export function hexToBin(hex: string): string {
+ return parseInt(hex, 16).toString(2);
+}
+
+export function processHexMask(mask: PalletBrokerScheduleItem['mask'] | undefined): string[] {
+ if (!mask) {
+ return [];
+ }
+
+ const trimmedHex: string = mask.toHex().slice(2);
+ const arr: string[] = trimmedHex.split('');
+ const buffArr: string[] = [];
+
+ arr.forEach((bit) => {
+ hexToBin(bit)
+ .split('')
+ .forEach((v) => buffArr.push(v));
+ });
+ buffArr.filter((v) => v === '1');
+
+ return buffArr;
+}
+
+export function extractWorkloadInfo(info: Vec, core: number): TWorkloadInfo {
+ return {
+ core,
+ info: info.map((c) => ({
+ isPool: c.assignment.isPool,
+ isTask: c.assignment.isTask,
+ mask: c.mask.toHex(),
+ task: c.assignment.isTask ? c.assignment.asTask.toString() : c.assignment.isPool ? 'Pool' : 'Idle',
+ }))[0],
+ };
+}
+
+export function extractWorkplanInfo(
+ info: Option>,
+ core: number,
+ timeslice: number,
+): TWorkplanInfo {
+ const workplanInfo = info.isSome ? info.unwrap() : [];
+
+ return {
+ core: core,
+ timeslice: timeslice,
+ info: workplanInfo?.map((c) => ({
+ mask: c.mask.toHex(),
+ isPool: c.assignment.isPool,
+ isTask: c.assignment.isTask,
+ task: c.assignment.isTask ? c.assignment.asTask.toString() : c.assignment.isPool ? 'Pool' : 'Idle',
+ })),
+ };
+}
+
+export function extractReservationInfo(info: PalletBrokerScheduleItem[]): TReservationInfo {
+ return {
+ mask: info[0]?.mask.toHex(),
+ task: info[0]?.assignment?.isTask
+ ? info[0]?.assignment?.asTask.toString()
+ : info[0]?.assignment?.isPool
+ ? 'Pool'
+ : '',
+ };
+}
+
+export function extractPotentialRenewalInfo(
+ info: Option,
+ item: StorageKey<[PalletBrokerPotentialRenewalId]>,
+): TPotentialRenewalInfo {
+ const unwrapped: PalletBrokerPotentialRenewalRecord | null = info.isSome ? info.unwrap() : null;
+ let mask: string | null = null;
+ let task = '';
+
+ 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 = complete.mask.toHex();
+ } else if (completion?.isPartial) {
+ mask = completion?.asPartial.toHex();
+ task = '';
+ } else {
+ mask = null;
+ }
+
+ return {
+ completion: completion?.type,
+ core: item.args[0].core.toNumber(),
+ mask,
+ price: unwrapped?.price.toBn(),
+ task,
+ when: item.args[0].when.toNumber(),
+ };
+}
+
+export function extractLeaseInfo(info: PalletBrokerLeaseRecordItem, core?: number): TLeaseInfo {
+ return {
+ ...(core ? { core } : {}),
+ task: info.task.toString(),
+ until: info.until.toNumber(),
+ };
+}
+
+export function extractSaleInfo(info: PalletBrokerSaleInfoRecord): TSaleInfo {
+ return {
+ saleStart: info.saleStart.toNumber(),
+ leadinLength: info.leadinLength.toNumber(),
+ endPrice: info.endPrice.toBn(),
+ regionBegin: info.regionBegin.toNumber(),
+ regionEnd: info.regionEnd.toNumber(),
+ idealCoresSold: info.idealCoresSold.toNumber(),
+ coresOffered: info.coresOffered.toNumber(),
+ firstCore: info.firstCore.toNumber(),
+ selloutPrice: info.selloutPrice.isSome ? info.selloutPrice.unwrapOr(undefined)?.toBn() : undefined,
+ coresSold: info.coresSold.toNumber(),
+ };
+}
+
+export function extractStatusInfo(info: Option): TStatusInfo {
+ const unwrapped: PalletBrokerStatusRecord | null = info.isSome ? info.unwrap() : null;
+
+ return {
+ coreCount: unwrapped?.coreCount.toNumber(),
+ privatePoolSize: unwrapped?.privatePoolSize.toNumber(),
+ systemPoolSize: unwrapped?.systemPoolSize.toNumber(),
+ lastCommittedTimeslice: unwrapped?.lastCommittedTimeslice.toNumber(),
+ lastTimeslice: unwrapped?.lastTimeslice.toNumber(),
+ };
+}
+
+export function extractRegionInfo(
+ info: [StorageKey<[PalletBrokerRegionId]>, Option],
+): TRegionInfo {
+ const regionInfo = info[0].args[0];
+ const value = info[1].isSome ? info[1].unwrap() : null;
+ return {
+ core: regionInfo.core.toNumber(),
+ begin: regionInfo.begin.toNumber(),
+ end: value?.end.toNumber(),
+ owner: value?.owner.toString(),
+ paid: value?.paid.isSome ? value?.paid.unwrap().toNumber() : undefined,
+ mask: regionInfo.mask.toHex(),
+ };
+}
+
+export function extractConfigInfo(info: Option): TConfigInfo {
+ return {
+ advanceNotice: info.unwrap().advanceNotice.toNumber(),
+ interludeLength: info.unwrap().interludeLength.toNumber(),
+ leadinLength: info.unwrap().leadinLength.toNumber(),
+ regionLength: info.unwrap().regionLength.toNumber(),
+ idealBulkProportion: info.unwrap().idealBulkProportion.toNumber(),
+ limitCoresOffered: info.unwrap().limitCoresOffered.unwrapOr(undefined)?.toNumber(),
+ renewalBump: info.unwrap().renewalBump.toNumber(),
+ contributionTimeout: info.unwrap().contributionTimeout.toNumber(),
+ };
+}
+
+export function extractCoreDescriptorInfo(
+ _key: StorageKey,
+ info: PolkadotRuntimeParachainsAssignerCoretimeCoreDescriptor,
+): TCoreDescriptor {
+ 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 || [];
+ return {
+ info: {
+ currentWork: {
+ assignments: assignments?.map(
+ (
+ assgn: [
+ PalletBrokerCoretimeInterfaceCoreAssignment,
+ PolkadotRuntimeParachainsAssignerCoretimeAssignmentState,
+ ],
+ ) => {
+ return {
+ isPool: assgn[0]?.isPool,
+ isTask: assgn[0]?.isTask,
+ ratio: assgn[1]?.ratio.toNumber(),
+ remaining: assgn[1]?.remaining.toNumber(),
+ task: assgn[0]?.isTask ? assgn[0]?.asTask.toString() : assgn[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),
+ },
+ },
+ };
+}
+
+export function extractParachainLifecycleInfo(
+ key: StorageKey<[ParaId]>,
+ val: Option,
+): TParaLifecycle {
+ return {
+ paraId: key.args[0].toNumber(),
+ type: val.isSome ? val.unwrap().toString() : null,
+ };
+}
diff --git a/src/services/index.ts b/src/services/index.ts
index fe17a3016..82f0eb243 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -17,6 +17,7 @@
export * from './accounts';
export * from './blocks';
export * from './contracts';
+export * from './coretime';
export * from './node';
export * from './pallets';
export * from './paras';
diff --git a/src/services/paras/ParasService.ts b/src/services/paras/ParasService.ts
index 3ade6e237..444ddc42b 100644
--- a/src/services/paras/ParasService.ts
+++ b/src/services/paras/ParasService.ts
@@ -65,7 +65,6 @@ export class ParasService extends AbstractService {
const historicApi = await api.at(hash);
this.assertQueryModule(historicApi.query.crowdloan, 'crowdloan');
-
const [fund, { number }] = await Promise.all([
historicApi.query.crowdloan.funds