diff --git a/examples/assetHubToMoonriverPaysWithFeeOrigin.ts b/examples/assetHubToMoonriverPaysWithFeeOrigin.ts deleted file mode 100644 index 84dd78e6..00000000 --- a/examples/assetHubToMoonriverPaysWithFeeOrigin.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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 reserve payload to send 1 USDt (assetId: 1984) - * from a Kusama Asset Hub (System Parachain) account - * to a Moonriver (ParaChain) account, where the `xcmVersion` is set to 3 and no `weightLimit` is provided declaring that - * the allowable weight will be `unlimited` and `paysWithFeeOrigin` is `1984` - * declaring that asset with ID `1984` (USDt) should be used to pay for tx fees in the origin. - * - * 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 callInfo: TxResult<'payload'>; - try { - callInfo = await assetApi.createTransferTransaction( - '2023', - '5EWNeodpcQ6iYibJ3jmWVe85nsok1EDG8Kk3aFg8ZzpfY1qX', - ['1984'], - ['1000000'], - { - format: 'payload', - xcmVersion: safeXcmVersion, - paysWithFeeOrigin: '1984', - sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1', - }, - ); - - console.log(callInfo); - } catch (e) { - console.error(e); - throw Error(e as string); - } - - const decoded = assetApi.decodeExtrinsic(callInfo.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()); diff --git a/examples/kusama/assetHub/paysWithFeeOriginTransfers/usdtToMoonriverPaysWithUSDT.ts b/examples/kusama/assetHub/paysWithFeeOriginTransfers/usdtToMoonriverPaysWithUSDT.ts new file mode 100644 index 00000000..6c943648 --- /dev/null +++ b/examples/kusama/assetHub/paysWithFeeOriginTransfers/usdtToMoonriverPaysWithUSDT.ts @@ -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()); diff --git a/examples/polkadot/assetHub/paysWithFeeOriginTransfers/dotToHydrationPaysWithGLMR.ts b/examples/polkadot/assetHub/paysWithFeeOriginTransfers/dotToHydrationPaysWithGLMR.ts new file mode 100644 index 00000000..98e9ff05 --- /dev/null +++ b/examples/polkadot/assetHub/paysWithFeeOriginTransfers/dotToHydrationPaysWithGLMR.ts @@ -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()); diff --git a/examples/rococo/assetHub/paysWithFeeOriginTransfers/rocToBasiliskPaysWithMUSE.ts b/examples/rococo/assetHub/paysWithFeeOriginTransfers/rocToBasiliskPaysWithMUSE.ts new file mode 100644 index 00000000..a20fe8f8 --- /dev/null +++ b/examples/rococo/assetHub/paysWithFeeOriginTransfers/rocToBasiliskPaysWithMUSE.ts @@ -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()); diff --git a/src/AssetTransferApi.spec.ts b/src/AssetTransferApi.spec.ts index ac82de77..f6bdd7e2 100644 --- a/src/AssetTransferApi.spec.ts +++ b/src/AssetTransferApi.spec.ts @@ -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( @@ -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 () => { @@ -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', @@ -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', ); }); }); diff --git a/src/AssetTransferApi.ts b/src/AssetTransferApi.ts index 1caf2c6a..ac7bc4e9 100644 --- a/src/AssetTransferApi.ts +++ b/src/AssetTransferApi.ts @@ -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'; @@ -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'; @@ -749,37 +749,37 @@ export class AssetTransferApi { opts: { paysWithFeeOrigin?: string; sendersAddr: string }, ): Promise => { 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(); @@ -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 - */ - private checkAssetIsSufficient = async (assetId: BN): Promise => { - 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 * diff --git a/src/createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry.spec.ts b/src/createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry.spec.ts new file mode 100644 index 00000000..75fd65f3 --- /dev/null +++ b/src/createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry.spec.ts @@ -0,0 +1,103 @@ +// Copyright 2024 Parity Technologies (UK) Ltd. + +import { AssetTransferApi } from '../../AssetTransferApi'; +import { adjustedMockSystemApiV1011000 } from '../../testHelpers/adjustedMockSystemApiV1011000'; +import { getPaysWithFeeOriginAssetLocationFromRegistry } from './getPaysWithFeeOriginAssetLocationFromRegistry'; + +describe('getPaysWithFeeOriginAssetLocationFromRegistry', () => { + const assetHubAPI = new AssetTransferApi(adjustedMockSystemApiV1011000, 'asset-hub-westend', 4, { + registryType: 'NPM', + }); + + it('Should correctly return the asset location of a valid Assets Pallet integer asset ID on AssetHub', () => { + const paysWithFeeOriginAssetId = '1984'; + const expected = { + parents: 0, + interior: { + X2: [ + { + PalletInstance: '50', + }, + { + GeneralIndex: '1984', + }, + ], + }, + }; + + const result = getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, paysWithFeeOriginAssetId); + + expect(result).toEqual(expected); + }); + it('Should correctly return the asset location of a valid unique Assets Pallet asset symbol on AssetHub', () => { + const paysWithFeeOriginAssetId = 'RSD'; + const expected = { + parents: 0, + interior: { + X2: [ + { + PalletInstance: 50, + }, + { + GeneralIndex: '22061', + }, + ], + }, + }; + + const result = getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, paysWithFeeOriginAssetId); + + expect(result).toEqual(expected); + }); + it('Should correctly return the asset location of a valid Foreign Assets Pallet asset symbol on AssetHub', () => { + const paysWithFeeOriginAssetId = 'ROC'; + const expected = { + parents: '2', + interior: { + X1: { + GlobalConsensus: 'Rococo', + }, + }, + }; + + const result = getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, paysWithFeeOriginAssetId); + + expect(result).toEqual(expected); + }); + + it('Should correctly return the asset location of a valid foreign asset symbol on AssetHub', () => { + const paysWithFeeOriginAssetId = '1984'; + const expected = { + parents: 0, + interior: { + X2: [ + { + PalletInstance: '50', + }, + { + GeneralIndex: '1984', + }, + ], + }, + }; + + const result = getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, paysWithFeeOriginAssetId); + + expect(result).toEqual(expected); + }); + it('Should correctly return undefined when an asset ID is not found', () => { + expect(getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, 'DOESNOTEXIST')).toEqual(undefined); + }); + it('Should correctly return undefined when an empty paysWithFeeOrigin value is passed in', () => { + expect(getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, '')).toEqual(undefined); + }); + it('Should correctly error when given a non unique asset symbol', () => { + const paysWithFeeOriginAssetId = 'USDT'; + + const err = () => getPaysWithFeeOriginAssetLocationFromRegistry(assetHubAPI, paysWithFeeOriginAssetId); + + expect(err).toThrow( + `Multiple assets found with symbol USDT:\nassetId: 66 symbol: USDT\nassetId: 67 symbol: USDT\nassetId: 2000 symbol: USDT\nassetId: 8888 symbol: USDT\nPlease provide an integer assetId or valid asset location for paysWithFeeOrigin rather than the token symbol`, + ); + }); +}); diff --git a/src/createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry.ts b/src/createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry.ts new file mode 100644 index 00000000..18547f17 --- /dev/null +++ b/src/createXcmTypes/util/getPaysWithFeeOriginAssetLocationFromRegistry.ts @@ -0,0 +1,117 @@ +// Copyright 2024 Parity Technologies (UK) Ltd. + +import { AssetTransferApi } from '../../AssetTransferApi'; +import { BaseError, BaseErrorsEnum } from '../../errors'; +import { AssetsInfo } from '../../registry/types'; +import { validateNumber } from '../../validate'; +import { UnionXcmMultiLocation } from '../types'; + +export const getPaysWithFeeOriginAssetLocationFromRegistry = ( + ataAPI: AssetTransferApi, + paysWithFeeOriginAssetId: string, +): UnionXcmMultiLocation | undefined => { + if (paysWithFeeOriginAssetId.length === 0) { + return undefined; + } + + let location: UnionXcmMultiLocation | undefined = undefined; + + const { registry, specName } = ataAPI; + const currentChainId = registry.lookupChainIdBySpecName(specName); + const currentRelayRegistry = registry.currentRelayRegistry; + const assetIsValidInt = validateNumber(paysWithFeeOriginAssetId); + const { assetsInfo, foreignAssetsInfo } = currentRelayRegistry[currentChainId]; + + if (assetIsValidInt) { + // if assetId index is valid, return the location using the Assets Pallet Instance ID + if (assetsInfo[paysWithFeeOriginAssetId] && assetsInfo[paysWithFeeOriginAssetId].length > 0) { + location = { + parents: 0, + interior: { + X2: [ + { + PalletInstance: '50', + }, + { + GeneralIndex: paysWithFeeOriginAssetId, + }, + ], + }, + } as UnionXcmMultiLocation; + } + } else { + const assetsInfoTokensMatched: AssetsInfo[] = []; + for (const [id, symbol] of Object.entries(assetsInfo)) { + if (symbol.toLowerCase() === paysWithFeeOriginAssetId.toLowerCase()) { + const assetInfo: AssetsInfo = { + id, + symbol, + }; + assetsInfoTokensMatched.push(assetInfo); + } + } + + if (assetsInfoTokensMatched.length > 1) { + const assetMessageInfo = assetsInfoTokensMatched.map((token) => `assetId: ${token.id} symbol: ${token.symbol}`); + const message = + `Multiple assets found with symbol ${paysWithFeeOriginAssetId}:\n${assetMessageInfo.toString()}\nPlease provide an integer assetId or valid asset location for paysWithFeeOrigin rather than the token symbol` + .trim() + .replace(/,/g, '\n'); + + throw new BaseError(message, BaseErrorsEnum.MultipleNonUniqueAssetsFound); + } + + const registryAssetId = Object.keys(assetsInfo).find( + (key) => assetsInfo[key].toLowerCase() === paysWithFeeOriginAssetId.toLowerCase(), + ); + + if (registryAssetId) { + // if asset is in registry, return the asset location based on the Assets Pallet Instance ID + location = { + parents: 0, + interior: { + X2: [ + { + PalletInstance: 50, + }, + { + GeneralIndex: registryAssetId, + }, + ], + }, + } as UnionXcmMultiLocation; + } + } + + // Check Foreign Assets + if (!location) { + const assetsInfoTokensMatched: AssetsInfo[] = []; + for (const [key, data] of Object.entries(foreignAssetsInfo)) { + if (key.toLowerCase() === paysWithFeeOriginAssetId.toLowerCase()) { + const assetInfo: AssetsInfo = { + id: data.multiLocation, + symbol: data.symbol, + }; + assetsInfoTokensMatched.push(assetInfo); + } + } + + if (assetsInfoTokensMatched.length > 1) { + const assetMessageInfo = assetsInfoTokensMatched.map((token) => `assetId: ${token.id} symbol: ${token.symbol}`); + const message = + `Multiple assets found with symbol ${paysWithFeeOriginAssetId}:\n${assetMessageInfo.toString()}\nPlease provide a valid asset location for paysWithFeeOrigin rather than the token symbol` + .trim() + .replace(/,/g, '\n'); + + throw new BaseError(message, BaseErrorsEnum.MultipleNonUniqueAssetsFound); + } + + for (const [key, data] of Object.entries(foreignAssetsInfo)) { + if (key.toLowerCase() === paysWithFeeOriginAssetId.toLowerCase()) { + location = JSON.parse(data.multiLocation) as UnionXcmMultiLocation; + } + } + } + + return location; +}; diff --git a/src/integrationTests/AssetsTransferApi.spec.ts b/src/integrationTests/AssetsTransferApi.spec.ts index 0d0caea3..ca7186f3 100644 --- a/src/integrationTests/AssetsTransferApi.spec.ts +++ b/src/integrationTests/AssetsTransferApi.spec.ts @@ -495,26 +495,6 @@ describe('AssetTransferApi Integration Tests', () => { }, ); }; - const nativeBaseSystemPaysWithFeeOriginAssetIdCreateTx = async ( - ataAPI: AssetTransferApi, - format: T, - xcmVersion: number, - opts: CreateXcmCallOpts, - ): Promise> => { - return await ataAPI.createTransferTransaction( - '2000', // Since this is not `0` we know this is to a parachain - '0xf5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b', - ['KSM'], - ['100'], - { - paysWithFeeOrigin: `1984`, - format, - xcmVersion, - weightLimit: opts.weightLimit, - sendersAddr: 'FBeL7DanUDs5SZrxZY1CizMaPgG9vZgJgvr52C2dg81SsF1', - }, - ); - }; const foreignAssetMultiLocationBaseSystemCreateTx = async ( ataAPI: AssetTransferApi, format: T, @@ -765,19 +745,6 @@ describe('AssetTransferApi Integration Tests', () => { '0xf81f0801010100411f0100010100f5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b01040001000091010000000001a10f411f45022800010002043205011fcc240000040000000000000000000000000000000000000000000000000000000000000000000000be2554aa8a0151eb4d706308c47d16996af391e4c5e499c7cbef24259b7d4503', ); }); - it('Should correctly construct a paysWithFeeOrigin tx for V2 using an assets id', async () => { - const res = await nativeBaseSystemPaysWithFeeOriginAssetIdCreateTx(systemAssetsApi, 'payload', 2, { - weightLimit: { - refTime: '1000', - proofSize: '2000', - }, - isForeignAssetsTransfer: false, - isLiquidTokenTransfer: false, - }); - expect(res.tx.toHex()).toStrictEqual( - '0xf81f0801010100411f0100010100f5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b01040001000091010000000001a10f411f45022800010000cc240000040000000000000000000000000000000000000000000000000000000000000000000000be2554aa8a0151eb4d706308c47d16996af391e4c5e499c7cbef24259b7d4503', - ); - }); }); describe('V3', () => { it('Should correctly build a call for a limitedReserveTransferAssets for V3', async () => { @@ -1028,19 +995,6 @@ describe('AssetTransferApi Integration Tests', () => { }); }); }); - it('Should correctly construct a paysWithFeeOrigin tx for V3 using an assets id', async () => { - const res = await nativeBaseSystemPaysWithFeeOriginAssetIdCreateTx(systemAssetsApi, 'payload', 3, { - weightLimit: { - refTime: '1000', - proofSize: '2000', - }, - isForeignAssetsTransfer: false, - isLiquidTokenTransfer: false, - }); - expect(res.tx.toHex()).toStrictEqual( - '0xf81f0803010100411f0300010100f5d5714c084c112843aca74f8c498da06cc5a2d63153b825189baa51043b1f0b03040001000091010000000001a10f411f45022800010000cc240000040000000000000000000000000000000000000000000000000000000000000000000000be2554aa8a0151eb4d706308c47d16996af391e4c5e499c7cbef24259b7d4503', - ); - }); it('Should correctly construct a paysWithFeeOrigin tx for V3 using an assets location', async () => { const res = await nativeBaseSystemPaysWithFeeOriginAssetLocationCreateTx(systemAssetsApi, 'payload', 3, { weightLimit: {