difftreelog
Merge pull request #1003 from UniqueNetwork/feature/llxcm-qtz
in: master
added `XcmTestHelper` + allow XCM Transact for Gov/Identity/System calls
14 files changed
.baedeker/.gitignorediffbeforeafterboth--- a/.baedeker/.gitignore
+++ b/.baedeker/.gitignore
@@ -1,4 +1,5 @@
/.bdk-env
-/rewrites.jsonnet
+/rewrites*.jsonnet
/vendor
/baedeker-library
+!/rewrites.example.jsonnet
\ No newline at end of file
.github/workflows/xcm.ymldiffbeforeafterboth--- a/.github/workflows/xcm.yml
+++ b/.github/workflows/xcm.yml
@@ -38,7 +38,7 @@
with:
matrix: |
network {opal}, relay_branch {${{ env.UNIQUEWEST_MAINNET_BRANCH }}}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.WESTMINT_BUILD_BRANCH }}}, astar_version {${{ env.ASTAR_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testXcmOpal}, runtime_features {opal-runtime}
- network {quartz}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}}, astar_version {${{ env.SHIDEN_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testXcmQuartz}, runtime_features {quartz-runtime}
+ network {quartz}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}}, astar_version {${{ env.SHIDEN_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testFullXcmQuartz}, runtime_features {quartz-runtime}
network {unique}, relay_branch {${{ env.POLKADOT_MAINNET_BRANCH }}}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINT_BUILD_BRANCH }}}, astar_version {${{ env.ASTAR_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testFullXcmUnique}, runtime_features {unique-runtime}
xcm:
runtime/common/config/xcm/mod.rsdiffbeforeafterboth--- a/runtime/common/config/xcm/mod.rs
+++ b/runtime/common/config/xcm/mod.rs
@@ -15,7 +15,7 @@
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
use frame_support::{
- traits::{Everything, Nothing, Get, ConstU32, ProcessMessageError},
+ traits::{Everything, Nothing, Get, ConstU32, ProcessMessageError, Contains},
parameter_types,
};
use frame_system::EnsureRoot;
@@ -162,6 +162,54 @@
pub type Weigher = FixedWeightBounds<UnitWeightCost, RuntimeCall, MaxInstructions>;
+pub struct XcmCallFilter;
+impl XcmCallFilter {
+ fn allow_gov_and_sys_call(call: &RuntimeCall) -> bool {
+ match call {
+ RuntimeCall::System(..) => true,
+
+ #[cfg(feature = "governance")]
+ RuntimeCall::Identity(..)
+ | RuntimeCall::Preimage(..)
+ | RuntimeCall::Democracy(..)
+ | RuntimeCall::Council(..)
+ | RuntimeCall::TechnicalCommittee(..)
+ | RuntimeCall::CouncilMembership(..)
+ | RuntimeCall::TechnicalCommitteeMembership(..)
+ | RuntimeCall::FellowshipCollective(..)
+ | RuntimeCall::FellowshipReferenda(..) => true,
+ _ => false,
+ }
+ }
+
+ fn allow_utility_call(call: &RuntimeCall) -> bool {
+ match call {
+ RuntimeCall::Utility(pallet_utility::Call::batch { calls, .. }) => {
+ calls.iter().all(Self::allow_gov_and_sys_call)
+ }
+ RuntimeCall::Utility(pallet_utility::Call::batch_all { calls, .. }) => {
+ calls.iter().all(Self::allow_gov_and_sys_call)
+ }
+ RuntimeCall::Utility(pallet_utility::Call::as_derivative { call, .. }) => {
+ Self::allow_gov_and_sys_call(call)
+ }
+ RuntimeCall::Utility(pallet_utility::Call::dispatch_as { call, .. }) => {
+ Self::allow_gov_and_sys_call(call)
+ }
+ RuntimeCall::Utility(pallet_utility::Call::force_batch { calls, .. }) => {
+ calls.iter().all(Self::allow_gov_and_sys_call)
+ }
+ _ => false,
+ }
+ }
+}
+
+impl Contains<RuntimeCall> for XcmCallFilter {
+ fn contains(call: &RuntimeCall) -> bool {
+ Self::allow_gov_and_sys_call(call) || Self::allow_utility_call(call)
+ }
+}
+
pub struct XcmExecutorConfig<T>(PhantomData<T>);
impl<T> xcm_executor::Config for XcmExecutorConfig<T>
where
@@ -191,9 +239,7 @@
type MessageExporter = ();
type UniversalAliases = Nothing;
type CallDispatcher = RuntimeCall;
-
- // Deny all XCM Transacts.
- type SafeCallFilter = Nothing;
+ type SafeCallFilter = XcmCallFilter;
}
#[cfg(feature = "runtime-benchmarks")]
runtime/opal/src/xcm_barrier.rsdiffbeforeafterboth--- a/runtime/opal/src/xcm_barrier.rs
+++ b/runtime/opal/src/xcm_barrier.rs
@@ -14,7 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
-use frame_support::traits::Everything;
-use xcm_builder::{AllowTopLevelPaidExecutionFrom, TakeWeightCredit};
+use frame_support::{match_types, traits::Everything};
+use xcm::latest::{Junctions::*, MultiLocation};
+use xcm_builder::{AllowTopLevelPaidExecutionFrom, TakeWeightCredit, AllowExplicitUnpaidExecutionFrom};
-pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom<Everything>);
+match_types! {
+ pub type ParentOnly: impl Contains<MultiLocation> = {
+ MultiLocation { parents: 1, interior: Here }
+ };
+}
+
+pub type Barrier = (
+ TakeWeightCredit,
+ AllowExplicitUnpaidExecutionFrom<ParentOnly>,
+ AllowTopLevelPaidExecutionFrom<Everything>,
+);
runtime/quartz/src/xcm_barrier.rsdiffbeforeafterboth--- a/runtime/quartz/src/xcm_barrier.rs
+++ b/runtime/quartz/src/xcm_barrier.rs
@@ -18,12 +18,16 @@
use xcm::latest::{Junctions::*, MultiLocation};
use xcm_builder::{
AllowKnownQueryResponses, AllowSubscriptionsFrom, TakeWeightCredit,
- AllowTopLevelPaidExecutionFrom,
+ AllowTopLevelPaidExecutionFrom, AllowExplicitUnpaidExecutionFrom,
};
use crate::PolkadotXcm;
match_types! {
+ pub type ParentOnly: impl Contains<MultiLocation> = {
+ MultiLocation { parents: 1, interior: Here }
+ };
+
pub type ParentOrSiblings: impl Contains<MultiLocation> = {
MultiLocation { parents: 1, interior: Here } |
MultiLocation { parents: 1, interior: X1(_) }
@@ -32,6 +36,7 @@
pub type Barrier = (
TakeWeightCredit,
+ AllowExplicitUnpaidExecutionFrom<ParentOnly>,
AllowTopLevelPaidExecutionFrom<Everything>,
// Expected responses are OK.
AllowKnownQueryResponses<PolkadotXcm>,
runtime/unique/src/xcm_barrier.rsdiffbeforeafterboth--- a/runtime/unique/src/xcm_barrier.rs
+++ b/runtime/unique/src/xcm_barrier.rs
@@ -18,12 +18,16 @@
use xcm::latest::{Junctions::*, MultiLocation};
use xcm_builder::{
AllowKnownQueryResponses, AllowSubscriptionsFrom, TakeWeightCredit,
- AllowTopLevelPaidExecutionFrom,
+ AllowTopLevelPaidExecutionFrom, AllowExplicitUnpaidExecutionFrom,
};
use crate::PolkadotXcm;
match_types! {
+ pub type ParentOnly: impl Contains<MultiLocation> = {
+ MultiLocation { parents: 1, interior: Here }
+ };
+
pub type ParentOrSiblings: impl Contains<MultiLocation> = {
MultiLocation { parents: 1, interior: Here } |
MultiLocation { parents: 1, interior: X1(_) }
@@ -32,6 +36,7 @@
pub type Barrier = (
TakeWeightCredit,
+ AllowExplicitUnpaidExecutionFrom<ParentOnly>,
AllowTopLevelPaidExecutionFrom<Everything>,
// Expected responses are OK.
AllowKnownQueryResponses<PolkadotXcm>,
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -117,6 +117,8 @@
"testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts",
"testFullXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Unique.test.ts",
"testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts",
+ "testLowLevelXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/lowLevelXcmQuartz.test.ts",
+ "testFullXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Quartz.test.ts",
"testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts",
"testXcmTransferAcala": "yarn _test ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000",
"testXcmTransferStatemine": "yarn _test ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000",
tests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -9,7 +9,7 @@
import {IKeyringPair} from '@polkadot/types/types';
import {EventRecord} from '@polkadot/types/interfaces';
import {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';
-import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';
+import {FrameSystemEventRecord, XcmV2TraitsError, XcmV3TraitsOutcome} from '@polkadot/types/lookup';
import {SignerOptions, VoidFn} from '@polkadot/api/types';
import {Pallets} from '..';
import {spawnSync} from 'child_process';
@@ -260,6 +260,12 @@
outcome: eventData<XcmV2TraitsError>(data, 1),
}));
};
+
+ static DmpQueue = class extends EventSection('dmpQueue') {
+ static ExecutedDownward = this.Method('ExecutedDownward', data => ({
+ outcome: eventData<XcmV3TraitsOutcome>(data, 1),
+ }));
+ };
}
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -559,6 +565,12 @@
super(logger, options);
this.wait = new WaitGroup(this);
}
+
+ getSudo() {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const SudoHelperType = SudoHelper(this.helperBase);
+ return this.clone(SudoHelperType) as DevRelayHelper;
+ }
}
export class DevWestmintHelper extends WestmintHelper {
@@ -968,6 +980,31 @@
],
};
}
+
+ makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {
+ return {
+ V3: [
+ {
+ UnpaidExecution: {
+ weightLimit: 'Unlimited',
+ checkOrigin: null,
+ },
+ },
+ {
+ Transact: {
+ originKind: 'Superuser',
+ requireWeightAtMost: {
+ refTime: info.weightMultiplier * 200000000,
+ proofSize: info.weightMultiplier * 3000,
+ },
+ call: {
+ encoded: info.call,
+ },
+ },
+ },
+ ],
+ };
+ }
}
class MoonbeamAccountGroup {
@@ -1501,4 +1538,4 @@
);
}
};
-}
\ No newline at end of file
+}
tests/src/util/playgrounds/unique.xcm.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.xcm.ts
+++ b/tests/src/util/playgrounds/unique.xcm.ts
@@ -240,19 +240,19 @@
}
export class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
- async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {
+ async create(signer: TSigner, assetId: number | bigint, admin: string, minimalBalance: bigint) {
await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
}
- async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {
+ async setMetadata(signer: TSigner, assetId: number | bigint, name: string, symbol: string, decimals: number) {
await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);
}
- async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {
+ async mint(signer: TSigner, assetId: number | bigint, beneficiary: string, amount: bigint) {
await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
}
- async account(assetId: string | number, address: string) {
+ async account(assetId: string | number | bigint, address: string) {
const accountAsset = (
await this.helper.callRpc('api.query.assets.account', [assetId, address])
).toJSON()! as any;
tests/src/xcm/lowLevelXcmQuartz.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/xcm/lowLevelXcmQuartz.test.ts
@@ -0,0 +1,364 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {IKeyringPair} from '@polkadot/types/types';
+import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingMoonriverPlaygrounds, usingShidenPlaygrounds, usingRelayPlaygrounds} from '../util';
+import {QUARTZ_CHAIN, QTZ_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, shidenUrl, SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT, SENDER_BUDGET, relayUrl} from './xcm.types';
+import {hexToString} from '@polkadot/util';
+
+const testHelper = new XcmTestHelper('quartz');
+
+describeXCM('[XCMLL] Integration test: Exchanging tokens with Karura', () => {
+ let alice: IKeyringPair;
+ let randomAccount: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ alice = await privateKey('//Alice');
+ [randomAccount] = await helper.arrange.createAccounts([0n], alice);
+
+ // Set the default version to wrap the first message to other chains.
+ await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+ });
+
+ await usingKaruraPlaygrounds(karuraUrl, async (helper) => {
+ const destination = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: QUARTZ_CHAIN,
+ },
+ },
+ },
+ };
+
+ const metadata = {
+ name: 'Quartz',
+ symbol: 'QTZ',
+ decimals: 18,
+ minimalBalance: 1000000000000000000n,
+ };
+
+ const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) =>
+ hexToString(v.toJSON()['symbol'])) as string[];
+
+ if(!assets.includes('QTZ')) {
+ await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+ } else {
+ console.log('QTZ token already registered on Karura assetRegistry pallet');
+ }
+ await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);
+ });
+
+ await usingPlaygrounds(async (helper) => {
+ await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
+ });
+ });
+
+ itSub('Should connect and send QTZ to Karura', async () => {
+ await testHelper.sendUnqTo('karura', randomAccount);
+ });
+
+ itSub('Should connect to Karura and send QTZ back', async () => {
+ await testHelper.sendUnqBack('karura', alice, randomAccount);
+ });
+
+ itSub('Karura can send only up to its balance', async () => {
+ await testHelper.sendOnlyOwnedBalance('karura', alice);
+ });
+});
+// These tests are relevant only when
+// the the corresponding foreign assets are not registered
+describeXCM('[XCMLL] Integration test: Quartz rejects non-native tokens', () => {
+ let alice: IKeyringPair;
+
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ alice = await privateKey('//Alice');
+
+
+
+ // Set the default version to wrap the first message to other chains.
+ await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+ });
+ });
+
+ itSub('Quartz rejects KAR tokens from Karura', async () => {
+ await testHelper.rejectNativeTokensFrom('karura', alice);
+ });
+
+ itSub('Quartz rejects MOVR tokens from Moonriver', async () => {
+ await testHelper.rejectNativeTokensFrom('moonriver', alice);
+ });
+
+ itSub('Quartz rejects SDN tokens from Shiden', async () => {
+ await testHelper.rejectNativeTokensFrom('shiden', alice);
+ });
+});
+
+describeXCM('[XCMLL] Integration test: Exchanging QTZ with Moonriver', () => {
+ // Quartz constants
+ let alice: IKeyringPair;
+ let quartzAssetLocation;
+
+ let randomAccountQuartz: IKeyringPair;
+ let randomAccountMoonriver: IKeyringPair;
+
+ // Moonriver constants
+ let assetId: string;
+
+ const quartzAssetMetadata = {
+ name: 'xcQuartz',
+ symbol: 'xcQTZ',
+ decimals: 18,
+ isFrozen: false,
+ minimalBalance: 1n,
+ };
+
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ alice = await privateKey('//Alice');
+ [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);
+
+
+ // Set the default version to wrap the first message to other chains.
+ await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+ });
+
+ await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {
+ const alithAccount = helper.account.alithAccount();
+ const baltatharAccount = helper.account.baltatharAccount();
+ const dorothyAccount = helper.account.dorothyAccount();
+
+ randomAccountMoonriver = helper.account.create();
+
+ // >>> Sponsoring Dorothy >>>
+ console.log('Sponsoring Dorothy.......');
+ await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);
+ console.log('Sponsoring Dorothy.......DONE');
+ // <<< Sponsoring Dorothy <<<
+
+ quartzAssetLocation = {
+ XCM: {
+ parents: 1,
+ interior: {X1: {Parachain: QUARTZ_CHAIN}},
+ },
+ };
+ const existentialDeposit = 1n;
+ const isSufficient = true;
+ const unitsPerSecond = 1n;
+ const numAssetsWeightHint = 0;
+ if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) {
+ console.log('Quartz asset already registered on Moonriver');
+ } else {
+ const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
+ location: quartzAssetLocation,
+ metadata: quartzAssetMetadata,
+ existentialDeposit,
+ isSufficient,
+ unitsPerSecond,
+ numAssetsWeightHint,
+ });
+
+ console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
+
+ await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+ }
+ // >>> Acquire Quartz AssetId Info on Moonriver >>>
+ console.log('Acquire Quartz AssetId Info on Moonriver.......');
+
+ assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();
+
+ console.log('QTZ asset ID is %s', assetId);
+ console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');
+ // >>> Acquire Quartz AssetId Info on Moonriver >>>
+
+ // >>> Sponsoring random Account >>>
+ console.log('Sponsoring random Account.......');
+ await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);
+ console.log('Sponsoring random Account.......DONE');
+ // <<< Sponsoring random Account <<<
+ });
+
+ await usingPlaygrounds(async (helper) => {
+ await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);
+ });
+ });
+
+ itSub('Should connect and send QTZ to Moonriver', async () => {
+ await testHelper.sendUnqTo('moonriver', randomAccountQuartz, randomAccountMoonriver);
+ });
+
+ itSub('Should connect to Moonriver and send QTZ back', async () => {
+ await testHelper.sendUnqBack('moonriver', alice, randomAccountQuartz);
+ });
+
+ itSub('Moonriver can send only up to its balance', async () => {
+ await testHelper.sendOnlyOwnedBalance('moonriver', alice);
+ });
+
+ itSub('Should not accept reserve transfer of QTZ from Moonriver', async () => {
+ await testHelper.rejectReserveTransferUNQfrom('moonriver', alice);
+ });
+});
+
+describeXCM('[XCMLL] Integration test: Exchanging tokens with Shiden', () => {
+ let alice: IKeyringPair;
+ let randomAccount: IKeyringPair;
+
+ const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden
+ const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden
+
+ // Quartz -> Shiden
+ const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden
+ const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ alice = await privateKey('//Alice');
+ randomAccount = helper.arrange.createEmptyAccount();
+ await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
+ console.log('sender: ', randomAccount.address);
+
+ // Set the default version to wrap the first message to other chains.
+ await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+ });
+
+ await usingShidenPlaygrounds(shidenUrl, async (helper) => {
+ if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) {
+ console.log('1. Create foreign asset and metadata');
+ await helper.assets.create(
+ alice,
+ QTZ_ASSET_ID_ON_SHIDEN,
+ alice.address,
+ QTZ_MINIMAL_BALANCE_ON_SHIDEN,
+ );
+
+ await helper.assets.setMetadata(
+ alice,
+ QTZ_ASSET_ID_ON_SHIDEN,
+ 'Quartz',
+ 'QTZ',
+ Number(QTZ_DECIMALS),
+ );
+
+ console.log('2. Register asset location on Shiden');
+ const assetLocation = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: QUARTZ_CHAIN,
+ },
+ },
+ },
+ };
+
+ await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
+
+ console.log('3. Set QTZ payment for XCM execution on Shiden');
+ await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
+ } else {
+ console.log('QTZ is already registered on Shiden');
+ }
+ console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');
+ await helper.balance.transferToSubstrate(alice, randomAccount.address, shidenInitialBalance);
+ });
+ });
+
+ itSub('Should connect and send QTZ to Shiden', async () => {
+ await testHelper.sendUnqTo('shiden', randomAccount);
+ });
+
+ itSub('Should connect to Shiden and send QTZ back', async () => {
+ await testHelper.sendUnqBack('shiden', alice, randomAccount);
+ });
+
+ itSub('Shiden can send only up to its balance', async () => {
+ await testHelper.sendOnlyOwnedBalance('shiden', alice);
+ });
+
+ itSub('Should not accept reserve transfer of QTZ from Shiden', async () => {
+ await testHelper.rejectReserveTransferUNQfrom('shiden', alice);
+ });
+});
+
+describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => {
+ let sudoer: IKeyringPair;
+
+ before(async function () {
+ await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => {
+ sudoer = await privateKey('//Alice');
+ });
+ });
+
+ // At the moment there is no reliable way
+ // to establish the correspondence between the `ExecutedDownward` event
+ // and the relay's sent message due to `SetTopic` instruction
+ // containing an unpredictable topic silently added by the relay's messages on the router level.
+ // This changes the message hash on arrival to our chain.
+ //
+ // See:
+ // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83
+ // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36
+ //
+ // Because of this, we insert time gaps between tests so
+ // different `ExecutedDownward` events won't interfere with each other.
+ afterEach(async () => {
+ await usingPlaygrounds(async (helper) => {
+ await helper.wait.newBlocks(3);
+ });
+ });
+
+ itSub('The relay can set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain');
+ });
+
+ itSub('The relay can batch set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch');
+ });
+
+ itSub('The relay can batchAll set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll');
+ });
+
+ itSub('The relay can forceBatch set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch');
+ });
+
+ itSub('[negative] The relay cannot set balance', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain');
+ });
+
+ itSub('[negative] The relay cannot set balance via batch', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch');
+ });
+
+ itSub('[negative] The relay cannot set balance via batchAll', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll');
+ });
+
+ itSub('[negative] The relay cannot set balance via forceBatch', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch');
+ });
+
+ itSub('[negative] The relay cannot set balance via dispatchAs', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs');
+ });
+});
tests/src/xcm/lowLevelXcmUnique.test.tsdiffbeforeafterboth--- a/tests/src/xcm/lowLevelXcmUnique.test.ts
+++ b/tests/src/xcm/lowLevelXcmUnique.test.ts
@@ -16,317 +16,14 @@
import {IKeyringPair} from '@polkadot/types/types';
import config from '../config';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util';
-import {Event} from '../util/playgrounds/unique.dev';
+import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds} from '../util';
import {nToBigInt} from '@polkadot/util';
import {hexToString} from '@polkadot/util';
-import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types';
-
-
-const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
-const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
-const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
-const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
-const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
-
-let balanceUniqueTokenInit: bigint;
-let balanceUniqueTokenMiddle: bigint;
-let balanceUniqueTokenFinal: bigint;
-let unqFees: bigint;
-
-
-async function genericSendUnqTo(
- networkName: keyof typeof NETWORKS,
- randomAccount: IKeyringPair,
- randomAccountOnTargetChain = randomAccount,
-) {
- const networkUrl = mapToChainUrl(networkName);
- const targetPlayground = getDevPlayground(networkName);
- await usingPlaygrounds(async (helper) => {
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
- const destination = {
- V2: {
- parents: 1,
- interior: {
- X1: {
- Parachain: mapToChainId(networkName),
- },
- },
- },
- };
-
- const beneficiary = {
- V2: {
- parents: 0,
- interior: {
- X1: (
- networkName == 'moonbeam' ?
- {
- AccountKey20: {
- network: 'Any',
- key: randomAccountOnTargetChain.address,
- },
- }
- :
- {
- AccountId32: {
- network: 'Any',
- id: randomAccountOnTargetChain.addressRaw,
- },
- }
- ),
- },
- },
- };
-
- const assets = {
- V2: [
- {
- id: {
- Concrete: {
- parents: 0,
- interior: 'Here',
- },
- },
- fun: {
- Fungible: TRANSFER_AMOUNT,
- },
- },
- ],
- };
- const feeAssetItem = 0;
-
- await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
- const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
-
- unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
- console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(unqFees));
- expect(unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true;
-
- await targetPlayground(networkUrl, async (helper) => {
- /*
- Since only the parachain part of the Polkadex
- infrastructure is launched (without their
- solochain validators), processing incoming
- assets will lead to an error.
- This error indicates that the Polkadex chain
- received a message from the Unique network,
- since the hash is being checked to ensure
- it matches what was sent.
- */
- if(networkName == 'polkadex') {
- await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
- } else {
- await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
- }
- });
-
- });
-}
-
-async function genericSendUnqBack(
- networkName: keyof typeof NETWORKS,
- sudoer: IKeyringPair,
- randomAccountOnUnq: IKeyringPair,
-) {
- const networkUrl = mapToChainUrl(networkName);
-
- const targetPlayground = getDevPlayground(networkName);
- await usingPlaygrounds(async (helper) => {
-
- const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
- randomAccountOnUnq.addressRaw,
- uniqueAssetId,
- SENDBACK_AMOUNT,
- );
-
- let xcmProgramSent: any;
-
-
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram);
- xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- } else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
- xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
- await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
-
- balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
-
- expect(balanceUniqueTokenFinal).to.be.equal(balanceUniqueTokenInit - unqFees - STAYED_ON_TARGET_CHAIN);
-
- });
-}
-
-async function genericSendOnlyOwnedBalance(
- networkName: keyof typeof NETWORKS,
- sudoer: IKeyringPair,
-) {
- const networkUrl = mapToChainUrl(networkName);
- const targetPlayground = getDevPlayground(networkName);
-
- const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
-
- await usingPlaygrounds(async (helper) => {
- const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
- await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
- const moreThanTargetChainHas = 2n * targetChainBalance;
-
- const targetAccount = helper.arrange.createEmptyAccount();
-
- const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
- targetAccount.addressRaw,
- {
- Concrete: {
- parents: 0,
- interior: 'Here',
- },
- },
- moreThanTargetChainHas,
- );
-
- let maliciousXcmProgramSent: any;
-
-
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgram);
- maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- } else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
- maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
- await expectFailedToTransact(helper, maliciousXcmProgramSent);
-
- const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
- expect(targetAccountBalance).to.be.equal(0n);
- });
-}
-
-async function genericReserveTransferUNQfrom(netwokrName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
- const networkUrl = mapToChainUrl(netwokrName);
- const targetPlayground = getDevPlayground(netwokrName);
-
- await usingPlaygrounds(async (helper) => {
- const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
- const targetAccount = helper.arrange.createEmptyAccount();
-
- const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
- targetAccount.addressRaw,
- uniqueAssetId,
- testAmount,
- );
-
- const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
- targetAccount.addressRaw,
- {
- Concrete: {
- parents: 0,
- interior: 'Here',
- },
- },
- testAmount,
- );
-
- let maliciousXcmProgramFullIdSent: any;
- let maliciousXcmProgramHereIdSent: any;
- const maxWaitBlocks = 3;
-
- // Try to trick Unique using full UNQ identification
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
- maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- // Moonbeam case
- else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using path asset identification`,batchCall);
-
- maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
-
- await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);
-
- let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
- expect(accountBalance).to.be.equal(0n);
-
- // Try to trick Unique using shortened UNQ identification
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramHereId);
- maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
-
- maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
-
- await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
+import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl, moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types';
- accountBalance = await helper.balance.getSubstrate(targetAccount.address);
- expect(accountBalance).to.be.equal(0n);
- });
-}
+const testHelper = new XcmTestHelper('unique');
-async function genericRejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
- const networkUrl = mapToChainUrl(networkName);
- const targetPlayground = getDevPlayground(networkName);
- let messageSent: any;
- await usingPlaygrounds(async (helper) => {
- const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
- helper.arrange.createEmptyAccount().addressRaw,
- {
- Concrete: {
- parents: 1,
- interior: {
- X1: {
- Parachain: mapToChainId(networkName),
- },
- },
- },
- },
- TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
- );
- await targetPlayground(networkUrl, async (helper) => {
- if('getSudo' in helper) {
- await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
- messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- } else if('fastDemocracy' in helper) {
- const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
- // Needed to bypass the call filter.
- const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
- await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);
-
- messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
- }
- });
- await expectFailedToTransact(helper, messageSent);
- });
-}
describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {
@@ -374,24 +71,23 @@
await usingPlaygrounds(async (helper) => {
await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
});
});
itSub('Should connect and send UNQ to Acala', async () => {
- await genericSendUnqTo('acala', randomAccount);
+ await testHelper.sendUnqTo('acala', randomAccount);
});
itSub('Should connect to Acala and send UNQ back', async () => {
- await genericSendUnqBack('acala', alice, randomAccount);
+ await testHelper.sendUnqBack('acala', alice, randomAccount);
});
itSub('Acala can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('acala', alice);
+ await testHelper.sendOnlyOwnedBalance('acala', alice);
});
itSub('Should not accept reserve transfer of UNQ from Acala', async () => {
- await genericReserveTransferUNQfrom('acala', alice);
+ await testHelper.rejectReserveTransferUNQfrom('acala', alice);
});
});
@@ -429,25 +125,24 @@
await usingPlaygrounds(async (helper) => {
await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
});
});
itSub('Should connect and send UNQ to Polkadex', async () => {
- await genericSendUnqTo('polkadex', randomAccount);
+ await testHelper.sendUnqTo('polkadex', randomAccount);
});
itSub('Should connect to Polkadex and send UNQ back', async () => {
- await genericSendUnqBack('polkadex', alice, randomAccount);
+ await testHelper.sendUnqBack('polkadex', alice, randomAccount);
});
itSub('Polkadex can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('polkadex', alice);
+ await testHelper.sendOnlyOwnedBalance('polkadex', alice);
});
itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {
- await genericReserveTransferUNQfrom('polkadex', alice);
+ await testHelper.rejectReserveTransferUNQfrom('polkadex', alice);
});
});
@@ -466,19 +161,19 @@
});
itSub('Unique rejects ACA tokens from Acala', async () => {
- await genericRejectNativeTokensFrom('acala', alice);
+ await testHelper.rejectNativeTokensFrom('acala', alice);
});
itSub('Unique rejects GLMR tokens from Moonbeam', async () => {
- await genericRejectNativeTokensFrom('moonbeam', alice);
+ await testHelper.rejectNativeTokensFrom('moonbeam', alice);
});
itSub('Unique rejects ASTR tokens from Astar', async () => {
- await genericRejectNativeTokensFrom('astar', alice);
+ await testHelper.rejectNativeTokensFrom('astar', alice);
});
itSub('Unique rejects PDX tokens from Polkadex', async () => {
- await genericRejectNativeTokensFrom('polkadex', alice);
+ await testHelper.rejectNativeTokensFrom('polkadex', alice);
});
});
@@ -569,24 +264,23 @@
await usingPlaygrounds(async (helper) => {
await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);
- balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address);
});
});
itSub('Should connect and send UNQ to Moonbeam', async () => {
- await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
+ await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
});
itSub('Should connect to Moonbeam and send UNQ back', async () => {
- await genericSendUnqBack('moonbeam', alice, randomAccountUnique);
+ await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);
});
itSub('Moonbeam can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('moonbeam', alice);
+ await testHelper.sendOnlyOwnedBalance('moonbeam', alice);
});
itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {
- await genericReserveTransferUNQfrom('moonbeam', alice);
+ await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice);
});
});
@@ -594,12 +288,12 @@
let alice: IKeyringPair;
let randomAccount: IKeyringPair;
- const UNQ_ASSET_ID_ON_ASTAR = 1;
- const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;
+ const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar
+ const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar
// Unique -> Astar
const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.
- const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+ const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar
before(async () => {
await usingPlaygrounds(async (helper, privateKey) => {
@@ -615,7 +309,6 @@
await usingAstarPlaygrounds(astarUrl, async (helper) => {
if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {
console.log('1. Create foreign asset and metadata');
- // TODO update metadata with values from production
await helper.assets.create(
alice,
UNQ_ASSET_ID_ON_ASTAR,
@@ -626,8 +319,8 @@
await helper.assets.setMetadata(
alice,
UNQ_ASSET_ID_ON_ASTAR,
- 'Cross chain UNQ',
- 'xcUNQ',
+ 'Unique Network',
+ 'UNQ',
Number(UNQ_DECIMALS),
);
@@ -656,18 +349,82 @@
});
itSub('Should connect and send UNQ to Astar', async () => {
- await genericSendUnqTo('astar', randomAccount);
+ await testHelper.sendUnqTo('astar', randomAccount);
});
itSub('Should connect to Astar and send UNQ back', async () => {
- await genericSendUnqBack('astar', alice, randomAccount);
+ await testHelper.sendUnqBack('astar', alice, randomAccount);
});
itSub('Astar can send only up to its balance', async () => {
- await genericSendOnlyOwnedBalance('astar', alice);
+ await testHelper.sendOnlyOwnedBalance('astar', alice);
});
itSub('Should not accept reserve transfer of UNQ from Astar', async () => {
- await genericReserveTransferUNQfrom('astar', alice);
+ await testHelper.rejectReserveTransferUNQfrom('astar', alice);
+ });
+});
+
+describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => {
+ let sudoer: IKeyringPair;
+
+ before(async function () {
+ await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => {
+ sudoer = await privateKey('//Alice');
+ });
+ });
+
+ // At the moment there is no reliable way
+ // to establish the correspondence between the `ExecutedDownward` event
+ // and the relay's sent message due to `SetTopic` instruction
+ // containing an unpredictable topic silently added by the relay's messages on the router level.
+ // This changes the message hash on arrival to our chain.
+ //
+ // See:
+ // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83
+ // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36
+ //
+ // Because of this, we insert time gaps between tests so
+ // different `ExecutedDownward` events won't interfere with each other.
+ afterEach(async () => {
+ await usingPlaygrounds(async (helper) => {
+ await helper.wait.newBlocks(3);
+ });
+ });
+
+ itSub('The relay can set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain');
+ });
+
+ itSub('The relay can batch set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch');
+ });
+
+ itSub('The relay can batchAll set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll');
+ });
+
+ itSub('The relay can forceBatch set storage', async () => {
+ await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch');
+ });
+
+ itSub('[negative] The relay cannot set balance', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain');
+ });
+
+ itSub('[negative] The relay cannot set balance via batch', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch');
+ });
+
+ itSub('[negative] The relay cannot set balance via batchAll', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll');
+ });
+
+ itSub('[negative] The relay cannot set balance via forceBatch', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch');
+ });
+
+ itSub('[negative] The relay cannot set balance via dispatchAs', async () => {
+ await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs');
});
});
tests/src/xcm/xcm.types.tsdiffbeforeafterboth1import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util';2import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';3import config from '../config';45export const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037);6export const STATEMINT_CHAIN = +(process.env.RELAY_STATEMINT_ID || 1000);7export const ACALA_CHAIN = +(process.env.RELAY_ACALA_ID || 2000);8export const MOONBEAM_CHAIN = +(process.env.RELAY_MOONBEAM_ID || 2004);9export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);10export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);1112export const acalaUrl = config.acalaUrl;13export const moonbeamUrl = config.moonbeamUrl;14export const astarUrl = config.astarUrl;15export const polkadexUrl = config.polkadexUrl;1617export const SAFE_XCM_VERSION = 3;1819export const maxWaitBlocks = 6;202122export const ASTAR_DECIMALS = 18n;23export const UNQ_DECIMALS = 18n;2425export const uniqueMultilocation = {26 parents: 1,27 interior: {28 X1: {29 Parachain: UNIQUE_CHAIN,30 },31 },32};33export const uniqueVersionedMultilocation = {34 V3: uniqueMultilocation,35};3637export const uniqueAssetId = {38 Concrete: uniqueMultilocation,39};4041export const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {42 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash43 && event.outcome.isFailedToTransactAsset);44};45export const expectUntrustedReserveLocationFail = async (helper: DevUniqueHelper, messageSent: any) => {46 await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash47 && event.outcome.isUntrustedReserveLocation);48};4950export const NETWORKS = {51 acala: usingAcalaPlaygrounds,52 astar: usingAstarPlaygrounds,53 polkadex: usingPolkadexPlaygrounds,54 moonbeam: usingMoonbeamPlaygrounds,55} as const;5657export function mapToChainId(networkName: keyof typeof NETWORKS) {58 switch (networkName) {59 case 'acala':60 return ACALA_CHAIN;61 case 'astar':62 return ASTAR_CHAIN;63 case 'moonbeam':64 return MOONBEAM_CHAIN;65 case 'polkadex':66 return POLKADEX_CHAIN;67 }68}6970export function mapToChainUrl(networkName: keyof typeof NETWORKS): string {71 switch (networkName) {72 case 'acala':73 return acalaUrl;74 case 'astar':75 return astarUrl;76 case 'moonbeam':77 return moonbeamUrl;78 case 'polkadex':79 return polkadexUrl;80 }81}8283export function getDevPlayground<T extends keyof typeof NETWORKS>(name: T) {84 return NETWORKS[name];85}tests/src/xcm/xcmQuartz.test.tsdiffbeforeafterboth--- a/tests/src/xcm/xcmQuartz.test.ts
+++ b/tests/src/xcm/xcmQuartz.test.ts
@@ -15,30 +15,15 @@
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
import {IKeyringPair} from '@polkadot/types/types';
-import config from '../config';
import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util';
import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
+import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl} from './xcm.types';
+import {hexToString} from '@polkadot/util';
-const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);
-const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);
-const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);
-const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);
-const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);
-const STATEMINE_PALLET_INSTANCE = 50;
-const relayUrl = config.relayUrl;
-const statemineUrl = config.statemineUrl;
-const karuraUrl = config.karuraUrl;
-const moonriverUrl = config.moonriverUrl;
-const shidenUrl = config.shidenUrl;
+const STATEMINE_PALLET_INSTANCE = 50;
-const RELAY_DECIMALS = 12;
-const STATEMINE_DECIMALS = 12;
-const KARURA_DECIMALS = 12;
-const SHIDEN_DECIMALS = 18n;
-const QTZ_DECIMALS = 18n;
-
const TRANSFER_AMOUNT = 2000000000000000000000000n;
const FUNDING_AMOUNT = 3_500_000_0000_000_000n;
@@ -499,7 +484,14 @@
minimalBalance: 1000000000000000000n,
};
- await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+ const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) =>
+ hexToString(v.toJSON()['symbol'])) as string[];
+
+ if(!assets.includes('QTZ')) {
+ await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+ } else {
+ console.log('QTZ token already registered on Karura assetRegistry pallet');
+ }
await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);
balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);
balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});
@@ -985,19 +977,22 @@
const unitsPerSecond = 1n;
const numAssetsWeightHint = 0;
- const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
- location: quartzAssetLocation,
- metadata: quartzAssetMetadata,
- existentialDeposit,
- isSufficient,
- unitsPerSecond,
- numAssetsWeightHint,
- });
+ if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) {
+ console.log('Quartz asset already registered on Moonriver');
+ } else {
+ const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
+ location: quartzAssetLocation,
+ metadata: quartzAssetMetadata,
+ existentialDeposit,
+ isSufficient,
+ unitsPerSecond,
+ numAssetsWeightHint,
+ });
- console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
-
- await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+ console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
+ await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+ }
// >>> Acquire Quartz AssetId Info on Moonriver >>>
console.log('Acquire Quartz AssetId Info on Moonriver.......');
@@ -1288,12 +1283,12 @@
let alice: IKeyringPair;
let sender: IKeyringPair;
- const QTZ_ASSET_ID_ON_SHIDEN = 1;
- const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n;
+ const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden
+ const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden
// Quartz -> Shiden
const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden
- const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+ const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden
const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ
const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens
@@ -1314,40 +1309,42 @@
});
await usingShidenPlaygrounds(shidenUrl, async (helper) => {
- console.log('1. Create foreign asset and metadata');
- // TODO update metadata with values from production
- await helper.assets.create(
- alice,
- QTZ_ASSET_ID_ON_SHIDEN,
- alice.address,
- QTZ_MINIMAL_BALANCE_ON_SHIDEN,
- );
+ if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) {
+ console.log('1. Create foreign asset and metadata');
+ await helper.assets.create(
+ alice,
+ QTZ_ASSET_ID_ON_SHIDEN,
+ alice.address,
+ QTZ_MINIMAL_BALANCE_ON_SHIDEN,
+ );
- await helper.assets.setMetadata(
- alice,
- QTZ_ASSET_ID_ON_SHIDEN,
- 'Cross chain QTZ',
- 'xcQTZ',
- Number(QTZ_DECIMALS),
- );
+ await helper.assets.setMetadata(
+ alice,
+ QTZ_ASSET_ID_ON_SHIDEN,
+ 'Quartz',
+ 'QTZ',
+ Number(QTZ_DECIMALS),
+ );
- console.log('2. Register asset location on Shiden');
- const assetLocation = {
- V2: {
- parents: 1,
- interior: {
- X1: {
- Parachain: QUARTZ_CHAIN,
+ console.log('2. Register asset location on Shiden');
+ const assetLocation = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: QUARTZ_CHAIN,
+ },
},
},
- },
- };
+ };
- await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
+ await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
- console.log('3. Set QTZ payment for XCM execution on Shiden');
- await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
-
+ console.log('3. Set QTZ payment for XCM execution on Shiden');
+ await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
+ } else {
+ console.log('QTZ is already registered on Shiden');
+ }
console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');
await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);
});
tests/src/xcm/xcmUnique.test.tsdiffbeforeafterboth--- a/tests/src/xcm/xcmUnique.test.ts
+++ b/tests/src/xcm/xcmUnique.test.ts
@@ -1511,12 +1511,12 @@
let alice: IKeyringPair;
let randomAccount: IKeyringPair;
- const UNQ_ASSET_ID_ON_ASTAR = 1;
- const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;
+ const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar
+ const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar
// Unique -> Astar
const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.
- const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+ const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar
const unqToAstarTransferred = 10n * (10n ** UNQ_DECIMALS); // 10 UNQ
const unqToAstarArrived = 9_999_999_999_088_000_000n; // 9.999 ... UNQ, Astar takes a commision in foreign tokens
@@ -1539,7 +1539,6 @@
await usingAstarPlaygrounds(astarUrl, async (helper) => {
if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {
console.log('1. Create foreign asset and metadata');
- // TODO update metadata with values from production
await helper.assets.create(
alice,
UNQ_ASSET_ID_ON_ASTAR,
@@ -1550,8 +1549,8 @@
await helper.assets.setMetadata(
alice,
UNQ_ASSET_ID_ON_ASTAR,
- 'Cross chain UNQ',
- 'xcUNQ',
+ 'Unique Network',
+ 'UNQ',
Number(UNQ_DECIMALS),
);