Skip to content

feat: add pallets/foreign-assets endpoint #1314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e1aa255
feat: add pallets/foreign-assets endpoint
Imod7 Aug 15, 2023
e682fbd
fix: remove `asset-info` from endpoint & fix docs
Imod7 Aug 21, 2023
e9ca2e4
add name & symbol in human readable format
Imod7 Aug 21, 2023
eb9628e
Refactored the code that retrieves the metadata of the foreign asset
Imod7 Aug 28, 2023
0683642
test: added test & mock data
Imod7 Sep 5, 2023
07fddc4
Merge branch 'master' into domi-foreign-assets
Imod7 Sep 5, 2023
cc4b02b
Update src/services/pallets/PalletsForeignAssetsService.ts
Imod7 Sep 6, 2023
2c418c7
Update src/services/pallets/PalletsForeignAssetsService.ts
Imod7 Sep 6, 2023
c8b6d7e
Update src/services/pallets/PalletsForeignAssetsService.ts
Imod7 Sep 6, 2023
151396b
Update src/services/pallets/PalletsForeignAssetsService.ts
Imod7 Sep 6, 2023
e87cede
Update src/services/pallets/PalletsForeignAssetsService.ts
Imod7 Sep 6, 2023
34a04ec
replaced type anyjson with codec
Imod7 Sep 6, 2023
dae423e
changed types in assetInfo & assetMetadata & copyright
Imod7 Sep 6, 2023
1df530f
removed storageKeyMultilocation & changed storageKey
Imod7 Sep 6, 2023
8efc7f2
fix: changes to support a list of multiple foreign assets
Imod7 Sep 7, 2023
20f967e
Updated test to work with multiple foreign assets
Imod7 Sep 8, 2023
ffb3d54
Changes types of foreignAssetInfo and foreignAssetMetadata
Imod7 Sep 8, 2023
2d49b26
Merge branch 'master', resolved conflicts & rebuilt docs
Imod7 Sep 8, 2023
b4dd1f3
fix typing on queried values, and return values
TarikGul Sep 8, 2023
646083a
fix test
Imod7 Sep 8, 2023
1ecc91b
removed an unnecessary console.log
Imod7 Sep 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/dist/app.bundle.js

Large diffs are not rendered by default.

