Skip to content

Commit beb02ba

Browse files
Imod7TarikGul
andauthored
feat: add metadata versions endpoints (#1424)
* feat: add metadata versions endpoints * updated docs * fix typo & renamed variables * made the metadata version check dynamic, replacing the specific check for versions 14 and 15 - transaction material with metadata returns metadata regardless of the 'metadata' query param - updated docs * added changes from Tarik's review - throw an error if newer call is not available for older blocks - removed some comments left in node transaction spec file * Tarik's change on regExPattern check Co-authored-by: Tarik Gul <[email protected]> * added Tarik's suggestion also in transaction material * fix typo --------- Co-authored-by: Tarik Gul <[email protected]>
1 parent c73c801 commit beb02ba

File tree

8 files changed

+410
-94
lines changed

8 files changed

+410
-94
lines changed

docs/dist/app.bundle.js

+57-57
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/openapi-v1.yaml

+110
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,53 @@ paths:
11531153
application/json:
11541154
schema:
11551155
$ref: '#/components/schemas/Error'
1156+
/transaction/material/{metadataVersion}:
1157+
get:
1158+
tags:
1159+
- transaction
1160+
summary: Get all the network information needed to construct a transaction offline and
1161+
the version of metadata specified in `metadataVersion`.
1162+
description: Returns all the materials necessary for constructing any signed transactions
1163+
offline.
1164+
operationId: getTransactionMaterialwithVersionedMetadata
1165+
parameters:
1166+
- name: metadataVersion
1167+
in: path
1168+
description: The version of metadata. The input is expected in a `vX` format, where `X`
1169+
represents the version number (e.g. `v14`, `v15`). By default, metadata is outputted
1170+
in 'json' format, unless the `metadata` query parameter is provided, in which case it
1171+
can be either in 'json' or 'scale' format.
1172+
required: true
1173+
schema:
1174+
type: string
1175+
- name: at
1176+
in: query
1177+
description: Block at which to retrieve the transaction construction
1178+
material.
1179+
required: false
1180+
schema:
1181+
type: string
1182+
description: Block identifier, as the block height or block hash.
1183+
format: unsignedInteger or $hex
1184+
- name: metadata
1185+
in: query
1186+
description: Specifies the format of the metadata to be returned. Accepted values are
1187+
'json', and 'scale'. 'json' being the decoded metadata, and 'scale' being the SCALE encoded metadata.
1188+
schema:
1189+
type: string
1190+
responses:
1191+
"200":
1192+
description: successful operation
1193+
content:
1194+
application/json:
1195+
schema:
1196+
$ref: '#/components/schemas/TransactionMaterial'
1197+
"400":
1198+
description: invalid blockId supplied for at query param
1199+
content:
1200+
application/json:
1201+
schema:
1202+
$ref: '#/components/schemas/Error'
11561203
/pallets/assets/{assetId}/asset-info:
11571204
get:
11581205
tags:
@@ -1753,6 +1800,69 @@ paths:
17531800
schema:
17541801
type: object
17551802
description: Response is dependent on the runtime metadata contents.
1803+
/runtime/metadata/{metadataVersion}:
1804+
get:
1805+
tags:
1806+
- runtime
1807+
summary: Get the requested version of runtime metadata in decoded, JSON form.
1808+
description: >-
1809+
Returns the requested version of runtime metadata as a JSON object.
1810+
Substrate Reference:
1811+
- FRAME Support: https://crates.parity.io/frame_support/metadata/index.html
1812+
- Knowledge Base: https://substrate.dev/docs/en/knowledgebase/runtime/metadata
1813+
parameters:
1814+
- name: metadataVersion
1815+
in: path
1816+
description: The version of metadata. The input is expected in a `vX` format, where `X`
1817+
represents the version number (e.g. `v14`, `v15`).
1818+
required: true
1819+
schema:
1820+
type: string
1821+
- name: at
1822+
in: query
1823+
description: Block at which to retrieve the metadata at.
1824+
required: false
1825+
schema:
1826+
type: string
1827+
description: Block identifier, as the block height or block hash.
1828+
format: unsignedInteger or $hex
1829+
responses:
1830+
"200":
1831+
description: successful operation
1832+
content:
1833+
application/json:
1834+
schema:
1835+
type: object
1836+
description: Response is dependent on the runtime metadata contents.
1837+
/runtime/metadata/versions:
1838+
get:
1839+
tags:
1840+
- runtime
1841+
summary: Get the available versions of runtime metadata.
1842+
description: >-
1843+
Returns the available versions of runtime metadata.
1844+
Substrate Reference:
1845+
- FRAME Support: https://crates.parity.io/frame_support/metadata/index.html
1846+
- Knowledge Base: https://substrate.dev/docs/en/knowledgebase/runtime/metadata
1847+
parameters:
1848+
- name: at
1849+
in: query
1850+
description: Block at which to retrieve the metadata versions at.
1851+
required: false
1852+
schema:
1853+
type: string
1854+
description: Block identifier, as the block height or block hash.
1855+
format: unsignedInteger or $hex
1856+
responses:
1857+
"200":
1858+
description: successful operation
1859+
content:
1860+
application/json:
1861+
schema:
1862+
type: array
1863+
items:
1864+
type: string
1865+
description: An array with the available metadata versions.
17561866
/runtime/code:
17571867
get:
17581868
tags:

src/controllers/runtime/RuntimeMetadataController.ts

+71-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
1+
// Copyright 2017-2024 Parity Technologies (UK) Ltd.
22
// This file is part of Substrate API Sidecar.
33
//
44
// Substrate API Sidecar is free software: you can redistribute it and/or modify
@@ -15,6 +15,7 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
import { ApiPromise } from '@polkadot/api';
18+
import { u32, Vec } from '@polkadot/types';
1819
import { RequestHandler } from 'express';
1920

2021
import { RuntimeMetadataService } from '../../services';
@@ -23,6 +24,10 @@ import AbstractController from '../AbstractController';
2324
/**
2425
* GET the chain's metadata.
2526
*
27+
* Path params:
28+
* - (Optional) `metadataVersion`: The specific version of the Metadata to query.
29+
* The input must conform to the `vX` format, where `X` represents the version number (examples: 'v14', 'v15').
30+
*
2631
* Query:
2732
* - (Optional) `at`: Block hash or height at which to query. If not provided, queries
2833
* finalized head.
@@ -41,7 +46,11 @@ export default class RuntimeMetadataController extends AbstractController<Runtim
4146
}
4247

4348
protected initRoutes(): void {
44-
this.safeMountAsyncGetHandlers([['', this.getMetadata]]);
49+
this.safeMountAsyncGetHandlers([
50+
['/', this.getMetadata],
51+
['/versions', this.getMetadataAvailableVersions],
52+
['/:metadataVersion', this.getMetadataVersioned],
53+
]);
4554
}
4655

4756
/**
@@ -65,4 +74,64 @@ export default class RuntimeMetadataController extends AbstractController<Runtim
6574
metadataOpts: { registry, version: metadata.version },
6675
});
6776
};
77+
78+
/**
79+
* Get the chain's metadata at a specific version in a decoded, JSON format.
80+
*
81+
* @param _req Express Request
82+
* @param res Express Response
83+
*/
84+
private getMetadataVersioned: RequestHandler = async (
85+
{ params: { metadataVersion }, query: { at } },
86+
res,
87+
): Promise<void> => {
88+
const hash = await this.getHashFromAt(at);
89+
90+
const api = at ? await this.api.at(hash) : this.api;
91+
92+
// Validation of the `metadataVersion` path parameter.
93+
const metadataV = metadataVersion.slice(1);
94+
const version = this.parseNumberOrThrow(
95+
metadataV,
96+
`Version ${metadataV.toString()} of metadata provided is not a number.`,
97+
);
98+
99+
const regExPattern = new RegExp('^[vV][0-9]+$');
100+
if (!regExPattern.test(metadataVersion)) {
101+
throw new Error(
102+
`${metadataVersion} input is not of the expected 'vX' format, where 'X' represents the version number (examples: 'v14', 'v15').`,
103+
);
104+
}
105+
106+
let availableVersions = [];
107+
try {
108+
availableVersions = (await api.call.metadata.metadataVersions()).toJSON() as unknown as Vec<u32>;
109+
} catch {
110+
throw new Error(`Function 'api.call.metadata.metadataVersions()' is not available at this block height.`);
111+
}
112+
if (version && !availableVersions?.includes(version as unknown as u32)) {
113+
throw new Error(`Version ${version} of Metadata is not available.`);
114+
}
115+
116+
const registry = api.registry;
117+
const metadata = await this.service.fetchMetadataVersioned(api, version);
118+
119+
RuntimeMetadataController.sanitizedSend(res, metadata, {
120+
metadataOpts: { registry, version },
121+
});
122+
};
123+
124+
/**
125+
* Get the available versions of chain's metadata.
126+
*
127+
* @param _req Express Request
128+
* @param res Express Response
129+
*/
130+
private getMetadataAvailableVersions: RequestHandler = async ({ query: { at } }, res): Promise<void> => {
131+
const hash = await this.getHashFromAt(at);
132+
133+
const metadataVersions = await this.service.fetchMetadataVersions(hash);
134+
135+
RuntimeMetadataController.sanitizedSend(res, metadataVersions, {});
136+
};
68137
}

