git.delta.rocks / unique-network / refs/commits / 61fc26e2c8d2

difftreelog

test(xcm qtz) added `XcmTestHelper`

PraetorP2023-09-28parent: #dc6f0e2.patch.diff
in: master

5 files changed

modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -116,6 +116,8 @@
     "testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts",
     "testFullXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Unique.test.ts",
     "testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts",
+    "testLowLevelXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/lowLevelXcmQuartz.test.ts",
+    "testFullXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Quartz.test.ts",
     "testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts",
     "testXcmTransferAcala": "yarn _test ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000",
     "testXcmTransferStatemine": "yarn _test ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000",
addedtests/src/xcm/lowLevelXcmQuartz.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/xcm/lowLevelXcmQuartz.test.ts
@@ -0,0 +1,381 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network 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.
+
+// Unique Network 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 Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {IKeyringPair} from '@polkadot/types/types';
+import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds,  usingMoonriverPlaygrounds, usingShidenPlaygrounds} from '../util';
+import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
+import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl, SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT} from './xcm.types';
+
+
+const testHelper = new XcmTestHelper('quartz');
+
+describeXCM('[XCMLL] Integration test: Exchanging tokens with Karura', () => {
+  let alice: IKeyringPair;
+  let randomAccount: IKeyringPair;
+
+  let balanceQuartzTokenInit: bigint;
+  let balanceQuartzTokenMiddle: bigint;
+  let balanceQuartzTokenFinal: bigint;
+  let balanceKaruraTokenInit: bigint;
+  let balanceKaruraTokenMiddle: bigint;
+  let balanceKaruraTokenFinal: bigint;
+  let balanceQuartzForeignTokenInit: bigint;
+  let balanceQuartzForeignTokenMiddle: bigint;
+  let balanceQuartzForeignTokenFinal: bigint;
+
+  // computed by a test transfer from prod Quartz to prod Karura.
+  // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9
+  // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block)
+  const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n;
+  const karuraEps = 8n * 10n ** 16n;
+
+  let karuraBackwardTransferAmount: bigint;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      [randomAccount] = await helper.arrange.createAccounts([0n], alice);
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {
+      const destination = {
+        V2: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: QUARTZ_CHAIN,
+            },
+          },
+        },
+      };
+
+      const metadata = {
+        name: 'Quartz',
+        symbol: 'QTZ',
+        decimals: 18,
+        minimalBalance: 1000000000000000000n,
+      };
+
+      await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);
+      balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);
+      balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});
+    });
+
+    await usingPlaygrounds(async (helper) => {
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);
+      balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address);
+    });
+  });
+
+  itSub('Should connect and send QTZ to Karura', async () => {
+    await testHelper.sendUnqTo('karura', randomAccount);
+  });
+
+  itSub('Should connect to Karura and send QTZ back', async () => {
+    await testHelper.sendUnqBack('karura', alice, randomAccount);
+  });
+
+  itSub('Karura can send only up to its balance', async () => {
+    await testHelper.sendOnlyOwnedBalance('karura', alice);
+  });
+});
+// These tests are relevant only when
+// the the corresponding foreign assets are not registered
+describeXCM('[XCMLL] Integration test: Quartz rejects non-native tokens', () => {
+  let alice: IKeyringPair;
+  let alith: IKeyringPair;
+
+  const testAmount = 100_000_000_000n;
+  let quartzParachainJunction;
+  let quartzAccountJunction;
+
+  let quartzParachainMultilocation: any;
+  let quartzAccountMultilocation: any;
+  let quartzCombinedMultilocation: any;
+
+  let messageSent: any;
+
+  const maxWaitBlocks = 3;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+
+      quartzParachainJunction = {Parachain: QUARTZ_CHAIN};
+      quartzAccountJunction = {
+        AccountId32: {
+          network: 'Any',
+          id: alice.addressRaw,
+        },
+      };
+
+      quartzParachainMultilocation = {
+        V2: {
+          parents: 1,
+          interior: {
+            X1: quartzParachainJunction,
+          },
+        },
+      };
+
+      quartzAccountMultilocation = {
+        V2: {
+          parents: 0,
+          interior: {
+            X1: quartzAccountJunction,
+          },
+        },
+      };
+
+      quartzCombinedMultilocation = {
+        V2: {
+          parents: 1,
+          interior: {
+            X2: [quartzParachainJunction, quartzAccountJunction],
+          },
+        },
+      };
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    // eslint-disable-next-line require-await
+    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {
+      alith = helper.account.alithAccount();
+    });
+  });
+
+  itSub('Quartz rejects KAR tokens from Karura', async () => {
+    await testHelper.rejectNativeTokensFrom('karura', alice);
+  });
+
+  itSub('Quartz rejects MOVR tokens from Moonriver', async () => {
+    await testHelper.rejectNativeTokensFrom('moonriver', alice);
+  });
+
+  itSub('Quartz rejects SDN tokens from Shiden', async () => {
+    await testHelper.rejectNativeTokensFrom('shiden', alice);
+  });
+});
+
+describeXCM('[XCMLL] Integration test: Exchanging QTZ with Moonriver', () => {
+  // Quartz constants
+  let alice: IKeyringPair;
+  let quartzAssetLocation;
+
+  let randomAccountQuartz: IKeyringPair;
+  let randomAccountMoonriver: IKeyringPair;
+
+  // Moonriver constants
+  let assetId: string;
+
+  const quartzAssetMetadata = {
+    name: 'xcQuartz',
+    symbol: 'xcQTZ',
+    decimals: 18,
+    isFrozen: false,
+    minimalBalance: 1n,
+  };
+
+  let balanceQuartzTokenInit: bigint;
+  let balanceQuartzTokenMiddle: bigint;
+  let balanceQuartzTokenFinal: bigint;
+  let balanceForeignQtzTokenInit: bigint;
+  let balanceForeignQtzTokenMiddle: bigint;
+  let balanceForeignQtzTokenFinal: bigint;
+  let balanceMovrTokenInit: bigint;
+  let balanceMovrTokenMiddle: bigint;
+  let balanceMovrTokenFinal: bigint;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);
+
+      balanceForeignQtzTokenInit = 0n;
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {
+      const alithAccount = helper.account.alithAccount();
+      const baltatharAccount = helper.account.baltatharAccount();
+      const dorothyAccount = helper.account.dorothyAccount();
+
+      randomAccountMoonriver = helper.account.create();
+
+      // >>> Sponsoring Dorothy >>>
+      console.log('Sponsoring Dorothy.......');
+      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);
+      console.log('Sponsoring Dorothy.......DONE');
+      // <<< Sponsoring Dorothy <<<
+
+      quartzAssetLocation = {
+        XCM: {
+          parents: 1,
+          interior: {X1: {Parachain: QUARTZ_CHAIN}},
+        },
+      };
+      const existentialDeposit = 1n;
+      const isSufficient = true;
+      const unitsPerSecond = 1n;
+      const numAssetsWeightHint = 0;
+
+      const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
+        location: quartzAssetLocation,
+        metadata: quartzAssetMetadata,
+        existentialDeposit,
+        isSufficient,
+        unitsPerSecond,
+        numAssetsWeightHint,
+      });
+
+      console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
+
+      await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+
+      // >>> Acquire Quartz AssetId Info on Moonriver >>>
+      console.log('Acquire Quartz AssetId Info on Moonriver.......');
+
+      assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();
+
+      console.log('QTZ asset ID is %s', assetId);
+      console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');
+      // >>> Acquire Quartz AssetId Info on Moonriver >>>
+
+      // >>> Sponsoring random Account >>>
+      console.log('Sponsoring random Account.......');
+      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);
+      console.log('Sponsoring random Account.......DONE');
+      // <<< Sponsoring random Account <<<
+
+      balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address);
+    });
+
+    await usingPlaygrounds(async (helper) => {
+      await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);
+      balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address);
+    });
+  });
+
+  itSub('Should connect and send QTZ to Moonriver', async () => {
+    await testHelper.sendUnqTo('moonriver', randomAccountQuartz, randomAccountMoonriver);
+  });
+
+  itSub('Should connect to Moonriver and send QTZ back', async () => {
+    await testHelper.sendUnqBack('moonriver', alice, randomAccountQuartz);
+  });
+
+  itSub('Moonriver can send only up to its balance', async () => {
+    await testHelper.sendOnlyOwnedBalance('moonriver', alice);
+  });
+
+  itSub('Should not accept reserve transfer of QTZ from Moonriver', async () => {
+    await testHelper.reserveTransferUNQfrom('moonriver', alice);
+  });
+});
+
+describeXCM('[XCMLL] Integration test: Exchanging tokens with Shiden', () => {
+  let alice: IKeyringPair;
+  let sender: IKeyringPair;
+
+  const QTZ_ASSET_ID_ON_SHIDEN = 1;
+  const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n;
+
+  // Quartz -> Shiden
+  const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden
+  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+  const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ
+  const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens
+
+  // Shiden -> Quartz
+  const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ
+  const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 4.999_999_999_088_000_000n QTZ
+
+  let balanceAfterQuartzToShidenXCM: bigint;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      [sender] = await helper.arrange.createAccounts([100n], alice);
+      console.log('sender', sender.address);
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    await usingShidenPlaygrounds(shidenUrl, async (helper) => {
+      console.log('1. Create foreign asset and metadata');
+      // TODO update metadata with values from production
+      await helper.assets.create(
+        alice,
+        QTZ_ASSET_ID_ON_SHIDEN,
+        alice.address,
+        QTZ_MINIMAL_BALANCE_ON_SHIDEN,
+      );
+
+      await helper.assets.setMetadata(
+        alice,
+        QTZ_ASSET_ID_ON_SHIDEN,
+        'Cross chain QTZ',
+        'xcQTZ',
+        Number(QTZ_DECIMALS),
+      );
+
+      console.log('2. Register asset location on Shiden');
+      const assetLocation = {
+        V2: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: QUARTZ_CHAIN,
+            },
+          },
+        },
+      };
+
+      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
+
+      console.log('3. Set QTZ payment for XCM execution on Shiden');
+      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
+
+      console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');
+      await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);
+    });
+  });
+
+  itSub('Should connect and send QTZ to Shiden', async () => {
+    await testHelper.sendUnqTo('shiden', sender);
+  });
+
+  itSub('Should connect to Shiden and send QTZ back', async () => {
+    await testHelper.sendUnqBack('shiden', alice, sender);
+  });
+
+  itSub('Shiden can send only up to its balance', async () => {
+    await testHelper.sendOnlyOwnedBalance('shiden', alice);
+  });
+
+  itSub('Should not accept reserve transfer of QTZ from Shiden', async () => {
+    await testHelper.reserveTransferUNQfrom('shiden', alice);
+  });
+});
modifiedtests/src/xcm/lowLevelXcmUnique.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/lowLevelXcmUnique.test.ts
+++ b/tests/src/xcm/lowLevelXcmUnique.test.ts
@@ -20,7 +20,7 @@
 import {Event} from '../util/playgrounds/unique.dev';
 import {nToBigInt} from '@polkadot/util';
 import {hexToString} from '@polkadot/util';
-import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types';
+import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types';
 
 
 const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
@@ -34,6 +34,7 @@
 let balanceUniqueTokenFinal: bigint;
 let unqFees: bigint;
 
+const testHelper = new XcmTestHelper('unique');
 
 async function genericSendUnqTo(
   networkName: keyof typeof NETWORKS,
@@ -434,20 +435,20 @@
   });
 
   itSub('Should connect and send UNQ to Polkadex', async () => {
-    await genericSendUnqTo('polkadex', randomAccount);
+    await testHelper.sendUnqTo('polkadex', randomAccount);
   });
 
 
   itSub('Should connect to Polkadex and send UNQ back', async () => {
-    await genericSendUnqBack('polkadex', alice, randomAccount);
+    await testHelper.sendUnqBack('polkadex', alice, randomAccount);
   });
 
   itSub('Polkadex can send only up to its balance', async () => {
-    await genericSendOnlyOwnedBalance('polkadex', alice);
+    await testHelper.sendOnlyOwnedBalance('polkadex', alice);
   });
 
   itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {
-    await genericReserveTransferUNQfrom('polkadex', alice);
+    await testHelper.reserveTransferUNQfrom('polkadex', alice);
   });
 });
 
@@ -574,19 +575,19 @@
   });
 
   itSub('Should connect and send UNQ to Moonbeam', async () => {
-    await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
+    await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
   });
 
   itSub('Should connect to Moonbeam and send UNQ back', async () => {
-    await genericSendUnqBack('moonbeam', alice, randomAccountUnique);
+    await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);
   });
 
   itSub('Moonbeam can send only up to its balance', async () => {
-    await genericSendOnlyOwnedBalance('moonbeam', alice);
+    await testHelper.sendOnlyOwnedBalance('moonbeam', alice);
   });
 
   itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {
-    await genericReserveTransferUNQfrom('moonbeam', alice);
+    await testHelper.reserveTransferUNQfrom('moonbeam', alice);
   });
 });
 
modifiedtests/src/xcm/xcm.types.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcm.types.ts
+++ b/tests/src/xcm/xcm.types.ts
@@ -1,4 +1,5 @@
-import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util';
+import {IKeyringPair} from '@polkadot/types/types';
+import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingShidenPlaygrounds} from '../util';
 import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
 import config from '../config';
 
@@ -9,19 +10,39 @@
 export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);
 export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);
 
