Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions js-packages/playgrounds/unique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,14 @@ class CollectionGroup extends HelperGroup<UniqueHelper> {
async doesTokenExist(collectionId: number, tokenId: number): Promise<boolean> {
return (await this.helper.callRpc('api.rpc.unique.tokenExists', [collectionId, tokenId])).toJSON();
}

async upgradeTokensPropertiesLimit(signer: IKeyringPair, collectionId: number, newLimit: 'Default' | 'Extended' | 'Max') {
return await this.helper.executeExtrinsic(signer, 'api.tx.unique.upgradeTokensPropertiesLimit', [collectionId, newLimit]);
}

async getTokensPropertiesLimit(collectionId: number): Promise<number> {
return (await this.helper.callRpc('api.query.common.collectionTokenPropertiesLimit', [collectionId])).toJSON();
}
}

class NFTnRFT extends CollectionGroup {
Expand Down Expand Up @@ -3172,6 +3180,14 @@ export class UniqueBaseCollection {
async burn(signer: TSigner) {
return await this.helper.collection.burn(signer, this.collectionId);
}

async upgradeTokensPropertiesLimit(signer: TSigner, newLimit: 'Default' | 'Extended' | 'Max') {
return await this.helper.collection.upgradeTokensPropertiesLimit(signer, this.collectionId, newLimit);
}

async getTokensPropertiesLimit() {
return await this.helper.collection.getTokensPropertiesLimit(this.collectionId);
}
}

export class UniqueNFTCollection extends UniqueBaseCollection {
Expand Down
1 change: 1 addition & 0 deletions js-packages/test-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@ export class ArrangeGroup {
return false;
};

// TODO fix the name. It should be `calculateFee`
async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {
const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);
let balance = await this.helper.balance.getSubstrate(address);
Expand Down
2 changes: 1 addition & 1 deletion js-packages/tests/apiConsts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const MAX_TOKEN_PREFIX_LENGTH = 16n;
const MAX_PROPERTY_KEY_LENGTH = 256n;
const MAX_PROPERTY_VALUE_LENGTH = 32768n;
const MAX_PROPERTIES_PER_ITEM = 64n;
const MAX_TOKEN_PROPERTIES_SIZE = 32768n;
const MAX_TOKEN_PROPERTIES_SIZE = 65536n;
const NESTING_BUDGET = 5n;

const DEFAULT_COLLETCTION_LIMIT = {
Expand Down
173 changes: 166 additions & 7 deletions js-packages/tests/sub/nesting/tokenProperties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

import type {IKeyringPair} from '@polkadot/types/types';
import {before, describe, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect, sizeOfProperty} from '@unique/test-utils/util';
import {UniqueHelper, UniqueNFToken, UniqueRFToken} from '@unique-nft/playgrounds/unique';
import {DevUniqueHelper} from '@unique/test-utils';
import {UniqueHelper, UniqueNFToken, UniqueRFToken, UniqueBaseCollection} from '@unique-nft/playgrounds/unique';
import {IProperty} from '@unique-nft/playgrounds/types';

describe('Integration Test: Token Properties', () => {
let alice: IKeyringPair; // collection owner
Expand All @@ -28,7 +30,7 @@ describe('Integration Test: Token Properties', () => {
before(async () => {
await usingPlaygrounds(async (helper, privateKey) => {
const donor = await privateKey({url: import.meta.url});
[alice, bob, charlie] = await helper.arrange.createAccounts([200n, 100n, 100n], donor);
[alice, bob, charlie] = await helper.arrange.createAccounts([100000n, 100n, 100n], donor);
});

permissions = [
Expand Down Expand Up @@ -337,9 +339,9 @@ describe('Integration Test: Token Properties', () => {
],
});

const maxTokenPropertiesSize = 32768;
const maxTokenPropertiesSize = 8192;

const propDataSize = 4096;
const propDataSize = 1024;

let propDataChar = 'a';
const makeNewPropData = () => {
Expand Down Expand Up @@ -426,9 +428,9 @@ describe('Integration Test: Token Properties', () => {
);
const originalSpace = await token.getTokenPropertiesConsumedSpace();

const initProp = {key: propKey, value: 'a'.repeat(4096)};
const biggerProp = {key: propKey, value: 'b'.repeat(5000)};
const smallerProp = {key: propKey, value: 'c'.repeat(4000)};
const initProp = {key: propKey, value: 'a'.repeat(1024)};
const biggerProp = {key: propKey, value: 'b'.repeat(4096)};
const smallerProp = {key: propKey, value: 'c'.repeat(512)};

let consumedSpace;
let expectedConsumedSpaceDiff;
Expand Down Expand Up @@ -471,6 +473,163 @@ describe('Integration Test: Token Properties', () => {
expect(bobBalanceAfter).to.be.equal(bobBalanceBefore);
expect(aliceBalanceBefore > aliceBalanceAfter).to.be.true;
});

const maxKeySize = 256;

function makeMaxLimitedProperties(limit: number) {
const propertiesNum = 4; // An arbitrary number of props

const encodedKeyMetadataLen = 2; // the max key is 256 bytes, Compcat<u32> will encode it using 2 bytes
const encodedValMetadataLen = 2; // the value will always be less than or equal to 16384 in this case. It is also encoded using 2 bytes
const kvSize = limit / propertiesNum - (encodedKeyMetadataLen + encodedValMetadataLen);
const valueSize = kvSize - maxKeySize;

return [
['a', '0'],
['b', '1'],
['c', '2'],
['d', '3']
].map(([k, v]) => ({ key: k.repeat(maxKeySize), value: v.repeat(valueSize) }));
}

async function testPropLimitUpgrade(helper: DevUniqueHelper, mode: 'NFT' | 'RFT', testSubjectCb: (collectionId: number, props: IProperty[]) => Promise<void>) {
const defaultLimit = 8*1024;
const extendedLimit = 32*1024;
const maxLimit = 64*1024;

const defaultProps = makeMaxLimitedProperties(defaultLimit);
const extendedProps = makeMaxLimitedProperties(extendedLimit);
const maxProps = makeMaxLimitedProperties(maxLimit);

const tokenPropertyPermissions = ['a', 'b', 'c', 'd']
.map(k => ({key: k.repeat(maxKeySize), permission: {mutable: true, tokenOwner: true}}));

let collection: UniqueBaseCollection;

if (mode === 'NFT') {
collection = await helper.nft.mintCollection(alice, {tokenPropertyPermissions});
} else {
collection = await helper.rft.mintCollection(alice, {tokenPropertyPermissions});
}

expect(await collection.getTokensPropertiesLimit()).to.be.equal(defaultLimit);

const collectionId = collection.collectionId;

await expect(testSubjectCb(collectionId, defaultProps)).to.be.fulfilled;
await expect(testSubjectCb(collectionId, extendedProps)).to.be.rejectedWith(/NoSpaceForProperty/);
await expect(testSubjectCb(collectionId, maxProps)).to.be.rejectedWith(/NoSpaceForProperty/);

await expect(collection.upgradeTokensPropertiesLimit(bob, 'Extended')).to.be.rejectedWith(/NoPermission/);

// No-op
await expect(collection.upgradeTokensPropertiesLimit(alice, 'Default')).to.be.fulfilled;
expect(await collection.getTokensPropertiesLimit()).to.be.equal(defaultLimit);

const upgradeToExtendedFee = await helper.arrange.calculcateFee({Substrate: alice.address}, async () => {
await expect(collection.upgradeTokensPropertiesLimit(alice, 'Extended')).to.be.fulfilled;
});
expect(upgradeToExtendedFee > 2000n * helper.balance.getOneTokenNominal()).to.be.true;
expect(await collection.getTokensPropertiesLimit()).to.be.equal(extendedLimit);

await expect(testSubjectCb(collectionId, defaultProps)).to.be.fulfilled;
await expect(testSubjectCb(collectionId, extendedProps)).to.be.fulfilled;
await expect(testSubjectCb(collectionId, maxProps)).to.be.rejectedWith(/NoSpaceForProperty/);

await expect(collection.upgradeTokensPropertiesLimit(alice, 'Default')).to.be.rejectedWith(/CollectionTokensPropertiesLimitDowngrade/);

// No-op
await expect(collection.upgradeTokensPropertiesLimit(alice, 'Extended')).to.be.fulfilled;
expect(await collection.getTokensPropertiesLimit()).to.be.equal(extendedLimit);

const upgradeToMaxFee = await helper.arrange.calculcateFee({Substrate: alice.address}, async () => {
await expect(collection.upgradeTokensPropertiesLimit(alice, 'Max')).to.be.fulfilled;
});
expect(upgradeToMaxFee > 5000n * helper.balance.getOneTokenNominal()).to.be.true;
expect(await collection.getTokensPropertiesLimit()).to.be.equal(maxLimit);

await expect(testSubjectCb(collectionId, defaultProps)).to.be.fulfilled;
await expect(testSubjectCb(collectionId, extendedProps)).to.be.fulfilled;
await expect(testSubjectCb(collectionId, maxProps)).to.be.fulfilled;

await expect(collection.upgradeTokensPropertiesLimit(alice, 'Default')).to.be.rejectedWith(/CollectionTokensPropertiesLimitDowngrade/);
await expect(collection.upgradeTokensPropertiesLimit(alice, 'Extended')).to.be.rejectedWith(/CollectionTokensPropertiesLimitDowngrade/);

// No-op
await expect(collection.upgradeTokensPropertiesLimit(alice, 'Max')).to.be.fulfilled;
expect(await collection.getTokensPropertiesLimit()).to.be.equal(maxLimit);

}

itSub('Upgrade collection tokens property size limit: minting new tokens with properties (NFT)', async({helper}) => {
await testPropLimitUpgrade(helper, 'NFT', async (collectionId, properties) => {
await helper.nft.mintToken(
alice,
{
collectionId,
owner: {Substrate: alice.address},
properties
},
);
});
});

itSub('Upgrade collection tokens property size limit: modifying properties of an existing token (NFT)', async({helper}) => {
let token: UniqueNFToken | null = null;

await testPropLimitUpgrade(helper, 'NFT', async (collectionId, properties) => {
if (!token) {
token = await helper.nft.mintToken(
alice,
{
collectionId,
owner: {Substrate: alice.address},
},
);
}

await token.setProperties(alice, properties);
});
});

itSub.ifWithPallets('Upgrade collection tokens property size limit: minting new tokens with properties (ReFungible)', [Pallets.ReFungible], async({helper}) => {
const pieces = 500n;

await testPropLimitUpgrade(helper, 'RFT', async (collectionId, properties) => {
await helper.rft.mintToken(
alice,
{
collectionId,
pieces,
owner: {Substrate: alice.address},
properties
},
);
});
});

itSub.ifWithPallets('Upgrade collection tokens property size limit: modifying properties of an existing token (RFT)', [Pallets.ReFungible], async({helper}) => {
let token: UniqueRFToken | null = null;

await testPropLimitUpgrade(helper, 'RFT', async (collectionId, properties) => {
if (!token) {
const pieces = 500n;

token = await helper.rft.mintToken(
alice,
{
collectionId,
pieces,
owner: {Substrate: alice.address},
},
);
}

await token.setProperties(alice, properties);
});
});

// TODO test eth-variants in the same manner
});

describe('Negative Integration Test: Token Properties', () => {
Expand Down
16 changes: 16 additions & 0 deletions pallets/balances-adapter/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ impl<T: Config> CommonWeightInfo<T::CrossAccountId> for CommonWeights<T> {
fn force_repair_item() -> Weight {
Weight::default()
}

fn upgrade_tokens_properties_limit() -> Weight {
Weight::default()
}
}

/// Implementation of `CommonCollectionOperations` for `FungibleHandle`. It wraps FungibleHandle Pallet
Expand Down Expand Up @@ -116,6 +120,18 @@ impl<T: Config> CommonCollectionOperations<T> for NativeFungibleHandle<T> {
fail!(<CommonError<T>>::UnsupportedOperation);
}

fn get_tokens_properties_limit(&self) -> u32 {
0
}

fn upgrade_tokens_properties_limit(
&mut self,
_sender: &T::CrossAccountId,
_new_limit: up_data_structs::PropertySizeLimit,
) -> sp_runtime::DispatchResult {
fail!(<CommonError<T>>::UnsupportedOperation);
}

fn set_collection_properties(
&self,
_sender: <T>::CrossAccountId,
Expand Down
Loading
Loading