Skip to content
Open
2 changes: 1 addition & 1 deletion docs-v2/dist/bundle.js

Large diffs are not rendered by default.

54 changes: 51 additions & 3 deletions docs-v2/guides/USE_RC_BLOCK_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ The `useRcBlock` parameter is a boolean parameter that works in conjunction with

**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.

### useRcBlockFormat Parameter
The `useRcBlockFormat` parameter controls the response format when using `useRcBlock=true`. This parameter is only valid when `useRcBlock=true` is specified.

**Values:**
- `array` (default): Returns the standard array format with enhanced metadata
- `object`: Wraps the response in an object containing relay chain block info and the parachain data array

**Validation**: Using `useRcBlockFormat` without `useRcBlock=true` will return a `400 Bad Request` error.

## Implementation: useRcBlock Query Parameter

### Core Functionality
Expand All @@ -40,6 +49,10 @@ GET /pallets/staking/progress?at=1000000&useRcBlock=true
GET /accounts/{accountId}/balance-info?at=0x123abc&useRcBlock=true
GET /blocks/head?useRcBlock=true
GET /blocks/12345?at=12345&useRcBlock=true

# With useRcBlockFormat=object for wrapped response format
GET /pallets/staking/progress?at=1000000&useRcBlock=true&useRcBlockFormat=object
GET /accounts/{accountId}/balance-info?at=0x123abc&useRcBlock=true&useRcBlockFormat=object
```

### Response Format Changes
Expand All @@ -52,7 +65,7 @@ Returns single response object (unchanged):
}
```

