@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
319 lines • 13.5 kB
JavaScript
;
// 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/>.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("@polkadot/util");
const express = __importStar(require("express"));
const http_errors_1 = require("http-errors");
const apiRegistry_1 = require("../../src/apiRegistry");
const sanitize_1 = require("../sanitize");
const errors_1 = require("../types/errors");
const verifyInt_1 = require("../util/integers/verifyInt");
/**
* Abstract base class for creating controller classes.
*/
class AbstractController {
constructor(_specName, _path, service) {
this._specName = _specName;
this._path = _path;
this.service = service;
this._router = express.Router();
this.ASSET_HUB_ID = 1000;
}
get path() {
return this._path;
}
get router() {
return this._router;
}
get api() {
const api = apiRegistry_1.ApiPromiseRegistry.getApi(this._specName);
if (!api) {
throw new http_errors_1.InternalServerError('API not found during controller initilization');
}
return api;
}
get assetHubInfo() {
return apiRegistry_1.ApiPromiseRegistry.assetHubInfo;
}
get specName() {
return this._specName;
}
/**
* Safely mount async GET routes by wrapping them with an express
* handler friendly try / catch block and then mounting on the controllers
* router.
*
* @param pathsAndHandlers array of tuples containing the suffix to the controller
* base path (use empty string if no suffix) and the get request handler function.
*/
safeMountAsyncGetHandlers(pathsAndHandlers) {
for (const pathAndHandler of pathsAndHandlers) {
const [pathSuffix, handler] = pathAndHandler;
this.router.get(`${this.path}${pathSuffix}`, _a.catchWrap(handler));
}
}
/**
* Safely mount async POST routes by wrapping them with an express
* handler friendly try / catch block and then mounting on the controllers
* router.
*
* @param pathsAndHandlers array of tuples containing the suffix to the controller
* base path (use empty string if no suffix) and the get request handler function.
*/
safeMountAsyncPostHandlers(pathsAndHandlers) {
for (const pathAndHandler of pathsAndHandlers) {
const [pathSuffix, handler] = pathAndHandler;
this.router.post(`${this.path}${pathSuffix}`, _a.catchWrap(handler));
}
}
/**
* Create or retrieve the corresponding BlockHash for the given block identifier.
* This also acts as a validation for string based block identifiers.
*
* @param blockId string representing a hash or number block identifier.
*/
async getHashForBlock(blockId, opts = {}) {
let blockNumber;
// isHex, as imported, returns "value is string", which undersells what it
// is checking for (it's not only checking that value is string, but that it's
// valid looking hex). So, we vaguen up the type signature here to avoid breakage
// below (see https://github.com/polkadot-js/common/issues/1102).
function isHexBool(value) {
return (0, util_1.isHex)(value);
}
const chosenApi = (opts === null || opts === void 0 ? void 0 : opts.api) ? opts.api : this.api;
try {
const isHexStr = isHexBool(blockId);
if (isHexStr && blockId.length === 66) {
// This is a block hash
return chosenApi.createType('BlockHash', blockId);
}
else if (isHexStr) {
throw new http_errors_1.BadRequest(`Cannot get block hash for ${blockId}. ` + `Hex string block IDs must be 32-bytes (66-characters) in length.`);
}
else if (blockId.slice(0, 2) === '0x') {
throw new http_errors_1.BadRequest(`Cannot get block hash for ${blockId}. ` +
`Hex string block IDs must be a valid hex string ` +
`and must be 32-bytes (66-characters) in length.`);
}
// Not a block hash, must be a block height
try {
blockNumber = this.parseNumberOrThrow(blockId, 'Invalid block number');
}
catch (err) {
throw new http_errors_1.BadRequest(`Cannot get block hash for ${blockId}. ` +
`Block IDs must be either 32-byte hex strings or non-negative decimal integers.`);
}
return await chosenApi.rpc.chain.getBlockHash(blockNumber);
}
catch (err) {
if (err instanceof http_errors_1.HttpError) {
// Throw errors we created in the above try block
throw err;
}
const { number } = await chosenApi.rpc.chain.getHeader().catch(() => {
throw new http_errors_1.InternalServerError('Failed while trying to get the latest header.');
});
if (blockNumber && number.toNumber() < blockNumber) {
throw new http_errors_1.BadRequest(`Specified block number is larger than the current largest block. ` +
`The largest known block number is ${number.toString()}.`);
}
// This should never be used, but here just in case
if ((0, errors_1.isBasicLegacyError)(err)) {
throw err;
}
throw new http_errors_1.InternalServerError(`Cannot get block hash for ${blockId}.`);
}
}
parseNumberOrThrow(n, errorMessage) {
const num = Number(n);
if (!(0, verifyInt_1.verifyUInt)(num)) {
throw new http_errors_1.BadRequest(errorMessage);
}
return num;
}
/**
* Expected format ie: 0-999
*/
parseRangeOfNumbersOrThrow(n, maxRange) {
const splitRange = n.split('-');
if (splitRange.length !== 2) {
throw new http_errors_1.BadRequest('Incorrect range format. Expected example: 0-999');
}
const min = Number(splitRange[0]);
const max = Number(splitRange[1]);
if (!(0, verifyInt_1.verifyUInt)(min)) {
throw new http_errors_1.BadRequest('Inputted min value for range must be an unsigned integer.');
}
if (!(0, verifyInt_1.verifyNonZeroUInt)(max)) {
throw new http_errors_1.BadRequest('Inputted max value for range must be an unsigned non zero integer.');
}
if (min >= max) {
throw new http_errors_1.BadRequest('Inputted min value cannot be greater than or equal to the max value.');
}
if (max - min > maxRange) {
throw new http_errors_1.BadRequest(`Inputted range is greater than the ${maxRange} range limit.`);
}
return [...Array(max - min + 1).keys()].map((i) => i + min);
}
parseQueryParamArrayOrThrow(n) {
return n.map((str) => this.parseNumberOrThrow(str, `Incorrect AssetId format: ${str} is not a positive integer.`));
}
verifyAndCastOr(name, str, or) {
if (!str) {
return or;
}
if (!(typeof str === 'string')) {
throw new http_errors_1.BadRequest(`Incorrect argument quantity or type passed in for ${name} query param`);
}
return this.parseNumberOrThrow(str, `${name} query param is an invalid number`);
}
/**
* Get a BlockHash based on the `at` query param.
*
* @param at should be a block height, hash, or undefined from the `at` query param
*/
async getHashFromAt(at, opts = {}) {
const chosenApi = (opts === null || opts === void 0 ? void 0 : opts.api) ? opts.api : this.api;
return typeof at === 'string'
? await this.getHashForBlock(at, { api: chosenApi })
: await chosenApi.rpc.chain.getFinalizedHead();
}
async getAhAtFromRcAt(at) {
var _b;
const rcApi = (_b = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay')[0]) === null || _b === void 0 ? void 0 : _b.api;
if (!rcApi) {
throw new Error('Relay chain api must be available');
}
const rcHash = await this.getHashFromAt(at, { api: rcApi });
return this.findAhBlockInRcBlock(rcHash, rcApi);
}
async findAhBlockInRcBlock(rcHash, rcApi) {
const rcApiAt = await rcApi.at(rcHash);
const events = await rcApiAt.query.system.events();
const paraInclusion = events.filter((record) => {
return record.event.section === 'paraInclusion' && record.event.method === 'CandidateIncluded';
});
const ahInfo = paraInclusion.find(({ event }) => {
const { data } = event;
const paraData = data[0];
const { paraId } = paraData.descriptor;
return paraId.toNumber() === this.ASSET_HUB_ID;
});
if (ahInfo) {
const headerData = ahInfo.event.data[1];
const header = rcApiAt.registry.createType('Header', headerData);
const ahBlockHash = header.hash;
return rcApi.createType('BlockHash', ahBlockHash);
}
return null;
}
async getHashFromRcAt(rcAt) {
var _b;
const ahHash = await this.getAhAtFromRcAt(rcAt);
if (!ahHash) {
return [];
}
// Get the actual relay chain block number
const rcApi = (_b = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay')[0]) === null || _b === void 0 ? void 0 : _b.api;
if (!rcApi) {
throw new Error('Relay chain api must be available');
}
const rcHash = await this.getHashFromAt(rcAt, { api: rcApi });
const rcHeader = await rcApi.rpc.chain.getHeader(rcHash);
const rcBlockNumber = rcHeader.number.toString();
return [{ ahHash, rcBlockNumber }];
}
/**
* Sanitize the numbers within the response body and then send the response
* body using the original Express Response object.
*
* @param res Response
* @param body response body
*/
static sanitizedSend(res, body, options = {}) {
res.send((0, sanitize_1.sanitizeNumbers)(body, options));
}
static canInjectByPallets(availablePallets) {
if (!this.requiredPallets) {
return true;
}
if (this.requiredPallets.length === 1) {
if (this.requiredPallets[0].length === 0) {
return true;
}
return this.requiredPallets[0].every((pallet) => availablePallets.includes(pallet));
}
else if (this.requiredPallets.length > 1) {
return this.requiredPallets.some((pallets) => pallets.every((p) => availablePallets.includes(p)));
}
// If requiredPallets is empty, then we can inject
return true;
}
}
_a = AbstractController;
/**
* Wrapper for any asynchronous RequestHandler function. Pipes errors
* to downstream error handling middleware.
*
* @param cb ExpressHandler
*/
AbstractController.catchWrap = (cb) => async (req, res, next) => {
try {
// eslint-disable-next-line @typescript-eslint/await-thenable
await cb(req, res, next);
}
catch (err) {
next(err);
}
};
exports.default = AbstractController;
//# sourceMappingURL=AbstractController.js.map