290 changes: 161 additions & 129 deletions docs/src/openapi-v1.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/chains-config/assetHubKusamaControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const assetHubKusamaControllers: ControllerConfig = {
'PalletsConsts',
'PalletsErrors',
'PalletsEvents',
'PalletsForeignAssets',
'RuntimeCode',
'RuntimeMetadata',
'RuntimeSpec',
Expand Down
1 change: 1 addition & 0 deletions src/chains-config/assetHubPolkadotControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const assetHubPolkadotControllers: ControllerConfig = {
'PalletsConsts',
'PalletsEvents',
'PalletsErrors',
'PalletsForeignAssets',
'RuntimeCode',
'RuntimeMetadata',
'RuntimeSpec',
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 @@ -40,6 +40,7 @@ export const defaultControllers: ControllerConfig = {
'PalletsConsts',
'PalletsErrors',
'PalletsEvents',
'PalletsForeignAssets',
'PalletsStakingProgress',
'PalletsStorage',
'Paras',
Expand Down
2 changes: 2 additions & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
PalletsDispatchables,
PalletsErrors,
PalletsEvents,
PalletsForeignAssets,
PalletsNominationPools,
PalletsStakingProgress,
PalletsStakingValidators,
Expand Down Expand Up @@ -68,6 +69,7 @@ export const controllers = {
PalletsConsts,
PalletsErrors,
PalletsEvents,
PalletsForeignAssets,
PalletsNominationPools,
PalletsStakingProgress,
PalletsStakingValidators,
Expand Down
58 changes: 58 additions & 0 deletions src/controllers/pallets/PalletsForeignAssetsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { ApiPromise } from '@polkadot/api';
import { RequestHandler } from 'express';

import { PalletsForeignAssetsService } from '../../services';
import AbstractController from '../AbstractController';

/**
* GET asset information for all foreign assets.
*
* 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.
*
* `/pallets/foreign-assets`
* Returns:
* - `at`: Block number and hash at which the call was made.
* - `items`: An array containing the `AssetDetails` and `AssetMetadata` of every foreign asset.
*
* Substrate References:
* - Foreign Assets Pallet Instance in Kusama Asset Hub: https://github.com/paritytech/cumulus/blob/master/parachains/runtimes/assets/asset-hub-kusama/src/lib.rs#L295
*/
export default class PalletsForeignAssetsController extends AbstractController<PalletsForeignAssetsService> {
constructor(api: ApiPromise) {
super(api, '/pallets/foreign-assets', new PalletsForeignAssetsService(api));
this.initRoutes();
}

protected initRoutes(): void {
this.safeMountAsyncGetHandlers([['', this.getForeignAssets]]);
}

private getForeignAssets: RequestHandler = async (
{ query: { at } },
res
): Promise<void> => {
const hash = await this.getHashFromAt(at);
PalletsForeignAssetsController.sanitizedSend(
res,
await this.service.fetchForeignAssets(hash)
);
};
}
1 change: 1 addition & 0 deletions src/controllers/pallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { default as PalletsConsts } from './PalletsConstsController';
export { default as PalletsDispatchables } from './PalletsDispatchablesController';
export { default as PalletsErrors } from './PalletsErrorsController';
export { default as PalletsEvents } from './PalletsEventsController';
export { default as PalletsForeignAssets } from './PalletsForeignAssetsController';
export { default as PalletsNominationPools } from './PalletsNominationPoolsController';
export { default as PalletsStakingProgress } from './PalletsStakingProgressController';
export { default as PalletsStakingValidators } from './PalletsStakingValidatorsController';
Expand Down
2 changes: 1 addition & 1 deletion src/services/pallets/PalletsDispatchablesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class PalletsDispatchablesService extends AbstractPalletsService {
);
} else {
items = Object.entries(dispatchables).map(
(disaptchableItem) => disaptchableItem[1].meta
(dispatchableItem) => dispatchableItem[1].meta
);
}

Expand Down
85 changes: 85 additions & 0 deletions src/services/pallets/PalletsForeignAssetsService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { ApiPromise } from '@polkadot/api';

import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers';
import { assetsMetadata } from '../test-helpers/mock/assets/mockAssetHubKusamaData';
import { foreignAssetsEntries } from '../test-helpers/mock/data/foreignAssetsEntries';
import { mockAssetHubKusamaApi } from '../test-helpers/mock/mockAssetHubKusamaApi';
import { blockHash523510 } from '../test-helpers/mock/mockBlock523510';
import { PalletsForeignAssetsService } from './PalletsForeignAssetsService';

const foreignAssetsEntriesAt = () =>
Promise.resolve().then(() => foreignAssetsEntries());

const mockApi = {
...mockAssetHubKusamaApi,
query: {
foreignAssets: {
asset: {
entries: foreignAssetsEntriesAt,
},
metadata: assetsMetadata,
},
},
} as unknown as ApiPromise;

const palletsForeignAssetsService = new PalletsForeignAssetsService(mockApi);

describe('PalletsForeignAssetsService', () => {
describe('PalletsForeignAssetsService.fetchForeignAssets', () => {
it('Should return the correct response for Foreign Assets', async () => {
const expectedResponse = {
at: {
hash: '0x814bb69eba28cf13066aa025d39526b503fc563162f1301c627548b9ccec54c8',
height: '523510',
},
items: [
{
foreignAssetInfo: {
owner: 'FBeL7DiQ6JkoypYATheXhH3GQr5de2L3hL444TP6qQr3yA9',
issuer: 'FBeL7DiQ6JkoypYATheXhH3GQr5de2L3hL444TP6qQr3yA9',
admin: 'FBeL7DiQ6JkoypYATheXhH3GQr5de2L3hL444TP6qQr3yA9',
freezer: 'FBeL7DiQ6JkoypYATheXhH3GQr5de2L3hL444TP6qQr3yA9',
supply: '0',
deposit: '100000000000',
minBalance: '1000000000',
isSufficient: false,
accounts: '0',
sufficients: '0',
approvals: '0',
isFrozen: false,
},
foreignAssetMetadata: {
deposit: '6693666633',
name: '0x54696e6b65726e6574',
symbol: '0x544e4b52',
decimals: '12',
isFrozen: false,
},
},
],
};

const response = await palletsForeignAssetsService.fetchForeignAssets(
blockHash523510
);

expect(sanitizeNumbers(response)).toStrictEqual(expectedResponse);
});
});
});
90 changes: 90 additions & 0 deletions src/services/pallets/PalletsForeignAssetsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { ApiPromise } from '@polkadot/api';
import { BlockHash } from '@polkadot/types/interfaces';
import { AnyJson } from 'src/types/polkadot-js';

import { IForeignAssetInfo, IForeignAssets } from '../../types/responses';
import { AbstractService } from '../AbstractService';

export class PalletsForeignAssetsService extends AbstractService {
constructor(api: ApiPromise) {
super(api);
}

/**
* Fetch all foreign asset's `AssetDetails` and `AssetMetadata`.
*
* @param hash `BlockHash` to make call at
*/
async fetchForeignAssets(hash: BlockHash): Promise<IForeignAssets> {
const { api } = this;

const [{ number }, foreignAssetInfo] = await Promise.all([
api.rpc.chain.getHeader(hash),
api.query.foreignAssets.asset.entries(),
Copy link
Member

@TarikGul TarikGul Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above we need to correctly type the entries method. This is because both assetStorageKeyData, and assetInfo will have there own specific types.

assetStorageKeyData: StorageKey<AnyTuple>
assetInfo: Option<PalletAssetsAssetDetails>

// Incorrect:
api.query.foreignAssets.asset.entries();

// Correct:
api.query.foreignAssets.asset.entries<Option<PalletAssetsAssetDetails>>()

// This is a little tricky but the above will map out to Promise<[StorageKey<AnyTuple>, Option<PalletAssetsAssetDetails>][]> once we input the correct type.

This will all ensure that the above is typed correctly so we wont need to type cast anymore and we can clean up all the types within the service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is amazing!!! Thank you so much @TarikGul for spending the time to explain and showcase this. I was fighting with the types quite a bit and now I get the logic! 🙏 🙏 🙏

]);

const items: IForeignAssetInfo[] = [];

/**
* This will iterate through all the foreign asset entries and for each entry it will create
* the `foreignAssetMultiLocation` variable based on the MultiLocation of the foreign asset.
* This variable will then be used as the key to get the corresponding metadata of the foreign asset.
*
* This is based on the logic implemented by marshacb in asset-transfer-api-registry
* https://github.com/paritytech/asset-transfer-api-registry/blob/main/src/createRegistry.ts#L193-L238
*/
for (const [assetStorageKeyData, assetInfo] of foreignAssetInfo) {
const foreignAssetData = assetStorageKeyData.toHuman();

if (foreignAssetData) {
// remove any commas from multilocation key values e.g. Parachain: 2,125 -> Parachain: 2125
const foreignAssetMultiLocationStr = JSON.stringify(
foreignAssetData[0]
).replace(/(\d),/g, '$1');
const foreignAssetMultiLocation = api.registry.createType(
'MultiLocation',
JSON.parse(foreignAssetMultiLocationStr)
);

const assetMetadata = await api.query.foreignAssets.metadata(
foreignAssetMultiLocation
);

if (assetMetadata) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think it's assetMetadata we need to check here, but instead if (assetInfo.isSome), and then we need to unwrap the value when we key it into foreignAssetInfo: assetInfo.unwrap().

But then we need to handle the case where the info is not available in an else statement. Such that if it doesn't exist we return an empty object.

const item: IForeignAssetInfo = {
foreignAssetInfo: assetInfo as unknown as AnyJson,
foreignAssetMetadata: assetMetadata as unknown as AnyJson,
};

items.push(item);
}
}
}

const at = {
hash,
height: number.unwrap().toString(10),
};

return {
at,
items,
};
}
}
1 change: 1 addition & 0 deletions src/services/pallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './PalletsAssetConversionService';
export * from './PalletsAssetsService';
export * from './PalletsConstantsService';
export * from './PalletsDispatchablesService';
export * from './PalletsForeignAssetsService';
export * from './PalletsNominationPoolsService';
export * from './PalletsStakingProgressService';
export * from './PalletsStakingValidatorsService';
Expand Down
73 changes: 73 additions & 0 deletions src/services/test-helpers/mock/assets/mockAssetHubKusamaData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { AssetDetails, AssetMetadata } from '@polkadot/types/interfaces';
import { Option } from '@polkadot/types-codec/base';

import { assetHubKusamaRegistryV9430 } from '../../../../test-helpers/registries';

/**
* This mock data uses Asset Hub Kusama specVersion V9430
*/

const falseBool = assetHubKusamaRegistryV9430.createType('bool', false);

const accountId = assetHubKusamaRegistryV9430.createType(
'AccountId',
'FBeL7DiQ6JkoypYATheXhH3GQr5de2L3hL444TP6qQr3yA9'
);
const balanceOf = assetHubKusamaRegistryV9430.createType(
'BalanceOf',
6693666633
);

export const foreignAssetInfo = (): Option<AssetDetails> => {
const responseObj = {
owner: accountId,
issuer: accountId,
admin: accountId,
freezer: accountId,
supply: assetHubKusamaRegistryV9430.createType('u64', 0),
deposit: assetHubKusamaRegistryV9430.createType('BalanceOf', 100000000000),
minBalance: assetHubKusamaRegistryV9430.createType('u64', 1000000000),
isSufficient: falseBool,
accounts: assetHubKusamaRegistryV9430.createType('u8', 0),
sufficients: assetHubKusamaRegistryV9430.createType('u8', 0),
approvals: assetHubKusamaRegistryV9430.createType('u8', 0),
isFrozen: falseBool,
};

return assetHubKusamaRegistryV9430.createType(
'Option<AssetDetails>',
responseObj
);
};

export const assetsMetadata = (): Promise<Option<AssetMetadata>> =>
Promise.resolve().then(() => {
const responseObj = {
deposit: balanceOf,
name: assetHubKusamaRegistryV9430.createType('Bytes', 'Tinkernet'),
symbol: assetHubKusamaRegistryV9430.createType('Bytes', 'TNKR'),
decimals: assetHubKusamaRegistryV9430.createType('u8', 12),
isFrozen: falseBool,
};

return assetHubKusamaRegistryV9430.createType(
'Option<AssetMetadata>',
responseObj
);
});
Loading