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

fix: Update paysWithFeeOrigin examples and errors #420

Merged
merged 3 commits into from
Jul 2, 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
50 changes: 0 additions & 50 deletions examples/assetHubToMoonriverPaysWithFeeOrigin.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 `transferAssets` payload to send 1 USDT (asset ID: `1984`)
* from a Kusama Asset Hub (System Parachain) account
* to a Moonriver (Parachain) account, where the `xcmVersion` is set to safeXcmVersion and no `weightLimit` is provided declaring that
* the allowable weight will be `unlimited` and `paysWithFeeOrigin` is asset ID `1984` (USDT)
* declaring that `USDT` `should be used to pay for tx fees in the origin. In order to pay fees on the origin with a different asset than the native asset, the selected asset is expected to have an existing liquidity pool/pair with the native asset in AssetHub.
*
* NOTE: To specify the amount of weight for the tx to use provide a `weightLimit` option containing desired values for `refTime` and `proofSize`.
*/
const main = async () => {
const { api, specName, safeXcmVersion } = await constructApiPromise('wss://kusama-asset-hub-rpc.polkadot.io');
const assetApi = new AssetTransferApi(api, specName, safeXcmVersion);

let payloadInfo: TxResult<'payload'>;
try {
payloadInfo = await assetApi.createTransferTransaction(
'2023',
'5EWNeodpcQ6iYibJ3jmWVe85nsok1EDG8Kk3aFg8ZzpfY1qX',
['1984'],
['1000000'],
{
format: 'payload',
xcmVersion: safeXcmVersion,
paysWithFeeOrigin: '1984',
sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1',
},
);

const payloadWithAssetId = {
origin: payloadInfo.origin,
dest: payloadInfo.dest,
direction: payloadInfo.direction,
tx: payloadInfo.tx.toHex(),
assetId: JSON.stringify(payloadInfo.tx.assetId),
format: payloadInfo.format,
method: payloadInfo.method,
xcmVersion: payloadInfo.xcmVersion,
};

console.log(payloadWithAssetId);
} catch (e) {
console.error(e);
throw Error(e as string);
}

const decoded = assetApi.decodeExtrinsic(payloadInfo.tx.toHex(), 'payload');
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());
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 `transferAssets` payload to send 1 DOT
* from a Polkadot Asset Hub (System Parachain) account
* to a Hydration (Parachain) account, where the `xcmVersion` is set to safeXcmVersion and no `weightLimit` is provided declaring that
* the allowable weight will be `unlimited` and `paysWithFeeOrigin` is asset ID `GLMR` (Glimmer)
* declaring that `GLMR` `should be used to pay for tx fees in the origin. In order to pay fees on the origin with a different asset than the native asset, the selected asset is expected to have an existing liquidity pool/pair with the native asset in AssetHub.
*
* NOTE: To specify the amount of weight for the tx to use provide a `weightLimit` option containing desired values for `refTime` and `proofSize`.
*/
const main = async () => {
const { api, specName, safeXcmVersion } = await constructApiPromise('wss://polkadot-asset-hub-rpc.polkadot.io');
const assetApi = new AssetTransferApi(api, specName, safeXcmVersion);

let payloadInfo: TxResult<'payload'>;
try {
payloadInfo = await assetApi.createTransferTransaction(
'2034',
'5EWNeodpcQ6iYibJ3jmWVe85nsok1EDG8Kk3aFg8ZzpfY1qX',
['dot'],
['1000000000000'],
{
format: 'payload',
xcmVersion: safeXcmVersion,
paysWithFeeOrigin: 'GLMR', // Foreign Asset Symbol (symbols must be unique if used)
sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1',
},
);

const payloadWithAssetId = {
origin: payloadInfo.origin,
dest: payloadInfo.dest,
direction: payloadInfo.direction,
tx: payloadInfo.tx.toHex(),
assetId: JSON.stringify(payloadInfo.tx.assetId),
format: payloadInfo.format,
method: payloadInfo.method,
xcmVersion: payloadInfo.xcmVersion,
};

console.log(payloadWithAssetId);
} catch (e) {
console.error(e);
throw Error(e as string);
}

const decoded = assetApi.decodeExtrinsic(payloadInfo.tx.toHex(), 'payload');
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());
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 `transferAssets` payload to send 1 ROC
* from a Rococo Asset Hub (System Parachain) account
* to a Basilisk (Parachain) account, where the `xcmVersion` is set to safeXcmVersion and no `weightLimit` is provided declaring that
* the allowable weight will be `unlimited` and `paysWithFeeOrigin` is asset location `{"parents":"1","interior":{"X1":{"Parachain":"3369"}}}` (MUSE)
* declaring that `MUSE` `should be used to pay for tx fees in the origin. In order to pay fees on the origin with a different asset than the native asset, the selected asset is expected to have an existing liquidity pool/pair with the native asset in AssetHub.
*
* NOTE: To specify the amount of weight for the tx to use provide a `weightLimit` option containing desired values for `refTime` and `proofSize`.
*/
const main = async () => {
const { api, specName, safeXcmVersion } = await constructApiPromise('wss://rococo-asset-hub-rpc.polkadot.io');
const assetApi = new AssetTransferApi(api, specName, safeXcmVersion);

let payloadInfo: TxResult<'payload'>;
try {
payloadInfo = await assetApi.createTransferTransaction(
'2090',
'5EWNeodpcQ6iYibJ3jmWVe85nsok1EDG8Kk3aFg8ZzpfY1qX',
[''],
['1000000'],
{
format: 'payload',
xcmVersion: safeXcmVersion,
paysWithFeeOrigin: `{"parents":"1","interior":{"X1":{"Parachain":"3369"}}}`,
sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1',
},
);

const payloadWithAssetId = {
origin: payloadInfo.origin,
dest: payloadInfo.dest,
direction: payloadInfo.direction,
tx: payloadInfo.tx.toHex(),
assetId: JSON.stringify(payloadInfo.tx.assetId),
format: payloadInfo.format,
method: payloadInfo.method,
xcmVersion: payloadInfo.xcmVersion,
};

console.log(payloadWithAssetId);
} catch (e) {
console.error(e);
throw Error(e as string);
}

const decoded = assetApi.decodeExtrinsic(payloadInfo.tx.toHex(), 'payload');
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());
27 changes: 4 additions & 23 deletions src/AssetTransferApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,26 +812,6 @@ describe('AssetTransferAPI', () => {

expect(unsigned.assetId).toStrictEqual(expected);
});

it('Should error during payload construction when a paysWithFeeOrigin is provided that matches a non sufficient asset', async () => {
await expect(async () => {
await systemAssetsApi.createTransferTransaction(
'2023',
'0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b',
['usdc'],
['4000000000'],
{
paysWithFeeOrigin: '100',
format: 'payload',
keepAlive: true,
paysWithFeeDest: 'USDC',
xcmVersion: 3,
sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1',
},
);
}).rejects.toThrow('asset with assetId 100 is not a sufficient asset to pay for fees');
});

it('Should error during payload construction when a non integer paysWithFeeOrigin is provided that is not a valid MultiLocation', async () => {
await expect(async () => {
await systemAssetsApi.createTransferTransaction(
Expand All @@ -848,7 +828,7 @@ describe('AssetTransferAPI', () => {
sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1',
},
);
}).rejects.toThrow('paysWithFeeOrigin value must be a valid asset location. Received: hello there');
}).rejects.toThrow('assetId "hello there" is not a valid paysWithFeeOrigin asset location');
});

it('Should error during payload construction when a paysWithFeeOrigin is provided that is not part of a valid lp token pair', async () => {
Expand All @@ -859,7 +839,8 @@ describe('AssetTransferAPI', () => {
['1984'],
['5000000'],
{
paysWithFeeOrigin: '{"parents":"1","interior":{"X2":["Parachain":"2007","PalletInstance":"1000000"]}}',
paysWithFeeOrigin:
'{"parents":"1","interior":{"X2":[{"Parachain":"20070223"},{"PalletInstance":"1000000"}]}}',
format: 'payload',
keepAlive: true,
paysWithFeeDest: '1984',
Expand All @@ -868,7 +849,7 @@ describe('AssetTransferAPI', () => {
},
);
}).rejects.toThrow(
'paysWithFeeOrigin value must be a valid asset location. Received: {"parents":"1","interior":{"X2":["Parachain":"2007","PalletInstance":"1000000"]}}',
'assetId {"parents":"1","interior":{"X2":[{"Parachain":"20070223"},{"PalletInstance":"1000000"}]}} is not a valid liquidity pool token for statemine',
);
});
});
Expand Down
59 changes: 19 additions & 40 deletions src/AssetTransferApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { GenericExtrinsicPayload } from '@polkadot/types/extrinsic';
import { EXTRINSIC_VERSION } from '@polkadot/types/extrinsic/v4/Extrinsic';
import type { RuntimeDispatchInfo, RuntimeDispatchInfoV1 } from '@polkadot/types/interfaces';
import type { AnyJson, ISubmittableResult } from '@polkadot/types/types';
import BN from 'bn.js';

import { CDN_URL, RELAY_CHAIN_IDS, RELAY_CHAIN_NAMES, SYSTEM_PARACHAINS_NAMES } from './consts';
import * as assets from './createCalls/assets';
Expand All @@ -35,6 +34,7 @@ import { assetIdsContainRelayAsset } from './createXcmTypes/util/assetIdsContain
import { chainDestIsBridge } from './createXcmTypes/util/chainDestIsBridge';
import { getAssetId } from './createXcmTypes/util/getAssetId';
import { getGlobalConsensusSystemName } from './createXcmTypes/util/getGlobalConsensusSystemName';
import { getPaysWithFeeOriginAssetLocationFromRegistry } from './createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry';
import { isParachain } from './createXcmTypes/util/isParachain';
import { isParachainPrimaryNativeAsset } from './createXcmTypes/util/isParachainPrimaryNativeAsset';
import { isSystemChain } from './createXcmTypes/util/isSystemChain';
Expand Down Expand Up @@ -749,37 +749,37 @@ export class AssetTransferApi {
opts: { paysWithFeeOrigin?: string; sendersAddr: string },
): Promise<GenericExtrinsicPayload> => {
const { paysWithFeeOrigin, sendersAddr } = opts;
let assetId: BN | AnyJson = new BN(0);
let assetId: AnyJson = {};

// if a paysWithFeeOrigin is provided and the chain is of system origin
// we assign the assetId to the value of paysWithFeeOrigin
const isOriginSystemParachain = SYSTEM_PARACHAINS_NAMES.includes(this.specName.toLowerCase());

if (paysWithFeeOrigin && isOriginSystemParachain) {
const isValidInt = validateNumber(paysWithFeeOrigin);
let paysWithFeeOriginAssetLocation: string;

if (isValidInt) {
assetId = new BN(paysWithFeeOrigin);
const isSufficient = await this.checkAssetIsSufficient(assetId);
if (!assetIdIsLocation(paysWithFeeOrigin)) {
const paysWithFeeOriginLocation = getPaysWithFeeOriginAssetLocationFromRegistry(this, paysWithFeeOrigin);

if (!isSufficient) {
if (!paysWithFeeOriginLocation) {
throw new BaseError(
`asset with assetId ${assetId.toString()} is not a sufficient asset to pay for fees`,
BaseErrorsEnum.InvalidAsset,
`assetId ${JSON.stringify(paysWithFeeOrigin)} is not a valid paysWithFeeOrigin asset location`,
BaseErrorsEnum.NoFeeAssetLpFound,
);
}
paysWithFeeOriginAssetLocation = JSON.stringify(paysWithFeeOriginLocation);
} else {
const [isValidLpToken, feeAsset] = await this.checkAssetLpTokenPairExists(paysWithFeeOrigin);
paysWithFeeOriginAssetLocation = paysWithFeeOrigin;
}

if (!isValidLpToken) {
throw new BaseError(
`assetId ${JSON.stringify(feeAsset)} is not a valid liquidity pool token for ${this.specName}`,
BaseErrorsEnum.NoFeeAssetLpFound,
);
}
const [isValidLpToken] = await this.checkAssetLpTokenPairExists(paysWithFeeOriginAssetLocation);

assetId = JSON.parse(paysWithFeeOrigin) as AnyJson;
if (!isValidLpToken) {
throw new BaseError(
`assetId ${paysWithFeeOrigin} is not a valid liquidity pool token for ${this.specName}`,
BaseErrorsEnum.NoFeeAssetLpFound,
);
}

assetId = JSON.parse(paysWithFeeOriginAssetLocation) as AnyJson;
}

const lastHeader = await this.api.rpc.chain.getHeader();
Expand Down Expand Up @@ -823,27 +823,6 @@ export class AssetTransferApi {
return extrinsicPayload;
};

/**
* checks the chains state to determine whether an asset is valid
* if it is valid, it returns whether it is marked as sufficient for paying fees
*
* @param assetId number
* @returns Promise<boolean>
*/
private checkAssetIsSufficient = async (assetId: BN): Promise<boolean> => {
try {
const asset = (await this.api.query.assets.asset(assetId)).unwrap();

if (asset.isSufficient.toString().toLowerCase() === 'true') {
return true;
}

return false;
} catch (err: unknown) {
throw new BaseError(`assetId ${assetId.toString()} does not match a valid asset`, BaseErrorsEnum.InvalidAsset);
}
};

/**
* Return the specName of the destination chainId
*
Expand Down
Loading