--- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -1815,6 +1815,9 @@ /// The price of setting approval for all fn set_allowance_for_all() -> Weight; + + /// The price of repairing an item. + fn repair_item() -> Weight; } /// Weight info extension trait for refungible pallet. @@ -2136,6 +2139,9 @@ /// Tells whether the given `owner` approves the `operator`. fn allowance_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool; + + /// Repairs a possibly broken item. + fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo; } /// Extension for RFT collection. --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -111,6 +111,10 @@ fn set_allowance_for_all() -> Weight { Weight::zero() } + + fn repair_item() -> Weight { + Weight::zero() + } } /// Implementation of `CommonCollectionOperations` for `FungibleHandle`. It wraps FungibleHandle Pallete @@ -441,4 +445,9 @@ fn allowance_for_all(&self, _owner: T::CrossAccountId, _operator: T::CrossAccountId) -> bool { false } + + /// Repairs a possibly broken item. + fn repair_item(&self, _token: TokenId) -> DispatchResultWithPostInfo { + fail!(>::FungibleTokensAreAlwaysValid) + } } --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -129,6 +129,8 @@ SettingPropertiesNotAllowed, /// Setting allowance for all is not allowed. SettingAllowanceForAllNotAllowed, + /// Only a fungible collection could be possibly broken; any fungible token is valid. + FungibleTokensAreAlwaysValid, } #[pallet::config] --- a/pallets/nonfungible/src/benchmarking.rs +++ b/pallets/nonfungible/src/benchmarking.rs @@ -236,4 +236,12 @@ operator: cross_sub; }; }: {>::allowance_for_all(&collection, &owner, &operator)} + + repair_item { + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let item = create_max_item(&collection, &owner, owner.clone())?; + }: {>::repair_item(&collection, item)?} } --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -126,6 +126,10 @@ fn set_allowance_for_all() -> Weight { >::set_allowance_for_all() } + + fn repair_item() -> Weight { + >::repair_item() + } } fn map_create_data( @@ -532,4 +536,11 @@ fn allowance_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool { >::allowance_for_all(self, &owner, &operator) } + + fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo { + with_weight( + >::repair_item(self, token), + >::repair_item(), + ) + } } --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -1398,4 +1398,12 @@ ) -> bool { >::get((collection.id, owner, operator)) } + + pub fn repair_item(collection: &NonfungibleHandle, token: TokenId) -> DispatchResult { + >::mutate((collection.id, token), |properties| { + properties.recompute_consumed_space(); + }); + + Ok(()) + } } --- a/pallets/nonfungible/src/weights.rs +++ b/pallets/nonfungible/src/weights.rs @@ -50,6 +50,7 @@ fn token_owner() -> Weight; fn set_allowance_for_all() -> Weight; fn allowance_for_all() -> Weight; + fn repair_item() -> Weight; } /// Weights for pallet_nonfungible using the Substrate node and recommended hardware. @@ -208,6 +209,12 @@ Weight::from_ref_time(6_161_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) } + // Storage: Nonfungible TokenProperties (r:1 w:1) + fn repair_item() -> 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 @@ -365,4 +372,10 @@ Weight::from_ref_time(6_161_000 as u64) .saturating_add(RocksDbWeight::get().reads(1 as u64)) } + // Storage: Nonfungible TokenProperties (r:1 w:1) + fn repair_item() -> 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/pallets/refungible/src/benchmarking.rs +++ b/pallets/refungible/src/benchmarking.rs @@ -304,4 +304,12 @@ operator: cross_sub; }; }: {>::allowance_for_all(&collection, &owner, &operator)} + + repair_item { + bench_init!{ + owner: sub; collection: collection(owner); + owner: cross_from_sub; + }; + let item = create_max_item(&collection, &owner, [(owner.clone(), 100)])?; + }: {>::repair_item(&collection, item)?} } --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -156,6 +156,10 @@ fn set_allowance_for_all() -> Weight { >::set_allowance_for_all() } + + fn repair_item() -> Weight { + >::repair_item() + } } fn map_create_data( @@ -536,6 +540,13 @@ fn allowance_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool { >::allowance_for_all(self, &owner, &operator) } + + fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo { + with_weight( + >::repair_item(self, token), + >::repair_item(), + ) + } } impl RefungibleExtensions for RefungibleHandle { --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -1461,4 +1461,12 @@ ) -> bool { >::get((collection.id, owner, operator)) } + + pub fn repair_item(collection: &RefungibleHandle, token: TokenId) -> DispatchResult { + >::mutate((collection.id, token), |properties| { + properties.recompute_consumed_space(); + }); + + Ok(()) + } } --- a/pallets/refungible/src/weights.rs +++ b/pallets/refungible/src/weights.rs @@ -57,6 +57,7 @@ fn token_owner() -> Weight; fn set_allowance_for_all() -> Weight; fn allowance_for_all() -> Weight; + fn repair_item() -> Weight; } /// Weights for pallet_refungible using the Substrate node and recommended hardware. @@ -272,6 +273,12 @@ Weight::from_ref_time(5_901_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) } + // Storage: Refungible TokenProperties (r:1 w:1) + fn repair_item() -> Weight { + Weight::from_ref_time(5_489_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 @@ -486,4 +493,10 @@ Weight::from_ref_time(5_901_000 as u64) .saturating_add(RocksDbWeight::get().reads(1 as u64)) } + // Storage: Refungible TokenProperties (r:1 w:1) + fn repair_item() -> Weight { + Weight::from_ref_time(5_489_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } } --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -982,6 +982,23 @@ d.set_allowance_for_all(sender, operator, approve) }) } + + /// Repairs a broken item + /// + /// # 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, + collection_id: CollectionId, + item_id: TokenId, + ) -> DispatchResultWithPostInfo { + dispatch_tx::(collection_id, |d| { + d.repair_item(item_id) + }) + } } } --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -1218,6 +1218,10 @@ Ok(()) } + + pub fn values(&self) -> impl Iterator { + self.0.values() + } } impl IntoIterator for PropertiesMap { @@ -1290,6 +1294,12 @@ pub fn get(&self, key: &PropertyKey) -> Option<&PropertyValue> { self.map.get(key) } + + /// Recomputes the consumed space for the current properties state. + /// Needed to repair a token due to a bug fixed in the [PR #733](https://github.com/UniqueNetwork/unique-chain/pull/773). + pub fn recompute_consumed_space(&mut self) { + self.consumed_space = self.map.values().map(|value| value.len() as u32).sum(); + } } impl IntoIterator for Properties { --- a/runtime/common/weights.rs +++ b/runtime/common/weights.rs @@ -124,6 +124,10 @@ fn set_allowance_for_all() -> Weight { max_weight_of!(set_allowance_for_all()) } + + fn repair_item() -> Weight { + max_weight_of!(repair_item()) + } } #[cfg(feature = "refungible")] --- a/tests/src/nesting/tokenProperties.test.ts +++ b/tests/src/nesting/tokenProperties.test.ts @@ -28,7 +28,7 @@ before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor); + [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 100n, 100n], donor); }); permissions = [ @@ -401,6 +401,39 @@ consumedSpace = await token.getTokenPropertiesConsumedSpace(); expect(consumedSpace).to.be.equal(originalSpace); })); + + [ + {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); + })); }); describe('Negative Integration Test: Token Properties', () => {