--- /dev/null +++ b/.docker/xcm-config/launch-config-xcm-unique-rococo.json @@ -0,0 +1,207 @@ +{ + "relaychain": { + "bin": "/polkadot/target/release/polkadot", + "chain": "rococo-local", + "chainInitializer": [ + "chainql", + "--tla-code=spec=import '${spec}'", + "5validators.jsonnet" + ], + "nodes": [ + { + "name": "alice", + "wsPort": 9844, + "rpcPort": 9843, + "port": 30444, + "flags": [ + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lparachain::candidate_validation=debug" + ] + }, + { + "name": "bob", + "wsPort": 9855, + "rpcPort": 9854, + "port": 30555, + "flags": [ + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lparachain::candidate_validation=debug" + ] + }, + { + "name": "charlie", + "wsPort": 9866, + "rpcPort": 9865, + "port": 30666, + "flags": [ + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lparachain::candidate_validation=debug" + ] + }, + { + "name": "dave", + "wsPort": 9877, + "rpcPort": 9876, + "port": 30777, + "flags": [ + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lparachain::candidate_validation=debug" + ] + }, + { + "name": "eve", + "wsPort": 9888, + "rpcPort": 9887, + "port": 30888, + "flags": [ + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lparachain::candidate_validation=debug" + ] + } + + ], + "genesis": { + "runtime": { + "runtime_genesis_config": { + "parachainsConfiguration": { + "config": { + "validation_upgrade_frequency": 1, + "validation_upgrade_delay": 1 + } + } + } + } + } + }, + "parachains": [ + { + "bin": "/unique-chain/target/release/unique-collator", + "id": "2037", + "balance": "1000000000000000000000000", + "nodes": [ + { + "port": 31200, + "wsPort": 9944, + "rpcPort": 9933, + "name": "alice", + "flags": [ + "-lruntime=trace", + "-lxcm=trace", + "--unsafe-rpc-external", + "--unsafe-ws-external" + ] + } + ] + }, + { + "bin": "/acala/target/release/acala", + "id": "2000", + "chain": "acala-dev", + "balance": "1000000000000000000000", + "chainInitializer": [ + "chainql", + "-e", + "(import '${spec}') {id+: '-local'}" + ], + "nodes": [ + { + "wsPort": 9946, + "port": 31202, + "name": "alice", + "flags": [ + "-lruntime=trace", + "-lxcm=trace", + "--unsafe-rpc-external", + "--unsafe-ws-external" + ] + } + ] + }, + { + "bin": "/moonbeam/target/release/moonbeam", + "id": "2004", + "balance": "1000000000000000000000", + "chain": "moonbeam-local", + "nodes": [ + { + "wsPort": 9947, + "port": 31203, + "name": "alice", + "flags": [ + "-lruntime=trace", + "-lxcm=trace", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "--", + "--execution=wasm" + ] + } + ] + }, + { + "bin": "/cumulus/target/release/cumulus", + "id": "1000", + "chain": "statemint-local", + "balance": "1000000000000000000000000", + "nodes": [ + { + "wsPort": 9948, + "port": 31204, + "name": "alice", + "flags": [ + "-lruntime=trace", + "-lxcm=trace", + "--unsafe-rpc-external", + "--unsafe-ws-external" + ] + } + ] + } + ], + "simpleParachains": [], + "hrmpChannels": [ + { + "sender": 2037, + "recipient": 2000, + "maxCapacity": 8, + "maxMessageSize": 512 + }, + { + "sender": 2000, + "recipient": 2037, + "maxCapacity": 8, + "maxMessageSize": 512 + }, + { + "sender": 2037, + "recipient": 2004, + "maxCapacity": 8, + "maxMessageSize": 512 + }, + { + "sender": 2004, + "recipient": 2037, + "maxCapacity": 8, + "maxMessageSize": 512 + }, + { + "sender": 2037, + "recipient": 1000, + "maxCapacity": 8, + "maxMessageSize": 512 + }, + { + "sender": 1000, + "recipient": 2037, + "maxCapacity": 8, + "maxMessageSize": 512 + } + ], + "finalization": false +} + --- a/.docker/xcm-config/launch-config-xcm-unique.json +++ b/.docker/xcm-config/launch-config-xcm-unique.json @@ -101,6 +101,11 @@ "id": "2000", "chain": "acala-dev", "balance": "1000000000000000000000", + "chainInitializer": [ + "chainql", + "-e", + "(import '${spec}') {id+: '-local'}" + ], "nodes": [ { "wsPort": 9946, --- a/.env +++ b/.env @@ -15,6 +15,13 @@ POLKADOT_LAUNCH_BRANCH=feature/rewrite-chain-id-in-spec -ACALA_BUILD_BRANCH=2.9.0 -MOONBEAM_BUILD_BRANCH=v0.25.0 -CUMULUS_BUILD_BRANCH=release-v0.9.230 +KARURA_BUILD_BRANCH=2.9.1 +ACALA_BUILD_BRANCH=2.9.2 + +MOONRIVER_BUILD_BRANCH=runtime-1701 +MOONBEAM_BUILD_BRANCH=runtime-1701 + +STATEMINE_BUILD_BRANCH=parachains-v9270 +STATEMINT_BUILD_BRANCH=release-parachains-v9230 +WESTMINT_BUILD_BRANCH=parachains-v9270 + --- a/.github/workflows/ci-develop.yml +++ b/.github/workflows/ci-develop.yml @@ -12,27 +12,23 @@ jobs: yarn-test-dev: - if: ${{ github.event.pull_request.draft == 'false' }} uses: ./.github/workflows/dev-build-tests_v2.yml unit-test: - if: ${{ github.event.pull_request.draft == 'false' }} uses: ./.github/workflows/unit-test_v2.yml - - forkless: - if: ${{ contains( github.event.pull_request.labels.*.name, 'forkless') }} - uses: ./.github/workflows/forkless.yml canary: if: ${{ contains( github.event.pull_request.labels.*.name, 'canary') }} uses: ./.github/workflows/canary.yml secrets: inherit # pass all secrets - xcm: if: ${{ contains( github.event.pull_request.labels.*.name, 'xcm') }} uses: ./.github/workflows/xcm.yml secrets: inherit # pass all secrets codestyle: - uses: ./.github/workflows/codestyle_v2.yml \ No newline at end of file + uses: ./.github/workflows/codestyle_v2.yml + + yarn_eslint: + uses: ./.github/workflows/test_codestyle_v2.yml --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -12,11 +12,9 @@ jobs: unit-test: - if: ${{ github.event.pull_request.draft != 'true' }} uses: ./.github/workflows/unit-test_v2.yml node-only-update: - if: ${{ github.event.pull_request.draft == 'false' }} uses: ./.github/workflows/node-only-update_v2.yml forkless: @@ -34,5 +32,4 @@ secrets: inherit # pass all secrets codestyle: - if: ${{ github.event.pull_request.draft == 'false' }} uses: ./.github/workflows/codestyle_v2.yml \ No newline at end of file --- a/.github/workflows/dev-build-tests_v2.yml +++ b/.github/workflows/dev-build-tests_v2.yml @@ -37,23 +37,6 @@ features: "unique-runtime" steps: - - name: Skip if pull request is in Draft - # `if: github.event.pull_request.draft == true` should be kept here, at - # the step level, rather than at the job level. The latter is not - # recommended because when the PR is moved from "Draft" to "Ready to - # review" the workflow will immediately be passing (since it was skipped), - # even though it hasn't actually ran, since it takes a few seconds for - # the workflow to start. This is also disclosed in: - # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 - # That scenario would open an opportunity for the check to be bypassed: - # 1. Get your PR approved - # 2. Move it to Draft - # 3. Push whatever commits you want - # 4. Move it to "Ready for review"; now the workflow is passing (it was - # skipped) and "Check reviews" is also passing (it won't be updated - # until the workflow is finished) - if: github.event.pull_request.draft == true - run: exit 1 - name: Clean Workspace uses: AutoModality/action-clean@v1.1.0 @@ -92,7 +75,6 @@ yarn install yarn add mochawesome echo "Ready to start tests" - node scripts/readyness.js NOW=$(date +%s) && yarn test --reporter mochawesome --reporter-options reportFilename=test-${NOW} env: RPC_URL: http://127.0.0.1:9933/ --- a/.github/workflows/test_codestyle_v2.yml +++ b/.github/workflows/test_codestyle_v2.yml @@ -5,7 +5,7 @@ jobs: code_style: - runs-on: self-hosted-ci + runs-on: [ self-hosted-ci ] steps: - uses: actions/checkout@v3 --- a/.github/workflows/xcm-testnet-build.yml +++ b/.github/workflows/xcm-testnet-build.yml @@ -18,7 +18,7 @@ name: Prepare execution matrix - runs-on: XL + runs-on: [XL] outputs: matrix: ${{ steps.create_matrix.outputs.matrix }} @@ -40,9 +40,9 @@ id: create_matrix with: matrix: | - network {opal}, runtime {opal}, features {opal-runtime} - network {quartz}, runtime {quartz}, features {quartz-runtime} - network {unique}, runtime {unique}, features {unique-runtime} + network {opal}, runtime {opal}, features {opal-runtime}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.WESTMINT_BUILD_BRANCH }}} + network {quartz}, runtime {quartz}, features {quartz-runtime}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}} + network {unique}, runtime {unique}, features {unique-runtime}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINT_BUILD_BRANCH }}} xcm-build: @@ -89,9 +89,9 @@ FEATURE=${{ matrix.features }} RUNTIME=${{ matrix.runtime }} BRANCH=${{ github.head_ref }} - ACALA_BUILD_BRANCH=${{ env.ACALA_BUILD_BRANCH }} - MOONBEAM_BUILD_BRANCH=${{ env.MOONBEAM_BUILD_BRANCH }} - CUMULUS_BUILD_BRANCH=${{ env.CUMULUS_BUILD_BRANCH }} + ACALA_BUILD_BRANCH=${{ matrix.acala_version }} + MOONBEAM_BUILD_BRANCH=${{ matrix.moonbeam_version }} + CUMULUS_BUILD_BRANCH=${{ matrix.cumulus_version }} - name: Show build Dockerfile run: cat .docker/Dockerfile-xcm.${{ matrix.network }}.yml @@ -114,13 +114,13 @@ password: ${{ secrets.CORE_DOCKERHUB_TOKEN }} - name: Pull acala docker image - run: docker pull uniquenetwork/builder-acala:${{ env.ACALA_BUILD_BRANCH }} + run: docker pull uniquenetwork/builder-acala:${{ matrix.acala_version }} - name: Pull moonbeam docker image - run: docker pull uniquenetwork/builder-moonbeam:${{ env.MOONBEAM_BUILD_BRANCH }} + run: docker pull uniquenetwork/builder-moonbeam:${{ matrix.moonbeam_version }} - name: Pull cumulus docker image - run: docker pull uniquenetwork/builder-cumulus:${{ env.CUMULUS_BUILD_BRANCH }} + run: docker pull uniquenetwork/builder-cumulus:${{ matrix.cumulus_version }} - name: Pull polkadot docker image run: docker pull uniquenetwork/builder-polkadot:${{ env.POLKADOT_BUILD_BRANCH }} --- a/.github/workflows/xcm-tests_v2.yml +++ b/.github/workflows/xcm-tests_v2.yml @@ -146,6 +146,7 @@ run: | yarn install yarn add mochawesome + node scripts/readyness.js echo "Ready to start tests" NOW=$(date +%s) && yarn ${{ matrix.runtest }} --reporter mochawesome --reporter-options reportFilename=test-${NOW} --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -302,7 +302,7 @@ [dependencies] futures = '0.3.17' -log = '0.4.14' +log = '0.4.16' flexi_logger = "0.22.5" parking_lot = '0.12.1' clap = "3.1.2" --- a/pallets/evm-contract-helpers/Cargo.toml +++ b/pallets/evm-contract-helpers/Cargo.toml @@ -8,7 +8,7 @@ scale-info = { version = "2.0.1", default-features = false, features = [ "derive", ] } -log = { default-features = false, version = "0.4.14" } +log = { default-features = false, version = "0.4.16" } # Substrate frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.27" } --- a/pallets/foreing-assets/Cargo.toml +++ b/pallets/foreing-assets/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] -log = { version = "0.4.14", default-features = false } +log = { version = "0.4.16", default-features = false } serde = { version = "1.0.136", optional = true } scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } --- a/pallets/scheduler/Cargo.toml +++ b/pallets/scheduler/Cargo.toml @@ -25,7 +25,7 @@ frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.27" } up-sponsorship = { version = "0.1.0", default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.27" } -log = { version = "0.4.14", default-features = false } +log = { version = "0.4.16", default-features = false } [dev-dependencies] sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.27" } --- a/runtime/common/construct_runtime/mod.rs +++ b/runtime/common/construct_runtime/mod.rs @@ -42,7 +42,7 @@ Treasury: pallet_treasury::{Pallet, Call, Storage, Config, Event} = 34, Sudo: pallet_sudo::{Pallet, Call, Storage, Config, Event} = 35, Vesting: orml_vesting::{Pallet, Storage, Call, Event, Config} = 37, - #[runtimes(opal)] + XTokens: orml_xtokens = 38, Tokens: orml_tokens = 39, // Vesting: pallet_vesting::{Pallet, Call, Config, Storage, Event} = 37, --- a/runtime/opal/src/xcm_config.rs +++ b/runtime/opal/src/xcm_config.rs @@ -498,7 +498,7 @@ impl orml_xtokens::Config for Runtime { type Event = Event; type Balance = Balance; - type CurrencyId = AssetIds; + type CurrencyId = CurrencyId; type CurrencyIdConvert = CurrencyIdConvert; type AccountIdToMultiLocation = AccountIdToMultiLocation; type SelfLocation = SelfLocation; --- a/runtime/quartz/src/xcm_config.rs +++ b/runtime/quartz/src/xcm_config.rs @@ -180,11 +180,10 @@ type OnKilledTokenAccount = (); } -/* impl orml_xtokens::Config for Runtime { type Event = Event; type Balance = Balance; - type CurrencyId = AssetIds; + type CurrencyId = CurrencyId; type CurrencyIdConvert = CurrencyIdConvert; type AccountIdToMultiLocation = AccountIdToMultiLocation; type SelfLocation = SelfLocation; @@ -197,8 +196,8 @@ type MultiLocationsFilter = Everything; type ReserveProvider = AbsoluteReserveProvider; } - */ + parameter_type_with_key! { pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance { match currency_id { @@ -218,7 +217,7 @@ } } -/* + pub struct CurrencyIdConvert; impl Convert> for CurrencyIdConvert { fn convert(id: AssetIds) -> Option { @@ -228,12 +227,11 @@ X1(Parachain(ParachainInfo::get().into())), )), AssetIds::NativeAssetId(NativeCurrency::Parent) => Some(MultiLocation::parent()), - AssetIds::ForeignAssetId(foreign_asset_id) => { - XcmForeignAssetIdMapping::::get_multi_location(foreign_asset_id) - } + AssetIds::ForeignAssetId(_) => None, } } } +/* impl Convert> for CurrencyIdConvert { fn convert(location: MultiLocation) -> Option { if location == MultiLocation::here() --- a/runtime/unique/src/xcm_config.rs +++ b/runtime/unique/src/xcm_config.rs @@ -88,6 +88,8 @@ MultiLocation { parents: 1, interior: Here }, // Karura/Acala location MultiLocation { parents: 1, interior: X1(Parachain(2000)) }, + // Moonbeam location + MultiLocation { parents: 1, interior: X1(Parachain(2004)) }, // Self parachain address MultiLocation { parents: 1, interior: X1(Parachain(ParachainInfo::get().into())) }, ] @@ -201,10 +203,42 @@ type OnKilledTokenAccount = (); } +impl orml_xtokens::Config for Runtime { + type Event = Event; + type Balance = Balance; + type CurrencyId = CurrencyId; + type CurrencyIdConvert = CurrencyIdConvert; + type AccountIdToMultiLocation = AccountIdToMultiLocation; + type SelfLocation = SelfLocation; + type XcmExecutor = XcmExecutor>; + type Weigher = FixedWeightBounds; + type BaseXcmWeight = BaseXcmWeight; + type LocationInverter = LocationInverter; + type MaxAssetsForTransfer = MaxAssetsForTransfer; + type MinXcmFee = ParachainMinFee; + type MultiLocationsFilter = Everything; + type ReserveProvider = AbsoluteReserveProvider; +} + +pub struct CurrencyIdConvert; +impl Convert> for CurrencyIdConvert { + fn convert(id: AssetIds) -> Option { + match id { + AssetIds::NativeAssetId(NativeCurrency::Here) => Some(MultiLocation::new( + 1, + X1(Parachain(ParachainInfo::get().into())), + )), + _ => None, + } + } +} parameter_types! { pub const BaseXcmWeight: Weight = 100_000_000; // TODO: recheck this pub const MaxAssetsForTransfer: usize = 2; + + pub Ancestry: MultiLocation = Parachain(ParachainInfo::parachain_id().into()).into(); + pub SelfLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(ParachainInfo::get().into()))); } parameter_type_with_key! { @@ -223,4 +257,4 @@ }) .into() } -} \ No newline at end of file +} --- a/tests/package.json +++ b/tests/package.json @@ -74,11 +74,12 @@ "testInflation": "mocha --timeout 9999999 -r ts-node/register ./**/inflation.test.ts", "testScheduler": "mocha --timeout 9999999 -r ts-node/register ./**/scheduler.test.ts", "testSchedulingEVM": "mocha --timeout 9999999 -r ts-node/register ./**/eth/scheduling.test.ts", - "testXcmUnique": "mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmUnique.test.ts", + "testXcmUnique": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmUnique.test.ts", + "testXcmQuartz": "RUN_XCM_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmQuartz.test.ts", "testXcmOpal": "mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmOpal.test.ts", "testXcmTransferAcala": "mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000", "testXcmTransferStatemine": "mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000", - "testXcmTransferMoonbeam": "mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmTransferMoonbeam.test.ts moonbeamId=2000 uniqueId=5000", + "testXcmTransferMoonbeam": "mocha --timeout 9999999 -r ts-node/register ./**/xcm/xcmTransferMoonbeam.test.ts", "testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts", "testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts", "testEnableDisableTransfers": "mocha --timeout 9999999 -r ts-node/register ./**/enableDisableTransfer.test.ts", --- a/tests/scripts/readyness.js +++ b/tests/scripts/readyness.js @@ -6,9 +6,9 @@ await api.isReadyOrError; const head = (await api.rpc.chain.getHeader()).number.toNumber(); + await api.disconnect(); if(head < 1) throw Error('No block #1'); - await api.disconnect(); } const sleep = time => { --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -85,7 +85,7 @@ for (const arg of args) { if (typeof arg !== 'string') continue; - if (arg.includes('1000:: Normal connection closure' || arg === 'Normal connection closure')) + if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure') return; } printer(...args); @@ -169,13 +169,24 @@ /* eslint no-async-promise-executor: "off" */ return new Promise(async (resolve, reject) => { try { - await transaction.signAndSend(sender, ({events = [], status}) => { + await transaction.signAndSend(sender, ({events = [], status, dispatchError}) => { const transactionStatus = getTransactionStatus(events, status); if (transactionStatus === TransactionStatus.Success) { resolve(events); } else if (transactionStatus === TransactionStatus.Fail) { - console.log(`Something went wrong with transaction. Status: ${status}`); + let moduleError = null; + + if (dispatchError) { + if (dispatchError.isModule) { + const modErr = dispatchError.asModule; + const errorMeta = dispatchError.registry.findMetaError(modErr); + + moduleError = JSON.stringify(errorMeta, null, 4); + } + } + + console.log(`Something went wrong with transaction. Status: ${status}\nModule error: ${moduleError}`); reject(events); } }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -1746,6 +1746,12 @@ return (await api.rpc.unique.collectionById(collectionId)).unwrap(); } +export const describe_xcm = ( + process.env.RUN_XCM_TESTS + ? describe + : describe.skip +); + export async function waitNewBlocks(blocksCount = 1): Promise { await usingApi(async (api) => { const promise = new Promise(async (resolve) => { @@ -1762,6 +1768,35 @@ }); } +export async function waitEvent( + api: ApiPromise, + maxBlocksToWait: number, + eventSection: string, + eventMethod: string, +): Promise { + + const promise = new Promise(async (resolve) => { + const unsubscribe = await api.query.system.events(eventRecords => { + const neededEvent = eventRecords.find(r => { + return r.event.section == eventSection && r.event.method == eventMethod; + }); + + if (neededEvent) { + unsubscribe(); + resolve(neededEvent); + } + + if (maxBlocksToWait > 0) { + maxBlocksToWait--; + } else { + unsubscribe(); + resolve(null); + } + }); + }); + return promise; +} + export async function repartitionRFT( api: ApiPromise, collectionId: number, --- a/tests/src/util/playgrounds/index.ts +++ b/tests/src/util/playgrounds/index.ts @@ -26,7 +26,7 @@ for (const arg of args) { if (typeof arg !== 'string') continue; - if (arg.includes('1000:: Normal connection closure' || arg === 'Normal connection closure')) + if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure') return; } printer(...args); @@ -49,4 +49,4 @@ console.log = consoleLog; console.warn = consoleWarn; } -}; \ No newline at end of file +}; --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -103,7 +103,6 @@ let accountsCreated = false; // checkBalances retry up to 5 blocks for (let index = 0; index < 5; index++) { - console.log(await this.helper.chain.getLatestBlockNumber()); accountsCreated = await checkBalances(); if(accountsCreated) break; await this.waitNewBlocks(1); --- a/tests/src/xcm/xcmOpal.test.ts +++ b/tests/src/xcm/xcmOpal.test.ts @@ -35,7 +35,7 @@ const RELAY_PORT = '9844'; const UNIQUE_PORT = '9944'; -const STATEMINE_PORT = '9946'; +const STATEMINE_PORT = '9948'; const STATEMINE_PALLET_INSTANCE = 50; const ASSET_ID = 100; const ASSET_METADATA_DECIMALS = 18; --- /dev/null +++ b/tests/src/xcm/xcmQuartz.test.ts @@ -0,0 +1,682 @@ +// 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 . + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import {WsProvider, Keyring} from '@polkadot/api'; +import {ApiOptions} from '@polkadot/api/types'; +import {IKeyringPair} from '@polkadot/types/types'; +import usingApi, {submitTransactionAsync} from '../substrate/substrate-api'; +import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm} from '../util/helpers'; +import {MultiLocation} from '@polkadot/types/interfaces'; +import {blake2AsHex} from '@polkadot/util-crypto'; +import waitNewBlocks from '../substrate/wait-new-blocks'; +import getBalance from '../substrate/get-balance'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const QUARTZ_CHAIN = 2095; +const KARURA_CHAIN = 2000; +const MOONRIVER_CHAIN = 2023; + +const KARURA_PORT = 9946; +const MOONRIVER_PORT = 9947; + +const TRANSFER_AMOUNT = 2000000000000000000000000n; + +function parachainApiOptions(port: number): ApiOptions { + return { + provider: new WsProvider('ws://127.0.0.1:' + port.toString()), + }; +} + +function karuraOptions(): ApiOptions { + return parachainApiOptions(KARURA_PORT); +} + +function moonriverOptions(): ApiOptions { + return parachainApiOptions(MOONRIVER_PORT); +} + +describe_xcm('Integration test: Exchanging tokens with Karura', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + + let balanceQuartzTokenInit: bigint; + let balanceQuartzTokenMiddle: bigint; + let balanceQuartzTokenFinal: bigint; + let balanceKaruraTokenInit: bigint; + let balanceKaruraTokenMiddle: bigint; + let balanceKaruraTokenFinal: bigint; + let balanceQuartzForeignTokenInit: bigint; + let balanceQuartzForeignTokenMiddle: bigint; + let balanceQuartzForeignTokenFinal: bigint; + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + alice = privateKeyWrapper('//Alice'); + randomAccount = generateKeyringPair(); + }); + + // Karura side + await usingApi( + async (api) => { + const destination = { + V0: { + X2: [ + 'Parent', + { + Parachain: QUARTZ_CHAIN, + }, + ], + }, + }; + + const metadata = { + name: 'QTZ', + symbol: 'QTZ', + decimals: 18, + minimalBalance: 1, + }; + + const tx = api.tx.assetRegistry.registerForeignAsset(destination, metadata); + const sudoTx = api.tx.sudo.sudo(tx as any); + const events = await submitTransactionAsync(alice, sudoTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + const tx1 = api.tx.balances.transfer(randomAccount.address, 10000000000000n); + const events1 = await submitTransactionAsync(alice, tx1); + const result1 = getGenericResult(events1); + expect(result1.success).to.be.true; + + [balanceKaruraTokenInit] = await getBalance(api, [randomAccount.address]); + { + const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any; + balanceQuartzForeignTokenInit = BigInt(free); + } + }, + karuraOptions(), + ); + + // Quartz side + await usingApi(async (api) => { + const tx0 = api.tx.balances.transfer(randomAccount.address, 10n * TRANSFER_AMOUNT); + const events0 = await submitTransactionAsync(alice, tx0); + const result0 = getGenericResult(events0); + expect(result0.success).to.be.true; + + [balanceQuartzTokenInit] = await getBalance(api, [randomAccount.address]); + }); + }); + + it('Should connect and send QTZ to Karura', async () => { + + // Quartz side + await usingApi(async (api) => { + + const destination = { + V0: { + X2: [ + 'Parent', + { + Parachain: KARURA_CHAIN, + }, + ], + }, + }; + + const beneficiary = { + V0: { + X1: { + AccountId32: { + network: 'Any', + id: randomAccount.addressRaw, + }, + }, + }, + }; + + const assets = { + V1: [ + { + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: TRANSFER_AMOUNT, + }, + }, + ], + }; + + const feeAssetItem = 0; + + const weightLimit = { + Limited: 5000000000, + }; + + const tx = api.tx.polkadotXcm.limitedReserveTransferAssets(destination, beneficiary, assets, feeAssetItem, weightLimit); + const events = await submitTransactionAsync(randomAccount, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + [balanceQuartzTokenMiddle] = await getBalance(api, [randomAccount.address]); + + const qtzFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT; + console.log('[Quartz -> Karura] transaction fees on Quartz: %s QTZ', qtzFees); + expect(qtzFees > 0n).to.be.true; + }); + + // Karura side + await usingApi( + async (api) => { + await waitNewBlocks(api, 3); + const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any; + balanceQuartzForeignTokenMiddle = BigInt(free); + + [balanceKaruraTokenMiddle] = await getBalance(api, [randomAccount.address]); + + const karFees = balanceKaruraTokenInit - balanceKaruraTokenMiddle; + const qtzIncomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenInit; + + console.log('[Quartz -> Karura] transaction fees on Karura: %s KAR', karFees); + console.log('[Quartz -> Karura] income %s QTZ', qtzIncomeTransfer); + expect(karFees == 0n).to.be.true; + expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true; + }, + karuraOptions(), + ); + }); + + it('Should connect to Karura and send QTZ back', async () => { + + // Karura side + await usingApi( + async (api) => { + const destination = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: QUARTZ_CHAIN}, + { + AccountId32: { + network: 'Any', + id: randomAccount.addressRaw, + }, + }, + ], + }, + }, + }; + + const id = { + ForeignAsset: 0, + }; + + const destWeight = 50000000; + + const tx = api.tx.xTokens.transfer(id, TRANSFER_AMOUNT, destination, destWeight); + const events = await submitTransactionAsync(randomAccount, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + [balanceKaruraTokenFinal] = await getBalance(api, [randomAccount.address]); + { + const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any; + balanceQuartzForeignTokenFinal = BigInt(free); + } + + const karFees = balanceKaruraTokenMiddle - balanceKaruraTokenFinal; + const qtzOutcomeTransfer = balanceQuartzForeignTokenMiddle - balanceQuartzForeignTokenFinal; + + console.log('[Karura -> Quartz] transaction fees on Karura: %s KAR', karFees); + console.log('[Karura -> Quartz] outcome %s QTZ', qtzOutcomeTransfer); + + expect(karFees > 0).to.be.true; + expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true; + }, + karuraOptions(), + ); + + // Quartz side + await usingApi(async (api) => { + await waitNewBlocks(api, 3); + + [balanceQuartzTokenFinal] = await getBalance(api, [randomAccount.address]); + const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle; + expect(actuallyDelivered > 0).to.be.true; + + console.log('[Karura -> Quartz] actually delivered %s QTZ', actuallyDelivered); + + const qtzFees = TRANSFER_AMOUNT - actuallyDelivered; + console.log('[Karura -> Quartz] transaction fees on Quartz: %s QTZ', qtzFees); + expect(qtzFees == 0n).to.be.true; + }); + }); + + it('Quartz rejects KAR tokens from Karura', async () => { + // This test is relevant only when the foreign asset pallet is disabled + + await usingApi(async (api) => { + const destination = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: QUARTZ_CHAIN}, + { + AccountId32: { + network: 'Any', + id: randomAccount.addressRaw, + }, + }, + ], + }, + }, + }; + + const id = { + Token: 'KAR', + }; + + const destWeight = 50000000; + + const tx = api.tx.xTokens.transfer(id, 100_000_000_000, destination, destWeight); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + }, karuraOptions()); + + await usingApi(async api => { + const maxWaitBlocks = 3; + const xcmpQueueFailEvent = await waitEvent(api, maxWaitBlocks, 'xcmpQueue', 'Fail'); + + expect( + xcmpQueueFailEvent != null, + 'Only native token is supported when the Foreign-Assets pallet is not connected', + ).to.be.true; + }); + }); +}); + +describe_xcm('Integration test: Exchanging QTZ with Moonriver', () => { + + // Quartz constants + let quartzAlice: IKeyringPair; + let quartzAssetLocation; + + let randomAccountQuartz: IKeyringPair; + let randomAccountMoonriver: IKeyringPair; + + // Moonriver constants + let assetId: string; + + const moonriverKeyring = new Keyring({type: 'ethereum'}); + const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133'; + const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b'; + const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68'; + + const alithAccount = moonriverKeyring.addFromUri(alithPrivateKey, undefined, 'ethereum'); + const baltatharAccount = moonriverKeyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum'); + const dorothyAccount = moonriverKeyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum'); + + const councilVotingThreshold = 2; + const technicalCommitteeThreshold = 2; + const votingPeriod = 3; + const delayPeriod = 0; + + const quartzAssetMetadata = { + name: 'xcQuartz', + symbol: 'xcQTZ', + decimals: 18, + isFrozen: false, + minimalBalance: 1, + }; + + let balanceQuartzTokenInit: bigint; + let balanceQuartzTokenMiddle: bigint; + let balanceQuartzTokenFinal: bigint; + let balanceForeignQtzTokenInit: bigint; + let balanceForeignQtzTokenMiddle: bigint; + let balanceForeignQtzTokenFinal: bigint; + let balanceMovrTokenInit: bigint; + let balanceMovrTokenMiddle: bigint; + let balanceMovrTokenFinal: bigint; + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + quartzAlice = privateKeyWrapper('//Alice'); + randomAccountQuartz = generateKeyringPair(); + randomAccountMoonriver = generateKeyringPair('ethereum'); + + balanceForeignQtzTokenInit = 0n; + }); + + await usingApi( + async (api) => { + + // >>> Sponsoring Dorothy >>> + console.log('Sponsoring Dorothy.......'); + const tx0 = api.tx.balances.transfer(dorothyAccount.address, 11_000_000_000_000_000_000n); + const events0 = await submitTransactionAsync(alithAccount, tx0); + const result0 = getGenericResult(events0); + expect(result0.success).to.be.true; + console.log('Sponsoring Dorothy.......DONE'); + // <<< Sponsoring Dorothy <<< + + const sourceLocation: MultiLocation = api.createType( + 'MultiLocation', + { + parents: 1, + interior: {X1: {Parachain: QUARTZ_CHAIN}}, + }, + ); + + quartzAssetLocation = {XCM: sourceLocation}; + const existentialDeposit = 1; + const isSufficient = true; + const unitsPerSecond = '1'; + const numAssetsWeightHint = 0; + + const registerTx = api.tx.assetManager.registerForeignAsset( + quartzAssetLocation, + quartzAssetMetadata, + existentialDeposit, + isSufficient, + ); + console.log('Encoded proposal for registerAsset is %s', registerTx.method.toHex() || ''); + + const setUnitsTx = api.tx.assetManager.setAssetUnitsPerSecond( + quartzAssetLocation, + unitsPerSecond, + numAssetsWeightHint, + ); + console.log('Encoded proposal for setAssetUnitsPerSecond is %s', setUnitsTx.method.toHex() || ''); + + const batchCall = api.tx.utility.batchAll([registerTx, setUnitsTx]); + console.log('Encoded proposal for batchCall is %s', batchCall.method.toHex() || ''); + + // >>> Note motion preimage >>> + console.log('Note motion preimage.......'); + const encodedProposal = batchCall?.method.toHex() || ''; + const proposalHash = blake2AsHex(encodedProposal); + console.log('Encoded proposal for batch utility after schedule is %s', encodedProposal); + console.log('Encoded proposal hash for batch utility after schedule is %s', proposalHash); + console.log('Encoded length %d', encodedProposal.length); + + const tx1 = api.tx.democracy.notePreimage(encodedProposal); + const events1 = await submitTransactionAsync(baltatharAccount, tx1); + const result1 = getGenericResult(events1); + expect(result1.success).to.be.true; + console.log('Note motion preimage.......DONE'); + // <<< Note motion preimage <<< + + // >>> Propose external motion through council >>> + console.log('Propose external motion through council.......'); + const externalMotion = api.tx.democracy.externalProposeMajority(proposalHash); + const tx2 = api.tx.councilCollective.propose( + councilVotingThreshold, + externalMotion, + externalMotion.encodedLength, + ); + const events2 = await submitTransactionAsync(baltatharAccount, tx2); + const result2 = getGenericResult(events2); + expect(result2.success).to.be.true; + + const encodedMotion = externalMotion?.method.toHex() || ''; + const motionHash = blake2AsHex(encodedMotion); + console.log('Motion hash is %s', motionHash); + + const tx3 = api.tx.councilCollective.vote(motionHash, 0, true); + { + const events3 = await submitTransactionAsync(dorothyAccount, tx3); + const result3 = getGenericResult(events3); + expect(result3.success).to.be.true; + } + { + const events3 = await submitTransactionAsync(baltatharAccount, tx3); + const result3 = getGenericResult(events3); + expect(result3.success).to.be.true; + } + + const tx4 = api.tx.councilCollective.close(motionHash, 0, 1_000_000_000, externalMotion.encodedLength); + const events4 = await submitTransactionAsync(dorothyAccount, tx4); + const result4 = getGenericResult(events4); + expect(result4.success).to.be.true; + console.log('Propose external motion through council.......DONE'); + // <<< Propose external motion through council <<< + + // >>> Fast track proposal through technical committee >>> + console.log('Fast track proposal through technical committee.......'); + const fastTrack = api.tx.democracy.fastTrack(proposalHash, votingPeriod, delayPeriod); + const tx5 = api.tx.techCommitteeCollective.propose( + technicalCommitteeThreshold, + fastTrack, + fastTrack.encodedLength, + ); + const events5 = await submitTransactionAsync(alithAccount, tx5); + const result5 = getGenericResult(events5); + expect(result5.success).to.be.true; + + const encodedFastTrack = fastTrack?.method.toHex() || ''; + const fastTrackHash = blake2AsHex(encodedFastTrack); + console.log('FastTrack hash is %s', fastTrackHash); + + const proposalIdx = Number(await api.query.techCommitteeCollective.proposalCount()) - 1; + const tx6 = api.tx.techCommitteeCollective.vote(fastTrackHash, proposalIdx, true); + { + const events6 = await submitTransactionAsync(baltatharAccount, tx6); + const result6 = getGenericResult(events6); + expect(result6.success).to.be.true; + } + { + const events6 = await submitTransactionAsync(alithAccount, tx6); + const result6 = getGenericResult(events6); + expect(result6.success).to.be.true; + } + + const tx7 = api.tx.techCommitteeCollective + .close(fastTrackHash, proposalIdx, 1_000_000_000, fastTrack.encodedLength); + const events7 = await submitTransactionAsync(baltatharAccount, tx7); + const result7 = getGenericResult(events7); + expect(result7.success).to.be.true; + console.log('Fast track proposal through technical committee.......DONE'); + // <<< Fast track proposal through technical committee <<< + + // >>> Referendum voting >>> + console.log('Referendum voting.......'); + const tx8 = api.tx.democracy.vote( + 0, + {Standard: {balance: 10_000_000_000_000_000_000n, vote: {aye: true, conviction: 1}}}, + ); + const events8 = await submitTransactionAsync(dorothyAccount, tx8); + const result8 = getGenericResult(events8); + expect(result8.success).to.be.true; + console.log('Referendum voting.......DONE'); + // <<< Referendum voting <<< + + // >>> Acquire Quartz AssetId Info on Moonriver >>> + console.log('Acquire Quartz AssetId Info on Moonriver.......'); + + // Wait for the democracy execute + await waitNewBlocks(api, 5); + + assetId = (await api.query.assetManager.assetTypeId({ + XCM: sourceLocation, + })).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.......'); + const tx10 = api.tx.balances.transfer(randomAccountMoonriver.address, 11_000_000_000_000_000_000n); + const events10 = await submitTransactionAsync(baltatharAccount, tx10); + const result10 = getGenericResult(events10); + expect(result10.success).to.be.true; + console.log('Sponsoring random Account.......DONE'); + // <<< Sponsoring random Account <<< + + [balanceMovrTokenInit] = await getBalance(api, [randomAccountMoonriver.address]); + }, + moonriverOptions(), + ); + + await usingApi(async (api) => { + const tx0 = api.tx.balances.transfer(randomAccountQuartz.address, 10n * TRANSFER_AMOUNT); + const events0 = await submitTransactionAsync(quartzAlice, tx0); + const result0 = getGenericResult(events0); + expect(result0.success).to.be.true; + + [balanceQuartzTokenInit] = await getBalance(api, [randomAccountQuartz.address]); + }); + }); + + it('Should connect and send QTZ to Moonriver', async () => { + await usingApi(async (api) => { + const currencyId = { + NativeAssetId: 'Here', + }; + const dest = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: MOONRIVER_CHAIN}, + {AccountKey20: {network: 'Any', key: randomAccountMoonriver.address}}, + ], + }, + }, + }; + const amount = TRANSFER_AMOUNT; + const destWeight = 850000000; + + const tx = api.tx.xTokens.transfer(currencyId, amount, dest, destWeight); + const events = await submitTransactionAsync(randomAccountQuartz, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + [balanceQuartzTokenMiddle] = await getBalance(api, [randomAccountQuartz.address]); + expect(balanceQuartzTokenMiddle < balanceQuartzTokenInit).to.be.true; + + const transactionFees = balanceQuartzTokenInit - balanceQuartzTokenMiddle - TRANSFER_AMOUNT; + console.log('[Quartz -> Moonriver] transaction fees on Quartz: %s QTZ', transactionFees); + expect(transactionFees > 0).to.be.true; + }); + + await usingApi( + async (api) => { + await waitNewBlocks(api, 3); + + [balanceMovrTokenMiddle] = await getBalance(api, [randomAccountMoonriver.address]); + + const movrFees = balanceMovrTokenInit - balanceMovrTokenMiddle; + console.log('[Quartz -> Moonriver] transaction fees on Moonriver: %s MOVR', movrFees); + expect(movrFees == 0n).to.be.true; + + const qtzRandomAccountAsset = ( + await api.query.assets.account(assetId, randomAccountMoonriver.address) + ).toJSON()! as any; + + balanceForeignQtzTokenMiddle = BigInt(qtzRandomAccountAsset['balance']); + const qtzIncomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenInit; + console.log('[Quartz -> Moonriver] income %s QTZ', qtzIncomeTransfer); + expect(qtzIncomeTransfer == TRANSFER_AMOUNT).to.be.true; + }, + moonriverOptions(), + ); + }); + + it('Should connect to Moonriver and send QTZ back', async () => { + await usingApi( + async (api) => { + const asset = { + V1: { + id: { + Concrete: { + parents: 1, + interior: { + X1: {Parachain: QUARTZ_CHAIN}, + }, + }, + }, + fun: { + Fungible: TRANSFER_AMOUNT, + }, + }, + }; + const destination = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: QUARTZ_CHAIN}, + {AccountId32: {network: 'Any', id: randomAccountQuartz.addressRaw}}, + ], + }, + }, + }; + const destWeight = 50000000; + + const tx = api.tx.xTokens.transferMultiasset(asset, destination, destWeight); + const events = await submitTransactionAsync(randomAccountMoonriver, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + [balanceMovrTokenFinal] = await getBalance(api, [randomAccountMoonriver.address]); + + const movrFees = balanceMovrTokenMiddle - balanceMovrTokenFinal; + console.log('[Moonriver -> Quartz] transaction fees on Moonriver: %s MOVR', movrFees); + expect(movrFees > 0).to.be.true; + + const qtzRandomAccountAsset = ( + await api.query.assets.account(assetId, randomAccountMoonriver.address) + ).toJSON()! as any; + + expect(qtzRandomAccountAsset).to.be.null; + + balanceForeignQtzTokenFinal = 0n; + + const qtzOutcomeTransfer = balanceForeignQtzTokenMiddle - balanceForeignQtzTokenFinal; + console.log('[Quartz -> Moonriver] outcome %s QTZ', qtzOutcomeTransfer); + expect(qtzOutcomeTransfer == TRANSFER_AMOUNT).to.be.true; + }, + moonriverOptions(), + ); + + await usingApi(async (api) => { + await waitNewBlocks(api, 3); + + [balanceQuartzTokenFinal] = await getBalance(api, [randomAccountQuartz.address]); + const actuallyDelivered = balanceQuartzTokenFinal - balanceQuartzTokenMiddle; + expect(actuallyDelivered > 0).to.be.true; + + console.log('[Moonriver -> Quartz] actually delivered %s QTZ', actuallyDelivered); + + const qtzFees = TRANSFER_AMOUNT - actuallyDelivered; + console.log('[Moonriver -> Quartz] transaction fees on Quartz: %s QTZ', qtzFees); + expect(qtzFees == 0n).to.be.true; + }); + }); +}); --- a/tests/src/xcm/xcmTransferMoonbeam.test.ts +++ b/tests/src/xcm/xcmTransferMoonbeam.test.ts @@ -50,7 +50,7 @@ }); const UNIQUE_PORT = '9944'; -const MOONBEAM_PORT = '9946'; +const MOONBEAM_PORT = '9947'; const TRANSFER_AMOUNT = 2000000000000000000000000n; describe('Integration test: Exchanging UNQ with Moonbeam', () => { --- a/tests/src/xcm/xcmTransferStatemine.test.ts +++ b/tests/src/xcm/xcmTransferStatemine.test.ts @@ -53,7 +53,7 @@ const RELAY_PORT = '9844'; const UNIQUE_PORT = '9944'; -const STATEMINE_PORT = '9946'; +const STATEMINE_PORT = '9948'; const STATEMINE_PALLET_INSTANCE = 50; const ASSET_ID = 100; const ASSET_METADATA_DECIMALS = 18; --- a/tests/src/xcm/xcmUnique.test.ts +++ b/tests/src/xcm/xcmUnique.test.ts @@ -17,11 +17,13 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import {WsProvider} from '@polkadot/api'; +import {WsProvider, Keyring} from '@polkadot/api'; import {ApiOptions} from '@polkadot/api/types'; import {IKeyringPair} from '@polkadot/types/types'; import usingApi, {submitTransactionAsync} from '../substrate/substrate-api'; -import {getGenericResult, generateKeyringPair} from '../util/helpers'; +import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm} from '../util/helpers'; +import {MultiLocation} from '@polkadot/types/interfaces'; +import {blake2AsHex} from '@polkadot/util-crypto'; import waitNewBlocks from '../substrate/wait-new-blocks'; import getBalance from '../substrate/get-balance'; @@ -30,23 +32,39 @@ const UNIQUE_CHAIN = 2037; const ACALA_CHAIN = 2000; +const MOONBEAM_CHAIN = 2004; -const ACALA_PORT = '9946'; +const ACALA_PORT = 9946; +const MOONBEAM_PORT = 9947; const TRANSFER_AMOUNT = 2000000000000000000000000n; -describe('Integration test: Exchanging UNQ with Acala', () => { +function parachainApiOptions(port: number): ApiOptions { + return { + provider: new WsProvider('ws://127.0.0.1:' + port.toString()), + }; +} + +function acalaOptions(): ApiOptions { + return parachainApiOptions(ACALA_PORT); +} + +function moonbeamOptions(): ApiOptions { + return parachainApiOptions(MOONBEAM_PORT); +} + +describe_xcm('Integration test: Exchanging tokens with Acala', () => { let alice: IKeyringPair; let randomAccount: IKeyringPair; - let balanceUniqueTokenBefore: bigint; - let balanceUniqueTokenAfter: bigint; + let balanceUniqueTokenInit: bigint; + let balanceUniqueTokenMiddle: bigint; let balanceUniqueTokenFinal: bigint; - let balanceAcalaTokenBefore: bigint; - let balanceAcalaTokenAfter: bigint; + let balanceAcalaTokenInit: bigint; + let balanceAcalaTokenMiddle: bigint; let balanceAcalaTokenFinal: bigint; - let balanceUniqueForeignTokenAfter: bigint; - let balanceUniqueForeignTokenBefore: bigint; + let balanceUniqueForeignTokenInit: bigint; + let balanceUniqueForeignTokenMiddle: bigint; let balanceUniqueForeignTokenFinal: bigint; before(async () => { @@ -54,11 +72,7 @@ alice = privateKeyWrapper('//Alice'); randomAccount = generateKeyringPair(); }); - - const acalaApiOptions: ApiOptions = { - provider: new WsProvider('ws://127.0.0.1:' + ACALA_PORT), - }; - + // Acala side await usingApi( async (api) => { @@ -91,13 +105,13 @@ const result1 = getGenericResult(events1); expect(result1.success).to.be.true; - [balanceAcalaTokenBefore] = await getBalance(api, [randomAccount.address]); + [balanceAcalaTokenInit] = await getBalance(api, [randomAccount.address]); { const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any; - balanceUniqueForeignTokenBefore = BigInt(free); + balanceUniqueForeignTokenInit = BigInt(free); } }, - acalaApiOptions, + acalaOptions(), ); // Unique side @@ -107,7 +121,7 @@ const result0 = getGenericResult(events0); expect(result0.success).to.be.true; - [balanceUniqueTokenBefore] = await getBalance(api, [randomAccount.address]); + [balanceUniqueTokenInit] = await getBalance(api, [randomAccount.address]); }); }); @@ -165,9 +179,9 @@ const result = getGenericResult(events); expect(result.success).to.be.true; - [balanceUniqueTokenAfter] = await getBalance(api, [randomAccount.address]); + [balanceUniqueTokenMiddle] = await getBalance(api, [randomAccount.address]); - const unqFees = balanceUniqueTokenBefore - balanceUniqueTokenAfter; + const unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT; console.log('[Unique -> Acala] transaction fees on Unique: %s UNQ', unqFees); expect(unqFees > 0n).to.be.true; }); @@ -175,24 +189,21 @@ // Acala side await usingApi( async (api) => { - // todo do something about instant sealing, where there might not be any new blocks await waitNewBlocks(api, 3); const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any; - balanceUniqueForeignTokenAfter = BigInt(free); + balanceUniqueForeignTokenMiddle = BigInt(free); - [balanceAcalaTokenAfter] = await getBalance(api, [randomAccount.address]); + [balanceAcalaTokenMiddle] = await getBalance(api, [randomAccount.address]); - const acaFees = balanceAcalaTokenBefore - balanceAcalaTokenAfter; - const unqDiffAfterIncomeTransfer = balanceUniqueForeignTokenAfter - balanceUniqueForeignTokenBefore; - - const unqFees = unqDiffAfterIncomeTransfer - TRANSFER_AMOUNT; + const acaFees = balanceAcalaTokenInit - balanceAcalaTokenMiddle; + const unqIncomeTransfer = balanceUniqueForeignTokenMiddle - balanceUniqueForeignTokenInit; console.log('[Unique -> Acala] transaction fees on Acala: %s ACA', acaFees); - console.log('[Unique -> Acala] transaction fees on Acala: %s UNQ', unqFees); + console.log('[Unique -> Acala] income %s UNQ', unqIncomeTransfer); expect(acaFees == 0n).to.be.true; - expect(unqFees == 0n).to.be.true; + expect(unqIncomeTransfer == TRANSFER_AMOUNT).to.be.true; }, - {provider: new WsProvider('ws://127.0.0.1:' + ACALA_PORT)}, + acalaOptions(), ); }); @@ -221,11 +232,10 @@ const id = { ForeignAsset: 0, }; - - const amount = TRANSFER_AMOUNT; + const destWeight = 50000000; - const tx = api.tx.xTokens.transfer(id, amount, destination, destWeight); + const tx = api.tx.xTokens.transfer(id, TRANSFER_AMOUNT, destination, destWeight); const events = await submitTransactionAsync(randomAccount, tx); const result = getGenericResult(events); expect(result.success).to.be.true; @@ -236,32 +246,437 @@ balanceUniqueForeignTokenFinal = BigInt(free); } - const acaFees = balanceAcalaTokenAfter - balanceAcalaTokenFinal; - const unqDiffAfterOutcomeTransfer = balanceUniqueForeignTokenAfter - TRANSFER_AMOUNT; - - const unqFees = unqDiffAfterOutcomeTransfer - balanceUniqueForeignTokenFinal; + const acaFees = balanceAcalaTokenMiddle - balanceAcalaTokenFinal; + const unqOutcomeTransfer = balanceUniqueForeignTokenMiddle - balanceUniqueForeignTokenFinal; console.log('[Acala -> Unique] transaction fees on Acala: %s ACA', acaFees); - console.log('[Acala -> Unique] transaction fees on Acala: %s UNQ', unqFees); + console.log('[Acala -> Unique] outcome %s UNQ', unqOutcomeTransfer); expect(acaFees > 0).to.be.true; - expect(unqFees == 0n).to.be.true; + expect(unqOutcomeTransfer == TRANSFER_AMOUNT).to.be.true; }, - {provider: new WsProvider('ws://127.0.0.1:' + ACALA_PORT)}, + acalaOptions(), ); // Unique side await usingApi(async (api) => { - // todo do something about instant sealing, where there might not be any new blocks await waitNewBlocks(api, 3); [balanceUniqueTokenFinal] = await getBalance(api, [randomAccount.address]); - const actuallyDelivered = balanceUniqueTokenFinal - balanceUniqueTokenAfter; + const actuallyDelivered = balanceUniqueTokenFinal - balanceUniqueTokenMiddle; expect(actuallyDelivered > 0).to.be.true; + + console.log('[Acala -> Unique] actually delivered %s UNQ', actuallyDelivered); const unqFees = TRANSFER_AMOUNT - actuallyDelivered; console.log('[Acala -> Unique] transaction fees on Unique: %s UNQ', unqFees); expect(unqFees == 0n).to.be.true; }); }); + + it('Unique rejects ACA tokens from Acala', async () => { + // This test is relevant only when the foreign asset pallet is disabled + + await usingApi(async (api) => { + const destination = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: UNIQUE_CHAIN}, + { + AccountId32: { + network: 'Any', + id: randomAccount.addressRaw, + }, + }, + ], + }, + }, + }; + + const id = { + Token: 'ACA', + }; + + const destWeight = 50000000; + + const tx = api.tx.xTokens.transfer(id, 100_000_000_000, destination, destWeight); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + }, acalaOptions()); + + await usingApi(async api => { + const maxWaitBlocks = 3; + const xcmpQueueFailEvent = await waitEvent(api, maxWaitBlocks, 'xcmpQueue', 'Fail'); + + expect( + xcmpQueueFailEvent != null, + 'Only native token is supported when the Foreign-Assets pallet is not connected', + ).to.be.true; + }); + }); +}); + +describe_xcm('Integration test: Exchanging UNQ with Moonbeam', () => { + + // Unique constants + let uniqueAlice: IKeyringPair; + let uniqueAssetLocation; + + let randomAccountUnique: IKeyringPair; + let randomAccountMoonbeam: IKeyringPair; + + // Moonbeam constants + let assetId: string; + + const moonbeamKeyring = new Keyring({type: 'ethereum'}); + const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133'; + const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b'; + const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68'; + + const alithAccount = moonbeamKeyring.addFromUri(alithPrivateKey, undefined, 'ethereum'); + const baltatharAccount = moonbeamKeyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum'); + const dorothyAccount = moonbeamKeyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum'); + + const councilVotingThreshold = 2; + const technicalCommitteeThreshold = 2; + const votingPeriod = 3; + const delayPeriod = 0; + + const uniqueAssetMetadata = { + name: 'xcUnique', + symbol: 'xcUNQ', + decimals: 18, + isFrozen: false, + minimalBalance: 1, + }; + + let balanceUniqueTokenInit: bigint; + let balanceUniqueTokenMiddle: bigint; + let balanceUniqueTokenFinal: bigint; + let balanceForeignUnqTokenInit: bigint; + let balanceForeignUnqTokenMiddle: bigint; + let balanceForeignUnqTokenFinal: bigint; + let balanceGlmrTokenInit: bigint; + let balanceGlmrTokenMiddle: bigint; + let balanceGlmrTokenFinal: bigint; + + before(async () => { + await usingApi(async (api, privateKeyWrapper) => { + uniqueAlice = privateKeyWrapper('//Alice'); + randomAccountUnique = generateKeyringPair(); + randomAccountMoonbeam = generateKeyringPair('ethereum'); + + balanceForeignUnqTokenInit = 0n; + }); + + await usingApi( + async (api) => { + + // >>> Sponsoring Dorothy >>> + console.log('Sponsoring Dorothy.......'); + const tx0 = api.tx.balances.transfer(dorothyAccount.address, 11_000_000_000_000_000_000n); + const events0 = await submitTransactionAsync(alithAccount, tx0); + const result0 = getGenericResult(events0); + expect(result0.success).to.be.true; + console.log('Sponsoring Dorothy.......DONE'); + // <<< Sponsoring Dorothy <<< + + const sourceLocation: MultiLocation = api.createType( + 'MultiLocation', + { + parents: 1, + interior: {X1: {Parachain: UNIQUE_CHAIN}}, + }, + ); + + uniqueAssetLocation = {XCM: sourceLocation}; + const existentialDeposit = 1; + const isSufficient = true; + const unitsPerSecond = '1'; + const numAssetsWeightHint = 0; + + const registerTx = api.tx.assetManager.registerForeignAsset( + uniqueAssetLocation, + uniqueAssetMetadata, + existentialDeposit, + isSufficient, + ); + console.log('Encoded proposal for registerAsset is %s', registerTx.method.toHex() || ''); + + const setUnitsTx = api.tx.assetManager.setAssetUnitsPerSecond( + uniqueAssetLocation, + unitsPerSecond, + numAssetsWeightHint, + ); + console.log('Encoded proposal for setAssetUnitsPerSecond is %s', setUnitsTx.method.toHex() || ''); + + const batchCall = api.tx.utility.batchAll([registerTx, setUnitsTx]); + console.log('Encoded proposal for batchCall is %s', batchCall.method.toHex() || ''); + + // >>> Note motion preimage >>> + console.log('Note motion preimage.......'); + const encodedProposal = batchCall?.method.toHex() || ''; + const proposalHash = blake2AsHex(encodedProposal); + console.log('Encoded proposal for batch utility after schedule is %s', encodedProposal); + console.log('Encoded proposal hash for batch utility after schedule is %s', proposalHash); + console.log('Encoded length %d', encodedProposal.length); + + const tx1 = api.tx.democracy.notePreimage(encodedProposal); + const events1 = await submitTransactionAsync(baltatharAccount, tx1); + const result1 = getGenericResult(events1); + expect(result1.success).to.be.true; + console.log('Note motion preimage.......DONE'); + // <<< Note motion preimage <<< + + // >>> Propose external motion through council >>> + console.log('Propose external motion through council.......'); + const externalMotion = api.tx.democracy.externalProposeMajority(proposalHash); + const tx2 = api.tx.councilCollective.propose( + councilVotingThreshold, + externalMotion, + externalMotion.encodedLength, + ); + const events2 = await submitTransactionAsync(baltatharAccount, tx2); + const result2 = getGenericResult(events2); + expect(result2.success).to.be.true; + + const encodedMotion = externalMotion?.method.toHex() || ''; + const motionHash = blake2AsHex(encodedMotion); + console.log('Motion hash is %s', motionHash); + + const tx3 = api.tx.councilCollective.vote(motionHash, 0, true); + { + const events3 = await submitTransactionAsync(dorothyAccount, tx3); + const result3 = getGenericResult(events3); + expect(result3.success).to.be.true; + } + { + const events3 = await submitTransactionAsync(baltatharAccount, tx3); + const result3 = getGenericResult(events3); + expect(result3.success).to.be.true; + } + + const tx4 = api.tx.councilCollective.close(motionHash, 0, 1_000_000_000, externalMotion.encodedLength); + const events4 = await submitTransactionAsync(dorothyAccount, tx4); + const result4 = getGenericResult(events4); + expect(result4.success).to.be.true; + console.log('Propose external motion through council.......DONE'); + // <<< Propose external motion through council <<< + + // >>> Fast track proposal through technical committee >>> + console.log('Fast track proposal through technical committee.......'); + const fastTrack = api.tx.democracy.fastTrack(proposalHash, votingPeriod, delayPeriod); + const tx5 = api.tx.techCommitteeCollective.propose( + technicalCommitteeThreshold, + fastTrack, + fastTrack.encodedLength, + ); + const events5 = await submitTransactionAsync(alithAccount, tx5); + const result5 = getGenericResult(events5); + expect(result5.success).to.be.true; + + const encodedFastTrack = fastTrack?.method.toHex() || ''; + const fastTrackHash = blake2AsHex(encodedFastTrack); + console.log('FastTrack hash is %s', fastTrackHash); + + const proposalIdx = Number(await api.query.techCommitteeCollective.proposalCount()) - 1; + const tx6 = api.tx.techCommitteeCollective.vote(fastTrackHash, proposalIdx, true); + { + const events6 = await submitTransactionAsync(baltatharAccount, tx6); + const result6 = getGenericResult(events6); + expect(result6.success).to.be.true; + } + { + const events6 = await submitTransactionAsync(alithAccount, tx6); + const result6 = getGenericResult(events6); + expect(result6.success).to.be.true; + } + + const tx7 = api.tx.techCommitteeCollective + .close(fastTrackHash, proposalIdx, 1_000_000_000, fastTrack.encodedLength); + const events7 = await submitTransactionAsync(baltatharAccount, tx7); + const result7 = getGenericResult(events7); + expect(result7.success).to.be.true; + console.log('Fast track proposal through technical committee.......DONE'); + // <<< Fast track proposal through technical committee <<< + + // >>> Referendum voting >>> + console.log('Referendum voting.......'); + const tx8 = api.tx.democracy.vote( + 0, + {Standard: {balance: 10_000_000_000_000_000_000n, vote: {aye: true, conviction: 1}}}, + ); + const events8 = await submitTransactionAsync(dorothyAccount, tx8); + const result8 = getGenericResult(events8); + expect(result8.success).to.be.true; + console.log('Referendum voting.......DONE'); + // <<< Referendum voting <<< + + // >>> Acquire Unique AssetId Info on Moonbeam >>> + console.log('Acquire Unique AssetId Info on Moonbeam.......'); + + // Wait for the democracy execute + await waitNewBlocks(api, 5); + + assetId = (await api.query.assetManager.assetTypeId({ + XCM: sourceLocation, + })).toString(); + + console.log('UNQ asset ID is %s', assetId); + console.log('Acquire Unique AssetId Info on Moonbeam.......DONE'); + // >>> Acquire Unique AssetId Info on Moonbeam >>> + + // >>> Sponsoring random Account >>> + console.log('Sponsoring random Account.......'); + const tx10 = api.tx.balances.transfer(randomAccountMoonbeam.address, 11_000_000_000_000_000_000n); + const events10 = await submitTransactionAsync(baltatharAccount, tx10); + const result10 = getGenericResult(events10); + expect(result10.success).to.be.true; + console.log('Sponsoring random Account.......DONE'); + // <<< Sponsoring random Account <<< + + [balanceGlmrTokenInit] = await getBalance(api, [randomAccountMoonbeam.address]); + }, + moonbeamOptions(), + ); + + await usingApi(async (api) => { + const tx0 = api.tx.balances.transfer(randomAccountUnique.address, 10n * TRANSFER_AMOUNT); + const events0 = await submitTransactionAsync(uniqueAlice, tx0); + const result0 = getGenericResult(events0); + expect(result0.success).to.be.true; + + [balanceUniqueTokenInit] = await getBalance(api, [randomAccountUnique.address]); + }); + }); + + it('Should connect and send UNQ to Moonbeam', async () => { + await usingApi(async (api) => { + const currencyId = { + NativeAssetId: 'Here', + }; + const dest = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: MOONBEAM_CHAIN}, + {AccountKey20: {network: 'Any', key: randomAccountMoonbeam.address}}, + ], + }, + }, + }; + const amount = TRANSFER_AMOUNT; + const destWeight = 850000000; + + const tx = api.tx.xTokens.transfer(currencyId, amount, dest, destWeight); + const events = await submitTransactionAsync(randomAccountUnique, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + [balanceUniqueTokenMiddle] = await getBalance(api, [randomAccountUnique.address]); + expect(balanceUniqueTokenMiddle < balanceUniqueTokenInit).to.be.true; + + const transactionFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT; + console.log('[Unique -> Moonbeam] transaction fees on Unique: %s UNQ', transactionFees); + expect(transactionFees > 0).to.be.true; + }); + + await usingApi( + async (api) => { + await waitNewBlocks(api, 3); + + [balanceGlmrTokenMiddle] = await getBalance(api, [randomAccountMoonbeam.address]); + + const glmrFees = balanceGlmrTokenInit - balanceGlmrTokenMiddle; + console.log('[Unique -> Moonbeam] transaction fees on Moonbeam: %s GLMR', glmrFees); + expect(glmrFees == 0n).to.be.true; + + const unqRandomAccountAsset = ( + await api.query.assets.account(assetId, randomAccountMoonbeam.address) + ).toJSON()! as any; + + balanceForeignUnqTokenMiddle = BigInt(unqRandomAccountAsset['balance']); + const unqIncomeTransfer = balanceForeignUnqTokenMiddle - balanceForeignUnqTokenInit; + console.log('[Unique -> Moonbeam] income %s UNQ', unqIncomeTransfer); + expect(unqIncomeTransfer == TRANSFER_AMOUNT).to.be.true; + }, + moonbeamOptions(), + ); + }); + + it('Should connect to Moonbeam and send UNQ back', async () => { + await usingApi( + async (api) => { + const asset = { + V1: { + id: { + Concrete: { + parents: 1, + interior: { + X1: {Parachain: UNIQUE_CHAIN}, + }, + }, + }, + fun: { + Fungible: TRANSFER_AMOUNT, + }, + }, + }; + const destination = { + V1: { + parents: 1, + interior: { + X2: [ + {Parachain: UNIQUE_CHAIN}, + {AccountId32: {network: 'Any', id: randomAccountUnique.addressRaw}}, + ], + }, + }, + }; + const destWeight = 50000000; + + const tx = api.tx.xTokens.transferMultiasset(asset, destination, destWeight); + const events = await submitTransactionAsync(randomAccountMoonbeam, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + [balanceGlmrTokenFinal] = await getBalance(api, [randomAccountMoonbeam.address]); + + const glmrFees = balanceGlmrTokenMiddle - balanceGlmrTokenFinal; + console.log('[Moonbeam -> Unique] transaction fees on Moonbeam: %s GLMR', glmrFees); + expect(glmrFees > 0).to.be.true; + + const unqRandomAccountAsset = ( + await api.query.assets.account(assetId, randomAccountMoonbeam.address) + ).toJSON()! as any; + + expect(unqRandomAccountAsset).to.be.null; + + balanceForeignUnqTokenFinal = 0n; + + const unqOutcomeTransfer = balanceForeignUnqTokenMiddle - balanceForeignUnqTokenFinal; + console.log('[Unique -> Moonbeam] outcome %s UNQ', unqOutcomeTransfer); + expect(unqOutcomeTransfer == TRANSFER_AMOUNT).to.be.true; + }, + moonbeamOptions(), + ); + + await usingApi(async (api) => { + await waitNewBlocks(api, 3); + + [balanceUniqueTokenFinal] = await getBalance(api, [randomAccountUnique.address]); + const actuallyDelivered = balanceUniqueTokenFinal - balanceUniqueTokenMiddle; + expect(actuallyDelivered > 0).to.be.true; + + console.log('[Moonbeam -> Unique] actually delivered %s UNQ', actuallyDelivered); + + const unqFees = TRANSFER_AMOUNT - actuallyDelivered; + console.log('[Moonbeam -> Unique] transaction fees on Unique: %s UNQ', unqFees); + expect(unqFees == 0n).to.be.true; + }); + }); });