1234567891011121314151617import type {IKeyringPair} from '@polkadot/types/types';18import {ApiPromise} from '@polkadot/api';19import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from '@unique/test-utils/util.js';20import {itEth} from '@unique/test-utils/eth/util.js';21import {main as correctState} from '@unique/scripts/correctStateAfterMaintenance.js';22import type {PalletBalancesIdAmount} from '@polkadot/types/lookup';23import type {Vec} from '@polkadot/types-codec';2425async function maintenanceEnabled(api: ApiPromise): Promise<boolean> {26 return (await api.query.maintenance.enabled()).toJSON() as boolean;27}2829describe('Integration Test: Maintenance Functionality', () => {30 let superuser: IKeyringPair;31 let donor: IKeyringPair;32 let bob: IKeyringPair;3334 before(async function() {35 await usingPlaygrounds(async (helper, privateKey) => {36 requirePalletsOrSkip(this, helper, [Pallets.Maintenance]);37 superuser = await privateKey('//Alice');38 donor = await privateKey({url: import.meta.url});39 [bob] = await helper.arrange.createAccounts([10000n], donor);4041 });42 });4344 describe('Maintenance Mode', () => {45 before(async function() {46 await usingPlaygrounds(async (helper) => {47 if(await maintenanceEnabled(helper.getApi())) {48 console.warn('\tMaintenance mode was left enabled BEFORE the test suite! Disabling it now.');49 await expect(helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', [])).to.be.fulfilled;50 }51 });52 });5354 itSub('Allows superuser to enable and disable maintenance mode - and disallows anyone else', async ({helper}) => {55 56 await expect(helper.executeExtrinsic(superuser, 'api.tx.maintenance.enable', []), 'on commoner enabling MM')57 .to.be.rejectedWith(/BadOrigin/);5859 60 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);61 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;6263 64 await expect(helper.executeExtrinsic(bob, 'api.tx.maintenance.disable', []), 'on commoner disabling MM')65 .to.be.rejectedWith(/BadOrigin/);6667 68 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);69 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;70 });7172 itSub('MM blocks unique pallet calls', async ({helper}) => {73 74 const nftCollection = await helper.nft.mintCollection(bob, {75 tokenPropertyPermissions: [{76 key: 'test', permission: {77 collectionAdmin: true,78 tokenOwner: true,79 mutable: true,80 },81 }],82 });8384 85 const nft = await nftCollection.mintToken(bob);8687 88 const ftCollection = await helper.ft.mintCollection(superuser);8990 91 await expect(ftCollection.mint(superuser)).to.be.fulfilled;9293 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);94 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;9596 97 await expect(helper.nft.mintCollection(superuser), 'cudo forbidden stuff')98 .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);99100 101 await expect(nft.setProperties(102 bob,103 [{key: 'test', value: 'test-val'}],104 )).to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);105106 107 await expect(nftCollection.mintToken(superuser))108 .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);109110 111 await expect(ftCollection.mint(superuser))112 .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);113114 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);115 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;116117 118 await expect(helper.nft.mintCollection(bob), 'MM is disabled, the collection should be created').to.be.fulfilled;119120 121 await nft.setProperties(bob, [{key: 'test', value: 'test-val'}]);122123 124 await nftCollection.mintToken(bob);125126 127 await ftCollection.mint(superuser);128 });129130 itSub.ifWithPallets('MM blocks unique pallet calls (Re-Fungible)', [Pallets.ReFungible], async ({helper}) => {131 132 const rftCollection = await helper.rft.mintCollection(superuser);133134 135 await expect(rftCollection.mintToken(superuser)).to.be.fulfilled;136137 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);138 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;139140 141 await expect(rftCollection.mintToken(superuser))142 .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);143144 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);145 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;146147 148 await rftCollection.mintToken(superuser);149 });150151 itSub('MM allows native token transfers and RPC calls', async ({helper}) => {152 153 const totalCount = await helper.collection.getTotalCount();154155 156 await expect(helper.balance.transferToSubstrate(superuser, bob.address, 2n)).to.be.fulfilled;157158 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);159 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;160161 162 expect(await helper.collection.getTotalCount()).to.be.deep.equal(totalCount);163164 165 await expect(helper.balance.transferToSubstrate(bob, superuser.address, 1n)).to.be.fulfilled;166167 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);168 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;169170 171 expect(await helper.collection.getTotalCount()).to.be.deep.equal(totalCount);172173 174 await expect(helper.balance.transferToSubstrate(bob, superuser.address, 1n)).to.be.fulfilled;175 });176177 itSched.ifWithPallets('MM blocks scheduled calls and the scheduler itself', [Pallets.UniqueScheduler], async (scheduleKind, {helper}) => {178 const collection = await helper.nft.mintCollection(bob);179180 const nftBeforeMM = await collection.mintToken(bob);181 const nftDuringMM = await collection.mintToken(bob);182 const nftAfterMM = await collection.mintToken(bob);183184 const [185 scheduledIdBeforeMM,186 scheduledIdDuringMM,187 scheduledIdBunkerThroughMM,188 scheduledIdAttemptDuringMM,189 scheduledIdAfterMM,190 ] = scheduleKind == 'named'191 ? helper.arrange.makeScheduledIds(5)192 : new Array(5);193194 const blocksToWait = 6;195196 197 await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdBeforeMM})198 .nft.transferToken(bob, collection.collectionId, nftBeforeMM.tokenId, {Substrate: superuser.address});199200201 await helper.wait.newBlocks(blocksToWait + 1);202 expect(await nftBeforeMM.getOwner()).to.be.deep.equal({Substrate: superuser.address});203204 205 await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdDuringMM})206 .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address});207208209 210 await helper.scheduler.scheduleAfter(blocksToWait * 2, {scheduledId: scheduledIdBunkerThroughMM})211 .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address});212213214 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);215 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;216217 await helper.wait.newBlocks(blocksToWait + 1);218 219 expect(await nftDuringMM.getOwner()).to.be.deep.equal({Substrate: bob.address});220221 222 await expect(helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAttemptDuringMM})223 .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address}))224 .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);225226 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);227 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;228229 230 await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAfterMM})231 .nft.transferToken(bob, collection.collectionId, nftAfterMM.tokenId, {Substrate: superuser.address});232233 await helper.wait.newBlocks(blocksToWait + 1);234235 expect(await nftAfterMM.getOwner()).to.be.deep.equal({Substrate: superuser.address});236 237 expect(await nftDuringMM.getOwner()).to.be.deep.equal({Substrate: superuser.address});238 });239240 itEth('Disallows Ethereum transactions to execute while in maintenance', async ({helper}) => {241 const owner = await helper.eth.createAccountWithBalance(donor);242 const receiver = helper.eth.createAccount();243244 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'A', 'B', 'C', '');245246 247 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);248 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;249250 const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);251 const tokenId = await contract.methods.nextTokenId().call();252 expect(tokenId).to.be.equal('1');253254 await expect(contract.methods.mintWithTokenURI(receiver, 'Test URI').send())255 .to.be.rejectedWith(/Returned error: unknown error/);256257 await expect(contract.methods.ownerOf(tokenId).call()).rejectedWith(/token not found/);258259 260 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);261 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;262 });263264 itSub('Allows to enable and disable MM repeatedly', async ({helper}) => {265 266 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);267 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);268 expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;269270 271 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);272 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);273 expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;274 });275276 afterEach(async () => {277 await usingPlaygrounds(async helper => {278 if(helper.fetchMissingPalletNames([Pallets.Maintenance]).length != 0) return;279 if(await maintenanceEnabled(helper.getApi())) {280 console.warn('\tMaintenance mode was left enabled AFTER a test has finished! Be careful. Disabling it now.');281 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);282 }283 expect(await maintenanceEnabled(helper.getApi()), 'Disastrous! Exited the test suite with maintenance mode on.').to.be.false;284 });285 });286 });287288 describe('Integration Test: Maintenance mode & App Promo', () => {289 let superuser: IKeyringPair;290291 before(async function() {292 await usingPlaygrounds(async (helper, privateKey) => {293 requirePalletsOrSkip(this, helper, [Pallets.Maintenance]);294 superuser = await privateKey('//Alice');295 });296 });297298 describe('Test AppPromo script for check state after Maintenance mode', () => {299 before(async function () {300 await usingPlaygrounds(async (helper) => {301 if(await maintenanceEnabled(helper.getApi())) {302 console.warn('\tMaintenance mode was left enabled BEFORE the test suite! Disabling it now.');303 await expect(helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', [])).to.be.fulfilled;304 }305 });306 });307 itSub('Can find and fix inconsistent state', async ({helper}) => {308 const api = helper.getApi();309310 await helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [api.tx.system.setStorage([311 312 ['0x42b67acb8bd223c60d0c8f621ffefc0ae280fa2db99bd3827aac976de75af95f5153cb1f00942ff401000000',313 '0x04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d000010632d5ec76b0500000000000000'],314 315 ['0x42b67acb8bd223c60d0c8f621ffefc0ae280fa2db99bd3827aac976de75af95f9eb2dcce60f37a2702000000',316 '0x04d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d000010632d5ec76b0500000000000000'],317 318 ['0xc2261276cc9d1f8598ea4b6a74b15c2fb1c0eb12e038e5c7f91e120ed4b7ebf1de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',319 '0x046170707374616b656170707374616b65000020c65abc8ed70a00000000000000'],320 ])]);321322 expect((await api.query.appPromotion.pendingUnstake(1)).toJSON()).to.be.deep.equal([[superuser.address, '0x00000000000000056bc75e2d63100000']]);323 expect((await api.query.appPromotion.pendingUnstake(2)).toJSON()).to.be.deep.equal([[superuser.address, '0x00000000000000056bc75e2d63100000']]);324 expect((await api.query.balances.freezes(superuser.address) as Vec<PalletBalancesIdAmount>)325 .map(lock => ({id: lock.id.toUtf8(), amount: lock.amount.toBigInt()})))326 .to.be.deep.equal([{id: 'appstakeappstake', amount: 200000000000000000000n}]);327 await correctState();328329 expect((await api.query.appPromotion.pendingUnstake(1)).toJSON()).to.be.deep.equal([]);330 expect((await api.query.appPromotion.pendingUnstake(2)).toJSON()).to.be.deep.equal([]);331 expect((await api.query.balances.freezes(superuser.address)).toJSON()).to.be.deep.equal([]);332333 });334335 itSub('(!negative test!) Only works when Maintenance mode is disabled', async({helper}) => {336 await expect(helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', [])).to.be.fulfilled;337 await expect(correctState()).to.be.rejectedWith('The network is still in maintenance mode');338 await expect(helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', [])).to.be.fulfilled;339 });340 });341 });342343 after(async () => {344 await usingPlaygrounds(async(helper) => {345 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);346 });347 });348});