@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
458 lines • 21.4 kB
JavaScript
"use strict";
/* 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 <http://www.gnu.org/licenses/>.
Object.defineProperty(exports, "__esModule", { value: true });
exports.CoretimeService = void 0;
const util_1 = require("@polkadot/util");
const AbstractService_1 = require("../AbstractService");
const util_2 = require("./util");
var ChainType;
(function (ChainType) {
ChainType["Relay"] = "Relay";
ChainType["Parachain"] = "Parachain";
})(ChainType || (ChainType = {}));
const SCALE = new util_1.BN(10000);
class CoretimeService extends AbstractService_1.AbstractService {
constructor() {
super(...arguments);
this.getAndDecodeRegions = async (api) => {
const regions = await api.query.broker.regions.entries();
const regionsInfo = regions.map((region) => {
return (0, util_2.extractRegionInfo)([region[0], region[1]]);
});
return regionsInfo;
};
this.getAndDecodeLeases = async (api) => {
const leases = await api.query.broker.leases();
return leases.map((lease) => (0, util_2.extractLeaseInfo)(lease));
};
this.getAndDecodeWorkload = async (api) => {
const workloads = await api.query.broker.workload.entries();
return (0, util_2.sortByCore)(workloads.map((workload) => {
return (0, util_2.extractWorkloadInfo)(workload[1], workload[0].args[0].toNumber());
}));
};
this.getAndDecodeWorkplan = async (api) => {
const workplans = await api.query.broker.workplan.entries();
const wplsInfo = (0, util_2.sortByCore)(workplans.map(([key, val]) => {
const [timeslice, core] = key.args[0].map((a) => a.toNumber());
return (0, util_2.extractWorkplanInfo)(val, core, timeslice);
}));
return wplsInfo;
};
this.getAndDecodeSaleInfo = async (api) => {
const saleInfo = await api.query.broker.saleInfo();
return saleInfo.isSome ? (0, util_2.extractSaleInfo)(saleInfo.unwrap()) : null;
};
this.getAndDecodeStatus = async (api) => {
const status = await api.query.broker.status();
return (0, util_2.extractStatusInfo)(status);
};
this.getAndDecodeConfiguration = async (api) => {
const configuration = await api.query.broker.configuration();
return (0, util_2.extractConfigInfo)(configuration);
};
this.getAndDecodePotentialRenewals = async (api) => {
const potentialRenewals = await api.query.broker.potentialRenewals.entries();
const potentialRenewalsInfo = (0, util_2.sortByCore)(potentialRenewals.map((renewal) => (0, util_2.extractPotentialRenewalInfo)(renewal[1], renewal[0])));
return potentialRenewalsInfo;
};
this.getAndDecodeReservations = async (api) => {
const reservations = await api.query.broker.reservations();
return reservations.map((res) => (0, util_2.extractReservationInfo)(res));
};
this.getAndDecodeCoreSchedules = async (api) => {
const coreSchedules = await api.query.coretimeAssignmentProvider.coreSchedules.entries();
return coreSchedules;
};
this.getAndDecodeCoreDescriptors = async (api) => {
const coreDescriptors = await api.query.coretimeAssignmentProvider.coreDescriptors.entries();
const descriptors = coreDescriptors;
return descriptors.map((descriptor) => (0, util_2.extractCoreDescriptorInfo)(descriptor[0], descriptor[1]));
};
this.getAndDecodeParachainsLifecycle = async (api) => {
const parachains = await api.query.paras.paraLifecycles.entries();
return parachains.map((para) => (0, util_2.extractParachainLifecycleInfo)(para[0], para[1]));
};
this.leadinFactorAt = (scaledWhen) => {
const scaledHalf = SCALE.div(new util_1.BN(2)); // 0.5 scaled to 10000
if (scaledWhen.lte(scaledHalf)) {
// First half of the graph, steeper slope
return SCALE.mul(new util_1.BN(100)).sub(scaledWhen.mul(new util_1.BN(180)));
}
else {
// Second half of the graph, flatter slope
return SCALE.mul(new util_1.BN(19)).sub(scaledWhen.mul(new util_1.BN(18)));
}
};
this.getCorePriceAt = (blockNumber, saleInfo) => {
const { endPrice, leadinLength, saleStart } = saleInfo;
// Explicit conversion to BN
const blockNowBn = new util_1.BN(blockNumber);
const saleStartBn = new util_1.BN(saleStart);
const leadinLengthBn = new util_1.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 util_1.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;
};
this.getPhaseConfiguration = (currentRegionStart, regionLength, interludeLengthTs, leadInLengthTs, lastCommittedTimeslice) => {
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,
};
};
this.getCurrentRegionStartEndTs = (saleInfo, regionLength) => {
return {
currentRegionEnd: saleInfo.regionBegin,
currentRegionStart: saleInfo.regionBegin - regionLength,
};
};
this.assertCoretimeModule = (api, 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.');
};
}
async getCoretimeInfo(hash) {
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,
palletVersion: palletVersion,
maxHistoricalRevenue: maxHistoricalRevenue,
};
}
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;
const currentRegionStats = saleInfo && this.getCurrentRegionStartEndTs(saleInfo, config.regionLength);
const phaseConfig = this.getPhaseConfiguration((currentRegionStats === null || currentRegionStats === void 0 ? void 0 : currentRegionStats.currentRegionStart) || 0, config.regionLength, config.interludeLength, (saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.leadinLength) || 0, status.lastCommittedTimeslice || 0);
return {
at: {
hash,
height: blockNumber.toString(10),
},
configuration: {
regionLength: config.regionLength,
interludeLength: config.interludeLength,
leadinLength: (saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.leadinLength) || 0,
relayBlocksPerTimeslice: blocksPerTimeslice.toNumber(),
},
currentRegion: {
start: (currentRegionStats === null || currentRegionStats === void 0 ? void 0 : currentRegionStats.currentRegionStart) || null,
end: (currentRegionStats === null || currentRegionStats === void 0 ? void 0 : currentRegionStats.currentRegionEnd) || null,
},
cores: {
available: Number(saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.coresOffered) - Number(saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.coresSold),
sold: Number(saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.coresSold),
total: Number(saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.coresOffered),
currentCorePrice: this.getCorePriceAt(blockNumber.toNumber(), saleInfo),
selloutPrice: saleInfo === null || saleInfo === void 0 ? void 0 : saleInfo.selloutPrice,
firstCore: saleInfo === null || saleInfo === void 0 ? void 0 : 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) {
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 = leases.reduce((acc, lease) => {
var _a;
const core = (_a = workload.find((wl) => wl.info.task.includes(lease.task))) === null || _a === void 0 ? void 0 : _a.core;
return [
...acc,
{
...lease,
core: core,
},
];
}, []);
return {
at: {
hash,
height: blockNumber.toString(10),
},
leases: (0, util_2.sortByCore)(leasesWithCore),
};
}
}
async getCoretimeRegions(hash) {
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: (0, util_2.sortByCore)(regions),
};
}
}
async getCoretimeReservations(hash) {
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) {
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: (0, util_2.sortByCore)(renewals),
};
}
}
async getCoretimeCores(hash) {
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((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) => {
var _a, _b;
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: (_a = reservations.find((f) => f.task === wl.info.task)) === null || _a === void 0 ? void 0 : _a.mask };
}
else if (coreType === 'lease') {
details = { until: (_b = leases.find((f) => f.task === wl.info.task)) === null || _b === void 0 ? void 0 : _b.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
*/
assertQueryModule(queryFn, mod) {
if (!queryFn) {
throw Error(`The runtime does not include the ${mod} module at this block`);
}
}
getChainType(specName) {
const relay = ['polkadot', 'kusama', 'westend', 'paseo'];
if (relay.includes(specName.toLowerCase())) {
return ChainType.Relay;
}
else {
return ChainType.Parachain;
}
}
}
exports.CoretimeService = CoretimeService;
//# sourceMappingURL=CoretimeService.js.map