Skip to content

Commit

Permalink
feat: add route /blocks/{blockId}/extrinsics/{extrinsicIndex} (#400)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update docs/src/openapi-v1.yaml

Co-authored-by: Zeke Mostov <[email protected]>

* Update docs/src/openapi-v1.yaml

Co-authored-by: Zeke Mostov <[email protected]>

* 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 <[email protected]>

* fix: getExtrinsicByExtrinsicIndex => getExtrinsicByIndex

* fix: getExtrinsicByIndex => getExtrinsicByTimepoint

Co-authored-by: Zeke Mostov <[email protected]>
  • Loading branch information
TarikGul and emostov committed Jan 26, 2021
1 parent b95f913 commit 6507ce7
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 3 deletions.
59 changes: 59 additions & 0 deletions docs/src/openapi-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/defaultControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/dockMainnetControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/dockTestnetControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/kulupuControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/mandalaControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
66 changes: 66 additions & 0 deletions src/controllers/blocks/BlocksExtrinsicsController.ts
Original file line number Diff line number Diff line change
@@ -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<BlocksService> {
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<INumberParam> = async (
{
params: { blockId, extrinsicIndex },
query: { eventDocs, extrinsicDocs },
},
res
): Promise<void> => {
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)
);
};
}
1 change: 1 addition & 0 deletions src/controllers/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as Blocks } from './BlocksController';
export { default as BlocksExtrinsics } from './BlocksExtrinsicsController';
3 changes: 2 additions & 1 deletion src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,6 +20,7 @@ import {
*/
export const controllers = {
Blocks,
BlocksExtrinsics,
AccountsBalanceInfo,
AccountsStakingInfo,
AccountsVestingInfo,
Expand Down
55 changes: 55 additions & 0 deletions src/services/blocks/BlocksService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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')
);
});
});
});
28 changes: 27 additions & 1 deletion src/services/blocks/BlocksService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand Down
11 changes: 11 additions & 0 deletions src/services/test-helpers/mock/parseNumberOrThrow.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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
}
}
7 changes: 6 additions & 1 deletion src/types/responses/Extrinsic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,3 +29,8 @@ export interface ISignature {
signature: EcdsaSignature | Ed25519Signature | Sr25519Signature;
signer: Address;
}

export interface IExtrinsicIndex {
at: IAt;
extrinsics: IExtrinsic;
}

0 comments on commit 6507ce7

Please sign in to comment.