Skip to content

Commit 59b88c9

Browse files
authored
feat: add useRcBlockFormat parameter for RC block response formatting (#1872)
* add IRcBlockInfo and IRcBlockObjectResponse types * add useRcBlockFormat to IBlockQueryParams * validate useRcBlockFormat in middleware * add RC block format helpers to AbstractController * add useRcBlockFormat support to BlocksController * add useRcBlockFormat support to BlocksExtrinsicsController * add useRcBlockFormat support to BlocksRawExtrinsicsController * add useRcBlockFormat support to all account controllers * add useRcBlockFormat support to pallets controllers * Update the docs
1 parent a80cbee commit 59b88c9

34 files changed

Lines changed: 1460 additions & 163 deletions

docs-v2/dist/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs-v2/guides/USE_RC_BLOCK_SPEC.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ The `useRcBlock` parameter is a boolean parameter that works in conjunction with
2424

2525
**Block Finalization Note**: The `useRcBlock` parameter does not make any assumptions about whether the block you pass is finalized or a best block. It is recommended to ensure the block you are passing is finalized if block finalization is important for your use case.
2626

27+
### useRcBlockFormat Parameter
28+
The `useRcBlockFormat` parameter controls the response format when using `useRcBlock=true`. This parameter is only valid when `useRcBlock=true` is specified.
29+
30+
**Values:**
31+
- `array` (default): Returns the standard array format with enhanced metadata
32+
- `object`: Wraps the response in an object containing relay chain block info and the parachain data array
33+
34+
**Validation**: Using `useRcBlockFormat` without `useRcBlock=true` will return a `400 Bad Request` error.
35+
2736
## Implementation: useRcBlock Query Parameter
2837

2938
### Core Functionality
@@ -40,6 +49,10 @@ GET /pallets/staking/progress?at=1000000&useRcBlock=true
4049
GET /accounts/{accountId}/balance-info?at=0x123abc&useRcBlock=true
4150
GET /blocks/head?useRcBlock=true
4251
GET /blocks/12345?at=12345&useRcBlock=true
52+
53+
# With useRcBlockFormat=object for wrapped response format
54+
GET /pallets/staking/progress?at=1000000&useRcBlock=true&useRcBlockFormat=object
55+
GET /accounts/{accountId}/balance-info?at=0x123abc&useRcBlock=true&useRcBlockFormat=object
4356
```
4457

4558
### Response Format Changes
@@ -52,7 +65,7 @@ Returns single response object (unchanged):
5265
}
5366
```
5467

55-
**With useRcBlock=true:**
68+
**With useRcBlock=true (default array format):**
5669
Returns array format with additional metadata:
5770
```json
5871
[{
@@ -65,6 +78,38 @@ Returns array format with additional metadata:
6578

6679
Or empty array `[]` if no corresponding Asset Hub block exists.
6780

81+
**With useRcBlock=true&useRcBlockFormat=object:**
82+
Returns object format wrapping the data with relay chain block info:
83+
```json
84+
{
85+
"rcBlock": {
86+
"hash": "0x1234567890abcdef...",
87+
"parentHash": "0xabcdef1234567890...",
88+
"number": "1000000"
89+
},
90+
"parachainDataPerBlock": [
91+
{
92+
// ... existing endpoint response data
93+
"rcBlockHash": "0x1234567890abcdef...",
94+
"rcBlockNumber": "1000000",
95+
"ahTimestamp": "1642694400"
96+
}
97+
]
98+
}
99+
```
100+
101+
Or with empty `parachainDataPerBlock` array if no corresponding Asset Hub block exists:
102+
```json
103+
{
104+
"rcBlock": {
105+
"hash": "0x1234567890abcdef...",
106+
"parentHash": "0xabcdef1234567890...",
107+
"number": "1000000"
108+
},
109+
"parachainDataPerBlock": []
110+
}
111+
```
112+
68113
## Supported Endpoints
69114

70115
### Block Endpoints Supporting useRcBlock:
@@ -90,7 +135,8 @@ When `useRcBlock=true` is used, responses include additional context fields:
90135
- `rcBlockHash`: The relay chain block hash
91136
- `rcBlockNumber`: The relay chain block number
92137
- `ahTimestamp`: The Asset Hub block timestamp
93-
- Array format prepares for future elastic scaling scenarios
138+
- Array format (default) prepares for future elastic scaling scenarios
139+
- Object format (`useRcBlockFormat=object`) provides relay chain block metadata wrapper with `rcBlock` info (hash, parentHash, number) and `parachainDataPerBlock` array
94140

95141
### Backward Compatibility
96142
- Defaults to `false`, maintaining existing functionality when not specified
@@ -104,9 +150,11 @@ When `useRcBlock=true` is used, responses include additional context fields:
104150

105151
### Validation Logic
106152
The `validateUseRcBlock` middleware ensures:
107-
1. **Boolean validation**: Must be "true" or "false" string
153+
1. **Boolean validation**: `useRcBlock` must be "true" or "false" string
108154
2. **Asset Hub requirement**: Only works when connected to Asset Hub
109155
3. **Relay chain availability**: Requires relay chain API configuration via `SAS_SUBSTRATE_MULTI_CHAIN_URL`
156+
4. **useRcBlockFormat dependency**: `useRcBlockFormat` requires `useRcBlock=true` to be specified
157+
5. **useRcBlockFormat values**: Must be either "array" or "object" string
110158

111159
## Multi-Block and Elastic Scaling Scenarios
112160

docs-v2/openapi-v1.yaml

Lines changed: 327 additions & 3 deletions
Large diffs are not rendered by default.

docs/dist/app.bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/openapi-v1.yaml

Lines changed: 327 additions & 3 deletions
Large diffs are not rendered by default.

src/controllers/AbstractController.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
IParaIdParam,
3333
IRangeQueryParam,
3434
} from 'src/types/requests';
35+
import { IRcBlockInfo, IRcBlockObjectResponse } from 'src/types/responses';
3536

3637
import { ApiPromiseRegistry } from '../../src/apiRegistry';
3738
import type { AssetHubInfo } from '../apiRegistry';
@@ -351,6 +352,42 @@ export default abstract class AbstractController<T extends AbstractService> {
351352
return ahHashes.map((ahHash) => ({ ahHash, rcBlockHash: rcHash, rcBlockNumber }));
352353
}
353354

355+
/**
356+
* Get minimal relay chain block info for the object format response.
357+
*
358+
* @param rcAt - Block identifier (hash or number) for the relay chain
359+
* @returns IRcBlockInfo containing hash, parentHash, and number
360+
*/
361+
protected async getRcBlockInfo(rcAt: unknown): Promise<IRcBlockInfo> {
362+
const rcApi = ApiPromiseRegistry.getApiByType('relay')[0]?.api;
363+
if (!rcApi) {
364+
throw new InternalServerError('Relay chain api must be available');
365+
}
366+
367+
const rcHash = await this.getHashFromAt(rcAt, { api: rcApi });
368+
const rcHeader = await rcApi.rpc.chain.getHeader(rcHash);
369+
370+
return {
371+
hash: rcHash.toHex(),
372+
parentHash: rcHeader.parentHash.toHex(),
373+
number: rcHeader.number.toString(),
374+
};
375+
}
376+
377+
/**
378+
* Format a response in the RC block object format.
379+
*
380+
* @param rcBlockInfo - The relay chain block info
381+
* @param parachainData - Array of parachain data (endpoint-specific responses)
382+
* @returns IRcBlockObjectResponse with rcBlock and parachainDataPerBlock
383+
*/
384+
protected formatRcBlockObjectResponse<T>(rcBlockInfo: IRcBlockInfo, parachainData: T[]): IRcBlockObjectResponse<T> {
385+
return {
386+
rcBlock: rcBlockInfo,
387+
parachainDataPerBlock: parachainData,
388+
};
389+
}
390+
354391
/**
355392
* Sanitize the numbers within the response body and then send the response
356393
* body using the original Express Response object.

src/controllers/accounts/AccountsAssetsController.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,22 @@ export default class AccountsAssetsController extends AbstractController<Account
9898
}
9999

100100
private getAssetBalances: RequestHandler = async (
101-
{ params: { address }, query: { at, useRcBlock, assets } },
101+
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, assets } },
102102
res,
103103
): Promise<void> => {
104+
const useObjectFormat = useRcBlockFormat === 'object';
105+
104106
if (useRcBlock === 'true') {
105107
const rcAtResults = await this.getHashFromRcAt(at);
106108

107-
// Return empty array if no Asset Hub blocks found
109+
// Handle empty results based on format
108110
if (rcAtResults.length === 0) {
109-
AccountsAssetsController.sanitizedSend(res, []);
111+
if (useObjectFormat) {
112+
const rcBlockInfo = await this.getRcBlockInfo(at);
113+
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
114+
} else {
115+
AccountsAssetsController.sanitizedSend(res, []);
116+
}
110117
return;
111118
}
112119

@@ -130,7 +137,13 @@ export default class AccountsAssetsController extends AbstractController<Account
130137
results.push(enhancedResult);
131138
}
132139

133-
AccountsAssetsController.sanitizedSend(res, results);
140+
// Send response based on format
141+
if (useObjectFormat) {
142+
const rcBlockInfo = await this.getRcBlockInfo(at);
143+
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
144+
} else {
145+
AccountsAssetsController.sanitizedSend(res, results);
146+
}
134147
} else {
135148
const hash = await this.getHashFromAt(at);
136149
const assetsArray = Array.isArray(assets) ? this.parseQueryParamArrayOrThrow(assets as string[]) : [];
@@ -140,21 +153,27 @@ export default class AccountsAssetsController extends AbstractController<Account
140153
};
141154

142155
private getAssetApprovals: RequestHandler = async (
143-
{ params: { address }, query: { at, useRcBlock, delegate, assetId } },
156+
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, delegate, assetId } },
144157
res,
145158
): Promise<void> => {
146159
if (typeof delegate !== 'string' || typeof assetId !== 'string') {
147160
throw new BadRequest('Must include a `delegate` and `assetId` query param');
148161
}
149162

150163
const id = this.parseNumberOrThrow(assetId, '`assetId` provided is not a number.');
164+
const useObjectFormat = useRcBlockFormat === 'object';
151165

152166
if (useRcBlock === 'true') {
153167
const rcAtResults = await this.getHashFromRcAt(at);
154168

155-
// Return empty array if no Asset Hub blocks found
169+
// Handle empty results based on format
156170
if (rcAtResults.length === 0) {
157-
AccountsAssetsController.sanitizedSend(res, []);
171+
if (useObjectFormat) {
172+
const rcBlockInfo = await this.getRcBlockInfo(at);
173+
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
174+
} else {
175+
AccountsAssetsController.sanitizedSend(res, []);
176+
}
158177
return;
159178
}
160179

@@ -176,7 +195,13 @@ export default class AccountsAssetsController extends AbstractController<Account
176195
results.push(enhancedResult);
177196
}
178197

179-
AccountsAssetsController.sanitizedSend(res, results);
198+
// Send response based on format
199+
if (useObjectFormat) {
200+
const rcBlockInfo = await this.getRcBlockInfo(at);
201+
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
202+
} else {
203+
AccountsAssetsController.sanitizedSend(res, results);
204+
}
180205
} else {
181206
const hash = await this.getHashFromAt(at);
182207
const result = await this.service.fetchAssetApproval(hash, address, id, delegate);

src/controllers/accounts/AccountsBalanceInfoController.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,22 @@ export default class AccountsBalanceController extends AbstractController<Accoun
8585
* @param res Express Response
8686
*/
8787
private getAccountBalanceInfo: RequestHandler<IAddressParam> = async (
88-
{ params: { address }, query: { at, useRcBlock, token, denominated } },
88+
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, token, denominated } },
8989
res,
9090
): Promise<void> => {
91+
const useObjectFormat = useRcBlockFormat === 'object';
92+
9193
if (useRcBlock === 'true') {
9294
const rcAtResults = await this.getHashFromRcAt(at);
9395

94-
// Return empty array if no Asset Hub blocks found
96+
// Handle empty results based on format
9597
if (rcAtResults.length === 0) {
96-
AccountsBalanceController.sanitizedSend(res, []);
98+
if (useObjectFormat) {
99+
const rcBlockInfo = await this.getRcBlockInfo(at);
100+
AccountsBalanceController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
101+
} else {
102+
AccountsBalanceController.sanitizedSend(res, []);
103+
}
97104
return;
98105
}
99106

@@ -129,7 +136,13 @@ export default class AccountsBalanceController extends AbstractController<Accoun
129136
results.push(enhancedResult);
130137
}
131138

132-
AccountsBalanceController.sanitizedSend(res, results);
139+
// Send response based on format
140+
if (useObjectFormat) {
141+
const rcBlockInfo = await this.getRcBlockInfo(at);
142+
AccountsBalanceController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
143+
} else {
144+
AccountsBalanceController.sanitizedSend(res, results);
145+
}
133146
} else {
134147
const hash = await this.getHashFromAt(at);
135148
const tokenArg =

src/controllers/accounts/AccountsForeignAssetsController.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,22 @@ export default class AccountsForeignAssetsController extends AbstractController<
7777
}
7878

7979
private getForeignAssetBalances: RequestHandler = async (
80-
{ params: { address }, query: { at, useRcBlock, foreignAssets } },
80+
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, foreignAssets } },
8181
res,
8282
): Promise<void> => {
83+
const useObjectFormat = useRcBlockFormat === 'object';
84+
8385
if (useRcBlock === 'true') {
8486
const rcAtResults = await this.getHashFromRcAt(at);
8587

86-
// Return empty array if no Asset Hub blocks found
88+
// Handle empty results based on format
8789
if (rcAtResults.length === 0) {
88-
AccountsForeignAssetsController.sanitizedSend(res, []);
90+
if (useObjectFormat) {
91+
const rcBlockInfo = await this.getRcBlockInfo(at);
92+
AccountsForeignAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
93+
} else {
94+
AccountsForeignAssetsController.sanitizedSend(res, []);
95+
}
8996
return;
9097
}
9198

@@ -109,7 +116,13 @@ export default class AccountsForeignAssetsController extends AbstractController<
109116
results.push(enhancedResult);
110117
}
111118

112-
AccountsForeignAssetsController.sanitizedSend(res, results);
119+
// Send response based on format
120+
if (useObjectFormat) {
121+
const rcBlockInfo = await this.getRcBlockInfo(at);
122+
AccountsForeignAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
123+
} else {
124+
AccountsForeignAssetsController.sanitizedSend(res, results);
125+
}
113126
} else {
114127
const hash = await this.getHashFromAt(at);
115128
const foreignAssetsArray = Array.isArray(foreignAssets) ? (foreignAssets as string[]) : [];

0 commit comments

Comments
 (0)