From 6a65032c6ebfd2b2768ca5cb8a75e7013fbecec8 Mon Sep 17 00:00:00 2001 From: Daniel Shiposha Date: Wed, 18 Dec 2024 00:29:57 +0100 Subject: [PATCH] feat(tests): add relay and assethub xcm tests --- js-packages/playgrounds/unique.ts | 27 ++ js-packages/scripts/createHrmp.ts | 2 + js-packages/test-utils/index.ts | 22 +- .../tests/sub/governance/council.test.ts | 20 +- .../tests/sub/governance/democracy.test.ts | 2 +- .../tests/sub/governance/electsudo.test.ts | 2 +- .../tests/sub/governance/fellowship.test.ts | 14 +- .../sub/governance/financialCouncil.test.ts | 12 +- .../sub/governance/technicalCommittee.test.ts | 28 +- js-packages/tests/xcm/quartz.test.ts | 241 +++++++++++- js-packages/tests/xcm/unique.test.ts | 239 +++++++++++- js-packages/tests/xcm/xcm.types.ts | 368 +++++++++++++----- 12 files changed, 812 insertions(+), 165 deletions(-) diff --git a/js-packages/playgrounds/unique.ts b/js-packages/playgrounds/unique.ts index e000ead897..dc3a5aa633 100644 --- a/js-packages/playgrounds/unique.ts +++ b/js-packages/playgrounds/unique.ts @@ -38,6 +38,7 @@ import type { TSubstrateAccount, TNetworks, IEthCrossAccountId, + IPhasicEvent, } from './types.js'; import type {RuntimeDispatchInfo} from '@polkadot/types/interfaces'; @@ -368,6 +369,32 @@ class UniqueEventHelper { return parsedEvents; } + + public static extractPhasicEvents(events: { event: any, phase: any }[]): IPhasicEvent[] { + const parsedEvents: IPhasicEvent[] = []; + + events.forEach((record) => { + const {event, phase} = record; + + const eventData: IEvent = { + section: event.section.toString(), + method: event.method.toString(), + index: this.extractIndex(event.index), + + // assigned without any transformation for compatibility with the `ITransactionResult` events + data: event.data, + + phase: phase.toJSON(), + }; + + parsedEvents.push({ + phase, + event: eventData, + }); + }); + + return parsedEvents; + } } const InvalidTypeSymbol = Symbol('Invalid type'); // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/js-packages/scripts/createHrmp.ts b/js-packages/scripts/createHrmp.ts index ec5ab725d5..0b56dfec33 100644 --- a/js-packages/scripts/createHrmp.ts +++ b/js-packages/scripts/createHrmp.ts @@ -29,6 +29,8 @@ await usingPlaygrounds(async (helper, privateKey) => { default: throw new Error(`unknown hrmp config profile: ${profile}`); } + + await helper.wait.newSessions(1); }, config.relayUrl); // We miss disconnect/unref somewhere. process.exit(0); diff --git a/js-packages/test-utils/index.ts b/js-packages/test-utils/index.ts index 85f39e7193..fa913637eb 100644 --- a/js-packages/test-utils/index.ts +++ b/js-packages/test-utils/index.ts @@ -13,7 +13,7 @@ import {ApiPromise, Keyring, WsProvider} from '@polkadot/api'; import * as defs from '@unique-nft/opal-testnet-types/definitions.js'; import type {IKeyringPair} from '@polkadot/types/types'; import type {EventRecord} from '@polkadot/types/interfaces'; -import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js'; +import type {ICrossAccountId, ILogger, IPhasicEvent, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js'; import type {FrameSystemEventRecord, XcmV2TraitsError, StagingXcmV4TraitsOutcome} from '@polkadot/types/lookup'; import type {SignerOptions, VoidFn} from '@polkadot/api/types'; import {spawnSync} from 'child_process'; @@ -102,13 +102,13 @@ function EventHelper(section: string, method: string, wrapEvent: (data: any[]) = .map(e => this.wrapEvent(e.event.data)); } - find(txres: ITransactionResult) { - const e = txres.result.events.find(e => e.event.section === section && e.event.method === method); + find(events: IPhasicEvent[]) { + const e = events.find(e => e.event.section === section && e.event.method === method); return e ? this.wrapEvent(e.event.data) : null; } - expect(txres: ITransactionResult) { - const e = this.find(txres); + expect(events: IPhasicEvent[]) { + const e = this.find(events); if(e) { return e; } else { @@ -274,6 +274,18 @@ export class Event { })); }; + static XcmPallet = class extends EventSection('xcmPallet') { + static Sent = this.Method('Sent', data => ({ + messageHash: eventJsonData(data, 3), + })); + }; + + static PolkadotXcm = class extends EventSection('polkadotXcm') { + static Sent = this.Method('Sent', data => ({ + messageHash: eventJsonData(data, 3), + })); + }; + static MessageQueue = class extends EventSection('messageQueue') { static Processed = this.Method('Processed', data => ({ messageHash: eventJsonData(data, 0), diff --git a/js-packages/tests/sub/governance/council.test.ts b/js-packages/tests/sub/governance/council.test.ts index d963370276..e3557d0cee 100644 --- a/js-packages/tests/sub/governance/council.test.ts +++ b/js-packages/tests/sub/governance/council.test.ts @@ -39,7 +39,7 @@ describeGov('Governance: Council tests', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.Council.Proposed.expect(proposeResult); + const councilProposedEvent = Event.Council.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; @@ -61,7 +61,7 @@ describeGov('Governance: Council tests', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.Council.Proposed.expect(proposeResult); + const councilProposedEvent = Event.Council.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; @@ -92,7 +92,7 @@ describeGov('Governance: Council tests', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.Council.Proposed.expect(proposeResult); + const councilProposedEvent = Event.Council.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; @@ -145,7 +145,7 @@ describeGov('Governance: Council tests', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.Council.Proposed.expect(proposeResult); + const councilProposedEvent = Event.Council.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; @@ -153,7 +153,7 @@ describeGov('Governance: Council tests', () => { await helper.wait.newBlocks(councilMotionDuration); const closeResult = await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex); - const closeEvent = Event.Council.Closed.expect(closeResult); + const closeEvent = Event.Council.Closed.expect(closeResult.result.events); const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON() as string[]; expect(closeEvent.yes).to.be.equal(members.length); }); @@ -396,7 +396,7 @@ describeGov('Governance: Council tests', () => { itSub('[Negative] Council cannot cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; await expect(proposalFromAllCouncil(helper.democracy.cancelProposalCall(proposalIndex))) .to.be.rejectedWith(/BadOrigin/); @@ -405,7 +405,7 @@ describeGov('Governance: Council tests', () => { itSub('[Negative] Council member cannot cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; await expect(helper.council.collective.execute( counselors.alex, @@ -445,7 +445,7 @@ describeGov('Governance: Council tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await expect(proposalFromAllCouncil(helper.fellowship.referenda.cancelCall(referendumIndex))) .to.be.rejectedWith(/BadOrigin/); @@ -462,7 +462,7 @@ describeGov('Governance: Council tests', () => { proposal, defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await expect(helper.council.collective.execute( counselors.alex, helper.fellowship.referenda.cancelCall(referendumIndex), @@ -478,7 +478,7 @@ describeGov('Governance: Council tests', () => { councilSize, ); - const councilProposedEvent = Event.Council.Proposed.expect(proposeResult); + const councilProposedEvent = Event.Council.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; diff --git a/js-packages/tests/sub/governance/democracy.test.ts b/js-packages/tests/sub/governance/democracy.test.ts index 37f91a6be9..fa79b3b404 100644 --- a/js-packages/tests/sub/governance/democracy.test.ts +++ b/js-packages/tests/sub/governance/democracy.test.ts @@ -35,7 +35,7 @@ describeGov('Governance: Democracy tests', () => { {After: 0}, ); - const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await voteUnanimouslyInFellowship(helper, fellows, democracyTrackMinRank, fellowshipReferendumIndex); await helper.fellowship.referenda.placeDecisionDeposit(donor, fellowshipReferendumIndex); diff --git a/js-packages/tests/sub/governance/electsudo.test.ts b/js-packages/tests/sub/governance/electsudo.test.ts index dd5566c941..9983f7a82e 100644 --- a/js-packages/tests/sub/governance/electsudo.test.ts +++ b/js-packages/tests/sub/governance/electsudo.test.ts @@ -62,7 +62,7 @@ describeGov('Governance: Elect Sudo', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.Council.Proposed.expect(proposeResult); + const councilProposedEvent = Event.Council.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; diff --git a/js-packages/tests/sub/governance/fellowship.test.ts b/js-packages/tests/sub/governance/fellowship.test.ts index f44126b528..07c00e643e 100644 --- a/js-packages/tests/sub/governance/fellowship.test.ts +++ b/js-packages/tests/sub/governance/fellowship.test.ts @@ -30,7 +30,7 @@ describeGov('Governance: Fellowship tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await voteUnanimouslyInFellowship(helper, members, democracyTrackMinRank, referendumIndex); await helper.fellowship.referenda.placeDecisionDeposit(donor, referendumIndex); @@ -85,7 +85,7 @@ describeGov('Governance: Fellowship tests', () => { defaultEnactmentMoment, ); - const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await voteUnanimouslyInFellowship(helper, members, democracyTrackMinRank, fellowshipReferendumIndex); await helper.fellowship.referenda.placeDecisionDeposit(donor, fellowshipReferendumIndex); @@ -116,7 +116,7 @@ describeGov('Governance: Fellowship tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; const referendumInfo = await helper.fellowship.referenda.referendumInfo(referendumIndex); expect(referendumInfo.ongoing.track, `${memberIdx}-th member of rank #${rank}: proposal #${referendumIndex} is on invalid track`) .to.be.equal(democracyTrackId); @@ -134,7 +134,7 @@ describeGov('Governance: Fellowship tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; let expectedAyes = 0; for(let rank = democracyTrackMinRank; rank < fellowshipRankLimit; rank++) { @@ -171,7 +171,7 @@ describeGov('Governance: Fellowship tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; for(let rank = democracyTrackMinRank; rank < fellowshipRankLimit; rank++) { const rankMembers = members[rank]; @@ -253,7 +253,7 @@ describeGov('Governance: Fellowship tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; for(let rank = 0; rank < democracyTrackMinRank; rank++) { for(const member of members[rank]) { @@ -304,7 +304,7 @@ describeGov('Governance: Fellowship tests', () => { itSub('[Negative] FellowshipProposition cannot cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; await testBadFellowshipProposal(helper, helper.democracy.cancelProposalCall(proposalIndex)); }); diff --git a/js-packages/tests/sub/governance/financialCouncil.test.ts b/js-packages/tests/sub/governance/financialCouncil.test.ts index 549f958535..a2afecbb0d 100644 --- a/js-packages/tests/sub/governance/financialCouncil.test.ts +++ b/js-packages/tests/sub/governance/financialCouncil.test.ts @@ -36,7 +36,7 @@ describeGov('Governance: Financial Council tests', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult); + const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; @@ -56,7 +56,7 @@ describeGov('Governance: Financial Council tests', () => { moreThanHalfCouncilThreshold, ); - const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult); + const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult.result.events); const proposalIndex = councilProposedEvent.proposalIndex; const proposalHash = councilProposedEvent.proposalHash; @@ -118,7 +118,7 @@ describeGov('Governance: Financial Council tests', () => { itSub('[Negative] FinCouncil can\'t cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; await expect(proposalFromAllFinCouncil(helper.democracy.cancelProposalCall(proposalIndex))) .rejectedWith('BadOrigin'); @@ -126,7 +126,7 @@ describeGov('Governance: Financial Council tests', () => { itSub('[Negative] FinCouncil member cannot cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; await expect(helper.finCouncil.collective.execute( finCounselors.andy, @@ -201,7 +201,7 @@ describeGov('Governance: Financial Council tests', () => { proposal, defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await expect(proposalFromAllFinCouncil(helper.fellowship.referenda.cancelCall(referendumIndex))) .rejectedWith('BadOrigin'); }); @@ -218,7 +218,7 @@ describeGov('Governance: Financial Council tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await expect(helper.finCouncil.collective.execute( finCounselors.andy, diff --git a/js-packages/tests/sub/governance/technicalCommittee.test.ts b/js-packages/tests/sub/governance/technicalCommittee.test.ts index 2dea4ef70e..02d1952438 100644 --- a/js-packages/tests/sub/governance/technicalCommittee.test.ts +++ b/js-packages/tests/sub/governance/technicalCommittee.test.ts @@ -46,7 +46,7 @@ describeGov('Governance: Technical Committee tests', () => { allTechCommitteeThreshold, ); - const commiteeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult); + const commiteeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult.result.events); const proposalIndex = commiteeProposedEvent.proposalIndex; const proposalHash = commiteeProposedEvent.proposalHash; @@ -56,9 +56,9 @@ describeGov('Governance: Technical Committee tests', () => { await helper.technicalCommittee.collective.vote(techcomms.greg, proposalHash, proposalIndex, true); const closeResult = await helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex); - Event.TechnicalCommittee.Closed.expect(closeResult); - Event.TechnicalCommittee.Approved.expect(closeResult); - const {result} = Event.TechnicalCommittee.Executed.expect(closeResult); + Event.TechnicalCommittee.Closed.expect(closeResult.result.events); + Event.TechnicalCommittee.Approved.expect(closeResult.result.events); + const {result} = Event.TechnicalCommittee.Executed.expect(closeResult.result.events); expect(result).to.eq('Ok'); return closeResult; @@ -72,15 +72,15 @@ describeGov('Governance: Technical Committee tests', () => { await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); const fastTrackProposal = await proposalFromAllCommittee(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)); - Event.Democracy.Started.expect(fastTrackProposal); + Event.Democracy.Started.expect(fastTrackProposal.result.events); }); itSub('TechComm can cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; const cancelProposal = await proposalFromAllCommittee(helper.democracy.cancelProposalCall(proposalIndex)); - Event.Democracy.ProposalCanceled.expect(cancelProposal); + Event.Democracy.ProposalCanceled.expect(cancelProposal.result.events); }); itSub('TechComm can cancel ongoing Democracy referendums', async ({helper}) => { @@ -89,7 +89,7 @@ describeGov('Governance: Technical Committee tests', () => { const referendumIndex = startedEvent.referendumIndex; const emergencyCancelProposal = await proposalFromAllCommittee(helper.democracy.emergencyCancelCall(referendumIndex)); - Event.Democracy.Cancelled.expect(emergencyCancelProposal); + Event.Democracy.Cancelled.expect(emergencyCancelProposal.result.events); }); itSub('TechComm member can veto Democracy proposals', async ({helper}) => { @@ -100,7 +100,7 @@ describeGov('Governance: Technical Committee tests', () => { techcomms.andy, helper.democracy.vetoExternalCall(preimageHash), ); - Event.Democracy.Vetoed.expect(vetoExternalCall); + Event.Democracy.Vetoed.expect(vetoExternalCall.result.events); }); itSub('TechComm can cancel Fellowship referendums', async ({helper}) => { @@ -114,9 +114,9 @@ describeGov('Governance: Technical Committee tests', () => { proposal, defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; const cancelProposal = await proposalFromAllCommittee(helper.fellowship.referenda.cancelCall(referendumIndex)); - Event.FellowshipReferenda.Cancelled.expect(cancelProposal); + Event.FellowshipReferenda.Cancelled.expect(cancelProposal.result.events); }); itSub('TechComm member can add a Fellowship member', async ({helper}) => { @@ -369,7 +369,7 @@ describeGov('Governance: Technical Committee tests', () => { itSub('[Negative] TechComm member cannot cancel Democracy proposals', async ({helper}) => { const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult.result.events).proposalIndex; await expect(helper.technicalCommittee.collective.execute( techcomms.andy, @@ -422,7 +422,7 @@ describeGov('Governance: Technical Committee tests', () => { defaultEnactmentMoment, ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult.result.events).referendumIndex; await expect(helper.technicalCommittee.collective.execute( techcomms.andy, @@ -439,7 +439,7 @@ describeGov('Governance: Technical Committee tests', () => { committeeSize, ); - const committeeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult); + const committeeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult.result.events); const proposalIndex = committeeProposedEvent.proposalIndex; const proposalHash = committeeProposedEvent.proposalHash; diff --git a/js-packages/tests/xcm/quartz.test.ts b/js-packages/tests/xcm/quartz.test.ts index 4bb3e556d4..7b846d0fbc 100644 --- a/js-packages/tests/xcm/quartz.test.ts +++ b/js-packages/tests/xcm/quartz.test.ts @@ -15,12 +15,229 @@ // along with Unique Network. If not, see . import type {IKeyringPair} from '@polkadot/types/types'; -import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingShidenPlaygrounds, usingMoonriverPlaygrounds} from '@unique/test-utils/util.js'; -import {QUARTZ_CHAIN, SAFE_XCM_VERSION, XcmTestHelper, SENDER_BUDGET, SENDTO_AMOUNT, SENDBACK_AMOUNT, SHIDEN_DECIMALS, UNQ_DECIMALS} from './xcm.types.js'; +import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingShidenPlaygrounds, usingMoonriverPlaygrounds, usingRelayPlaygrounds, usingKusamaAssetHubPlaygrounds} from '@unique/test-utils/util.js'; +import {QUARTZ_CHAIN, SAFE_XCM_VERSION, XcmTestHelper, SENDER_BUDGET, SENDTO_AMOUNT, SENDBACK_AMOUNT, SHIDEN_DECIMALS, UNQ_DECIMALS, POLKADOT_ASSETHUB_CHAIN, USDT_ASSET_ID, USDT_DECIMALS, ASSET_HUB_PALLET_ASSETS} from './xcm.types.js'; import {hexToString} from '@polkadot/util'; const testHelper = new XcmTestHelper; +describeXCM('[XCM] Integration test: Exchanging tokens with Relay', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + let dotDerivativeCollectionId: number; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + randomAccount = helper.arrange.createEmptyAccount(); + + const relayLocation = { + parents: 1, + interior: 'Here', + }; + + dotDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(relayLocation); + if(dotDerivativeCollectionId == null) { + const name = 'DOT'; + const tokenPrefix = 'DOT'; + const decimals = 10; + await helper.getSudo().foreignAssets.register(alice, relayLocation, name, tokenPrefix, {Fungible: decimals}); + + dotDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(relayLocation); + } else { + console.log('Relay foreign collection is already registered'); + } + + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingRelayPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + }); + + itSub('Should connect and send DOT to Quartz', async () => { + await testHelper.sendDotFromTo( + 'relay', + 'quartz', + randomAccount, + randomAccount, + SENDTO_AMOUNT, + dotDerivativeCollectionId, + ); + }); + + itSub('Should connect to Quartz and send DOT back', async () => { + await testHelper.sendDotFromTo( + 'quartz', + 'relay', + randomAccount, + randomAccount, + SENDBACK_AMOUNT, + dotDerivativeCollectionId, + ); + }); + + itSub('Should not accept reserve transfer of QTZ from Relay', async () => { + await testHelper.rejectReserveTransferUNQfrom( + alice, + 'relay', + 'quartz', + ); + }); +}); + +describeXCM('[XCM] Integration test: Exchanging tokens with AssetHub', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + let usdtDerivativeCollectionId: number; + + const USDT_NAME = 'USDT'; + const USDT_SYM = 'USDT'; + + let setupAssetHubCall: any; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + randomAccount = helper.arrange.createEmptyAccount(); + + const usdtLocation = { + parents: 1, + interior: { + X3: [ + { + Parachain: POLKADOT_ASSETHUB_CHAIN, + }, + { + PalletInstance: ASSET_HUB_PALLET_ASSETS, + }, + { + GeneralIndex: USDT_ASSET_ID, + }, + ], + }, + }; + + usdtDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(usdtLocation); + if(usdtDerivativeCollectionId == null) { + await helper.getSudo().foreignAssets.register(alice, usdtLocation, USDT_NAME, USDT_SYM, {Fungible: USDT_DECIMALS}); + + usdtDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(usdtLocation); + } else { + console.log('USDT collection is already registered'); + } + + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingKusamaAssetHubPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + const setSafeXcmVersion = helper.constructApiCall(`api.tx.${helper.xcm.palletName}.forceDefaultXcmVersion`, [SAFE_XCM_VERSION]); + + const isSufficient = true; + const minBalance = 10000; + const isFrozen = false; + + const createUsdtCall = helper.constructApiCall( + 'api.tx.assets.forceCreate', [ + USDT_ASSET_ID, + alice.address, + isSufficient, + minBalance, + ], + ); + + const setUsdtMetadata = helper.constructApiCall( + 'api.tx.assets.forceSetMetadata', [ + USDT_ASSET_ID, + USDT_NAME, + USDT_SYM, + USDT_DECIMALS, + isFrozen, + ], + ); + + setupAssetHubCall = helper.constructApiCall('api.tx.utility.batchAll', [[ + setSafeXcmVersion, + createUsdtCall, + setUsdtMetadata, + ]]); + }); + + await usingRelayPlaygrounds(async (helper) => { + await helper.getSudo().xcm.send( + alice, + { + V4: { + parents: 0, + interior: { + X1: [{Parachain: POLKADOT_ASSETHUB_CHAIN}], + }, + }, + }, + { + V4: [ + { + UnpaidExecution: { + weightLimit: 'Unlimited', + }, + }, + { + Transact: { + originKind: 'Superuser', + requireWeightAtMost: { + refTime: 8000000000, + proofSize: 8000, + }, + call: { + encoded: setupAssetHubCall.method.toHex(), + }, + } + } + ], + }, + ); + }); + + await usingKusamaAssetHubPlaygrounds(async (helper) => { + await helper.assets.mint(alice, USDT_ASSET_ID, randomAccount.address, SENDER_BUDGET); + }); + }); + + itSub('Should connect and send USDT to Quartz', async () => { + await testHelper.sendUsdtFromTo( + 'kusamaAssetHub', + 'quartz', + randomAccount, + randomAccount, + SENDTO_AMOUNT, + usdtDerivativeCollectionId, + ); + }); + + itSub('Should connect to Quartz and send USDT back', async () => { + await testHelper.sendUsdtFromTo( + 'quartz', + 'kusamaAssetHub', + randomAccount, + randomAccount, + SENDBACK_AMOUNT, + usdtDerivativeCollectionId, + ); + }); +}); + describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => { let alice: IKeyringPair; let randomAccount: IKeyringPair; @@ -28,7 +245,9 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => { before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - [randomAccount] = await helper.arrange.createAccounts([0n], alice); + randomAccount = helper.arrange.createEmptyAccount(); + + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); // Set the default version to wrap the first message to other chains. await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); @@ -47,8 +266,8 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => { const metadata = { name: 'Quartz', symbol: 'QTZ', - decimals: Number(UNQ_DECIMALS), - minimalBalance: 1000000000000000000n, + decimals: UNQ_DECIMALS, + minimalBalance: 1n * 10n ** BigInt(UNQ_DECIMALS), }; const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) => @@ -61,10 +280,6 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Karura', () => { } await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); }); - - await usingPlaygrounds(async (helper) => { - await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); - }); }); itSub('Should connect and send QTZ to Karura', async () => { @@ -113,12 +328,12 @@ describeXCM('[XCM] Integration test: Exchanging QTZ with Moonriver', () => { before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice); + randomAccountQuartz = helper.arrange.createEmptyAccount(); + await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, SENDER_BUDGET); + // Set the default version to wrap the first message to other chains. await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); - - await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, SENDER_BUDGET); }); await usingMoonriverPlaygrounds(async (helper) => { @@ -207,7 +422,7 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Shiden', () => { QTZ_ASSET_ID_ON_SHIDEN, 'Quartz', 'QTZ', - Number(UNQ_DECIMALS), + UNQ_DECIMALS, ); console.log('2. Register asset location on Shiden'); diff --git a/js-packages/tests/xcm/unique.test.ts b/js-packages/tests/xcm/unique.test.ts index 80dda3384e..26932fdd7d 100644 --- a/js-packages/tests/xcm/unique.test.ts +++ b/js-packages/tests/xcm/unique.test.ts @@ -16,13 +16,230 @@ import type {IKeyringPair} from '@polkadot/types/types'; import config from '../config.js'; -import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingHydraDxPlaygrounds} from '@unique/test-utils/util.js'; +import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingHydraDxPlaygrounds, usingRelayPlaygrounds, usingPolkadotAssetHubPlaygrounds} from '@unique/test-utils/util.js'; import {nToBigInt} from '@polkadot/util'; import {hexToString} from '@polkadot/util'; -import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDBACK_AMOUNT, SENDER_BUDGET, SENDTO_AMOUNT, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper} from './xcm.types.js'; +import {ASSET_HUB_PALLET_ASSETS, ASTAR_DECIMALS, POLKADOT_ASSETHUB_CHAIN, SAFE_XCM_VERSION, SENDBACK_AMOUNT, SENDER_BUDGET, SENDTO_AMOUNT, UNIQUE_CHAIN, UNQ_DECIMALS, USDT_ASSET_ID, USDT_DECIMALS, XcmTestHelper} from './xcm.types.js'; const testHelper = new XcmTestHelper; +describeXCM('[XCM] Integration test: Exchanging tokens with Relay', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + let dotDerivativeCollectionId: number; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + randomAccount = helper.arrange.createEmptyAccount(); + + const relayLocation = { + parents: 1, + interior: 'Here', + }; + + dotDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(relayLocation); + if(dotDerivativeCollectionId == null) { + const name = 'DOT'; + const tokenPrefix = 'DOT'; + const decimals = 10; + await helper.getSudo().foreignAssets.register(alice, relayLocation, name, tokenPrefix, {Fungible: decimals}); + + dotDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(relayLocation); + } else { + console.log('Relay foreign collection is already registered'); + } + + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingRelayPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + }); + + itSub('Should connect and send DOT to Unique', async () => { + await testHelper.sendDotFromTo( + 'relay', + 'unique', + randomAccount, + randomAccount, + SENDTO_AMOUNT, + dotDerivativeCollectionId, + ); + }); + + itSub('Should connect to Unique and send DOT back', async () => { + await testHelper.sendDotFromTo( + 'unique', + 'relay', + randomAccount, + randomAccount, + SENDBACK_AMOUNT, + dotDerivativeCollectionId, + ); + }); + + itSub('Should not accept reserve transfer of UNQ from Relay', async () => { + await testHelper.rejectReserveTransferUNQfrom( + alice, + 'relay', + 'unique', + ); + }); +}); + +describeXCM('[XCM] Integration test: Exchanging tokens with AssetHub', () => { + let alice: IKeyringPair; + let randomAccount: IKeyringPair; + let usdtDerivativeCollectionId: number; + + const USDT_NAME = 'USDT'; + const USDT_SYM = 'USDT'; + + let setupAssetHubCall: any; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + alice = await privateKey('//Alice'); + randomAccount = helper.arrange.createEmptyAccount(); + + const usdtLocation = { + parents: 1, + interior: { + X3: [ + { + Parachain: POLKADOT_ASSETHUB_CHAIN, + }, + { + PalletInstance: ASSET_HUB_PALLET_ASSETS, + }, + { + GeneralIndex: USDT_ASSET_ID, + }, + ], + }, + }; + + usdtDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(usdtLocation); + if(usdtDerivativeCollectionId == null) { + await helper.getSudo().foreignAssets.register(alice, usdtLocation, USDT_NAME, USDT_SYM, {Fungible: USDT_DECIMALS}); + + usdtDerivativeCollectionId = await helper.foreignAssets.foreignCollectionId(usdtLocation); + } else { + console.log('USDT collection is already registered'); + } + + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + // Set the default version to wrap the first message to other chains. + await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + }); + + await usingPolkadotAssetHubPlaygrounds(async (helper) => { + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); + + const setSafeXcmVersion = helper.constructApiCall(`api.tx.${helper.xcm.palletName}.forceDefaultXcmVersion`, [SAFE_XCM_VERSION]); + + const isSufficient = true; + const minBalance = 10000; + const isFrozen = false; + + const createUsdtCall = helper.constructApiCall( + 'api.tx.assets.forceCreate', [ + USDT_ASSET_ID, + alice.address, + isSufficient, + minBalance, + ], + ); + + const setUsdtMetadata = helper.constructApiCall( + 'api.tx.assets.forceSetMetadata', [ + USDT_ASSET_ID, + USDT_NAME, + USDT_SYM, + USDT_DECIMALS, + isFrozen, + ], + ); + + setupAssetHubCall = helper.constructApiCall('api.tx.utility.batchAll', [[ + setSafeXcmVersion, + createUsdtCall, + setUsdtMetadata, + ]]); + }); + + await usingRelayPlaygrounds(async (helper) => { + await helper.getSudo().xcm.send( + alice, + { + V4: { + parents: 0, + interior: { + X1: [{Parachain: POLKADOT_ASSETHUB_CHAIN}], + }, + }, + }, + { + V4: [ + { + UnpaidExecution: { + weightLimit: 'Unlimited', + }, + }, + { + Transact: { + originKind: 'Superuser', + requireWeightAtMost: { + refTime: 8000000000, + proofSize: 8000, + }, + call: { + encoded: setupAssetHubCall.method.toHex(), + }, + } + } + ], + }, + ); + }); + + await usingPolkadotAssetHubPlaygrounds(async (helper) => { + await helper.assets.mint(alice, USDT_ASSET_ID, randomAccount.address, SENDER_BUDGET); + }); + }); + + itSub('Should connect and send USDT to Unique', async () => { + await testHelper.sendUsdtFromTo( + 'polkadotAssetHub', + 'unique', + randomAccount, + randomAccount, + SENDTO_AMOUNT, + usdtDerivativeCollectionId, + ); + }); + + itSub('Should connect to Unique and send USDT back', async () => { + await testHelper.sendUsdtFromTo( + 'unique', + 'polkadotAssetHub', + randomAccount, + randomAccount, + SENDBACK_AMOUNT, + usdtDerivativeCollectionId, + ); + }); +}); + describeXCM('[XCM] Integration test: Exchanging tokens with Acala', () => { let alice: IKeyringPair; let randomAccount: IKeyringPair; @@ -35,6 +252,8 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Acala', () => { // Set the default version to wrap the first message to other chains. await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); + + await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); }); await usingAcalaPlaygrounds(async (helper) => { @@ -50,7 +269,7 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Acala', () => { const metadata = { name: 'Unique Network', symbol: 'UNQ', - decimals: Number(UNQ_DECIMALS), + decimals: UNQ_DECIMALS, minimalBalance: 1250_000_000_000_000_000n, }; const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v] : [any, any]) => @@ -63,10 +282,6 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Acala', () => { } await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); }); - - await usingPlaygrounds(async (helper) => { - await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET); - }); }); itSub('Should connect and send UNQ to Acala', async () => { @@ -193,12 +408,12 @@ describeXCM('[XCM] Integration test: Exchanging UNQ with Moonbeam', () => { before(async () => { await usingPlaygrounds(async (helper, privateKey) => { alice = await privateKey('//Alice'); - [randomAccountUnique] = await helper.arrange.createAccounts([0n], alice); + randomAccountUnique = helper.arrange.createEmptyAccount(); + await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET); + // Set the default version to wrap the first message to other chains. await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION); - - await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET); }); await usingMoonbeamPlaygrounds(async (helper) => { @@ -258,7 +473,7 @@ describeXCM('[XCM] Integration test: Exchanging tokens with 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 astarInitialBalance = 1n * 10n ** BigInt(ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar. const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar before(async () => { @@ -287,7 +502,7 @@ describeXCM('[XCM] Integration test: Exchanging tokens with Astar', () => { UNQ_ASSET_ID_ON_ASTAR, 'Unique Network', 'UNQ', - Number(UNQ_DECIMALS), + UNQ_DECIMALS, ); console.log('2. Register asset location on Astar'); diff --git a/js-packages/tests/xcm/xcm.types.ts b/js-packages/tests/xcm/xcm.types.ts index 9c92f5c473..3499f45d32 100644 --- a/js-packages/tests/xcm/xcm.types.ts +++ b/js-packages/tests/xcm/xcm.types.ts @@ -1,11 +1,12 @@ import type {IKeyringPair} from '@polkadot/types/types'; -import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingHydraDxPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '@unique/test-utils/util.js'; +import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingHydraDxPlaygrounds, usingKaruraPlaygrounds, usingKusamaAssetHubPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingPolkadotAssetHubPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '@unique/test-utils/util.js'; import {DevUniqueHelper, Event} from '@unique/test-utils'; import { AcalaHelper, AstarHelper } from '@unique/test-utils/xcm/index.js'; -import { IEvent } from '@unique-nft/playgrounds/types.js'; +import { IEvent, ITransactionResult } from '@unique-nft/playgrounds/types.js'; +import { ChainHelperBase } from '@unique-nft/playgrounds'; export const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037); -export const POLKADOT_ASSETHUB_CHAIN = +(process.env.RELAY_ASSETHUB_URL || 1000); +export const POLKADOT_ASSETHUB_CHAIN = +(process.env.RELAY_ASSETHUB_ID || 1000); export const ACALA_CHAIN = +(process.env.RELAY_ACALA_ID || 2000); export const MOONBEAM_CHAIN = +(process.env.RELAY_MOONBEAM_ID || 2004); export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006); @@ -25,8 +26,8 @@ export const KUSAMA_ASSETHUB_DECIMALS = 12; export const KARURA_DECIMALS = 12; export const SHIDEN_DECIMALS = 18n; -export const ASTAR_DECIMALS = 18n; -export const UNQ_DECIMALS = 18n; +export const ASTAR_DECIMALS = 18; +export const UNQ_DECIMALS = 18; export const maxWaitBlocks = 50; @@ -44,6 +45,8 @@ export const NETWORKS = { unique: usingPlaygrounds, quartz: usingPlaygrounds, relay: usingRelayPlaygrounds, + kusamaAssetHub: usingKusamaAssetHubPlaygrounds, + polkadotAssetHub: usingPolkadotAssetHubPlaygrounds, acala: usingAcalaPlaygrounds, astar: usingAstarPlaygrounds, polkadex: usingPolkadexPlaygrounds, @@ -65,6 +68,9 @@ export function mapToChainId(networkName: keyof typeof NETWORKS): number { return QUARTZ_CHAIN; case 'relay': throw new Error('Relay chain has no para ID'); + case 'kusamaAssetHub': + case 'polkadotAssetHub': + return POLKADOT_ASSETHUB_CHAIN; case 'acala': return ACALA_CHAIN; case 'astar': @@ -93,11 +99,47 @@ export function mapToChainLocation(networkName: keyof typeof NETWORKS) { }; } +export const USDT_ASSET_ID = 1984; +export const USDT_DECIMALS = 6; +export const ASSET_HUB_PALLET_ASSETS = 50; + +export function mapToUsdtLocation(networkName: keyof typeof NETWORKS) { + const basicInteriors = [ + { + PalletInstance: ASSET_HUB_PALLET_ASSETS, + }, + { + GeneralIndex: USDT_ASSET_ID, + }, + ]; + + if (networkName === 'kusamaAssetHub' || networkName === 'polkadotAssetHub') { + return { + parents: 0, + interior: { + X2: basicInteriors, + }, + }; + } else { + return { + parents: 1, + interior: { + X3: [ + { + Parachain: POLKADOT_ASSETHUB_CHAIN, + }, + ...basicInteriors, + ], + }, + }; + } +} + export function getDevPlayground(name: NetworkNames) { return NETWORKS[name]; } -export const TRANSFER_AMOUNT = 2000000n * 10n ** UNQ_DECIMALS; +export const TRANSFER_AMOUNT = 2000000n * 10n ** BigInt(UNQ_DECIMALS); export const SENDER_BUDGET = 2n * TRANSFER_AMOUNT; export const SENDTO_AMOUNT = TRANSFER_AMOUNT; export const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n; @@ -163,8 +205,15 @@ export class XcmTestHelper { const otherChainPlayground = getDevPlayground(sendFrom); return await otherChainPlayground(async (helper) => { - const destination = {V4: mapToChainLocation(sendTo)}; - const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [destination, program]); + const destination = sendFrom === 'relay' + ? { + V4: { + parents: 0, + interior: {X1: [{Parachain: mapToChainId(sendTo)}]}, + }, + } + : {V4: mapToChainLocation(sendTo)}; + const xcmSend = helper.constructApiCall(`api.tx.${helper.xcm.palletName}.send`, [destination, program]); if('getSudo' in helper) { // can't use `getSudo` here because of types issues. @@ -177,7 +226,7 @@ export class XcmTestHelper { 'api.tx.sudo.sudo', [xcmSend], ); - const messageSent = Event.XcmpQueue.XcmpMessageSent.expect(sendResult); + const messageSent = await this.#expectSentEvent(helper, sendFrom, sendTo, sendResult); return messageSent.messageHash; } else if ('fastDemocracy' in helper) { // Needed to bypass the call filter. @@ -214,45 +263,63 @@ export class XcmTestHelper { }); } - async #sendTokens( + async #expectSentEvent( + fromHelper: ChainHelperBase, + from: keyof typeof NETWORKS, + to: keyof typeof NETWORKS, + txResult: ITransactionResult, + ) { + // FIXME + // WORKAROUND: sometimes a part of the events collected directly from the extrinsic + // is lost somehow. Let's query all of them from the block. + const apiAt = await fromHelper.getApi().at(txResult.blockHash); + const eventRecords = (await apiAt.query.system.events()).toArray(); + const events = fromHelper.eventHelper.extractPhasicEvents(eventRecords); + + if (from === 'relay') { + return Event.XcmPallet.Sent.expect(events); + } else if (to === 'relay' || from === 'kusamaAssetHub' || from === 'polkadotAssetHub') { + return Event.PolkadotXcm.Sent.expect(events); + } else { + return Event.XcmpQueue.XcmpMessageSent.expect(events); + } + } + + async #sendTokens({ + from, + to, + fromAccount, + toAccount, + assetId, + amount, + decimals, + getAssetBalanceOnUnique, + setMessageHash, + }: { from: keyof typeof NETWORKS, to: keyof typeof NETWORKS, fromAccount: IKeyringPair, toAccount: IKeyringPair, + assetId: any, amount: bigint, + decimals: number, + getAssetBalanceOnUnique: (helper: DevUniqueHelper) => Promise, setMessageHash: (messageHash: any) => void, - ) { + }) { let isFromUnique = from === 'unique' || from === 'quartz'; - let isToUnique = to === 'unique' || to === 'quartz'; const fromPlayground = getDevPlayground(from); await fromPlayground(async (helper) => { - let assetId: any; - if (isFromUnique) { - assetId = { - parents: 0, - interior: 'here', - }; - } else if (isToUnique) { - assetId = mapToChainLocation(to); - } else { - throw new Error('sendUnqFromTo: either `from` or `to` MUST point to a Unique chain'); - } - const getRandomAccountBalance = async (): Promise => { if (!isFromUnique) { return 0n; } - if ('getSubstrate' in helper.balance) { - return await helper.balance.getSubstrate(fromAccount.address); - } else { - return await helper.balance.getEthereum(fromAccount.address); - } + return await getAssetBalanceOnUnique(helper as DevUniqueHelper); }; - const unqBalanceBefore = await getRandomAccountBalance(); + const balanceBefore = await getRandomAccountBalance(); let beneficiaryAccount: any; if (this._isAddress20FormatFor(to)) { @@ -315,7 +382,9 @@ export class XcmTestHelper { 'Unlimited', ); } else { - const destination = {V4: mapToChainLocation(to)}; + const destination = from === 'relay' + ? {V4: {parents: 0, interior: {X1: [{Parachain: mapToChainId(to)}]}}} + : {V4: mapToChainLocation(to)}; const beneficiary = { V4: { @@ -336,20 +405,20 @@ export class XcmTestHelper { ); } - const messageSent = Event.XcmpQueue.XcmpMessageSent.expect(transferResult); + const messageSent = await this.#expectSentEvent(helper, from, to, transferResult); - const unqBalanceAfter = await getRandomAccountBalance(); + const balanceAfter = await getRandomAccountBalance(); if (isFromUnique) { - const unqBalanceDiff = unqBalanceBefore - unqBalanceAfter; - const unqFees = unqBalanceDiff - amount; - const unqMinFees = 0n; - const unqMaxFees = 2n * 10n ** UNQ_DECIMALS; + const balanceDiff = balanceBefore - balanceAfter; + const fees = balanceDiff - amount; + const minFees = 0n; + const maxFees = 2n * 10n ** BigInt(decimals); - console.log('[%s -> %s] transaction fees: %s UNQ/QTZ', from, to, helper.util.bigIntToDecimals(unqFees)); + console.log('[%s -> %s] transaction fees: %s asset tokens', from, to, helper.util.bigIntToDecimals(fees, decimals)); expect( - unqMinFees < unqFees && unqFees <= unqMaxFees, - `invalid UNQ/QTZ fees when transferring from Unique/Quartz: ${unqFees}` + minFees <= fees && fees <= maxFees, + `invalid asset fees when transferring from ${from}: ${fees}` ).to.be.true; } @@ -357,13 +426,21 @@ export class XcmTestHelper { }); } - async #awaitTokens( + async #awaitTokens({ + from, + to, + amount, + decimals, + getAssetBalanceOnUnique, + getMessageHash, + }: { from: keyof typeof NETWORKS, to: keyof typeof NETWORKS, - getMessageHash: () => any, - account: string, amount: bigint, - ) { + decimals: number, + getAssetBalanceOnUnique: (helper: DevUniqueHelper) => Promise, + getMessageHash: () => any, + }) { const toPlayground = getDevPlayground(to); let isToUnique = to === 'unique' || to === 'quartz'; @@ -373,14 +450,10 @@ export class XcmTestHelper { return 0n; } - if ('getSubstrate' in helper.balance) { - return await helper.balance.getSubstrate(account); - } else { - return await helper.balance.getEthereum(account); - } + return await getAssetBalanceOnUnique(helper as DevUniqueHelper); }; - const unqBalanceBefore = await getRandomAccountBalance(); + const balanceBefore = await getRandomAccountBalance(); const collectedEventData = await this.#collectProcessedMsgsEvents( helper, @@ -397,22 +470,124 @@ export class XcmTestHelper { `no 'MessageQueue.Processed' event was found on ${to}`, ).to.be.true; - const unqBalanceAfter = await getRandomAccountBalance(); + const balanceAfter = await getRandomAccountBalance(); if (isToUnique) { - const unqBalanceDiff = unqBalanceAfter - unqBalanceBefore; - const unqFees = unqBalanceDiff - amount; + const balanceDiff = balanceAfter - balanceBefore; + const fees = balanceDiff - amount; - console.log('[%s -> %s] transaction fees: %s UNQ/QTZ', from, to, helper.util.bigIntToDecimals(unqFees)); + console.log('[%s -> %s] transaction fees: %s asset tokens', from, to, helper.util.bigIntToDecimals(fees, decimals)); expect( - unqFees === 0n, - `invalid UNQ/QTZ fees when receiving to ${to}: ${unqFees}` + fees === 0n, + `invalid asset fees when receiving to ${to}: ${fees}` ).to.be.true; } }); } + async sendDotFromTo( + from: keyof typeof NETWORKS, + to: keyof typeof NETWORKS, + randomAccountOnFrom: IKeyringPair, + randomAccountOnTo: IKeyringPair, + amount: bigint, + dotDerivativeCollectionId: number, + ) { + let messageHash: any = null; + + let isFromUnique = from === 'unique' || from === 'quartz'; + + let assetId: any; + if (isFromUnique) { + assetId = { + parents: 1, + interior: 'here', + }; + } else { + assetId = { + parents: 0, + interior: 'here', + }; + }; + + await Promise.all([ + this.#sendTokens({ + from, + to, + fromAccount: randomAccountOnFrom, + toAccount: randomAccountOnTo, + assetId, + amount, + decimals: UNQ_DECIMALS, + getAssetBalanceOnUnique: async (helper: DevUniqueHelper) => { + return await helper.ft.getBalance( + dotDerivativeCollectionId, + {Substrate: randomAccountOnFrom.address}, + ); + }, + setMessageHash: (hash) => messageHash = hash, + }), + this.#awaitTokens({ + from, + to, + amount, + decimals: UNQ_DECIMALS, + getAssetBalanceOnUnique: async (helper: DevUniqueHelper) => { + return await helper.ft.getBalance( + dotDerivativeCollectionId, + {Substrate: randomAccountOnTo.address}, + ); + }, + getMessageHash: () => messageHash, + }), + ]); + } + + async sendUsdtFromTo( + from: keyof typeof NETWORKS, + to: keyof typeof NETWORKS, + randomAccountOnFrom: IKeyringPair, + randomAccountOnTo: IKeyringPair, + amount: bigint, + usdtDerivativeCollectionId: number, + ) { + let messageHash: any = null; + let assetId = mapToUsdtLocation(from); + + await Promise.all([ + this.#sendTokens({ + from, + to, + fromAccount: randomAccountOnFrom, + toAccount: randomAccountOnTo, + assetId, + amount, + decimals: USDT_DECIMALS, + getAssetBalanceOnUnique: async (helper: DevUniqueHelper) => { + return await helper.ft.getBalance( + usdtDerivativeCollectionId, + {Substrate: randomAccountOnFrom.address}, + ); + }, + setMessageHash: (hash) => messageHash = hash, + }), + this.#awaitTokens({ + from, + to, + amount, + decimals: UNQ_DECIMALS, + getAssetBalanceOnUnique: async (helper: DevUniqueHelper) => { + return await helper.ft.getBalance( + usdtDerivativeCollectionId, + {Substrate: randomAccountOnTo.address}, + ); + }, + getMessageHash: () => messageHash, + }), + ]); + } + async sendUnqFromTo( from: keyof typeof NETWORKS, to: keyof typeof NETWORKS, @@ -422,22 +597,42 @@ export class XcmTestHelper { ) { let messageHash: any = null; + let isFromUnique = from === 'unique' || from === 'quartz'; + + let assetId: any; + if (isFromUnique) { + assetId = { + parents: 0, + interior: 'here', + }; + } else { + assetId = mapToChainLocation(to); + }; + await Promise.all([ - this.#sendTokens( + this.#sendTokens({ from, to, - randomAccountOnFrom, - randomAccountOnTo, + fromAccount: randomAccountOnFrom, + toAccount: randomAccountOnTo, + assetId, amount, - (hash) => messageHash = hash, - ), - this.#awaitTokens( + decimals: UNQ_DECIMALS, + getAssetBalanceOnUnique: async (helper: DevUniqueHelper) => { + return await helper.balance.getSubstrate(randomAccountOnFrom.address); + }, + setMessageHash: (hash) => messageHash = hash, + }), + this.#awaitTokens({ from, to, - () => messageHash, - randomAccountOnTo.address, amount, - ), + decimals: UNQ_DECIMALS, + getAssetBalanceOnUnique: async (helper: DevUniqueHelper) => { + return await helper.balance.getSubstrate(randomAccountOnTo.address); + }, + getMessageHash: () => messageHash, + }), ]); } @@ -446,7 +641,7 @@ export class XcmTestHelper { otherChain: keyof typeof NETWORKS, uniqueChain: UniqueChain, ) { - const otherChainBalance = 10000n * (10n ** UNQ_DECIMALS); + const otherChainBalance = 10000n * 10n ** BigInt(UNQ_DECIMALS); let randomAccount: IKeyringPair; let maliciousXcmProgram: any; @@ -515,13 +710,16 @@ export class XcmTestHelper { await Promise.all([ sendGoodProgram(), - this.#awaitTokens( - otherChain, - uniqueChain, - () => messageHash, - randomAccount!.address, - otherChainBalance, - ) + this.#awaitTokens({ + from: otherChain, + to: uniqueChain, + amount: otherChainBalance, + decimals: UNQ_DECIMALS, + getAssetBalanceOnUnique: async (helper) => { + return await helper.balance.getSubstrate(randomAccount!.address); + }, + getMessageHash: () => messageHash, + }) ]); await usingPlaygrounds(async (helper) => { @@ -535,7 +733,7 @@ export class XcmTestHelper { otherChain: keyof typeof NETWORKS, uniqueChain: UniqueChain, ) { - const testAmount = 10_000n * (10n ** UNQ_DECIMALS); + const testAmount = 10_000n * 10n ** BigInt(UNQ_DECIMALS); let randomAccount: IKeyringPair; let messageHash: any = null; @@ -627,26 +825,4 @@ export class XcmTestHelper { this.#awaitMaliciousProgramRejection(() => messageHash), ]); } - - async registerRelayNativeTokenOnUnique(alice: IKeyringPair) { - return await usingPlaygrounds(async (helper) => { - const relayLocation = { - parents: 1, - interior: 'Here', - }; - - const relayCollectionId = await helper.foreignAssets.foreignCollectionId(relayLocation); - if(relayCollectionId == null) { - const name = 'DOT'; - const tokenPrefix = 'DOT'; - const decimals = 10; - await helper.getSudo().foreignAssets.register(alice, relayLocation, name, tokenPrefix, {Fungible: decimals}); - - return await helper.foreignAssets.foreignCollectionId(relayLocation); - } else { - console.log('Relay foreign collection is already registered'); - return relayCollectionId; - } - }); - } }