Skip to content
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

feat: add support for pallet-xcm claimAssets call #394

Merged
merged 11 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,13 +304,32 @@ For more information, refer to the [docs](https://github.com/paritytech/asset-tr

Note: For other parachains, local transfers are currently supported via the balances and tokens pallets. For local parachain transactions, if an asset id is not passed in it will resolve to the balances pallet, and if a asset id is passed in it will resolve to the tokens pallet.

### Claiming Trapped Assets

The api can be used to construct `claimAssets` calls in order to retrieve assets trapped locally on chain after a failed XCM.

An example would look like:
```typescript
api.claimAssets(
[`DOT`], // Asset(s) to claim
['1000000000000'], // Amount of asset(s) to claim
'0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b', // Beneficiary
{
xcmVersion: 4,
format: 'payload',
sendersAddr: '5EWNeodpcQ6iYibJ3jmWVe85nsok1EDG8Kk3aFg8ZzpfY1qX'
} // opts
)
```
Note: claimAssets works when pallet-xcm is configured as the AssetTrap for the given runtime. This is true for all relay chains and system parachains but may not be for other chains.

## License

The source code in this repository is distributed under the Apache 2.0 license. See the [LICENSE](https://github.com/paritytech/asset-transfer-api/blob/main/LICENSE) file. This source code comes with absolutely no warranty. Use at your own risk.

## Zombienet Testing

Zombienet is used to launch a complete network including a relay chain, and two parachains. It will create HRMP channels betweens the launched parachains allowing the testing enviornment to send XCM messages and transfer assets.
Zombienet is used to launch a complete network including a relay chain, and two parachains. It will create HRMP channels betweens the launched parachains allowing the testing environment to send XCM messages and transfer assets.

### **Requirements**:

Expand Down
41 changes: 41 additions & 0 deletions examples/claimAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* When importing from @substrate/asset-transfer-api it would look like the following
*
* import { AssetTransferApi, constructApiPromise } from '@substrate/asset-transfer-api'
*/
import { AssetTransferApi, constructApiPromise } from '../src';
import { TxResult } from '../src/types';
import { GREEN, PURPLE, RESET } from './colors';

/**
* In this example we are creating a `claimAssets` call to claim `WND` tokens trapped in the AssetTrap of Westend AssetHub where the `xcmVersion` is set to 3.
*
*/
const main = async () => {
const { api, specName, safeXcmVersion } = await constructApiPromise('wss://westend-asset-hub-rpc.polkadot.io');
const assetApi = new AssetTransferApi(api, specName, safeXcmVersion);
let callInfo: TxResult<'call'>;
try {
callInfo = await assetApi.claimAssets(
['WND'],
['10000000000'],
'5HBuLJz9LdkUNseUEL6DLeVkx2bqEi6pQr8Ea7fS4bzx7i7E',
{
format: 'call',
xcmVersion: 3,
},
);

console.log(`${PURPLE}The following call data that is returned:\n${GREEN}${JSON.stringify(callInfo, null, 4)}`);
} catch (e) {
console.error(e);
throw Error(e as string);
}

const decoded = assetApi.decodeExtrinsic(callInfo.tx, 'call');
console.log(`\n${PURPLE}The following decoded tx:\n${GREEN} ${JSON.stringify(JSON.parse(decoded), null, 4)}${RESET}`);
};

main()
.catch((err) => console.error(err))
.finally(() => process.exit());
4 changes: 2 additions & 2 deletions src/AssetTransferApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { adjustedMockMoonriverNoXTokensParachainApi } from './testHelpers/adjust
import { adjustedMockRelayApiNoLimitedReserveTransferAssets } from './testHelpers/adjustedMockRelayApiNoLimitedReserveTransferAssets';
import { adjustedMockRelayApi } from './testHelpers/adjustedMockRelayApiV9420';
import { adjustedMockSystemApi } from './testHelpers/adjustedMockSystemApiV1004000';
import { adjustedMockSystemApiV1007000 } from './testHelpers/adjustedMockSystemApiV1007000';
import { adjustedMockSystemApiV1009000 } from './testHelpers/adjustedMockSystemApiV1009000';
import { mockSystemApi } from './testHelpers/mockSystemApi';
import { mockWeightInfo } from './testHelpers/mockWeightInfo';
import { AssetCallType, Direction, ResolvedCallInfo, UnsignedTransaction, XcmBaseArgs, XcmDirection } from './types';
Expand Down Expand Up @@ -50,7 +50,7 @@ const bifrostAssetsApi = new AssetTransferApi(adjustedMockBifrostParachainApi, '
const moonriverAssetsNoXTokensApi = new AssetTransferApi(adjustedMockMoonriverNoXTokensParachainApi, 'moonriver', 2, {
registryType: 'NPM',
});
const westmintAssetsApi = new AssetTransferApi(adjustedMockSystemApiV1007000, 'westmint', 4, {
const westmintAssetsApi = new AssetTransferApi(adjustedMockSystemApiV1009000, 'westmint', 4, {
registryType: 'NPM',
});

Expand Down
77 changes: 72 additions & 5 deletions src/AssetTransferApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Parity Technologies (UK) Ltd.
// Copyright 2024 Parity Technologies (UK) Ltd.

import '@polkadot/api-augment';

Expand All @@ -17,6 +17,7 @@ import * as foreignAssets from './createCalls/foreignAssets';
import * as poolAssets from './createCalls/poolAssets';
import * as tokens from './createCalls/tokens';
import {
claimAssets,
limitedReserveTransferAssets,
limitedTeleportAssets,
transferAssets,
Expand Down Expand Up @@ -46,6 +47,7 @@ import {
checkXcmVersion,
} from './errors';
import { LocalTxType } from './errors/checkLocalTxInput/types';
import { checkClaimAssetsInputs } from './errors/checkXcmTxInputs';
import { Registry } from './registry';
import { ChainInfoKeys, ChainInfoRegistry } from './registry/types';
import { sanitizeAddress } from './sanitize/sanitizeAddress';
Expand Down Expand Up @@ -204,7 +206,7 @@ export class AssetTransferApi {
const localTxChainType = this.establishLocalTxChainType(originChainId, destChainId, chainOriginDestInfo);
const isLocalTx = localTxChainType !== LocalTxChainType.None;
const xcmDirection = this.establishDirection(isLocalTx, chainOriginDestInfo);
const isForeignAssetsTransfer: boolean = this.checkIsForeignAssetTransfer(assetIds);
const isForeignAssetsTransfer = this.checkContainsAssetLocations(assetIds);
const isPrimaryParachainNativeAsset = isParachainPrimaryNativeAsset(registry, specName, xcmDirection, assetIds[0]);
const xcmPallet = establishXcmPallet(api, xcmDirection);
const declaredXcmVersion = xcmVersion === undefined ? safeXcmVersion : xcmVersion;
Expand Down Expand Up @@ -287,6 +289,72 @@ export class AssetTransferApi {
});
}

/**
* Create a local claimAssets XCM transaction to retrieve trapped assets. This can be either locally on a systems parachain, on the relay chain or any chain that supports the pallet-xcm `claimAssets` runtime call.
*
* ```ts
* import { TxResult } from '@substrate/asset-transfer-api'
*
* let callInfo: TxResult<'call'>;
* try {
* callInfo = await assetsApi.claimAssets(
* [`{"parents":"0","interior":{"X2":[{"PalletInstance":"50"},{"GeneralIndex":"1984"}]}}`],
* ['1000000000000'],
* '0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b',
* {
* format: 'call',
* xcmVersion: 2,
* }
* )
* } catch (e) {
* console.error(e);
* throw Error(e);
* }
* ```
*
* @param assetIds Array of assetId's to be claimed from the AssetTrap
* @param amounts Array of the amounts of each trapped asset to be claimed
* @param beneficiary Address of the account to receive the trapped assets
* @param opts Options
*/
public async claimAssets<T extends Format>(
assetIds: string[],
amounts: string[],
beneficiary: string,
opts: TransferArgsOpts<T>,
): Promise<TxResult<T>> {
const { api, specName, originChainId, registry, safeXcmVersion } = this;
const { format, sendersAddr, transferLiquidToken: isLiquidToken, xcmVersion } = opts;
const declaredXcmVersion = xcmVersion === undefined ? safeXcmVersion : xcmVersion;
const isLiquidTokenTransfer = isLiquidToken ? true : false;
const assetIdsContainLocations = this.checkContainsAssetLocations(assetIds);
const beneficiaryAddress = sanitizeAddress(beneficiary);

checkXcmVersion(declaredXcmVersion);
checkBaseInputOptions(opts, specName);
checkClaimAssetsInputs(assetIds, amounts);

const ext = await claimAssets(
api,
registry,
specName,
assetIds,
amounts,
beneficiaryAddress,
declaredXcmVersion,
originChainId,
{
isForeignAssetsTransfer: assetIdsContainLocations,
isLiquidTokenTransfer,
},
);

return await this.constructFormat(ext, 'local', declaredXcmVersion, 'claimAssets', originChainId, originChainId, {
format,
sendersAddr,
});
}

/**
* Initialize the registry. This will only activate the registry for the CDN.
* If the `registryType` is `NPM` the initalization will exit since the AssetTransferApi
Expand Down Expand Up @@ -758,13 +826,12 @@ export class AssetTransferApi {
}

/**
* Returns if assetIds contains a values for a foreign asset transfer
* Returns if `assetIds` contains asset location values
*
* @param assetIds string[]
* @returns boolean
*/
private checkIsForeignAssetTransfer(assetIds: string[]): boolean {
// if assetIds is empty it is not a multilocation foreign asset transfer
private checkContainsAssetLocations(assetIds: string[]): boolean {
if (assetIds.length === 0) {
return false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/createXcmCalls/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2023 Parity Technologies (UK) Ltd.
// Copyright 2024 Parity Technologies (UK) Ltd.

export { claimAssets } from './polkadotXcm/claimAssets';
export { limitedReserveTransferAssets } from './polkadotXcm/limitedReserveTransferAssets';
export { limitedTeleportAssets } from './polkadotXcm/limitedTeleportAssets';
export { transferAssets } from './polkadotXcm/transferAssets';
Expand Down
110 changes: 110 additions & 0 deletions src/createXcmCalls/polkadotXcm/claimAssets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2024 Parity Technologies (UK) Ltd.

import { Registry } from '../../registry';
import { adjustedMockSystemApi } from '../../testHelpers/adjustedMockSystemApiV1004000';
import { adjustedMockSystemApiV1009000 } from '../../testHelpers/adjustedMockSystemApiV1009000';
import { claimAssets } from './claimAssets';

describe('claimAssets', () => {
const specName = 'westmint';
const registry = new Registry(specName, {});

it('Should correctly construct an XCM V4 claimAssets call', async () => {
const assetIds = [`{"parents":"1","interior":{"X2":[{"PalletInstance":"50"},{"GeneralIndex":"1984"}]}}`];
const amounts = ['1000000'];
const xcmVersion = 4;
const beneficiaryAddress = '0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b';

const ext = await claimAssets(
adjustedMockSystemApiV1009000,
registry,
specName,
assetIds,
amounts,
beneficiaryAddress,
xcmVersion,
'1000',
{
isForeignAssetsTransfer: true,
isLiquidTokenTransfer: false,
},
);

expect(ext.toHex()).toEqual(
'0xd8041f0c04040102043205011f0002093d000400010100f5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b',
);
});
it('Should correctly construct an XCM V3 claimAssets call', async () => {
const assets = [`{"parents":"1","interior":{"X2":[{"PalletInstance":"50"},{"GeneralIndex":"1984"}]}}`];
const amounts = ['1000000'];
const xcmVersion = 3;
const beneficiaryAddress = '0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b';

const ext = await claimAssets(
adjustedMockSystemApiV1009000,
registry,
specName,
assets,
amounts,
beneficiaryAddress,
xcmVersion,
'1000',
{
isForeignAssetsTransfer: true,
isLiquidTokenTransfer: false,
},
);

expect(ext.toHex()).toEqual(
'0xdc041f0c0304000102043205011f0002093d000300010100f5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b',
);
});
it('Should correctly construct an XCM V2 claimAssets call', async () => {
const assets = [`{"parents":"1","interior":{"X2":[{"PalletInstance":"50"},{"GeneralIndex":"1984"}]}}`];
const amounts = ['1000000'];
const xcmVersion = 2;
const beneficiaryAddress = '0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b';

const ext = await claimAssets(
adjustedMockSystemApiV1009000,
registry,
specName,
assets,
amounts,
beneficiaryAddress,
xcmVersion,
'1000',
{
isForeignAssetsTransfer: true,
isLiquidTokenTransfer: false,
},
);

expect(ext.toHex()).toEqual(
'0xdc041f0c0104000102043205011f0002093d000100010100f5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b',
);
});
it('Should correctly throw an error when the claimAssets call is not found in the runtime', async () => {
const assetIds = [`{"parents":"1","interior":{"X2":[{"PalletInstance":"50"},{"GeneralIndex":"1984"}]}}`];
const amounts = ['1000000'];
const xcmVersion = 4;
const beneficiaryAddress = '0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b';

await expect(async () => {
await claimAssets(
adjustedMockSystemApi,
registry,
specName,
assetIds,
amounts,
beneficiaryAddress,
xcmVersion,
'1000',
{
isForeignAssetsTransfer: true,
isLiquidTokenTransfer: false,
},
);
}).rejects.toThrow('Did not find claimAssets call from pallet polkadotXcm in the current runtime.');
});
});
60 changes: 60 additions & 0 deletions src/createXcmCalls/polkadotXcm/claimAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2024 Parity Technologies (UK) Ltd.

import { ApiPromise } from '@polkadot/api';
import type { SubmittableExtrinsic } from '@polkadot/api/submittable/types';
import type { ISubmittableResult } from '@polkadot/types/types';

import { createAssetLocations } from '../../createXcmTypes/util/createAssetLocations';
import { createBeneficiary } from '../../createXcmTypes/util/createBeneficiary';
import { BaseError, BaseErrorsEnum } from '../../errors';
import { Registry } from '../../registry';
import { CreateXcmCallOpts } from '../types';
import { establishXcmPallet } from '../util/establishXcmPallet';

/**
* Allow users to claim assets trapped locally on-chain during failed XCM execution.
*
* @param api ApiPromise
* @param assetLocations string[]
* @param amounts string[]
* @param xcmVersion number
* @param beneficiaryAddress string
*/
export const claimAssets = async (
api: ApiPromise,
registry: Registry,
specName: string,
assetIds: string[],
amounts: string[],
beneficiaryAddress: string,
xcmVersion: number,
originChainId: string,
opts: CreateXcmCallOpts,
): Promise<SubmittableExtrinsic<'promise', ISubmittableResult>> => {
const { isForeignAssetsTransfer: assetIdsContainLocations, isLiquidTokenTransfer } = opts;
const beneficiary = createBeneficiary(beneficiaryAddress, xcmVersion);

const assets = await createAssetLocations(
api,
assetIds,
specName,
amounts,
xcmVersion,
registry,
originChainId,
assetIdsContainLocations,
isLiquidTokenTransfer,
);

const pallet = establishXcmPallet(api);
const ext = api.tx[pallet].claimAssets;

if (!ext) {
throw new BaseError(
`Did not find claimAssets call from pallet ${pallet} in the current runtime.`,
BaseErrorsEnum.RuntimeCallNotFound,
);
}

return ext(assets, beneficiary);
};
Loading