1234567891011121314151617import type {IKeyringPair} from '@polkadot/types/types';18import {expect, itSub, Pallets, usingPlaygrounds} from '../util/index.js';19import {CrossAccountId, UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/src/unique.js';2021describe('Integration Test: Unnesting', () => {22 let alice: IKeyringPair;23 let bob: IKeyringPair;24 let charlie: IKeyringPair;2526 before(async () => {27 await usingPlaygrounds(async (helper, privateKey) => {28 const donor = await privateKey({url: import.meta.url});29 [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 50n, 50n], donor);30 });31 });3233 itSub('NFT: allows the owner to successfully unnest a token', async ({helper}) => {34 const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});35 const targetToken = await collection.mintToken(alice);3637 38 const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());3940 41 await expect(nestedToken.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}), 'while unnesting').to.be.fulfilled;42 expect(await nestedToken.getOwner()).to.be.deep.equal({Substrate: alice.address});4344 45 await nestedToken.nest(alice, targetToken);46 await expect(nestedToken.burnFrom(alice, targetToken.nestingAccount()), 'while burning').to.be.fulfilled;47 await expect(nestedToken.getOwner()).to.be.rejected;48 });4950 itSub('NativeFungible: allows the owner to successfully unnest a token', async ({helper}) => {51 const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});52 const targetToken = await collection.mintToken(alice);5354 const collectionFT = helper.ft.getCollectionObject(0);5556 57 await collectionFT.transfer(alice, targetToken.nestingAccount(), 10n);58 59 await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;6061 expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);62 });6364 itSub('Fungible: allows the owner to successfully unnest a token', async ({helper}) => {65 const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});66 const targetToken = await collection.mintToken(alice);6768 const collectionFT = await helper.ft.mintCollection(alice);6970 71 await collectionFT.mint(alice, 10n, targetToken.nestingAccount());72 await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;73 expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(9n);74 expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);7576 77 await collectionFT.transfer(alice, targetToken.nestingAccount(), 5n);78 await expect(collectionFT.burnTokensFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled;79 expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(4n);80 expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(0n);81 expect(await targetToken.getChildren()).to.be.length(0);82 });8384 itSub.ifWithPallets('ReFungible: allows the owner to successfully unnest a token', [Pallets.ReFungible], async ({helper}) => {85 const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});86 const targetToken = await collection.mintToken(alice);8788 const collectionRFT = await helper.rft.mintCollection(alice);8990 91 const token = await collectionRFT.mintToken(alice, 10n, targetToken.nestingAccount());92 await expect(token.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;93 expect(await token.getBalance({Substrate: alice.address})).to.be.equal(9n);94 expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(1n);9596 97 await token.transfer(alice, targetToken.nestingAccount(), 5n);98 await expect(token.burnFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled;99 expect(await token.getBalance({Substrate: alice.address})).to.be.equal(4n);100 expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(0n);101 expect(await targetToken.getChildren()).to.be.length(0);102 });103104 async function checkNestedAmountState({105 expectedBalance,106 childrenShouldPresent,107 nested,108 targetNft,109 }: {110 expectedBalance: bigint,111 childrenShouldPresent: boolean,112 nested: UniqueFTCollection | UniqueRFToken,113 targetNft: UniqueNFToken,114 }) {115 const balance = await nested.getBalance(targetNft.nestingAccount());116 expect(balance).to.be.equal(expectedBalance);117118 const children = await targetNft.getChildren();119120 if(childrenShouldPresent) {121 expect(children[0]).to.be.deep.equal({122 collectionId: nested.collectionId,123 tokenId: (nested instanceof UniqueFTCollection) ? 0 : nested.tokenId,124 });125 } else {126 expect(children.length).to.be.equal(0);127 }128 }129130 function ownerOrAdminUnnestCases(modes: ('ft' | 'nft' | 'rft')[]): {131 mode: 'ft' | 'nft' | 'rft',132 sender: string,133 op: 'transfer' | 'burn',134 requiredPallets: Pallets[],135 }[] {136 const senders = ['owner', 'admin'];137 const ops = ['transfer', 'burn'];138139 const cases = [];140 for(const mode of modes) {141 const requiredPallets = (mode === 'rft')142 ? [Pallets.ReFungible]143 : [];144145 for(const sender of senders) {146 for(const op of ops) {147 cases.push({148 mode: mode as 'ft' | 'nft' | 'rft',149 sender,150 op: op as 'transfer' | 'burn',151 requiredPallets,152 });153 }154 }155 }156157 return cases;158 }159160 ownerOrAdminUnnestCases(['ft', 'rft']).map(testCase =>161 itSub.ifWithPallets(`[${testCase.mode}]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, testCase.requiredPallets, async({helper}) => {162 const owner = alice;163 const admin = bob;164165 const unnester = (testCase.sender === 'owner')166 ? owner167 : admin;168169 const collectionNFT = await helper.nft.mintCollection(owner);170 await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});171172 const collectionNested = await helper[testCase.mode as 'ft' | 'rft'].mintCollection(owner, {173 limits: {174 ownerCanTransfer: true,175 },176 });177 await collectionNested.addAdmin(owner, {Substrate: admin.address});178179 const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});180181 let nested: UniqueFTCollection | UniqueRFToken;182 const totalAmount = 5n;183 const firstUnnestAmount = 2n;184 const restUnnestAmount = totalAmount - firstUnnestAmount;185186 if(collectionNested instanceof UniqueFTCollection) {187 await collectionNested.mint(owner, totalAmount, {Substrate: charlie.address});188 nested = collectionNested;189 } else {190 nested = await collectionNested.mintToken(owner, totalAmount, {Substrate: charlie.address});191 }192193 194 const doOperationAndCheck = async ({195 amount,196 shouldBeNestedAfterOp,197 }: {198 amount: bigint,199 shouldBeNestedAfterOp: boolean,200 }) => {201 const nestedBalanceBeforeOp = await nested.getBalance(targetNft.nestingAccount());202203 if(testCase.op === 'transfer') {204 const bobBalanceBeforeOp = await nested.getBalance({Substrate: bob.address});205206 await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}, amount);207 expect(await nested.getBalance({Substrate: bob.address})).to.be.equal(bobBalanceBeforeOp + amount);208 } else {209 if(nested instanceof UniqueFTCollection) {210 await nested.burnTokensFrom(unnester, targetNft.nestingAccount(), amount);211 } else {212 await nested.burnFrom(unnester, targetNft.nestingAccount(), amount);213 }214 }215216 await checkNestedAmountState({217 expectedBalance: nestedBalanceBeforeOp - amount,218 childrenShouldPresent: shouldBeNestedAfterOp,219 nested,220 targetNft,221 });222 };223224 225 226 await nested.transfer(charlie, targetNft.nestingAccount(), totalAmount);227 await checkNestedAmountState({228 expectedBalance: totalAmount,229 childrenShouldPresent: true,230 nested,231 targetNft,232 });233234 235 236 await doOperationAndCheck({237 amount: firstUnnestAmount,238 shouldBeNestedAfterOp: true,239 });240241 242 243 await doOperationAndCheck({244 amount: restUnnestAmount,245 shouldBeNestedAfterOp: false,246 });247 }));248249 ownerOrAdminUnnestCases(['nft']).map(testCase =>250 itSub(`[nft]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, async ({helper}) => {251 const owner = alice;252 const admin = bob;253254 const unnester = (testCase.sender === 'owner')255 ? owner256 : admin;257258 const collectionNFT = await helper.nft.mintCollection(owner);259 await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});260261 const collectionNested = await helper.nft.mintCollection(owner, {262 limits: {263 ownerCanTransfer: true,264 },265 });266 await collectionNested.addAdmin(owner, {Substrate: admin.address});267268 const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});269 const nested = await collectionNested.mintToken(owner, {Substrate: charlie.address});270271 await nested.transfer(charlie, targetNft.nestingAccount());272 expect(await targetNft.getChildren()).to.be.deep.equal([{273 collectionId: nested.collectionId,274 tokenId: nested.tokenId,275 }]);276277 if(testCase.op === 'transfer') {278 await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address});279 } else {280 await nested.burnFrom(unnester, targetNft.nestingAccount());281 }282283 expect((await targetNft.getChildren()).length).to.be.equal(0);284 }));285});286287describe('Negative Test: Unnesting', () => {288 let alice: IKeyringPair;289 let bob: IKeyringPair;290291 before(async () => {292 await usingPlaygrounds(async (helper, privateKey) => {293 const donor = await privateKey({url: import.meta.url});294 [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);295 });296 });297298 itSub('Disallows a non-owner to unnest/burn a token', async ({helper}) => {299 const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});300 const targetToken = await collection.mintToken(alice);301302 303 const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());304305 306 await expect(nestedToken.unnest(bob, targetToken, {Substrate: alice.address})).to.be.rejectedWith(/common\.ApprovedValueTooLow/);307 expect(await nestedToken.getOwner()).to.be.deep.equal(CrossAccountId.toLowerCase(targetToken.nestingAccount()));308309 310 await expect(nestedToken.burnFrom(bob, targetToken.nestingAccount())).to.be.rejectedWith(/common\.ApprovedValueTooLow/);311 expect(await nestedToken.getOwner()).to.be.deep.equal(CrossAccountId.toLowerCase(targetToken.nestingAccount()));312 });313314 315316 317 itSub('Prevents Ouroboros creation', async ({helper}) => {318 const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});319 const targetToken = await collection.mintToken(alice);320321 322 const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());323 await expect(targetToken.nest(alice, nestedToken)).to.be.rejectedWith(/^structure\.OuroborosDetected$/);324 });325});