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.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, ChainHelperBase, ChainHelperBaseConstructor, HelperGroup, UniqueHelperConstructor} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {SignerOptions, VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';16import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup} from './unique.xcm';17import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, ICollectiveGroup, IFellowshipGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance';1819export class SilentLogger {20 log(_msg: any, _level: any): void { }21 level = {22 ERROR: 'ERROR' as const,23 WARNING: 'WARNING' as const,24 INFO: 'INFO' as const,25 };26}2728export class SilentConsole {29 // TODO: Remove, this is temporary: Filter unneeded API output30 // (Jaco promised it will be removed in the next version)31 consoleErr: any;32 consoleLog: any;33 consoleWarn: any;3435 constructor() {36 this.consoleErr = console.error;37 this.consoleLog = console.log;38 this.consoleWarn = console.warn;39 }4041 enable() {42 const outFn = (printer: any) => (...args: any[]) => {43 for(const arg of args) {44 if(typeof arg !== 'string')45 continue;46 const skippedWarnings = ['1000:: Normal connection closure', 'Not decorating unknown runtime apis:', 'RPC methods not decorated:', 'Not decorating runtime apis', 'Bad input data provided to validate_transaction', 'account balance too low', '1006:: Abnormal Closure'];47 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);48 if(needToSkip || arg === 'Normal connection closure')49 return;50 }51 printer(...args);52 };5354 console.error = outFn(this.consoleErr.bind(console));55 console.log = outFn(this.consoleLog.bind(console));56 console.warn = outFn(this.consoleWarn.bind(console));57 }5859 disable() {60 console.error = this.consoleErr;61 console.log = this.consoleLog;62 console.warn = this.consoleWarn;63 }64}6566export interface IEventHelper {67 section(): string;6869 method(): string;7071 wrapEvent(data: any[]): any;72}7374// eslint-disable-next-line @typescript-eslint/naming-convention75function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {76 const helperClass = class implements IEventHelper {77 wrapEvent: (data: any[]) => any;78 _section: string;79 _method: string;8081 constructor() {82 this.wrapEvent = wrapEvent;83 this._section = section;84 this._method = method;85 }8687 section(): string {88 return this._section;89 }9091 method(): string {92 return this._method;93 }9495 filter(txres: ITransactionResult) {96 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)97 .map(e => this.wrapEvent(e.event.data));98 }99100 find(txres: ITransactionResult) {101 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);102 return e ? this.wrapEvent(e.event.data) : null;103 }104105 expect(txres: ITransactionResult) {106 const e = this.find(txres);107 if(e) {108 return e;109 } else {110 throw Error(`Expected event ${section}.${method}`);111 }112 }113 };114115 return helperClass;116}117118function eventJsonData<T = any>(data: any[], index: number) {119 return data[index].toJSON() as T;120}121122function eventHumanData(data: any[], index: number) {123 return data[index].toHuman();124}125126function eventData<T = any>(data: any[], index: number) {127 return data[index] as T;128}129130// eslint-disable-next-line @typescript-eslint/naming-convention131function EventSection(section: string) {132 return class Section {133 static section = section;134135 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {136 const helperClass = EventHelper(Section.section, name, wrapEvent);137 return new helperClass();138 }139 };140}141142function schedulerSection(schedulerInstance: string) {143 return class extends EventSection(schedulerInstance) {144 static Dispatched = this.Method('Dispatched', data => ({145 task: eventJsonData(data, 0),146 id: eventHumanData(data, 1),147 result: data[2],148 }));149150 static PriorityChanged = this.Method('PriorityChanged', data => ({151 task: eventJsonData(data, 0),152 priority: eventJsonData(data, 1),153 }));154 };155}156157export class Event {158 static Democracy = class extends EventSection('democracy') {159 static Proposed = this.Method('Proposed', data => ({160 proposalIndex: eventJsonData<number>(data, 0),161 }));162163 static ExternalTabled = this.Method('ExternalTabled');164165 static Started = this.Method('Started', data => ({166 referendumIndex: eventJsonData<number>(data, 0),167 threshold: eventHumanData(data, 1),168 }));169170 static Voted = this.Method('Voted', data => ({171 voter: eventJsonData(data, 0),172 referendumIndex: eventJsonData<number>(data, 1),173 vote: eventJsonData(data, 2),174 }));175176 static Passed = this.Method('Passed', data => ({177 referendumIndex: eventJsonData<number>(data, 0),178 }));179180 static ProposalCanceled = this.Method('ProposalCanceled', data => ({181 propIndex: eventJsonData<number>(data, 0),182 }));183184 static Cancelled = this.Method('Cancelled', data => ({185 propIndex: eventJsonData<number>(data, 0),186 }));187188 static Vetoed = this.Method('Vetoed', data => ({189 who: eventHumanData(data, 0),190 proposalHash: eventHumanData(data, 1),191 until: eventJsonData<number>(data, 1),192 }));193 };194195 static Council = class extends EventSection('council') {196 static Proposed = this.Method('Proposed', data => ({197 account: eventHumanData(data, 0),198 proposalIndex: eventJsonData<number>(data, 1),199 proposalHash: eventHumanData(data, 2),200 threshold: eventJsonData<number>(data, 3),201 }));202 static Closed = this.Method('Closed', data => ({203 proposalHash: eventHumanData(data, 0),204 yes: eventJsonData<number>(data, 1),205 no: eventJsonData<number>(data, 2),206 }));207 static Executed = this.Method('Executed', data => ({208 proposalHash: eventHumanData(data, 0),209 }));210 };211212 static TechnicalCommittee = class extends EventSection('technicalCommittee') {213 static Proposed = this.Method('Proposed', data => ({214 account: eventHumanData(data, 0),215 proposalIndex: eventJsonData<number>(data, 1),216 proposalHash: eventHumanData(data, 2),217 threshold: eventJsonData<number>(data, 3),218 }));219 static Closed = this.Method('Closed', data => ({220 proposalHash: eventHumanData(data, 0),221 yes: eventJsonData<number>(data, 1),222 no: eventJsonData<number>(data, 2),223 }));224 static Approved = this.Method('Approved', data => ({225 proposalHash: eventHumanData(data, 0),226 }));227 static Executed = this.Method('Executed', data => ({228 proposalHash: eventHumanData(data, 0),229 result: eventHumanData(data, 1),230 }));231 };232233 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {234 static Submitted = this.Method('Submitted', data => ({235 referendumIndex: eventJsonData<number>(data, 0),236 trackId: eventJsonData<number>(data, 1),237 proposal: eventJsonData(data, 2),238 }));239240 static Cancelled = this.Method('Cancelled', data => ({241 index: eventJsonData<number>(data, 0),242 tally: eventJsonData(data, 1),243 }));244 };245246 static UniqueScheduler = schedulerSection('uniqueScheduler');247 static Scheduler = schedulerSection('scheduler');248249 static XcmpQueue = class extends EventSection('xcmpQueue') {250 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({251 messageHash: eventJsonData(data, 0),252 }));253254 static Success = this.Method('Success', data => ({255 messageHash: eventJsonData(data, 0),256 }));257258 static Fail = this.Method('Fail', data => ({259 messageHash: eventJsonData(data, 0),260 outcome: eventData<XcmV2TraitsError>(data, 1),261 }));262 };263}264265// eslint-disable-next-line @typescript-eslint/naming-convention266export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {267 return class extends Base {268 constructor(...args: any[]) {269 super(...args);270 }271272 async executeExtrinsic(273 sender: IKeyringPair,274 extrinsic: string,275 params: any[],276 expectSuccess?: boolean,277 options: Partial<SignerOptions> | null = null,278 ): Promise<ITransactionResult> {279 const call = this.constructApiCall(extrinsic, params);280 const result = await super.executeExtrinsic(281 sender,282 'api.tx.sudo.sudo',283 [call],284 expectSuccess,285 options,286 );287288 if(result.status === 'Fail') return result;289290 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;291 if(data.isErr) {292 if(data.asErr.isModule) {293 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;294 const metaError = super.getApi()?.registry.findMetaError(error);295 throw new Error(`${metaError.section}.${metaError.name}`);296 } else if(data.asErr.isToken) {297 throw new Error(`Token: ${data.asErr.asToken}`);298 }299 // May be [object Object] in case of unhandled non-unit enum300 throw new Error(`Misc: ${data.asErr.toHuman()}`);301 }302 return result;303 }304 async executeExtrinsicUncheckedWeight(305 sender: IKeyringPair,306 extrinsic: string,307 params: any[],308 expectSuccess?: boolean,309 options: Partial<SignerOptions> | null = null,310 ): Promise<ITransactionResult> {311 const call = this.constructApiCall(extrinsic, params);312 const result = await super.executeExtrinsic(313 sender,314 'api.tx.sudo.sudoUncheckedWeight',315 [call, {refTime: 0, proofSize: 0}],316 expectSuccess,317 options,318 );319320 if(result.status === 'Fail') return result;321322 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;323 if(data.isErr) {324 if(data.asErr.isModule) {325 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;326 const metaError = super.getApi()?.registry.findMetaError(error);327 throw new Error(`${metaError.section}.${metaError.name}`);328 } else if(data.asErr.isToken) {329 throw new Error(`Token: ${data.asErr.asToken}`);330 }331 // May be [object Object] in case of unhandled non-unit enum332 throw new Error(`Misc: ${data.asErr.toHuman()}`);333 }334 return result;335 }336 };337}338339class SchedulerGroup extends HelperGroup<UniqueHelper> {340 constructor(helper: UniqueHelper) {341 super(helper);342 }343344 cancelScheduled(signer: TSigner, scheduledId: string) {345 return this.helper.executeExtrinsic(346 signer,347 'api.tx.scheduler.cancelNamed',348 [scheduledId],349 true,350 );351 }352353 changePriority(signer: TSigner, scheduledId: string, priority: number) {354 return this.helper.executeExtrinsic(355 signer,356 'api.tx.scheduler.changeNamedPriority',357 [scheduledId, priority],358 true,359 );360 }361362 scheduleAt<T extends DevUniqueHelper>(363 executionBlockNumber: number,364 options: ISchedulerOptions = {},365 ) {366 return this.schedule<T>('schedule', executionBlockNumber, options);367 }368369 scheduleAfter<T extends DevUniqueHelper>(370 blocksBeforeExecution: number,371 options: ISchedulerOptions = {},372 ) {373 return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);374 }375376 schedule<T extends UniqueHelper>(377 scheduleFn: 'schedule' | 'scheduleAfter',378 blocksNum: number,379 options: ISchedulerOptions = {},380 ) {381 // eslint-disable-next-line @typescript-eslint/naming-convention382 const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);383 return this.helper.clone(ScheduledHelperType, {384 scheduleFn,385 blocksNum,386 options,387 }) as T;388 }389}390391class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {392 //todo:collator documentation393 addInvulnerable(signer: TSigner, address: string) {394 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);395 }396397 removeInvulnerable(signer: TSigner, address: string) {398 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);399 }400401 async getInvulnerables(): Promise<string[]> {402 return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());403 }404405 /** and also total max invulnerables */406 maxCollators(): number {407 return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);408 }409410 async getDesiredCollators(): Promise<number> {411 return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();412 }413414 setLicenseBond(signer: TSigner, amount: bigint) {415 return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);416 }417418 async getLicenseBond(): Promise<bigint> {419 return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();420 }421422 obtainLicense(signer: TSigner) {423 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);424 }425426 releaseLicense(signer: TSigner) {427 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);428 }429430 forceReleaseLicense(signer: TSigner, released: string) {431 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);432 }433434 async hasLicense(address: string): Promise<bigint> {435 return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();436 }437438 onboard(signer: TSigner) {439 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);440 }441442 offboard(signer: TSigner) {443 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);444 }445446 async getCandidates(): Promise<string[]> {447 return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());448 }449}450451export class DevUniqueHelper extends UniqueHelper {452 /**453 * Arrange methods for tests454 */455 arrange: ArrangeGroup;456 wait: WaitGroup;457 admin: AdminGroup;458 session: SessionGroup;459 testUtils: TestUtilGroup;460 foreignAssets: ForeignAssetsGroup;461 xcm: XcmGroup<DevUniqueHelper>;462 xTokens: XTokensGroup<DevUniqueHelper>;463 tokens: TokensGroup<DevUniqueHelper>;464 scheduler: SchedulerGroup;465 collatorSelection: CollatorSelectionGroup;466 council: ICollectiveGroup;467 technicalCommittee: ICollectiveGroup;468 fellowship: IFellowshipGroup;469 democracy: DemocracyGroup;470471 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {472 options.helperBase = options.helperBase ?? DevUniqueHelper;473474 super(logger, options);475 this.arrange = new ArrangeGroup(this);476 this.wait = new WaitGroup(this);477 this.admin = new AdminGroup(this);478 this.testUtils = new TestUtilGroup(this);479 this.session = new SessionGroup(this);480 this.foreignAssets = new ForeignAssetsGroup(this);481 this.xcm = new XcmGroup(this, 'polkadotXcm');482 this.xTokens = new XTokensGroup(this);483 this.tokens = new TokensGroup(this);484 this.scheduler = new SchedulerGroup(this);485 this.collatorSelection = new CollatorSelectionGroup(this);486 this.council = {487 collective: new CollectiveGroup(this, 'council'),488 membership: new CollectiveMembershipGroup(this, 'councilMembership'),489 };490 this.technicalCommittee = {491 collective: new CollectiveGroup(this, 'technicalCommittee'),492 membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),493 };494 this.fellowship = {495 collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),496 referenda: new ReferendaGroup(this, 'fellowshipReferenda'),497 };498 this.democracy = new DemocracyGroup(this);499 }500501 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {502 if(!wsEndpoint) throw new Error('wsEndpoint was not set');503 const wsProvider = new WsProvider(wsEndpoint);504 this.api = new ApiPromise({505 provider: wsProvider,506 signedExtensions: {507 ContractHelpers: {508 extrinsic: {},509 payload: {},510 },511 CheckMaintenance: {512 extrinsic: {},513 payload: {},514 },515 DisableIdentityCalls: {516 extrinsic: {},517 payload: {},518 },519 FakeTransactionFinalizer: {520 extrinsic: {},521 payload: {},522 },523 },524 rpc: {525 unique: defs.unique.rpc,526 appPromotion: defs.appPromotion.rpc,527 povinfo: defs.povinfo.rpc,528 eth: {529 feeHistory: {530 description: 'Dummy',531 params: [],532 type: 'u8',533 },534 maxPriorityFeePerGas: {535 description: 'Dummy',536 params: [],537 type: 'u8',538 },539 },540 },541 });542 await this.api.isReadyOrError;543 this.network = await UniqueHelper.detectNetwork(this.api);544 this.wsEndpoint = wsEndpoint;545 }546 getSudo<T extends DevUniqueHelper>() {547 // eslint-disable-next-line @typescript-eslint/naming-convention548 const SudoHelperType = SudoHelper(this.helperBase);549 return this.clone(SudoHelperType) as T;550 }551}552553export class DevRelayHelper extends RelayHelper {554 wait: WaitGroup;555556 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {557 options.helperBase = options.helperBase ?? DevRelayHelper;558559 super(logger, options);560 this.wait = new WaitGroup(this);561 }562}563564export class DevWestmintHelper extends WestmintHelper {565 wait: WaitGroup;566567 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {568 options.helperBase = options.helperBase ?? DevWestmintHelper;569570 super(logger, options);571 this.wait = new WaitGroup(this);572 }573}574575export class DevStatemineHelper extends DevWestmintHelper {}576577export class DevStatemintHelper extends DevWestmintHelper {}578579export class DevMoonbeamHelper extends MoonbeamHelper {580 account: MoonbeamAccountGroup;581 wait: WaitGroup;582 fastDemocracy: MoonbeamFastDemocracyGroup;583584 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {585 options.helperBase = options.helperBase ?? DevMoonbeamHelper;586 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';587588 super(logger, options);589 this.account = new MoonbeamAccountGroup(this);590 this.wait = new WaitGroup(this);591 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);592 }593}594595export class DevMoonriverHelper extends DevMoonbeamHelper {596 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {597 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';598 super(logger, options);599 }600}601602export class DevAstarHelper extends AstarHelper {603 wait: WaitGroup;604605 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {606 options.helperBase = options.helperBase ?? DevAstarHelper;607608 super(logger, options);609 this.wait = new WaitGroup(this);610 }611612 getSudo<T extends AstarHelper>() {613 // eslint-disable-next-line @typescript-eslint/naming-convention614 const SudoHelperType = SudoHelper(this.helperBase);615 return this.clone(SudoHelperType) as T;616 }617}618619export class DevShidenHelper extends DevAstarHelper { }620621export class DevAcalaHelper extends AcalaHelper {622 wait: WaitGroup;623624 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {625 options.helperBase = options.helperBase ?? DevAcalaHelper;626627 super(logger, options);628 this.wait = new WaitGroup(this);629 }630 getSudo() {631 // eslint-disable-next-line @typescript-eslint/naming-convention632 const SudoHelperType = SudoHelper(this.helperBase);633 return this.clone(SudoHelperType) as DevAcalaHelper;634 }635}636637export class DevPolkadexHelper extends PolkadexHelper {638 wait: WaitGroup;639 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {640 options.helperBase = options.helperBase ?? PolkadexHelper;641642 super(logger, options);643 this.wait = new WaitGroup(this);644 }645646 getSudo() {647 // eslint-disable-next-line @typescript-eslint/naming-convention648 const SudoHelperType = SudoHelper(this.helperBase);649 return this.clone(SudoHelperType) as DevPolkadexHelper;650 }651}652653export class DevKaruraHelper extends DevAcalaHelper {}654655export class ArrangeGroup {656 helper: DevUniqueHelper;657658 scheduledIdSlider = 0;659660 constructor(helper: DevUniqueHelper) {661 this.helper = helper;662 }663664 /**665 * Generates accounts with the specified UNQ token balance666 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.667 * @param donor donor account for balances668 * @returns array of newly created accounts669 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);670 */671 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {672 let nonce = await this.helper.chain.getNonce(donor.address);673 const wait = new WaitGroup(this.helper);674 const ss58Format = this.helper.chain.getChainProperties().ss58Format;675 const tokenNominal = this.helper.balance.getOneTokenNominal();676 const transactions = [];677 const accounts: IKeyringPair[] = [];678 for(const balance of balances) {679 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);680 accounts.push(recipient);681 if(balance !== 0n) {682 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);683 transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));684 nonce++;685 }686 }687688 await Promise.all(transactions).catch(_e => {});689690 //#region TODO remove this region, when nonce problem will be solved691 const checkBalances = async () => {692 let isSuccess = true;693 for(let i = 0; i < balances.length; i++) {694 const balance = await this.helper.balance.getSubstrate(accounts[i].address);695 if(balance !== balances[i] * tokenNominal) {696 isSuccess = false;697 break;698 }699 }700 return isSuccess;701 };702703 let accountsCreated = false;704 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;705 // checkBalances retry up to 5-50 blocks706 for(let index = 0; index < maxBlocksChecked; index++) {707 accountsCreated = await checkBalances();708 if(accountsCreated) break;709 await wait.newBlocks(1);710 }711712 if(!accountsCreated) throw Error('Accounts generation failed');713 //#endregion714715 return accounts;716 };717718 // TODO combine this method and createAccounts into one719 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {720 const createAsManyAsCan = async () => {721 let transactions: any = [];722 const accounts: IKeyringPair[] = [];723 let nonce = await this.helper.chain.getNonce(donor.address);724 const tokenNominal = this.helper.balance.getOneTokenNominal();725 const ss58Format = this.helper.chain.getChainProperties().ss58Format;726 for(let i = 0; i < accountsToCreate; i++) {727 if(i === 500) { // if there are too many accounts to create728 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled729 transactions = []; //730 nonce = await this.helper.chain.getNonce(donor.address); // update nonce731 }732 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);733 accounts.push(recipient);734 if(withBalance !== 0n) {735 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);736 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));737 nonce++;738 }739 }740741 const fullfilledAccounts = [];742 await Promise.allSettled(transactions);743 for(const account of accounts) {744 const accountBalance = await this.helper.balance.getSubstrate(account.address);745 if(accountBalance === withBalance * tokenNominal) {746 fullfilledAccounts.push(account);747 }748 }749 return fullfilledAccounts;750 };751752753 const crowd: IKeyringPair[] = [];754 // do up to 5 retries755 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {756 const asManyAsCan = await createAsManyAsCan();757 crowd.push(...asManyAsCan);758 accountsToCreate -= asManyAsCan.length;759 }760761 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);762763 return crowd;764 };765766 /**767 * Generates one account with zero balance768 * @returns the newly generated account769 * @example const account = await helper.arrange.createEmptyAccount();770 */771 createEmptyAccount = (): IKeyringPair => {772 const ss58Format = this.helper.chain.getChainProperties().ss58Format;773 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);774 };775776 isDevNode = async () => {777 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();778 if(blockNumber == 0) {779 await this.helper.wait.newBlocks(1);780 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();781 }782 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);783 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);784 const findCreationDate = (block: any) => {785 const humanBlock = block.toHuman();786 let date;787 humanBlock.block.extrinsics.forEach((ext: any) => {788 if(ext.method.section === 'timestamp') {789 date = Number(ext.method.args.now.replaceAll(',', ''));790 }791 });792 return date;793 };794 const block1date = await findCreationDate(block1);795 const block2date = await findCreationDate(block2);796 if(block2date! - block1date! < 9000) return true;797 };798799 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {800 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);801 let balance = await this.helper.balance.getSubstrate(address);802803 await promise();804805 balance -= await this.helper.balance.getSubstrate(address);806807 return balance;808 }809810 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {811 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);812813 const kvJson: {[key: string]: string} = {};814815 for(const kv of rawPovInfo.keyValues) {816 kvJson[kv.key.toHex()] = kv.value.toHex();817 }818819 const kvStr = JSON.stringify(kvJson);820821 const chainql = spawnSync(822 'chainql',823 [824 `--tla-code=data=${kvStr}`,825 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,826 ],827 );828829 if(!chainql.stdout) {830 throw Error('unable to get an output from the `chainql`');831 }832833 return {834 proofSize: rawPovInfo.proofSize.toNumber(),835 compactProofSize: rawPovInfo.compactProofSize.toNumber(),836 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),837 results: rawPovInfo.results,838 kv: JSON.parse(chainql.stdout.toString()),839 };840 }841842 calculatePalletAddress(palletId: any) {843 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));844 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);845 }846847 makeScheduledIds(num: number): string[] {848 function makeId(slider: number) {849 const scheduledIdSize = 64;850 const hexId = slider.toString(16);851 const prefixSize = scheduledIdSize - hexId.length;852853 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;854855 return scheduledId;856 }857858 const ids = [];859 for(let i = 0; i < num; i++) {860 ids.push(makeId(this.scheduledIdSlider));861 this.scheduledIdSlider += 1;862 }863864 return ids;865 }866867 makeScheduledId(): string {868 return (this.makeScheduledIds(1))[0];869 }870871 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {872 const capture = new EventCapture(this.helper, eventSection, eventMethod);873 await capture.startCapture();874875 return capture;876 }877878 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {879 return {880 V2: [881 {882 WithdrawAsset: [883 {884 id,885 fun: {886 Fungible: amount,887 },888 },889 ],890 },891 {892 BuyExecution: {893 fees: {894 id,895 fun: {896 Fungible: amount,897 },898 },899 weightLimit: 'Unlimited',900 },901 },902 {903 DepositAsset: {904 assets: {905 Wild: 'All',906 },907 maxAssets: 1,908 beneficiary: {909 parents: 0,910 interior: {911 X1: {912 AccountId32: {913 network: 'Any',914 id: beneficiary,915 },916 },917 },918 },919 },920 },921 ],922 };923 }924925 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {926 return {927 V2: [928 {929 ReserveAssetDeposited: [930 {931 id,932 fun: {933 Fungible: amount,934 },935 },936 ],937 },938 {939 BuyExecution: {940 fees: {941 id,942 fun: {943 Fungible: amount,944 },945 },946 weightLimit: 'Unlimited',947 },948 },949 {950 DepositAsset: {951 assets: {952 Wild: 'All',953 },954 maxAssets: 1,955 beneficiary: {956 parents: 0,957 interior: {958 X1: {959 AccountId32: {960 network: 'Any',961 id: beneficiary,962 },963 },964 },965 },966 },967 },968 ],969 };970 }971}972973class MoonbeamAccountGroup {974 helper: MoonbeamHelper;975976 keyring: Keyring;977 _alithAccount: IKeyringPair;978 _baltatharAccount: IKeyringPair;979 _dorothyAccount: IKeyringPair;980981 constructor(helper: MoonbeamHelper) {982 this.helper = helper;983984 this.keyring = new Keyring({type: 'ethereum'});985 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';986 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';987 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';988989 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');990 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');991 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');992 }993994 alithAccount() {995 return this._alithAccount;996 }997998 baltatharAccount() {999 return this._baltatharAccount;1000 }10011002 dorothyAccount() {1003 return this._dorothyAccount;1004 }10051006 create() {1007 return this.keyring.addFromUri(mnemonicGenerate());1008 }1009}10101011class MoonbeamFastDemocracyGroup {1012 helper: DevMoonbeamHelper;10131014 constructor(helper: DevMoonbeamHelper) {1015 this.helper = helper;1016 }10171018 async executeProposal(proposalDesciption: string, encodedProposal: string) {1019 const proposalHash = blake2AsHex(encodedProposal);10201021 const alithAccount = this.helper.account.alithAccount();1022 const baltatharAccount = this.helper.account.baltatharAccount();1023 const dorothyAccount = this.helper.account.dorothyAccount();10241025 const councilVotingThreshold = 2;1026 const technicalCommitteeThreshold = 2;1027 const fastTrackVotingPeriod = 3;1028 const fastTrackDelayPeriod = 0;10291030 console.log(`[democracy] executing '${proposalDesciption}' proposal`);10311032 // >>> Propose external motion through council >>>1033 console.log('\t* Propose external motion through council.......');1034 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1035 const encodedMotion = externalMotion?.method.toHex() || '';1036 const motionHash = blake2AsHex(encodedMotion);1037 console.log('\t* Motion hash is %s', motionHash);10381039 await this.helper.collective.council.propose(1040 baltatharAccount,1041 councilVotingThreshold,1042 externalMotion,1043 externalMotion.encodedLength,1044 );10451046 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1047 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1048 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);10491050 await this.helper.collective.council.close(1051 dorothyAccount,1052 motionHash,1053 councilProposalIdx,1054 {1055 refTime: 1_000_000_000,1056 proofSize: 1_000_000,1057 },1058 externalMotion.encodedLength,1059 );1060 console.log('\t* Propose external motion through council.......DONE');1061 // <<< Propose external motion through council <<<10621063 // >>> Fast track proposal through technical committee >>>1064 console.log('\t* Fast track proposal through technical committee.......');1065 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1066 const encodedFastTrack = fastTrack?.method.toHex() || '';1067 const fastTrackHash = blake2AsHex(encodedFastTrack);1068 console.log('\t* FastTrack hash is %s', fastTrackHash);10691070 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);10711072 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1073 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1074 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);10751076 await this.helper.collective.techCommittee.close(1077 baltatharAccount,1078 fastTrackHash,1079 techProposalIdx,1080 {1081 refTime: 1_000_000_000,1082 proofSize: 1_000_000,1083 },1084 fastTrack.encodedLength,1085 );1086 console.log('\t* Fast track proposal through technical committee.......DONE');1087 // <<< Fast track proposal through technical committee <<<10881089 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1090 const referendumIndex = democracyStarted.referendumIndex;10911092 // >>> Referendum voting >>>1093 console.log(`\t* Referendum #${referendumIndex} voting.......`);1094 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1095 balance: 10_000_000_000_000_000_000n,1096 vote: {aye: true, conviction: 1},1097 });1098 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1099 // <<< Referendum voting <<<11001101 // Wait the proposal to pass1102 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11031104 await this.helper.wait.newBlocks(1);11051106 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1107 }1108}11091110class WaitGroup {1111 helper: ChainHelperBase;11121113 constructor(helper: ChainHelperBase) {1114 this.helper = helper;1115 }11161117 sleep(milliseconds: number) {1118 return new Promise((resolve) => setTimeout(resolve, milliseconds));1119 }11201121 private async waitWithTimeout(promise: Promise<any>, timeout: number) {1122 let isBlock = false;1123 promise.then(() => isBlock = true).catch(() => isBlock = true);1124 let totalTime = 0;1125 const step = 100;1126 while(!isBlock) {1127 await this.sleep(step);1128 totalTime += step;1129 if(totalTime >= timeout) throw Error('Blocks production failed');1130 }1131 return promise;1132 }11331134 /**1135 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1136 * @param promise async operation to race against the timeout1137 * @param timeoutMS time after which to time out1138 * @param timeoutError error message to throw1139 * @returns promise of the same type the operation had1140 */1141 withTimeout<T>(1142 promise: Promise<T>,1143 timeoutMS = 30000,1144 timeoutError = 'The operation has timed out!',1145 ): Promise<T> {1146 const timeout = new Promise<never>((_, reject) => {1147 setTimeout(() => {1148 reject(new Error(timeoutError));1149 }, timeoutMS);1150 });11511152 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1153 }11541155 /**1156 * Wait for specified number of blocks1157 * @param blocksCount number of blocks to wait1158 * @returns1159 */1160 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1161 timeout = timeout ?? blocksCount * 60_000;1162 // eslint-disable-next-line no-async-promise-executor1163 const promise = new Promise<void>(async (resolve) => {1164 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1165 if(blocksCount > 0) {1166 blocksCount--;1167 } else {1168 unsubscribe();1169 resolve();1170 }1171 });1172 });1173 await this.waitWithTimeout(promise, timeout);1174 return promise;1175 }11761177 /**1178 * Wait for the specified number of sessions to pass.1179 * Only applicable if the Session pallet is turned on.1180 * @param sessionCount number of sessions to wait1181 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1182 * @returns1183 */1184 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1185 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.`1186 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');11871188 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1189 let currentSessionIndex = -1;11901191 while(currentSessionIndex < expectedSessionIndex) {1192 // eslint-disable-next-line no-async-promise-executor1193 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1194 await this.newBlocks(1);1195 const res = await (this.helper as DevUniqueHelper).session.getIndex();1196 resolve(res);1197 }), blockTimeout, 'The chain has stopped producing blocks!');1198 }1199 }12001201 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1202 timeout = timeout ?? 30 * 60 * 1000;1203 // eslint-disable-next-line no-async-promise-executor1204 const promise = new Promise<void>(async (resolve) => {1205 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1206 if(data.number.toNumber() >= blockNumber) {1207 unsubscribe();1208 resolve();1209 }1210 });1211 });1212 await this.waitWithTimeout(promise, timeout);1213 return promise;1214 }12151216 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1217 timeout = timeout ?? 30 * 60 * 1000;1218 // eslint-disable-next-line no-async-promise-executor1219 const promise = new Promise<void>(async (resolve) => {1220 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1221 if(data.value.relayParentNumber.toNumber() >= blockNumber) {1222 // @ts-ignore1223 unsubscribe();1224 resolve();1225 }1226 });1227 });1228 await this.waitWithTimeout(promise, timeout);1229 return promise;1230 }12311232 noScheduledTasks() {1233 const api = this.helper.getApi();12341235 // eslint-disable-next-line no-async-promise-executor1236 const promise = new Promise<void>(async resolve => {1237 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1238 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();12391240 if(areThereScheduledTasks.length == 0) {1241 unsubscribe();1242 resolve();1243 }1244 });1245 });12461247 return promise;1248 }12491250 parachainBlockMultiplesOf(val: bigint) {1251 // eslint-disable-next-line no-async-promise-executor1252 const promise = new Promise<void>(async resolve => {1253 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1254 if(data.number.toBigInt() % val == 0n) {1255 console.log(`from waiter: ${data.number.toBigInt()}`);1256 unsubscribe();1257 resolve();1258 }1259 });1260 });1261 return promise;1262 }12631264 event<T extends IEventHelper>(1265 maxBlocksToWait: number,1266 eventHelper: T,1267 filter: (_: any) => boolean = () => true,1268 ): any {1269 // eslint-disable-next-line no-async-promise-executor1270 const promise = new Promise<T | null>(async (resolve) => {1271 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1272 const blockNumber = header.number.toHuman();1273 const blockHash = header.hash;1274 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1275 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;12761277 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);12781279 const apiAt = await this.helper.getApi().at(blockHash);1280 const eventRecords = (await apiAt.query.system.events()) as any;12811282 const neededEvent = eventRecords.toArray()1283 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1284 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1285 .find(filter);12861287 if(neededEvent) {1288 unsubscribe();1289 resolve(neededEvent);1290 } else if(maxBlocksToWait > 0) {1291 maxBlocksToWait--;1292 } else {1293 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1294 The wait lasted until block ${blockNumber} inclusive`);1295 unsubscribe();1296 resolve(null);1297 }1298 });1299 });1300 return promise;1301 }13021303 async expectEvent<T extends IEventHelper>(1304 maxBlocksToWait: number,1305 eventHelper: T,1306 filter: (e: any) => boolean = () => true,1307 ) {1308 const e = await this.event(maxBlocksToWait, eventHelper, filter);1309 if(e == null) {1310 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1311 } else {1312 return e;1313 }1314 }1315}13161317class SessionGroup {1318 helper: ChainHelperBase;13191320 constructor(helper: ChainHelperBase) {1321 this.helper = helper;1322 }13231324 //todo:collator documentation1325 async getIndex(): Promise<number> {1326 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1327 }13281329 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1330 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1331 }13321333 setOwnKeys(signer: TSigner, key: string) {1334 return this.helper.executeExtrinsic(1335 signer,1336 'api.tx.session.setKeys',1337 [key, '0x0'],1338 true,1339 );1340 }13411342 setOwnKeysFromAddress(signer: TSigner) {1343 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1344 }1345}13461347class TestUtilGroup {1348 helper: DevUniqueHelper;13491350 constructor(helper: DevUniqueHelper) {1351 this.helper = helper;1352 }13531354 async enable() {1355 if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1356 return;1357 }13581359 const signer = this.helper.util.fromSeed('//Alice');1360 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1361 }13621363 async setTestValue(signer: TSigner, testVal: number) {1364 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1365 }13661367 async incTestValue(signer: TSigner) {1368 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1369 }13701371 async setTestValueAndRollback(signer: TSigner, testVal: number) {1372 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1373 }13741375 async testValue(blockIdx?: number) {1376 const api = blockIdx1377 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1378 : this.helper.getApi();13791380 return (await api.query.testUtils.testValue()).toJSON();1381 }13821383 async justTakeFee(signer: TSigner) {1384 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1385 }13861387 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1388 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1389 }1390}13911392class EventCapture {1393 helper: DevUniqueHelper;1394 eventSection: string;1395 eventMethod: string;1396 events: EventRecord[] = [];1397 unsubscribe: VoidFn | null = null;13981399 constructor(1400 helper: DevUniqueHelper,1401 eventSection: string,1402 eventMethod: string,1403 ) {1404 this.helper = helper;1405 this.eventSection = eventSection;1406 this.eventMethod = eventMethod;1407 }14081409 async startCapture() {1410 this.stopCapture();1411 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1412 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);14131414 this.events.push(...newEvents);1415 })) as any;1416 }14171418 stopCapture() {1419 if(this.unsubscribe !== null) {1420 this.unsubscribe();1421 }1422 }14231424 extractCapturedEvents() {1425 return this.events;1426 }1427}14281429class AdminGroup {1430 helper: UniqueHelper;14311432 constructor(helper: UniqueHelper) {1433 this.helper = helper;1434 }14351436 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1437 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1438 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1439 staker: e.event.data[0].toString(),1440 stake: e.event.data[1].toBigInt(),1441 payout: e.event.data[2].toBigInt(),1442 }));1443 }1444}14451446// eslint-disable-next-line @typescript-eslint/naming-convention1447function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1448 return class extends Base {1449 scheduleFn: 'schedule' | 'scheduleAfter';1450 blocksNum: number;1451 options: ISchedulerOptions;14521453 constructor(...args: any[]) {1454 const logger = args[0] as ILogger;1455 const options = args[1] as {1456 scheduleFn: 'schedule' | 'scheduleAfter',1457 blocksNum: number,1458 options: ISchedulerOptions1459 };14601461 super(logger);14621463 this.scheduleFn = options.scheduleFn;1464 this.blocksNum = options.blocksNum;1465 this.options = options.options;1466 }14671468 executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1469 const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);14701471 const mandatorySchedArgs = [1472 this.blocksNum,1473 this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1474 this.options.priority ?? null,1475 scheduledTx,1476 ];14771478 let schedArgs;1479 let scheduleFn;14801481 if(this.options.scheduledId) {1482 schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];14831484 if(this.scheduleFn == 'schedule') {1485 scheduleFn = 'scheduleNamed';1486 } else if(this.scheduleFn == 'scheduleAfter') {1487 scheduleFn = 'scheduleNamedAfter';1488 }1489 } else {1490 schedArgs = mandatorySchedArgs;1491 scheduleFn = this.scheduleFn;1492 }14931494 const extrinsic = 'api.tx.scheduler.' + scheduleFn;14951496 return super.executeExtrinsic(1497 sender,1498 extrinsic as any,1499 schedArgs,1500 expectSuccess,1501 );1502 }1503 };1504}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, ChainHelperBase, ChainHelperBaseConstructor, HelperGroup, UniqueHelperConstructor} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError, XcmV3TraitsOutcome} from '@polkadot/types/lookup';13import {SignerOptions, VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';16import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup} from './unique.xcm';17import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, ICollectiveGroup, IFellowshipGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance';1819export class SilentLogger {20 log(_msg: any, _level: any): void { }21 level = {22 ERROR: 'ERROR' as const,23 WARNING: 'WARNING' as const,24 INFO: 'INFO' as const,25 };26}2728export class SilentConsole {29 // TODO: Remove, this is temporary: Filter unneeded API output30 // (Jaco promised it will be removed in the next version)31 consoleErr: any;32 consoleLog: any;33 consoleWarn: any;3435 constructor() {36 this.consoleErr = console.error;37 this.consoleLog = console.log;38 this.consoleWarn = console.warn;39 }4041 enable() {42 const outFn = (printer: any) => (...args: any[]) => {43 for(const arg of args) {44 if(typeof arg !== 'string')45 continue;46 const skippedWarnings = ['1000:: Normal connection closure', 'Not decorating unknown runtime apis:', 'RPC methods not decorated:', 'Not decorating runtime apis', 'Bad input data provided to validate_transaction', 'account balance too low', '1006:: Abnormal Closure'];47 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);48 if(needToSkip || arg === 'Normal connection closure')49 return;50 }51 printer(...args);52 };5354 console.error = outFn(this.consoleErr.bind(console));55 console.log = outFn(this.consoleLog.bind(console));56 console.warn = outFn(this.consoleWarn.bind(console));57 }5859 disable() {60 console.error = this.consoleErr;61 console.log = this.consoleLog;62 console.warn = this.consoleWarn;63 }64}6566export interface IEventHelper {67 section(): string;6869 method(): string;7071 wrapEvent(data: any[]): any;72}7374// eslint-disable-next-line @typescript-eslint/naming-convention75function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {76 const helperClass = class implements IEventHelper {77 wrapEvent: (data: any[]) => any;78 _section: string;79 _method: string;8081 constructor() {82 this.wrapEvent = wrapEvent;83 this._section = section;84 this._method = method;85 }8687 section(): string {88 return this._section;89 }9091 method(): string {92 return this._method;93 }9495 filter(txres: ITransactionResult) {96 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)97 .map(e => this.wrapEvent(e.event.data));98 }99100 find(txres: ITransactionResult) {101 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);102 return e ? this.wrapEvent(e.event.data) : null;103 }104105 expect(txres: ITransactionResult) {106 const e = this.find(txres);107 if(e) {108 return e;109 } else {110 throw Error(`Expected event ${section}.${method}`);111 }112 }113 };114115 return helperClass;116}117118function eventJsonData<T = any>(data: any[], index: number) {119 return data[index].toJSON() as T;120}121122function eventHumanData(data: any[], index: number) {123 return data[index].toHuman();124}125126function eventData<T = any>(data: any[], index: number) {127 return data[index] as T;128}129130// eslint-disable-next-line @typescript-eslint/naming-convention131function EventSection(section: string) {132 return class Section {133 static section = section;134135 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {136 const helperClass = EventHelper(Section.section, name, wrapEvent);137 return new helperClass();138 }139 };140}141142function schedulerSection(schedulerInstance: string) {143 return class extends EventSection(schedulerInstance) {144 static Dispatched = this.Method('Dispatched', data => ({145 task: eventJsonData(data, 0),146 id: eventHumanData(data, 1),147 result: data[2],148 }));149150 static PriorityChanged = this.Method('PriorityChanged', data => ({151 task: eventJsonData(data, 0),152 priority: eventJsonData(data, 1),153 }));154 };155}156157export class Event {158 static Democracy = class extends EventSection('democracy') {159 static Proposed = this.Method('Proposed', data => ({160 proposalIndex: eventJsonData<number>(data, 0),161 }));162163 static ExternalTabled = this.Method('ExternalTabled');164165 static Started = this.Method('Started', data => ({166 referendumIndex: eventJsonData<number>(data, 0),167 threshold: eventHumanData(data, 1),168 }));169170 static Voted = this.Method('Voted', data => ({171 voter: eventJsonData(data, 0),172 referendumIndex: eventJsonData<number>(data, 1),173 vote: eventJsonData(data, 2),174 }));175176 static Passed = this.Method('Passed', data => ({177 referendumIndex: eventJsonData<number>(data, 0),178 }));179180 static ProposalCanceled = this.Method('ProposalCanceled', data => ({181 propIndex: eventJsonData<number>(data, 0),182 }));183184 static Cancelled = this.Method('Cancelled', data => ({185 propIndex: eventJsonData<number>(data, 0),186 }));187188 static Vetoed = this.Method('Vetoed', data => ({189 who: eventHumanData(data, 0),190 proposalHash: eventHumanData(data, 1),191 until: eventJsonData<number>(data, 1),192 }));193 };194195 static Council = class extends EventSection('council') {196 static Proposed = this.Method('Proposed', data => ({197 account: eventHumanData(data, 0),198 proposalIndex: eventJsonData<number>(data, 1),199 proposalHash: eventHumanData(data, 2),200 threshold: eventJsonData<number>(data, 3),201 }));202 static Closed = this.Method('Closed', data => ({203 proposalHash: eventHumanData(data, 0),204 yes: eventJsonData<number>(data, 1),205 no: eventJsonData<number>(data, 2),206 }));207 static Executed = this.Method('Executed', data => ({208 proposalHash: eventHumanData(data, 0),209 }));210 };211212 static TechnicalCommittee = class extends EventSection('technicalCommittee') {213 static Proposed = this.Method('Proposed', data => ({214 account: eventHumanData(data, 0),215 proposalIndex: eventJsonData<number>(data, 1),216 proposalHash: eventHumanData(data, 2),217 threshold: eventJsonData<number>(data, 3),218 }));219 static Closed = this.Method('Closed', data => ({220 proposalHash: eventHumanData(data, 0),221 yes: eventJsonData<number>(data, 1),222 no: eventJsonData<number>(data, 2),223 }));224 static Approved = this.Method('Approved', data => ({225 proposalHash: eventHumanData(data, 0),226 }));227 static Executed = this.Method('Executed', data => ({228 proposalHash: eventHumanData(data, 0),229 result: eventHumanData(data, 1),230 }));231 };232233 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {234 static Submitted = this.Method('Submitted', data => ({235 referendumIndex: eventJsonData<number>(data, 0),236 trackId: eventJsonData<number>(data, 1),237 proposal: eventJsonData(data, 2),238 }));239240 static Cancelled = this.Method('Cancelled', data => ({241 index: eventJsonData<number>(data, 0),242 tally: eventJsonData(data, 1),243 }));244 };245246 static UniqueScheduler = schedulerSection('uniqueScheduler');247 static Scheduler = schedulerSection('scheduler');248249 static XcmpQueue = class extends EventSection('xcmpQueue') {250 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({251 messageHash: eventJsonData(data, 0),252 }));253254 static Success = this.Method('Success', data => ({255 messageHash: eventJsonData(data, 0),256 }));257258 static Fail = this.Method('Fail', data => ({259 messageHash: eventJsonData(data, 0),260 outcome: eventData<XcmV2TraitsError>(data, 1),261 }));262 };263264 static DmpQueue = class extends EventSection('dmpQueue') {265 static ExecutedDownward = this.Method('ExecutedDownward', data => ({266 outcome: eventData<XcmV3TraitsOutcome>(data, 1),267 }));268 };269}270271// eslint-disable-next-line @typescript-eslint/naming-convention272export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {273 return class extends Base {274 constructor(...args: any[]) {275 super(...args);276 }277278 async executeExtrinsic(279 sender: IKeyringPair,280 extrinsic: string,281 params: any[],282 expectSuccess?: boolean,283 options: Partial<SignerOptions> | null = null,284 ): Promise<ITransactionResult> {285 const call = this.constructApiCall(extrinsic, params);286 const result = await super.executeExtrinsic(287 sender,288 'api.tx.sudo.sudo',289 [call],290 expectSuccess,291 options,292 );293294 if(result.status === 'Fail') return result;295296 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;297 if(data.isErr) {298 if(data.asErr.isModule) {299 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;300 const metaError = super.getApi()?.registry.findMetaError(error);301 throw new Error(`${metaError.section}.${metaError.name}`);302 } else if(data.asErr.isToken) {303 throw new Error(`Token: ${data.asErr.asToken}`);304 }305 // May be [object Object] in case of unhandled non-unit enum306 throw new Error(`Misc: ${data.asErr.toHuman()}`);307 }308 return result;309 }310 async executeExtrinsicUncheckedWeight(311 sender: IKeyringPair,312 extrinsic: string,313 params: any[],314 expectSuccess?: boolean,315 options: Partial<SignerOptions> | null = null,316 ): Promise<ITransactionResult> {317 const call = this.constructApiCall(extrinsic, params);318 const result = await super.executeExtrinsic(319 sender,320 'api.tx.sudo.sudoUncheckedWeight',321 [call, {refTime: 0, proofSize: 0}],322 expectSuccess,323 options,324 );325326 if(result.status === 'Fail') return result;327328 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;329 if(data.isErr) {330 if(data.asErr.isModule) {331 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;332 const metaError = super.getApi()?.registry.findMetaError(error);333 throw new Error(`${metaError.section}.${metaError.name}`);334 } else if(data.asErr.isToken) {335 throw new Error(`Token: ${data.asErr.asToken}`);336 }337 // May be [object Object] in case of unhandled non-unit enum338 throw new Error(`Misc: ${data.asErr.toHuman()}`);339 }340 return result;341 }342 };343}344345class SchedulerGroup extends HelperGroup<UniqueHelper> {346 constructor(helper: UniqueHelper) {347 super(helper);348 }349350 cancelScheduled(signer: TSigner, scheduledId: string) {351 return this.helper.executeExtrinsic(352 signer,353 'api.tx.scheduler.cancelNamed',354 [scheduledId],355 true,356 );357 }358359 changePriority(signer: TSigner, scheduledId: string, priority: number) {360 return this.helper.executeExtrinsic(361 signer,362 'api.tx.scheduler.changeNamedPriority',363 [scheduledId, priority],364 true,365 );366 }367368 scheduleAt<T extends DevUniqueHelper>(369 executionBlockNumber: number,370 options: ISchedulerOptions = {},371 ) {372 return this.schedule<T>('schedule', executionBlockNumber, options);373 }374375 scheduleAfter<T extends DevUniqueHelper>(376 blocksBeforeExecution: number,377 options: ISchedulerOptions = {},378 ) {379 return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);380 }381382 schedule<T extends UniqueHelper>(383 scheduleFn: 'schedule' | 'scheduleAfter',384 blocksNum: number,385 options: ISchedulerOptions = {},386 ) {387 // eslint-disable-next-line @typescript-eslint/naming-convention388 const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);389 return this.helper.clone(ScheduledHelperType, {390 scheduleFn,391 blocksNum,392 options,393 }) as T;394 }395}396397class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {398 //todo:collator documentation399 addInvulnerable(signer: TSigner, address: string) {400 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);401 }402403 removeInvulnerable(signer: TSigner, address: string) {404 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);405 }406407 async getInvulnerables(): Promise<string[]> {408 return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());409 }410411 /** and also total max invulnerables */412 maxCollators(): number {413 return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);414 }415416 async getDesiredCollators(): Promise<number> {417 return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();418 }419420 setLicenseBond(signer: TSigner, amount: bigint) {421 return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);422 }423424 async getLicenseBond(): Promise<bigint> {425 return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();426 }427428 obtainLicense(signer: TSigner) {429 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);430 }431432 releaseLicense(signer: TSigner) {433 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);434 }435436 forceReleaseLicense(signer: TSigner, released: string) {437 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);438 }439440 async hasLicense(address: string): Promise<bigint> {441 return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();442 }443444 onboard(signer: TSigner) {445 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);446 }447448 offboard(signer: TSigner) {449 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);450 }451452 async getCandidates(): Promise<string[]> {453 return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());454 }455}456457export class DevUniqueHelper extends UniqueHelper {458 /**459 * Arrange methods for tests460 */461 arrange: ArrangeGroup;462 wait: WaitGroup;463 admin: AdminGroup;464 session: SessionGroup;465 testUtils: TestUtilGroup;466 foreignAssets: ForeignAssetsGroup;467 xcm: XcmGroup<DevUniqueHelper>;468 xTokens: XTokensGroup<DevUniqueHelper>;469 tokens: TokensGroup<DevUniqueHelper>;470 scheduler: SchedulerGroup;471 collatorSelection: CollatorSelectionGroup;472 council: ICollectiveGroup;473 technicalCommittee: ICollectiveGroup;474 fellowship: IFellowshipGroup;475 democracy: DemocracyGroup;476477 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {478 options.helperBase = options.helperBase ?? DevUniqueHelper;479480 super(logger, options);481 this.arrange = new ArrangeGroup(this);482 this.wait = new WaitGroup(this);483 this.admin = new AdminGroup(this);484 this.testUtils = new TestUtilGroup(this);485 this.session = new SessionGroup(this);486 this.foreignAssets = new ForeignAssetsGroup(this);487 this.xcm = new XcmGroup(this, 'polkadotXcm');488 this.xTokens = new XTokensGroup(this);489 this.tokens = new TokensGroup(this);490 this.scheduler = new SchedulerGroup(this);491 this.collatorSelection = new CollatorSelectionGroup(this);492 this.council = {493 collective: new CollectiveGroup(this, 'council'),494 membership: new CollectiveMembershipGroup(this, 'councilMembership'),495 };496 this.technicalCommittee = {497 collective: new CollectiveGroup(this, 'technicalCommittee'),498 membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),499 };500 this.fellowship = {501 collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),502 referenda: new ReferendaGroup(this, 'fellowshipReferenda'),503 };504 this.democracy = new DemocracyGroup(this);505 }506507 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {508 if(!wsEndpoint) throw new Error('wsEndpoint was not set');509 const wsProvider = new WsProvider(wsEndpoint);510 this.api = new ApiPromise({511 provider: wsProvider,512 signedExtensions: {513 ContractHelpers: {514 extrinsic: {},515 payload: {},516 },517 CheckMaintenance: {518 extrinsic: {},519 payload: {},520 },521 DisableIdentityCalls: {522 extrinsic: {},523 payload: {},524 },525 FakeTransactionFinalizer: {526 extrinsic: {},527 payload: {},528 },529 },530 rpc: {531 unique: defs.unique.rpc,532 appPromotion: defs.appPromotion.rpc,533 povinfo: defs.povinfo.rpc,534 eth: {535 feeHistory: {536 description: 'Dummy',537 params: [],538 type: 'u8',539 },540 maxPriorityFeePerGas: {541 description: 'Dummy',542 params: [],543 type: 'u8',544 },545 },546 },547 });548 await this.api.isReadyOrError;549 this.network = await UniqueHelper.detectNetwork(this.api);550 this.wsEndpoint = wsEndpoint;551 }552 getSudo<T extends DevUniqueHelper>() {553 // eslint-disable-next-line @typescript-eslint/naming-convention554 const SudoHelperType = SudoHelper(this.helperBase);555 return this.clone(SudoHelperType) as T;556 }557}558559export class DevRelayHelper extends RelayHelper {560 wait: WaitGroup;561562 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {563 options.helperBase = options.helperBase ?? DevRelayHelper;564565 super(logger, options);566 this.wait = new WaitGroup(this);567 }568569 getSudo() {570 // eslint-disable-next-line @typescript-eslint/naming-convention571 const SudoHelperType = SudoHelper(this.helperBase);572 return this.clone(SudoHelperType) as DevRelayHelper;573 }574}575576export class DevWestmintHelper extends WestmintHelper {577 wait: WaitGroup;578579 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {580 options.helperBase = options.helperBase ?? DevWestmintHelper;581582 super(logger, options);583 this.wait = new WaitGroup(this);584 }585}586587export class DevStatemineHelper extends DevWestmintHelper {}588589export class DevStatemintHelper extends DevWestmintHelper {}590591export class DevMoonbeamHelper extends MoonbeamHelper {592 account: MoonbeamAccountGroup;593 wait: WaitGroup;594 fastDemocracy: MoonbeamFastDemocracyGroup;595596 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {597 options.helperBase = options.helperBase ?? DevMoonbeamHelper;598 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';599600 super(logger, options);601 this.account = new MoonbeamAccountGroup(this);602 this.wait = new WaitGroup(this);603 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);604 }605}606607export class DevMoonriverHelper extends DevMoonbeamHelper {608 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {609 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';610 super(logger, options);611 }612}613614export class DevAstarHelper extends AstarHelper {615 wait: WaitGroup;616617 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {618 options.helperBase = options.helperBase ?? DevAstarHelper;619620 super(logger, options);621 this.wait = new WaitGroup(this);622 }623624 getSudo<T extends AstarHelper>() {625 // eslint-disable-next-line @typescript-eslint/naming-convention626 const SudoHelperType = SudoHelper(this.helperBase);627 return this.clone(SudoHelperType) as T;628 }629}630631export class DevShidenHelper extends DevAstarHelper { }632633export class DevAcalaHelper extends AcalaHelper {634 wait: WaitGroup;635636 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {637 options.helperBase = options.helperBase ?? DevAcalaHelper;638639 super(logger, options);640 this.wait = new WaitGroup(this);641 }642 getSudo() {643 // eslint-disable-next-line @typescript-eslint/naming-convention644 const SudoHelperType = SudoHelper(this.helperBase);645 return this.clone(SudoHelperType) as DevAcalaHelper;646 }647}648649export class DevPolkadexHelper extends PolkadexHelper {650 wait: WaitGroup;651 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {652 options.helperBase = options.helperBase ?? PolkadexHelper;653654 super(logger, options);655 this.wait = new WaitGroup(this);656 }657658 getSudo() {659 // eslint-disable-next-line @typescript-eslint/naming-convention660 const SudoHelperType = SudoHelper(this.helperBase);661 return this.clone(SudoHelperType) as DevPolkadexHelper;662 }663}664665export class DevKaruraHelper extends DevAcalaHelper {}666667export class ArrangeGroup {668 helper: DevUniqueHelper;669670 scheduledIdSlider = 0;671672 constructor(helper: DevUniqueHelper) {673 this.helper = helper;674 }675676 /**677 * Generates accounts with the specified UNQ token balance678 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.679 * @param donor donor account for balances680 * @returns array of newly created accounts681 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);682 */683 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {684 let nonce = await this.helper.chain.getNonce(donor.address);685 const wait = new WaitGroup(this.helper);686 const ss58Format = this.helper.chain.getChainProperties().ss58Format;687 const tokenNominal = this.helper.balance.getOneTokenNominal();688 const transactions = [];689 const accounts: IKeyringPair[] = [];690 for(const balance of balances) {691 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);692 accounts.push(recipient);693 if(balance !== 0n) {694 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);695 transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));696 nonce++;697 }698 }699700 await Promise.all(transactions).catch(_e => {});701702 //#region TODO remove this region, when nonce problem will be solved703 const checkBalances = async () => {704 let isSuccess = true;705 for(let i = 0; i < balances.length; i++) {706 const balance = await this.helper.balance.getSubstrate(accounts[i].address);707 if(balance !== balances[i] * tokenNominal) {708 isSuccess = false;709 break;710 }711 }712 return isSuccess;713 };714715 let accountsCreated = false;716 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;717 // checkBalances retry up to 5-50 blocks718 for(let index = 0; index < maxBlocksChecked; index++) {719 accountsCreated = await checkBalances();720 if(accountsCreated) break;721 await wait.newBlocks(1);722 }723724 if(!accountsCreated) throw Error('Accounts generation failed');725 //#endregion726727 return accounts;728 };729730 // TODO combine this method and createAccounts into one731 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {732 const createAsManyAsCan = async () => {733 let transactions: any = [];734 const accounts: IKeyringPair[] = [];735 let nonce = await this.helper.chain.getNonce(donor.address);736 const tokenNominal = this.helper.balance.getOneTokenNominal();737 const ss58Format = this.helper.chain.getChainProperties().ss58Format;738 for(let i = 0; i < accountsToCreate; i++) {739 if(i === 500) { // if there are too many accounts to create740 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled741 transactions = []; //742 nonce = await this.helper.chain.getNonce(donor.address); // update nonce743 }744 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);745 accounts.push(recipient);746 if(withBalance !== 0n) {747 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);748 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));749 nonce++;750 }751 }752753 const fullfilledAccounts = [];754 await Promise.allSettled(transactions);755 for(const account of accounts) {756 const accountBalance = await this.helper.balance.getSubstrate(account.address);757 if(accountBalance === withBalance * tokenNominal) {758 fullfilledAccounts.push(account);759 }760 }761 return fullfilledAccounts;762 };763764765 const crowd: IKeyringPair[] = [];766 // do up to 5 retries767 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {768 const asManyAsCan = await createAsManyAsCan();769 crowd.push(...asManyAsCan);770 accountsToCreate -= asManyAsCan.length;771 }772773 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);774775 return crowd;776 };777778 /**779 * Generates one account with zero balance780 * @returns the newly generated account781 * @example const account = await helper.arrange.createEmptyAccount();782 */783 createEmptyAccount = (): IKeyringPair => {784 const ss58Format = this.helper.chain.getChainProperties().ss58Format;785 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);786 };787788 isDevNode = async () => {789 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();790 if(blockNumber == 0) {791 await this.helper.wait.newBlocks(1);792 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();793 }794 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);795 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);796 const findCreationDate = (block: any) => {797 const humanBlock = block.toHuman();798 let date;799 humanBlock.block.extrinsics.forEach((ext: any) => {800 if(ext.method.section === 'timestamp') {801 date = Number(ext.method.args.now.replaceAll(',', ''));802 }803 });804 return date;805 };806 const block1date = await findCreationDate(block1);807 const block2date = await findCreationDate(block2);808 if(block2date! - block1date! < 9000) return true;809 };810811 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {812 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);813 let balance = await this.helper.balance.getSubstrate(address);814815 await promise();816817 balance -= await this.helper.balance.getSubstrate(address);818819 return balance;820 }821822 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {823 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);824825 const kvJson: {[key: string]: string} = {};826827 for(const kv of rawPovInfo.keyValues) {828 kvJson[kv.key.toHex()] = kv.value.toHex();829 }830831 const kvStr = JSON.stringify(kvJson);832833 const chainql = spawnSync(834 'chainql',835 [836 `--tla-code=data=${kvStr}`,837 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,838 ],839 );840841 if(!chainql.stdout) {842 throw Error('unable to get an output from the `chainql`');843 }844845 return {846 proofSize: rawPovInfo.proofSize.toNumber(),847 compactProofSize: rawPovInfo.compactProofSize.toNumber(),848 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),849 results: rawPovInfo.results,850 kv: JSON.parse(chainql.stdout.toString()),851 };852 }853854 calculatePalletAddress(palletId: any) {855 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));856 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);857 }858859 makeScheduledIds(num: number): string[] {860 function makeId(slider: number) {861 const scheduledIdSize = 64;862 const hexId = slider.toString(16);863 const prefixSize = scheduledIdSize - hexId.length;864865 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;866867 return scheduledId;868 }869870 const ids = [];871 for(let i = 0; i < num; i++) {872 ids.push(makeId(this.scheduledIdSlider));873 this.scheduledIdSlider += 1;874 }875876 return ids;877 }878879 makeScheduledId(): string {880 return (this.makeScheduledIds(1))[0];881 }882883 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {884 const capture = new EventCapture(this.helper, eventSection, eventMethod);885 await capture.startCapture();886887 return capture;888 }889890 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {891 return {892 V2: [893 {894 WithdrawAsset: [895 {896 id,897 fun: {898 Fungible: amount,899 },900 },901 ],902 },903 {904 BuyExecution: {905 fees: {906 id,907 fun: {908 Fungible: amount,909 },910 },911 weightLimit: 'Unlimited',912 },913 },914 {915 DepositAsset: {916 assets: {917 Wild: 'All',918 },919 maxAssets: 1,920 beneficiary: {921 parents: 0,922 interior: {923 X1: {924 AccountId32: {925 network: 'Any',926 id: beneficiary,927 },928 },929 },930 },931 },932 },933 ],934 };935 }936937 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {938 return {939 V2: [940 {941 ReserveAssetDeposited: [942 {943 id,944 fun: {945 Fungible: amount,946 },947 },948 ],949 },950 {951 BuyExecution: {952 fees: {953 id,954 fun: {955 Fungible: amount,956 },957 },958 weightLimit: 'Unlimited',959 },960 },961 {962 DepositAsset: {963 assets: {964 Wild: 'All',965 },966 maxAssets: 1,967 beneficiary: {968 parents: 0,969 interior: {970 X1: {971 AccountId32: {972 network: 'Any',973 id: beneficiary,974 },975 },976 },977 },978 },979 },980 ],981 };982 }983984 makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {985 return {986 V3: [987 {988 UnpaidExecution: {989 weightLimit: 'Unlimited',990 checkOrigin: null,991 },992 },993 {994 Transact: {995 originKind: 'Superuser',996 requireWeightAtMost: {997 refTime: info.weightMultiplier * 200000000,998 proofSize: info.weightMultiplier * 3000,999 },1000 call: {1001 encoded: info.call,1002 },1003 },1004 },1005 ],1006 };1007 }1008}10091010class MoonbeamAccountGroup {1011 helper: MoonbeamHelper;10121013 keyring: Keyring;1014 _alithAccount: IKeyringPair;1015 _baltatharAccount: IKeyringPair;1016 _dorothyAccount: IKeyringPair;10171018 constructor(helper: MoonbeamHelper) {1019 this.helper = helper;10201021 this.keyring = new Keyring({type: 'ethereum'});1022 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';1023 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';1024 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';10251026 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');1027 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');1028 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');1029 }10301031 alithAccount() {1032 return this._alithAccount;1033 }10341035 baltatharAccount() {1036 return this._baltatharAccount;1037 }10381039 dorothyAccount() {1040 return this._dorothyAccount;1041 }10421043 create() {1044 return this.keyring.addFromUri(mnemonicGenerate());1045 }1046}10471048class MoonbeamFastDemocracyGroup {1049 helper: DevMoonbeamHelper;10501051 constructor(helper: DevMoonbeamHelper) {1052 this.helper = helper;1053 }10541055 async executeProposal(proposalDesciption: string, encodedProposal: string) {1056 const proposalHash = blake2AsHex(encodedProposal);10571058 const alithAccount = this.helper.account.alithAccount();1059 const baltatharAccount = this.helper.account.baltatharAccount();1060 const dorothyAccount = this.helper.account.dorothyAccount();10611062 const councilVotingThreshold = 2;1063 const technicalCommitteeThreshold = 2;1064 const fastTrackVotingPeriod = 3;1065 const fastTrackDelayPeriod = 0;10661067 console.log(`[democracy] executing '${proposalDesciption}' proposal`);10681069 // >>> Propose external motion through council >>>1070 console.log('\t* Propose external motion through council.......');1071 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1072 const encodedMotion = externalMotion?.method.toHex() || '';1073 const motionHash = blake2AsHex(encodedMotion);1074 console.log('\t* Motion hash is %s', motionHash);10751076 await this.helper.collective.council.propose(1077 baltatharAccount,1078 councilVotingThreshold,1079 externalMotion,1080 externalMotion.encodedLength,1081 );10821083 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1084 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1085 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);10861087 await this.helper.collective.council.close(1088 dorothyAccount,1089 motionHash,1090 councilProposalIdx,1091 {1092 refTime: 1_000_000_000,1093 proofSize: 1_000_000,1094 },1095 externalMotion.encodedLength,1096 );1097 console.log('\t* Propose external motion through council.......DONE');1098 // <<< Propose external motion through council <<<10991100 // >>> Fast track proposal through technical committee >>>1101 console.log('\t* Fast track proposal through technical committee.......');1102 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1103 const encodedFastTrack = fastTrack?.method.toHex() || '';1104 const fastTrackHash = blake2AsHex(encodedFastTrack);1105 console.log('\t* FastTrack hash is %s', fastTrackHash);11061107 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);11081109 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1110 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1111 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);11121113 await this.helper.collective.techCommittee.close(1114 baltatharAccount,1115 fastTrackHash,1116 techProposalIdx,1117 {1118 refTime: 1_000_000_000,1119 proofSize: 1_000_000,1120 },1121 fastTrack.encodedLength,1122 );1123 console.log('\t* Fast track proposal through technical committee.......DONE');1124 // <<< Fast track proposal through technical committee <<<11251126 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1127 const referendumIndex = democracyStarted.referendumIndex;11281129 // >>> Referendum voting >>>1130 console.log(`\t* Referendum #${referendumIndex} voting.......`);1131 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1132 balance: 10_000_000_000_000_000_000n,1133 vote: {aye: true, conviction: 1},1134 });1135 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1136 // <<< Referendum voting <<<11371138 // Wait the proposal to pass1139 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11401141 await this.helper.wait.newBlocks(1);11421143 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1144 }1145}11461147class WaitGroup {1148 helper: ChainHelperBase;11491150 constructor(helper: ChainHelperBase) {1151 this.helper = helper;1152 }11531154 sleep(milliseconds: number) {1155 return new Promise((resolve) => setTimeout(resolve, milliseconds));1156 }11571158 private async waitWithTimeout(promise: Promise<any>, timeout: number) {1159 let isBlock = false;1160 promise.then(() => isBlock = true).catch(() => isBlock = true);1161 let totalTime = 0;1162 const step = 100;1163 while(!isBlock) {1164 await this.sleep(step);1165 totalTime += step;1166 if(totalTime >= timeout) throw Error('Blocks production failed');1167 }1168 return promise;1169 }11701171 /**1172 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1173 * @param promise async operation to race against the timeout1174 * @param timeoutMS time after which to time out1175 * @param timeoutError error message to throw1176 * @returns promise of the same type the operation had1177 */1178 withTimeout<T>(1179 promise: Promise<T>,1180 timeoutMS = 30000,1181 timeoutError = 'The operation has timed out!',1182 ): Promise<T> {1183 const timeout = new Promise<never>((_, reject) => {1184 setTimeout(() => {1185 reject(new Error(timeoutError));1186 }, timeoutMS);1187 });11881189 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1190 }11911192 /**1193 * Wait for specified number of blocks1194 * @param blocksCount number of blocks to wait1195 * @returns1196 */1197 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1198 timeout = timeout ?? blocksCount * 60_000;1199 // eslint-disable-next-line no-async-promise-executor1200 const promise = new Promise<void>(async (resolve) => {1201 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1202 if(blocksCount > 0) {1203 blocksCount--;1204 } else {1205 unsubscribe();1206 resolve();1207 }1208 });1209 });1210 await this.waitWithTimeout(promise, timeout);1211 return promise;1212 }12131214 /**1215 * Wait for the specified number of sessions to pass.1216 * Only applicable if the Session pallet is turned on.1217 * @param sessionCount number of sessions to wait1218 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1219 * @returns1220 */1221 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1222 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.`1223 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');12241225 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1226 let currentSessionIndex = -1;12271228 while(currentSessionIndex < expectedSessionIndex) {1229 // eslint-disable-next-line no-async-promise-executor1230 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1231 await this.newBlocks(1);1232 const res = await (this.helper as DevUniqueHelper).session.getIndex();1233 resolve(res);1234 }), blockTimeout, 'The chain has stopped producing blocks!');1235 }1236 }12371238 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1239 timeout = timeout ?? 30 * 60 * 1000;1240 // eslint-disable-next-line no-async-promise-executor1241 const promise = new Promise<void>(async (resolve) => {1242 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1243 if(data.number.toNumber() >= blockNumber) {1244 unsubscribe();1245 resolve();1246 }1247 });1248 });1249 await this.waitWithTimeout(promise, timeout);1250 return promise;1251 }12521253 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1254 timeout = timeout ?? 30 * 60 * 1000;1255 // eslint-disable-next-line no-async-promise-executor1256 const promise = new Promise<void>(async (resolve) => {1257 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1258 if(data.value.relayParentNumber.toNumber() >= blockNumber) {1259 // @ts-ignore1260 unsubscribe();1261 resolve();1262 }1263 });1264 });1265 await this.waitWithTimeout(promise, timeout);1266 return promise;1267 }12681269 noScheduledTasks() {1270 const api = this.helper.getApi();12711272 // eslint-disable-next-line no-async-promise-executor1273 const promise = new Promise<void>(async resolve => {1274 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1275 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();12761277 if(areThereScheduledTasks.length == 0) {1278 unsubscribe();1279 resolve();1280 }1281 });1282 });12831284 return promise;1285 }12861287 parachainBlockMultiplesOf(val: bigint) {1288 // eslint-disable-next-line no-async-promise-executor1289 const promise = new Promise<void>(async resolve => {1290 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1291 if(data.number.toBigInt() % val == 0n) {1292 console.log(`from waiter: ${data.number.toBigInt()}`);1293 unsubscribe();1294 resolve();1295 }1296 });1297 });1298 return promise;1299 }13001301 event<T extends IEventHelper>(1302 maxBlocksToWait: number,1303 eventHelper: T,1304 filter: (_: any) => boolean = () => true,1305 ): any {1306 // eslint-disable-next-line no-async-promise-executor1307 const promise = new Promise<T | null>(async (resolve) => {1308 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1309 const blockNumber = header.number.toHuman();1310 const blockHash = header.hash;1311 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1312 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;13131314 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);13151316 const apiAt = await this.helper.getApi().at(blockHash);1317 const eventRecords = (await apiAt.query.system.events()) as any;13181319 const neededEvent = eventRecords.toArray()1320 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1321 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1322 .find(filter);13231324 if(neededEvent) {1325 unsubscribe();1326 resolve(neededEvent);1327 } else if(maxBlocksToWait > 0) {1328 maxBlocksToWait--;1329 } else {1330 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1331 The wait lasted until block ${blockNumber} inclusive`);1332 unsubscribe();1333 resolve(null);1334 }1335 });1336 });1337 return promise;1338 }13391340 async expectEvent<T extends IEventHelper>(1341 maxBlocksToWait: number,1342 eventHelper: T,1343 filter: (e: any) => boolean = () => true,1344 ) {1345 const e = await this.event(maxBlocksToWait, eventHelper, filter);1346 if(e == null) {1347 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1348 } else {1349 return e;1350 }1351 }1352}13531354class SessionGroup {1355 helper: ChainHelperBase;13561357 constructor(helper: ChainHelperBase) {1358 this.helper = helper;1359 }13601361 //todo:collator documentation1362 async getIndex(): Promise<number> {1363 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1364 }13651366 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1367 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1368 }13691370 setOwnKeys(signer: TSigner, key: string) {1371 return this.helper.executeExtrinsic(1372 signer,1373 'api.tx.session.setKeys',1374 [key, '0x0'],1375 true,1376 );1377 }13781379 setOwnKeysFromAddress(signer: TSigner) {1380 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1381 }1382}13831384class TestUtilGroup {1385 helper: DevUniqueHelper;13861387 constructor(helper: DevUniqueHelper) {1388 this.helper = helper;1389 }13901391 async enable() {1392 if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1393 return;1394 }13951396 const signer = this.helper.util.fromSeed('//Alice');1397 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1398 }13991400 async setTestValue(signer: TSigner, testVal: number) {1401 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1402 }14031404 async incTestValue(signer: TSigner) {1405 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1406 }14071408 async setTestValueAndRollback(signer: TSigner, testVal: number) {1409 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1410 }14111412 async testValue(blockIdx?: number) {1413 const api = blockIdx1414 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1415 : this.helper.getApi();14161417 return (await api.query.testUtils.testValue()).toJSON();1418 }14191420 async justTakeFee(signer: TSigner) {1421 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1422 }14231424 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1425 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1426 }1427}14281429class EventCapture {1430 helper: DevUniqueHelper;1431 eventSection: string;1432 eventMethod: string;1433 events: EventRecord[] = [];1434 unsubscribe: VoidFn | null = null;14351436 constructor(1437 helper: DevUniqueHelper,1438 eventSection: string,1439 eventMethod: string,1440 ) {1441 this.helper = helper;1442 this.eventSection = eventSection;1443 this.eventMethod = eventMethod;1444 }14451446 async startCapture() {1447 this.stopCapture();1448 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1449 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);14501451 this.events.push(...newEvents);1452 })) as any;1453 }14541455 stopCapture() {1456 if(this.unsubscribe !== null) {1457 this.unsubscribe();1458 }1459 }14601461 extractCapturedEvents() {1462 return this.events;1463 }1464}14651466class AdminGroup {1467 helper: UniqueHelper;14681469 constructor(helper: UniqueHelper) {1470 this.helper = helper;1471 }14721473 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1474 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1475 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1476 staker: e.event.data[0].toString(),1477 stake: e.event.data[1].toBigInt(),1478 payout: e.event.data[2].toBigInt(),1479 }));1480 }1481}14821483// eslint-disable-next-line @typescript-eslint/naming-convention1484function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1485 return class extends Base {1486 scheduleFn: 'schedule' | 'scheduleAfter';1487 blocksNum: number;1488 options: ISchedulerOptions;14891490 constructor(...args: any[]) {1491 const logger = args[0] as ILogger;1492 const options = args[1] as {1493 scheduleFn: 'schedule' | 'scheduleAfter',1494 blocksNum: number,1495 options: ISchedulerOptions1496 };14971498 super(logger);14991500 this.scheduleFn = options.scheduleFn;1501 this.blocksNum = options.blocksNum;1502 this.options = options.options;1503 }15041505 executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1506 const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);15071508 const mandatorySchedArgs = [1509 this.blocksNum,1510 this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1511 this.options.priority ?? null,1512 scheduledTx,1513 ];15141515 let schedArgs;1516 let scheduleFn;15171518 if(this.options.scheduledId) {1519 schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];15201521 if(this.scheduleFn == 'schedule') {1522 scheduleFn = 'scheduleNamed';1523 } else if(this.scheduleFn == 'scheduleAfter') {1524 scheduleFn = 'scheduleNamedAfter';1525 }1526 } else {1527 schedArgs = mandatorySchedArgs;1528 scheduleFn = this.scheduleFn;1529 }15301531 const extrinsic = 'api.tx.scheduler.' + scheduleFn;15321533 return super.executeExtrinsic(1534 sender,1535 extrinsic as any,1536 schedArgs,1537 expectSuccess,1538 );1539 }1540 };1541}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.tsdiffbeforeafterboth--- a/tests/src/xcm/xcm.types.ts
+++ b/tests/src/xcm/xcm.types.ts
@@ -1,4 +1,6 @@
-import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util';
+import {IKeyringPair} from '@polkadot/types/types';
+import {hexToString} from '@polkadot/util';
+import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '../util';
import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
import config from '../config';
@@ -9,19 +11,39 @@
export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);
export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);
+export const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);
+export const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);
+export const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);
+export const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);
+export const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);
+
+export const relayUrl = config.relayUrl;
+export const statemintUrl = config.statemintUrl;
+export const statemineUrl = config.statemineUrl;
+
export const acalaUrl = config.acalaUrl;
export const moonbeamUrl = config.moonbeamUrl;
export const astarUrl = config.astarUrl;
export const polkadexUrl = config.polkadexUrl;
+export const karuraUrl = config.karuraUrl;
+export const moonriverUrl = config.moonriverUrl;
+export const shidenUrl = config.shidenUrl;
+
export const SAFE_XCM_VERSION = 3;
-export const maxWaitBlocks = 6;
+export const RELAY_DECIMALS = 12;
+export const STATEMINE_DECIMALS = 12;
+export const KARURA_DECIMALS = 12;
+export const SHIDEN_DECIMALS = 18n;
+export const QTZ_DECIMALS = 18n;
export const ASTAR_DECIMALS = 18n;
export const UNQ_DECIMALS = 18n;
+export const maxWaitBlocks = 6;
+
export const uniqueMultilocation = {
parents: 1,
interior: {
@@ -47,14 +69,30 @@
&& event.outcome.isUntrustedReserveLocation);
};
+export const expectDownwardXcmNoPermission = async (helper: DevUniqueHelper) => {
+ // The correct messageHash for downward messages can't be reliably obtained
+ await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.asIncomplete[1].isNoPermission);
+};
+
+export const expectDownwardXcmComplete = async (helper: DevUniqueHelper) => {
+ // The correct messageHash for downward messages can't be reliably obtained
+ await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.isComplete);
+};
+
export const NETWORKS = {
acala: usingAcalaPlaygrounds,
astar: usingAstarPlaygrounds,
polkadex: usingPolkadexPlaygrounds,
moonbeam: usingMoonbeamPlaygrounds,
+ moonriver: usingMoonriverPlaygrounds,
+ karura: usingKaruraPlaygrounds,
+ shiden: usingShidenPlaygrounds,
} as const;
+type NetworkNames = keyof typeof NETWORKS;
+
+type NativeRuntime = 'opal' | 'quartz' | 'unique';
-export function mapToChainId(networkName: keyof typeof NETWORKS) {
+export function mapToChainId(networkName: keyof typeof NETWORKS): number {
switch (networkName) {
case 'acala':
return ACALA_CHAIN;
@@ -64,10 +102,16 @@
return MOONBEAM_CHAIN;
case 'polkadex':
return POLKADEX_CHAIN;
+ case 'moonriver':
+ return MOONRIVER_CHAIN;
+ case 'karura':
+ return KARURA_CHAIN;
+ case 'shiden':
+ return SHIDEN_CHAIN;
}
}
-export function mapToChainUrl(networkName: keyof typeof NETWORKS): string {
+export function mapToChainUrl(networkName: NetworkNames): string {
switch (networkName) {
case 'acala':
return acalaUrl;
@@ -77,9 +121,501 @@
return moonbeamUrl;
case 'polkadex':
return polkadexUrl;
+ case 'moonriver':
+ return moonriverUrl;
+ case 'karura':
+ return karuraUrl;
+ case 'shiden':
+ return shidenUrl;
}
}
-export function getDevPlayground<T extends keyof typeof NETWORKS>(name: T) {
+export function getDevPlayground(name: NetworkNames) {
return NETWORKS[name];
-}
\ No newline at end of file
+}
+
+export const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
+export const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
+export const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
+export const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
+export const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
+
+export class XcmTestHelper {
+ private _balanceUniqueTokenInit: bigint = 0n;
+ private _balanceUniqueTokenMiddle: bigint = 0n;
+ private _balanceUniqueTokenFinal: bigint = 0n;
+ private _unqFees: bigint = 0n;
+ private _nativeRuntime: NativeRuntime;
+
+ constructor(runtime: NativeRuntime) {
+ this._nativeRuntime = runtime;
+ }
+
+ private _getNativeId() {
+ switch (this._nativeRuntime) {
+ case 'opal':
+ // To-Do
+ return 1001;
+ case 'quartz':
+ return QUARTZ_CHAIN;
+ case 'unique':
+ return UNIQUE_CHAIN;
+ }
+ }
+
+ private _isAddress20FormatFor(network: NetworkNames) {
+ switch (network) {
+ case 'moonbeam':
+ case 'moonriver':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private _runtimeVersionedMultilocation() {
+ return {
+ V3: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: this._getNativeId(),
+ },
+ },
+ },
+ };
+ }
+
+ private _uniqueChainMultilocationForRelay() {
+ return {
+ V3: {
+ parents: 0,
+ interior: {
+ X1: {Parachain: this._getNativeId()},
+ },
+ },
+ };
+ }
+
+ async sendUnqTo(
+ networkName: keyof typeof NETWORKS,
+ randomAccount: IKeyringPair,
+ randomAccountOnTargetChain = randomAccount,
+ ) {
+ const networkUrl = mapToChainUrl(networkName);
+ const targetPlayground = getDevPlayground(networkName);
+ await usingPlaygrounds(async (helper) => {
+ this._balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
+ const destination = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: mapToChainId(networkName),
+ },
+ },
+ },
+ };
+
+ const beneficiary = {
+ V2: {
+ parents: 0,
+ interior: {
+ X1: (
+ this._isAddress20FormatFor(networkName) ?
+ {
+ AccountKey20: {
+ network: 'Any',
+ key: randomAccountOnTargetChain.address,
+ },
+ }
+ :
+ {
+ AccountId32: {
+ network: 'Any',
+ id: randomAccountOnTargetChain.addressRaw,
+ },
+ }
+ ),
+ },
+ },
+ };
+
+ const assets = {
+ V2: [
+ {
+ id: {
+ Concrete: {
+ parents: 0,
+ interior: 'Here',
+ },
+ },
+ fun: {
+ Fungible: TRANSFER_AMOUNT,
+ },
+ },
+ ],
+ };
+ const feeAssetItem = 0;
+
+ await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
+ const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ this._balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
+
+ this._unqFees = this._balanceUniqueTokenInit - this._balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
+ console.log('[%s -> %s] transaction fees: %s', this._nativeRuntime, networkName, helper.util.bigIntToDecimals(this._unqFees));
+ expect(this._unqFees > 0n, 'Negative fees, looks like nothing was transferred').to.be.true;
+
+ await targetPlayground(networkUrl, async (helper) => {
+ /*
+ Since only the parachain part of the Polkadex
+ infrastructure is launched (without their
+ solochain validators), processing incoming
+ assets will lead to an error.
+ This error indicates that the Polkadex chain
+ received a message from the Unique network,
+ since the hash is being checked to ensure
+ it matches what was sent.
+ */
+ if(networkName == 'polkadex') {
+ await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
+ } else {
+ await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
+ }
+ });
+
+ });
+ }
+
+ async sendUnqBack(
+ networkName: keyof typeof NETWORKS,
+ sudoer: IKeyringPair,
+ randomAccountOnUnq: IKeyringPair,
+ ) {
+ const networkUrl = mapToChainUrl(networkName);
+
+ const targetPlayground = getDevPlayground(networkName);
+ await usingPlaygrounds(async (helper) => {
+
+ const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+ randomAccountOnUnq.addressRaw,
+ {
+ Concrete: {
+ parents: 1,
+ interior: {
+ X1: {Parachain: this._getNativeId()},
+ },
+ },
+ },
+ SENDBACK_AMOUNT,
+ );
+
+ let xcmProgramSent: any;
+
+
+ await targetPlayground(networkUrl, async (helper) => {
+ if('getSudo' in helper) {
+ await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), xcmProgram);
+ xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ } else if('fastDemocracy' in helper) {
+ const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), xcmProgram]);
+ // Needed to bypass the call filter.
+ const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+ await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
+ xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ }
+ });
+
+ await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
+
+ this._balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
+
+ expect(this._balanceUniqueTokenFinal).to.be.equal(this._balanceUniqueTokenInit - this._unqFees - STAYED_ON_TARGET_CHAIN);
+
+ });
+ }
+
+ async sendOnlyOwnedBalance(
+ networkName: keyof typeof NETWORKS,
+ sudoer: IKeyringPair,
+ ) {
+ const networkUrl = mapToChainUrl(networkName);
+ const targetPlayground = getDevPlayground(networkName);
+
+ const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
+
+ await usingPlaygrounds(async (helper) => {
+ const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
+ await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
+ const moreThanTargetChainHas = 2n * targetChainBalance;
+
+ const targetAccount = helper.arrange.createEmptyAccount();
+
+ const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+ targetAccount.addressRaw,
+ {
+ Concrete: {
+ parents: 0,
+ interior: 'Here',
+ },
+ },
+ moreThanTargetChainHas,
+ );
+
+ let maliciousXcmProgramSent: any;
+
+
+ await targetPlayground(networkUrl, async (helper) => {
+ if('getSudo' in helper) {
+ await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgram);
+ maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ } else if('fastDemocracy' in helper) {
+ const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), 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 rejectReserveTransferUNQfrom(networkName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
+ const networkUrl = mapToChainUrl(networkName);
+ const targetPlayground = getDevPlayground(networkName);
+
+ await usingPlaygrounds(async (helper) => {
+ const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
+ const targetAccount = helper.arrange.createEmptyAccount();
+
+ const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+ targetAccount.addressRaw,
+ {
+ Concrete: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: this._getNativeId(),
+ },
+ },
+ },
+ },
+ testAmount,
+ );
+
+ const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+ targetAccount.addressRaw,
+ {
+ Concrete: {
+ parents: 0,
+ interior: 'Here',
+ },
+ },
+ testAmount,
+ );
+
+ let maliciousXcmProgramFullIdSent: any;
+ let maliciousXcmProgramHereIdSent: any;
+ const maxWaitBlocks = 3;
+
+ // Try to trick Unique using full UNQ identification
+ await targetPlayground(networkUrl, async (helper) => {
+ if('getSudo' in helper) {
+ await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), 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', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]);
+ // Needed to bypass the call filter.
+ const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+ await helper.fastDemocracy.executeProposal(`${networkName} 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, this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId);
+ maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ }
+ else if('fastDemocracy' in helper) {
+ const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId]);
+ // Needed to bypass the call filter.
+ const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+ await helper.fastDemocracy.executeProposal(`${networkName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
+
+ maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ }
+ });
+
+ await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
+
+ accountBalance = await helper.balance.getSubstrate(targetAccount.address);
+ expect(accountBalance).to.be.equal(0n);
+ });
+ }
+
+ async rejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
+ const networkUrl = mapToChainUrl(networkName);
+ const targetPlayground = getDevPlayground(networkName);
+ let messageSent: any;
+
+ await usingPlaygrounds(async (helper) => {
+ const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+ helper.arrange.createEmptyAccount().addressRaw,
+ {
+ Concrete: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: mapToChainId(networkName),
+ },
+ },
+ },
+ },
+ TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
+ );
+ await targetPlayground(networkUrl, async (helper) => {
+ if('getSudo' in helper) {
+ await helper.getSudo().xcm.send(sudoerOnTargetChain, this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId);
+ messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ } else if('fastDemocracy' in helper) {
+ const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), 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);
+ });
+ }
+
+ private async _relayXcmTransactSetStorage(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') {
+ // eslint-disable-next-line require-await
+ return await usingPlaygrounds(async (helper) => {
+ const relayForceKV = () => {
+ const random = Math.random();
+ const key = `relay-forced-key (instance: ${random})`;
+ const val = `relay-forced-value (instance: ${random})`;
+ const call = helper.constructApiCall('api.tx.system.setStorage', [[[key, val]]]).method.toHex();
+
+ return {
+ call,
+ key,
+ val,
+ };
+ };
+
+ if(variant == 'plain') {
+ const kv = relayForceKV();
+ return {
+ program: helper.arrange.makeUnpaidSudoTransactProgram({
+ weightMultiplier: 1,
+ call: kv.call,
+ }),
+ kvs: [kv],
+ };
+ } else {
+ const kv0 = relayForceKV();
+ const kv1 = relayForceKV();
+
+ const batchCall = helper.constructApiCall(`api.tx.utility.${variant}`, [[kv0.call, kv1.call]]).method.toHex();
+ return {
+ program: helper.arrange.makeUnpaidSudoTransactProgram({
+ weightMultiplier: 2,
+ call: batchCall,
+ }),
+ kvs: [kv0, kv1],
+ };
+ }
+ });
+ }
+
+ async relayIsPermittedToSetStorage(relaySudoer: IKeyringPair, variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') {
+ const {program, kvs} = await this._relayXcmTransactSetStorage(variant);
+
+ await usingRelayPlaygrounds(relayUrl, async (helper) => {
+ await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [
+ this._uniqueChainMultilocationForRelay(),
+ program,
+ ]);
+ });
+
+ await usingPlaygrounds(async (helper) => {
+ await expectDownwardXcmComplete(helper);
+
+ for(const kv of kvs) {
+ const forcedValue = await helper.callRpc('api.rpc.state.getStorage', [kv.key]);
+ expect(hexToString(forcedValue.toHex())).to.be.equal(kv.val);
+ }
+ });
+ }
+
+ private async _relayXcmTransactSetBalance(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs') {
+ // eslint-disable-next-line require-await
+ return await usingPlaygrounds(async (helper) => {
+ const emptyAccount = helper.arrange.createEmptyAccount().address;
+
+ const forceSetBalanceCall = helper.constructApiCall('api.tx.balances.forceSetBalance', [emptyAccount, 10_000n]).method.toHex();
+
+ let call;
+
+ if(variant == 'plain') {
+ call = forceSetBalanceCall;
+
+ } else if(variant == 'dispatchAs') {
+ call = helper.constructApiCall('api.tx.utility.dispatchAs', [
+ {
+ system: 'Root',
+ },
+ forceSetBalanceCall,
+ ]).method.toHex();
+ } else {
+ call = helper.constructApiCall(`api.tx.utility.${variant}`, [[forceSetBalanceCall]]).method.toHex();
+ }
+
+ return {
+ program: helper.arrange.makeUnpaidSudoTransactProgram({
+ weightMultiplier: 1,
+ call,
+ }),
+ emptyAccount,
+ };
+ });
+ }
+
+ async relayIsNotPermittedToSetBalance(
+ relaySudoer: IKeyringPair,
+ variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs',
+ ) {
+ const {program, emptyAccount} = await this._relayXcmTransactSetBalance(variant);
+
+ await usingRelayPlaygrounds(relayUrl, async (helper) => {
+ await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [
+ this._uniqueChainMultilocationForRelay(),
+ program,
+ ]);
+ });
+
+ await usingPlaygrounds(async (helper) => {
+ await expectDownwardXcmNoPermission(helper);
+ expect(await helper.balance.getSubstrate(emptyAccount)).to.be.equal(0n);
+ });
+ }
+}
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),
);