From 42f53fc98c25304ad12f62541162f3b4d28383a6 Mon Sep 17 00:00:00 2001 From: ut-akuznetsov <59873862+ut-akuznetsov@users.noreply.github.com> Date: Wed, 14 Dec 2022 17:23:37 +0000 Subject: [PATCH] Merge pull request #773 from UniqueNetwork/fix/properties-size-overflow Fix/properties size overflow --- --- 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 @@ -1133,7 +1133,7 @@ scope: PropertyScope, key: PropertyKey, value: Self::Value, - ) -> Result<(), PropertiesError>; + ) -> Result, PropertiesError>; /// Try to set property with scope from iterator. fn try_scoped_set_from_iter( @@ -1154,7 +1154,11 @@ } /// Try to set property. - fn try_set(&mut self, key: PropertyKey, value: Self::Value) -> Result<(), PropertiesError> { + fn try_set( + &mut self, + key: PropertyKey, + value: Self::Value, + ) -> Result, PropertiesError> { self.try_scoped_set(PropertyScope::None, key, value) } @@ -1214,6 +1218,10 @@ Ok(()) } + + pub fn values(&self) -> impl Iterator { + self.0.values() + } } impl IntoIterator for PropertiesMap { @@ -1239,15 +1247,13 @@ scope: PropertyScope, key: PropertyKey, value: Self::Value, - ) -> Result<(), PropertiesError> { + ) -> Result, PropertiesError> { Self::check_property_key(&key)?; let key = scope.apply(key)?; self.0 .try_insert(key, value) - .map_err(|_| PropertiesError::PropertyLimitReached)?; - - Ok(()) + .map_err(|_| PropertiesError::PropertyLimitReached) } } @@ -1288,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 { @@ -1307,7 +1319,7 @@ scope: PropertyScope, key: PropertyKey, value: Self::Value, - ) -> Result<(), PropertiesError> { + ) -> Result, PropertiesError> { let value_len = value.len(); if self.consumed_space as usize + value_len > self.space_limit as usize @@ -1316,11 +1328,13 @@ return Err(PropertiesError::NoSpaceForProperty); } - self.map.try_scoped_set(scope, key, value)?; + let old_value = self.map.try_scoped_set(scope, key, value)?; - self.consumed_space += value_len as u32; + if old_value.is_none() { + self.consumed_space += value_len as u32; + } - Ok(()) + Ok(old_value) } } --- 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/package.json +++ b/tests/package.json @@ -41,11 +41,10 @@ "testEvent": "yarn setup && mocha --timeout 9999999 -r ts-node/register ./src/check-event/*.*test.ts", "testRmrk": "yarn setup && mocha --timeout 9999999 -r ts-node/register ./**/rmrk/*.*test.ts", "testEthPayable": "mocha --timeout 9999999 -r ts-node/register './**/eth/payable.test.ts'", - "testEthTokenProperties": "mocha --timeout 9999999 -r ts-node/register ./**/eth/tokenProperties.test.ts", "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 ./**/properties.test.ts ./**/getPropertiesRpc.test.ts", + "testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/collectionProperties.test.ts ./**/tokenProperties.test.ts ./**/getPropertiesRpc.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", --- a/tests/src/nesting/collectionProperties.test.ts +++ b/tests/src/nesting/collectionProperties.test.ts @@ -25,7 +25,7 @@ before(async () => { await usingPlaygrounds(async (helper, privateKey) => { const donor = await privateKey({filename: __filename}); - [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor); + [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor); }); }); @@ -131,6 +131,66 @@ itSub.ifWithPallets('Deletes properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => { await testDeleteProperties(await helper.rft.mintCollection(alice)); }); + + [ + // TODO enable properties for FT collection in Substrate (release 040) + // {mode: 'ft' as const, requiredPallets: []}, + {mode: 'nft' as const, requiredPallets: []}, + {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, + ].map(testCase => + itSub.ifWithPallets(`Allows modifying a collection property multiple times (${testCase.mode})`, testCase.requiredPallets, async({helper}) => { + const propKey = 'tok-prop'; + + const collection = await helper[testCase.mode].mintCollection(alice); + + const maxCollectionPropertiesSize = 40960; + + const propDataSize = 4096; + + let propDataChar = 'a'; + const makeNewPropData = () => { + propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1); + return `${propDataChar}`.repeat(propDataSize); + }; + + await collection.setProperties(alice, [{key: propKey, value: makeNewPropData()}]); + const originalSpace = await collection.getPropertiesConsumedSpace(); + expect(originalSpace).to.be.equal(propDataSize); + + const sameSizePropertiesPossibleNum = maxCollectionPropertiesSize / propDataSize; + + // It is possible to modify a property as many times as needed. + // It will not consume any additional space. + for (let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) { + await collection.setProperties(alice, [{key: propKey, value: makeNewPropData()}]); + const consumedSpace = await collection.getPropertiesConsumedSpace(); + expect(consumedSpace).to.be.equal(originalSpace); + } + })); + + [ + // TODO enable properties for FT collection in Substrate (release 040) + // {mode: 'ft' as const, requiredPallets: []}, + {mode: 'nft' as const, requiredPallets: []}, + {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, + ].map(testCase => + itSub.ifWithPallets(`Adding then removing a collection property doesn't change the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => { + const propKey = 'tok-prop'; + + const collection = await helper[testCase.mode].mintCollection(alice); + const originalSpace = await collection.getPropertiesConsumedSpace(); + + const propDataSize = 4096; + const propData = 'a'.repeat(propDataSize); + + await collection.setProperties(alice, [{key: propKey, value: propData}]); + let consumedSpace = await collection.getPropertiesConsumedSpace(); + expect(consumedSpace).to.be.equal(propDataSize); + + await collection.deleteProperties(alice, [propKey]); + consumedSpace = await collection.getPropertiesConsumedSpace(); + expect(consumedSpace).to.be.equal(originalSpace); + })); }); describe('Negative Integration Test: Collection Properties', () => { --- 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 = [ @@ -320,6 +320,120 @@ expect((await nestedToken.getData())!.properties).to.be.empty; expect(await targetToken.getProperties()).to.be.empty; }); + + [ + {mode: 'nft' as const, storage: 'nonfungible' as const, pieces: undefined, requiredPallets: []}, + {mode: 'rft' as const, storage: 'refungible' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]}, + ].map(testCase => + itSub.ifWithPallets(`Allows modifying a token property multiple times (${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 maxTokenPropertiesSize = 32768; + + const propDataSize = 4096; + + let propDataChar = 'a'; + const makeNewPropData = () => { + propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1); + return `${propDataChar}`.repeat(propDataSize); + }; + + const token = await ( + testCase.pieces + ? collection.mintToken(alice, testCase.pieces) + : collection.mintToken(alice) + ); + + await token.setProperties(alice, [{key: propKey, value: makeNewPropData()}]); + const originalSpace = await token.getTokenPropertiesConsumedSpace(); + expect(originalSpace).to.be.equal(propDataSize); + + const sameSizePropertiesPossibleNum = maxTokenPropertiesSize / propDataSize; + + // It is possible to modify a property as many times as needed. + // It will not consume any additional space. + for (let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) { + await token.setProperties(alice, [{key: propKey, value: makeNewPropData()}]); + const 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(`Adding then removing a token property doesn't change the 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 originalSpace = await token.getTokenPropertiesConsumedSpace(); + + const propDataSize = 4096; + const propData = 'a'.repeat(propDataSize); + + await token.setProperties(alice, [{key: propKey, value: propData}]); + let consumedSpace = await token.getTokenPropertiesConsumedSpace(); + expect(consumedSpace).to.be.equal(propDataSize); + + await token.deleteProperties(alice, [propKey]); + 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', () => { @@ -476,7 +590,7 @@ await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount); }); - async function testForbidsAddingTooManyProperties(token: UniqueNFToken | UniqueRFToken, pieces: bigint) { + async function testForbidsAddingTooLargeProperties(token: UniqueNFToken | UniqueRFToken, pieces: bigint) { const originalSpace = await prepare(token, pieces); await expect( @@ -504,15 +618,36 @@ expect(consumedSpace).to.be.equal(originalSpace); } - itSub('Forbids adding too many properties to a token (NFT)', async ({helper}) => { + itSub('Forbids adding too large properties to a token (NFT)', async ({helper}) => { const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT'); - await testForbidsAddingTooManyProperties(token, amount); + await testForbidsAddingTooLargeProperties(token, amount); }); - itSub.ifWithPallets('Forbids adding too many properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => { + itSub.ifWithPallets('Forbids adding too large properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => { const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT'); - await testForbidsAddingTooManyProperties(token, amount); + await testForbidsAddingTooLargeProperties(token, amount); }); + + [ + {mode: 'nft' as const, requiredPallets: []}, + {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]}, + ].map(testCase => + itSub.ifWithPallets(`Forbids adding too many propeties to a token (${testCase.mode})`, testCase.requiredPallets, async({helper}) => { + const collection = await helper[testCase.mode].mintCollection(alice); + const maxPropertiesPerItem = 64; + + for (let i = 0; i < maxPropertiesPerItem; i++) { + await collection.setTokenPropertyPermissions(alice, [{ + key: `${i+1}`, + permission: {mutable: true, tokenOwner: true, collectionAdmin: true}, + }]); + } + + await expect(collection.setTokenPropertyPermissions(alice, [{ + key: `${maxPropertiesPerItem}-th`, + permission: {mutable: true, tokenOwner: true, collectionAdmin: true}, + }])).to.be.rejectedWith(/common\.PropertyLimitReached/); + })); }); describe('ReFungible token properties permissions tests', () => { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -1100,6 +1100,13 @@ return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, propertyKeys])).toHuman(); } + async getPropertiesConsumedSpace(collectionId: number): Promise { + const api = this.helper.getApi(); + const props = (await api.query.common.collectionProperties(collectionId)).toJSON(); + + return (props! as any).consumedSpace; + } + async getCollectionOptions(collectionId: number) { return (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman(); } @@ -3035,6 +3042,10 @@ return await this.helper.collection.getProperties(this.collectionId, propertyKeys); } + async getPropertiesConsumedSpace() { + return await this.helper.collection.getPropertiesConsumedSpace(this.collectionId); + } + async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) { return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj); } @@ -3158,6 +3169,13 @@ return await this.helper.nft.getTokenProperties(this.collectionId, tokenId, propertyKeys); } + async getTokenPropertiesConsumedSpace(tokenId: number): Promise { + const api = this.helper.getApi(); + const props = (await api.query.nonfungible.tokenProperties(this.collectionId, tokenId)).toJSON(); + + return (props! as any).consumedSpace; + } + async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) { return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj); } @@ -3269,6 +3287,13 @@ return await this.helper.rft.getTokenProperties(this.collectionId, tokenId, propertyKeys); } + async getTokenPropertiesConsumedSpace(tokenId: number): Promise { + const api = this.helper.getApi(); + const props = (await api.query.refungible.tokenProperties(this.collectionId, tokenId)).toJSON(); + + return (props! as any).consumedSpace; + } + async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=1n) { return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount); } @@ -3421,6 +3446,10 @@ return await this.collection.getTokenProperties(this.tokenId, propertyKeys); } + async getTokenPropertiesConsumedSpace() { + return await this.collection.getTokenPropertiesConsumedSpace(this.tokenId); + } + async setProperties(signer: TSigner, properties: IProperty[]) { return await this.collection.setTokenProperties(signer, this.tokenId, properties); } -- gitstuff