diff --git a/README.md b/README.md index c34bff0a9..66d6461fa 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,10 @@ a 32-byte hex string (`0x` followed by 64 hexadecimal digits) that denotes the b - [`/node/version` fetch information about the Substrates node's implementation and versioning.](src/controllers/node/NodeVersionController.ts) +- [`/runtime/code` fetch the Wasm code blob of the Substrate runtime.](src/controllers/runtime/RuntimeCodeController.ts) + +- [`/runtime/spec` version information of the Substrate runtime.](src/controllers/runtime/RuntimeSpecController.ts) + - [`/claims/ADDRESS` fetch claims data for an Ethereum `ADDRESS`.](src/controllers/claims/ClaimsController.ts) - [`/claims/ADDRESS/NUMBER` fetch claims data for an Ethereum `ADDRESS` at the block identified by 'NUMBER`.](src/controllers/claims/ClaimsController.ts) diff --git a/openapi/openapi-proposal.yaml b/openapi/openapi-proposal.yaml index 9fcdd1fdb..9ffe382af 100755 --- a/openapi/openapi-proposal.yaml +++ b/openapi/openapi-proposal.yaml @@ -984,7 +984,7 @@ paths: get: tags: - runtime - summary: Get version information of the runtime. + summary: Get version information of the Substrate runtime. description: Returns version information related to the runtime. parameters: - name: at @@ -1620,16 +1620,17 @@ components: - Live implVersion: type: string - description: Version of the implementation of the specification. Non-consensus-breaking + description: Version of the implementation specification. Non-consensus-breaking optimizations are about the only changes that could be made which would - result in only the `impl_version` changing. + result in only the `impl_version` changing. The `impl_version` is set to 0 + when `spec_version` is incremented. specName: type: string description: Identifies the different Substrate runtimes. specVersion: type: string - description: version of the runtime specification - txVersion: + description: Version of the runtime specification. + transactionVersion: type: string description: All existing dispatches are fully compatible when this number doesn't change. This number must change when an existing dispatchable diff --git a/src/controllers/runtime/RuntimeCodeController.ts b/src/controllers/runtime/RuntimeCodeController.ts new file mode 100644 index 000000000..c5dfd9065 --- /dev/null +++ b/src/controllers/runtime/RuntimeCodeController.ts @@ -0,0 +1,51 @@ +import { ApiPromise } from '@polkadot/api'; +import { RequestHandler } from 'express'; + +import { RuntimeCodeService } from '../../services'; +import AbstractController from '../AbstractController'; + +/** + * Get the Wasm code blob of the Substrate runtime. + * + * Query: + * - (Optional)`at`: Block at which to retrieve runtime version information at. Block + * identifier, as the block height or block hash. Defaults to most recent block. + * + * Returns: + * - `at`: Block number and hash at which the call was made. + * - `code`: Runtime code Wasm blob. + */ +export default class RuntimeCodeController extends AbstractController< + RuntimeCodeService +> { + constructor(api: ApiPromise) { + super(api, '/runtime/code', new RuntimeCodeService(api)); + this.initRoutes(); + } + + protected initRoutes(): void { + this.safeMountAsyncGetHandlers([['', this.getCodeAtBlock]]); + } + + /** + * Get the chain's latest metadata in a decoded, JSON format. + * + * @param _req Express Request + * @param res Express Response + */ + + private getCodeAtBlock: RequestHandler = async ( + { query: { at } }, + res + ): Promise => { + const hash = + typeof at === 'string' + ? await this.getHashForBlock(at) + : await this.api.rpc.chain.getFinalizedHead(); + + RuntimeCodeController.sanitizedSend( + res, + await this.service.fetchCode(hash) + ); + }; +} diff --git a/src/controllers/runtime/RuntimeSpecController.ts b/src/controllers/runtime/RuntimeSpecController.ts new file mode 100644 index 000000000..288241a72 --- /dev/null +++ b/src/controllers/runtime/RuntimeSpecController.ts @@ -0,0 +1,55 @@ +import { ApiPromise } from '@polkadot/api'; +import { RequestHandler } from 'express'; + +import { RuntimeSpecService } from '../../services'; +import AbstractController from '../AbstractController'; + +/** + * Get version information of the Substrate runtime. + * + * Query: + * - (Optional)`at`: Block at which to retrieve runtime version information. Block + * identifier, as the block height or block hash. Defaults to most recent block. + * + * Returns: + * - `at`: Block number and hash at which the call was made. + * - `authoringVersion`: The version of the authorship interface. An authoring node + * will not attempt to author blocks unless this is equal to its native runtime. + * - `chainType`: Type of the chain. + * - `implVersion`: Version of the implementation specification. Non-consensus-breaking + * optimizations are about the only changes that could be made which would + * result in only the `impl_version` changing. The `impl_version` is set to 0 + * when `spec_version` is incremented. + * - `specName`: Identifies the spec name for the current runtime. + * - `specVersion`: Version of the runtime specification. + * - `transactionVersion`: All existing dispatches are fully compatible when this + * number doesn't change. This number must change when an existing dispatchable + * (module ID, dispatch ID) is changed, either through an alteration in its + * user-level semantics, a parameter added/removed/changed, a dispatchable + * its index. + * - `properties`: Arbitrary properties defined in the chain spec. + */ +export default class RuntimeSpecController extends AbstractController< + RuntimeSpecService +> { + constructor(api: ApiPromise) { + super(api, '/runtime/spec', new RuntimeSpecService(api)); + this.initRoutes(); + } + + protected initRoutes(): void { + this.safeMountAsyncGetHandlers([['', this.getSpec]]); + } + + private getSpec: RequestHandler = async ({ query: { at } }, res) => { + const hash = + typeof at === 'string' + ? await this.getHashForBlock(at) + : await this.api.rpc.chain.getFinalizedHead(); + + RuntimeSpecController.sanitizedSend( + res, + await this.service.fetchSpec(hash) + ); + }; +} diff --git a/src/controllers/runtime/index.ts b/src/controllers/runtime/index.ts index de4d63830..91694536f 100644 --- a/src/controllers/runtime/index.ts +++ b/src/controllers/runtime/index.ts @@ -1 +1,3 @@ export { default as Metadata } from './RuntimeMetadataController'; +export { default as RuntimeCode } from './RuntimeCodeController'; +export { default as RuntimeSpec } from './RuntimeSpecController'; diff --git a/src/main.ts b/src/main.ts index 255d795dd..4415b0367 100644 --- a/src/main.ts +++ b/src/main.ts @@ -74,6 +74,8 @@ async function main() { const nodeTransactionPoolController = new controllers.NodeTransactionPool( api ); + const runtimeCodeController = new controllers.RuntimeCode(api); + const runtimeSpecController = new controllers.RuntimeSpec(api); const claimsController = new controllers.Claims(api); const txArtifactsController = new controllers.TransactionMaterial(api); const txFeeEstimateController = new controllers.TransactionFeeEstimate(api); @@ -93,6 +95,8 @@ async function main() { nodeNetworkController, nodeVersionController, nodeTransactionPoolController, + runtimeCodeController, + runtimeSpecController, claimsController, txArtifactsController, txFeeEstimateController, diff --git a/src/services/accounts/AccountsStakingInfoService.ts b/src/services/accounts/AccountsStakingInfoService.ts index bce591dda..fc39f45b8 100644 --- a/src/services/accounts/AccountsStakingInfoService.ts +++ b/src/services/accounts/AccountsStakingInfoService.ts @@ -23,7 +23,7 @@ export class AccountsStakingInfoService extends AbstractService { const at = { hash, - height: header.number.toNumber().toString(10), + height: header.number.unwrap().toString(10), }; if (controllerOption.isNone) { diff --git a/src/services/runtime/RuntimeCodeService.spec.ts b/src/services/runtime/RuntimeCodeService.spec.ts new file mode 100644 index 000000000..54bfe3ff3 --- /dev/null +++ b/src/services/runtime/RuntimeCodeService.spec.ts @@ -0,0 +1,18 @@ +import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers'; +import { blockHash789629, mockApi } from '../test-helpers/mock'; +import * as codeResponse from '../test-helpers/responses/runtime/code789629.json'; +import { RuntimeCodeService } from './RuntimeCodeService'; + +const runtimeCodeService = new RuntimeCodeService(mockApi); + +describe('RuntimeCodeService', () => { + describe('fetchCode', () => { + it('works when ApiPromise works', async () => { + expect( + sanitizeNumbers( + await runtimeCodeService.fetchCode(blockHash789629) + ) + ).toStrictEqual(codeResponse); + }); + }); +}); diff --git a/src/services/runtime/RuntimeCodeService.ts b/src/services/runtime/RuntimeCodeService.ts new file mode 100644 index 000000000..e4bc60a53 --- /dev/null +++ b/src/services/runtime/RuntimeCodeService.ts @@ -0,0 +1,32 @@ +import { Option, Raw } from '@polkadot/types'; +import { BlockHash } from '@polkadot/types/interfaces'; +import { IMetadataCode } from 'src/types/responses'; + +import { AbstractService } from '../AbstractService'; + +// https://github.com/shawntabrizi/substrate-graph-benchmarks/blob/ae9b82f/js/extensions/known-keys.js#L21 +export const CODE_KEY = '0x3a636f6465'; + +export class RuntimeCodeService extends AbstractService { + /** + * Fetch `Metadata` in decoded JSON form. + * + * @param hash `BlockHash` to make call at + */ + async fetchCode(hash: BlockHash): Promise { + const api = await this.ensureMeta(hash); + + const [code, { number }] = await Promise.all([ + api.rpc.state.getStorage(CODE_KEY, hash), + api.rpc.chain.getHeader(hash), + ]); + + return { + at: { + hash, + height: number.unwrap().toString(10), + }, + code: code as Option, + }; + } +} diff --git a/src/services/runtime/RuntimeSpecService.spec.ts b/src/services/runtime/RuntimeSpecService.spec.ts new file mode 100644 index 000000000..7aa5e7852 --- /dev/null +++ b/src/services/runtime/RuntimeSpecService.spec.ts @@ -0,0 +1,18 @@ +import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers'; +import { blockHash789629, mockApi } from '../test-helpers/mock'; +import * as response from '../test-helpers/responses/runtime/spec.json'; +import { RuntimeSpecService } from './RuntimeSpecService'; + +const runtimeSpecService = new RuntimeSpecService(mockApi); + +describe('RuntimeSpecService', () => { + describe('fetchSpec', () => { + it('works when ApiPromise works', async () => { + expect( + sanitizeNumbers( + await runtimeSpecService.fetchSpec(blockHash789629) + ) + ).toStrictEqual(response); + }); + }); +}); diff --git a/src/services/runtime/RuntimeSpecService.ts b/src/services/runtime/RuntimeSpecService.ts new file mode 100644 index 000000000..ffae506d8 --- /dev/null +++ b/src/services/runtime/RuntimeSpecService.ts @@ -0,0 +1,33 @@ +import { BlockHash } from '@polkadot/types/interfaces'; +import { IRuntimeSpec } from 'src/types/responses'; + +import { AbstractService } from '../AbstractService'; +export class RuntimeSpecService extends AbstractService { + async fetchSpec(hash: BlockHash): Promise { + const [ + { + authoringVersion, + specName, + specVersion, + transactionVersion, + implVersion, + }, + chainType, + properties, + ] = await Promise.all([ + this.api.rpc.state.getRuntimeVersion(hash), + this.api.rpc.system.chainType(), + this.api.rpc.system.properties(), + ]); + + return { + authoringVersion, + transactionVersion, + implVersion, + specName, + specVersion, + chainType, + properties, + }; + } +} diff --git a/src/services/runtime/index.ts b/src/services/runtime/index.ts index 015841544..8354625a9 100644 --- a/src/services/runtime/index.ts +++ b/src/services/runtime/index.ts @@ -1 +1,3 @@ export * from './RuntimeMetadataService'; +export * from './RuntimeCodeService'; +export * from './RuntimeSpecService'; diff --git a/src/services/test-helpers/mock/mockApi.ts b/src/services/test-helpers/mock/mockApi.ts index cb66bb3fb..8a45a19cc 100644 --- a/src/services/test-helpers/mock/mockApi.ts +++ b/src/services/test-helpers/mock/mockApi.ts @@ -51,6 +51,7 @@ const getRuntimeVersion = () => transactionVersion: polkadotRegistry.createType('u32', 2), implVersion: polkadotRegistry.createType('u32', 0), implName: polkadotRegistry.createType('Text', 'parity-polkadot'), + authoringVersion: polkadotRegistry.createType('u32', 0), }; }); @@ -202,6 +203,27 @@ export const queryInfoBalancesTransfer = ( export const submitExtrinsic = (_extrinsic: string): Promise => Promise.resolve().then(() => polkadotRegistry.createType('Hash')); +const getStorage = () => + Promise.resolve().then(() => + polkadotRegistry.createType('Option', '0x') + ); + +const chainType = () => + Promise.resolve().then(() => + polkadotRegistry.createType('ChainType', { + Live: null, + }) + ); + +const properties = () => + Promise.resolve().then(() => + polkadotRegistry.createType('ChainProperties', { + ss58Format: '0', + tokenDecimals: '12', + tokenSymbol: 'DOT', + }) + ); + const getFinalizedHead = () => Promise.resolve().then(() => blockHash789629); const health = () => @@ -310,6 +332,7 @@ export const mockApi = ({ state: { getRuntimeVersion, getMetadata, + getStorage, }, system: { chain, @@ -318,6 +341,8 @@ export const mockApi = ({ nodeRoles, localPeerId, version, + chainType, + properties, }, payment: { queryInfo: queryInfoBalancesTransfer, diff --git a/src/services/test-helpers/responses/runtime/code789629.json b/src/services/test-helpers/responses/runtime/code789629.json new file mode 100644 index 000000000..ed529e2a4 --- /dev/null +++ b/src/services/test-helpers/responses/runtime/code789629.json @@ -0,0 +1,7 @@ +{ + "at": { + "hash": "0x7b713de604a99857f6c25eacc115a4f28d2611a23d9ddff99ab0e4f1c17a8578", + "height": "789629" + }, + "code": "0x" +} diff --git a/src/services/test-helpers/responses/runtime/spec.json b/src/services/test-helpers/responses/runtime/spec.json new file mode 100644 index 000000000..e51551858 --- /dev/null +++ b/src/services/test-helpers/responses/runtime/spec.json @@ -0,0 +1,15 @@ +{ + "authoringVersion": "0", + "transactionVersion": "2", + "implVersion": "0", + "specName": "polkadot", + "specVersion": "16", + "chainType": { + "Live": null + }, + "properties": { + "ss58Format": "0", + "tokenDecimals": "12", + "tokenSymbol": "DOT" + } +} diff --git a/src/types/responses/MetadataCode.ts b/src/types/responses/MetadataCode.ts new file mode 100644 index 000000000..2e2472f2c --- /dev/null +++ b/src/types/responses/MetadataCode.ts @@ -0,0 +1,8 @@ +import { Option, Raw } from '@polkadot/types'; + +import { IAt } from '.'; + +export interface IMetadataCode { + at: IAt; + code: Option; +} diff --git a/src/types/responses/RuntimeSpec.ts b/src/types/responses/RuntimeSpec.ts new file mode 100644 index 000000000..cbd0968e7 --- /dev/null +++ b/src/types/responses/RuntimeSpec.ts @@ -0,0 +1,12 @@ +import { ChainProperties, ChainType } from '@polkadot/types/interfaces'; +import { Text, u32 } from '@polkadot/types/primitive'; + +export interface IRuntimeSpec { + authoringVersion: u32; + transactionVersion: u32; + implVersion: u32; + specName: Text; + specVersion: u32; + chainType: ChainType; + properties: ChainProperties; +} diff --git a/src/types/responses/index.ts b/src/types/responses/index.ts index 8d56af54e..17bdeade2 100644 --- a/src/types/responses/index.ts +++ b/src/types/responses/index.ts @@ -9,6 +9,8 @@ export * from './AccountStakingInfo'; export * from './AccountVestingInfo'; export * from './TransactionMaterial'; export * from './Extrinsic'; +export * from './MetadataCode'; +export * from './RuntimeSpec'; export * from './Payout'; export * from './EraPayouts'; export * from './AccountStakingPayouts';