src/controllers/transaction/TransactionMaterialController.ts

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
1+
// Copyright 2017-2024 Parity Technologies (UK) Ltd.
22
// This file is part of Substrate API Sidecar.
33
//
44
// Substrate API Sidecar is free software: you can redistribute it and/or modify
@@ -15,6 +15,7 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
import { ApiPromise } from '@polkadot/api';
18+
import { u32, Vec } from '@polkadot/types';
1819
import { RequestHandler } from 'express';
1920

2021
import { TransactionMaterialService } from '../../services';
@@ -25,6 +26,10 @@ export type MetadataOpts = 'json' | 'scale';
2526
/**
2627
* GET all the network information needed to construct a transaction offline.
2728
*
29+
* Path params:
30+
* - (Optional) `metadataVersion`: The specific version of the Metadata to query.
31+
* The input must conform to the `vX` format, where `X` represents the version number (examples: 'v14', 'v15').
32+
*
2833
* Query
2934
* - (Optional) `metadata`: It accepts `json`, or `scale` values. If it is not present,
3035
* the metadata field will not be included.
@@ -59,7 +64,10 @@ export default class TransactionMaterialController extends AbstractController<Tr
5964
}
6065

6166
protected initRoutes(): void {
62-
this.safeMountAsyncGetHandlers([['', this.getTransactionMaterial]]);
67+
this.safeMountAsyncGetHandlers([
68+
['/', this.getTransactionMaterial],
69+
['/:metadataVersion', this.getTransactionMaterialwithVersionedMetadata],
70+
]);
6371
}
6472

6573
/**
@@ -98,4 +106,51 @@ export default class TransactionMaterialController extends AbstractController<Tr
98106

99107
return false;
100108
}
109+
110+
/**
111+
* Get the chain's metadata at the requested version in JSON or scale format
112+
* depending on the `metadata` query param.
113+
*
114+
* @param _req Express Request
115+
* @param res Express Response
116+
*/
117+
private getTransactionMaterialwithVersionedMetadata: RequestHandler = async (
118+
{ params: { metadataVersion }, query: { at, metadata } },
119+
res,
120+
): Promise<void> => {
121+
const hash = await this.getHashFromAt(at);
122+
123+
const api = at ? await this.api.at(hash) : this.api;
124+
125+
// Validation of the `metadataVersion` path parameter.
126+
const metadataV = metadataVersion.slice(1);
127+
const version = this.parseNumberOrThrow(
128+
metadataV,
129+
`Version ${metadataV.toString()} of metadata provided is not a number.`,
130+
);
131+
132+
const regExPattern = new RegExp('^[vV][0-9]+$');
133+
if (!regExPattern.test(metadataVersion)) {
134+
throw new Error(
135+
`${metadataVersion} input is not of the expected 'vX' format, where 'X' represents the version number (examples: 'v14', 'v15').`,
136+
);
137+
}
138+
139+
let availableVersions = [];
140+
try {
141+
availableVersions = (await api.call.metadata.metadataVersions()).toJSON() as unknown as Vec<u32>;
142+
} catch {
143+
throw new Error(`Function 'api.call.metadata.metadataVersions()' is not available at this block height.`);
144+
}
145+
if (version && !availableVersions?.includes(version as unknown as u32)) {
146+
throw new Error(`Version ${version} of Metadata is not available.`);
147+
}
148+
149+
const metadataArg = this.parseMetadataArgs(metadata);
150+
151+
TransactionMaterialController.sanitizedSend(
152+
res,
153+
await this.service.fetchTransactionMaterialwithVersionedMetadata(api, hash, metadataArg, version),
154+
);
155+
};
101156
}