**With useRcBlock=true:**
**With useRcBlock=true (default array format):**
Returns array format with additional metadata:
```json
[{
Expand All @@ -65,6 +78,38 @@ Returns array format with additional metadata:

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

**With useRcBlock=true&useRcBlockFormat=object:**
Returns object format wrapping the data with relay chain block info:
```json
{
"rcBlock": {
"hash": "0x1234567890abcdef...",
"parentHash": "0xabcdef1234567890...",
"number": "1000000"
},
"parachainDataPerBlock": [
{
// ... existing endpoint response data
"rcBlockHash": "0x1234567890abcdef...",
"rcBlockNumber": "1000000",
"ahTimestamp": "1642694400"
}
]
}
```

Or with empty `parachainDataPerBlock` array if no corresponding Asset Hub block exists:
```json
{
"rcBlock": {
"hash": "0x1234567890abcdef...",
"parentHash": "0xabcdef1234567890...",
"number": "1000000"
},
"parachainDataPerBlock": []
}
```

## Supported Endpoints

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

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

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

## Multi-Block and Elastic Scaling Scenarios

Expand Down
330 changes: 327 additions & 3 deletions docs-v2/openapi-v1.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/dist/app.bundle.js

Large diffs are not rendered by default.

330 changes: 327 additions & 3 deletions docs/src/openapi-v1.yaml

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/controllers/AbstractController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
IParaIdParam,
IRangeQueryParam,
} from 'src/types/requests';
import { IRcBlockInfo, IRcBlockObjectResponse } from 'src/types/responses';

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

/**
* Get minimal relay chain block info for the object format response.
*
* @param rcAt - Block identifier (hash or number) for the relay chain
* @returns IRcBlockInfo containing hash, parentHash, and number
*/
protected async getRcBlockInfo(rcAt: unknown): Promise<IRcBlockInfo> {
const rcApi = ApiPromiseRegistry.getApiByType('relay')[0]?.api;
if (!rcApi) {
throw new InternalServerError('Relay chain api must be available');
}

const rcHash = await this.getHashFromAt(rcAt, { api: rcApi });
const rcHeader = await rcApi.rpc.chain.getHeader(rcHash);

return {
hash: rcHash.toHex(),
parentHash: rcHeader.parentHash.toHex(),
number: rcHeader.number.toString(),
};
}

/**
* Format a response in the RC block object format.
*
* @param rcBlockInfo - The relay chain block info
* @param parachainData - Array of parachain data (endpoint-specific responses)
* @returns IRcBlockObjectResponse with rcBlock and parachainDataPerBlock
*/
protected formatRcBlockObjectResponse<T>(rcBlockInfo: IRcBlockInfo, parachainData: T[]): IRcBlockObjectResponse<T> {
return {
rcBlock: rcBlockInfo,
parachainDataPerBlock: parachainData,
};
}

/**
* Sanitize the numbers within the response body and then send the response
* body using the original Express Response object.
Expand Down
41 changes: 33 additions & 8 deletions src/controllers/accounts/AccountsAssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,22 @@ export default class AccountsAssetsController extends AbstractController<Account
}

private getAssetBalances: RequestHandler = async (
{ params: { address }, query: { at, useRcBlock, assets } },
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, assets } },
res,
): Promise<void> => {
const useObjectFormat = useRcBlockFormat === 'object';

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

// Return empty array if no Asset Hub blocks found
// Handle empty results based on format
if (rcAtResults.length === 0) {
AccountsAssetsController.sanitizedSend(res, []);
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
} else {
AccountsAssetsController.sanitizedSend(res, []);
}
return;
}

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

AccountsAssetsController.sanitizedSend(res, results);
// Send response based on format
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
} else {
AccountsAssetsController.sanitizedSend(res, results);
}
} else {
const hash = await this.getHashFromAt(at);
const assetsArray = Array.isArray(assets) ? this.parseQueryParamArrayOrThrow(assets as string[]) : [];
Expand All @@ -140,21 +153,27 @@ export default class AccountsAssetsController extends AbstractController<Account
};

private getAssetApprovals: RequestHandler = async (
{ params: { address }, query: { at, useRcBlock, delegate, assetId } },
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, delegate, assetId } },
res,
): Promise<void> => {
if (typeof delegate !== 'string' || typeof assetId !== 'string') {
throw new BadRequest('Must include a `delegate` and `assetId` query param');
}

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

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

// Return empty array if no Asset Hub blocks found
// Handle empty results based on format
if (rcAtResults.length === 0) {
AccountsAssetsController.sanitizedSend(res, []);
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
} else {
AccountsAssetsController.sanitizedSend(res, []);
}
return;
}

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

AccountsAssetsController.sanitizedSend(res, results);
// Send response based on format
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
} else {
AccountsAssetsController.sanitizedSend(res, results);
}
} else {
const hash = await this.getHashFromAt(at);
const result = await this.service.fetchAssetApproval(hash, address, id, delegate);
Expand Down
21 changes: 17 additions & 4 deletions src/controllers/accounts/AccountsBalanceInfoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,22 @@ export default class AccountsBalanceController extends AbstractController<Accoun
* @param res Express Response
*/
private getAccountBalanceInfo: RequestHandler<IAddressParam> = async (
{ params: { address }, query: { at, useRcBlock, token, denominated } },
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, token, denominated } },
res,
): Promise<void> => {
const useObjectFormat = useRcBlockFormat === 'object';

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

// Return empty array if no Asset Hub blocks found
// Handle empty results based on format
if (rcAtResults.length === 0) {
AccountsBalanceController.sanitizedSend(res, []);
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsBalanceController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
} else {
AccountsBalanceController.sanitizedSend(res, []);
}
return;
}

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

AccountsBalanceController.sanitizedSend(res, results);
// Send response based on format
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsBalanceController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
} else {
AccountsBalanceController.sanitizedSend(res, results);
}
} else {
const hash = await this.getHashFromAt(at);
const tokenArg =
Expand Down
21 changes: 17 additions & 4 deletions src/controllers/accounts/AccountsForeignAssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,22 @@ export default class AccountsForeignAssetsController extends AbstractController<
}

private getForeignAssetBalances: RequestHandler = async (
{ params: { address }, query: { at, useRcBlock, foreignAssets } },
{ params: { address }, query: { at, useRcBlock, useRcBlockFormat, foreignAssets } },
res,
): Promise<void> => {
const useObjectFormat = useRcBlockFormat === 'object';

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

// Return empty array if no Asset Hub blocks found
// Handle empty results based on format
if (rcAtResults.length === 0) {
AccountsForeignAssetsController.sanitizedSend(res, []);
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsForeignAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, []));
} else {
AccountsForeignAssetsController.sanitizedSend(res, []);
}
return;
}

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

AccountsForeignAssetsController.sanitizedSend(res, results);
// Send response based on format
if (useObjectFormat) {
const rcBlockInfo = await this.getRcBlockInfo(at);
AccountsForeignAssetsController.sanitizedSend(res, this.formatRcBlockObjectResponse(rcBlockInfo, results));
} else {
AccountsForeignAssetsController.sanitizedSend(res, results);
}
} else {
const hash = await this.getHashFromAt(at);
const foreignAssetsArray = Array.isArray(foreignAssets) ? (foreignAssets as string[]) : [];
Expand Down
Loading
Loading