--- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -833,7 +833,7 @@ ), ); ::Currency::settle( - &owner.as_sub(), + owner.as_sub(), imbalance, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive, --- a/tests/src/nesting/properties.test.ts +++ b/tests/src/nesting/properties.test.ts @@ -2,6 +2,7 @@ import usingApi, {executeTransaction} from '../substrate/substrate-api'; import { addCollectionAdminExpectSuccess, + CollectionMode, createCollectionExpectSuccess, setCollectionPermissionsExpectSuccess, createItemExpectSuccess, @@ -23,9 +24,9 @@ }); }); - it('Makes sure collectionById supplies required fields', async () => { + async function testMakeSureSuppliesRequired(mode: CollectionMode) { await usingApi(async api => { - const collectionId = await createCollectionExpectSuccess(); + const collectionId = await createCollectionExpectSuccess({mode: mode}); const collectionOption = await api.rpc.unique.collectionById(collectionId); expect(collectionOption.isSome).to.be.true; @@ -57,6 +58,14 @@ expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties); }); + } + + it('Makes sure collectionById supplies required fields for NFT', async () => { + await testMakeSureSuppliesRequired({type: 'NFT'}); + }); + + it('Makes sure collectionById supplies required fields for ReFungible', async () => { + await testMakeSureSuppliesRequired({type: 'ReFungible'}); }); }); @@ -79,9 +88,10 @@ }); }); - it('Sets properties for a collection', async () => { + + async function testSetsPropertiesForCollection(mode: string) { await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'})); + const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); const {collectionId} = getCreateCollectionResult(events); // As owner @@ -106,48 +116,54 @@ {key: 'black_hole', value: ''}, ]); }); + } + it('Sets properties for a NFT collection', async () => { + await testSetsPropertiesForCollection('NFT'); }); + it('Sets properties for a ReFungible collection', async () => { + await testSetsPropertiesForCollection('ReFungible'); + }); - it('Check valid names for collection properties keys', async () => { + async function testCheckValidNames(mode: string) { await usingApi(async api => { - const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'})); + const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode})); const {collectionId} = getCreateCollectionResult(events); - + // alpha symbols await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), )).to.not.be.rejected; - + // numeric symbols await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), )).to.not.be.rejected; - + // underscore symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), )).to.not.be.rejected; - + // dash symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), )).to.not.be.rejected; - + // underscore symbol await expect(executeTransaction( api, bob, api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), )).to.not.be.rejected; - + const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs']; const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman(); expect(properties).to.be.deep.equal([ @@ -158,54 +174,72 @@ {key: 'build.rs', value: ''}, ]); }); + } + it('Check valid names for NFT collection properties keys', async () => { + await testCheckValidNames('NFT'); }); + it('Check valid names for ReFungible collection properties keys', async () => { + await testCheckValidNames('ReFungible'); + }); - it('Changes properties of a collection', async () => { + async function testChangesProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), )).to.not.be.rejected; - + // Mutate the properties await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ {key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}, ]); }); + } + it('Changes properties of a NFT collection', async () => { + await testChangesProperties({type: 'NFT'}); }); + it('Changes properties of a ReFungible collection', async () => { + await testChangesProperties({type: 'ReFungible'}); + }); - it('Deletes properties of a collection', async () => { + async function testDeleteProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.deleteCollectionProperties(collection, ['electron']), )).to.not.be.rejected; - + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ {key: 'black_hole', value: 'LIGO'}, ]); - }); + }); + } + it('Deletes properties of a NFT collection', async () => { + await testDeleteProperties({type: 'NFT'}); + }); + it('Deletes properties of a ReFungible collection', async () => { + await testDeleteProperties({type: 'ReFungible'}); }); }); @@ -217,10 +251,10 @@ }); }); - it('Fails to set properties in a collection if not its onwer/administrator', async () => { + async function testFailsSetPropertiesIfNotOwnerOrAdmin(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, bob, @@ -230,14 +264,20 @@ const properties = (await api.query.common.collectionProperties(collection)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.equal(0); - }); + }); + } + it('Fails to set properties in a NFT collection if not its onwer/administrator', async () => { + await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'NFT'}); + }); + it('Fails to set properties in a ReFungible collection if not its onwer/administrator', async () => { + await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'ReFungible'}); }); - it('Fails to set properties that exceed the limits', async () => { + async function testFailsSetPropertiesThatExeedLimits(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); + const collection = await createCollectionExpectSuccess({mode: mode}); const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; - + // Mute the general tx parsing error, too many bytes to process { console.error = () => {}; @@ -247,10 +287,10 @@ api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), )).to.be.rejected; } - + let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON(); expect(properties).to.be.empty; - + await expect(executeTransaction( api, alice, @@ -259,16 +299,22 @@ {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - + properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON(); expect(properties).to.be.empty; - }); + }); + } + it('Fails to set properties that exceed the limits (NFT)', async () => { + await testFailsSetPropertiesThatExeedLimits({type: 'NFT'}); + }); + it('Fails to set properties that exceed the limits (ReFungible)', async () => { + await testFailsSetPropertiesThatExeedLimits({type: 'ReFungible'}); }); - it('Fails to set more properties than it is allowed', async () => { + async function testFailsSetMorePropertiesThanAllowed(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const propertiesToBeSet = []; for (let i = 0; i < 65; i++) { propertiesToBeSet.push({ @@ -276,29 +322,35 @@ value: Math.random() > 0.5 ? 'high' : 'low', }); } - + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), )).to.be.rejectedWith(/common\.PropertyLimitReached/); - + const properties = (await api.query.common.collectionProperties(collection)).toJSON(); expect(properties.map).to.be.empty; expect(properties.consumedSpace).to.equal(0); - }); + }); + } + it('Fails to set more properties than it is allowed (NFT)', async () => { + await testFailsSetMorePropertiesThanAllowed({type: 'NFT'}); + }); + it('Fails to set more properties than it is allowed (ReFungible)', async () => { + await testFailsSetMorePropertiesThanAllowed({type: 'ReFungible'}); }); - - it('Fails to set properties with invalid names', async () => { + + async function testFailsSetPropertiesWithInvalidNames(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const invalidProperties = [ [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}], [{key: 'Mr/Sandman', value: 'Bring me a gene'}], [{key: 'déjà vu', value: 'hmm...'}], ]; - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -306,13 +358,13 @@ api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } - + await expect(executeTransaction( api, alice, api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - + await expect(executeTransaction( api, alice, @@ -320,14 +372,14 @@ {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, ]), ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat(''); - + const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman(); expect(properties).to.be.deep.equal([ {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, ]); - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -336,7 +388,13 @@ ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } }); + } + it('Fails to set properties with invalid names (NFT)', async () => { + await testFailsSetPropertiesWithInvalidNames({type: 'NFT'}); }); + it('Fails to set properties with invalid names (ReFungible)', async () => { + await testFailsSetPropertiesWithInvalidNames({type: 'ReFungible'}); + }); }); // ---------- ACCESS RIGHTS @@ -357,53 +415,65 @@ }); }); - it('Sets access rights to properties of a collection', async () => { + async function testSetsAccessRightsToProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), )).to.not.be.rejected; - + await addCollectionAdminExpectSuccess(alice, collection, bob.address); - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), )).to.not.be.rejected; - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}}, {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}}, ]); - }); + }); + } + it('Sets access rights to properties of a collection (NFT)', async () => { + await testSetsAccessRightsToProperties({type: 'NFT'}); }); + it('Sets access rights to properties of a collection (ReFungible)', async () => { + await testSetsAccessRightsToProperties({type: 'ReFungible'}); + }); - it('Changes access rights to properties of a collection', async () => { + async function testChangesAccessRightsToProperty(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); }); + } + it('Changes access rights to properties of a NFT collection', async () => { + await testChangesAccessRightsToProperty({type: 'NFT'}); + }); + it('Changes access rights to properties of a ReFungible collection', async () => { + await testChangesAccessRightsToProperty({type: 'ReFungible'}); }); }); @@ -415,25 +485,31 @@ }); }); - it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => { + async function testPreventsFromSettingAccessRightsNotAdminOrOwner(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, bob, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), )).to.be.rejectedWith(/common\.NoPermission/); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON(); expect(propertyRights).to.be.empty; }); + } + it('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async () => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'NFT'}); }); + it('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', async () => { + await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'ReFungible'}); + }); - it('Prevents from adding too many possible properties', async () => { + async function testPreventFromAddingTooManyPossibleProperties(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const constitution = []; for (let i = 0; i < 65; i++) { constitution.push({ @@ -441,51 +517,63 @@ permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {}, }); } - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, constitution), )).to.be.rejectedWith(/common\.PropertyLimitReached/); - + const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON(); expect(propertyRights).to.be.empty; - }); + }); + } + it('Prevents from adding too many possible properties (NFT)', async () => { + await testPreventFromAddingTooManyPossibleProperties({type: 'NFT'}); + }); + it('Prevents from adding too many possible properties (ReFungible)', async () => { + await testPreventFromAddingTooManyPossibleProperties({type: 'ReFungible'}); }); - it('Prevents access rights to be modified if constant', async () => { + async function testPreventAccessRightsModifiedIfConstant(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), )).to.be.rejectedWith(/common\.NoPermission/); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.deep.equal([ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); - }); + }); + } + it('Prevents access rights to be modified if constant (NFT)', async () => { + await testPreventAccessRightsModifiedIfConstant({type: 'NFT'}); }); + it('Prevents access rights to be modified if constant (ReFungible)', async () => { + await testPreventAccessRightsModifiedIfConstant({type: 'ReFungible'}); + }); - it('Prevents adding properties with invalid names', async () => { + async function testPreventsAddingPropertiesWithInvalidNames(mode: CollectionMode) { await usingApi(async api => { - const collection = await createCollectionExpectSuccess(); - + const collection = await createCollectionExpectSuccess({mode: mode}); + const invalidProperties = [ [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}], [{key: 'G#4', permission: {tokenOwner: true}}], [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}], ]; - + for (let i = 0; i < invalidProperties.length; i++) { await expect(executeTransaction( api, @@ -493,13 +581,13 @@ api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } - + await expect(executeTransaction( api, alice, api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/); - + const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string await expect(executeTransaction( api, @@ -508,14 +596,20 @@ {key: correctKey, permission: {collectionAdmin: true}}, ]), ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; - + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat(''); - + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman(); expect(propertyRights).to.be.deep.equal([ {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, ]); }); + } + it('Prevents adding properties with invalid names (NFT)', async () => { + await testPreventsAddingPropertiesWithInvalidNames({type: 'NFT'}); + }); + it('Prevents adding properties with invalid names (ReFungible)', async () => { + await testPreventsAddingPropertiesWithInvalidNames({type: 'ReFungible'}); }); }); --- a/tests/src/refungible.test.ts +++ b/tests/src/refungible.test.ts @@ -211,35 +211,4 @@ expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); }); }); - - it('Set properties for exist collection', async () => { - await usingApi(async api => { - const collectionId = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'ReFungible'}, - }); - - const properties = [ - {key: 'key1', value: 'val1'}, - {key: 'key2', value: 'val2'}, - ]; - await expect(executeTransaction( - api, - alice, - api.tx.unique.setCollectionProperties(collectionId, properties), - )).to.not.be.rejected; - - const propertyPermissions = [ - {key: 'key1', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}}, - {key: 'key2', permission: {collectionAdmin: false, mutable: true, tokenOwner: false}}, - ]; - await expect(executeTransaction( - api, - alice, - api.tx.unique.setTokenPropertyPermissions(collectionId, propertyPermissions), - )).to.not.be.rejected; - - const collection = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collection.properties.toHuman()).to.be.deep.equal(properties); - expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions); - }); - }); }); --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -284,7 +284,7 @@ type: 'ReFungible'; } -type CollectionMode = Nft | Fungible | ReFungible; +export type CollectionMode = Nft | Fungible | ReFungible; export type Property = { key: any,