From c12162af788548e21121396b7bb822b625bffb99 Mon Sep 17 00:00:00 2001 From: ut-akuznetsov <59873862+ut-akuznetsov@users.noreply.github.com> Date: Fri, 16 Dec 2022 18:42:26 +0000 Subject: [PATCH] Merge pull request #785 from UniqueNetwork/feature/force-repair-things --- --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -1223,7 +1223,7 @@ } /// Set a scoped collection property, where the scope is a special prefix - /// prohibiting a user to change the property. + /// prohibiting a user access to change the property directly. /// /// * `collection_id` - ID of the collection for which the property is being set. /// * `scope` - Property scope. @@ -1242,7 +1242,7 @@ } /// Set scoped collection properties, where the scope is a special prefix - /// prohibiting a user to change the properties. + /// prohibiting a user access to change the properties directly. /// /// * `collection_id` - ID of the collection for which the properties is being set. /// * `scope` - Property scope. @@ -1730,6 +1730,15 @@ ); Ok(new_permission) } + + /// Repair possibly broken properties of a collection. + pub fn repair_collection(collection_id: CollectionId) -> DispatchResult { + CollectionProperties::::mutate(collection_id, |properties| { + properties.recompute_consumed_space(); + }); + + Ok(()) + } } /// Indicates unsupported methods by returning [Error::UnsupportedOperation]. @@ -1819,7 +1828,7 @@ fn set_allowance_for_all() -> Weight; /// The price of repairing an item. - fn repair_item() -> Weight; + fn force_repair_item() -> Weight; } /// Weight info extension trait for refungible pallet. --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -113,7 +113,7 @@ Weight::zero() } - fn repair_item() -> Weight { + fn force_repair_item() -> Weight { Weight::zero() } } --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -127,7 +127,7 @@ >::set_allowance_for_all() } - fn repair_item() -> Weight { + fn force_repair_item() -> Weight { >::repair_item() } } @@ -540,7 +540,7 @@ fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo { with_weight( >::repair_item(self, token), - >::repair_item(), + >::force_repair_item(), ) } } --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -157,7 +157,7 @@ >::set_allowance_for_all() } - fn repair_item() -> Weight { + fn force_repair_item() -> Weight { >::repair_item() } } @@ -544,7 +544,7 @@ fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo { with_weight( >::repair_item(self, token), - >::repair_item(), + >::force_repair_item(), ) } } --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -82,7 +82,7 @@ BoundedVec, }; use scale_info::TypeInfo; -use frame_system::{self as system, ensure_signed}; +use frame_system::{self as system, ensure_signed, ensure_root}; use sp_std::{vec, vec::Vec}; use up_data_structs::{ MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, @@ -983,18 +983,33 @@ }) } - /// Repairs a broken item + /// Repairs a collection if the data was somehow corrupted. /// /// # Arguments /// + /// * `collection_id`: ID of the collection to repair. + #[weight = >::force_repair_collection()] + pub fn force_repair_collection( + origin, + collection_id: CollectionId, + ) -> DispatchResult { + ensure_root(origin)?; + >::repair_collection(collection_id) + } + + /// Repairs a token if the data was somehow corrupted. + /// + /// # Arguments + /// /// * `collection_id`: ID of the collection the item belongs to. /// * `item_id`: ID of the item. - #[weight = T::CommonWeightInfo::repair_item()] - pub fn repair_item( - _origin, + #[weight = T::CommonWeightInfo::force_repair_item()] + pub fn force_repair_item( + origin, collection_id: CollectionId, item_id: TokenId, ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; dispatch_tx::(collection_id, |d| { d.repair_item(item_id) }) --- a/pallets/unique/src/weights.rs +++ b/pallets/unique/src/weights.rs @@ -45,6 +45,7 @@ fn remove_collection_sponsor() -> Weight; fn set_transfers_enabled_flag() -> Weight; fn set_collection_limits() -> Weight; + fn force_repair_collection() -> Weight; } /// Weights for pallet_unique using the Substrate node and recommended hardware. @@ -139,6 +140,12 @@ .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } + // Storage: Common CollectionProperties (r:1 w:1) + fn force_repair_collection() -> Weight { + Weight::from_ref_time(5_701_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } } // For backwards compatibility and tests @@ -232,4 +239,10 @@ .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } + // Storage: Common CollectionProperties (r:1 w:1) + fn force_repair_collection() -> Weight { + Weight::from_ref_time(5_701_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } } --- a/runtime/common/weights.rs +++ b/runtime/common/weights.rs @@ -125,8 +125,8 @@ max_weight_of!(set_allowance_for_all()) } - fn repair_item() -> Weight { - max_weight_of!(repair_item()) + fn force_repair_item() -> Weight { + max_weight_of!(force_repair_item()) } } --- a/tests/package.json +++ b/tests/package.json @@ -44,8 +44,9 @@ "testEvmCoder": "mocha --timeout 9999999 -r ts-node/register './**/eth/evmCoder.test.ts'", "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nest.test.ts", "testUnnesting": "mocha --timeout 9999999 -r ts-node/register ./**/unnest.test.ts", - "testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/collectionProperties.test.ts ./**/tokenProperties.test.ts ./**/getPropertiesRpc.test.ts", - "testCollectionProperties": "mocha --timeout 9999999 -r ts-node/register ./**/collectionProperties.test.ts", + "testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/collectionProperties.*test.ts ./**/tokenProperties.*test.ts ./**/getPropertiesRpc.test.ts", + "testCollectionProperties": "mocha --timeout 9999999 -r ts-node/register ./**/collectionProperties.*test.ts", + "testTokenProperties": "mocha --timeout 9999999 -r ts-node/register ./**/tokenProperties.*test.ts", "testMigration": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/migration-check.test.ts", "testAddCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/addCollectionAdmin.test.ts", "testSetCollectionLimits": "mocha --timeout 9999999 -r ts-node/register ./**/setCollectionLimits.test.ts", --- /dev/null +++ b/tests/src/nesting/collectionProperties.seqtest.ts @@ -0,0 +1,61 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip} from '../util'; + +describe('Integration Test: Collection Properties with sudo', () => { + let superuser: IKeyringPair; + let alice: IKeyringPair; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + superuser = await privateKey('//Alice'); + const donor = await privateKey({filename: __filename}); + [alice] = await helper.arrange.createAccounts([100n], donor); + }); + }); + + [ + {mode: 'nft' as const, requiredPallets: []}, + {mode: 'ft' as const, requiredPallets: []}, + {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, + ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => { + before(async function() { + // eslint-disable-next-line require-await + await usingPlaygrounds(async helper => { + requirePalletsOrSkip(this, helper, testSuite.requiredPallets); + }); + }); + + itSub('Repairing an unbroken collection\'s properties preserves the consumed space', async({helper}) => { + const properties = [ + {key: 'sea-creatures', value: 'mermaids'}, + {key: 'goldenratio', value: '1.6180339887498948482045868343656381177203091798057628621354486227052604628189'}, + ]; + const collection = await helper[testSuite.mode].mintCollection(alice, {properties}); + + const newProperty = ' '.repeat(4096); + await collection.setProperties(alice, [{key: 'space', value: newProperty}]); + const originalSpace = await collection.getPropertiesConsumedSpace(); + expect(originalSpace).to.be.equal(properties[0].value.length + properties[1].value.length + newProperty.length); + + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairCollection', [collection.collectionId], true); + const recomputedSpace = await collection.getPropertiesConsumedSpace(); + expect(recomputedSpace).to.be.equal(originalSpace); + }); + })); +}); \ No newline at end of file --- a/tests/src/nesting/collectionProperties.test.ts +++ b/tests/src/nesting/collectionProperties.test.ts @@ -314,6 +314,16 @@ ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); } }); + + itSub('Forbids to repair a collection if called with non-sudo', async({helper}) => { + const collection = await helper[testSuite.mode].mintCollection(alice, {properties: [ + {key: 'sea-creatures', value: 'mermaids'}, + {key: 'goldenratio', value: '1.6180339887498948482045868343656381177203091798057628621354486227052604628189'}, + ]}); + + await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairCollection', [collection.collectionId], true)) + .to.be.rejectedWith(/BadOrigin/); + }); })); }); \ No newline at end of file --- /dev/null +++ b/tests/src/nesting/tokenProperties.seqtest.ts @@ -0,0 +1,72 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip} from '../util'; + +describe('Integration Test: Token Properties with sudo', () => { + let superuser: IKeyringPair; + let alice: IKeyringPair; // collection owner + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + superuser = await privateKey('//Alice'); + const donor = await privateKey({filename: __filename}); + [alice] = await helper.arrange.createAccounts([100n], donor); + }); + }); + + [ + {mode: 'nft' as const, pieces: undefined, requiredPallets: []}, + {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, + ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => { + before(async function() { + // eslint-disable-next-line require-await + await usingPlaygrounds(async helper => { + requirePalletsOrSkip(this, helper, testSuite.requiredPallets); + }); + }); + + itSub('force_repair_item preserves valid consumed space', async({helper}) => { + const propKey = 'tok-prop'; + + const collection = await helper[testSuite.mode].mintCollection(alice, { + tokenPropertyPermissions: [ + { + key: propKey, + permission: {mutable: true, tokenOwner: true}, + }, + ], + }); + const token = await ( + testSuite.pieces + ? collection.mintToken(alice, testSuite.pieces) + : collection.mintToken(alice) + ); + + const propDataSize = 4096; + const propData = 'a'.repeat(propDataSize); + + await token.setProperties(alice, [{key: propKey, value: propData}]); + const originalSpace = await token.getTokenPropertiesConsumedSpace(); + expect(originalSpace).to.be.equal(propDataSize); + + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true); + const recomputedSpace = await token.getTokenPropertiesConsumedSpace(); + expect(recomputedSpace).to.be.equal(originalSpace); + }); + })); +}); \ No newline at end of file --- a/tests/src/nesting/tokenProperties.test.ts +++ b/tests/src/nesting/tokenProperties.test.ts @@ -406,39 +406,6 @@ {mode: 'nft' as const, pieces: undefined, requiredPallets: []}, {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, ].map(testCase => - itSub.ifWithPallets(`repair_item preserves valid consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => { - const propKey = 'tok-prop'; - - const collection = await helper[testCase.mode].mintCollection(alice, { - tokenPropertyPermissions: [ - { - key: propKey, - permission: {mutable: true, tokenOwner: true}, - }, - ], - }); - const token = await ( - testCase.pieces - ? collection.mintToken(alice, testCase.pieces) - : collection.mintToken(alice) - ); - - const propDataSize = 4096; - const propData = 'a'.repeat(propDataSize); - - await token.setProperties(alice, [{key: propKey, value: propData}]); - const originalSpace = await token.getTokenPropertiesConsumedSpace(); - expect(originalSpace).to.be.equal(propDataSize); - - await helper.executeExtrinsic(alice, 'api.tx.unique.repairItem', [token.collectionId, token.tokenId], true); - const recomputedSpace = await token.getTokenPropertiesConsumedSpace(); - expect(recomputedSpace).to.be.equal(originalSpace); - })); - - [ - {mode: 'nft' as const, pieces: undefined, requiredPallets: []}, - {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, - ].map(testCase => itSub.ifWithPallets(`Modifying a token property with different sizes correctly changes the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => { const propKey = 'tok-prop'; @@ -697,6 +664,35 @@ permission: {mutable: true, tokenOwner: true, collectionAdmin: true}, }])).to.be.rejectedWith(/common\.PropertyLimitReached/); })); + + [ + {mode: 'nft' as const, pieces: undefined, requiredPallets: []}, + {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, + ].map(testCase => + itSub.ifWithPallets(`Forbids force_repair_item from non-sudo (${testCase.mode})`, testCase.requiredPallets, async({helper}) => { + const propKey = 'tok-prop'; + + const collection = await helper[testCase.mode].mintCollection(alice, { + tokenPropertyPermissions: [ + { + key: propKey, + permission: {mutable: true, tokenOwner: true}, + }, + ], + }); + const token = await ( + testCase.pieces + ? collection.mintToken(alice, testCase.pieces) + : collection.mintToken(alice) + ); + + const propDataSize = 4096; + const propData = 'a'.repeat(propDataSize); + await token.setProperties(alice, [{key: propKey, value: propData}]); + + await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true)) + .to.be.rejectedWith(/BadOrigin/); + })); }); describe('ReFungible token properties permissions tests', () => { -- gitstuff