+export const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);
+export const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);
+export const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);
+export const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);
+export const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);
+
+export const relayUrl = config.relayUrl;
+export const statemintUrl = config.statemintUrl;
+export const statemineUrl = config.statemineUrl;
+
 export const acalaUrl = config.acalaUrl;
 export const moonbeamUrl = config.moonbeamUrl;
 export const astarUrl = config.astarUrl;
 export const polkadexUrl = config.polkadexUrl;
 
+export const karuraUrl = config.karuraUrl;
+export const moonriverUrl = config.moonriverUrl;
+export const shidenUrl = config.shidenUrl;
+
 export const SAFE_XCM_VERSION = 3;
 
-export const maxWaitBlocks = 6;
 
+export const RELAY_DECIMALS = 12;
+export const STATEMINE_DECIMALS = 12;
+export const KARURA_DECIMALS = 12;
+export const SHIDEN_DECIMALS = 18n;
+export const QTZ_DECIMALS = 18n;
 
 export const ASTAR_DECIMALS = 18n;
 export const UNQ_DECIMALS = 18n;
 
+export const maxWaitBlocks = 6;
+
 export const uniqueMultilocation = {
   parents: 1,
   interior: {
@@ -52,9 +73,15 @@
   astar: usingAstarPlaygrounds,
   polkadex: usingPolkadexPlaygrounds,
   moonbeam: usingMoonbeamPlaygrounds,
+  moonriver: usingMoonriverPlaygrounds,
+  karura: usingKaruraPlaygrounds,
+  shiden: usingShidenPlaygrounds,
 } as const;
+type NetworkNames = keyof typeof NETWORKS;
 
-export function mapToChainId(networkName: keyof typeof NETWORKS) {
+type NativeRuntime = 'opal' | 'quartz' | 'unique';
+
+export function mapToChainId(networkName: keyof typeof NETWORKS): number {
   switch (networkName) {
     case 'acala':
       return ACALA_CHAIN;
@@ -64,10 +91,16 @@
       return MOONBEAM_CHAIN;
     case 'polkadex':
       return POLKADEX_CHAIN;
+    case 'moonriver':
+      return MOONRIVER_CHAIN;
+    case 'karura':
+      return KARURA_CHAIN;
+    case 'shiden':
+      return SHIDEN_CHAIN;
   }
 }
 
-export function mapToChainUrl(networkName: keyof typeof NETWORKS): string {
+export function mapToChainUrl(networkName: NetworkNames): string {
   switch (networkName) {
     case 'acala':
       return acalaUrl;
@@ -77,9 +110,364 @@
       return moonbeamUrl;
     case 'polkadex':
       return polkadexUrl;
+    case 'moonriver':
+      return moonriverUrl;
+    case 'karura':
+      return karuraUrl;
+    case 'shiden':
+      return shidenUrl;
   }
 }
 
-export function getDevPlayground<T extends keyof typeof NETWORKS>(name: T) {
+export function getDevPlayground(name: NetworkNames) {
   return NETWORKS[name];
+}
+
+export const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
+const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
+const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
+const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
+const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
+
+export class XcmTestHelper {
+  private _balanceUniqueTokenInit: bigint = 0n;
+  private _balanceUniqueTokenMiddle: bigint = 0n;
+  private _balanceUniqueTokenFinal: bigint = 0n;
+  private _unqFees: bigint = 0n;
+  private _nativeRuntime: NativeRuntime;
+
+  constructor(runtime: NativeRuntime) {
+    this._nativeRuntime = runtime;
+  }
+
+  private _getNativeId() {
+    switch (this._nativeRuntime) {
+      case 'opal':
+        // To-Do
+        return 10;
+      case 'quartz':
+        return QUARTZ_CHAIN;
+      case 'unique':
+        return UNIQUE_CHAIN;
+    }
+  }
+  private _isAddress20FormatFor(network: NetworkNames) {
+    switch (network) {
+      case 'moonbeam':
+      case 'moonriver':
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  async sendUnqTo(
+    networkName: keyof typeof NETWORKS,
+    randomAccount: IKeyringPair,
+    randomAccountOnTargetChain = randomAccount,
+  ) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+    await usingPlaygrounds(async (helper) => {
+      this._balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
+      const destination = {
+        V2: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: mapToChainId(networkName),
+            },
+          },
+        },
+      };
+
+      const beneficiary = {
+        V2: {
+          parents: 0,
+          interior: {
+            X1: (
+              this._isAddress20FormatFor(networkName) ?
+                {
+                  AccountKey20: {
+                    network: 'Any',
+                    key: randomAccountOnTargetChain.address,
+                  },
+                }
+                :
+                {
+                  AccountId32: {
+                    network: 'Any',
+                    id: randomAccountOnTargetChain.addressRaw,
+                  },
+                }
+            ),
+          },
+        },
+      };
+
+      const assets = {
+        V2: [
+          {
+            id: {
+              Concrete: {
+                parents: 0,
+                interior: 'Here',
+              },
+            },
+            fun: {
+              Fungible: TRANSFER_AMOUNT,
+            },
+          },
+        ],
+      };
+      const feeAssetItem = 0;
+
+      await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
+      const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+      this._balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
+
+      this._unqFees = this._balanceUniqueTokenInit - this._balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
+      console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(this._unqFees));
+      expect(this._unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true;
+
+      await targetPlayground(networkUrl, async (helper) => {
+      /*
+        Since only the parachain part of the Polkadex
+        infrastructure is launched (without their
+        solochain validators), processing incoming
+        assets will lead to an error.
+        This error indicates that the Polkadex chain
+        received a message from the Unique network,
+        since the hash is being checked to ensure
+        it matches what was sent.
+      */
+        if(networkName == 'polkadex') {
+          await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
+        } else {
+          await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
+        }
+      });
+
+    });
+  }
+
+  async sendUnqBack(
+    networkName: keyof typeof NETWORKS,
+    sudoer: IKeyringPair,
+    randomAccountOnUnq: IKeyringPair,
+  ) {
+    const networkUrl = mapToChainUrl(networkName);
+
+    const targetPlayground = getDevPlayground(networkName);
+    await usingPlaygrounds(async (helper) => {
+
+      const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+        randomAccountOnUnq.addressRaw,
+        {
+          Concrete: {
+            parents: 1,
+            interior: {
+              X1: {Parachain: this._getNativeId()},
+            },
+          },
+        },
+        SENDBACK_AMOUNT,
+      );
+
+      let xcmProgramSent: any;
+
+
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram);
+          xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        } else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
+          xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+      await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
+
+      this._balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
+
+      expect(this._balanceUniqueTokenFinal).to.be.equal(this._balanceUniqueTokenInit - this._unqFees - STAYED_ON_TARGET_CHAIN);
+
+    });
+  }
+
+  async sendOnlyOwnedBalance(
+    networkName: keyof typeof NETWORKS,
+    sudoer: IKeyringPair,
+  ) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+
+    const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
+
+    await usingPlaygrounds(async (helper) => {
+      const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
+      await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
+      const moreThanTargetChainHas = 2n * targetChainBalance;
+
+      const targetAccount = helper.arrange.createEmptyAccount();
+
+      const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+        targetAccount.addressRaw,
+        {
+          Concrete: {
+            parents: 0,
+            interior: 'Here',
+          },
+        },
+        moreThanTargetChainHas,
+      );
+
+      let maliciousXcmProgramSent: any;
+
+
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgram);
+          maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        } else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
+          maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+      await expectFailedToTransact(helper, maliciousXcmProgramSent);
+
+      const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
+      expect(targetAccountBalance).to.be.equal(0n);
+    });
+  }
+
+  async reserveTransferUNQfrom(netwokrName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
+    const networkUrl = mapToChainUrl(netwokrName);
+    const targetPlayground = getDevPlayground(netwokrName);
+
+    await usingPlaygrounds(async (helper) => {
+      const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
+      const targetAccount = helper.arrange.createEmptyAccount();
+
+      const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+        targetAccount.addressRaw,
+        {
+          Concrete: {
+            parents: 1,
+            interior: {
+              X1: {
+                Parachain: this._getNativeId(),
+              },
+            },
+          },
+        },
+        testAmount,
+      );
+
+      const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+        targetAccount.addressRaw,
+        {
+          Concrete: {
+            parents: 0,
+            interior: 'Here',
+          },
+        },
+        testAmount,
+      );
+
+      let maliciousXcmProgramFullIdSent: any;
+      let maliciousXcmProgramHereIdSent: any;
+      const maxWaitBlocks = 3;
+
+      // Try to trick Unique using full UNQ identification
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
+          maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+        // Moonbeam case
+        else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using path asset identification`,batchCall);
+
+          maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+
+      await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);
+
+      let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
+      expect(accountBalance).to.be.equal(0n);
+
+      // Try to trick Unique using shortened UNQ identification
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramHereId);
+          maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+        else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
+
+          maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+      await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
+
+      accountBalance = await helper.balance.getSubstrate(targetAccount.address);
+      expect(accountBalance).to.be.equal(0n);
+    });
+  }
+
+  async rejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+    let messageSent: any;
+
+    await usingPlaygrounds(async (helper) => {
+      const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+        helper.arrange.createEmptyAccount().addressRaw,
+        {
+          Concrete: {
+            parents: 1,
+            interior: {
+              X1: {
+                Parachain: mapToChainId(networkName),
+              },
+            },
+          },
+        },
+        TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
+      );
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
+          messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        } else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);
+
+          messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+      await expectFailedToTransact(helper, messageSent);
+    });
+  }
+
 }
\ No newline at end of file
modifiedtests/src/xcm/xcmQuartz.test.tsdiffbeforeafterboth
before · tests/src/xcm/xcmQuartz.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import config from '../config';19import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util';20import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';2122const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);23const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);24const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);25const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);26const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);2728const STATEMINE_PALLET_INSTANCE = 50;2930const relayUrl = config.relayUrl;31const statemineUrl = config.statemineUrl;32const karuraUrl = config.karuraUrl;33const moonriverUrl = config.moonriverUrl;34const shidenUrl = config.shidenUrl;3536const RELAY_DECIMALS = 12;37const STATEMINE_DECIMALS = 12;38const KARURA_DECIMALS = 12;39const SHIDEN_DECIMALS = 18n;40const QTZ_DECIMALS = 18n;4142const TRANSFER_AMOUNT = 2000000000000000000000000n;4344const FUNDING_AMOUNT = 3_500_000_0000_000_000n;4546const TRANSFER_AMOUNT_RELAY = 50_000_000_000_000_000n;4748const USDT_ASSET_ID = 100;49const USDT_ASSET_METADATA_DECIMALS = 18;50const USDT_ASSET_METADATA_NAME = 'USDT';51const USDT_ASSET_METADATA_DESCRIPTION = 'USDT';52const USDT_ASSET_METADATA_MINIMAL_BALANCE = 1n;53const USDT_ASSET_AMOUNT = 10_000_000_000_000_000_000_000_000n;5455const SAFE_XCM_VERSION = 2;5657describeXCM('[XCM] Integration test: Exchanging USDT with Statemine', () => {58  let alice: IKeyringPair;59  let bob: IKeyringPair;6061  let balanceStmnBefore: bigint;62  let balanceStmnAfter: bigint;6364  let balanceQuartzBefore: bigint;65  let balanceQuartzAfter: bigint;66  let balanceQuartzFinal: bigint;6768  let balanceBobBefore: bigint;69  let balanceBobAfter: bigint;70  let balanceBobFinal: bigint;7172  let balanceBobRelayTokenBefore: bigint;73  let balanceBobRelayTokenAfter: bigint;747576  before(async () => {77    await usingPlaygrounds(async (helper, privateKey) => {78      alice = await privateKey('//Alice');79      bob = await privateKey('//Bob'); // sovereign account on Statemine(t) funds donor8081      // Set the default version to wrap the first message to other chains.82      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);83    });8485    await usingRelayPlaygrounds(relayUrl, async (helper) => {86      // Fund accounts on Statemine(t)87      await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, alice.addressRaw, FUNDING_AMOUNT);88      await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, bob.addressRaw, FUNDING_AMOUNT);89    });9091    await usingStateminePlaygrounds(statemineUrl, async (helper) => {92      const sovereignFundingAmount = 3_500_000_000n;9394      await helper.assets.create(95        alice,96        USDT_ASSET_ID,97        alice.address,98        USDT_ASSET_METADATA_MINIMAL_BALANCE,99      );100      await helper.assets.setMetadata(101        alice,102        USDT_ASSET_ID,103        USDT_ASSET_METADATA_NAME,104        USDT_ASSET_METADATA_DESCRIPTION,105        USDT_ASSET_METADATA_DECIMALS,106      );107      await helper.assets.mint(108        alice,109        USDT_ASSET_ID,110        alice.address,111        USDT_ASSET_AMOUNT,112      );113114      // funding parachain sovereing account on Statemine(t).115      // The sovereign account should be created before any action116      // (the assets pallet on Statemine(t) check if the sovereign account exists)117      const parachainSovereingAccount = helper.address.paraSiblingSovereignAccount(QUARTZ_CHAIN);118      await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, sovereignFundingAmount);119    });120121122    await usingPlaygrounds(async (helper) => {123      const location = {124        V2: {125          parents: 1,126          interior: {X3: [127            {128              Parachain: STATEMINE_CHAIN,129            },130            {131              PalletInstance: STATEMINE_PALLET_INSTANCE,132            },133            {134              GeneralIndex: USDT_ASSET_ID,135            },136          ]},137        },138      };139140      const metadata =141      {142        name: USDT_ASSET_ID,143        symbol: USDT_ASSET_METADATA_NAME,144        decimals: USDT_ASSET_METADATA_DECIMALS,145        minimalBalance: USDT_ASSET_METADATA_MINIMAL_BALANCE,146      };147      await helper.getSudo().foreignAssets.register(alice, alice.address, location, metadata);148      balanceQuartzBefore = await helper.balance.getSubstrate(alice.address);149    });150151152    // Providing the relay currency to the quartz sender account153    // (fee for USDT XCM are paid in relay tokens)154    await usingRelayPlaygrounds(relayUrl, async (helper) => {155      const destination = {156        V2: {157          parents: 0,158          interior: {X1: {159            Parachain: QUARTZ_CHAIN,160          },161          },162        }};163164      const beneficiary = {165        V2: {166          parents: 0,167          interior: {X1: {168            AccountId32: {169              network: 'Any',170              id: alice.addressRaw,171            },172          }},173        },174      };175176      const assets = {177        V2: [178          {179            id: {180              Concrete: {181                parents: 0,182                interior: 'Here',183              },184            },185            fun: {186              Fungible: TRANSFER_AMOUNT_RELAY,187            },188          },189        ],190      };191192      const feeAssetItem = 0;193194      await helper.xcm.limitedReserveTransferAssets(alice, destination, beneficiary, assets, feeAssetItem, 'Unlimited');195    });196197  });198199  itSub('Should connect and send USDT from Statemine to Quartz', async ({helper}) => {200    await usingStateminePlaygrounds(statemineUrl, async (helper) => {201      const dest = {202        V2: {203          parents: 1,204          interior: {X1: {205            Parachain: QUARTZ_CHAIN,206          },207          },208        }};209210      const beneficiary = {211        V2: {212          parents: 0,213          interior: {X1: {214            AccountId32: {215              network: 'Any',216              id: alice.addressRaw,217            },218          }},219        },220      };221222      const assets = {223        V2: [224          {225            id: {226              Concrete: {227                parents: 0,228                interior: {229                  X2: [230                    {231                      PalletInstance: STATEMINE_PALLET_INSTANCE,232                    },233                    {234                      GeneralIndex: USDT_ASSET_ID,235                    },236                  ]},237              },238            },239            fun: {240              Fungible: TRANSFER_AMOUNT,241            },242          },243        ],244      };245246      const feeAssetItem = 0;247248      balanceStmnBefore = await helper.balance.getSubstrate(alice.address);249      await helper.xcm.limitedReserveTransferAssets(alice, dest, beneficiary, assets, feeAssetItem, 'Unlimited');250251      balanceStmnAfter = await helper.balance.getSubstrate(alice.address);252253      // common good parachain take commission in it native token254      console.log(255        '[Statemine -> Quartz] transaction fees on Statemine: %s WND',256        helper.util.bigIntToDecimals(balanceStmnBefore - balanceStmnAfter, STATEMINE_DECIMALS),257      );258      expect(balanceStmnBefore > balanceStmnAfter).to.be.true;259260    });261262263    // ensure that asset has been delivered264    await helper.wait.newBlocks(3);265266    // expext collection id will be with id 1267    const free = await helper.ft.getBalance(1, {Substrate: alice.address});268269    balanceQuartzAfter = await helper.balance.getSubstrate(alice.address);270271    console.log(272      '[Statemine -> Quartz] transaction fees on Quartz: %s USDT',273      helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, USDT_ASSET_METADATA_DECIMALS),274    );275    console.log(276      '[Statemine -> Quartz] transaction fees on Quartz: %s QTZ',277      helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzBefore),278    );279    // commission has not paid in USDT token280    expect(free).to.be.equal(TRANSFER_AMOUNT);281    // ... and parachain native token282    expect(balanceQuartzAfter == balanceQuartzBefore).to.be.true;283  });284285  itSub('Should connect and send USDT from Quartz to Statemine back', async ({helper}) => {286    const destination = {287      V2: {288        parents: 1,289        interior: {X2: [290          {291            Parachain: STATEMINE_CHAIN,292          },293          {294            AccountId32: {295              network: 'Any',296              id: alice.addressRaw,297            },298          },299        ]},300      },301    };302303    const relayFee = 400_000_000_000_000n;304    const currencies: [any, bigint][] = [305      [306        {307          ForeignAssetId: 0,308        },309        TRANSFER_AMOUNT,310      ],311      [312        {313          NativeAssetId: 'Parent',314        },315        relayFee,316      ],317    ];318319    const feeItem = 1;320321    await helper.xTokens.transferMulticurrencies(alice, currencies, feeItem, destination, 'Unlimited');322323    // the commission has been paid in parachain native token324    balanceQuartzFinal = await helper.balance.getSubstrate(alice.address);325    console.log('[Quartz -> Statemine] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzFinal));326    expect(balanceQuartzAfter > balanceQuartzFinal).to.be.true;327328    await usingStateminePlaygrounds(statemineUrl, async (helper) => {329      await helper.wait.newBlocks(3);330331      // The USDT token never paid fees. Its amount not changed from begin value.332      // Also check that xcm transfer has been succeeded333      expect((await helper.assets.account(USDT_ASSET_ID, alice.address))! == USDT_ASSET_AMOUNT).to.be.true;334    });335  });336337  itSub('Should connect and send Relay token to Quartz', async ({helper}) => {338    balanceBobBefore = await helper.balance.getSubstrate(bob.address);339    balanceBobRelayTokenBefore = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});340341    await usingRelayPlaygrounds(relayUrl, async (helper) => {342      const destination = {343        V2: {344          parents: 0,345          interior: {X1: {346            Parachain: QUARTZ_CHAIN,347          },348          },349        }};350351      const beneficiary = {352        V2: {353          parents: 0,354          interior: {X1: {355            AccountId32: {356              network: 'Any',357              id: bob.addressRaw,358            },359          }},360        },361      };362363      const assets = {364        V2: [365          {366            id: {367              Concrete: {368                parents: 0,369                interior: 'Here',370              },371            },372            fun: {373              Fungible: TRANSFER_AMOUNT_RELAY,374            },375          },376        ],377      };378379      const feeAssetItem = 0;380381      await helper.xcm.limitedReserveTransferAssets(bob, destination, beneficiary, assets, feeAssetItem, 'Unlimited');382    });383384    await helper.wait.newBlocks(3);385386    balanceBobAfter = await helper.balance.getSubstrate(bob.address);387    balanceBobRelayTokenAfter = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});388389    const wndFeeOnQuartz = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;390    const wndDiffOnQuartz = balanceBobRelayTokenAfter - balanceBobRelayTokenBefore;391    console.log(392      '[Relay (Westend) -> Quartz] transaction fees: %s QTZ',393      helper.util.bigIntToDecimals(balanceBobAfter - balanceBobBefore),394    );395    console.log(396      '[Relay (Westend) -> Quartz] transaction fees: %s WND',397      helper.util.bigIntToDecimals(wndFeeOnQuartz, STATEMINE_DECIMALS),398    );399    console.log('[Relay (Westend) -> Quartz] actually delivered: %s WND', wndDiffOnQuartz);400    expect(wndFeeOnQuartz == 0n, 'No incoming WND fees should be taken').to.be.true;401    expect(balanceBobBefore == balanceBobAfter, 'No incoming QTZ fees should be taken').to.be.true;402  });403404  itSub('Should connect and send Relay token back', async ({helper}) => {405    let relayTokenBalanceBefore: bigint;406    let relayTokenBalanceAfter: bigint;407    await usingRelayPlaygrounds(relayUrl, async (helper) => {408      relayTokenBalanceBefore = await helper.balance.getSubstrate(bob.address);409    });410411    const destination = {412      V2: {413        parents: 1,414        interior: {415          X1:{416            AccountId32: {417              network: 'Any',418              id: bob.addressRaw,419            },420          },421        },422      },423    };424425    const currencies: any = [426      [427        {428          NativeAssetId: 'Parent',429        },430        TRANSFER_AMOUNT_RELAY,431      ],432    ];433434    const feeItem = 0;435436    await helper.xTokens.transferMulticurrencies(bob, currencies, feeItem, destination, 'Unlimited');437438    balanceBobFinal = await helper.balance.getSubstrate(bob.address);439    console.log('[Quartz -> Relay (Westend)] transaction fees: %s QTZ',  helper.util.bigIntToDecimals(balanceBobAfter - balanceBobFinal));440441    await usingRelayPlaygrounds(relayUrl, async (helper) => {442      await helper.wait.newBlocks(10);443      relayTokenBalanceAfter = await helper.balance.getSubstrate(bob.address);444445      const diff = relayTokenBalanceAfter - relayTokenBalanceBefore;446      console.log('[Quartz -> Relay (Westend)] actually delivered: %s WND', helper.util.bigIntToDecimals(diff, RELAY_DECIMALS));447      expect(diff > 0, 'Relay tokens was not delivered back').to.be.true;448    });449  });450});451452describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => {453  let alice: IKeyringPair;454  let randomAccount: IKeyringPair;455456  let balanceQuartzTokenInit: bigint;457  let balanceQuartzTokenMiddle: bigint;458  let balanceQuartzTokenFinal: bigint;459  let balanceKaruraTokenInit: bigint;460  let balanceKaruraTokenMiddle: bigint;461  let balanceKaruraTokenFinal: bigint;462  let balanceQuartzForeignTokenInit: bigint;463  let balanceQuartzForeignTokenMiddle: bigint;464  let balanceQuartzForeignTokenFinal: bigint;465466  // computed by a test transfer from prod Quartz to prod Karura.467  // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9468  // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block)469  const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n;470  const karuraEps = 8n * 10n ** 16n;471472  let karuraBackwardTransferAmount: bigint;473474  before(async () => {475    await usingPlaygrounds(async (helper, privateKey) => {476      alice = await privateKey('//Alice');477      [randomAccount] = await helper.arrange.createAccounts([0n], alice);478479      // Set the default version to wrap the first message to other chains.480      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);481    });482483    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {484      const destination = {485        V2: {486          parents: 1,487          interior: {488            X1: {489              Parachain: QUARTZ_CHAIN,490            },491          },492        },493      };494495      const metadata = {496        name: 'Quartz',497        symbol: 'QTZ',498        decimals: 18,499        minimalBalance: 1000000000000000000n,500      };501502      await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);503      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);504      balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);505      balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});506    });507508    await usingPlaygrounds(async (helper) => {509      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);510      balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address);511    });512  });513514  itSub('Should connect and send QTZ to Karura', async ({helper}) => {515    const destination = {516      V2: {517        parents: 1,518        interior: {519          X1: {520            Parachain: KARURA_CHAIN,521          },522        },523      },524    };525526    const beneficiary = {527      V2: {528        parents: 0,529        interior: {530          X1: {531            AccountId32: {532              network: 'Any',533              id: randomAccount.addressRaw,534            },535          },536        },537      },538    };539540    const assets = {541      V2: [542        {543          id: {544            Concrete: {545              parents: 0,546              interior: 'Here',547            },548          },549          fun: {550            Fungible: TRANSFER_AMOUNT,551          },552        },553      ],554    };555556    const feeAssetItem = 0;557558    await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');559    balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);560561    const qtzFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;562    expect(qtzFees > 0n, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;563    console.log('[Quartz -> Karura] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));564565    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {566      await helper.wait.newBlocks(3);567568      balanceQuartzForeignTokenMiddle = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});569      balanceKaruraTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);570571      const karFees = balanceKaruraTokenInit - balanceKaruraTokenMiddle;572      const qtzIncomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenInit;573      karuraBackwardTransferAmount = qtzIncomeTransfer;574575      const karUnqFees = TRANSFER_AMOUNT - qtzIncomeTransfer;576577      console.log(578        '[Quartz -> Karura] transaction fees on Karura: %s KAR',579        helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),580      );581      console.log(582        '[Quartz -> Karura] transaction fees on Karura: %s QTZ',583        helper.util.bigIntToDecimals(karUnqFees),584      );585      console.log('[Quartz -> Karura] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));586      expect(karFees == 0n).to.be.true;587588      const bigintAbs = (n: bigint) => (n < 0n) ? -n : n;589590      expect(591        bigintAbs(karUnqFees - expectedKaruraIncomeFee) < karuraEps,592        'Karura took different income fee, check the Karura foreign asset config',593      ).to.be.true;594    });595  });596597  itSub('Should connect to Karura and send QTZ back', async ({helper}) => {598    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {599      const destination = {600        V2: {601          parents: 1,602          interior: {603            X2: [604              {Parachain: QUARTZ_CHAIN},605              {606                AccountId32: {607                  network: 'Any',608                  id: randomAccount.addressRaw,609                },610              },611            ],612          },613        },614      };615616      const id = {617        ForeignAsset: 0,618      };619620      await helper.xTokens.transfer(randomAccount, id, karuraBackwardTransferAmount, destination, 'Unlimited');621      balanceKaruraTokenFinal = await helper.balance.getSubstrate(randomAccount.address);622      balanceQuartzForeignTokenFinal = await helper.tokens.accounts(randomAccount.address, id);623624      const karFees = balanceKaruraTokenMiddle - balanceKaruraTokenFinal;625      const qtzOutcomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenFinal;626627      console.log(628        '[Karura -> Quartz] transaction fees on Karura: %s KAR',629        helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),630      );631      console.log('[Karura -> Quartz] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));632633      expect(karFees > 0, 'Negative fees KAR, looks like nothing was transferred').to.be.true;634      expect(qtzOutcomeTransfer == karuraBackwardTransferAmount).to.be.true;635    });636637    await helper.wait.newBlocks(3);638639    balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccount.address);640    const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;641    expect(actuallyDelivered > 0).to.be.true;642643    console.log('[Karura -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));644645    const qtzFees = karuraBackwardTransferAmount - actuallyDelivered;646    console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));647    expect(qtzFees == 0n).to.be.true;648  });649650  itSub('Karura can send only up to its balance', async ({helper}) => {651    // set Karura's sovereign account's balance652    const karuraBalance = 10000n * (10n ** QTZ_DECIMALS);653    const karuraSovereignAccount = helper.address.paraSiblingSovereignAccount(KARURA_CHAIN);654    await helper.getSudo().balance.setBalanceSubstrate(alice, karuraSovereignAccount, karuraBalance);655656    const moreThanKaruraHas = karuraBalance * 2n;657658    let targetAccountBalance = 0n;659    const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);660661    const quartzMultilocation = {662      V2: {663        parents: 1,664        interior: {665          X1: {Parachain: QUARTZ_CHAIN},666        },667      },668    };669670    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(671      targetAccount.addressRaw,672      {673        Concrete: {674          parents: 0,675          interior: 'Here',676        },677      },678      moreThanKaruraHas,679    );680681    let maliciousXcmProgramSent: any;682    const maxWaitBlocks = 5;683684    // Try to trick Quartz685    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {686      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);687688      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);689    });690691    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash692        && event.outcome.isFailedToTransactAsset);693694    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);695    expect(targetAccountBalance).to.be.equal(0n);696697    // But Karura still can send the correct amount698    const validTransferAmount = karuraBalance / 2n;699    const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(700      targetAccount.addressRaw,701      {702        Concrete: {703          parents: 0,704          interior: 'Here',705        },706      },707      validTransferAmount,708    );709710    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {711      await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);712    });713714    await helper.wait.newBlocks(maxWaitBlocks);715716    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);717    expect(targetAccountBalance).to.be.equal(validTransferAmount);718  });719720  itSub('Should not accept reserve transfer of QTZ from Karura', async ({helper}) => {721    const testAmount = 10_000n * (10n ** QTZ_DECIMALS);722    const [targetAccount] = await helper.arrange.createAccounts([0n], alice);723724    const quartzMultilocation = {725      V2: {726        parents: 1,727        interior: {728          X1: {729            Parachain: QUARTZ_CHAIN,730          },731        },732      },733    };734735    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(736      targetAccount.addressRaw,737      {738        Concrete: {739          parents: 1,740          interior: {741            X1: {742              Parachain: QUARTZ_CHAIN,743            },744          },745        },746      },747      testAmount,748    );749750    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(751      targetAccount.addressRaw,752      {753        Concrete: {754          parents: 0,755          interior: 'Here',756        },757      },758      testAmount,759    );760761    let maliciousXcmProgramFullIdSent: any;762    let maliciousXcmProgramHereIdSent: any;763    const maxWaitBlocks = 3;764765    // Try to trick Quartz using full QTZ identification766    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {767      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);768769      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);770    });771772    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash773        && event.outcome.isUntrustedReserveLocation);774775    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);776    expect(accountBalance).to.be.equal(0n);777778    // Try to trick Quartz using shortened QTZ identification779    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {780      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);781782      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);783    });784785    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash786        && event.outcome.isUntrustedReserveLocation);787788    accountBalance = await helper.balance.getSubstrate(targetAccount.address);789    expect(accountBalance).to.be.equal(0n);790  });791});792793// These tests are relevant only when794// the the corresponding foreign assets are not registered795describeXCM('[XCM] Integration test: Quartz rejects non-native tokens', () => {796  let alice: IKeyringPair;797  let alith: IKeyringPair;798799  const testAmount = 100_000_000_000n;800  let quartzParachainJunction;801  let quartzAccountJunction;802803  let quartzParachainMultilocation: any;804  let quartzAccountMultilocation: any;805  let quartzCombinedMultilocation: any;806807  let messageSent: any;808809  const maxWaitBlocks = 3;810811  before(async () => {812    await usingPlaygrounds(async (helper, privateKey) => {813      alice = await privateKey('//Alice');814815      quartzParachainJunction = {Parachain: QUARTZ_CHAIN};816      quartzAccountJunction = {817        AccountId32: {818          network: 'Any',819          id: alice.addressRaw,820        },821      };822823      quartzParachainMultilocation = {824        V2: {825          parents: 1,826          interior: {827            X1: quartzParachainJunction,828          },829        },830      };831832      quartzAccountMultilocation = {833        V2: {834          parents: 0,835          interior: {836            X1: quartzAccountJunction,837          },838        },839      };840841      quartzCombinedMultilocation = {842        V2: {843          parents: 1,844          interior: {845            X2: [quartzParachainJunction, quartzAccountJunction],846          },847        },848      };849850      // Set the default version to wrap the first message to other chains.851      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);852    });853854    // eslint-disable-next-line require-await855    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {856      alith = helper.account.alithAccount();857    });858  });859860  const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {861    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash862        && event.outcome.isFailedToTransactAsset);863  };864865  itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => {866    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {867      const id = {868        Token: 'KAR',869      };870      const destination = quartzCombinedMultilocation;871      await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');872873      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);874    });875876    await expectFailedToTransact(helper, messageSent);877  });878879  itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => {880    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {881      const id = 'SelfReserve';882      const destination = quartzCombinedMultilocation;883      await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');884885      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);886    });887888    await expectFailedToTransact(helper, messageSent);889  });890891  itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => {892    await usingShidenPlaygrounds(shidenUrl, async (helper) => {893      const destinationParachain = quartzParachainMultilocation;894      const beneficiary = quartzAccountMultilocation;895      const assets = {896        V2: [{897          id: {898            Concrete: {899              parents: 0,900              interior: 'Here',901            },902          },903          fun: {904            Fungible: testAmount,905          },906        }],907      };908      const feeAssetItem = 0;909910      await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [911        destinationParachain,912        beneficiary,913        assets,914        feeAssetItem,915      ]);916917      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);918    });919920    await expectFailedToTransact(helper, messageSent);921  });922});923924describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => {925  // Quartz constants926  let alice: IKeyringPair;927  let quartzAssetLocation;928929  let randomAccountQuartz: IKeyringPair;930  let randomAccountMoonriver: IKeyringPair;931932  // Moonriver constants933  let assetId: string;934935  const quartzAssetMetadata = {936    name: 'xcQuartz',937    symbol: 'xcQTZ',938    decimals: 18,939    isFrozen: false,940    minimalBalance: 1n,941  };942943  let balanceQuartzTokenInit: bigint;944  let balanceQuartzTokenMiddle: bigint;945  let balanceQuartzTokenFinal: bigint;946  let balanceForeignQtzTokenInit: bigint;947  let balanceForeignQtzTokenMiddle: bigint;948  let balanceForeignQtzTokenFinal: bigint;949  let balanceMovrTokenInit: bigint;950  let balanceMovrTokenMiddle: bigint;951  let balanceMovrTokenFinal: bigint;952953  before(async () => {954    await usingPlaygrounds(async (helper, privateKey) => {955      alice = await privateKey('//Alice');956      [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);957958      balanceForeignQtzTokenInit = 0n;959960      // Set the default version to wrap the first message to other chains.961      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);962    });963964    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {965      const alithAccount = helper.account.alithAccount();966      const baltatharAccount = helper.account.baltatharAccount();967      const dorothyAccount = helper.account.dorothyAccount();968969      randomAccountMoonriver = helper.account.create();970971      // >>> Sponsoring Dorothy >>>972      console.log('Sponsoring Dorothy.......');973      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);974      console.log('Sponsoring Dorothy.......DONE');975      // <<< Sponsoring Dorothy <<<976977      quartzAssetLocation = {978        XCM: {979          parents: 1,980          interior: {X1: {Parachain: QUARTZ_CHAIN}},981        },982      };983      const existentialDeposit = 1n;984      const isSufficient = true;985      const unitsPerSecond = 1n;986      const numAssetsWeightHint = 0;987988      const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({989        location: quartzAssetLocation,990        metadata: quartzAssetMetadata,991        existentialDeposit,992        isSufficient,993        unitsPerSecond,994        numAssetsWeightHint,995      });996997      console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);998999      await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);10001001      // >>> Acquire Quartz AssetId Info on Moonriver >>>1002      console.log('Acquire Quartz AssetId Info on Moonriver.......');10031004      assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();10051006      console.log('QTZ asset ID is %s', assetId);1007      console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');1008      // >>> Acquire Quartz AssetId Info on Moonriver >>>10091010      // >>> Sponsoring random Account >>>1011      console.log('Sponsoring random Account.......');1012      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);1013      console.log('Sponsoring random Account.......DONE');1014      // <<< Sponsoring random Account <<<10151016      balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address);1017    });10181019    await usingPlaygrounds(async (helper) => {1020      await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);1021      balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address);1022    });1023  });10241025  itSub('Should connect and send QTZ to Moonriver', async ({helper}) => {1026    const currencyId = {1027      NativeAssetId: 'Here',1028    };1029    const dest = {1030      V2: {1031        parents: 1,1032        interior: {1033          X2: [1034            {Parachain: MOONRIVER_CHAIN},1035            {AccountKey20: {network: 'Any', key: randomAccountMoonriver.address}},1036          ],1037        },1038      },1039    };1040    const amount = TRANSFER_AMOUNT;10411042    await helper.xTokens.transfer(randomAccountQuartz, currencyId, amount, dest, 'Unlimited');10431044    balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccountQuartz.address);1045    expect(balanceQuartzTokenMiddle < balanceQuartzTokenInit).to.be.true;10461047    const transactionFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;1048    console.log('[Quartz -> Moonriver] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(transactionFees));1049    expect(transactionFees > 0, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;10501051    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1052      await helper.wait.newBlocks(3);10531054      balanceMovrTokenMiddle = await helper.balance.getEthereum(randomAccountMoonriver.address);10551056      const movrFees = balanceMovrTokenInit - balanceMovrTokenMiddle;1057      console.log('[Quartz -> Moonriver] transaction fees on Moonriver: %s MOVR',helper.util.bigIntToDecimals(movrFees));1058      expect(movrFees == 0n).to.be.true;10591060      balanceForeignQtzTokenMiddle = (await helper.assets.account(assetId, randomAccountMoonriver.address))!; // BigInt(qtzRandomAccountAsset['balance']);1061      const qtzIncomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenInit;1062      console.log('[Quartz -> Moonriver] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));1063      expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true;1064    });1065  });10661067  itSub('Should connect to Moonriver and send QTZ back', async ({helper}) => {1068    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1069      const asset = {1070        V2: {1071          id: {1072            Concrete: {1073              parents: 1,1074              interior: {1075                X1: {Parachain: QUARTZ_CHAIN},1076              },1077            },1078          },1079          fun: {1080            Fungible: TRANSFER_AMOUNT,1081          },1082        },1083      };1084      const destination = {1085        V2: {1086          parents: 1,1087          interior: {1088            X2: [1089              {Parachain: QUARTZ_CHAIN},1090              {AccountId32: {network: 'Any', id: randomAccountQuartz.addressRaw}},1091            ],1092          },1093        },1094      };10951096      await helper.xTokens.transferMultiasset(randomAccountMoonriver, asset, destination, 'Unlimited');10971098      balanceMovrTokenFinal = await helper.balance.getEthereum(randomAccountMoonriver.address);10991100      const movrFees = balanceMovrTokenMiddle - balanceMovrTokenFinal;1101      console.log('[Moonriver -> Quartz] transaction fees on Moonriver: %s MOVR', helper.util.bigIntToDecimals(movrFees));1102      expect(movrFees > 0, 'Negative fees MOVR, looks like nothing was transferred').to.be.true;11031104      const qtzRandomAccountAsset = await helper.assets.account(assetId, randomAccountMoonriver.address);11051106      expect(qtzRandomAccountAsset).to.be.null;11071108      balanceForeignQtzTokenFinal = 0n;11091110      const qtzOutcomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenFinal;1111      console.log('[Quartz -> Moonriver] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));1112      expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;1113    });11141115    await helper.wait.newBlocks(3);11161117    balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccountQuartz.address);1118    const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;1119    expect(actuallyDelivered > 0).to.be.true;11201121    console.log('[Moonriver -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));11221123    const qtzFees = TRANSFER_AMOUNT - actuallyDelivered;1124    console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));1125    expect(qtzFees == 0n).to.be.true;1126  });11271128  itSub('Moonriver can send only up to its balance', async ({helper}) => {1129    // set Moonriver's sovereign account's balance1130    const moonriverBalance = 10000n * (10n ** QTZ_DECIMALS);1131    const moonriverSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONRIVER_CHAIN);1132    await helper.getSudo().balance.setBalanceSubstrate(alice, moonriverSovereignAccount, moonriverBalance);11331134    const moreThanMoonriverHas = moonriverBalance * 2n;11351136    let targetAccountBalance = 0n;1137    const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);11381139    const quartzMultilocation = {1140      V2: {1141        parents: 1,1142        interior: {1143          X1: {Parachain: QUARTZ_CHAIN},1144        },1145      },1146    };11471148    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1149      targetAccount.addressRaw,1150      {1151        Concrete: {1152          parents: 0,1153          interior: 'Here',1154        },1155      },1156      moreThanMoonriverHas,1157    );11581159    let maliciousXcmProgramSent: any;1160    const maxWaitBlocks = 3;11611162    // Try to trick Quartz1163    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1164      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]);11651166      // Needed to bypass the call filter.1167      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1168      await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall);11691170      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1171    });11721173    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1174        && event.outcome.isFailedToTransactAsset);11751176    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1177    expect(targetAccountBalance).to.be.equal(0n);11781179    // But Moonriver still can send the correct amount1180    const validTransferAmount = moonriverBalance / 2n;1181    const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1182      targetAccount.addressRaw,1183      {1184        Concrete: {1185          parents: 0,1186          interior: 'Here',1187        },1188      },1189      validTransferAmount,1190    );11911192    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1193      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, validXcmProgram]);11941195      // Needed to bypass the call filter.1196      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1197      await helper.fastDemocracy.executeProposal('Spend the correct amount of QTZ', batchCall);1198    });11991200    await helper.wait.newBlocks(maxWaitBlocks);12011202    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1203    expect(targetAccountBalance).to.be.equal(validTransferAmount);1204  });12051206  itSub('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => {1207    const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1208    const [targetAccount] = await helper.arrange.createAccounts([0n], alice);12091210    const quartzMultilocation = {1211      V2: {1212        parents: 1,1213        interior: {1214          X1: {1215            Parachain: QUARTZ_CHAIN,1216          },1217        },1218      },1219    };12201221    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1222      targetAccount.addressRaw,1223      {1224        Concrete: {1225          parents: 0,1226          interior: {1227            X1: {1228              Parachain: QUARTZ_CHAIN,1229            },1230          },1231        },1232      },1233      testAmount,1234    );12351236    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1237      targetAccount.addressRaw,1238      {1239        Concrete: {1240          parents: 0,1241          interior: 'Here',1242        },1243      },1244      testAmount,1245    );12461247    let maliciousXcmProgramFullIdSent: any;1248    let maliciousXcmProgramHereIdSent: any;1249    const maxWaitBlocks = 3;12501251    // Try to trick Quartz using full QTZ identification1252    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1253      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]);12541255      // Needed to bypass the call filter.1256      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1257      await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall);12581259      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1260    });12611262    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1263        && event.outcome.isUntrustedReserveLocation);12641265    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1266    expect(accountBalance).to.be.equal(0n);12671268    // Try to trick Quartz using shortened QTZ identification1269    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1270      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramHereId]);12711272      // Needed to bypass the call filter.1273      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1274      await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall);12751276      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1277    });12781279    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1280        && event.outcome.isUntrustedReserveLocation);12811282    accountBalance = await helper.balance.getSubstrate(targetAccount.address);1283    expect(accountBalance).to.be.equal(0n);1284  });1285});12861287describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => {1288  let alice: IKeyringPair;1289  let sender: IKeyringPair;12901291  const QTZ_ASSET_ID_ON_SHIDEN = 1;1292  const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n;12931294  // Quartz -> Shiden1295  const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden1296  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?1297  const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ1298  const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens12991300  // Shiden -> Quartz1301  const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ1302  const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 4.999_999_999_088_000_000n QTZ13031304  let balanceAfterQuartzToShidenXCM: bigint;13051306  before(async () => {1307    await usingPlaygrounds(async (helper, privateKey) => {1308      alice = await privateKey('//Alice');1309      [sender] = await helper.arrange.createAccounts([100n], alice);1310      console.log('sender', sender.address);13111312      // Set the default version to wrap the first message to other chains.1313      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);1314    });13151316    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1317      console.log('1. Create foreign asset and metadata');1318      // TODO update metadata with values from production1319      await helper.assets.create(1320        alice,1321        QTZ_ASSET_ID_ON_SHIDEN,1322        alice.address,1323        QTZ_MINIMAL_BALANCE_ON_SHIDEN,1324      );13251326      await helper.assets.setMetadata(1327        alice,1328        QTZ_ASSET_ID_ON_SHIDEN,1329        'Cross chain QTZ',1330        'xcQTZ',1331        Number(QTZ_DECIMALS),1332      );13331334      console.log('2. Register asset location on Shiden');1335      const assetLocation = {1336        V2: {1337          parents: 1,1338          interior: {1339            X1: {1340              Parachain: QUARTZ_CHAIN,1341            },1342          },1343        },1344      };13451346      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);13471348      console.log('3. Set QTZ payment for XCM execution on Shiden');1349      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);13501351      console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');1352      await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);1353    });1354  });13551356  itSub('Should connect and send QTZ to Shiden', async ({helper}) => {1357    const destination = {1358      V2: {1359        parents: 1,1360        interior: {1361          X1: {1362            Parachain: SHIDEN_CHAIN,1363          },1364        },1365      },1366    };13671368    const beneficiary = {1369      V2: {1370        parents: 0,1371        interior: {1372          X1: {1373            AccountId32: {1374              network: 'Any',1375              id: sender.addressRaw,1376            },1377          },1378        },1379      },1380    };13811382    const assets = {1383      V2: [1384        {1385          id: {1386            Concrete: {1387              parents: 0,1388              interior: 'Here',1389            },1390          },1391          fun: {1392            Fungible: qtzToShidenTransferred,1393          },1394        },1395      ],1396    };13971398    // Initial balance is 100 QTZ1399    const balanceBefore = await helper.balance.getSubstrate(sender.address);1400    console.log(`Initial balance is: ${balanceBefore}`);14011402    const feeAssetItem = 0;1403    await helper.xcm.limitedReserveTransferAssets(sender, destination, beneficiary, assets, feeAssetItem, 'Unlimited');14041405    // Balance after reserve transfer is less than 901406    balanceAfterQuartzToShidenXCM = await helper.balance.getSubstrate(sender.address);1407    console.log(`QTZ Balance on Quartz after XCM is: ${balanceAfterQuartzToShidenXCM}`);1408    console.log(`Quartz's QTZ commission is: ${balanceBefore - balanceAfterQuartzToShidenXCM}`);1409    expect(balanceBefore - balanceAfterQuartzToShidenXCM > 0).to.be.true;14101411    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1412      await helper.wait.newBlocks(3);1413      const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1414      const shidenBalance = await helper.balance.getSubstrate(sender.address);14151416      console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);1417      console.log(`Shiden's QTZ commission is: ${qtzToShidenTransferred - xcQTZbalance!}`);14181419      expect(xcQTZbalance).to.eq(qtzToShidenArrived);1420      // SHD balance does not changed:1421      expect(shidenBalance).to.eq(shidenInitialBalance);1422    });1423  });14241425  itSub('Should connect to Shiden and send QTZ back', async ({helper}) => {1426    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1427      const destination = {1428        V2: {1429          parents: 1,1430          interior: {1431            X1: {1432              Parachain: QUARTZ_CHAIN,1433            },1434          },1435        },1436      };14371438      const beneficiary = {1439        V2: {1440          parents: 0,1441          interior: {1442            X1: {1443              AccountId32: {1444                network: 'Any',1445                id: sender.addressRaw,1446              },1447            },1448          },1449        },1450      };14511452      const assets = {1453        V2: [1454          {1455            id: {1456              Concrete: {1457                parents: 1,1458                interior: {1459                  X1: {1460                    Parachain: QUARTZ_CHAIN,1461                  },1462                },1463              },1464            },1465            fun: {1466              Fungible: qtzFromShidenTransfered,1467            },1468          },1469        ],1470      };14711472      // Initial balance is 1 SDN1473      const balanceSDNbefore = await helper.balance.getSubstrate(sender.address);1474      console.log(`SDN balance is: ${balanceSDNbefore}, it does not changed`);1475      expect(balanceSDNbefore).to.eq(shidenInitialBalance);14761477      const feeAssetItem = 0;1478      // this is non-standard polkadotXcm extension for Astar only. It calls InitiateReserveWithdraw1479      await helper.executeExtrinsic(sender, 'api.tx.polkadotXcm.reserveWithdrawAssets', [destination, beneficiary, assets, feeAssetItem]);14801481      // Balance after reserve transfer is less than 1 SDN1482      const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1483      const balanceSDN = await helper.balance.getSubstrate(sender.address);1484      console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);14851486      // Assert: xcQTZ balance correctly decreased1487      expect(xcQTZbalance).to.eq(qtzOnShidenLeft);1488      // Assert: SDN balance is 0.996...1489      expect(balanceSDN / (10n ** (SHIDEN_DECIMALS - 3n))).to.eq(996n);1490    });14911492    await helper.wait.newBlocks(3);1493    const balanceQTZ = await helper.balance.getSubstrate(sender.address);1494    console.log(`QTZ Balance on Quartz after XCM is: ${balanceQTZ}`);1495    expect(balanceQTZ).to.eq(balanceAfterQuartzToShidenXCM + qtzFromShidenTransfered);1496  });14971498  itSub('Shiden can send only up to its balance', async ({helper}) => {1499    // set Shiden's sovereign account's balance1500    const shidenBalance = 10000n * (10n ** QTZ_DECIMALS);1501    const shidenSovereignAccount = helper.address.paraSiblingSovereignAccount(SHIDEN_CHAIN);1502    await helper.getSudo().balance.setBalanceSubstrate(alice, shidenSovereignAccount, shidenBalance);15031504    const moreThanShidenHas = shidenBalance * 2n;15051506    let targetAccountBalance = 0n;1507    const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);15081509    const quartzMultilocation = {1510      V2: {1511        parents: 1,1512        interior: {1513          X1: {Parachain: QUARTZ_CHAIN},1514        },1515      },1516    };15171518    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1519      targetAccount.addressRaw,1520      {1521        Concrete: {1522          parents: 0,1523          interior: 'Here',1524        },1525      },1526      moreThanShidenHas,1527    );15281529    let maliciousXcmProgramSent: any;1530    const maxWaitBlocks = 3;15311532    // Try to trick Quartz1533    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1534      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);15351536      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1537    });15381539    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1540        && event.outcome.isFailedToTransactAsset);15411542    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1543    expect(targetAccountBalance).to.be.equal(0n);15441545    // But Shiden still can send the correct amount1546    const validTransferAmount = shidenBalance / 2n;1547    const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1548      targetAccount.addressRaw,1549      {1550        Concrete: {1551          parents: 0,1552          interior: 'Here',1553        },1554      },1555      validTransferAmount,1556    );15571558    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1559      await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);1560    });15611562    await helper.wait.newBlocks(maxWaitBlocks);15631564    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1565    expect(targetAccountBalance).to.be.equal(validTransferAmount);1566  });15671568  itSub('Should not accept reserve transfer of QTZ from Shiden', async ({helper}) => {1569    const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1570    const [targetAccount] = await helper.arrange.createAccounts([0n], alice);15711572    const quartzMultilocation = {1573      V2: {1574        parents: 1,1575        interior: {1576          X1: {1577            Parachain: QUARTZ_CHAIN,1578          },1579        },1580      },1581    };15821583    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1584      targetAccount.addressRaw,1585      {1586        Concrete: {1587          parents: 1,1588          interior: {1589            X1: {1590              Parachain: QUARTZ_CHAIN,1591            },1592          },1593        },1594      },1595      testAmount,1596    );15971598    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1599      targetAccount.addressRaw,1600      {1601        Concrete: {1602          parents: 0,1603          interior: 'Here',1604        },1605      },1606      testAmount,1607    );16081609    let maliciousXcmProgramFullIdSent: any;1610    let maliciousXcmProgramHereIdSent: any;1611    const maxWaitBlocks = 3;16121613    // Try to trick Quartz using full QTZ identification1614    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1615      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);16161617      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1618    });16191620    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1621        && event.outcome.isUntrustedReserveLocation);16221623    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1624    expect(accountBalance).to.be.equal(0n);16251626    // Try to trick Quartz using shortened QTZ identification1627    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1628      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);16291630      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1631    });16321633    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1634        && event.outcome.isUntrustedReserveLocation);16351636    accountBalance = await helper.balance.getSubstrate(targetAccount.address);1637    expect(accountBalance).to.be.equal(0n);1638  });1639});
after · tests/src/xcm/xcmQuartz.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util';19import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';20import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl} from './xcm.types';21222324const STATEMINE_PALLET_INSTANCE = 50;2526const TRANSFER_AMOUNT = 2000000000000000000000000n;2728const FUNDING_AMOUNT = 3_500_000_0000_000_000n;2930const TRANSFER_AMOUNT_RELAY = 50_000_000_000_000_000n;3132const USDT_ASSET_ID = 100;33const USDT_ASSET_METADATA_DECIMALS = 18;34const USDT_ASSET_METADATA_NAME = 'USDT';35const USDT_ASSET_METADATA_DESCRIPTION = 'USDT';36const USDT_ASSET_METADATA_MINIMAL_BALANCE = 1n;37const USDT_ASSET_AMOUNT = 10_000_000_000_000_000_000_000_000n;3839const SAFE_XCM_VERSION = 2;4041describeXCM('[XCM] Integration test: Exchanging USDT with Statemine', () => {42  let alice: IKeyringPair;43  let bob: IKeyringPair;4445  let balanceStmnBefore: bigint;46  let balanceStmnAfter: bigint;4748  let balanceQuartzBefore: bigint;49  let balanceQuartzAfter: bigint;50  let balanceQuartzFinal: bigint;5152  let balanceBobBefore: bigint;53  let balanceBobAfter: bigint;54  let balanceBobFinal: bigint;5556  let balanceBobRelayTokenBefore: bigint;57  let balanceBobRelayTokenAfter: bigint;585960  before(async () => {61    await usingPlaygrounds(async (helper, privateKey) => {62      alice = await privateKey('//Alice');63      bob = await privateKey('//Bob'); // sovereign account on Statemine(t) funds donor6465      // Set the default version to wrap the first message to other chains.66      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);67    });6869    await usingRelayPlaygrounds(relayUrl, async (helper) => {70      // Fund accounts on Statemine(t)71      await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, alice.addressRaw, FUNDING_AMOUNT);72      await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, bob.addressRaw, FUNDING_AMOUNT);73    });7475    await usingStateminePlaygrounds(statemineUrl, async (helper) => {76      const sovereignFundingAmount = 3_500_000_000n;7778      await helper.assets.create(79        alice,80        USDT_ASSET_ID,81        alice.address,82        USDT_ASSET_METADATA_MINIMAL_BALANCE,83      );84      await helper.assets.setMetadata(85        alice,86        USDT_ASSET_ID,87        USDT_ASSET_METADATA_NAME,88        USDT_ASSET_METADATA_DESCRIPTION,89        USDT_ASSET_METADATA_DECIMALS,90      );91      await helper.assets.mint(92        alice,93        USDT_ASSET_ID,94        alice.address,95        USDT_ASSET_AMOUNT,96      );9798      // funding parachain sovereing account on Statemine(t).99      // The sovereign account should be created before any action100      // (the assets pallet on Statemine(t) check if the sovereign account exists)101      const parachainSovereingAccount = helper.address.paraSiblingSovereignAccount(QUARTZ_CHAIN);102      await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, sovereignFundingAmount);103    });104105106    await usingPlaygrounds(async (helper) => {107      const location = {108        V2: {109          parents: 1,110          interior: {X3: [111            {112              Parachain: STATEMINE_CHAIN,113            },114            {115              PalletInstance: STATEMINE_PALLET_INSTANCE,116            },117            {118              GeneralIndex: USDT_ASSET_ID,119            },120          ]},121        },122      };123124      const metadata =125      {126        name: USDT_ASSET_ID,127        symbol: USDT_ASSET_METADATA_NAME,128        decimals: USDT_ASSET_METADATA_DECIMALS,129        minimalBalance: USDT_ASSET_METADATA_MINIMAL_BALANCE,130      };131      await helper.getSudo().foreignAssets.register(alice, alice.address, location, metadata);132      balanceQuartzBefore = await helper.balance.getSubstrate(alice.address);133    });134135136    // Providing the relay currency to the quartz sender account137    // (fee for USDT XCM are paid in relay tokens)138    await usingRelayPlaygrounds(relayUrl, async (helper) => {139      const destination = {140        V2: {141          parents: 0,142          interior: {X1: {143            Parachain: QUARTZ_CHAIN,144          },145          },146        }};147148      const beneficiary = {149        V2: {150          parents: 0,151          interior: {X1: {152            AccountId32: {153              network: 'Any',154              id: alice.addressRaw,155            },156          }},157        },158      };159160      const assets = {161        V2: [162          {163            id: {164              Concrete: {165                parents: 0,166                interior: 'Here',167              },168            },169            fun: {170              Fungible: TRANSFER_AMOUNT_RELAY,171            },172          },173        ],174      };175176      const feeAssetItem = 0;177178      await helper.xcm.limitedReserveTransferAssets(alice, destination, beneficiary, assets, feeAssetItem, 'Unlimited');179    });180181  });182183  itSub('Should connect and send USDT from Statemine to Quartz', async ({helper}) => {184    await usingStateminePlaygrounds(statemineUrl, async (helper) => {185      const dest = {186        V2: {187          parents: 1,188          interior: {X1: {189            Parachain: QUARTZ_CHAIN,190          },191          },192        }};193194      const beneficiary = {195        V2: {196          parents: 0,197          interior: {X1: {198            AccountId32: {199              network: 'Any',200              id: alice.addressRaw,201            },202          }},203        },204      };205206      const assets = {207        V2: [208          {209            id: {210              Concrete: {211                parents: 0,212                interior: {213                  X2: [214                    {215                      PalletInstance: STATEMINE_PALLET_INSTANCE,216                    },217                    {218                      GeneralIndex: USDT_ASSET_ID,219                    },220                  ]},221              },222            },223            fun: {224              Fungible: TRANSFER_AMOUNT,225            },226          },227        ],228      };229230      const feeAssetItem = 0;231232      balanceStmnBefore = await helper.balance.getSubstrate(alice.address);233      await helper.xcm.limitedReserveTransferAssets(alice, dest, beneficiary, assets, feeAssetItem, 'Unlimited');234235      balanceStmnAfter = await helper.balance.getSubstrate(alice.address);236237      // common good parachain take commission in it native token238      console.log(239        '[Statemine -> Quartz] transaction fees on Statemine: %s WND',240        helper.util.bigIntToDecimals(balanceStmnBefore - balanceStmnAfter, STATEMINE_DECIMALS),241      );242      expect(balanceStmnBefore > balanceStmnAfter).to.be.true;243244    });245246247    // ensure that asset has been delivered248    await helper.wait.newBlocks(3);249250    // expext collection id will be with id 1251    const free = await helper.ft.getBalance(1, {Substrate: alice.address});252253    balanceQuartzAfter = await helper.balance.getSubstrate(alice.address);254255    console.log(256      '[Statemine -> Quartz] transaction fees on Quartz: %s USDT',257      helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, USDT_ASSET_METADATA_DECIMALS),258    );259    console.log(260      '[Statemine -> Quartz] transaction fees on Quartz: %s QTZ',261      helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzBefore),262    );263    // commission has not paid in USDT token264    expect(free).to.be.equal(TRANSFER_AMOUNT);265    // ... and parachain native token266    expect(balanceQuartzAfter == balanceQuartzBefore).to.be.true;267  });268269  itSub('Should connect and send USDT from Quartz to Statemine back', async ({helper}) => {270    const destination = {271      V2: {272        parents: 1,273        interior: {X2: [274          {275            Parachain: STATEMINE_CHAIN,276          },277          {278            AccountId32: {279              network: 'Any',280              id: alice.addressRaw,281            },282          },283        ]},284      },285    };286287    const relayFee = 400_000_000_000_000n;288    const currencies: [any, bigint][] = [289      [290        {291          ForeignAssetId: 0,292        },293        TRANSFER_AMOUNT,294      ],295      [296        {297          NativeAssetId: 'Parent',298        },299        relayFee,300      ],301    ];302303    const feeItem = 1;304305    await helper.xTokens.transferMulticurrencies(alice, currencies, feeItem, destination, 'Unlimited');306307    // the commission has been paid in parachain native token308    balanceQuartzFinal = await helper.balance.getSubstrate(alice.address);309    console.log('[Quartz -> Statemine] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzFinal));310    expect(balanceQuartzAfter > balanceQuartzFinal).to.be.true;311312    await usingStateminePlaygrounds(statemineUrl, async (helper) => {313      await helper.wait.newBlocks(3);314315      // The USDT token never paid fees. Its amount not changed from begin value.316      // Also check that xcm transfer has been succeeded317      expect((await helper.assets.account(USDT_ASSET_ID, alice.address))! == USDT_ASSET_AMOUNT).to.be.true;318    });319  });320321  itSub('Should connect and send Relay token to Quartz', async ({helper}) => {322    balanceBobBefore = await helper.balance.getSubstrate(bob.address);323    balanceBobRelayTokenBefore = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});324325    await usingRelayPlaygrounds(relayUrl, async (helper) => {326      const destination = {327        V2: {328          parents: 0,329          interior: {X1: {330            Parachain: QUARTZ_CHAIN,331          },332          },333        }};334335      const beneficiary = {336        V2: {337          parents: 0,338          interior: {X1: {339            AccountId32: {340              network: 'Any',341              id: bob.addressRaw,342            },343          }},344        },345      };346347      const assets = {348        V2: [349          {350            id: {351              Concrete: {352                parents: 0,353                interior: 'Here',354              },355            },356            fun: {357              Fungible: TRANSFER_AMOUNT_RELAY,358            },359          },360        ],361      };362363      const feeAssetItem = 0;364365      await helper.xcm.limitedReserveTransferAssets(bob, destination, beneficiary, assets, feeAssetItem, 'Unlimited');366    });367368    await helper.wait.newBlocks(3);369370    balanceBobAfter = await helper.balance.getSubstrate(bob.address);371    balanceBobRelayTokenAfter = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});372373    const wndFeeOnQuartz = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;374    const wndDiffOnQuartz = balanceBobRelayTokenAfter - balanceBobRelayTokenBefore;375    console.log(376      '[Relay (Westend) -> Quartz] transaction fees: %s QTZ',377      helper.util.bigIntToDecimals(balanceBobAfter - balanceBobBefore),378    );379    console.log(380      '[Relay (Westend) -> Quartz] transaction fees: %s WND',381      helper.util.bigIntToDecimals(wndFeeOnQuartz, STATEMINE_DECIMALS),382    );383    console.log('[Relay (Westend) -> Quartz] actually delivered: %s WND', wndDiffOnQuartz);384    expect(wndFeeOnQuartz == 0n, 'No incoming WND fees should be taken').to.be.true;385    expect(balanceBobBefore == balanceBobAfter, 'No incoming QTZ fees should be taken').to.be.true;386  });387388  itSub('Should connect and send Relay token back', async ({helper}) => {389    let relayTokenBalanceBefore: bigint;390    let relayTokenBalanceAfter: bigint;391    await usingRelayPlaygrounds(relayUrl, async (helper) => {392      relayTokenBalanceBefore = await helper.balance.getSubstrate(bob.address);393    });394395    const destination = {396      V2: {397        parents: 1,398        interior: {399          X1:{400            AccountId32: {401              network: 'Any',402              id: bob.addressRaw,403            },404          },405        },406      },407    };408409    const currencies: any = [410      [411        {412          NativeAssetId: 'Parent',413        },414        TRANSFER_AMOUNT_RELAY,415      ],416    ];417418    const feeItem = 0;419420    await helper.xTokens.transferMulticurrencies(bob, currencies, feeItem, destination, 'Unlimited');421422    balanceBobFinal = await helper.balance.getSubstrate(bob.address);423    console.log('[Quartz -> Relay (Westend)] transaction fees: %s QTZ',  helper.util.bigIntToDecimals(balanceBobAfter - balanceBobFinal));424425    await usingRelayPlaygrounds(relayUrl, async (helper) => {426      await helper.wait.newBlocks(10);427      relayTokenBalanceAfter = await helper.balance.getSubstrate(bob.address);428429      const diff = relayTokenBalanceAfter - relayTokenBalanceBefore;430      console.log('[Quartz -> Relay (Westend)] actually delivered: %s WND', helper.util.bigIntToDecimals(diff, RELAY_DECIMALS));431      expect(diff > 0, 'Relay tokens was not delivered back').to.be.true;432    });433  });434});435436describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => {437  let alice: IKeyringPair;438  let randomAccount: IKeyringPair;439440  let balanceQuartzTokenInit: bigint;441  let balanceQuartzTokenMiddle: bigint;442  let balanceQuartzTokenFinal: bigint;443  let balanceKaruraTokenInit: bigint;444  let balanceKaruraTokenMiddle: bigint;445  let balanceKaruraTokenFinal: bigint;446  let balanceQuartzForeignTokenInit: bigint;447  let balanceQuartzForeignTokenMiddle: bigint;448  let balanceQuartzForeignTokenFinal: bigint;449450  // computed by a test transfer from prod Quartz to prod Karura.451  // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9452  // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block)453  const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n;454  const karuraEps = 8n * 10n ** 16n;455456  let karuraBackwardTransferAmount: bigint;457458  before(async () => {459    await usingPlaygrounds(async (helper, privateKey) => {460      alice = await privateKey('//Alice');461      [randomAccount] = await helper.arrange.createAccounts([0n], alice);462463      // Set the default version to wrap the first message to other chains.464      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);465    });466467    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {468      const destination = {469        V2: {470          parents: 1,471          interior: {472            X1: {473              Parachain: QUARTZ_CHAIN,474            },475          },476        },477      };478479      const metadata = {480        name: 'Quartz',481        symbol: 'QTZ',482        decimals: 18,483        minimalBalance: 1000000000000000000n,484      };485486      await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);487      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);488      balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);489      balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});490    });491492    await usingPlaygrounds(async (helper) => {493      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);494      balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address);495    });496  });497498  itSub('Should connect and send QTZ to Karura', async ({helper}) => {499    const destination = {500      V2: {501        parents: 1,502        interior: {503          X1: {504            Parachain: KARURA_CHAIN,505          },506        },507      },508    };509510    const beneficiary = {511      V2: {512        parents: 0,513        interior: {514          X1: {515            AccountId32: {516              network: 'Any',517              id: randomAccount.addressRaw,518            },519          },520        },521      },522    };523524    const assets = {525      V2: [526        {527          id: {528            Concrete: {529              parents: 0,530              interior: 'Here',531            },532          },533          fun: {534            Fungible: TRANSFER_AMOUNT,535          },536        },537      ],538    };539540    const feeAssetItem = 0;541542    await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');543    balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);544545    const qtzFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;546    expect(qtzFees > 0n, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;547    console.log('[Quartz -> Karura] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));548549    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {550      await helper.wait.newBlocks(3);551552      balanceQuartzForeignTokenMiddle = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});553      balanceKaruraTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);554555      const karFees = balanceKaruraTokenInit - balanceKaruraTokenMiddle;556      const qtzIncomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenInit;557      karuraBackwardTransferAmount = qtzIncomeTransfer;558559      const karUnqFees = TRANSFER_AMOUNT - qtzIncomeTransfer;560561      console.log(562        '[Quartz -> Karura] transaction fees on Karura: %s KAR',563        helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),564      );565      console.log(566        '[Quartz -> Karura] transaction fees on Karura: %s QTZ',567        helper.util.bigIntToDecimals(karUnqFees),568      );569      console.log('[Quartz -> Karura] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));570      expect(karFees == 0n).to.be.true;571572      const bigintAbs = (n: bigint) => (n < 0n) ? -n : n;573574      expect(575        bigintAbs(karUnqFees - expectedKaruraIncomeFee) < karuraEps,576        'Karura took different income fee, check the Karura foreign asset config',577      ).to.be.true;578    });579  });580581  itSub('Should connect to Karura and send QTZ back', async ({helper}) => {582    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {583      const destination = {584        V2: {585          parents: 1,586          interior: {587            X2: [588              {Parachain: QUARTZ_CHAIN},589              {590                AccountId32: {591                  network: 'Any',592                  id: randomAccount.addressRaw,593                },594              },595            ],596          },597        },598      };599600      const id = {601        ForeignAsset: 0,602      };603604      await helper.xTokens.transfer(randomAccount, id, karuraBackwardTransferAmount, destination, 'Unlimited');605      balanceKaruraTokenFinal = await helper.balance.getSubstrate(randomAccount.address);606      balanceQuartzForeignTokenFinal = await helper.tokens.accounts(randomAccount.address, id);607608      const karFees = balanceKaruraTokenMiddle - balanceKaruraTokenFinal;609      const qtzOutcomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenFinal;610611      console.log(612        '[Karura -> Quartz] transaction fees on Karura: %s KAR',613        helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),614      );615      console.log('[Karura -> Quartz] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));616617      expect(karFees > 0, 'Negative fees KAR, looks like nothing was transferred').to.be.true;618      expect(qtzOutcomeTransfer == karuraBackwardTransferAmount).to.be.true;619    });620621    await helper.wait.newBlocks(3);622623    balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccount.address);624    const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;625    expect(actuallyDelivered > 0).to.be.true;626627    console.log('[Karura -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));628629    const qtzFees = karuraBackwardTransferAmount - actuallyDelivered;630    console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));631    expect(qtzFees == 0n).to.be.true;632  });633634  itSub('Karura can send only up to its balance', async ({helper}) => {635    // set Karura's sovereign account's balance636    const karuraBalance = 10000n * (10n ** QTZ_DECIMALS);637    const karuraSovereignAccount = helper.address.paraSiblingSovereignAccount(KARURA_CHAIN);638    await helper.getSudo().balance.setBalanceSubstrate(alice, karuraSovereignAccount, karuraBalance);639640    const moreThanKaruraHas = karuraBalance * 2n;641642    let targetAccountBalance = 0n;643    const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);644645    const quartzMultilocation = {646      V2: {647        parents: 1,648        interior: {649          X1: {Parachain: QUARTZ_CHAIN},650        },651      },652    };653654    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(655      targetAccount.addressRaw,656      {657        Concrete: {658          parents: 0,659          interior: 'Here',660        },661      },662      moreThanKaruraHas,663    );664665    let maliciousXcmProgramSent: any;666    const maxWaitBlocks = 5;667668    // Try to trick Quartz669    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {670      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);671672      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);673    });674675    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash676        && event.outcome.isFailedToTransactAsset);677678    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);679    expect(targetAccountBalance).to.be.equal(0n);680681    // But Karura still can send the correct amount682    const validTransferAmount = karuraBalance / 2n;683    const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(684      targetAccount.addressRaw,685      {686        Concrete: {687          parents: 0,688          interior: 'Here',689        },690      },691      validTransferAmount,692    );693694    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {695      await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);696    });697698    await helper.wait.newBlocks(maxWaitBlocks);699700    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);701    expect(targetAccountBalance).to.be.equal(validTransferAmount);702  });703704  itSub('Should not accept reserve transfer of QTZ from Karura', async ({helper}) => {705    const testAmount = 10_000n * (10n ** QTZ_DECIMALS);706    const [targetAccount] = await helper.arrange.createAccounts([0n], alice);707708    const quartzMultilocation = {709      V2: {710        parents: 1,711        interior: {712          X1: {713            Parachain: QUARTZ_CHAIN,714          },715        },716      },717    };718719    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(720      targetAccount.addressRaw,721      {722        Concrete: {723          parents: 1,724          interior: {725            X1: {726              Parachain: QUARTZ_CHAIN,727            },728          },729        },730      },731      testAmount,732    );733734    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(735      targetAccount.addressRaw,736      {737        Concrete: {738          parents: 0,739          interior: 'Here',740        },741      },742      testAmount,743    );744745    let maliciousXcmProgramFullIdSent: any;746    let maliciousXcmProgramHereIdSent: any;747    const maxWaitBlocks = 3;748749    // Try to trick Quartz using full QTZ identification750    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {751      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);752753      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);754    });755756    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash757        && event.outcome.isUntrustedReserveLocation);758759    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);760    expect(accountBalance).to.be.equal(0n);761762    // Try to trick Quartz using shortened QTZ identification763    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {764      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);765766      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);767    });768769    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash770        && event.outcome.isUntrustedReserveLocation);771772    accountBalance = await helper.balance.getSubstrate(targetAccount.address);773    expect(accountBalance).to.be.equal(0n);774  });775});776777// These tests are relevant only when778// the the corresponding foreign assets are not registered779describeXCM('[XCM] Integration test: Quartz rejects non-native tokens', () => {780  let alice: IKeyringPair;781  let alith: IKeyringPair;782783  const testAmount = 100_000_000_000n;784  let quartzParachainJunction;785  let quartzAccountJunction;786787  let quartzParachainMultilocation: any;788  let quartzAccountMultilocation: any;789  let quartzCombinedMultilocation: any;790791  let messageSent: any;792793  const maxWaitBlocks = 3;794795  before(async () => {796    await usingPlaygrounds(async (helper, privateKey) => {797      alice = await privateKey('//Alice');798799      quartzParachainJunction = {Parachain: QUARTZ_CHAIN};800      quartzAccountJunction = {801        AccountId32: {802          network: 'Any',803          id: alice.addressRaw,804        },805      };806807      quartzParachainMultilocation = {808        V2: {809          parents: 1,810          interior: {811            X1: quartzParachainJunction,812          },813        },814      };815816      quartzAccountMultilocation = {817        V2: {818          parents: 0,819          interior: {820            X1: quartzAccountJunction,821          },822        },823      };824825      quartzCombinedMultilocation = {826        V2: {827          parents: 1,828          interior: {829            X2: [quartzParachainJunction, quartzAccountJunction],830          },831        },832      };833834      // Set the default version to wrap the first message to other chains.835      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);836    });837838    // eslint-disable-next-line require-await839    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {840      alith = helper.account.alithAccount();841    });842  });843844  const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {845    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash846        && event.outcome.isFailedToTransactAsset);847  };848849  itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => {850    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {851      const id = {852        Token: 'KAR',853      };854      const destination = quartzCombinedMultilocation;855      await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');856857      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);858    });859860    await expectFailedToTransact(helper, messageSent);861  });862863  itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => {864    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {865      const id = 'SelfReserve';866      const destination = quartzCombinedMultilocation;867      await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');868869      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);870    });871872    await expectFailedToTransact(helper, messageSent);873  });874875  itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => {876    await usingShidenPlaygrounds(shidenUrl, async (helper) => {877      const destinationParachain = quartzParachainMultilocation;878      const beneficiary = quartzAccountMultilocation;879      const assets = {880        V2: [{881          id: {882            Concrete: {883              parents: 0,884              interior: 'Here',885            },886          },887          fun: {888            Fungible: testAmount,889          },890        }],891      };892      const feeAssetItem = 0;893894      await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [895        destinationParachain,896        beneficiary,897        assets,898        feeAssetItem,899      ]);900901      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);902    });903904    await expectFailedToTransact(helper, messageSent);905  });906});907908describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => {909  // Quartz constants910  let alice: IKeyringPair;911  let quartzAssetLocation;912913  let randomAccountQuartz: IKeyringPair;914  let randomAccountMoonriver: IKeyringPair;915916  // Moonriver constants917  let assetId: string;918919  const quartzAssetMetadata = {920    name: 'xcQuartz',921    symbol: 'xcQTZ',922    decimals: 18,923    isFrozen: false,924    minimalBalance: 1n,925  };926927  let balanceQuartzTokenInit: bigint;928  let balanceQuartzTokenMiddle: bigint;929  let balanceQuartzTokenFinal: bigint;930  let balanceForeignQtzTokenInit: bigint;931  let balanceForeignQtzTokenMiddle: bigint;932  let balanceForeignQtzTokenFinal: bigint;933  let balanceMovrTokenInit: bigint;934  let balanceMovrTokenMiddle: bigint;935  let balanceMovrTokenFinal: bigint;936937  before(async () => {938    await usingPlaygrounds(async (helper, privateKey) => {939      alice = await privateKey('//Alice');940      [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);941942      balanceForeignQtzTokenInit = 0n;943944      // Set the default version to wrap the first message to other chains.945      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);946    });947948    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {949      const alithAccount = helper.account.alithAccount();950      const baltatharAccount = helper.account.baltatharAccount();951      const dorothyAccount = helper.account.dorothyAccount();952953      randomAccountMoonriver = helper.account.create();954955      // >>> Sponsoring Dorothy >>>956      console.log('Sponsoring Dorothy.......');957      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);958      console.log('Sponsoring Dorothy.......DONE');959      // <<< Sponsoring Dorothy <<<960961      quartzAssetLocation = {962        XCM: {963          parents: 1,964          interior: {X1: {Parachain: QUARTZ_CHAIN}},965        },966      };967      const existentialDeposit = 1n;968      const isSufficient = true;969      const unitsPerSecond = 1n;970      const numAssetsWeightHint = 0;971972      const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({973        location: quartzAssetLocation,974        metadata: quartzAssetMetadata,975        existentialDeposit,976        isSufficient,977        unitsPerSecond,978        numAssetsWeightHint,979      });980981      console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);982983      await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);984985      // >>> Acquire Quartz AssetId Info on Moonriver >>>986      console.log('Acquire Quartz AssetId Info on Moonriver.......');987988      assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();989990      console.log('QTZ asset ID is %s', assetId);991      console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');992      // >>> Acquire Quartz AssetId Info on Moonriver >>>993994      // >>> Sponsoring random Account >>>995      console.log('Sponsoring random Account.......');996      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);997      console.log('Sponsoring random Account.......DONE');998      // <<< Sponsoring random Account <<<9991000      balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address);1001    });10021003    await usingPlaygrounds(async (helper) => {1004      await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);1005      balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address);1006    });1007  });10081009  itSub('Should connect and send QTZ to Moonriver', async ({helper}) => {1010    const currencyId = {1011      NativeAssetId: 'Here',1012    };1013    const dest = {1014      V2: {1015        parents: 1,1016        interior: {1017          X2: [1018            {Parachain: MOONRIVER_CHAIN},1019            {AccountKey20: {network: 'Any', key: randomAccountMoonriver.address}},1020          ],1021        },1022      },1023    };1024    const amount = TRANSFER_AMOUNT;10251026    await helper.xTokens.transfer(randomAccountQuartz, currencyId, amount, dest, 'Unlimited');10271028    balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccountQuartz.address);1029    expect(balanceQuartzTokenMiddle < balanceQuartzTokenInit).to.be.true;10301031    const transactionFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;1032    console.log('[Quartz -> Moonriver] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(transactionFees));1033    expect(transactionFees > 0, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;10341035    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1036      await helper.wait.newBlocks(3);10371038      balanceMovrTokenMiddle = await helper.balance.getEthereum(randomAccountMoonriver.address);10391040      const movrFees = balanceMovrTokenInit - balanceMovrTokenMiddle;1041      console.log('[Quartz -> Moonriver] transaction fees on Moonriver: %s MOVR',helper.util.bigIntToDecimals(movrFees));1042      expect(movrFees == 0n).to.be.true;10431044      balanceForeignQtzTokenMiddle = (await helper.assets.account(assetId, randomAccountMoonriver.address))!; // BigInt(qtzRandomAccountAsset['balance']);1045      const qtzIncomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenInit;1046      console.log('[Quartz -> Moonriver] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));1047      expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true;1048    });1049  });10501051  itSub('Should connect to Moonriver and send QTZ back', async ({helper}) => {1052    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1053      const asset = {1054        V2: {1055          id: {1056            Concrete: {1057              parents: 1,1058              interior: {1059                X1: {Parachain: QUARTZ_CHAIN},1060              },1061            },1062          },1063          fun: {1064            Fungible: TRANSFER_AMOUNT,1065          },1066        },1067      };1068      const destination = {1069        V2: {1070          parents: 1,1071          interior: {1072            X2: [1073              {Parachain: QUARTZ_CHAIN},1074              {AccountId32: {network: 'Any', id: randomAccountQuartz.addressRaw}},1075            ],1076          },1077        },1078      };10791080      await helper.xTokens.transferMultiasset(randomAccountMoonriver, asset, destination, 'Unlimited');10811082      balanceMovrTokenFinal = await helper.balance.getEthereum(randomAccountMoonriver.address);10831084      const movrFees = balanceMovrTokenMiddle - balanceMovrTokenFinal;1085      console.log('[Moonriver -> Quartz] transaction fees on Moonriver: %s MOVR', helper.util.bigIntToDecimals(movrFees));1086      expect(movrFees > 0, 'Negative fees MOVR, looks like nothing was transferred').to.be.true;10871088      const qtzRandomAccountAsset = await helper.assets.account(assetId, randomAccountMoonriver.address);10891090      expect(qtzRandomAccountAsset).to.be.null;10911092      balanceForeignQtzTokenFinal = 0n;10931094      const qtzOutcomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenFinal;1095      console.log('[Quartz -> Moonriver] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));1096      expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;1097    });10981099    await helper.wait.newBlocks(3);11001101    balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccountQuartz.address);1102    const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;1103    expect(actuallyDelivered > 0).to.be.true;11041105    console.log('[Moonriver -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));11061107    const qtzFees = TRANSFER_AMOUNT - actuallyDelivered;1108    console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));1109    expect(qtzFees == 0n).to.be.true;1110  });11111112  itSub('Moonriver can send only up to its balance', async ({helper}) => {1113    // set Moonriver's sovereign account's balance1114    const moonriverBalance = 10000n * (10n ** QTZ_DECIMALS);1115    const moonriverSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONRIVER_CHAIN);1116    await helper.getSudo().balance.setBalanceSubstrate(alice, moonriverSovereignAccount, moonriverBalance);11171118    const moreThanMoonriverHas = moonriverBalance * 2n;11191120    let targetAccountBalance = 0n;1121    const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);11221123    const quartzMultilocation = {1124      V2: {1125        parents: 1,1126        interior: {1127          X1: {Parachain: QUARTZ_CHAIN},1128        },1129      },1130    };11311132    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1133      targetAccount.addressRaw,1134      {1135        Concrete: {1136          parents: 0,1137          interior: 'Here',1138        },1139      },1140      moreThanMoonriverHas,1141    );11421143    let maliciousXcmProgramSent: any;1144    const maxWaitBlocks = 3;11451146    // Try to trick Quartz1147    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1148      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]);11491150      // Needed to bypass the call filter.1151      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1152      await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall);11531154      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1155    });11561157    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1158        && event.outcome.isFailedToTransactAsset);11591160    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1161    expect(targetAccountBalance).to.be.equal(0n);11621163    // But Moonriver still can send the correct amount1164    const validTransferAmount = moonriverBalance / 2n;1165    const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1166      targetAccount.addressRaw,1167      {1168        Concrete: {1169          parents: 0,1170          interior: 'Here',1171        },1172      },1173      validTransferAmount,1174    );11751176    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1177      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, validXcmProgram]);11781179      // Needed to bypass the call filter.1180      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1181      await helper.fastDemocracy.executeProposal('Spend the correct amount of QTZ', batchCall);1182    });11831184    await helper.wait.newBlocks(maxWaitBlocks);11851186    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1187    expect(targetAccountBalance).to.be.equal(validTransferAmount);1188  });11891190  itSub('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => {1191    const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1192    const [targetAccount] = await helper.arrange.createAccounts([0n], alice);11931194    const quartzMultilocation = {1195      V2: {1196        parents: 1,1197        interior: {1198          X1: {1199            Parachain: QUARTZ_CHAIN,1200          },1201        },1202      },1203    };12041205    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1206      targetAccount.addressRaw,1207      {1208        Concrete: {1209          parents: 0,1210          interior: {1211            X1: {1212              Parachain: QUARTZ_CHAIN,1213            },1214          },1215        },1216      },1217      testAmount,1218    );12191220    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1221      targetAccount.addressRaw,1222      {1223        Concrete: {1224          parents: 0,1225          interior: 'Here',1226        },1227      },1228      testAmount,1229    );12301231    let maliciousXcmProgramFullIdSent: any;1232    let maliciousXcmProgramHereIdSent: any;1233    const maxWaitBlocks = 3;12341235    // Try to trick Quartz using full QTZ identification1236    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1237      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]);12381239      // Needed to bypass the call filter.1240      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1241      await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall);12421243      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1244    });12451246    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1247        && event.outcome.isUntrustedReserveLocation);12481249    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1250    expect(accountBalance).to.be.equal(0n);12511252    // Try to trick Quartz using shortened QTZ identification1253    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1254      const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramHereId]);12551256      // Needed to bypass the call filter.1257      const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1258      await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall);12591260      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1261    });12621263    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1264        && event.outcome.isUntrustedReserveLocation);12651266    accountBalance = await helper.balance.getSubstrate(targetAccount.address);1267    expect(accountBalance).to.be.equal(0n);1268  });1269});12701271describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => {1272  let alice: IKeyringPair;1273  let sender: IKeyringPair;12741275  const QTZ_ASSET_ID_ON_SHIDEN = 1;1276  const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n;12771278  // Quartz -> Shiden1279  const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden1280  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?1281  const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ1282  const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens12831284  // Shiden -> Quartz1285  const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ1286  const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 4.999_999_999_088_000_000n QTZ12871288  let balanceAfterQuartzToShidenXCM: bigint;12891290  before(async () => {1291    await usingPlaygrounds(async (helper, privateKey) => {1292      alice = await privateKey('//Alice');1293      [sender] = await helper.arrange.createAccounts([100n], alice);1294      console.log('sender', sender.address);12951296      // Set the default version to wrap the first message to other chains.1297      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);1298    });12991300    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1301      console.log('1. Create foreign asset and metadata');1302      // TODO update metadata with values from production1303      await helper.assets.create(1304        alice,1305        QTZ_ASSET_ID_ON_SHIDEN,1306        alice.address,1307        QTZ_MINIMAL_BALANCE_ON_SHIDEN,1308      );13091310      await helper.assets.setMetadata(1311        alice,1312        QTZ_ASSET_ID_ON_SHIDEN,1313        'Cross chain QTZ',1314        'xcQTZ',1315        Number(QTZ_DECIMALS),1316      );13171318      console.log('2. Register asset location on Shiden');1319      const assetLocation = {1320        V2: {1321          parents: 1,1322          interior: {1323            X1: {1324              Parachain: QUARTZ_CHAIN,1325            },1326          },1327        },1328      };13291330      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);13311332      console.log('3. Set QTZ payment for XCM execution on Shiden');1333      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);13341335      console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');1336      await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);1337    });1338  });13391340  itSub('Should connect and send QTZ to Shiden', async ({helper}) => {1341    const destination = {1342      V2: {1343        parents: 1,1344        interior: {1345          X1: {1346            Parachain: SHIDEN_CHAIN,1347          },1348        },1349      },1350    };13511352    const beneficiary = {1353      V2: {1354        parents: 0,1355        interior: {1356          X1: {1357            AccountId32: {1358              network: 'Any',1359              id: sender.addressRaw,1360            },1361          },1362        },1363      },1364    };13651366    const assets = {1367      V2: [1368        {1369          id: {1370            Concrete: {1371              parents: 0,1372              interior: 'Here',1373            },1374          },1375          fun: {1376            Fungible: qtzToShidenTransferred,1377          },1378        },1379      ],1380    };13811382    // Initial balance is 100 QTZ1383    const balanceBefore = await helper.balance.getSubstrate(sender.address);1384    console.log(`Initial balance is: ${balanceBefore}`);13851386    const feeAssetItem = 0;1387    await helper.xcm.limitedReserveTransferAssets(sender, destination, beneficiary, assets, feeAssetItem, 'Unlimited');13881389    // Balance after reserve transfer is less than 901390    balanceAfterQuartzToShidenXCM = await helper.balance.getSubstrate(sender.address);1391    console.log(`QTZ Balance on Quartz after XCM is: ${balanceAfterQuartzToShidenXCM}`);1392    console.log(`Quartz's QTZ commission is: ${balanceBefore - balanceAfterQuartzToShidenXCM}`);1393    expect(balanceBefore - balanceAfterQuartzToShidenXCM > 0).to.be.true;13941395    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1396      await helper.wait.newBlocks(3);1397      const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1398      const shidenBalance = await helper.balance.getSubstrate(sender.address);13991400      console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);1401      console.log(`Shiden's QTZ commission is: ${qtzToShidenTransferred - xcQTZbalance!}`);14021403      expect(xcQTZbalance).to.eq(qtzToShidenArrived);1404      // SHD balance does not changed:1405      expect(shidenBalance).to.eq(shidenInitialBalance);1406    });1407  });14081409  itSub('Should connect to Shiden and send QTZ back', async ({helper}) => {1410    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1411      const destination = {1412        V2: {1413          parents: 1,1414          interior: {1415            X1: {1416              Parachain: QUARTZ_CHAIN,1417            },1418          },1419        },1420      };14211422      const beneficiary = {1423        V2: {1424          parents: 0,1425          interior: {1426            X1: {1427              AccountId32: {1428                network: 'Any',1429                id: sender.addressRaw,1430              },1431            },1432          },1433        },1434      };14351436      const assets = {1437        V2: [1438          {1439            id: {1440              Concrete: {1441                parents: 1,1442                interior: {1443                  X1: {1444                    Parachain: QUARTZ_CHAIN,1445                  },1446                },1447              },1448            },1449            fun: {1450              Fungible: qtzFromShidenTransfered,1451            },1452          },1453        ],1454      };14551456      // Initial balance is 1 SDN1457      const balanceSDNbefore = await helper.balance.getSubstrate(sender.address);1458      console.log(`SDN balance is: ${balanceSDNbefore}, it does not changed`);1459      expect(balanceSDNbefore).to.eq(shidenInitialBalance);14601461      const feeAssetItem = 0;1462      // this is non-standard polkadotXcm extension for Astar only. It calls InitiateReserveWithdraw1463      await helper.executeExtrinsic(sender, 'api.tx.polkadotXcm.reserveWithdrawAssets', [destination, beneficiary, assets, feeAssetItem]);14641465      // Balance after reserve transfer is less than 1 SDN1466      const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1467      const balanceSDN = await helper.balance.getSubstrate(sender.address);1468      console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);14691470      // Assert: xcQTZ balance correctly decreased1471      expect(xcQTZbalance).to.eq(qtzOnShidenLeft);1472      // Assert: SDN balance is 0.996...1473      expect(balanceSDN / (10n ** (SHIDEN_DECIMALS - 3n))).to.eq(996n);1474    });14751476    await helper.wait.newBlocks(3);1477    const balanceQTZ = await helper.balance.getSubstrate(sender.address);1478    console.log(`QTZ Balance on Quartz after XCM is: ${balanceQTZ}`);1479    expect(balanceQTZ).to.eq(balanceAfterQuartzToShidenXCM + qtzFromShidenTransfered);1480  });14811482  itSub('Shiden can send only up to its balance', async ({helper}) => {1483    // set Shiden's sovereign account's balance1484    const shidenBalance = 10000n * (10n ** QTZ_DECIMALS);1485    const shidenSovereignAccount = helper.address.paraSiblingSovereignAccount(SHIDEN_CHAIN);1486    await helper.getSudo().balance.setBalanceSubstrate(alice, shidenSovereignAccount, shidenBalance);14871488    const moreThanShidenHas = shidenBalance * 2n;14891490    let targetAccountBalance = 0n;1491    const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);14921493    const quartzMultilocation = {1494      V2: {1495        parents: 1,1496        interior: {1497          X1: {Parachain: QUARTZ_CHAIN},1498        },1499      },1500    };15011502    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1503      targetAccount.addressRaw,1504      {1505        Concrete: {1506          parents: 0,1507          interior: 'Here',1508        },1509      },1510      moreThanShidenHas,1511    );15121513    let maliciousXcmProgramSent: any;1514    const maxWaitBlocks = 3;15151516    // Try to trick Quartz1517    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1518      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);15191520      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1521    });15221523    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1524        && event.outcome.isFailedToTransactAsset);15251526    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1527    expect(targetAccountBalance).to.be.equal(0n);15281529    // But Shiden still can send the correct amount1530    const validTransferAmount = shidenBalance / 2n;1531    const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1532      targetAccount.addressRaw,1533      {1534        Concrete: {1535          parents: 0,1536          interior: 'Here',1537        },1538      },1539      validTransferAmount,1540    );15411542    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1543      await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);1544    });15451546    await helper.wait.newBlocks(maxWaitBlocks);15471548    targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1549    expect(targetAccountBalance).to.be.equal(validTransferAmount);1550  });15511552  itSub('Should not accept reserve transfer of QTZ from Shiden', async ({helper}) => {1553    const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1554    const [targetAccount] = await helper.arrange.createAccounts([0n], alice);15551556    const quartzMultilocation = {1557      V2: {1558        parents: 1,1559        interior: {1560          X1: {1561            Parachain: QUARTZ_CHAIN,1562          },1563        },1564      },1565    };15661567    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1568      targetAccount.addressRaw,1569      {1570        Concrete: {1571          parents: 1,1572          interior: {1573            X1: {1574              Parachain: QUARTZ_CHAIN,1575            },1576          },1577        },1578      },1579      testAmount,1580    );15811582    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1583      targetAccount.addressRaw,1584      {1585        Concrete: {1586          parents: 0,1587          interior: 'Here',1588        },1589      },1590      testAmount,1591    );15921593    let maliciousXcmProgramFullIdSent: any;1594    let maliciousXcmProgramHereIdSent: any;1595    const maxWaitBlocks = 3;15961597    // Try to trick Quartz using full QTZ identification1598    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1599      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);16001601      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1602    });16031604    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1605        && event.outcome.isUntrustedReserveLocation);16061607    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1608    expect(accountBalance).to.be.equal(0n);16091610    // Try to trick Quartz using shortened QTZ identification1611    await usingShidenPlaygrounds(shidenUrl, async (helper) => {1612      await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);16131614      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1615    });16161617    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1618        && event.outcome.isUntrustedReserveLocation);16191620    accountBalance = await helper.balance.getSubstrate(targetAccount.address);1621    expect(accountBalance).to.be.equal(0n);1622  });1623});