1234567891011121314151617import {IKeyringPair} from '@polkadot/types/types';18import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../util';19import {UniqueHelper, UniqueNFToken, UniqueRFToken} from '../util/playgrounds/unique';2021describe('Integration Test: Token Properties', () => {22 let superuser: IKeyringPair;23 let alice: IKeyringPair; 24 let bob: IKeyringPair; 25 let charlie: IKeyringPair; 2627 let permissions: {permission: any, signers: IKeyringPair[]}[];2829 before(async () => {30 await usingPlaygrounds(async (helper, privateKey) => {31 superuser = await privateKey('//Alice');32 const donor = await privateKey({filename: __filename});33 [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 100n, 100n], donor);34 });3536 permissions = [37 {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},38 {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},39 {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},40 {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},41 {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},42 {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},43 ];44 });4546 async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {47 const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {48 tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) => 49 signers.map(signer => {return {key: `${i+1}_${signer.address}`, permission};})),50 });51 return mode == 'NFT' ? [await collection.mintToken(alice), 1n] : [await collection.mintToken(alice, 100n), 100n];52 }53 54 async function testReadsYetEmptyProperties(token: UniqueNFToken | UniqueRFToken) {55 const properties = await token.getProperties();56 expect(properties).to.be.empty;5758 const tokenData = await token.getData();59 expect(tokenData!.properties).to.be.empty;60 }6162 itSub('Reads yet empty properties of a token (NFT)', async ({helper}) => {63 const collection = await helper.nft.mintCollection(alice);64 const token = await collection.mintToken(alice);65 await testReadsYetEmptyProperties(token);66 });6768 itSub.ifWithPallets('Reads yet empty properties of a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {69 const collection = await helper.rft.mintCollection(alice);70 const token = await collection.mintToken(alice);71 await testReadsYetEmptyProperties(token);72 });7374 async function testAssignPropertiesAccordingToPermissions(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {75 await token.collection.addAdmin(alice, {Substrate: bob.address});76 await token.transfer(alice, {Substrate: charlie.address}, pieces);7778 const propertyKeys: string[] = [];79 let i = 0;80 for (const permission of permissions) {81 i++;82 let j = 0;83 for (const signer of permission.signers) {84 j++;85 const key = i + '_' + signer.address;86 propertyKeys.push(key);8788 await expect(89 token.setProperties(signer, [{key: key, value: 'Serotonin increase'}]), 90 `on adding property #${i} by signer #${j}`,91 ).to.be.fulfilled;92 }93 }9495 const properties = await token.getProperties(propertyKeys);96 const tokenData = await token.getData();97 for (let i = 0; i < properties.length; i++) {98 expect(properties[i].value).to.be.equal('Serotonin increase');99 expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');100 }101 }102103 itSub('Assigns properties to a token according to permissions (NFT)', async ({helper}) => {104 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');105 await testAssignPropertiesAccordingToPermissions(token, amount);106 });107108 itSub.ifWithPallets('Assigns properties to a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {109 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');110 await testAssignPropertiesAccordingToPermissions(token, amount);111 });112113 async function testChangesPropertiesAccordingPermission(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {114 await token.collection.addAdmin(alice, {Substrate: bob.address});115 await token.transfer(alice, {Substrate: charlie.address}, pieces);116117 const propertyKeys: string[] = [];118 let i = 0;119 for (const permission of permissions) {120 i++;121 if (!permission.permission.mutable) continue;122 123 let j = 0;124 for (const signer of permission.signers) {125 j++;126 const key = i + '_' + signer.address;127 propertyKeys.push(key);128129 await expect(130 token.setProperties(signer, [{key, value: 'Serotonin increase'}]), 131 `on adding property #${i} by signer #${j}`,132 ).to.be.fulfilled;133134 await expect(135 token.setProperties(signer, [{key, value: 'Serotonin stable'}]), 136 `on changing property #${i} by signer #${j}`,137 ).to.be.fulfilled;138 }139 }140141 const properties = await token.getProperties(propertyKeys);142 const tokenData = await token.getData();143 for (let i = 0; i < properties.length; i++) {144 expect(properties[i].value).to.be.equal('Serotonin stable');145 expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');146 }147 }148149 itSub('Changes properties of a token according to permissions (NFT)', async ({helper}) => {150 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');151 await testChangesPropertiesAccordingPermission(token, amount);152 });153154 itSub.ifWithPallets('Changes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {155 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');156 await testChangesPropertiesAccordingPermission(token, amount);157 });158159 async function testDeletePropertiesAccordingPermission(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {160 await token.collection.addAdmin(alice, {Substrate: bob.address});161 await token.transfer(alice, {Substrate: charlie.address}, pieces);162163 const propertyKeys: string[] = [];164 let i = 0;165166 for (const permission of permissions) {167 i++;168 if (!permission.permission.mutable) continue;169 170 let j = 0;171 for (const signer of permission.signers) {172 j++;173 const key = i + '_' + signer.address;174 propertyKeys.push(key);175176 await expect(177 token.setProperties(signer, [{key, value: 'Serotonin increase'}]), 178 `on adding property #${i} by signer #${j}`,179 ).to.be.fulfilled;180181 await expect(182 token.deleteProperties(signer, [key]), 183 `on deleting property #${i} by signer #${j}`,184 ).to.be.fulfilled;185 }186 }187188 expect(await token.getProperties(propertyKeys)).to.be.empty;189 expect((await token.getData())!.properties).to.be.empty;190 }191 192 itSub('Deletes properties of a token according to permissions (NFT)', async ({helper}) => {193 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');194 await testDeletePropertiesAccordingPermission(token, amount);195 });196197 itSub.ifWithPallets('Deletes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {198 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');199 await testDeletePropertiesAccordingPermission(token, amount);200 });201202 itSub('Assigns properties to a nested token according to permissions', async ({helper}) => {203 const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});204 const collectionB = await helper.nft.mintCollection(alice, {205 tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) => 206 signers.map(signer => {return {key: `${i+1}_${signer.address}`, permission};})),207 });208 const targetToken = await collectionA.mintToken(alice);209 const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());210211 await collectionB.addAdmin(alice, {Substrate: bob.address});212 await targetToken.transfer(alice, {Substrate: charlie.address});213214 const propertyKeys: string[] = [];215 let i = 0;216 for (const permission of permissions) {217 i++;218 let j = 0;219 for (const signer of permission.signers) {220 j++;221 const key = i + '_' + signer.address;222 propertyKeys.push(key);223224 await expect(225 nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 226 `on adding property #${i} by signer #${j}`,227 ).to.be.fulfilled;228 }229 }230231 const properties = await nestedToken.getProperties(propertyKeys);232 const tokenData = await nestedToken.getData();233 for (let i = 0; i < properties.length; i++) {234 expect(properties[i].value).to.be.equal('Serotonin increase');235 expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');236 }237 expect(await targetToken.getProperties()).to.be.empty;238 });239240 itSub('Changes properties of a nested token according to permissions', async ({helper}) => {241 const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});242 const collectionB = await helper.nft.mintCollection(alice, {243 tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) => 244 signers.map(signer => {return {key: `${i+1}_${signer.address}`, permission};})),245 });246 const targetToken = await collectionA.mintToken(alice);247 const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());248249 await collectionB.addAdmin(alice, {Substrate: bob.address});250 await targetToken.transfer(alice, {Substrate: charlie.address});251252 const propertyKeys: string[] = [];253 let i = 0;254 for (const permission of permissions) {255 i++;256 if (!permission.permission.mutable) continue;257 258 let j = 0;259 for (const signer of permission.signers) {260 j++;261 const key = i + '_' + signer.address;262 propertyKeys.push(key);263264 await expect(265 nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 266 `on adding property #${i} by signer #${j}`,267 ).to.be.fulfilled;268269 await expect(270 nestedToken.setProperties(signer, [{key, value: 'Serotonin stable'}]), 271 `on changing property #${i} by signer #${j}`,272 ).to.be.fulfilled;273 }274 }275276 const properties = await nestedToken.getProperties(propertyKeys);277 const tokenData = await nestedToken.getData();278 for (let i = 0; i < properties.length; i++) {279 expect(properties[i].value).to.be.equal('Serotonin stable');280 expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');281 }282 expect(await targetToken.getProperties()).to.be.empty;283 });284285 itSub('Deletes properties of a nested token according to permissions', async ({helper}) => {286 const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});287 const collectionB = await helper.nft.mintCollection(alice, {288 tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) => 289 signers.map(signer => {return {key: `${i+1}_${signer.address}`, permission};})),290 });291 const targetToken = await collectionA.mintToken(alice);292 const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());293294 await collectionB.addAdmin(alice, {Substrate: bob.address});295 await targetToken.transfer(alice, {Substrate: charlie.address});296297 const propertyKeys: string[] = [];298 let i = 0;299 for (const permission of permissions) {300 i++;301 if (!permission.permission.mutable) continue;302 303 let j = 0;304 for (const signer of permission.signers) {305 j++;306 const key = i + '_' + signer.address;307 propertyKeys.push(key);308309 await expect(310 nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]), 311 `on adding property #${i} by signer #${j}`,312 ).to.be.fulfilled;313314 await expect(315 nestedToken.deleteProperties(signer, [key]), 316 `on deleting property #${i} by signer #${j}`,317 ).to.be.fulfilled;318 }319 }320321 expect(await nestedToken.getProperties(propertyKeys)).to.be.empty;322 expect((await nestedToken.getData())!.properties).to.be.empty;323 expect(await targetToken.getProperties()).to.be.empty;324 });325326 [327 {mode: 'nft' as const, storage: 'nonfungible' as const, pieces: undefined, requiredPallets: []},328 {mode: 'rft' as const, storage: 'refungible' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, 329 ].map(testCase =>330 itSub.ifWithPallets(`Allows modifying a token property multiple times with the same size (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {331 const propKey = 'tok-prop';332333 const collection = await helper[testCase.mode].mintCollection(alice, {334 tokenPropertyPermissions: [335 {336 key: propKey,337 permission: {mutable: true, tokenOwner: true},338 },339 ],340 });341342 const maxTokenPropertiesSize = 32768;343344 const propDataSize = 4096;345346 let propDataChar = 'a';347 const makeNewPropData = () => {348 propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1);349 return `${propDataChar}`.repeat(propDataSize);350 };351352 const token = await (353 testCase.pieces354 ? collection.mintToken(alice, testCase.pieces)355 : collection.mintToken(alice)356 );357358 await token.setProperties(alice, [{key: propKey, value: makeNewPropData()}]);359 const originalSpace = await token.getTokenPropertiesConsumedSpace();360 expect(originalSpace).to.be.equal(propDataSize);361362 const sameSizePropertiesPossibleNum = maxTokenPropertiesSize / propDataSize;363364 365 366 for (let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) {367 await token.setProperties(alice, [{key: propKey, value: makeNewPropData()}]);368 const consumedSpace = await token.getTokenPropertiesConsumedSpace();369 expect(consumedSpace).to.be.equal(originalSpace);370 }371 }));372373 [374 {mode: 'nft' as const, pieces: undefined, requiredPallets: []},375 {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, 376 ].map(testCase =>377 itSub.ifWithPallets(`Adding then removing a token property doesn't change the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {378 const propKey = 'tok-prop';379380 const collection = await helper[testCase.mode].mintCollection(alice, {381 tokenPropertyPermissions: [382 {383 key: propKey,384 permission: {mutable: true, tokenOwner: true},385 },386 ],387 });388 const token = await (389 testCase.pieces390 ? collection.mintToken(alice, testCase.pieces)391 : collection.mintToken(alice)392 );393 const originalSpace = await token.getTokenPropertiesConsumedSpace();394395 const propDataSize = 4096;396 const propData = 'a'.repeat(propDataSize);397398 await token.setProperties(alice, [{key: propKey, value: propData}]);399 let consumedSpace = await token.getTokenPropertiesConsumedSpace();400 expect(consumedSpace).to.be.equal(propDataSize);401402 await token.deleteProperties(alice, [propKey]);403 consumedSpace = await token.getTokenPropertiesConsumedSpace();404 expect(consumedSpace).to.be.equal(originalSpace);405 }));406407 [408 {mode: 'nft' as const, pieces: undefined, requiredPallets: []},409 {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, 410 ].map(testCase =>411 itSub.ifWithPallets(`force_repair_item preserves valid consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {412 const propKey = 'tok-prop';413414 const collection = await helper[testCase.mode].mintCollection(alice, {415 tokenPropertyPermissions: [416 {417 key: propKey,418 permission: {mutable: true, tokenOwner: true},419 },420 ],421 });422 const token = await (423 testCase.pieces424 ? collection.mintToken(alice, testCase.pieces)425 : collection.mintToken(alice)426 );427428 const propDataSize = 4096;429 const propData = 'a'.repeat(propDataSize);430431 await token.setProperties(alice, [{key: propKey, value: propData}]);432 const originalSpace = await token.getTokenPropertiesConsumedSpace();433 expect(originalSpace).to.be.equal(propDataSize);434435 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true);436 const recomputedSpace = await token.getTokenPropertiesConsumedSpace();437 expect(recomputedSpace).to.be.equal(originalSpace);438 }));439440 [441 {mode: 'nft' as const, pieces: undefined, requiredPallets: []},442 {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, 443 ].map(testCase =>444 itSub.ifWithPallets(`Modifying a token property with different sizes correctly changes the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {445 const propKey = 'tok-prop';446447 const collection = await helper[testCase.mode].mintCollection(alice, {448 tokenPropertyPermissions: [449 {450 key: propKey,451 permission: {mutable: true, tokenOwner: true},452 },453 ],454 });455 const token = await (456 testCase.pieces457 ? collection.mintToken(alice, testCase.pieces)458 : collection.mintToken(alice)459 );460 const originalSpace = await token.getTokenPropertiesConsumedSpace();461462 const initPropDataSize = 4096;463 const biggerPropDataSize = 5000;464 const smallerPropDataSize = 4000;465466 const initPropData = 'a'.repeat(initPropDataSize);467 const biggerPropData = 'b'.repeat(biggerPropDataSize);468 const smallerPropData = 'c'.repeat(smallerPropDataSize);469470 let consumedSpace;471 let expectedConsumedSpaceDiff;472473 await token.setProperties(alice, [{key: propKey, value: initPropData}]);474 consumedSpace = await token.getTokenPropertiesConsumedSpace();475 expectedConsumedSpaceDiff = initPropDataSize - originalSpace;476 expect(consumedSpace).to.be.equal(originalSpace + expectedConsumedSpaceDiff);477478 await token.setProperties(alice, [{key: propKey, value: biggerPropData}]);479 consumedSpace = await token.getTokenPropertiesConsumedSpace();480 expectedConsumedSpaceDiff = biggerPropDataSize - initPropDataSize;481 expect(consumedSpace).to.be.equal(initPropDataSize + expectedConsumedSpaceDiff);482483 await token.setProperties(alice, [{key: propKey, value: smallerPropData}]);484 consumedSpace = await token.getTokenPropertiesConsumedSpace();485 expectedConsumedSpaceDiff = biggerPropDataSize - smallerPropDataSize;486 expect(consumedSpace).to.be.equal(biggerPropDataSize - expectedConsumedSpaceDiff);487 }));488});489490describe('Negative Integration Test: Token Properties', () => {491 let alice: IKeyringPair; 492 let bob: IKeyringPair; 493 let charlie: IKeyringPair; 494495 let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];496497 before(async () => {498 await usingPlaygrounds(async (helper, privateKey) => {499 const donor = await privateKey({filename: __filename});500 let dave: IKeyringPair;501 [alice, bob, charlie, dave] = await helper.arrange.createAccounts([100n, 100n, 100n, 100n], donor);502503 504 constitution = [505 {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},506 {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},507 {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},508 {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},509 {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},510 {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},511 ];512 });513 });514515 async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {516 const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {517 tokenPropertyPermissions: constitution.map(({permission}, i) => {return {key: `${i+1}`, permission};}),518 });519 return mode == 'NFT' ? [await collection.mintToken(alice), 1n] : [await collection.mintToken(alice, 100n), 100n];520 }521522 async function getConsumedSpace(api: any, collectionId: number, tokenId: number, mode: 'NFT' | 'RFT'): Promise<number> {523 return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace;524 }525526 async function prepare(token: UniqueNFToken | UniqueRFToken, pieces: bigint): Promise<number> {527 await token.collection.addAdmin(alice, {Substrate: bob.address});528 await token.transfer(alice, {Substrate: charlie.address}, pieces);529530 let i = 0;531 for (const passage of constitution) {532 i++;533 const signer = passage.signers[0];534 await expect(535 token.setProperties(signer, [{key: `${i}`, value: 'Serotonin increase'}]), 536 `on adding property ${i} by ${signer.address}`,537 ).to.be.fulfilled;538 }539540 const originalSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 541 return originalSpace;542 }543544 async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {545 const originalSpace = await prepare(token, pieces);546547 let i = 0;548 for (const forbiddance of constitution) {549 i++;550 if (!forbiddance.permission.mutable) continue;551552 await expect(553 token.setProperties(forbiddance.sinner, [{key: `${i}`, value: 'Serotonin down'}]), 554 `on failing to change property ${i} by the malefactor`,555 ).to.be.rejectedWith(/common\.NoPermission/);556557 await expect(558 token.deleteProperties(forbiddance.sinner, [`${i}`]), 559 `on failing to delete property ${i} by the malefactor`,560 ).to.be.rejectedWith(/common\.NoPermission/);561 }562563 const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 564 expect(consumedSpace).to.be.equal(originalSpace);565 }566567 itSub('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async ({helper}) => {568 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');569 await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, amount);570 });571572 itSub.ifWithPallets('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {573 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');574 await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, amount);575 });576577 async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {578 const originalSpace = await prepare(token, pieces);579580 let i = 0;581 for (const permission of constitution) {582 i++;583 if (permission.permission.mutable) continue;584585 await expect(586 token.setProperties(permission.signers[0], [{key: `${i}`, value: 'Serotonin down'}]), 587 `on failing to change property ${i} by signer #0`,588 ).to.be.rejectedWith(/common\.NoPermission/);589590 await expect(591 token.deleteProperties(permission.signers[0], [i.toString()]), 592 `on failing to delete property ${i} by signer #0`,593 ).to.be.rejectedWith(/common\.NoPermission/);594 }595 596 const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 597 expect(consumedSpace).to.be.equal(originalSpace);598 }599600 itSub('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async ({helper}) => {601 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');602 await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, amount);603 });604605 itSub.ifWithPallets('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', [Pallets.ReFungible], async ({helper}) => {606 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');607 await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, amount);608 });609610 async function testForbidsAddingPropertiesIfPropertyNotDeclared(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {611 const originalSpace = await prepare(token, pieces);612613 await expect(614 token.setProperties(alice, [{key: 'non-existent', value: 'I exist!'}]), 615 'on failing to add a previously non-existent property',616 ).to.be.rejectedWith(/common\.NoPermission/);617 618 await expect(619 token.collection.setTokenPropertyPermissions(alice, [{key: 'now-existent', permission: {}}]), 620 'on setting a new non-permitted property',621 ).to.be.fulfilled;622623 await expect(624 token.setProperties(alice, [{key: 'now-existent', value: 'I exist!'}]), 625 'on failing to add a property forbidden by the \'None\' permission',626 ).to.be.rejectedWith(/common\.NoPermission/);627628 expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty;629 630 const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 631 expect(consumedSpace).to.be.equal(originalSpace);632 }633634 itSub('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async ({helper}) => {635 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');636 await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount);637 });638639 itSub.ifWithPallets('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', [Pallets.ReFungible], async ({helper}) => {640 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');641 await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount);642 });643644 async function testForbidsAddingTooLargeProperties(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {645 const originalSpace = await prepare(token, pieces);646647 await expect(648 token.collection.setTokenPropertyPermissions(alice, [649 {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 650 {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},651 ]), 652 'on setting new permissions for properties',653 ).to.be.fulfilled;654655 656 {657 console.error = () => {};658 await expect(token.setProperties(alice, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]))659 .to.be.rejected;660 }661662 await expect(token.setProperties(alice, [663 {key: 'a_holy_book', value: 'word '.repeat(3277)}, 664 {key: 'young_years', value: 'neverending'.repeat(1490)},665 ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);666 667 expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty;668 const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT'); 669 expect(consumedSpace).to.be.equal(originalSpace);670 }671672 itSub('Forbids adding too large properties to a token (NFT)', async ({helper}) => {673 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');674 await testForbidsAddingTooLargeProperties(token, amount);675 });676677 itSub.ifWithPallets('Forbids adding too large properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {678 const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');679 await testForbidsAddingTooLargeProperties(token, amount);680 });681682 [683 {mode: 'nft' as const, requiredPallets: []},684 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, 685 ].map(testCase =>686 itSub.ifWithPallets(`Forbids adding too many propeties to a token (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {687 const collection = await helper[testCase.mode].mintCollection(alice);688 const maxPropertiesPerItem = 64;689690 for (let i = 0; i < maxPropertiesPerItem; i++) {691 await collection.setTokenPropertyPermissions(alice, [{692 key: `${i+1}`,693 permission: {mutable: true, tokenOwner: true, collectionAdmin: true},694 }]);695 }696697 await expect(collection.setTokenPropertyPermissions(alice, [{698 key: `${maxPropertiesPerItem}-th`,699 permission: {mutable: true, tokenOwner: true, collectionAdmin: true},700 }])).to.be.rejectedWith(/common\.PropertyLimitReached/);701 }));702703 [704 {mode: 'nft' as const, pieces: undefined, requiredPallets: []},705 {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, 706 ].map(testCase =>707 itSub.ifWithPallets(`Forbids force_repair_item from non-sudo (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {708 const propKey = 'tok-prop';709710 const collection = await helper[testCase.mode].mintCollection(alice, {711 tokenPropertyPermissions: [712 {713 key: propKey,714 permission: {mutable: true, tokenOwner: true},715 },716 ],717 });718 const token = await (719 testCase.pieces720 ? collection.mintToken(alice, testCase.pieces)721 : collection.mintToken(alice)722 );723724 const propDataSize = 4096;725 const propData = 'a'.repeat(propDataSize);726 await token.setProperties(alice, [{key: propKey, value: propData}]);727728 await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true))729 .to.be.rejectedWith(/BadOrigin/);730 }));731});732733describe('ReFungible token properties permissions tests', () => {734 let alice: IKeyringPair;735 let bob: IKeyringPair;736 let charlie: IKeyringPair;737738 before(async function() {739 await usingPlaygrounds(async (helper, privateKey) => {740 requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);741742 const donor = await privateKey({filename: __filename});743 [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);744 });745 });746747 async function prepare(helper: UniqueHelper): Promise<UniqueRFToken> {748 const collection = await helper.rft.mintCollection(alice);749 const token = await collection.mintToken(alice, 100n);750 751 await collection.addAdmin(alice, {Substrate: bob.address});752 await collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable: true, tokenOwner: true}}]);753 754 return token;755 }756757 itSub('Forbids adding token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {758 const token = await prepare(helper);759760 await token.transfer(alice, {Substrate: charlie.address}, 33n);761762 await expect(token.setProperties(alice, [763 {key: 'fractals', value: 'multiverse'}, 764 ])).to.be.rejectedWith(/common\.NoPermission/);765 });766767 itSub('Forbids mutating token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {768 const token = await prepare(helper);769770 await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, tokenOwner: true}}]))771 .to.be.fulfilled;772773 await expect(token.setProperties(alice, [774 {key: 'fractals', value: 'multiverse'}, 775 ])).to.be.fulfilled;776777 await token.transfer(alice, {Substrate: charlie.address}, 33n);778779 await expect(token.setProperties(alice, [780 {key: 'fractals', value: 'want to rule the world'}, 781 ])).to.be.rejectedWith(/common\.NoPermission/);782 });783784 itSub('Forbids deleting token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {785 const token = await prepare(helper);786787 await expect(token.setProperties(alice, [788 {key: 'fractals', value: 'one headline - why believe it'}, 789 ])).to.be.fulfilled;790791 await token.transfer(alice, {Substrate: charlie.address}, 33n);792793 await expect(token.deleteProperties(alice, ['fractals'])).794 to.be.rejectedWith(/common\.NoPermission/);795 });796797 itSub('Allows token property mutation with collectionOwner==true when admin doesn\'t have all pieces', async ({helper}) => {798 const token = await prepare(helper);799800 await token.transfer(alice, {Substrate: charlie.address}, 33n);801802 await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, collectionAdmin: true}}]))803 .to.be.fulfilled;804805 await expect(token.setProperties(alice, [806 {key: 'fractals', value: 'multiverse'}, 807 ])).to.be.fulfilled;808 });809});