difftreelog
fix XCM tests
in: master
6 files changed
js-packages/playgrounds/types.xcm.tsdiffbeforeafterboth--- a/js-packages/playgrounds/types.xcm.ts
+++ b/js-packages/playgrounds/types.xcm.ts
@@ -27,10 +27,3 @@
conviction: number,
},
}
-
-export interface IForeignAssetMetadata {
- name?: number | Uint8Array,
- symbol?: string,
- decimals?: number,
- minimalBalance?: bigint,
-}
\ No newline at end of file
js-packages/playgrounds/unique.xcm.tsdiffbeforeafterboth--- a/js-packages/playgrounds/unique.xcm.ts
+++ b/js-packages/playgrounds/unique.xcm.ts
@@ -2,7 +2,7 @@
import type {IKeyringPair} from '@polkadot/types/types';
import {ChainHelperBase, EthereumBalanceGroup, HelperGroup, SubstrateBalanceGroup, UniqueHelper} from './unique.js';
import type {ILogger, TSigner, TSubstrateAccount} from './types.js';
-import type {AcalaAssetMetadata, DemocracyStandardAccountVote, IForeignAssetMetadata, MoonbeamAssetInfo} from './types.xcm.js';
+import type {AcalaAssetMetadata, DemocracyStandardAccountVote, MoonbeamAssetInfo} from './types.xcm.js';
export class XcmChainHelper extends ChainHelperBase {
@@ -104,22 +104,17 @@
}
export class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {
- async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {
+ async register(signer: TSigner, location: any, name: string, tokenPrefix: string, mode: 'NFT' | { Fungible: number }) {
await this.helper.executeExtrinsic(
signer,
- 'api.tx.foreignAssets.registerForeignAsset',
- [ownerAddress, location, metadata],
+ 'api.tx.foreignAssets.forceRegisterForeignAsset',
+ [location, this.helper.util.str2vec(name), tokenPrefix, mode],
true,
);
}
- async update(signer: TSigner, foreignAssetId: number, location: any, metadata: IForeignAssetMetadata) {
- await this.helper.executeExtrinsic(
- signer,
- 'api.tx.foreignAssets.updateForeignAsset',
- [foreignAssetId, location, metadata],
- true,
- );
+ async foreignCollectionId(location: any) {
+ return (await this.helper.callRpc('api.query.foreignAssets.foreignReserveLocationToCollection', [location])).toJSON();
}
}
@@ -256,6 +251,10 @@
await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
}
+ async assetInfo(assetId: number | bigint) {
+ return (await this.helper.callRpc('api.query.assets.asset', [assetId])).toJSON();
+ }
+
async account(assetId: string | number | bigint, address: string) {
const accountAsset = (
await this.helper.callRpc('api.query.assets.account', [assetId, address])
js-packages/tests/xcm/xcm.types.tsdiffbeforeafterboth--- a/js-packages/tests/xcm/xcm.types.ts
+++ b/js-packages/tests/xcm/xcm.types.ts
@@ -505,4 +505,26 @@
await expectFailedToTransact(helper, messageSent);
});
}
+
+ async registerRelayNativeTokenOnUnique(alice: IKeyringPair) {
+ return await usingPlaygrounds(async (helper) => {
+ const relayLocation = {
+ parents: 1,
+ interior: 'Here',
+ };
+
+ const relayCollectionId = await helper.foreignAssets.foreignCollectionId(relayLocation);
+ if(relayCollectionId == null) {
+ const name = 'Relay Tokens';
+ const tokenPrefix = 'xDOT';
+ const decimals = 10;
+ await helper.getSudo().foreignAssets.register(alice, relayLocation, name, tokenPrefix, {Fungible: decimals});
+
+ return await helper.foreignAssets.foreignCollectionId(relayLocation);
+ } else {
+ console.log('Relay foreign collection is already registered');
+ return relayCollectionId;
+ }
+ });
+ }
}
js-packages/tests/xcm/xcmOpal.test.tsdiffbeforeafterboth--- a/js-packages/tests/xcm/xcmOpal.test.ts
+++ b/js-packages/tests/xcm/xcmOpal.test.ts
@@ -17,6 +17,7 @@
import type {IKeyringPair} from '@polkadot/types/types';
import config from '../config.js';
import {itSub, expect, describeXCM, usingPlaygrounds, usingWestmintPlaygrounds, usingRelayPlaygrounds} from '../util/index.js';
+import {XcmTestHelper} from './xcm.types';
const STATEMINE_CHAIN = +(process.env.RELAY_WESTMINT_ID || 1000);
const UNIQUE_CHAIN = +(process.env.RELAY_OPAL_ID || 2095);
@@ -25,11 +26,11 @@
const westmintUrl = config.westmintUrl;
const STATEMINE_PALLET_INSTANCE = 50;
-const ASSET_ID = 100;
-const ASSET_METADATA_DECIMALS = 18;
-const ASSET_METADATA_NAME = 'USDT';
-const ASSET_METADATA_DESCRIPTION = 'USDT';
-const ASSET_METADATA_MINIMAL_BALANCE = 1n;
+const USDT_ASSET_ID = 100;
+const USDT_ASSET_METADATA_DECIMALS = 18;
+const USDT_ASSET_METADATA_NAME = 'USDT';
+const USDT_ASSET_METADATA_DESCRIPTION = 'USDT';
+const USDT_ASSET_METADATA_MINIMAL_BALANCE = 1n;
const RELAY_DECIMALS = 12;
const WESTMINT_DECIMALS = 12;
@@ -37,7 +38,9 @@
const TRANSFER_AMOUNT = 1_000_000_000_000_000_000n;
// 10,000.00 (ten thousands) USDT
-const ASSET_AMOUNT = 1_000_000_000_000_000_000_000n;
+const USDT_ASSET_AMOUNT = 1_000_000_000_000_000_000_000n;
+
+const testHelper = new XcmTestHelper('opal');
describeXCM('[XCM] Integration test: Exchanging USDT with Westmint', () => {
let alice: IKeyringPair;
@@ -57,57 +60,78 @@
let balanceBobRelayTokenBefore: bigint;
let balanceBobRelayTokenAfter: bigint;
+ let usdtCollectionId: number;
+ let relayCollectionId: number;
before(async () => {
await usingPlaygrounds(async (_helper, privateKey) => {
alice = await privateKey('//Alice');
bob = await privateKey('//Bob'); // funds donor
+
+ relayCollectionId = await testHelper.registerRelayNativeTokenOnUnique(alice);
});
await usingWestmintPlaygrounds(westmintUrl, async (helper) => {
- // 350.00 (three hundred fifty) DOT
- const fundingAmount = 3_500_000_000_000n;
+ const assetInfo = await helper.assets.assetInfo(USDT_ASSET_ID);
+ if(assetInfo == null) {
+ await helper.assets.create(
+ alice,
+ USDT_ASSET_ID,
+ alice.address,
+ USDT_ASSET_METADATA_MINIMAL_BALANCE,
+ );
+ await helper.assets.setMetadata(
+ alice,
+ USDT_ASSET_ID,
+ USDT_ASSET_METADATA_NAME,
+ USDT_ASSET_METADATA_DESCRIPTION,
+ USDT_ASSET_METADATA_DECIMALS,
+ );
+ } else {
+ console.log('The USDT asset is already registered on AssetHub');
+ }
- await helper.assets.create(alice, ASSET_ID, alice.address, ASSET_METADATA_MINIMAL_BALANCE);
- await helper.assets.setMetadata(alice, ASSET_ID, ASSET_METADATA_NAME, ASSET_METADATA_DESCRIPTION, ASSET_METADATA_DECIMALS);
- await helper.assets.mint(alice, ASSET_ID, alice.address, ASSET_AMOUNT);
+ await helper.assets.mint(
+ alice,
+ USDT_ASSET_ID,
+ alice.address,
+ USDT_ASSET_AMOUNT,
+ );
+
+ const sovereignFundingAmount = 3_500_000_000n;
// funding parachain sovereing account (Parachain: 2095)
const parachainSovereingAccount = helper.address.paraSiblingSovereignAccount(UNIQUE_CHAIN);
- await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, fundingAmount);
+ await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, sovereignFundingAmount);
});
-
await usingPlaygrounds(async (helper) => {
const location = {
- V2: {
- parents: 1,
- interior: {X3: [
- {
- Parachain: STATEMINE_CHAIN,
- },
- {
- PalletInstance: STATEMINE_PALLET_INSTANCE,
- },
- {
- GeneralIndex: ASSET_ID,
- },
- ]},
- },
+ parents: 1,
+ interior: {X3: [
+ {
+ Parachain: STATEMINE_CHAIN,
+ },
+ {
+ PalletInstance: STATEMINE_PALLET_INSTANCE,
+ },
+ {
+ GeneralIndex: USDT_ASSET_ID,
+ },
+ ]},
};
- const metadata =
- {
- name: ASSET_ID,
- symbol: ASSET_METADATA_NAME,
- decimals: ASSET_METADATA_DECIMALS,
- minimalBalance: ASSET_METADATA_MINIMAL_BALANCE,
- };
- await helper.getSudo().foreignAssets.register(alice, alice.address, location, metadata);
+ if(await helper.foreignAssets.foreignCollectionId(location) == null) {
+ const tokenPrefix = USDT_ASSET_METADATA_NAME;
+ await helper.getSudo().foreignAssets.register(alice, location, USDT_ASSET_METADATA_NAME, tokenPrefix, {Fungible: USDT_ASSET_METADATA_DECIMALS});
+ } else {
+ console.log('Foreign collection is already registered on Opal');
+ }
+
balanceOpalBefore = await helper.balance.getSubstrate(alice.address);
+ usdtCollectionId = await helper.foreignAssets.foreignCollectionId(location);
});
-
// Providing the relay currency to the unique sender account
await usingRelayPlaygrounds(relayUrl, async (helper) => {
const destination = {
@@ -189,7 +213,7 @@
PalletInstance: STATEMINE_PALLET_INSTANCE,
},
{
- GeneralIndex: ASSET_ID,
+ GeneralIndex: USDT_ASSET_ID,
},
]},
},
@@ -216,19 +240,17 @@
expect(balanceStmnBefore > balanceStmnAfter).to.be.true;
});
-
// ensure that asset has been delivered
await helper.wait.newBlocks(3);
- // expext collection id will be with id 1
- const free = await helper.ft.getBalance(1, {Substrate: alice.address});
+ const free = await helper.ft.getBalance(usdtCollectionId, {Substrate: alice.address});
balanceOpalAfter = await helper.balance.getSubstrate(alice.address);
console.log(
'[Westmint -> Opal] transaction fees on Opal: %s USDT',
- helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, ASSET_METADATA_DECIMALS),
+ helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, USDT_ASSET_METADATA_DECIMALS),
);
console.log(
'[Westmint -> Opal] transaction fees on Opal: %s OPL',
@@ -261,16 +283,11 @@
const currencies: [any, bigint][] = [
[
- {
- ForeignAssetId: 0,
- },
- //10_000_000_000_000_000n,
+ usdtCollectionId,
TRANSFER_AMOUNT,
],
[
- {
- NativeAssetId: 'Parent',
- },
+ relayCollectionId,
400_000_000_000_000n,
],
];
@@ -288,7 +305,7 @@
// The USDT token never paid fees. Its amount not changed from begin value.
// Also check that xcm transfer has been succeeded
- expect((await helper.assets.account(ASSET_ID, alice.address))! == ASSET_AMOUNT).to.be.true;
+ expect((await helper.assets.account(USDT_ASSET_ID, alice.address))! == USDT_ASSET_AMOUNT).to.be.true;
});
});
@@ -296,7 +313,7 @@
const TRANSFER_AMOUNT_RELAY = 50_000_000_000_000_000n;
balanceBobBefore = await helper.balance.getSubstrate(bob.address);
- balanceBobRelayTokenBefore = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});
+ balanceBobRelayTokenBefore = await helper.ft.getBalance(relayCollectionId, {Substrate: bob.address});
// Providing the relay currency to the unique sender account
await usingRelayPlaygrounds(relayUrl, async (helper) => {
@@ -345,7 +362,7 @@
await helper.wait.newBlocks(3);
balanceBobAfter = await helper.balance.getSubstrate(bob.address);
- balanceBobRelayTokenAfter = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});
+ balanceBobRelayTokenAfter = await helper.ft.getBalance(relayCollectionId, {Substrate: bob.address});
const wndFee = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;
console.log(
@@ -383,9 +400,7 @@
const currencies: any = [
[
- {
- NativeAssetId: 'Parent',
- },
+ relayCollectionId,
50_000_000_000_000_000n,
],
];
js-packages/tests/xcm/xcmQuartz.test.tsdiffbeforeafterboth1// 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 type {IKeyringPair} from '@polkadot/types/types';18import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util/index.js';19import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';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.js';21import {hexToString} from '@polkadot/util';22232425const STATEMINE_PALLET_INSTANCE = 50;2627const TRANSFER_AMOUNT = 2000000000000000000000000n;2829const FUNDING_AMOUNT = 3_500_000_0000_000_000n;3031const TRANSFER_AMOUNT_RELAY = 50_000_000_000_000_000n;3233const USDT_ASSET_ID = 100;34const USDT_ASSET_METADATA_DECIMALS = 18;35const USDT_ASSET_METADATA_NAME = 'USDT';36const USDT_ASSET_METADATA_DESCRIPTION = 'USDT';37const USDT_ASSET_METADATA_MINIMAL_BALANCE = 1n;38const USDT_ASSET_AMOUNT = 10_000_000_000_000_000_000_000_000n;3940const SAFE_XCM_VERSION = 2;4142describeXCM('[XCM] Integration test: Exchanging USDT with Statemine', () => {43 let alice: IKeyringPair;44 let bob: IKeyringPair;4546 let balanceStmnBefore: bigint;47 let balanceStmnAfter: bigint;4849 let balanceQuartzBefore: bigint;50 let balanceQuartzAfter: bigint;51 let balanceQuartzFinal: bigint;5253 let balanceBobBefore: bigint;54 let balanceBobAfter: bigint;55 let balanceBobFinal: bigint;5657 let balanceBobRelayTokenBefore: bigint;58 let balanceBobRelayTokenAfter: bigint;596061 before(async () => {62 await usingPlaygrounds(async (helper, privateKey) => {63 alice = await privateKey('//Alice');64 bob = await privateKey('//Bob'); // sovereign account on Statemine(t) funds donor6566 // Set the default version to wrap the first message to other chains.67 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);68 });6970 await usingRelayPlaygrounds(relayUrl, async (helper) => {71 // Fund accounts on Statemine(t)72 await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, alice.addressRaw, FUNDING_AMOUNT);73 await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, bob.addressRaw, FUNDING_AMOUNT);74 });7576 await usingStateminePlaygrounds(statemineUrl, async (helper) => {77 const sovereignFundingAmount = 3_500_000_000n;7879 await helper.assets.create(80 alice,81 USDT_ASSET_ID,82 alice.address,83 USDT_ASSET_METADATA_MINIMAL_BALANCE,84 );85 await helper.assets.setMetadata(86 alice,87 USDT_ASSET_ID,88 USDT_ASSET_METADATA_NAME,89 USDT_ASSET_METADATA_DESCRIPTION,90 USDT_ASSET_METADATA_DECIMALS,91 );92 await helper.assets.mint(93 alice,94 USDT_ASSET_ID,95 alice.address,96 USDT_ASSET_AMOUNT,97 );9899 // funding parachain sovereing account on Statemine(t).100 // The sovereign account should be created before any action101 // (the assets pallet on Statemine(t) check if the sovereign account exists)102 const parachainSovereingAccount = helper.address.paraSiblingSovereignAccount(QUARTZ_CHAIN);103 await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, sovereignFundingAmount);104 });105106107 await usingPlaygrounds(async (helper) => {108 const location = {109 V2: {110 parents: 1,111 interior: {X3: [112 {113 Parachain: STATEMINE_CHAIN,114 },115 {116 PalletInstance: STATEMINE_PALLET_INSTANCE,117 },118 {119 GeneralIndex: USDT_ASSET_ID,120 },121 ]},122 },123 };124125 const metadata =126 {127 name: USDT_ASSET_ID,128 symbol: USDT_ASSET_METADATA_NAME,129 decimals: USDT_ASSET_METADATA_DECIMALS,130 minimalBalance: USDT_ASSET_METADATA_MINIMAL_BALANCE,131 };132 await helper.getSudo().foreignAssets.register(alice, alice.address, location, metadata);133 balanceQuartzBefore = await helper.balance.getSubstrate(alice.address);134 });135136137 // Providing the relay currency to the quartz sender account138 // (fee for USDT XCM are paid in relay tokens)139 await usingRelayPlaygrounds(relayUrl, async (helper) => {140 const destination = {141 V2: {142 parents: 0,143 interior: {X1: {144 Parachain: QUARTZ_CHAIN,145 },146 },147 }};148149 const beneficiary = {150 V2: {151 parents: 0,152 interior: {X1: {153 AccountId32: {154 network: 'Any',155 id: alice.addressRaw,156 },157 }},158 },159 };160161 const assets = {162 V2: [163 {164 id: {165 Concrete: {166 parents: 0,167 interior: 'Here',168 },169 },170 fun: {171 Fungible: TRANSFER_AMOUNT_RELAY,172 },173 },174 ],175 };176177 const feeAssetItem = 0;178179 await helper.xcm.limitedReserveTransferAssets(alice, destination, beneficiary, assets, feeAssetItem, 'Unlimited');180 });181182 });183184 itSub('Should connect and send USDT from Statemine to Quartz', async ({helper}) => {185 await usingStateminePlaygrounds(statemineUrl, async (helper) => {186 const dest = {187 V2: {188 parents: 1,189 interior: {X1: {190 Parachain: QUARTZ_CHAIN,191 },192 },193 }};194195 const beneficiary = {196 V2: {197 parents: 0,198 interior: {X1: {199 AccountId32: {200 network: 'Any',201 id: alice.addressRaw,202 },203 }},204 },205 };206207 const assets = {208 V2: [209 {210 id: {211 Concrete: {212 parents: 0,213 interior: {214 X2: [215 {216 PalletInstance: STATEMINE_PALLET_INSTANCE,217 },218 {219 GeneralIndex: USDT_ASSET_ID,220 },221 ]},222 },223 },224 fun: {225 Fungible: TRANSFER_AMOUNT,226 },227 },228 ],229 };230231 const feeAssetItem = 0;232233 balanceStmnBefore = await helper.balance.getSubstrate(alice.address);234 await helper.xcm.limitedReserveTransferAssets(alice, dest, beneficiary, assets, feeAssetItem, 'Unlimited');235236 balanceStmnAfter = await helper.balance.getSubstrate(alice.address);237238 // common good parachain take commission in it native token239 console.log(240 '[Statemine -> Quartz] transaction fees on Statemine: %s WND',241 helper.util.bigIntToDecimals(balanceStmnBefore - balanceStmnAfter, STATEMINE_DECIMALS),242 );243 expect(balanceStmnBefore > balanceStmnAfter).to.be.true;244245 });246247248 // ensure that asset has been delivered249 await helper.wait.newBlocks(3);250251 // expext collection id will be with id 1252 const free = await helper.ft.getBalance(1, {Substrate: alice.address});253254 balanceQuartzAfter = await helper.balance.getSubstrate(alice.address);255256 console.log(257 '[Statemine -> Quartz] transaction fees on Quartz: %s USDT',258 helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, USDT_ASSET_METADATA_DECIMALS),259 );260 console.log(261 '[Statemine -> Quartz] transaction fees on Quartz: %s QTZ',262 helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzBefore),263 );264 // commission has not paid in USDT token265 expect(free).to.be.equal(TRANSFER_AMOUNT);266 // ... and parachain native token267 expect(balanceQuartzAfter == balanceQuartzBefore).to.be.true;268 });269270 itSub('Should connect and send USDT from Quartz to Statemine back', async ({helper}) => {271 const destination = {272 V2: {273 parents: 1,274 interior: {X2: [275 {276 Parachain: STATEMINE_CHAIN,277 },278 {279 AccountId32: {280 network: 'Any',281 id: alice.addressRaw,282 },283 },284 ]},285 },286 };287288 const relayFee = 400_000_000_000_000n;289 const currencies: [any, bigint][] = [290 [291 {292 ForeignAssetId: 0,293 },294 TRANSFER_AMOUNT,295 ],296 [297 {298 NativeAssetId: 'Parent',299 },300 relayFee,301 ],302 ];303304 const feeItem = 1;305306 await helper.xTokens.transferMulticurrencies(alice, currencies, feeItem, destination, 'Unlimited');307308 // the commission has been paid in parachain native token309 balanceQuartzFinal = await helper.balance.getSubstrate(alice.address);310 console.log('[Quartz -> Statemine] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzFinal));311 expect(balanceQuartzAfter > balanceQuartzFinal).to.be.true;312313 await usingStateminePlaygrounds(statemineUrl, async (helper) => {314 await helper.wait.newBlocks(3);315316 // The USDT token never paid fees. Its amount not changed from begin value.317 // Also check that xcm transfer has been succeeded318 expect((await helper.assets.account(USDT_ASSET_ID, alice.address))! == USDT_ASSET_AMOUNT).to.be.true;319 });320 });321322 itSub('Should connect and send Relay token to Quartz', async ({helper}) => {323 balanceBobBefore = await helper.balance.getSubstrate(bob.address);324 balanceBobRelayTokenBefore = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});325326 await usingRelayPlaygrounds(relayUrl, async (helper) => {327 const destination = {328 V2: {329 parents: 0,330 interior: {X1: {331 Parachain: QUARTZ_CHAIN,332 },333 },334 }};335336 const beneficiary = {337 V2: {338 parents: 0,339 interior: {X1: {340 AccountId32: {341 network: 'Any',342 id: bob.addressRaw,343 },344 }},345 },346 };347348 const assets = {349 V2: [350 {351 id: {352 Concrete: {353 parents: 0,354 interior: 'Here',355 },356 },357 fun: {358 Fungible: TRANSFER_AMOUNT_RELAY,359 },360 },361 ],362 };363364 const feeAssetItem = 0;365366 await helper.xcm.limitedReserveTransferAssets(bob, destination, beneficiary, assets, feeAssetItem, 'Unlimited');367 });368369 await helper.wait.newBlocks(3);370371 balanceBobAfter = await helper.balance.getSubstrate(bob.address);372 balanceBobRelayTokenAfter = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});373374 const wndFeeOnQuartz = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;375 const wndDiffOnQuartz = balanceBobRelayTokenAfter - balanceBobRelayTokenBefore;376 console.log(377 '[Relay (Westend) -> Quartz] transaction fees: %s QTZ',378 helper.util.bigIntToDecimals(balanceBobAfter - balanceBobBefore),379 );380 console.log(381 '[Relay (Westend) -> Quartz] transaction fees: %s WND',382 helper.util.bigIntToDecimals(wndFeeOnQuartz, STATEMINE_DECIMALS),383 );384 console.log('[Relay (Westend) -> Quartz] actually delivered: %s WND', wndDiffOnQuartz);385 expect(wndFeeOnQuartz == 0n, 'No incoming WND fees should be taken').to.be.true;386 expect(balanceBobBefore == balanceBobAfter, 'No incoming QTZ fees should be taken').to.be.true;387 });388389 itSub('Should connect and send Relay token back', async ({helper}) => {390 let relayTokenBalanceBefore: bigint;391 let relayTokenBalanceAfter: bigint;392 await usingRelayPlaygrounds(relayUrl, async (helper) => {393 relayTokenBalanceBefore = await helper.balance.getSubstrate(bob.address);394 });395396 const destination = {397 V2: {398 parents: 1,399 interior: {400 X1:{401 AccountId32: {402 network: 'Any',403 id: bob.addressRaw,404 },405 },406 },407 },408 };409410 const currencies: any = [411 [412 {413 NativeAssetId: 'Parent',414 },415 TRANSFER_AMOUNT_RELAY,416 ],417 ];418419 const feeItem = 0;420421 await helper.xTokens.transferMulticurrencies(bob, currencies, feeItem, destination, 'Unlimited');422423 balanceBobFinal = await helper.balance.getSubstrate(bob.address);424 console.log('[Quartz -> Relay (Westend)] transaction fees: %s QTZ', helper.util.bigIntToDecimals(balanceBobAfter - balanceBobFinal));425426 await usingRelayPlaygrounds(relayUrl, async (helper) => {427 await helper.wait.newBlocks(10);428 relayTokenBalanceAfter = await helper.balance.getSubstrate(bob.address);429430 const diff = relayTokenBalanceAfter - relayTokenBalanceBefore;431 console.log('[Quartz -> Relay (Westend)] actually delivered: %s WND', helper.util.bigIntToDecimals(diff, RELAY_DECIMALS));432 expect(diff > 0, 'Relay tokens was not delivered back').to.be.true;433 });434 });435});436437describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => {438 let alice: IKeyringPair;439 let randomAccount: IKeyringPair;440441 let balanceQuartzTokenInit: bigint;442 let balanceQuartzTokenMiddle: bigint;443 let balanceQuartzTokenFinal: bigint;444 let balanceKaruraTokenInit: bigint;445 let balanceKaruraTokenMiddle: bigint;446 let balanceKaruraTokenFinal: bigint;447 let balanceQuartzForeignTokenInit: bigint;448 let balanceQuartzForeignTokenMiddle: bigint;449 let balanceQuartzForeignTokenFinal: bigint;450451 // computed by a test transfer from prod Quartz to prod Karura.452 // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9453 // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block)454 const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n;455 const karuraEps = 8n * 10n ** 16n;456457 let karuraBackwardTransferAmount: bigint;458459 before(async () => {460 await usingPlaygrounds(async (helper, privateKey) => {461 alice = await privateKey('//Alice');462 [randomAccount] = await helper.arrange.createAccounts([0n], alice);463464 // Set the default version to wrap the first message to other chains.465 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);466 });467468 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {469 const destination = {470 V2: {471 parents: 1,472 interior: {473 X1: {474 Parachain: QUARTZ_CHAIN,475 },476 },477 },478 };479480 const metadata = {481 name: 'Quartz',482 symbol: 'QTZ',483 decimals: 18,484 minimalBalance: 1000000000000000000n,485 };486487 const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) =>488 hexToString(v.toJSON()['symbol'])) as string[];489490 if(!assets.includes('QTZ')) {491 await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);492 } else {493 console.log('QTZ token already registered on Karura assetRegistry pallet');494 }495 await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);496 balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);497 balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});498 });499500 await usingPlaygrounds(async (helper) => {501 await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);502 balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address);503 });504 });505506 itSub('Should connect and send QTZ to Karura', async ({helper}) => {507 const destination = {508 V2: {509 parents: 1,510 interior: {511 X1: {512 Parachain: KARURA_CHAIN,513 },514 },515 },516 };517518 const beneficiary = {519 V2: {520 parents: 0,521 interior: {522 X1: {523 AccountId32: {524 network: 'Any',525 id: randomAccount.addressRaw,526 },527 },528 },529 },530 };531532 const assets = {533 V2: [534 {535 id: {536 Concrete: {537 parents: 0,538 interior: 'Here',539 },540 },541 fun: {542 Fungible: TRANSFER_AMOUNT,543 },544 },545 ],546 };547548 const feeAssetItem = 0;549550 await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');551 balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);552553 const qtzFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;554 expect(qtzFees > 0n, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;555 console.log('[Quartz -> Karura] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));556557 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {558 await helper.wait.newBlocks(3);559560 balanceQuartzForeignTokenMiddle = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});561 balanceKaruraTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);562563 const karFees = balanceKaruraTokenInit - balanceKaruraTokenMiddle;564 const qtzIncomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenInit;565 karuraBackwardTransferAmount = qtzIncomeTransfer;566567 const karUnqFees = TRANSFER_AMOUNT - qtzIncomeTransfer;568569 console.log(570 '[Quartz -> Karura] transaction fees on Karura: %s KAR',571 helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),572 );573 console.log(574 '[Quartz -> Karura] transaction fees on Karura: %s QTZ',575 helper.util.bigIntToDecimals(karUnqFees),576 );577 console.log('[Quartz -> Karura] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));578 expect(karFees == 0n).to.be.true;579580 const bigintAbs = (n: bigint) => (n < 0n) ? -n : n;581582 expect(583 bigintAbs(karUnqFees - expectedKaruraIncomeFee) < karuraEps,584 'Karura took different income fee, check the Karura foreign asset config',585 ).to.be.true;586 });587 });588589 itSub('Should connect to Karura and send QTZ back', async ({helper}) => {590 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {591 const destination = {592 V2: {593 parents: 1,594 interior: {595 X2: [596 {Parachain: QUARTZ_CHAIN},597 {598 AccountId32: {599 network: 'Any',600 id: randomAccount.addressRaw,601 },602 },603 ],604 },605 },606 };607608 const id = {609 ForeignAsset: 0,610 };611612 await helper.xTokens.transfer(randomAccount, id, karuraBackwardTransferAmount, destination, 'Unlimited');613 balanceKaruraTokenFinal = await helper.balance.getSubstrate(randomAccount.address);614 balanceQuartzForeignTokenFinal = await helper.tokens.accounts(randomAccount.address, id);615616 const karFees = balanceKaruraTokenMiddle - balanceKaruraTokenFinal;617 const qtzOutcomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenFinal;618619 console.log(620 '[Karura -> Quartz] transaction fees on Karura: %s KAR',621 helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),622 );623 console.log('[Karura -> Quartz] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));624625 expect(karFees > 0, 'Negative fees KAR, looks like nothing was transferred').to.be.true;626 expect(qtzOutcomeTransfer == karuraBackwardTransferAmount).to.be.true;627 });628629 await helper.wait.newBlocks(3);630631 balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccount.address);632 const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;633 expect(actuallyDelivered > 0).to.be.true;634635 console.log('[Karura -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));636637 const qtzFees = karuraBackwardTransferAmount - actuallyDelivered;638 console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));639 expect(qtzFees == 0n).to.be.true;640 });641642 itSub('Karura can send only up to its balance', async ({helper}) => {643 // set Karura's sovereign account's balance644 const karuraBalance = 10000n * (10n ** QTZ_DECIMALS);645 const karuraSovereignAccount = helper.address.paraSiblingSovereignAccount(KARURA_CHAIN);646 await helper.getSudo().balance.setBalanceSubstrate(alice, karuraSovereignAccount, karuraBalance);647648 const moreThanKaruraHas = karuraBalance * 2n;649650 let targetAccountBalance = 0n;651 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);652653 const quartzMultilocation = {654 V2: {655 parents: 1,656 interior: {657 X1: {Parachain: QUARTZ_CHAIN},658 },659 },660 };661662 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(663 targetAccount.addressRaw,664 {665 Concrete: {666 parents: 0,667 interior: 'Here',668 },669 },670 moreThanKaruraHas,671 );672673 let maliciousXcmProgramSent: any;674 const maxWaitBlocks = 5;675676 // Try to trick Quartz677 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {678 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);679680 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);681 });682683 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash684 && event.outcome.isFailedToTransactAsset);685686 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);687 expect(targetAccountBalance).to.be.equal(0n);688689 // But Karura still can send the correct amount690 const validTransferAmount = karuraBalance / 2n;691 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(692 targetAccount.addressRaw,693 {694 Concrete: {695 parents: 0,696 interior: 'Here',697 },698 },699 validTransferAmount,700 );701702 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {703 await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);704 });705706 await helper.wait.newBlocks(maxWaitBlocks);707708 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);709 expect(targetAccountBalance).to.be.equal(validTransferAmount);710 });711712 itSub('Should not accept reserve transfer of QTZ from Karura', async ({helper}) => {713 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);714 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);715716 const quartzMultilocation = {717 V2: {718 parents: 1,719 interior: {720 X1: {721 Parachain: QUARTZ_CHAIN,722 },723 },724 },725 };726727 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(728 targetAccount.addressRaw,729 {730 Concrete: {731 parents: 1,732 interior: {733 X1: {734 Parachain: QUARTZ_CHAIN,735 },736 },737 },738 },739 testAmount,740 );741742 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(743 targetAccount.addressRaw,744 {745 Concrete: {746 parents: 0,747 interior: 'Here',748 },749 },750 testAmount,751 );752753 let maliciousXcmProgramFullIdSent: any;754 let maliciousXcmProgramHereIdSent: any;755 const maxWaitBlocks = 3;756757 // Try to trick Quartz using full QTZ identification758 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {759 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);760761 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);762 });763764 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash765 && event.outcome.isUntrustedReserveLocation);766767 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);768 expect(accountBalance).to.be.equal(0n);769770 // Try to trick Quartz using shortened QTZ identification771 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {772 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);773774 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);775 });776777 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash778 && event.outcome.isUntrustedReserveLocation);779780 accountBalance = await helper.balance.getSubstrate(targetAccount.address);781 expect(accountBalance).to.be.equal(0n);782 });783});784785// These tests are relevant only when786// the the corresponding foreign assets are not registered787describeXCM('[XCM] Integration test: Quartz rejects non-native tokens', () => {788 let alice: IKeyringPair;789 let alith: IKeyringPair;790791 const testAmount = 100_000_000_000n;792 let quartzParachainJunction;793 let quartzAccountJunction;794795 let quartzParachainMultilocation: any;796 let quartzAccountMultilocation: any;797 let quartzCombinedMultilocation: any;798799 let messageSent: any;800801 const maxWaitBlocks = 3;802803 before(async () => {804 await usingPlaygrounds(async (helper, privateKey) => {805 alice = await privateKey('//Alice');806807 quartzParachainJunction = {Parachain: QUARTZ_CHAIN};808 quartzAccountJunction = {809 AccountId32: {810 network: 'Any',811 id: alice.addressRaw,812 },813 };814815 quartzParachainMultilocation = {816 V2: {817 parents: 1,818 interior: {819 X1: quartzParachainJunction,820 },821 },822 };823824 quartzAccountMultilocation = {825 V2: {826 parents: 0,827 interior: {828 X1: quartzAccountJunction,829 },830 },831 };832833 quartzCombinedMultilocation = {834 V2: {835 parents: 1,836 interior: {837 X2: [quartzParachainJunction, quartzAccountJunction],838 },839 },840 };841842 // Set the default version to wrap the first message to other chains.843 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);844 });845846 // eslint-disable-next-line require-await847 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {848 alith = helper.account.alithAccount();849 });850 });851852 const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {853 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash854 && event.outcome.isFailedToTransactAsset);855 };856857 itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => {858 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {859 const id = {860 Token: 'KAR',861 };862 const destination = quartzCombinedMultilocation;863 await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');864865 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);866 });867868 await expectFailedToTransact(helper, messageSent);869 });870871 itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => {872 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {873 const id = 'SelfReserve';874 const destination = quartzCombinedMultilocation;875 await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');876877 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);878 });879880 await expectFailedToTransact(helper, messageSent);881 });882883 itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => {884 await usingShidenPlaygrounds(shidenUrl, async (helper) => {885 const destinationParachain = quartzParachainMultilocation;886 const beneficiary = quartzAccountMultilocation;887 const assets = {888 V2: [{889 id: {890 Concrete: {891 parents: 0,892 interior: 'Here',893 },894 },895 fun: {896 Fungible: testAmount,897 },898 }],899 };900 const feeAssetItem = 0;901902 await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [903 destinationParachain,904 beneficiary,905 assets,906 feeAssetItem,907 ]);908909 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);910 });911912 await expectFailedToTransact(helper, messageSent);913 });914});915916describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => {917 // Quartz constants918 let alice: IKeyringPair;919 let quartzAssetLocation;920921 let randomAccountQuartz: IKeyringPair;922 let randomAccountMoonriver: IKeyringPair;923924 // Moonriver constants925 let assetId: string;926927 const quartzAssetMetadata = {928 name: 'xcQuartz',929 symbol: 'xcQTZ',930 decimals: 18,931 isFrozen: false,932 minimalBalance: 1n,933 };934935 let balanceQuartzTokenInit: bigint;936 let balanceQuartzTokenMiddle: bigint;937 let balanceQuartzTokenFinal: bigint;938 let balanceForeignQtzTokenInit: bigint;939 let balanceForeignQtzTokenMiddle: bigint;940 let balanceForeignQtzTokenFinal: bigint;941 let balanceMovrTokenInit: bigint;942 let balanceMovrTokenMiddle: bigint;943 let balanceMovrTokenFinal: bigint;944945 before(async () => {946 await usingPlaygrounds(async (helper, privateKey) => {947 alice = await privateKey('//Alice');948 [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);949950 balanceForeignQtzTokenInit = 0n;951952 // Set the default version to wrap the first message to other chains.953 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);954 });955956 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {957 const alithAccount = helper.account.alithAccount();958 const baltatharAccount = helper.account.baltatharAccount();959 const dorothyAccount = helper.account.dorothyAccount();960961 randomAccountMoonriver = helper.account.create();962963 // >>> Sponsoring Dorothy >>>964 console.log('Sponsoring Dorothy.......');965 await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);966 console.log('Sponsoring Dorothy.......DONE');967 // <<< Sponsoring Dorothy <<<968969 quartzAssetLocation = {970 XCM: {971 parents: 1,972 interior: {X1: {Parachain: QUARTZ_CHAIN}},973 },974 };975 const existentialDeposit = 1n;976 const isSufficient = true;977 const unitsPerSecond = 1n;978 const numAssetsWeightHint = 0;979980 if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) {981 console.log('Quartz asset already registered on Moonriver');982 } else {983 const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({984 location: quartzAssetLocation,985 metadata: quartzAssetMetadata,986 existentialDeposit,987 isSufficient,988 unitsPerSecond,989 numAssetsWeightHint,990 });991992 console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);993994 await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);995 }996 // >>> Acquire Quartz AssetId Info on Moonriver >>>997 console.log('Acquire Quartz AssetId Info on Moonriver.......');998999 assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();10001001 console.log('QTZ asset ID is %s', assetId);1002 console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');1003 // >>> Acquire Quartz AssetId Info on Moonriver >>>10041005 // >>> Sponsoring random Account >>>1006 console.log('Sponsoring random Account.......');1007 await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);1008 console.log('Sponsoring random Account.......DONE');1009 // <<< Sponsoring random Account <<<10101011 balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address);1012 });10131014 await usingPlaygrounds(async (helper) => {1015 await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);1016 balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address);1017 });1018 });10191020 itSub('Should connect and send QTZ to Moonriver', async ({helper}) => {1021 const currencyId = {1022 NativeAssetId: 'Here',1023 };1024 const dest = {1025 V2: {1026 parents: 1,1027 interior: {1028 X2: [1029 {Parachain: MOONRIVER_CHAIN},1030 {AccountKey20: {network: 'Any', key: randomAccountMoonriver.address}},1031 ],1032 },1033 },1034 };1035 const amount = TRANSFER_AMOUNT;10361037 await helper.xTokens.transfer(randomAccountQuartz, currencyId, amount, dest, 'Unlimited');10381039 balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccountQuartz.address);1040 expect(balanceQuartzTokenMiddle < balanceQuartzTokenInit).to.be.true;10411042 const transactionFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;1043 console.log('[Quartz -> Moonriver] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(transactionFees));1044 expect(transactionFees > 0, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;10451046 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1047 await helper.wait.newBlocks(3);10481049 balanceMovrTokenMiddle = await helper.balance.getEthereum(randomAccountMoonriver.address);10501051 const movrFees = balanceMovrTokenInit - balanceMovrTokenMiddle;1052 console.log('[Quartz -> Moonriver] transaction fees on Moonriver: %s MOVR',helper.util.bigIntToDecimals(movrFees));1053 expect(movrFees == 0n).to.be.true;10541055 balanceForeignQtzTokenMiddle = (await helper.assets.account(assetId, randomAccountMoonriver.address))!; // BigInt(qtzRandomAccountAsset['balance']);1056 const qtzIncomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenInit;1057 console.log('[Quartz -> Moonriver] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));1058 expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true;1059 });1060 });10611062 itSub('Should connect to Moonriver and send QTZ back', async ({helper}) => {1063 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1064 const asset = {1065 V2: {1066 id: {1067 Concrete: {1068 parents: 1,1069 interior: {1070 X1: {Parachain: QUARTZ_CHAIN},1071 },1072 },1073 },1074 fun: {1075 Fungible: TRANSFER_AMOUNT,1076 },1077 },1078 };1079 const destination = {1080 V2: {1081 parents: 1,1082 interior: {1083 X2: [1084 {Parachain: QUARTZ_CHAIN},1085 {AccountId32: {network: 'Any', id: randomAccountQuartz.addressRaw}},1086 ],1087 },1088 },1089 };10901091 await helper.xTokens.transferMultiasset(randomAccountMoonriver, asset, destination, 'Unlimited');10921093 balanceMovrTokenFinal = await helper.balance.getEthereum(randomAccountMoonriver.address);10941095 const movrFees = balanceMovrTokenMiddle - balanceMovrTokenFinal;1096 console.log('[Moonriver -> Quartz] transaction fees on Moonriver: %s MOVR', helper.util.bigIntToDecimals(movrFees));1097 expect(movrFees > 0, 'Negative fees MOVR, looks like nothing was transferred').to.be.true;10981099 const qtzRandomAccountAsset = await helper.assets.account(assetId, randomAccountMoonriver.address);11001101 expect(qtzRandomAccountAsset).to.be.null;11021103 balanceForeignQtzTokenFinal = 0n;11041105 const qtzOutcomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenFinal;1106 console.log('[Quartz -> Moonriver] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));1107 expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;1108 });11091110 await helper.wait.newBlocks(3);11111112 balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccountQuartz.address);1113 const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;1114 expect(actuallyDelivered > 0).to.be.true;11151116 console.log('[Moonriver -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));11171118 const qtzFees = TRANSFER_AMOUNT - actuallyDelivered;1119 console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));1120 expect(qtzFees == 0n).to.be.true;1121 });11221123 itSub('Moonriver can send only up to its balance', async ({helper}) => {1124 // set Moonriver's sovereign account's balance1125 const moonriverBalance = 10000n * (10n ** QTZ_DECIMALS);1126 const moonriverSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONRIVER_CHAIN);1127 await helper.getSudo().balance.setBalanceSubstrate(alice, moonriverSovereignAccount, moonriverBalance);11281129 const moreThanMoonriverHas = moonriverBalance * 2n;11301131 let targetAccountBalance = 0n;1132 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);11331134 const quartzMultilocation = {1135 V2: {1136 parents: 1,1137 interior: {1138 X1: {Parachain: QUARTZ_CHAIN},1139 },1140 },1141 };11421143 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1144 targetAccount.addressRaw,1145 {1146 Concrete: {1147 parents: 0,1148 interior: 'Here',1149 },1150 },1151 moreThanMoonriverHas,1152 );11531154 let maliciousXcmProgramSent: any;1155 const maxWaitBlocks = 3;11561157 // Try to trick Quartz1158 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1159 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]);11601161 // Needed to bypass the call filter.1162 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1163 await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall);11641165 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1166 });11671168 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1169 && event.outcome.isFailedToTransactAsset);11701171 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1172 expect(targetAccountBalance).to.be.equal(0n);11731174 // But Moonriver still can send the correct amount1175 const validTransferAmount = moonriverBalance / 2n;1176 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1177 targetAccount.addressRaw,1178 {1179 Concrete: {1180 parents: 0,1181 interior: 'Here',1182 },1183 },1184 validTransferAmount,1185 );11861187 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1188 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, validXcmProgram]);11891190 // Needed to bypass the call filter.1191 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1192 await helper.fastDemocracy.executeProposal('Spend the correct amount of QTZ', batchCall);1193 });11941195 await helper.wait.newBlocks(maxWaitBlocks);11961197 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1198 expect(targetAccountBalance).to.be.equal(validTransferAmount);1199 });12001201 itSub('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => {1202 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1203 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);12041205 const quartzMultilocation = {1206 V2: {1207 parents: 1,1208 interior: {1209 X1: {1210 Parachain: QUARTZ_CHAIN,1211 },1212 },1213 },1214 };12151216 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1217 targetAccount.addressRaw,1218 {1219 Concrete: {1220 parents: 0,1221 interior: {1222 X1: {1223 Parachain: QUARTZ_CHAIN,1224 },1225 },1226 },1227 },1228 testAmount,1229 );12301231 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1232 targetAccount.addressRaw,1233 {1234 Concrete: {1235 parents: 0,1236 interior: 'Here',1237 },1238 },1239 testAmount,1240 );12411242 let maliciousXcmProgramFullIdSent: any;1243 let maliciousXcmProgramHereIdSent: any;1244 const maxWaitBlocks = 3;12451246 // Try to trick Quartz using full QTZ identification1247 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1248 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]);12491250 // Needed to bypass the call filter.1251 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1252 await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall);12531254 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1255 });12561257 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1258 && event.outcome.isUntrustedReserveLocation);12591260 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1261 expect(accountBalance).to.be.equal(0n);12621263 // Try to trick Quartz using shortened QTZ identification1264 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1265 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramHereId]);12661267 // Needed to bypass the call filter.1268 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1269 await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall);12701271 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1272 });12731274 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1275 && event.outcome.isUntrustedReserveLocation);12761277 accountBalance = await helper.balance.getSubstrate(targetAccount.address);1278 expect(accountBalance).to.be.equal(0n);1279 });1280});12811282describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => {1283 let alice: IKeyringPair;1284 let sender: IKeyringPair;12851286 const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden1287 const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden12881289 // Quartz -> Shiden1290 const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden1291 const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden1292 const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ1293 const qtzToShidenArrived = 7_998_196_000_000_000_000n; // 7.99 ... QTZ, Shiden takes a commision in foreign tokens12941295 // Shiden -> Quartz1296 const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ1297 const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 2.99 ... QTZ12981299 let balanceAfterQuartzToShidenXCM: bigint;13001301 before(async () => {1302 await usingPlaygrounds(async (helper, privateKey) => {1303 alice = await privateKey('//Alice');1304 [sender] = await helper.arrange.createAccounts([100n], alice);1305 console.log('sender', sender.address);13061307 // Set the default version to wrap the first message to other chains.1308 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);1309 });13101311 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1312 if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) {1313 console.log('1. Create foreign asset and metadata');1314 await helper.getSudo().assets.forceCreate(1315 alice,1316 QTZ_ASSET_ID_ON_SHIDEN,1317 alice.address,1318 QTZ_MINIMAL_BALANCE_ON_SHIDEN,1319 );13201321 await helper.assets.setMetadata(1322 alice,1323 QTZ_ASSET_ID_ON_SHIDEN,1324 'Quartz',1325 'QTZ',1326 Number(QTZ_DECIMALS),1327 );13281329 console.log('2. Register asset location on Shiden');1330 const assetLocation = {1331 V2: {1332 parents: 1,1333 interior: {1334 X1: {1335 Parachain: QUARTZ_CHAIN,1336 },1337 },1338 },1339 };13401341 await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);13421343 console.log('3. Set QTZ payment for XCM execution on Shiden');1344 await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);1345 } else {1346 console.log('QTZ is already registered on Shiden');1347 }1348 console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');1349 await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);1350 });1351 });13521353 itSub('Should connect and send QTZ to Shiden', async ({helper}) => {1354 const destination = {1355 V2: {1356 parents: 1,1357 interior: {1358 X1: {1359 Parachain: SHIDEN_CHAIN,1360 },1361 },1362 },1363 };13641365 const beneficiary = {1366 V2: {1367 parents: 0,1368 interior: {1369 X1: {1370 AccountId32: {1371 network: 'Any',1372 id: sender.addressRaw,1373 },1374 },1375 },1376 },1377 };13781379 const assets = {1380 V2: [1381 {1382 id: {1383 Concrete: {1384 parents: 0,1385 interior: 'Here',1386 },1387 },1388 fun: {1389 Fungible: qtzToShidenTransferred,1390 },1391 },1392 ],1393 };13941395 // Initial balance is 100 QTZ1396 const balanceBefore = await helper.balance.getSubstrate(sender.address);1397 console.log(`Initial balance is: ${balanceBefore}`);13981399 const feeAssetItem = 0;1400 await helper.xcm.limitedReserveTransferAssets(sender, destination, beneficiary, assets, feeAssetItem, 'Unlimited');14011402 // Balance after reserve transfer is less than 901403 balanceAfterQuartzToShidenXCM = await helper.balance.getSubstrate(sender.address);1404 console.log(`QTZ Balance on Quartz after XCM is: ${balanceAfterQuartzToShidenXCM}`);1405 console.log(`Quartz's QTZ commission is: ${balanceBefore-balanceAfterQuartzToShidenXCM}`);1406 expect(balanceBefore - balanceAfterQuartzToShidenXCM > 0).to.be.true;14071408 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1409 await helper.wait.newBlocks(3);1410 const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1411 const shidenBalance = await helper.balance.getSubstrate(sender.address);14121413 console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);1414 console.log(`Shiden's QTZ commission is: ${qtzToShidenTransferred-xcQTZbalance!}`);14151416 expect(xcQTZbalance).to.eq(qtzToShidenArrived);1417 // SHD balance does not changed:1418 expect(shidenBalance).to.eq(shidenInitialBalance);1419 });1420 });14211422 itSub('Should connect to Shiden and send QTZ back', async ({helper}) => {1423 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1424 const destination = {1425 V2: {1426 parents: 1,1427 interior: {1428 X1: {1429 Parachain: QUARTZ_CHAIN,1430 },1431 },1432 },1433 };14341435 const beneficiary = {1436 V2: {1437 parents: 0,1438 interior: {1439 X1: {1440 AccountId32: {1441 network: 'Any',1442 id: sender.addressRaw,1443 },1444 },1445 },1446 },1447 };14481449 const assets = {1450 V2: [1451 {1452 id: {1453 Concrete: {1454 parents: 1,1455 interior: {1456 X1: {1457 Parachain: QUARTZ_CHAIN,1458 },1459 },1460 },1461 },1462 fun: {1463 Fungible: qtzFromShidenTransfered,1464 },1465 },1466 ],1467 };14681469 // Initial balance is 1 SDN1470 const balanceSDNbefore = await helper.balance.getSubstrate(sender.address);1471 console.log(`SDN balance is: ${balanceSDNbefore}, it does not changed`);1472 expect(balanceSDNbefore).to.eq(shidenInitialBalance);14731474 const feeAssetItem = 0;1475 // this is non-standard polkadotXcm extension for Astar only. It calls InitiateReserveWithdraw1476 await helper.executeExtrinsic(sender, 'api.tx.polkadotXcm.reserveWithdrawAssets', [destination, beneficiary, assets, feeAssetItem]);14771478 // Balance after reserve transfer is less than 1 SDN1479 const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1480 const balanceSDN = await helper.balance.getSubstrate(sender.address);1481 console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);14821483 // Assert: xcQTZ balance correctly decreased1484 expect(xcQTZbalance).to.eq(qtzOnShidenLeft);1485 // Assert: SDN balance is 0.996...1486 expect(balanceSDN / (10n ** (SHIDEN_DECIMALS - 3n))).to.eq(996n);1487 });14881489 await helper.wait.newBlocks(3);1490 const balanceQTZ = await helper.balance.getSubstrate(sender.address);1491 console.log(`QTZ Balance on Quartz after XCM is: ${balanceQTZ}`);1492 expect(balanceQTZ).to.eq(balanceAfterQuartzToShidenXCM + qtzFromShidenTransfered);1493 });14941495 itSub('Shiden can send only up to its balance', async ({helper}) => {1496 // set Shiden's sovereign account's balance1497 const shidenBalance = 10000n * (10n ** QTZ_DECIMALS);1498 const shidenSovereignAccount = helper.address.paraSiblingSovereignAccount(SHIDEN_CHAIN);1499 await helper.getSudo().balance.setBalanceSubstrate(alice, shidenSovereignAccount, shidenBalance);15001501 const moreThanShidenHas = shidenBalance * 2n;15021503 let targetAccountBalance = 0n;1504 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);15051506 const quartzMultilocation = {1507 V2: {1508 parents: 1,1509 interior: {1510 X1: {Parachain: QUARTZ_CHAIN},1511 },1512 },1513 };15141515 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1516 targetAccount.addressRaw,1517 {1518 Concrete: {1519 parents: 0,1520 interior: 'Here',1521 },1522 },1523 moreThanShidenHas,1524 );15251526 let maliciousXcmProgramSent: any;1527 const maxWaitBlocks = 3;15281529 // Try to trick Quartz1530 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1531 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);15321533 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1534 });15351536 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1537 && event.outcome.isFailedToTransactAsset);15381539 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1540 expect(targetAccountBalance).to.be.equal(0n);15411542 // But Shiden still can send the correct amount1543 const validTransferAmount = shidenBalance / 2n;1544 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1545 targetAccount.addressRaw,1546 {1547 Concrete: {1548 parents: 0,1549 interior: 'Here',1550 },1551 },1552 validTransferAmount,1553 );15541555 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1556 await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);1557 });15581559 await helper.wait.newBlocks(maxWaitBlocks);15601561 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1562 expect(targetAccountBalance).to.be.equal(validTransferAmount);1563 });15641565 itSub('Should not accept reserve transfer of QTZ from Shiden', async ({helper}) => {1566 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1567 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);15681569 const quartzMultilocation = {1570 V2: {1571 parents: 1,1572 interior: {1573 X1: {1574 Parachain: QUARTZ_CHAIN,1575 },1576 },1577 },1578 };15791580 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1581 targetAccount.addressRaw,1582 {1583 Concrete: {1584 parents: 1,1585 interior: {1586 X1: {1587 Parachain: QUARTZ_CHAIN,1588 },1589 },1590 },1591 },1592 testAmount,1593 );15941595 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1596 targetAccount.addressRaw,1597 {1598 Concrete: {1599 parents: 0,1600 interior: 'Here',1601 },1602 },1603 testAmount,1604 );16051606 let maliciousXcmProgramFullIdSent: any;1607 let maliciousXcmProgramHereIdSent: any;1608 const maxWaitBlocks = 3;16091610 // Try to trick Quartz using full QTZ identification1611 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1612 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);16131614 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1615 });16161617 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1618 && event.outcome.isUntrustedReserveLocation);16191620 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1621 expect(accountBalance).to.be.equal(0n);16221623 // Try to trick Quartz using shortened QTZ identification1624 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1625 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);16261627 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1628 });16291630 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1631 && event.outcome.isUntrustedReserveLocation);16321633 accountBalance = await helper.balance.getSubstrate(targetAccount.address);1634 expect(accountBalance).to.be.equal(0n);1635 });1636});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 type {IKeyringPair} from '@polkadot/types/types';18import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util/index.js';19import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';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.js';21import {hexToString} from '@polkadot/util';22import {XcmTestHelper} from './xcm.types';2324const 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;4041const testHelper = new XcmTestHelper('quartz');4243describeXCM('[XCM] Integration test: Exchanging USDT with Statemine', () => {44 let alice: IKeyringPair;45 let bob: IKeyringPair;4647 let balanceStmnBefore: bigint;48 let balanceStmnAfter: bigint;4950 let balanceQuartzBefore: bigint;51 let balanceQuartzAfter: bigint;52 let balanceQuartzFinal: bigint;5354 let balanceBobBefore: bigint;55 let balanceBobAfter: bigint;56 let balanceBobFinal: bigint;5758 let balanceBobRelayTokenBefore: bigint;59 let balanceBobRelayTokenAfter: bigint;6061 let usdtCollectionId: number;62 let relayCollectionId: number;6364 before(async () => {65 await usingPlaygrounds(async (helper, privateKey) => {66 alice = await privateKey('//Alice');67 bob = await privateKey('//Bob'); // sovereign account on Statemine(t) funds donor6869 // Set the default version to wrap the first message to other chains.70 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);7172 relayCollectionId = await testHelper.registerRelayNativeTokenOnUnique(alice);73 });7475 await usingRelayPlaygrounds(relayUrl, async (helper) => {76 // Fund accounts on Statemine(t)77 await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, alice.addressRaw, FUNDING_AMOUNT);78 await helper.xcm.teleportNativeAsset(alice, STATEMINE_CHAIN, bob.addressRaw, FUNDING_AMOUNT);79 });8081 await usingStateminePlaygrounds(statemineUrl, async (helper) => {82 const assetInfo = await helper.assets.assetInfo(USDT_ASSET_ID);83 if(assetInfo == null) {84 await helper.assets.create(85 alice,86 USDT_ASSET_ID,87 alice.address,88 USDT_ASSET_METADATA_MINIMAL_BALANCE,89 );90 await helper.assets.setMetadata(91 alice,92 USDT_ASSET_ID,93 USDT_ASSET_METADATA_NAME,94 USDT_ASSET_METADATA_DESCRIPTION,95 USDT_ASSET_METADATA_DECIMALS,96 );97 } else {98 console.log('The USDT asset is already registered on AssetHub');99 }100101 await helper.assets.mint(102 alice,103 USDT_ASSET_ID,104 alice.address,105 USDT_ASSET_AMOUNT,106 );107108 const sovereignFundingAmount = 3_500_000_000n;109110 // funding parachain sovereing account on Statemine(t).111 // The sovereign account should be created before any action112 // (the assets pallet on Statemine(t) check if the sovereign account exists)113 const parachainSovereingAccount = helper.address.paraSiblingSovereignAccount(QUARTZ_CHAIN);114 await helper.balance.transferToSubstrate(bob, parachainSovereingAccount, sovereignFundingAmount);115 });116117118 await usingPlaygrounds(async (helper) => {119 const location = {120 parents: 1,121 interior: {X3: [122 {123 Parachain: STATEMINE_CHAIN,124 },125 {126 PalletInstance: STATEMINE_PALLET_INSTANCE,127 },128 {129 GeneralIndex: USDT_ASSET_ID,130 },131 ]},132 };133134 if(await helper.foreignAssets.foreignCollectionId(location) == null) {135 const tokenPrefix = USDT_ASSET_METADATA_NAME;136 await helper.getSudo().foreignAssets.register(alice, location, USDT_ASSET_METADATA_NAME, tokenPrefix, {Fungible: USDT_ASSET_METADATA_DECIMALS});137 } else {138 console.log('Foreign collection is already registered on Quartz');139 }140141 balanceQuartzBefore = await helper.balance.getSubstrate(alice.address);142 usdtCollectionId = await helper.foreignAssets.foreignCollectionId(location);143 });144145146 // Providing the relay currency to the quartz sender account147 // (fee for USDT XCM are paid in relay tokens)148 await usingRelayPlaygrounds(relayUrl, async (helper) => {149 const destination = {150 V2: {151 parents: 0,152 interior: {X1: {153 Parachain: QUARTZ_CHAIN,154 },155 },156 }};157158 const beneficiary = {159 V2: {160 parents: 0,161 interior: {X1: {162 AccountId32: {163 network: 'Any',164 id: alice.addressRaw,165 },166 }},167 },168 };169170 const assets = {171 V2: [172 {173 id: {174 Concrete: {175 parents: 0,176 interior: 'Here',177 },178 },179 fun: {180 Fungible: TRANSFER_AMOUNT_RELAY,181 },182 },183 ],184 };185186 const feeAssetItem = 0;187188 await helper.xcm.limitedReserveTransferAssets(alice, destination, beneficiary, assets, feeAssetItem, 'Unlimited');189 });190191 });192193 itSub('Should connect and send USDT from Statemine to Quartz', async ({helper}) => {194 await usingStateminePlaygrounds(statemineUrl, async (helper) => {195 const dest = {196 V2: {197 parents: 1,198 interior: {X1: {199 Parachain: QUARTZ_CHAIN,200 },201 },202 }};203204 const beneficiary = {205 V2: {206 parents: 0,207 interior: {X1: {208 AccountId32: {209 network: 'Any',210 id: alice.addressRaw,211 },212 }},213 },214 };215216 const assets = {217 V2: [218 {219 id: {220 Concrete: {221 parents: 0,222 interior: {223 X2: [224 {225 PalletInstance: STATEMINE_PALLET_INSTANCE,226 },227 {228 GeneralIndex: USDT_ASSET_ID,229 },230 ]},231 },232 },233 fun: {234 Fungible: TRANSFER_AMOUNT,235 },236 },237 ],238 };239240 const feeAssetItem = 0;241242 balanceStmnBefore = await helper.balance.getSubstrate(alice.address);243 await helper.xcm.limitedReserveTransferAssets(alice, dest, beneficiary, assets, feeAssetItem, 'Unlimited');244245 balanceStmnAfter = await helper.balance.getSubstrate(alice.address);246247 // common good parachain take commission in it native token248 console.log(249 '[Statemine -> Quartz] transaction fees on Statemine: %s WND',250 helper.util.bigIntToDecimals(balanceStmnBefore - balanceStmnAfter, STATEMINE_DECIMALS),251 );252 expect(balanceStmnBefore > balanceStmnAfter).to.be.true;253254 });255256257 // ensure that asset has been delivered258 await helper.wait.newBlocks(3);259260 const free = await helper.ft.getBalance(usdtCollectionId, {Substrate: alice.address});261262 balanceQuartzAfter = await helper.balance.getSubstrate(alice.address);263264 console.log(265 '[Statemine -> Quartz] transaction fees on Quartz: %s USDT',266 helper.util.bigIntToDecimals(TRANSFER_AMOUNT - free, USDT_ASSET_METADATA_DECIMALS),267 );268 console.log(269 '[Statemine -> Quartz] transaction fees on Quartz: %s QTZ',270 helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzBefore),271 );272 // commission has not paid in USDT token273 expect(free).to.be.equal(TRANSFER_AMOUNT);274 // ... and parachain native token275 expect(balanceQuartzAfter == balanceQuartzBefore).to.be.true;276 });277278 itSub('Should connect and send USDT from Quartz to Statemine back', async ({helper}) => {279 const destination = {280 V2: {281 parents: 1,282 interior: {X2: [283 {284 Parachain: STATEMINE_CHAIN,285 },286 {287 AccountId32: {288 network: 'Any',289 id: alice.addressRaw,290 },291 },292 ]},293 },294 };295296 const relayFee = 400_000_000_000_000n;297 const currencies: [any, bigint][] = [298 [299 usdtCollectionId,300 TRANSFER_AMOUNT,301 ],302 [303 relayCollectionId,304 relayFee,305 ],306 ];307308 const feeItem = 1;309310 await helper.xTokens.transferMulticurrencies(alice, currencies, feeItem, destination, 'Unlimited');311312 // the commission has been paid in parachain native token313 balanceQuartzFinal = await helper.balance.getSubstrate(alice.address);314 console.log('[Quartz -> Statemine] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(balanceQuartzAfter - balanceQuartzFinal));315 expect(balanceQuartzAfter > balanceQuartzFinal).to.be.true;316317 await usingStateminePlaygrounds(statemineUrl, async (helper) => {318 await helper.wait.newBlocks(3);319320 // The USDT token never paid fees. Its amount not changed from begin value.321 // Also check that xcm transfer has been succeeded322 expect((await helper.assets.account(USDT_ASSET_ID, alice.address))! == USDT_ASSET_AMOUNT).to.be.true;323 });324 });325326 itSub('Should connect and send Relay token to Quartz', async ({helper}) => {327 balanceBobBefore = await helper.balance.getSubstrate(bob.address);328 balanceBobRelayTokenBefore = await helper.ft.getBalance(relayCollectionId, {Substrate: bob.address});329330 await usingRelayPlaygrounds(relayUrl, async (helper) => {331 const destination = {332 V2: {333 parents: 0,334 interior: {X1: {335 Parachain: QUARTZ_CHAIN,336 },337 },338 }};339340 const beneficiary = {341 V2: {342 parents: 0,343 interior: {X1: {344 AccountId32: {345 network: 'Any',346 id: bob.addressRaw,347 },348 }},349 },350 };351352 const assets = {353 V2: [354 {355 id: {356 Concrete: {357 parents: 0,358 interior: 'Here',359 },360 },361 fun: {362 Fungible: TRANSFER_AMOUNT_RELAY,363 },364 },365 ],366 };367368 const feeAssetItem = 0;369370 await helper.xcm.limitedReserveTransferAssets(bob, destination, beneficiary, assets, feeAssetItem, 'Unlimited');371 });372373 await helper.wait.newBlocks(3);374375 balanceBobAfter = await helper.balance.getSubstrate(bob.address);376 balanceBobRelayTokenAfter = await helper.ft.getBalance(relayCollectionId, {Substrate: bob.address});377378 const wndFeeOnQuartz = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;379 const wndDiffOnQuartz = balanceBobRelayTokenAfter - balanceBobRelayTokenBefore;380 console.log(381 '[Relay (Westend) -> Quartz] transaction fees: %s QTZ',382 helper.util.bigIntToDecimals(balanceBobAfter - balanceBobBefore),383 );384 console.log(385 '[Relay (Westend) -> Quartz] transaction fees: %s WND',386 helper.util.bigIntToDecimals(wndFeeOnQuartz, STATEMINE_DECIMALS),387 );388 console.log('[Relay (Westend) -> Quartz] actually delivered: %s WND', wndDiffOnQuartz);389 expect(wndFeeOnQuartz == 0n, 'No incoming WND fees should be taken').to.be.true;390 expect(balanceBobBefore == balanceBobAfter, 'No incoming QTZ fees should be taken').to.be.true;391 });392393 itSub('Should connect and send Relay token back', async ({helper}) => {394 let relayTokenBalanceBefore: bigint;395 let relayTokenBalanceAfter: bigint;396 await usingRelayPlaygrounds(relayUrl, async (helper) => {397 relayTokenBalanceBefore = await helper.balance.getSubstrate(bob.address);398 });399400 const destination = {401 V2: {402 parents: 1,403 interior: {404 X1:{405 AccountId32: {406 network: 'Any',407 id: bob.addressRaw,408 },409 },410 },411 },412 };413414 const currencies: any = [415 [416 relayCollectionId,417 TRANSFER_AMOUNT_RELAY,418 ],419 ];420421 const feeItem = 0;422423 await helper.xTokens.transferMulticurrencies(bob, currencies, feeItem, destination, 'Unlimited');424425 balanceBobFinal = await helper.balance.getSubstrate(bob.address);426 console.log('[Quartz -> Relay (Westend)] transaction fees: %s QTZ', helper.util.bigIntToDecimals(balanceBobAfter - balanceBobFinal));427428 await usingRelayPlaygrounds(relayUrl, async (helper) => {429 await helper.wait.newBlocks(10);430 relayTokenBalanceAfter = await helper.balance.getSubstrate(bob.address);431432 const diff = relayTokenBalanceAfter - relayTokenBalanceBefore;433 console.log('[Quartz -> Relay (Westend)] actually delivered: %s WND', helper.util.bigIntToDecimals(diff, RELAY_DECIMALS));434 expect(diff > 0, 'Relay tokens was not delivered back').to.be.true;435 });436 });437});438439describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => {440 let alice: IKeyringPair;441 let randomAccount: IKeyringPair;442443 let balanceQuartzTokenInit: bigint;444 let balanceQuartzTokenMiddle: bigint;445 let balanceQuartzTokenFinal: bigint;446 let balanceKaruraTokenInit: bigint;447 let balanceKaruraTokenMiddle: bigint;448 let balanceKaruraTokenFinal: bigint;449 let balanceQuartzForeignTokenInit: bigint;450 let balanceQuartzForeignTokenMiddle: bigint;451 let balanceQuartzForeignTokenFinal: bigint;452453 // computed by a test transfer from prod Quartz to prod Karura.454 // 2 QTZ sent https://quartz.subscan.io/xcm_message/kusama-f60d821b049f8835a3005ce7102285006f5b61e9455 // 1.919176000000000000 QTZ received (you can check Karura's chain state in the corresponding block)456 const expectedKaruraIncomeFee = 2000000000000000000n - 1919176000000000000n;457 const karuraEps = 8n * 10n ** 16n;458459 let karuraBackwardTransferAmount: bigint;460461 before(async () => {462 await usingPlaygrounds(async (helper, privateKey) => {463 alice = await privateKey('//Alice');464 [randomAccount] = await helper.arrange.createAccounts([0n], alice);465466 // Set the default version to wrap the first message to other chains.467 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);468 });469470 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {471 const destination = {472 V2: {473 parents: 1,474 interior: {475 X1: {476 Parachain: QUARTZ_CHAIN,477 },478 },479 },480 };481482 const metadata = {483 name: 'Quartz',484 symbol: 'QTZ',485 decimals: 18,486 minimalBalance: 1000000000000000000n,487 };488489 const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) =>490 hexToString(v.toJSON()['symbol'])) as string[];491492 if(!assets.includes('QTZ')) {493 await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);494 } else {495 console.log('QTZ token already registered on Karura assetRegistry pallet');496 }497 await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);498 balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);499 balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});500 });501502 await usingPlaygrounds(async (helper) => {503 await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);504 balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccount.address);505 });506 });507508 itSub('Should connect and send QTZ to Karura', async ({helper}) => {509 const destination = {510 V2: {511 parents: 1,512 interior: {513 X1: {514 Parachain: KARURA_CHAIN,515 },516 },517 },518 };519520 const beneficiary = {521 V2: {522 parents: 0,523 interior: {524 X1: {525 AccountId32: {526 network: 'Any',527 id: randomAccount.addressRaw,528 },529 },530 },531 },532 };533534 const assets = {535 V2: [536 {537 id: {538 Concrete: {539 parents: 0,540 interior: 'Here',541 },542 },543 fun: {544 Fungible: TRANSFER_AMOUNT,545 },546 },547 ],548 };549550 const feeAssetItem = 0;551552 await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');553 balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);554555 const qtzFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;556 expect(qtzFees > 0n, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;557 console.log('[Quartz -> Karura] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));558559 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {560 await helper.wait.newBlocks(3);561562 balanceQuartzForeignTokenMiddle = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});563 balanceKaruraTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);564565 const karFees = balanceKaruraTokenInit - balanceKaruraTokenMiddle;566 const qtzIncomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenInit;567 karuraBackwardTransferAmount = qtzIncomeTransfer;568569 const karUnqFees = TRANSFER_AMOUNT - qtzIncomeTransfer;570571 console.log(572 '[Quartz -> Karura] transaction fees on Karura: %s KAR',573 helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),574 );575 console.log(576 '[Quartz -> Karura] transaction fees on Karura: %s QTZ',577 helper.util.bigIntToDecimals(karUnqFees),578 );579 console.log('[Quartz -> Karura] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));580 expect(karFees == 0n).to.be.true;581582 const bigintAbs = (n: bigint) => (n < 0n) ? -n : n;583584 expect(585 bigintAbs(karUnqFees - expectedKaruraIncomeFee) < karuraEps,586 'Karura took different income fee, check the Karura foreign asset config',587 ).to.be.true;588 });589 });590591 itSub('Should connect to Karura and send QTZ back', async ({helper}) => {592 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {593 const destination = {594 V2: {595 parents: 1,596 interior: {597 X2: [598 {Parachain: QUARTZ_CHAIN},599 {600 AccountId32: {601 network: 'Any',602 id: randomAccount.addressRaw,603 },604 },605 ],606 },607 },608 };609610 const id = {611 ForeignAsset: 0,612 };613614 await helper.xTokens.transfer(randomAccount, id, karuraBackwardTransferAmount, destination, 'Unlimited');615 balanceKaruraTokenFinal = await helper.balance.getSubstrate(randomAccount.address);616 balanceQuartzForeignTokenFinal = await helper.tokens.accounts(randomAccount.address, id);617618 const karFees = balanceKaruraTokenMiddle - balanceKaruraTokenFinal;619 const qtzOutcomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenFinal;620621 console.log(622 '[Karura -> Quartz] transaction fees on Karura: %s KAR',623 helper.util.bigIntToDecimals(karFees, KARURA_DECIMALS),624 );625 console.log('[Karura -> Quartz] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));626627 expect(karFees > 0, 'Negative fees KAR, looks like nothing was transferred').to.be.true;628 expect(qtzOutcomeTransfer == karuraBackwardTransferAmount).to.be.true;629 });630631 await helper.wait.newBlocks(3);632633 balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccount.address);634 const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;635 expect(actuallyDelivered > 0).to.be.true;636637 console.log('[Karura -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));638639 const qtzFees = karuraBackwardTransferAmount - actuallyDelivered;640 console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));641 expect(qtzFees == 0n).to.be.true;642 });643644 itSub('Karura can send only up to its balance', async ({helper}) => {645 // set Karura's sovereign account's balance646 const karuraBalance = 10000n * (10n ** QTZ_DECIMALS);647 const karuraSovereignAccount = helper.address.paraSiblingSovereignAccount(KARURA_CHAIN);648 await helper.getSudo().balance.setBalanceSubstrate(alice, karuraSovereignAccount, karuraBalance);649650 const moreThanKaruraHas = karuraBalance * 2n;651652 let targetAccountBalance = 0n;653 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);654655 const quartzMultilocation = {656 V2: {657 parents: 1,658 interior: {659 X1: {Parachain: QUARTZ_CHAIN},660 },661 },662 };663664 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(665 targetAccount.addressRaw,666 {667 Concrete: {668 parents: 0,669 interior: 'Here',670 },671 },672 moreThanKaruraHas,673 );674675 let maliciousXcmProgramSent: any;676 const maxWaitBlocks = 5;677678 // Try to trick Quartz679 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {680 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);681682 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);683 });684685 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash686 && event.outcome.isFailedToTransactAsset);687688 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);689 expect(targetAccountBalance).to.be.equal(0n);690691 // But Karura still can send the correct amount692 const validTransferAmount = karuraBalance / 2n;693 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(694 targetAccount.addressRaw,695 {696 Concrete: {697 parents: 0,698 interior: 'Here',699 },700 },701 validTransferAmount,702 );703704 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {705 await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);706 });707708 await helper.wait.newBlocks(maxWaitBlocks);709710 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);711 expect(targetAccountBalance).to.be.equal(validTransferAmount);712 });713714 itSub('Should not accept reserve transfer of QTZ from Karura', async ({helper}) => {715 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);716 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);717718 const quartzMultilocation = {719 V2: {720 parents: 1,721 interior: {722 X1: {723 Parachain: QUARTZ_CHAIN,724 },725 },726 },727 };728729 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(730 targetAccount.addressRaw,731 {732 Concrete: {733 parents: 1,734 interior: {735 X1: {736 Parachain: QUARTZ_CHAIN,737 },738 },739 },740 },741 testAmount,742 );743744 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(745 targetAccount.addressRaw,746 {747 Concrete: {748 parents: 0,749 interior: 'Here',750 },751 },752 testAmount,753 );754755 let maliciousXcmProgramFullIdSent: any;756 let maliciousXcmProgramHereIdSent: any;757 const maxWaitBlocks = 3;758759 // Try to trick Quartz using full QTZ identification760 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {761 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);762763 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);764 });765766 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash767 && event.outcome.isUntrustedReserveLocation);768769 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);770 expect(accountBalance).to.be.equal(0n);771772 // Try to trick Quartz using shortened QTZ identification773 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {774 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);775776 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);777 });778779 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash780 && event.outcome.isUntrustedReserveLocation);781782 accountBalance = await helper.balance.getSubstrate(targetAccount.address);783 expect(accountBalance).to.be.equal(0n);784 });785});786787// These tests are relevant only when788// the the corresponding foreign assets are not registered789describeXCM('[XCM] Integration test: Quartz rejects non-native tokens', () => {790 let alice: IKeyringPair;791 let alith: IKeyringPair;792793 const testAmount = 100_000_000_000n;794 let quartzParachainJunction;795 let quartzAccountJunction;796797 let quartzParachainMultilocation: any;798 let quartzAccountMultilocation: any;799 let quartzCombinedMultilocation: any;800801 let messageSent: any;802803 const maxWaitBlocks = 3;804805 before(async () => {806 await usingPlaygrounds(async (helper, privateKey) => {807 alice = await privateKey('//Alice');808809 quartzParachainJunction = {Parachain: QUARTZ_CHAIN};810 quartzAccountJunction = {811 AccountId32: {812 network: 'Any',813 id: alice.addressRaw,814 },815 };816817 quartzParachainMultilocation = {818 V2: {819 parents: 1,820 interior: {821 X1: quartzParachainJunction,822 },823 },824 };825826 quartzAccountMultilocation = {827 V2: {828 parents: 0,829 interior: {830 X1: quartzAccountJunction,831 },832 },833 };834835 quartzCombinedMultilocation = {836 V2: {837 parents: 1,838 interior: {839 X2: [quartzParachainJunction, quartzAccountJunction],840 },841 },842 };843844 // Set the default version to wrap the first message to other chains.845 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);846 });847848 // eslint-disable-next-line require-await849 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {850 alith = helper.account.alithAccount();851 });852 });853854 const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {855 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash856 && event.outcome.isFailedToTransactAsset);857 };858859 itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => {860 await usingKaruraPlaygrounds(karuraUrl, async (helper) => {861 const id = {862 Token: 'KAR',863 };864 const destination = quartzCombinedMultilocation;865 await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');866867 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);868 });869870 await expectFailedToTransact(helper, messageSent);871 });872873 itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => {874 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {875 const id = 'SelfReserve';876 const destination = quartzCombinedMultilocation;877 await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');878879 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);880 });881882 await expectFailedToTransact(helper, messageSent);883 });884885 itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => {886 await usingShidenPlaygrounds(shidenUrl, async (helper) => {887 const destinationParachain = quartzParachainMultilocation;888 const beneficiary = quartzAccountMultilocation;889 const assets = {890 V2: [{891 id: {892 Concrete: {893 parents: 0,894 interior: 'Here',895 },896 },897 fun: {898 Fungible: testAmount,899 },900 }],901 };902 const feeAssetItem = 0;903904 await helper.executeExtrinsic(alice, 'api.tx.polkadotXcm.reserveWithdrawAssets', [905 destinationParachain,906 beneficiary,907 assets,908 feeAssetItem,909 ]);910911 messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);912 });913914 await expectFailedToTransact(helper, messageSent);915 });916});917918describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => {919 // Quartz constants920 let alice: IKeyringPair;921 let quartzAssetLocation;922923 let randomAccountQuartz: IKeyringPair;924 let randomAccountMoonriver: IKeyringPair;925926 // Moonriver constants927 let assetId: string;928929 const quartzAssetMetadata = {930 name: 'xcQuartz',931 symbol: 'xcQTZ',932 decimals: 18,933 isFrozen: false,934 minimalBalance: 1n,935 };936937 let balanceQuartzTokenInit: bigint;938 let balanceQuartzTokenMiddle: bigint;939 let balanceQuartzTokenFinal: bigint;940 let balanceForeignQtzTokenInit: bigint;941 let balanceForeignQtzTokenMiddle: bigint;942 let balanceForeignQtzTokenFinal: bigint;943 let balanceMovrTokenInit: bigint;944 let balanceMovrTokenMiddle: bigint;945 let balanceMovrTokenFinal: bigint;946947 before(async () => {948 await usingPlaygrounds(async (helper, privateKey) => {949 alice = await privateKey('//Alice');950 [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);951952 balanceForeignQtzTokenInit = 0n;953954 // Set the default version to wrap the first message to other chains.955 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);956 });957958 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {959 const alithAccount = helper.account.alithAccount();960 const baltatharAccount = helper.account.baltatharAccount();961 const dorothyAccount = helper.account.dorothyAccount();962963 randomAccountMoonriver = helper.account.create();964965 // >>> Sponsoring Dorothy >>>966 console.log('Sponsoring Dorothy.......');967 await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);968 console.log('Sponsoring Dorothy.......DONE');969 // <<< Sponsoring Dorothy <<<970971 quartzAssetLocation = {972 XCM: {973 parents: 1,974 interior: {X1: {Parachain: QUARTZ_CHAIN}},975 },976 };977 const existentialDeposit = 1n;978 const isSufficient = true;979 const unitsPerSecond = 1n;980 const numAssetsWeightHint = 0;981982 if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) {983 console.log('Quartz asset already registered on Moonriver');984 } else {985 const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({986 location: quartzAssetLocation,987 metadata: quartzAssetMetadata,988 existentialDeposit,989 isSufficient,990 unitsPerSecond,991 numAssetsWeightHint,992 });993994 console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);995996 await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);997 }998 // >>> Acquire Quartz AssetId Info on Moonriver >>>999 console.log('Acquire Quartz AssetId Info on Moonriver.......');10001001 assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();10021003 console.log('QTZ asset ID is %s', assetId);1004 console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');1005 // >>> Acquire Quartz AssetId Info on Moonriver >>>10061007 // >>> Sponsoring random Account >>>1008 console.log('Sponsoring random Account.......');1009 await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);1010 console.log('Sponsoring random Account.......DONE');1011 // <<< Sponsoring random Account <<<10121013 balanceMovrTokenInit = await helper.balance.getEthereum(randomAccountMoonriver.address);1014 });10151016 await usingPlaygrounds(async (helper) => {1017 await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);1018 balanceQuartzTokenInit = await helper.balance.getSubstrate(randomAccountQuartz.address);1019 });1020 });10211022 itSub('Should connect and send QTZ to Moonriver', async ({helper}) => {1023 const currencyId = {1024 NativeAssetId: 'Here',1025 };1026 const dest = {1027 V2: {1028 parents: 1,1029 interior: {1030 X2: [1031 {Parachain: MOONRIVER_CHAIN},1032 {AccountKey20: {network: 'Any', key: randomAccountMoonriver.address}},1033 ],1034 },1035 },1036 };1037 const amount = TRANSFER_AMOUNT;10381039 await helper.xTokens.transfer(randomAccountQuartz, currencyId, amount, dest, 'Unlimited');10401041 balanceQuartzTokenMiddle = await helper.balance.getSubstrate(randomAccountQuartz.address);1042 expect(balanceQuartzTokenMiddle < balanceQuartzTokenInit).to.be.true;10431044 const transactionFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT;1045 console.log('[Quartz -> Moonriver] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(transactionFees));1046 expect(transactionFees > 0, 'Negative fees QTZ, looks like nothing was transferred').to.be.true;10471048 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1049 await helper.wait.newBlocks(3);10501051 balanceMovrTokenMiddle = await helper.balance.getEthereum(randomAccountMoonriver.address);10521053 const movrFees = balanceMovrTokenInit - balanceMovrTokenMiddle;1054 console.log('[Quartz -> Moonriver] transaction fees on Moonriver: %s MOVR',helper.util.bigIntToDecimals(movrFees));1055 expect(movrFees == 0n).to.be.true;10561057 balanceForeignQtzTokenMiddle = (await helper.assets.account(assetId, randomAccountMoonriver.address))!; // BigInt(qtzRandomAccountAsset['balance']);1058 const qtzIncomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenInit;1059 console.log('[Quartz -> Moonriver] income %s QTZ', helper.util.bigIntToDecimals(qtzIncomeTransfer));1060 expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true;1061 });1062 });10631064 itSub('Should connect to Moonriver and send QTZ back', async ({helper}) => {1065 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1066 const asset = {1067 V2: {1068 id: {1069 Concrete: {1070 parents: 1,1071 interior: {1072 X1: {Parachain: QUARTZ_CHAIN},1073 },1074 },1075 },1076 fun: {1077 Fungible: TRANSFER_AMOUNT,1078 },1079 },1080 };1081 const destination = {1082 V2: {1083 parents: 1,1084 interior: {1085 X2: [1086 {Parachain: QUARTZ_CHAIN},1087 {AccountId32: {network: 'Any', id: randomAccountQuartz.addressRaw}},1088 ],1089 },1090 },1091 };10921093 await helper.xTokens.transferMultiasset(randomAccountMoonriver, asset, destination, 'Unlimited');10941095 balanceMovrTokenFinal = await helper.balance.getEthereum(randomAccountMoonriver.address);10961097 const movrFees = balanceMovrTokenMiddle - balanceMovrTokenFinal;1098 console.log('[Moonriver -> Quartz] transaction fees on Moonriver: %s MOVR', helper.util.bigIntToDecimals(movrFees));1099 expect(movrFees > 0, 'Negative fees MOVR, looks like nothing was transferred').to.be.true;11001101 const qtzRandomAccountAsset = await helper.assets.account(assetId, randomAccountMoonriver.address);11021103 expect(qtzRandomAccountAsset).to.be.null;11041105 balanceForeignQtzTokenFinal = 0n;11061107 const qtzOutcomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenFinal;1108 console.log('[Quartz -> Moonriver] outcome %s QTZ', helper.util.bigIntToDecimals(qtzOutcomeTransfer));1109 expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;1110 });11111112 await helper.wait.newBlocks(3);11131114 balanceQuartzTokenFinal = await helper.balance.getSubstrate(randomAccountQuartz.address);1115 const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle;1116 expect(actuallyDelivered > 0).to.be.true;11171118 console.log('[Moonriver -> Quartz] actually delivered %s QTZ', helper.util.bigIntToDecimals(actuallyDelivered));11191120 const qtzFees = TRANSFER_AMOUNT - actuallyDelivered;1121 console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', helper.util.bigIntToDecimals(qtzFees));1122 expect(qtzFees == 0n).to.be.true;1123 });11241125 itSub('Moonriver can send only up to its balance', async ({helper}) => {1126 // set Moonriver's sovereign account's balance1127 const moonriverBalance = 10000n * (10n ** QTZ_DECIMALS);1128 const moonriverSovereignAccount = helper.address.paraSiblingSovereignAccount(MOONRIVER_CHAIN);1129 await helper.getSudo().balance.setBalanceSubstrate(alice, moonriverSovereignAccount, moonriverBalance);11301131 const moreThanMoonriverHas = moonriverBalance * 2n;11321133 let targetAccountBalance = 0n;1134 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);11351136 const quartzMultilocation = {1137 V2: {1138 parents: 1,1139 interior: {1140 X1: {Parachain: QUARTZ_CHAIN},1141 },1142 },1143 };11441145 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1146 targetAccount.addressRaw,1147 {1148 Concrete: {1149 parents: 0,1150 interior: 'Here',1151 },1152 },1153 moreThanMoonriverHas,1154 );11551156 let maliciousXcmProgramSent: any;1157 const maxWaitBlocks = 3;11581159 // Try to trick Quartz1160 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1161 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]);11621163 // Needed to bypass the call filter.1164 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1165 await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall);11661167 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1168 });11691170 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1171 && event.outcome.isFailedToTransactAsset);11721173 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1174 expect(targetAccountBalance).to.be.equal(0n);11751176 // But Moonriver still can send the correct amount1177 const validTransferAmount = moonriverBalance / 2n;1178 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1179 targetAccount.addressRaw,1180 {1181 Concrete: {1182 parents: 0,1183 interior: 'Here',1184 },1185 },1186 validTransferAmount,1187 );11881189 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1190 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, validXcmProgram]);11911192 // Needed to bypass the call filter.1193 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1194 await helper.fastDemocracy.executeProposal('Spend the correct amount of QTZ', batchCall);1195 });11961197 await helper.wait.newBlocks(maxWaitBlocks);11981199 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1200 expect(targetAccountBalance).to.be.equal(validTransferAmount);1201 });12021203 itSub('Should not accept reserve transfer of QTZ from Moonriver', async ({helper}) => {1204 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1205 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);12061207 const quartzMultilocation = {1208 V2: {1209 parents: 1,1210 interior: {1211 X1: {1212 Parachain: QUARTZ_CHAIN,1213 },1214 },1215 },1216 };12171218 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1219 targetAccount.addressRaw,1220 {1221 Concrete: {1222 parents: 0,1223 interior: {1224 X1: {1225 Parachain: QUARTZ_CHAIN,1226 },1227 },1228 },1229 },1230 testAmount,1231 );12321233 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1234 targetAccount.addressRaw,1235 {1236 Concrete: {1237 parents: 0,1238 interior: 'Here',1239 },1240 },1241 testAmount,1242 );12431244 let maliciousXcmProgramFullIdSent: any;1245 let maliciousXcmProgramHereIdSent: any;1246 const maxWaitBlocks = 3;12471248 // Try to trick Quartz using full QTZ identification1249 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1250 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]);12511252 // Needed to bypass the call filter.1253 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1254 await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall);12551256 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1257 });12581259 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1260 && event.outcome.isUntrustedReserveLocation);12611262 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1263 expect(accountBalance).to.be.equal(0n);12641265 // Try to trick Quartz using shortened QTZ identification1266 await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {1267 const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramHereId]);12681269 // Needed to bypass the call filter.1270 const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);1271 await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall);12721273 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1274 });12751276 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1277 && event.outcome.isUntrustedReserveLocation);12781279 accountBalance = await helper.balance.getSubstrate(targetAccount.address);1280 expect(accountBalance).to.be.equal(0n);1281 });1282});12831284describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => {1285 let alice: IKeyringPair;1286 let sender: IKeyringPair;12871288 const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden1289 const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden12901291 // Quartz -> Shiden1292 const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden1293 const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden1294 const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ1295 const qtzToShidenArrived = 7_998_196_000_000_000_000n; // 7.99 ... QTZ, Shiden takes a commision in foreign tokens12961297 // Shiden -> Quartz1298 const qtzFromShidenTransfered = 5n * (10n ** QTZ_DECIMALS); // 5 QTZ1299 const qtzOnShidenLeft = qtzToShidenArrived - qtzFromShidenTransfered; // 2.99 ... QTZ13001301 let balanceAfterQuartzToShidenXCM: bigint;13021303 before(async () => {1304 await usingPlaygrounds(async (helper, privateKey) => {1305 alice = await privateKey('//Alice');1306 [sender] = await helper.arrange.createAccounts([100n], alice);1307 console.log('sender', sender.address);13081309 // Set the default version to wrap the first message to other chains.1310 await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);1311 });13121313 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1314 if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) {1315 console.log('1. Create foreign asset and metadata');1316 await helper.getSudo().assets.forceCreate(1317 alice,1318 QTZ_ASSET_ID_ON_SHIDEN,1319 alice.address,1320 QTZ_MINIMAL_BALANCE_ON_SHIDEN,1321 );13221323 await helper.assets.setMetadata(1324 alice,1325 QTZ_ASSET_ID_ON_SHIDEN,1326 'Quartz',1327 'QTZ',1328 Number(QTZ_DECIMALS),1329 );13301331 console.log('2. Register asset location on Shiden');1332 const assetLocation = {1333 V2: {1334 parents: 1,1335 interior: {1336 X1: {1337 Parachain: QUARTZ_CHAIN,1338 },1339 },1340 },1341 };13421343 await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);13441345 console.log('3. Set QTZ payment for XCM execution on Shiden');1346 await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);1347 } else {1348 console.log('QTZ is already registered on Shiden');1349 }1350 console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');1351 await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);1352 });1353 });13541355 itSub('Should connect and send QTZ to Shiden', async ({helper}) => {1356 const destination = {1357 V2: {1358 parents: 1,1359 interior: {1360 X1: {1361 Parachain: SHIDEN_CHAIN,1362 },1363 },1364 },1365 };13661367 const beneficiary = {1368 V2: {1369 parents: 0,1370 interior: {1371 X1: {1372 AccountId32: {1373 network: 'Any',1374 id: sender.addressRaw,1375 },1376 },1377 },1378 },1379 };13801381 const assets = {1382 V2: [1383 {1384 id: {1385 Concrete: {1386 parents: 0,1387 interior: 'Here',1388 },1389 },1390 fun: {1391 Fungible: qtzToShidenTransferred,1392 },1393 },1394 ],1395 };13961397 // Initial balance is 100 QTZ1398 const balanceBefore = await helper.balance.getSubstrate(sender.address);1399 console.log(`Initial balance is: ${balanceBefore}`);14001401 const feeAssetItem = 0;1402 await helper.xcm.limitedReserveTransferAssets(sender, destination, beneficiary, assets, feeAssetItem, 'Unlimited');14031404 // Balance after reserve transfer is less than 901405 balanceAfterQuartzToShidenXCM = await helper.balance.getSubstrate(sender.address);1406 console.log(`QTZ Balance on Quartz after XCM is: ${balanceAfterQuartzToShidenXCM}`);1407 console.log(`Quartz's QTZ commission is: ${balanceBefore-balanceAfterQuartzToShidenXCM}`);1408 expect(balanceBefore - balanceAfterQuartzToShidenXCM > 0).to.be.true;14091410 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1411 await helper.wait.newBlocks(3);1412 const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1413 const shidenBalance = await helper.balance.getSubstrate(sender.address);14141415 console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);1416 console.log(`Shiden's QTZ commission is: ${qtzToShidenTransferred-xcQTZbalance!}`);14171418 expect(xcQTZbalance).to.eq(qtzToShidenArrived);1419 // SHD balance does not changed:1420 expect(shidenBalance).to.eq(shidenInitialBalance);1421 });1422 });14231424 itSub('Should connect to Shiden and send QTZ back', async ({helper}) => {1425 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1426 const destination = {1427 V2: {1428 parents: 1,1429 interior: {1430 X1: {1431 Parachain: QUARTZ_CHAIN,1432 },1433 },1434 },1435 };14361437 const beneficiary = {1438 V2: {1439 parents: 0,1440 interior: {1441 X1: {1442 AccountId32: {1443 network: 'Any',1444 id: sender.addressRaw,1445 },1446 },1447 },1448 },1449 };14501451 const assets = {1452 V2: [1453 {1454 id: {1455 Concrete: {1456 parents: 1,1457 interior: {1458 X1: {1459 Parachain: QUARTZ_CHAIN,1460 },1461 },1462 },1463 },1464 fun: {1465 Fungible: qtzFromShidenTransfered,1466 },1467 },1468 ],1469 };14701471 // Initial balance is 1 SDN1472 const balanceSDNbefore = await helper.balance.getSubstrate(sender.address);1473 console.log(`SDN balance is: ${balanceSDNbefore}, it does not changed`);1474 expect(balanceSDNbefore).to.eq(shidenInitialBalance);14751476 const feeAssetItem = 0;1477 // this is non-standard polkadotXcm extension for Astar only. It calls InitiateReserveWithdraw1478 await helper.executeExtrinsic(sender, 'api.tx.polkadotXcm.reserveWithdrawAssets', [destination, beneficiary, assets, feeAssetItem]);14791480 // Balance after reserve transfer is less than 1 SDN1481 const xcQTZbalance = await helper.assets.account(QTZ_ASSET_ID_ON_SHIDEN, sender.address);1482 const balanceSDN = await helper.balance.getSubstrate(sender.address);1483 console.log(`xcQTZ balance on Shiden after XCM is: ${xcQTZbalance}`);14841485 // Assert: xcQTZ balance correctly decreased1486 expect(xcQTZbalance).to.eq(qtzOnShidenLeft);1487 // Assert: SDN balance is 0.996...1488 expect(balanceSDN / (10n ** (SHIDEN_DECIMALS - 3n))).to.eq(996n);1489 });14901491 await helper.wait.newBlocks(3);1492 const balanceQTZ = await helper.balance.getSubstrate(sender.address);1493 console.log(`QTZ Balance on Quartz after XCM is: ${balanceQTZ}`);1494 expect(balanceQTZ).to.eq(balanceAfterQuartzToShidenXCM + qtzFromShidenTransfered);1495 });14961497 itSub('Shiden can send only up to its balance', async ({helper}) => {1498 // set Shiden's sovereign account's balance1499 const shidenBalance = 10000n * (10n ** QTZ_DECIMALS);1500 const shidenSovereignAccount = helper.address.paraSiblingSovereignAccount(SHIDEN_CHAIN);1501 await helper.getSudo().balance.setBalanceSubstrate(alice, shidenSovereignAccount, shidenBalance);15021503 const moreThanShidenHas = shidenBalance * 2n;15041505 let targetAccountBalance = 0n;1506 const [targetAccount] = await helper.arrange.createAccounts([targetAccountBalance], alice);15071508 const quartzMultilocation = {1509 V2: {1510 parents: 1,1511 interior: {1512 X1: {Parachain: QUARTZ_CHAIN},1513 },1514 },1515 };15161517 const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1518 targetAccount.addressRaw,1519 {1520 Concrete: {1521 parents: 0,1522 interior: 'Here',1523 },1524 },1525 moreThanShidenHas,1526 );15271528 let maliciousXcmProgramSent: any;1529 const maxWaitBlocks = 3;15301531 // Try to trick Quartz1532 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1533 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);15341535 maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1536 });15371538 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash1539 && event.outcome.isFailedToTransactAsset);15401541 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1542 expect(targetAccountBalance).to.be.equal(0n);15431544 // But Shiden still can send the correct amount1545 const validTransferAmount = shidenBalance / 2n;1546 const validXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(1547 targetAccount.addressRaw,1548 {1549 Concrete: {1550 parents: 0,1551 interior: 'Here',1552 },1553 },1554 validTransferAmount,1555 );15561557 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1558 await helper.getSudo().xcm.send(alice, quartzMultilocation, validXcmProgram);1559 });15601561 await helper.wait.newBlocks(maxWaitBlocks);15621563 targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);1564 expect(targetAccountBalance).to.be.equal(validTransferAmount);1565 });15661567 itSub('Should not accept reserve transfer of QTZ from Shiden', async ({helper}) => {1568 const testAmount = 10_000n * (10n ** QTZ_DECIMALS);1569 const [targetAccount] = await helper.arrange.createAccounts([0n], alice);15701571 const quartzMultilocation = {1572 V2: {1573 parents: 1,1574 interior: {1575 X1: {1576 Parachain: QUARTZ_CHAIN,1577 },1578 },1579 },1580 };15811582 const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(1583 targetAccount.addressRaw,1584 {1585 Concrete: {1586 parents: 1,1587 interior: {1588 X1: {1589 Parachain: QUARTZ_CHAIN,1590 },1591 },1592 },1593 },1594 testAmount,1595 );15961597 const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(1598 targetAccount.addressRaw,1599 {1600 Concrete: {1601 parents: 0,1602 interior: 'Here',1603 },1604 },1605 testAmount,1606 );16071608 let maliciousXcmProgramFullIdSent: any;1609 let maliciousXcmProgramHereIdSent: any;1610 const maxWaitBlocks = 3;16111612 // Try to trick Quartz using full QTZ identification1613 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1614 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);16151616 maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1617 });16181619 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramFullIdSent.messageHash1620 && event.outcome.isUntrustedReserveLocation);16211622 let accountBalance = await helper.balance.getSubstrate(targetAccount.address);1623 expect(accountBalance).to.be.equal(0n);16241625 // Try to trick Quartz using shortened QTZ identification1626 await usingShidenPlaygrounds(shidenUrl, async (helper) => {1627 await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);16281629 maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);1630 });16311632 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramHereIdSent.messageHash1633 && event.outcome.isUntrustedReserveLocation);16341635 accountBalance = await helper.balance.getSubstrate(targetAccount.address);1636 expect(accountBalance).to.be.equal(0n);1637 });1638});js-packages/tests/xcm/xcmUnique.test.tsdiffbeforeafterboth--- a/js-packages/tests/xcm/xcmUnique.test.ts
+++ b/js-packages/tests/xcm/xcmUnique.test.ts
@@ -20,7 +20,7 @@
import {Event} from '@unique/playgrounds/unique.dev.js';
import {hexToString, nToBigInt} from '@polkadot/util';
import {ACALA_CHAIN, ASTAR_CHAIN, MOONBEAM_CHAIN, POLKADEX_CHAIN, SAFE_XCM_VERSION, STATEMINT_CHAIN, UNIQUE_CHAIN, expectFailedToTransact, expectUntrustedReserveLocationFail, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types.js';
-
+import {XcmTestHelper} from './xcm.types';
const STATEMINT_PALLET_INSTANCE = 50;
@@ -50,6 +50,8 @@
const USDT_ASSET_METADATA_MINIMAL_BALANCE = 1n;
const USDT_ASSET_AMOUNT = 10_000_000_000_000_000_000_000_000n;
+const testHelper = new XcmTestHelper('unique');
+
describeXCM('[XCM] Integration test: Exchanging USDT with Statemint', () => {
let alice: IKeyringPair;
let bob: IKeyringPair;
@@ -68,6 +70,8 @@
let balanceBobRelayTokenBefore: bigint;
let balanceBobRelayTokenAfter: bigint;
+ let usdtCollectionId: number;
+ let relayCollectionId: number;
before(async () => {
await usingPlaygrounds(async (helper, privateKey) => {
@@ -76,6 +80,8 @@
// Set the default version to wrap the first message to other chains.
await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+
+ relayCollectionId = await testHelper.registerRelayNativeTokenOnUnique(alice);
});
await usingRelayPlaygrounds(relayUrl, async (helper) => {
@@ -85,21 +91,25 @@
});
await usingStatemintPlaygrounds(statemintUrl, async (helper) => {
- const sovereignFundingAmount = 3_500_000_000n;
+ const assetInfo = await helper.assets.assetInfo(USDT_ASSET_ID);
+ if(assetInfo == null) {
+ await helper.assets.create(
+ alice,
+ USDT_ASSET_ID,
+ alice.address,
+ USDT_ASSET_METADATA_MINIMAL_BALANCE,
+ );
+ await helper.assets.setMetadata(
+ alice,
+ USDT_ASSET_ID,
+ USDT_ASSET_METADATA_NAME,
+ USDT_ASSET_METADATA_DESCRIPTION,
+ USDT_ASSET_METADATA_DECIMALS,
+ );
+ } else {
+ console.log('The USDT asset is already registered on AssetHub');
+ }
- await helper.assets.create(
- alice,
- USDT_ASSET_ID,
- alice.address,
- USDT_ASSET_METADATA_MINIMAL_BALANCE,
- );
- await helper.assets.setMetadata(
- alice,
- USDT_ASSET_ID,
- USDT_ASSET_METADATA_NAME,
- USDT_ASSET_METADATA_DESCRIPTION,
- USDT_ASSET_METADATA_DECIMALS,
- );
await helper.assets.mint(
alice,
USDT_ASSET_ID,
@@ -107,6 +117,8 @@
USDT_ASSET_AMOUNT,
);
+ const sovereignFundingAmount = 3_500_000_000n;
+
// funding parachain sovereing account on Statemint.
// The sovereign account should be created before any action
// (the assets pallet on Statemint check if the sovereign account exists)
@@ -117,31 +129,29 @@
await usingPlaygrounds(async (helper) => {
const location = {
- V2: {
- parents: 1,
- interior: {X3: [
- {
- Parachain: STATEMINT_CHAIN,
- },
- {
- PalletInstance: STATEMINT_PALLET_INSTANCE,
- },
- {
- GeneralIndex: USDT_ASSET_ID,
- },
- ]},
- },
+ parents: 1,
+ interior: {X3: [
+ {
+ Parachain: STATEMINT_CHAIN,
+ },
+ {
+ PalletInstance: STATEMINT_PALLET_INSTANCE,
+ },
+ {
+ GeneralIndex: USDT_ASSET_ID,
+ },
+ ]},
};
- const metadata =
- {
- name: USDT_ASSET_ID,
- symbol: USDT_ASSET_METADATA_NAME,
- decimals: USDT_ASSET_METADATA_DECIMALS,
- minimalBalance: USDT_ASSET_METADATA_MINIMAL_BALANCE,
- };
- await helper.getSudo().foreignAssets.register(alice, alice.address, location, metadata);
+ if(await helper.foreignAssets.foreignCollectionId(location) == null) {
+ const tokenPrefix = USDT_ASSET_METADATA_NAME;
+ await helper.getSudo().foreignAssets.register(alice, location, USDT_ASSET_METADATA_NAME, tokenPrefix, {Fungible: USDT_ASSET_METADATA_DECIMALS});
+ } else {
+ console.log('Foreign collection is already registered on Unique');
+ }
+
balanceUniqueBefore = await helper.balance.getSubstrate(alice.address);
+ usdtCollectionId = await helper.foreignAssets.foreignCollectionId(location);
});
@@ -259,8 +269,7 @@
// ensure that asset has been delivered
await helper.wait.newBlocks(3);
- // expext collection id will be with id 1
- const free = await helper.ft.getBalance(1, {Substrate: alice.address});
+ const free = await helper.ft.getBalance(usdtCollectionId, {Substrate: alice.address});
balanceUniqueAfter = await helper.balance.getSubstrate(alice.address);
@@ -299,15 +308,11 @@
const relayFee = 400_000_000_000_000n;
const currencies: [any, bigint][] = [
[
- {
- ForeignAssetId: 0,
- },
+ usdtCollectionId,
TRANSFER_AMOUNT,
],
[
- {
- NativeAssetId: 'Parent',
- },
+ relayCollectionId,
relayFee,
],
];
@@ -332,7 +337,7 @@
itSub('Should connect and send Relay token to Unique', async ({helper}) => {
balanceBobBefore = await helper.balance.getSubstrate(bob.address);
- balanceBobRelayTokenBefore = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});
+ balanceBobRelayTokenBefore = await helper.ft.getBalance(relayCollectionId, {Substrate: bob.address});
await usingRelayPlaygrounds(relayUrl, async (helper) => {
const destination = {
@@ -380,7 +385,7 @@
await helper.wait.newBlocks(3);
balanceBobAfter = await helper.balance.getSubstrate(bob.address);
- balanceBobRelayTokenAfter = await helper.tokens.accounts(bob.address, {NativeAssetId: 'Parent'});
+ balanceBobRelayTokenAfter = await helper.ft.getBalance(relayCollectionId, {Substrate: bob.address});
const wndFeeOnUnique = balanceBobRelayTokenAfter - TRANSFER_AMOUNT_RELAY - balanceBobRelayTokenBefore;
const wndDiffOnUnique = balanceBobRelayTokenAfter - balanceBobRelayTokenBefore;
@@ -420,9 +425,7 @@
const currencies: any = [
[
- {
- NativeAssetId: 'Parent',
- },
+ relayCollectionId,
TRANSFER_AMOUNT_RELAY,
],
];