src/services/node/NodeTransactionPoolService.spec.ts

+9-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
1+
// Copyright 2017-2024 Parity Technologies (UK) Ltd.
22
// This file is part of Substrate API Sidecar.
33
//
44
// Substrate API Sidecar is free software: you can redistribute it and/or modify
@@ -18,29 +18,18 @@
1818
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
1919
import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers';
2020
import { polkadotRegistryV9300 } from '../../test-helpers/registries';
21-
import {
22-
// blockHash789629,
23-
defaultMockApi,
24-
pendingExtrinsics,
25-
} from '../test-helpers/mock';
21+
import { defaultMockApi, pendingExtrinsics } from '../test-helpers/mock';
2622
import transactionPoolResponse from '../test-helpers/responses/node/transactionPool.json';
2723
import transactionPoolWithTipResponse from '../test-helpers/responses/node/transactionPoolWithTip.json';
2824
import transactionPoolWithTipOperationalResponse from '../test-helpers/responses/node/transactionPoolWithTipOperational.json';
2925
import { NodeTransactionPoolService } from '.';
3026

31-
const nodeTranstionPoolService = new NodeTransactionPoolService(defaultMockApi);
27+
const nodeTransactionPoolService = new NodeTransactionPoolService(defaultMockApi);
3228

3329
describe('NodeTransactionPoolService', () => {
3430
describe('fetchTransactionPool', () => {
3531
it('works when ApiPromiseWorks (no txs)', async () => {
36-
expect(
37-
sanitizeNumbers(
38-
await nodeTranstionPoolService.fetchTransactionPool(
39-
// blockHash789629
40-
false,
41-
),
42-
),
43-
).toStrictEqual({ pool: [] });
32+
expect(sanitizeNumbers(await nodeTransactionPoolService.fetchTransactionPool(false))).toStrictEqual({ pool: [] });
4433
});
4534

4635
it('works when ApiPromiseWorks (1 tx)', async () => {
@@ -52,14 +41,9 @@ describe('NodeTransactionPoolService', () => {
5241
const pool = defaultMockApi.createType('Vec<Extrinsic>', [ext]);
5342
(defaultMockApi.rpc.author as any).pendingExtrinsics = () => Promise.resolve().then(() => pool);
5443

55-
expect(
56-
sanitizeNumbers(
57-
await nodeTranstionPoolService.fetchTransactionPool(
58-
// blockHash789629
59-
false,
60-
),
61-
),
62-
).toStrictEqual(transactionPoolResponse);
44+
expect(sanitizeNumbers(await nodeTransactionPoolService.fetchTransactionPool(false))).toStrictEqual(
45+
transactionPoolResponse,
46+
);
6347

6448
(defaultMockApi.rpc.author as any).pendingExtrinsics = pendingExtrinsics;
6549
});
@@ -74,7 +58,7 @@ describe('NodeTransactionPoolService', () => {
7458
const pool = polkadotRegistryV9300.createType('Vec<Extrinsic>', [normalExt]);
7559
(defaultMockApi.rpc.author as any).pendingExtrinsics = () => Promise.resolve().then(() => pool);
7660

77-
expect(sanitizeNumbers(await nodeTranstionPoolService.fetchTransactionPool(true))).toStrictEqual(
61+
expect(sanitizeNumbers(await nodeTransactionPoolService.fetchTransactionPool(true))).toStrictEqual(
7862
transactionPoolWithTipResponse,
7963
);
8064

@@ -89,7 +73,7 @@ describe('NodeTransactionPoolService', () => {
8973
const pool = polkadotRegistryV9300.createType('Vec<Extrinsic>', [operationalExt]);
9074
(defaultMockApi.rpc.author as any).pendingExtrinsics = () => Promise.resolve().then(() => pool);
9175

92-
expect(sanitizeNumbers(await nodeTranstionPoolService.fetchTransactionPool(true))).toStrictEqual(
76+
expect(sanitizeNumbers(await nodeTransactionPoolService.fetchTransactionPool(true))).toStrictEqual(
9377
transactionPoolWithTipOperationalResponse,
9478
);
9579

0 commit comments

Comments
 (0)