UNPKG

@substrate/api-sidecar

Version:

REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.

566 lines 28.4 kB
"use strict"; // 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const util_1 = require("@polkadot/util"); const http_errors_1 = require("http-errors"); const apiRegistry_1 = require("../../apiRegistry"); const validate_1 = require("../../middleware/validate"); const services_1 = require("../../services"); const PromiseQueue_1 = require("../../util/PromiseQueue"); const AbstractController_1 = __importDefault(require("../AbstractController")); /** * GET a block. * * Paths: * - `head`: Get the latest finalized block. * - (Optional) `number`: Block hash or height at which to query. If not provided, queries * finalized head. * * Query: * - (Optional) `eventDocs`: When set to `true`, every event will have an extra * `docs` property with a string of the events documentation. * - (Optional) `extrinsicDocs`: When set to `true`, every extrinsic will have an extra * `docs` property with a string of the extrinsics documentation. * - (Optional for `/blocks/head`) `finalized`: When set to `false`, it will fetch the head of * the node's canon chain, which might not be finalized. When set to `true` it * will fetch the head of the finalized chain. * - (Optional) `noFees`: When set to `true`, it will not calculate the fee for each extrinsic. * - (Optional for `/blocks/{blockId}`) `decodedXcmMsgs`: When set to `true`, it will show the * decoded XCM messages within the extrinsics of the requested block. * - (Optional for `/blocks/{blockId}) `paraId`: When it is set, it will return only the decoded * XCM messages for the specified paraId/parachain Id. To activate this functionality, ensure * that the `decodedXcmMsgs` parameter is set to true. * - (Optional for `/blocks/head`) `useRcBlock`: When set to `true`, it will use the latest * relay chain block to determine the corresponding Asset Hub block. Only supported for * Asset Hub endpoints with relay chain API available. * * * Returns: * - When using `useRcBlock` parameter: An array of response objects, one for each Asset Hub block found * corresponding to the relay chain block. Returns empty array `[]` if no Asset Hub blocks found. * - When using standard block identifiers or no query params: A single response object. * * Response object structure: * - `number`: Block height. * - `hash`: The block's hash. * - `parentHash`: The hash of the parent block. * - `stateRoot`: The state root after executing this block. * - `extrinsicsRoot`: The Merkle root of the extrinsics. * - `authorId`: The account ID of the block author (may be undefined for some chains). * - `logs`: Array of `DigestItem`s associated with the block. * - `onInitialize`: Object with an array of `SanitizedEvent`s that occurred during block * initialization with the `method` and `data` for each. * - `extrinsics`: Array of extrinsics (inherents and transactions) within the block. Each * contains: * - `method`: Extrinsic method. * - `signature`: Object with `signature` and `signer`, or `null` if unsigned. * - `nonce`: Account nonce, if applicable. * - `args`: Array of arguments. Note: if you are expecting an [`OpaqueCall`](https://substrate.dev/rustdocs/v2.0.0/pallet_multisig/type.OpaqueCall.html) * and it is not decoded in the response (i.e. it is just a hex string), then Sidecar was not * able to decode it and likely that it is not a valid call for the runtime. * - `tip`: Any tip added to the transaction. * - `hash`: The transaction's hash. * - `info`: `RuntimeDispatchInfo` for the transaction. Includes the `partialFee`. * - `events`: An array of `SanitizedEvent`s that occurred during extrinsic execution. * - `success`: Whether or not the extrinsic succeeded. * - `paysFee`: Whether the extrinsic requires a fee. Careful! This field relates to whether or * not the extrinsic requires a fee if called as a transaction. Block authors could insert * the extrinsic as an inherent in the block and not pay a fee. Always check that `paysFee` * is `true` and that the extrinsic is signed when reconciling old blocks. * - `onFinalize`: Object with an array of `SanitizedEvent`s that occurred during block * finalization with the `method` and `data` for each. * - `decodedXcmMsgs`: An array of the decoded XCM messages found within the extrinsics * of the requested block. * - `rcBlockNumber`: The relay chain block number used for the query. Only present when `useRcBlock` parameter is used. * - `ahTimestamp`: The Asset Hub block timestamp. Only present when `useRcBlock` parameter is used. * * Note: Block finalization does not correspond to consensus, i.e. whether the block is in the * canonical chain. It denotes the finalization of block _construction._ * * Substrate Reference: * - `DigestItem`: https://crates.parity.io/sp_runtime/enum.DigestItem.html * - `RawEvent`: https://crates.parity.io/frame_system/enum.RawEvent.html * - Extrinsics: https://substrate.dev/docs/en/knowledgebase/learn-substrate/extrinsics * - `Extrinsic`: https://crates.parity.io/sp_runtime/traits/trait.Extrinsic.html * - `OnInitialize`: https://crates.parity.io/frame_support/traits/trait.OnInitialize.html * - `OnFinalize`: https://crates.parity.io/frame_support/traits/trait.OnFinalize.html */ class BlocksController extends AbstractController_1.default { constructor(api, options) { super(api, '/blocks', new services_1.BlocksService(api, options.minCalcFeeRuntime, options.hasQueryFeeApi)); this.options = options; this.emitExtrinsicMetrics = (totExtrinsics, totBlocks, method, path, res) => { if (res.locals.metrics) { const seconds = res.locals.metrics.timer(); const extrinsics_in_request = res.locals.metrics.registry['sas_extrinsics_in_request']; extrinsics_in_request.labels({ method: method, route: path, status_code: res.statusCode }).observe(totExtrinsics); const extrinsics_per_second = res.locals.metrics.registry['sas_extrinsics_per_second']; extrinsics_per_second .labels({ method: method, route: path, status_code: res.statusCode }) .observe(totExtrinsics / seconds); const extrinsicsPerBlockMetrics = res.locals.metrics.registry['sas_extrinsics_per_block']; extrinsicsPerBlockMetrics .labels({ method: 'GET', route: path, status_code: res.statusCode }) .observe(totExtrinsics / totBlocks); const seconds_per_block = res.locals.metrics.registry['sas_seconds_per_block']; seconds_per_block .labels({ method: method, route: path, status_code: res.statusCode }) .observe(seconds / totBlocks); if (totBlocks > 1) { const seconds_per_block = res.locals.metrics.registry['sas_seconds_per_block']; seconds_per_block .labels({ method: method, route: path, status_code: res.statusCode }) .observe(seconds / totBlocks); } } }; /** * Get the latest block. * * @param _req Express Request * @param res Express Response */ this.getLatestBlock = async ({ query: { eventDocs, extrinsicDocs, finalized, noFees, decodedXcmMsgs, paraId, useRcBlock, useEvmFormat }, method, route, }, res) => { var _a, _b, _c; const eventDocsArg = eventDocs === 'true'; const extrinsicDocsArg = extrinsicDocs === 'true'; const useRcBlockArg = useRcBlock === 'true'; let hash, queryFinalizedHead, omitFinalizedTag; let rcBlockNumber; if (!this.options.finalizes) { // If the network chain doesn't finalize blocks, we dont want a finalized tag. omitFinalizedTag = true; queryFinalizedHead = false; if (useRcBlockArg) { const rcApi = (_a = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay')[0]) === null || _a === void 0 ? void 0 : _a.api; const rcHeader = await rcApi.rpc.chain.getHeader(); const rcHash = rcHeader.hash; rcBlockNumber = rcHeader.number.toString(); hash = await this.getAhAtFromRcAt(rcHash); // Return empty array if no Asset Hub block found if (!hash) { BlocksController.sanitizedSend(res, []); return; } } else { hash = (await this.api.rpc.chain.getHeader()).hash; } } else if (finalized === 'false') { // We query the finalized head to know where the latest finalized block // is. It is a way to confirm whether the queried block is less than or // equal to the finalized head. omitFinalizedTag = false; queryFinalizedHead = true; if (useRcBlockArg) { const rcApi = (_b = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay')[0]) === null || _b === void 0 ? void 0 : _b.api; const rcHeader = await rcApi.rpc.chain.getHeader(); const rcHash = rcHeader.hash; rcBlockNumber = rcHeader.number.toString(); hash = await this.getAhAtFromRcAt(rcHash); // Return empty array if no Asset Hub block found if (!hash) { BlocksController.sanitizedSend(res, []); return; } } else { hash = (await this.api.rpc.chain.getHeader()).hash; } } else { omitFinalizedTag = false; queryFinalizedHead = false; if (useRcBlockArg) { const rcApi = (_c = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay')[0]) === null || _c === void 0 ? void 0 : _c.api; const rcHash = await rcApi.rpc.chain.getFinalizedHead(); const rcHeader = await rcApi.rpc.chain.getHeader(rcHash); rcBlockNumber = rcHeader.number.toString(); hash = await this.getAhAtFromRcAt(rcHash); // Return empty array if no Asset Hub block found if (!hash) { BlocksController.sanitizedSend(res, []); return; } } else { hash = await this.api.rpc.chain.getFinalizedHead(); } } const noFeesArg = noFees === 'true'; const decodedXcmMsgsArg = decodedXcmMsgs === 'true'; const paraIdArg = paraId ? this.parseNumberOrThrow(paraId, 'paraId must be an integer') : undefined; const options = { eventDocs: eventDocsArg, extrinsicDocs: extrinsicDocsArg, checkFinalized: false, queryFinalizedHead, omitFinalizedTag, noFees: noFeesArg, checkDecodedXcm: decodedXcmMsgsArg, paraId: paraIdArg, useEvmAddressFormat: useEvmFormat === 'true', }; // Create a key for the cache that is a concatenation of the block hash and all the query params included in the request const cacheKey = hash.toString() + Number(options.eventDocs) + Number(options.extrinsicDocs) + Number(options.checkFinalized) + Number(options.noFees) + Number(options.checkDecodedXcm) + Number(options.paraId) + Number(options.useEvmAddressFormat); const isBlockCached = this.blockStore.get(cacheKey); if (isBlockCached) { if (rcBlockNumber) { const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); const enhancedCachedResult = { ...isBlockCached, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; BlocksController.sanitizedSend(res, [enhancedCachedResult]); } else { BlocksController.sanitizedSend(res, isBlockCached); } return; } const historicApi = await this.api.at(hash); const block = await this.service.fetchBlock(hash, historicApi, options); this.blockStore.set(cacheKey, block); if (rcBlockNumber) { const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); const enhancedResult = { ...block, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; BlocksController.sanitizedSend(res, [enhancedResult]); } else { BlocksController.sanitizedSend(res, block); } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const path = route.path; if (res.locals.metrics) { this.emitExtrinsicMetrics(block.extrinsics.length, 1, method, path, res); } }; /** * Get a block by its hash or number identifier. * * @param req Express Request * @param res Express Response */ this.getBlockById = async ({ params: { number }, query: { eventDocs, extrinsicDocs, noFees, finalizedKey, decodedXcmMsgs, paraId, useRcBlock, useEvmFormat }, method, route, }, res) => { const useRcBlockArg = useRcBlock === 'true'; const checkFinalized = (0, util_1.isHex)(number); let hash; let rcBlockNumber; if (useRcBlockArg) { // Treat the 'number' parameter as a relay chain block identifier rcBlockNumber = number; hash = await this.getAhAtFromRcAt(number); // Return empty array if no Asset Hub block found if (!hash) { BlocksController.sanitizedSend(res, []); return; } } else { hash = await this.getHashForBlock(number); } const eventDocsArg = eventDocs === 'true'; const extrinsicDocsArg = extrinsicDocs === 'true'; const finalizeOverride = finalizedKey === 'false'; const queryFinalizedHead = !this.options.finalizes ? false : true; const noFeesArg = noFees === 'true'; let omitFinalizedTag = !this.options.finalizes ? true : false; if (finalizeOverride) { omitFinalizedTag = true; } const decodedXcmMsgsArg = decodedXcmMsgs === 'true'; const paraIdArg = paraId ? this.parseNumberOrThrow(paraId, 'paraId must be an integer') : undefined; const options = { eventDocs: eventDocsArg, extrinsicDocs: extrinsicDocsArg, checkFinalized, queryFinalizedHead, omitFinalizedTag, noFees: noFeesArg, checkDecodedXcm: decodedXcmMsgsArg, paraId: paraIdArg, useEvmAddressFormat: useEvmFormat === 'true', }; // Create a key for the cache that is a concatenation of the block hash and all the query params included in the request const cacheKey = hash.toString() + Number(options.eventDocs) + Number(options.extrinsicDocs) + Number(options.checkFinalized) + Number(options.noFees) + Number(options.checkDecodedXcm) + Number(options.paraId) + Number(options.useEvmAddressFormat); const isBlockCached = this.blockStore.get(cacheKey); if (isBlockCached) { if (rcBlockNumber) { const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); const enhancedCachedResult = { ...isBlockCached, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; BlocksController.sanitizedSend(res, [enhancedCachedResult]); } else { BlocksController.sanitizedSend(res, isBlockCached); } return; } // HistoricApi to fetch any historic information that doesnt include the current runtime const historicApi = await this.api.at(hash); const block = await this.service.fetchBlock(hash, historicApi, options); this.blockStore.set(cacheKey, block); if (rcBlockNumber) { const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); const enhancedResult = { ...block, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; BlocksController.sanitizedSend(res, [enhancedResult]); } else { // We set the last param to true because we haven't queried the finalizedHead BlocksController.sanitizedSend(res, block); } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const path = route.path; if (res.locals.metrics) { this.emitExtrinsicMetrics(block.extrinsics.length, 1, method, path, res); } }; /** * Return the Header of the identified block. * * @param req Express Request * @param res Express Response */ this.getBlockHeaderById = async ({ params: { number }, query: { useRcBlock } }, res) => { const useRcBlockArg = useRcBlock === 'true'; let hash; let rcBlockNumber; if (useRcBlockArg) { // Treat the 'number' parameter as a relay chain block identifier rcBlockNumber = number; hash = await this.getAhAtFromRcAt(number); // Return empty array if no Asset Hub block found if (!hash) { BlocksController.sanitizedSend(res, []); return; } } else { hash = await this.getHashForBlock(number); } const headerResult = await this.service.fetchBlockHeader(hash); if (rcBlockNumber) { const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); const enhancedResult = { parentHash: headerResult.parentHash, number: headerResult.number, stateRoot: headerResult.stateRoot, extrinsicsRoot: headerResult.extrinsicsRoot, digest: headerResult.digest, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; BlocksController.sanitizedSend(res, [enhancedResult]); } else { BlocksController.sanitizedSend(res, headerResult); } }; /** * Return the header of the latest block * * @param req Express Request * @param res Express Response */ this.getLatestBlockHeader = async ({ query: { finalized, useRcBlock } }, res) => { var _a; const paramFinalized = finalized !== 'false'; const useRcBlockArg = useRcBlock === 'true'; let hash; let rcBlockNumber; if (useRcBlockArg) { const rcApi = (_a = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay')[0]) === null || _a === void 0 ? void 0 : _a.api; if (paramFinalized) { const rcHash = await rcApi.rpc.chain.getFinalizedHead(); const rcHeader = await rcApi.rpc.chain.getHeader(rcHash); rcBlockNumber = rcHeader.number.toString(); hash = await this.getAhAtFromRcAt(rcHash); } else { const rcHeader = await rcApi.rpc.chain.getHeader(); const rcHash = rcHeader.hash; rcBlockNumber = rcHeader.number.toString(); hash = await this.getAhAtFromRcAt(rcHash); } // Return empty array if no Asset Hub block found if (!hash) { BlocksController.sanitizedSend(res, []); return; } } else { hash = paramFinalized ? await this.api.rpc.chain.getFinalizedHead() : undefined; } const headerResult = await this.service.fetchBlockHeader(hash); if (rcBlockNumber) { const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); const enhancedResult = { parentHash: headerResult.parentHash, number: headerResult.number, stateRoot: headerResult.stateRoot, extrinsicsRoot: headerResult.extrinsicsRoot, digest: headerResult.digest, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; BlocksController.sanitizedSend(res, [enhancedResult]); } else { BlocksController.sanitizedSend(res, headerResult); } }; /** * Return a collection of blocks, given a range. * * @param req Express Request * @param res Express Response */ this.getBlocks = async ({ query: { range, eventDocs, extrinsicDocs, noFees, useRcBlock, useEvmFormat }, method, route }, res) => { if (!range) throw new http_errors_1.BadRequest('range query parameter must be inputted.'); // We set a max range to 500 blocks. const rangeOfNums = this.parseRangeOfNumbersOrThrow(range, 500); const useRcBlockArg = useRcBlock === 'true'; const eventDocsArg = eventDocs === 'true'; const extrinsicDocsArg = extrinsicDocs === 'true'; const queryFinalizedHead = !this.options.finalizes ? false : true; const omitFinalizedTag = !this.options.finalizes ? true : false; const noFeesArg = noFees === 'true'; const options = { eventDocs: eventDocsArg, extrinsicDocs: extrinsicDocsArg, checkFinalized: false, queryFinalizedHead, omitFinalizedTag, noFees: noFeesArg, checkDecodedXcm: false, paraId: undefined, useEvmAddressFormat: useEvmFormat === 'true', }; const pQueue = new PromiseQueue_1.PromiseQueue(4); const blocksPromise = []; for (let i = 0; i < rangeOfNums.length; i++) { const result = pQueue.run(async () => { let hash; let rcBlockNumber; if (useRcBlockArg) { // Treat range numbers as relay chain block identifiers rcBlockNumber = rangeOfNums[i].toString(); hash = await this.getAhAtFromRcAt(rcBlockNumber); // Skip this RC block if it doesn't have a corresponding AH block if (!hash) { return null; } } else { // Get block hash: hash = await this.getHashForBlock(rangeOfNums[i].toString()); } // Get API at that hash: const historicApi = await this.api.at(hash); // Get block details using this API/hash: const block = await this.service.fetchBlock(hash, historicApi, options); if (rcBlockNumber) { // Add RC context to the block const apiAt = await this.api.at(hash); const ahTimestamp = await apiAt.query.timestamp.now(); return { ...block, rcBlockNumber, ahTimestamp: ahTimestamp.toString(), }; } return block; }); blocksPromise.push(result); } const allBlocks = await Promise.all(blocksPromise); blocksPromise.length = 0; // Filter out null values (skipped RC blocks that don't have corresponding AH blocks) const blocks = allBlocks.filter((block) => block !== null); /** * Sort blocks from least to greatest. */ blocks.sort((a, b) => a.number.toNumber() - b.number.toNumber()); BlocksController.sanitizedSend(res, blocks); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const path = route.path; if (res.locals.metrics) { const totExtrinsics = blocks.reduce((current, block) => { const extrinsics = block.extrinsics; if (Array.isArray(extrinsics)) { return current + extrinsics.length; } return current; }, 0); this.emitExtrinsicMetrics(totExtrinsics, blocks.length, method, path, res); } }; this.initRoutes(); this.blockStore = options.blockStore; } initRoutes() { this.router.use(this.path, (0, validate_1.validateBoolean)(['eventDocs', 'extrinsicDocs', 'finalized']), validate_1.validateUseRcBlock); this.safeMountAsyncGetHandlers([ ['/', this.getBlocks], ['/head', this.getLatestBlock], ['/:number', this.getBlockById], ['/head/header', this.getLatestBlockHeader], ['/:number/header', this.getBlockHeaderById], ]); } } BlocksController.controllerName = 'Blocks'; BlocksController.requiredPallets = [['System', 'Session']]; exports.default = BlocksController; //# sourceMappingURL=BlocksController.js.map