1234567891011121314151617import chai from 'chai';18import chaiAsPromised from 'chai-as-promised';1920import {WsProvider, Keyring} from '@polkadot/api';21import {ApiOptions} from '@polkadot/api/types';22import {IKeyringPair} from '@polkadot/types/types';23import usingApi, {submitTransactionAsync} from '../substrate/substrate-api';24import {getGenericResult, generateKeyringPair, waitEvent, describe_xcm} from '../util/helpers';25import {MultiLocation} from '@polkadot/types/interfaces';26import {blake2AsHex} from '@polkadot/util-crypto';27import waitNewBlocks from '../substrate/wait-new-blocks';28import getBalance from '../substrate/get-balance';2930chai.use(chaiAsPromised);31const expect = chai.expect;3233const UNIQUE_CHAIN = 2037;34const ACALA_CHAIN = 2000;35const MOONBEAM_CHAIN = 2004;3637const ACALA_PORT = 9946;38const MOONBEAM_PORT = 9947;3940const TRANSFER_AMOUNT = 2000000000000000000000000n;4142function parachainApiOptions(port: number): ApiOptions {43 return {44 provider: new WsProvider('ws://127.0.0.1:' + port.toString()),45 };46}4748function acalaOptions(): ApiOptions {49 return parachainApiOptions(ACALA_PORT);50}5152function moonbeamOptions(): ApiOptions {53 return parachainApiOptions(MOONBEAM_PORT);54}5556describe_xcm('Integration test: Exchanging tokens with Acala', () => {57 let alice: IKeyringPair;58 let randomAccount: IKeyringPair;59 60 let balanceUniqueTokenInit: bigint;61 let balanceUniqueTokenMiddle: bigint;62 let balanceUniqueTokenFinal: bigint;63 let balanceAcalaTokenInit: bigint;64 let balanceAcalaTokenMiddle: bigint;65 let balanceAcalaTokenFinal: bigint;66 let balanceUniqueForeignTokenInit: bigint;67 let balanceUniqueForeignTokenMiddle: bigint;68 let balanceUniqueForeignTokenFinal: bigint;69 70 before(async () => {71 await usingApi(async (api, privateKeyWrapper) => {72 alice = privateKeyWrapper('//Alice');73 randomAccount = generateKeyringPair();74 });7576 77 await usingApi(78 async (api) => {79 const destination = {80 V0: {81 X2: [82 'Parent',83 {84 Parachain: UNIQUE_CHAIN,85 },86 ],87 },88 };89 90 const metadata = {91 name: 'UNQ',92 symbol: 'UNQ',93 decimals: 18,94 minimalBalance: 1,95 };96 97 const tx = api.tx.assetRegistry.registerForeignAsset(destination, metadata);98 const sudoTx = api.tx.sudo.sudo(tx as any);99 const events = await submitTransactionAsync(alice, sudoTx);100 const result = getGenericResult(events);101 expect(result.success).to.be.true;102 103 const tx1 = api.tx.balances.transfer(randomAccount.address, 10000000000000n);104 const events1 = await submitTransactionAsync(alice, tx1);105 const result1 = getGenericResult(events1);106 expect(result1.success).to.be.true;107 108 [balanceAcalaTokenInit] = await getBalance(api, [randomAccount.address]);109 {110 const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any;111 balanceUniqueForeignTokenInit = BigInt(free);112 }113 },114 acalaOptions(),115 );116 117 118 await usingApi(async (api) => {119 const tx0 = api.tx.balances.transfer(randomAccount.address, 10n * TRANSFER_AMOUNT);120 const events0 = await submitTransactionAsync(alice, tx0);121 const result0 = getGenericResult(events0);122 expect(result0.success).to.be.true;123 124 [balanceUniqueTokenInit] = await getBalance(api, [randomAccount.address]);125 });126 });127 128 it('Should connect and send UNQ to Acala', async () => {129 130 131 await usingApi(async (api) => {132 133 const destination = {134 V0: {135 X2: [136 'Parent',137 {138 Parachain: ACALA_CHAIN,139 },140 ],141 },142 };143 144 const beneficiary = {145 V0: {146 X1: {147 AccountId32: {148 network: 'Any',149 id: randomAccount.addressRaw,150 },151 },152 },153 };154 155 const assets = {156 V1: [157 {158 id: {159 Concrete: {160 parents: 0,161 interior: 'Here',162 },163 },164 fun: {165 Fungible: TRANSFER_AMOUNT,166 },167 },168 ],169 };170 171 const feeAssetItem = 0;172 173 const weightLimit = {174 Limited: 5000000000,175 };176 177 const tx = api.tx.polkadotXcm.limitedReserveTransferAssets(destination, beneficiary, assets, feeAssetItem, weightLimit);178 const events = await submitTransactionAsync(randomAccount, tx);179 const result = getGenericResult(events);180 expect(result.success).to.be.true;181 182 [balanceUniqueTokenMiddle] = await getBalance(api, [randomAccount.address]);183 184 const unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT;185 console.log('[Unique -> Acala] transaction fees on Unique: %s UNQ', unqFees);186 expect(unqFees > 0n).to.be.true;187 });188 189 190 await usingApi(191 async (api) => {192 await waitNewBlocks(api, 3);193 const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any;194 balanceUniqueForeignTokenMiddle = BigInt(free);195 196 [balanceAcalaTokenMiddle] = await getBalance(api, [randomAccount.address]);197198 const acaFees = balanceAcalaTokenInit - balanceAcalaTokenMiddle;199 const unqIncomeTransfer = balanceUniqueForeignTokenMiddle - balanceUniqueForeignTokenInit;200201 console.log('[Unique -> Acala] transaction fees on Acala: %s ACA', acaFees);202 console.log('[Unique -> Acala] income %s UNQ', unqIncomeTransfer);203 expect(acaFees == 0n).to.be.true;204 expect(unqIncomeTransfer == TRANSFER_AMOUNT).to.be.true;205 },206 acalaOptions(),207 );208 });209 210 it('Should connect to Acala and send UNQ back', async () => {211 212 213 await usingApi(214 async (api) => {215 const destination = {216 V1: {217 parents: 1,218 interior: {219 X2: [220 {Parachain: UNIQUE_CHAIN},221 {222 AccountId32: {223 network: 'Any',224 id: randomAccount.addressRaw,225 },226 },227 ],228 },229 },230 };231 232 const id = {233 ForeignAsset: 0,234 };235236 const destWeight = 50000000;237 238 const tx = api.tx.xTokens.transfer(id, TRANSFER_AMOUNT, destination, destWeight);239 const events = await submitTransactionAsync(randomAccount, tx);240 const result = getGenericResult(events);241 expect(result.success).to.be.true;242 243 [balanceAcalaTokenFinal] = await getBalance(api, [randomAccount.address]);244 {245 const {free} = (await api.query.tokens.accounts(randomAccount.addressRaw, {ForeignAsset: 0})).toJSON() as any;246 balanceUniqueForeignTokenFinal = BigInt(free);247 }248 249 const acaFees = balanceAcalaTokenMiddle - balanceAcalaTokenFinal;250 const unqOutcomeTransfer = balanceUniqueForeignTokenMiddle - balanceUniqueForeignTokenFinal;251252 console.log('[Acala -> Unique] transaction fees on Acala: %s ACA', acaFees);253 console.log('[Acala -> Unique] outcome %s UNQ', unqOutcomeTransfer);254255 expect(acaFees > 0).to.be.true;256 expect(unqOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;257 },258 acalaOptions(),259 );260261 262 await usingApi(async (api) => {263 await waitNewBlocks(api, 3);264 265 [balanceUniqueTokenFinal] = await getBalance(api, [randomAccount.address]);266 const actuallyDelivered = balanceUniqueTokenFinal - balanceUniqueTokenMiddle;267 expect(actuallyDelivered > 0).to.be.true;268269 console.log('[Acala -> Unique] actually delivered %s UNQ', actuallyDelivered);270 271 const unqFees = TRANSFER_AMOUNT - actuallyDelivered;272 console.log('[Acala -> Unique] transaction fees on Unique: %s UNQ', unqFees);273 expect(unqFees == 0n).to.be.true;274 });275 });276277 it('Unique rejects ACA tokens from Acala', async () => {278 279280 await usingApi(async (api) => {281 const destination = {282 V1: {283 parents: 1,284 interior: {285 X2: [286 {Parachain: UNIQUE_CHAIN},287 {288 AccountId32: {289 network: 'Any',290 id: randomAccount.addressRaw,291 },292 },293 ],294 },295 },296 };297298 const id = {299 Token: 'ACA',300 };301302 const destWeight = 50000000;303304 const tx = api.tx.xTokens.transfer(id, 100_000_000_000, destination, destWeight);305 const events = await submitTransactionAsync(alice, tx);306 const result = getGenericResult(events);307 expect(result.success).to.be.true;308 }, acalaOptions());309310 await usingApi(async api => {311 const maxWaitBlocks = 3;312 const xcmpQueueFailEvent = await waitEvent(api, maxWaitBlocks, 'xcmpQueue', 'Fail');313314 expect(315 xcmpQueueFailEvent != null,316 'Only native token is supported when the Foreign-Assets pallet is not connected',317 ).to.be.true;318 });319 });320});321322describe_xcm('Integration test: Exchanging UNQ with Moonbeam', () => {323324 325 let uniqueAlice: IKeyringPair;326 let uniqueAssetLocation;327328 let randomAccountUnique: IKeyringPair;329 let randomAccountMoonbeam: IKeyringPair;330331 332 let assetId: string;333334 const moonbeamKeyring = new Keyring({type: 'ethereum'});335 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';336 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';337 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';338339 const alithAccount = moonbeamKeyring.addFromUri(alithPrivateKey, undefined, 'ethereum');340 const baltatharAccount = moonbeamKeyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');341 const dorothyAccount = moonbeamKeyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');342343 const councilVotingThreshold = 2;344 const technicalCommitteeThreshold = 2;345 const votingPeriod = 3;346 const delayPeriod = 0;347348 const uniqueAssetMetadata = {349 name: 'xcUnique',350 symbol: 'xcUNQ',351 decimals: 18,352 isFrozen: false,353 minimalBalance: 1,354 };355356 let balanceUniqueTokenInit: bigint;357 let balanceUniqueTokenMiddle: bigint;358 let balanceUniqueTokenFinal: bigint;359 let balanceForeignUnqTokenInit: bigint;360 let balanceForeignUnqTokenMiddle: bigint;361 let balanceForeignUnqTokenFinal: bigint;362 let balanceGlmrTokenInit: bigint;363 let balanceGlmrTokenMiddle: bigint;364 let balanceGlmrTokenFinal: bigint;365366 before(async () => {367 await usingApi(async (api, privateKeyWrapper) => {368 uniqueAlice = privateKeyWrapper('//Alice');369 randomAccountUnique = generateKeyringPair();370 randomAccountMoonbeam = generateKeyringPair('ethereum');371372 balanceForeignUnqTokenInit = 0n;373 });374375 await usingApi(376 async (api) => {377378 379 console.log('Sponsoring Dorothy.......');380 const tx0 = api.tx.balances.transfer(dorothyAccount.address, 11_000_000_000_000_000_000n);381 const events0 = await submitTransactionAsync(alithAccount, tx0);382 const result0 = getGenericResult(events0);383 expect(result0.success).to.be.true;384 console.log('Sponsoring Dorothy.......DONE');385 386387 const sourceLocation: MultiLocation = api.createType(388 'MultiLocation',389 {390 parents: 1,391 interior: {X1: {Parachain: UNIQUE_CHAIN}},392 },393 );394395 uniqueAssetLocation = {XCM: sourceLocation};396 const existentialDeposit = 1;397 const isSufficient = true;398 const unitsPerSecond = '1';399 const numAssetsWeightHint = 0;400401 const registerTx = api.tx.assetManager.registerForeignAsset(402 uniqueAssetLocation,403 uniqueAssetMetadata,404 existentialDeposit,405 isSufficient,406 );407 console.log('Encoded proposal for registerAsset is %s', registerTx.method.toHex() || '');408409 const setUnitsTx = api.tx.assetManager.setAssetUnitsPerSecond(410 uniqueAssetLocation,411 unitsPerSecond,412 numAssetsWeightHint,413 );414 console.log('Encoded proposal for setAssetUnitsPerSecond is %s', setUnitsTx.method.toHex() || '');415416 const batchCall = api.tx.utility.batchAll([registerTx, setUnitsTx]);417 console.log('Encoded proposal for batchCall is %s', batchCall.method.toHex() || '');418419 420 console.log('Note motion preimage.......');421 const encodedProposal = batchCall?.method.toHex() || '';422 const proposalHash = blake2AsHex(encodedProposal);423 console.log('Encoded proposal for batch utility after schedule is %s', encodedProposal);424 console.log('Encoded proposal hash for batch utility after schedule is %s', proposalHash);425 console.log('Encoded length %d', encodedProposal.length);426427 const tx1 = api.tx.democracy.notePreimage(encodedProposal);428 const events1 = await submitTransactionAsync(baltatharAccount, tx1);429 const result1 = getGenericResult(events1);430 expect(result1.success).to.be.true;431 console.log('Note motion preimage.......DONE');432 433434 435 console.log('Propose external motion through council.......');436 const externalMotion = api.tx.democracy.externalProposeMajority(proposalHash);437 const tx2 = api.tx.councilCollective.propose(438 councilVotingThreshold,439 externalMotion,440 externalMotion.encodedLength,441 );442 const events2 = await submitTransactionAsync(baltatharAccount, tx2);443 const result2 = getGenericResult(events2);444 expect(result2.success).to.be.true;445446 const encodedMotion = externalMotion?.method.toHex() || '';447 const motionHash = blake2AsHex(encodedMotion);448 console.log('Motion hash is %s', motionHash);449450 const tx3 = api.tx.councilCollective.vote(motionHash, 0, true);451 {452 const events3 = await submitTransactionAsync(dorothyAccount, tx3);453 const result3 = getGenericResult(events3);454 expect(result3.success).to.be.true;455 }456 {457 const events3 = await submitTransactionAsync(baltatharAccount, tx3);458 const result3 = getGenericResult(events3);459 expect(result3.success).to.be.true;460 }461462 const tx4 = api.tx.councilCollective.close(motionHash, 0, 1_000_000_000, externalMotion.encodedLength);463 const events4 = await submitTransactionAsync(dorothyAccount, tx4);464 const result4 = getGenericResult(events4);465 expect(result4.success).to.be.true;466 console.log('Propose external motion through council.......DONE');467 468469 470 console.log('Fast track proposal through technical committee.......');471 const fastTrack = api.tx.democracy.fastTrack(proposalHash, votingPeriod, delayPeriod);472 const tx5 = api.tx.techCommitteeCollective.propose(473 technicalCommitteeThreshold,474 fastTrack,475 fastTrack.encodedLength,476 );477 const events5 = await submitTransactionAsync(alithAccount, tx5);478 const result5 = getGenericResult(events5);479 expect(result5.success).to.be.true;480481 const encodedFastTrack = fastTrack?.method.toHex() || '';482 const fastTrackHash = blake2AsHex(encodedFastTrack);483 console.log('FastTrack hash is %s', fastTrackHash);484485 const proposalIdx = Number(await api.query.techCommitteeCollective.proposalCount()) - 1;486 const tx6 = api.tx.techCommitteeCollective.vote(fastTrackHash, proposalIdx, true);487 {488 const events6 = await submitTransactionAsync(baltatharAccount, tx6);489 const result6 = getGenericResult(events6);490 expect(result6.success).to.be.true;491 }492 {493 const events6 = await submitTransactionAsync(alithAccount, tx6);494 const result6 = getGenericResult(events6);495 expect(result6.success).to.be.true;496 }497498 const tx7 = api.tx.techCommitteeCollective499 .close(fastTrackHash, proposalIdx, 1_000_000_000, fastTrack.encodedLength);500 const events7 = await submitTransactionAsync(baltatharAccount, tx7);501 const result7 = getGenericResult(events7);502 expect(result7.success).to.be.true;503 console.log('Fast track proposal through technical committee.......DONE');504 505506 507 console.log('Referendum voting.......');508 const tx8 = api.tx.democracy.vote(509 0,510 {Standard: {balance: 10_000_000_000_000_000_000n, vote: {aye: true, conviction: 1}}},511 );512 const events8 = await submitTransactionAsync(dorothyAccount, tx8);513 const result8 = getGenericResult(events8);514 expect(result8.success).to.be.true;515 console.log('Referendum voting.......DONE');516 517518 519 console.log('Acquire Unique AssetId Info on Moonbeam.......');520521 522 await waitNewBlocks(api, 5);523524 assetId = (await api.query.assetManager.assetTypeId({525 XCM: sourceLocation,526 })).toString();527528 console.log('UNQ asset ID is %s', assetId);529 console.log('Acquire Unique AssetId Info on Moonbeam.......DONE');530 531532 533 console.log('Sponsoring random Account.......');534 const tx10 = api.tx.balances.transfer(randomAccountMoonbeam.address, 11_000_000_000_000_000_000n);535 const events10 = await submitTransactionAsync(baltatharAccount, tx10);536 const result10 = getGenericResult(events10);537 expect(result10.success).to.be.true;538 console.log('Sponsoring random Account.......DONE');539 540541 [balanceGlmrTokenInit] = await getBalance(api, [randomAccountMoonbeam.address]);542 },543 moonbeamOptions(),544 );545546 await usingApi(async (api) => {547 const tx0 = api.tx.balances.transfer(randomAccountUnique.address, 10n * TRANSFER_AMOUNT);548 const events0 = await submitTransactionAsync(uniqueAlice, tx0);549 const result0 = getGenericResult(events0);550 expect(result0.success).to.be.true;551552 [balanceUniqueTokenInit] = await getBalance(api, [randomAccountUnique.address]);553 });554 });555556 it('Should connect and send UNQ to Moonbeam', async () => {557 await usingApi(async (api) => {558 const currencyId = {559 NativeAssetId: 'Here',560 };561 const dest = {562 V1: {563 parents: 1,564 interior: {565 X2: [566 {Parachain: MOONBEAM_CHAIN},567 {AccountKey20: {network: 'Any', key: randomAccountMoonbeam.address}},568 ],569 },570 },571 };572 const amount = TRANSFER_AMOUNT;573 const destWeight = 850000000;574575 const tx = api.tx.xTokens.transfer(currencyId, amount, dest, destWeight);576 const events = await submitTransactionAsync(randomAccountUnique, tx);577 const result = getGenericResult(events);578 expect(result.success).to.be.true;579580 [balanceUniqueTokenMiddle] = await getBalance(api, [randomAccountUnique.address]);581 expect(balanceUniqueTokenMiddle < balanceUniqueTokenInit).to.be.true;582583 const transactionFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT;584 console.log('[Unique -> Moonbeam] transaction fees on Unique: %s UNQ', transactionFees);585 expect(transactionFees > 0).to.be.true;586 });587588 await usingApi(589 async (api) => {590 await waitNewBlocks(api, 3);591592 [balanceGlmrTokenMiddle] = await getBalance(api, [randomAccountMoonbeam.address]);593594 const glmrFees = balanceGlmrTokenInit - balanceGlmrTokenMiddle;595 console.log('[Unique -> Moonbeam] transaction fees on Moonbeam: %s GLMR', glmrFees);596 expect(glmrFees == 0n).to.be.true;597598 const unqRandomAccountAsset = (599 await api.query.assets.account(assetId, randomAccountMoonbeam.address)600 ).toJSON()! as any;601602 balanceForeignUnqTokenMiddle = BigInt(unqRandomAccountAsset['balance']);603 const unqIncomeTransfer = balanceForeignUnqTokenMiddle - balanceForeignUnqTokenInit;604 console.log('[Unique -> Moonbeam] income %s UNQ', unqIncomeTransfer);605 expect(unqIncomeTransfer == TRANSFER_AMOUNT).to.be.true;606 },607 moonbeamOptions(),608 );609 });610611 it('Should connect to Moonbeam and send UNQ back', async () => {612 await usingApi(613 async (api) => {614 const asset = {615 V1: {616 id: {617 Concrete: {618 parents: 1,619 interior: {620 X1: {Parachain: UNIQUE_CHAIN},621 },622 },623 },624 fun: {625 Fungible: TRANSFER_AMOUNT,626 },627 },628 };629 const destination = {630 V1: {631 parents: 1,632 interior: {633 X2: [634 {Parachain: UNIQUE_CHAIN},635 {AccountId32: {network: 'Any', id: randomAccountUnique.addressRaw}},636 ],637 },638 },639 };640 const destWeight = 50000000;641642 const tx = api.tx.xTokens.transferMultiasset(asset, destination, destWeight);643 const events = await submitTransactionAsync(randomAccountMoonbeam, tx);644 const result = getGenericResult(events);645 expect(result.success).to.be.true;646647 [balanceGlmrTokenFinal] = await getBalance(api, [randomAccountMoonbeam.address]);648649 const glmrFees = balanceGlmrTokenMiddle - balanceGlmrTokenFinal;650 console.log('[Moonbeam -> Unique] transaction fees on Moonbeam: %s GLMR', glmrFees);651 expect(glmrFees > 0).to.be.true;652653 const unqRandomAccountAsset = (654 await api.query.assets.account(assetId, randomAccountMoonbeam.address)655 ).toJSON()! as any;656657 expect(unqRandomAccountAsset).to.be.null;658659 balanceForeignUnqTokenFinal = 0n;660661 const unqOutcomeTransfer = balanceForeignUnqTokenMiddle - balanceForeignUnqTokenFinal;662 console.log('[Unique -> Moonbeam] outcome %s UNQ', unqOutcomeTransfer);663 expect(unqOutcomeTransfer == TRANSFER_AMOUNT).to.be.true;664 },665 moonbeamOptions(),666 );667668 await usingApi(async (api) => {669 await waitNewBlocks(api, 3);670671 [balanceUniqueTokenFinal] = await getBalance(api, [randomAccountUnique.address]);672 const actuallyDelivered = balanceUniqueTokenFinal - balanceUniqueTokenMiddle;673 expect(actuallyDelivered > 0).to.be.true;674675 console.log('[Moonbeam -> Unique] actually delivered %s UNQ', actuallyDelivered);676677 const unqFees = TRANSFER_AMOUNT - actuallyDelivered;678 console.log('[Moonbeam -> Unique] transaction fees on Unique: %s UNQ', unqFees);679 expect(unqFees == 0n).to.be.true;680 });681 });682});