From 6507ce70ff458281d1a2e31b58716e20ad8183dc Mon Sep 17 00:00:00 2001 From: Tarik Gul <47201679+TarikGul@users.noreply.github.com> Date: Mon, 25 Jan 2021 23:27:39 -0500 Subject: [PATCH] feat: add route /blocks/{blockId}/extrinsics/{extrinsicIndex} (#400) * Add new /blocks/{blockId}/extrinsics/{extrinsicsIndex} endpoint * Add endpoint to chains-config endpoints * Add exports to entry files * Add request validations * Abstract logic from BlocksExtrinsicsController into BlocksServices * Refactor index, and add await t async call * Add types * fix: revert back to original method and cleanup index param fix: destructure fix: reorder index * fix: add options for PoW chains, adjust options passed into fetchBlock * feat: block 789629 extrinsic responce json file * fix: modify types for at[object] * fix: revert types for IExtrinsicIndex at:[object] * fix: mock json extrinsic data * fix: working fetchExtrinsicByIndex test * feat: Test extrinisics error * fix: change error type * fix: async/await functionality across fetch extrinsics, fix tests, lint * feat: docs * fix: docs * fix: (docs) events, extrinsics * fix: docs responses * fix: docs ExtrinsicIndex * fix: bugs in docs * fix: change thrown error to BadRequest for 400 error * fix: lint * fix: docs description for ExtrinsicIndex, organize BadRequest import * Update docs/src/openapi-v1.yaml Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com> * Update docs/src/openapi-v1.yaml Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com> * Update docs/src/openapi-v1.yaml Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com> * fix: extrinsicsIndex -> extriniscIndex (singular) fix: typos, naming, add parseNumberOrThrow fix: revert to parseInt * fix: typos, IAt type, docs, error messages * fix: lint * fix: fix error messaging, and docs fix: cleanup block extrinsics controller fix: omitFinalized -> true fix: add test to check parseNumberOrThrow will throw an error if a negative is passed in. Yarn fix * fix: remove async * fix: remove async fix: update extrinsic index test to query extrinsic 2 fix: lint * Update docs/src/openapi-v1.yaml Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com> * fix: getExtrinsicByExtrinsicIndex => getExtrinsicByIndex * fix: getExtrinsicByIndex => getExtrinsicByTimepoint Co-authored-by: Zeke Mostov <32168567+emostov@users.noreply.github.com> --- docs/src/openapi-v1.yaml | 59 +++++++++++++++++ src/chains-config/defaultControllers.ts | 1 + src/chains-config/dockMainnetControllers.ts | 1 + src/chains-config/dockTestnetControllers.ts | 1 + src/chains-config/kulupuControllers.ts | 1 + src/chains-config/mandalaControllers.ts | 1 + .../blocks/BlocksExtrinsicsController.ts | 66 +++++++++++++++++++ src/controllers/blocks/index.ts | 1 + src/controllers/index.ts | 3 +- src/services/blocks/BlocksService.spec.ts | 55 ++++++++++++++++ src/services/blocks/BlocksService.ts | 28 +++++++- .../test-helpers/mock/parseNumberOrThrow.ts | 11 ++++ .../blocks/block789629Extrinsic.json | 37 +++++++++++ src/types/responses/Extrinsic.ts | 7 +- 14 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 src/controllers/blocks/BlocksExtrinsicsController.ts create mode 100644 src/services/test-helpers/mock/parseNumberOrThrow.ts create mode 100644 src/services/test-helpers/responses/blocks/block789629Extrinsic.json diff --git a/docs/src/openapi-v1.yaml b/docs/src/openapi-v1.yaml index 2ed5e63ec..7e9d3b46a 100755 --- a/docs/src/openapi-v1.yaml +++ b/docs/src/openapi-v1.yaml @@ -273,6 +273,57 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /blocks/{blockId}/extrinsics/{extrinsicIndex}: + get: + tags: + - blocks + summary: Get an extrinsic by its extrinsicIndex and block height or hash. + The pair blockId, extrinsicIndex is sometimes referred to as a Timepoint. + description: Returns a single extrinsic. + operationId: getExtrinsicByTimepoint + parameters: + - name: blockId + in: path + description: Block identifier, as the block height or block hash. + required: true + schema: + pattern: a-km-zA-HJ-NP-Z1-9{8,64} + type: string + - name: extrinsicIndex + in: path + description: The extrinsic's index within the block's body. + required: true + schema: + type: string + - name: eventDocs + in: query + description: When set to `true`, every event will have an extra `docs` + property with a string of the events documentation. + required: false + schema: + type: boolean + default: false + - name: extrinsicDocs + in: query + description: When set to `true`, every extrinsic will have an extra `docs` + property with a string of the extrinsics documentation. + required: false + schema: + type: boolean + default: false + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExtrinsicIndex' + "400": + description: Requested `extrinsicIndex` does not exist + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /blocks/head: get: tags: @@ -1033,6 +1084,14 @@ components: 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. + ExtrinsicIndex: + type: object + properties: + at: + $ref: '#/components/schemas/BlockIdentifiers' + extrinsic: + $ref: '#/components/schemas/Extrinsic' + description: A single extrinsic at a given block. BlockInitialize: type: object properties: diff --git a/src/chains-config/defaultControllers.ts b/src/chains-config/defaultControllers.ts index 7ff0dd198..33479b557 100644 --- a/src/chains-config/defaultControllers.ts +++ b/src/chains-config/defaultControllers.ts @@ -7,6 +7,7 @@ import { ControllerConfig } from '../types/chains-config'; export const defaultControllers: ControllerConfig = { controllers: { Blocks: true, + BlocksExtrinsics: true, AccountsStakingPayouts: true, AccountsBalanceInfo: true, AccountsStakingInfo: true, diff --git a/src/chains-config/dockMainnetControllers.ts b/src/chains-config/dockMainnetControllers.ts index 7258f8a30..cd7cd2b27 100644 --- a/src/chains-config/dockMainnetControllers.ts +++ b/src/chains-config/dockMainnetControllers.ts @@ -6,6 +6,7 @@ import { ControllerConfig } from '../types/chains-config'; export const dockMainnetControllers: ControllerConfig = { controllers: { Blocks: true, + BlocksExtrinsics: true, AccountsStakingPayouts: false, AccountsBalanceInfo: true, AccountsStakingInfo: false, diff --git a/src/chains-config/dockTestnetControllers.ts b/src/chains-config/dockTestnetControllers.ts index 467f1b946..66bf01cd7 100644 --- a/src/chains-config/dockTestnetControllers.ts +++ b/src/chains-config/dockTestnetControllers.ts @@ -6,6 +6,7 @@ import { ControllerConfig } from '../types/chains-config'; export const dockTestnetControllers: ControllerConfig = { controllers: { Blocks: true, + BlocksExtrinsics: true, AccountsStakingPayouts: false, AccountsBalanceInfo: true, AccountsStakingInfo: false, diff --git a/src/chains-config/kulupuControllers.ts b/src/chains-config/kulupuControllers.ts index b6000ae4a..8cc3008c4 100644 --- a/src/chains-config/kulupuControllers.ts +++ b/src/chains-config/kulupuControllers.ts @@ -3,6 +3,7 @@ import { ControllerConfig } from '../types/chains-config'; export const kulupuControllers: ControllerConfig = { controllers: { Blocks: true, + BlocksExtrinsics: true, AccountsStakingPayouts: false, AccountsBalanceInfo: true, AccountsStakingInfo: false, diff --git a/src/chains-config/mandalaControllers.ts b/src/chains-config/mandalaControllers.ts index 3ec234baf..091967f70 100644 --- a/src/chains-config/mandalaControllers.ts +++ b/src/chains-config/mandalaControllers.ts @@ -6,6 +6,7 @@ import { ControllerConfig } from '../types/chains-config'; export const mandalaControllers: ControllerConfig = { controllers: { Blocks: true, + BlocksExtrinsics: true, AccountsStakingPayouts: true, AccountsBalanceInfo: true, AccountsStakingInfo: true, diff --git a/src/controllers/blocks/BlocksExtrinsicsController.ts b/src/controllers/blocks/BlocksExtrinsicsController.ts new file mode 100644 index 000000000..1c077ec00 --- /dev/null +++ b/src/controllers/blocks/BlocksExtrinsicsController.ts @@ -0,0 +1,66 @@ +import { ApiPromise } from '@polkadot/api'; +import { RequestHandler } from 'express'; + +import { BlocksService } from '../../services'; +import { INumberParam } from '../../types/requests'; +import AbstractController from '../AbstractController'; + +export default class BlocksExtrinsicsController extends AbstractController { + constructor(api: ApiPromise) { + super(api, '/blocks/:blockId/extrinsics', new BlocksService(api)); + this.initRoutes(); + } + + protected initRoutes(): void { + this.safeMountAsyncGetHandlers([ + ['/:extrinsicIndex', this.getExtrinsicByTimepoint], + ]); + } + + /** + * + * @param _req Express Request + * @param res Express Response + */ + private getExtrinsicByTimepoint: RequestHandler = async ( + { + params: { blockId, extrinsicIndex }, + query: { eventDocs, extrinsicDocs }, + }, + res + ): Promise => { + const hash = await this.getHashForBlock(blockId); + + const eventDocsArg = eventDocs === 'true'; + const extrinsicDocsArg = extrinsicDocs === 'true'; + + const options = { + eventDocs: eventDocsArg, + extrinsicDocs: extrinsicDocsArg, + checkFinalized: true, + queryFinalizedHead: false, + omitFinalizedTag: true, + }; + + const block = await this.service.fetchBlock(hash, options); + + /** + * Verify our param `extrinsicIndex` is an integer represented as a string + */ + this.parseNumberOrThrow( + extrinsicIndex, + '`exstrinsicIndex` path param is not a number' + ); + + /** + * Change extrinsicIndex from a type string to a number before passing it + * into any service. + */ + const index = parseInt(extrinsicIndex, 10); + + BlocksExtrinsicsController.sanitizedSend( + res, + this.service.fetchExtrinsicByIndex(block, index) + ); + }; +} diff --git a/src/controllers/blocks/index.ts b/src/controllers/blocks/index.ts index 96f7d7341..333ab8279 100644 --- a/src/controllers/blocks/index.ts +++ b/src/controllers/blocks/index.ts @@ -1 +1,2 @@ export { default as Blocks } from './BlocksController'; +export { default as BlocksExtrinsics } from './BlocksExtrinsicsController'; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index d0def0400..a235565f2 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -4,7 +4,7 @@ import { AccountsStakingPayouts, AccountsVestingInfo, } from './accounts'; -import { Blocks } from './blocks'; +import { Blocks, BlocksExtrinsics } from './blocks'; import { NodeNetwork, NodeTransactionPool, NodeVersion } from './node'; import { PalletsStakingProgress, PalletsStorage } from './pallets'; import { RuntimeCode, RuntimeMetadata, RuntimeSpec } from './runtime'; @@ -20,6 +20,7 @@ import { */ export const controllers = { Blocks, + BlocksExtrinsics, AccountsBalanceInfo, AccountsStakingInfo, AccountsVestingInfo, diff --git a/src/services/blocks/BlocksService.spec.ts b/src/services/blocks/BlocksService.spec.ts index a28bf1f04..bd4a6f59f 100644 --- a/src/services/blocks/BlocksService.spec.ts +++ b/src/services/blocks/BlocksService.spec.ts @@ -4,6 +4,7 @@ import { RpcPromiseResult } from '@polkadot/api/types/rpc'; import { GenericExtrinsic } from '@polkadot/types'; import { GenericCall } from '@polkadot/types/generic'; import { BlockHash, Hash, SignedBlock } from '@polkadot/types/interfaces'; +import { BadRequest } from 'http-errors'; import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers'; import { createCall } from '../../test-helpers/createCall'; @@ -19,6 +20,8 @@ import { mockForkedBlock789629, } from '../test-helpers/mock'; import * as block789629 from '../test-helpers/mock/data/block789629.json'; +import { parseNumberOrThrow } from '../test-helpers/mock/parseNumberOrThrow'; +import * as block789629Extrinsic from '../test-helpers/responses/blocks/block789629Extrinsic.json'; import * as blocks789629Response from '../test-helpers/responses/blocks/blocks789629.json'; import { BlocksService } from './BlocksService'; @@ -358,4 +361,56 @@ describe('BlocksService', () => { ).toEqual(true); }); }); + + describe('fetchExrinsicByIndex', () => { + // fetchBlock options + const options = { + eventDocs: false, + extrinsicDocs: false, + checkFinalized: false, + queryFinalizedHead: false, + omitFinalizedTag: false, + }; + + it('Returns the correct extrinisics object for block 789629', async () => { + const block = await blocksService.fetchBlock( + blockHash789629, + options + ); + + /** + * The `extrinsicIndex` (second param) is being tested for a non-zero + * index here. + */ + const extrinsic = blocksService['fetchExtrinsicByIndex'](block, 2); + + expect(JSON.stringify(sanitizeNumbers(extrinsic))).toEqual( + JSON.stringify(block789629Extrinsic) + ); + }); + + it("Throw an error when `extrinsicIndex` doesn't exist", async () => { + const block = await blocksService.fetchBlock( + blockHash789629, + options + ); + + expect(() => { + blocksService['fetchExtrinsicByIndex'](block, 5); + }).toThrow( + new BadRequest('Requested `extrinsicIndex` does not exist') + ); + }); + + it('Throw an error when param `extrinsicIndex` is less than 0', () => { + expect(() => { + parseNumberOrThrow( + '-5', + '`exstrinsicIndex` path param is not a number' + ); + }).toThrow( + new BadRequest('`exstrinsicIndex` path param is not a number') + ); + }); + }); }); diff --git a/src/services/blocks/BlocksService.ts b/src/services/blocks/BlocksService.ts index ac5396ac1..f9e201610 100644 --- a/src/services/blocks/BlocksService.ts +++ b/src/services/blocks/BlocksService.ts @@ -17,11 +17,12 @@ import { AnyJson, Codec, Registry } from '@polkadot/types/types'; import { u8aToHex } from '@polkadot/util'; import { blake2AsU8a } from '@polkadot/util-crypto'; import { CalcFee } from '@substrate/calc'; -import { InternalServerError } from 'http-errors'; +import { BadRequest, InternalServerError } from 'http-errors'; import { IBlock, IExtrinsic, + IExtrinsicIndex, ISanitizedCall, ISanitizedEvent, isFrameMethod, @@ -249,6 +250,31 @@ export class BlocksService extends AbstractService { }; } + /** + * + * @param block Takes in a block which is the result of `BlocksService.fetchBlock` + * @param extrinsicIndex Parameter passed into the request + */ + fetchExtrinsicByIndex( + block: IBlock, + extrinsicIndex: number + ): IExtrinsicIndex { + if (extrinsicIndex > block.extrinsics.length - 1) { + throw new BadRequest('Requested `extrinsicIndex` does not exist'); + } + + const { hash, number } = block; + const height = number.unwrap().toString(10); + + return { + at: { + height, + hash, + }, + extrinsics: block.extrinsics[extrinsicIndex], + }; + } + /** * Extract extrinsics from a block. * diff --git a/src/services/test-helpers/mock/parseNumberOrThrow.ts b/src/services/test-helpers/mock/parseNumberOrThrow.ts new file mode 100644 index 000000000..66882073f --- /dev/null +++ b/src/services/test-helpers/mock/parseNumberOrThrow.ts @@ -0,0 +1,11 @@ +import { BadRequest } from 'http-errors'; + +export function parseNumberOrThrow(n: string, errorMessage: string): number { + const num = Number(n); + + if (!Number.isInteger(num) || num < 0) { + throw new BadRequest(errorMessage); + } + + return num; +} diff --git a/src/services/test-helpers/responses/blocks/block789629Extrinsic.json b/src/services/test-helpers/responses/blocks/block789629Extrinsic.json new file mode 100644 index 000000000..11fe3e94b --- /dev/null +++ b/src/services/test-helpers/responses/blocks/block789629Extrinsic.json @@ -0,0 +1,37 @@ +{ + "at": { + "height": "789629", + "hash": "0x7b713de604a99857f6c25eacc115a4f28d2611a23d9ddff99ab0e4f1c17a8578" + }, + "extrinsics": { + "method": { + "pallet": "parachains", + "method": "setHeads" + }, + "signature": null, + "nonce": null, + "args": { + "heads": [] + }, + "tip": null, + "hash": "0xcf52705d1ade64fc0b05859ac28358c0770a217dd76b75e586ae848c56ae810d", + "info": {}, + "events": [ + { + "method": { + "pallet": "system", + "method": "ExtrinsicSuccess" + }, + "data": [ + { + "weight": "1000000000", + "class": "Mandatory", + "paysFee": "Yes" + } + ] + } + ], + "success": true, + "paysFee": false + } +} diff --git a/src/types/responses/Extrinsic.ts b/src/types/responses/Extrinsic.ts index 893d3599c..0412b3f1c 100644 --- a/src/types/responses/Extrinsic.ts +++ b/src/types/responses/Extrinsic.ts @@ -9,7 +9,7 @@ import { Sr25519Signature, } from '@polkadot/types/interfaces'; -import { IFrameMethod, ISanitizedArgs, ISanitizedEvent } from '.'; +import { IAt, IFrameMethod, ISanitizedArgs, ISanitizedEvent } from '.'; export interface IExtrinsic { method: string | IFrameMethod; @@ -29,3 +29,8 @@ export interface ISignature { signature: EcdsaSignature | Ed25519Signature | Sr25519Signature; signer: Address; } + +export interface IExtrinsicIndex { + at: IAt; + extrinsics: IExtrinsic; +}