difftreelog
test(structure) refactor properties and nesting
in: master
7 files changed
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -33,7 +33,9 @@
"loadTransfer": "ts-node src/transfer.nload.ts",
"testCollision": "mocha --timeout 9999999 -r ts-node/register ./src/collision-tests/*.test.ts",
"testEvent": "mocha --timeout 9999999 -r ts-node/register ./src/check-event/*.test.ts",
- "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/**.test.ts",
+ "testNesting": "mocha --timeout 9999999 -r ts-node/register ./**/nest.test.ts",
+ "testUnnesting": "mocha --timeout 9999999 -r ts-node/register ./**/unnest.test.ts",
+ "testStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/**.test.ts",
"testProperties": "mocha --timeout 9999999 -r ts-node/register ./**/properties.test.ts",
"testMigrationStructure": "mocha --timeout 9999999 -r ts-node/register ./**/nesting/migration-check.test.ts",
"testAddCollectionAdmin": "mocha --timeout 9999999 -r ts-node/register ./**/addCollectionAdmin.test.ts",
tests/src/nesting/graphs.test.tsdiffbeforeafterboth--- a/tests/src/nesting/graphs.test.ts
+++ b/tests/src/nesting/graphs.test.ts
@@ -14,7 +14,7 @@
* ```
*/
async function buildComplexObjectGraph(api: ApiPromise, sender: IKeyringPair): Promise<number> {
- const events = await executeTransaction(api, sender, api.tx.unique.createCollectionEx({mode: 'NFT'}));
+ const events = await executeTransaction(api, sender, api.tx.unique.createCollectionEx({mode: 'NFT', limits: {nestingRule: 'Owner'}}));
const {collectionId} = getCreateCollectionResult(events);
await executeTransaction(api, sender, api.tx.unique.createMultipleItemsEx(collectionId, {NFT: Array(8).fill({owner: {Substrate: sender.address}})}));
@@ -32,20 +32,26 @@
return collectionId;
}
-describe('graphs', () => {
- it('ouroboros can\'t be created in graph', async () => {
+describe('Graphs', () => {
+ it('Ouroboros can\'t be created in a complex graph', async () => {
await usingApi(async api => {
const alice = privateKey('//Alice');
const collection = await buildComplexObjectGraph(api, alice);
// to self
- await expect(executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 1), collection, 1, 1)))
- .to.be.rejectedWith(/structure\.OuroborosDetected/);
+ await expect(
+ executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 1), collection, 1, 1)),
+ 'first transaction',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
// to nested part of graph
- await expect(executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 5), collection, 1, 1)))
- .to.be.rejectedWith(/structure\.OuroborosDetected/);
- await expect(executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 8), collection, 2, 1)))
- .to.be.rejectedWith(/structure\.OuroborosDetected/);
+ await expect(
+ executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 5), collection, 1, 1)),
+ 'second transaction',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+ await expect(
+ executeTransaction(api, alice, api.tx.unique.transfer(tokenIdToCross(collection, 8), collection, 2, 1)),
+ 'third transaction',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
});
});
});
tests/src/nesting/migration-check.test.tsdiffbeforeafterboth--- a/tests/src/nesting/migration-check.test.ts
+++ b/tests/src/nesting/migration-check.test.ts
@@ -18,7 +18,7 @@
});
});
- it('Preserves collection settings', async () => {
+ it('Preserves collection settings after migration', async () => {
let oldVersion: number;
let collectionId: number;
let collectionOld: any;
@@ -71,6 +71,7 @@
});
} catch (_) {
connectionFailCounter++;
+ console.log(`Still waiting for the parachain upgrade from ${oldVersion!}...`);
await new Promise(resolve => setTimeout(resolve, 12000));
}
}
tests/src/nesting/nest.test.tsdiffbeforeafterboth--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -5,12 +5,12 @@
import {
addToAllowListExpectSuccess,
createCollectionExpectSuccess,
- createItemExpectFailure,
createItemExpectSuccess,
enableAllowListExpectSuccess,
enablePublicMintingExpectSuccess,
getTokenOwner,
getTopmostTokenOwner,
+ normalizeAccountId,
setCollectionLimitsExpectSuccess,
transferExpectFailure,
transferExpectSuccess,
@@ -29,11 +29,7 @@
});
});
- // ---------- Non-Fungible ----------
-
- // todo refactor names
- // todo remove excessive bundling, leave it for a single test
- it('NFT: allows to nest/unnest token if nesting rule is Owner', async () => {
+ it('Performs the full suite: bundles a token, transfers, and allows to unnest', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
@@ -49,8 +45,8 @@
// Nest
await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
- expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
- expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+ expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
+ expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
// Move bundle to different user
await transferExpectSuccess(collection, targetToken, alice, {Substrate: bob.address});
@@ -63,10 +59,12 @@
});
});
- it('NFT: allows to nest/unnest token if Owner-Restricted', async () => {
+ // ---------- Non-Fungible ----------
+
+ it('NFT: allows an Owner to nest/unnest their token', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
- await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[collection]}});
+ await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
// Create a nested token
@@ -74,28 +72,36 @@
expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
- // Create a token to be nested
+ // Create a token to be nested and nest
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
- // Nest
await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+ expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
+ expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
+ });
+ });
+
+ it('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async () => {
+ await usingApi(async api => {
+ const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+ await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[collection]}});
+ const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+
+ // Create a nested token
+ const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
- // Move bundle to different user
- await transferExpectSuccess(collection, targetToken, alice, {Substrate: bob.address});
- expect(await getTopmostTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: bob.address});
- expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
-
- // Unnest
- await transferFromExpectSuccess(collection, newToken, bob, {Ethereum: tokenIdToAddress(collection, targetToken)}, {Substrate: bob.address});
- expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: bob.address});
+ // Create a token to be nested and nest
+ const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
+ await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+ expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
+ expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
});
});
// ---------- Fungible ----------
- it('Fungible: allows to nest/unnest token if Owner', async () => {
+ it('Fungible: allows an Owner to nest/unnest their token', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -111,18 +117,13 @@
{Fungible: {Value: 10}},
))).to.not.be.rejected;
- // Create a token to be nested
+ // Nest a new token
const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
- // Nest
await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible');
- // Move bundle to different user
- await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
- // Unnest
- await transferFromExpectSuccess(collectionFT, newToken, bob, targetAddress, {Substrate: bob.address}, 1, 'Fungible');
});
});
- it('Fungible: allows to nest/unnest token if Owner-Restricted', async () => {
+ it('Fungible: allows an Owner to nest/unnest their token (Restricted nesting)', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address});
@@ -139,20 +140,15 @@
{Fungible: {Value: 10}},
))).to.not.be.rejected;
- // Create a token to be nested
+ // Nest a new token
const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
- // Nest
await transferExpectSuccess(collectionFT, newToken, alice, targetAddress, 1, 'Fungible');
- // Move bundle to different user
- await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
- // Unnest
- await transferFromExpectSuccess(collectionFT, newToken, bob, targetAddress, {Substrate: bob.address}, 1, 'Fungible');
});
});
// ---------- Re-Fungible ----------
- it('ReFungible: allows to nest/unnest token if Owner', async () => {
+ it('ReFungible: allows an Owner to nest/unnest their token', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -165,21 +161,16 @@
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
collectionRFT,
targetAddress,
- {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+ {ReFungible: {const_data: [], pieces: 100}},
))).to.not.be.rejected;
- // Create a token to be nested
+ // Nest a new token
const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
- // Nest
await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible');
- // Move bundle to different user
- await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
- // Unnest
- await transferFromExpectSuccess(collectionRFT, newToken, bob, targetAddress, {Substrate: bob.address}, 100, 'ReFungible');
});
});
- it('ReFungible: allows to nest/unnest token if Owner-Restricted', async () => {
+ it('ReFungible: allows an Owner to nest/unnest their token (Restricted nesting)', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
const targetToken = await createItemExpectSuccess(alice, collectionNFT, 'NFT', {Substrate: alice.address});
@@ -193,17 +184,12 @@
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
collectionRFT,
targetAddress,
- {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+ {ReFungible: {const_data: [], pieces: 100}},
))).to.not.be.rejected;
- // Create a token to be nested
+ // Nest a new token
const newToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
- // Nest
await transferExpectSuccess(collectionRFT, newToken, alice, targetAddress, 100, 'ReFungible');
- // Move bundle to different user
- await transferExpectSuccess(collectionNFT, targetToken, alice, {Substrate: bob.address});
- // Unnest
- await transferFromExpectSuccess(collectionRFT, newToken, bob, targetAddress, {Substrate: bob.address}, 100, 'ReFungible');
});
});
});
@@ -216,6 +202,29 @@
});
});
+ it('Disallows excessive token nesting', async () => {
+ await usingApi(async api => {
+ const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+ await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
+ const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+
+ // Create a nested-token matryoshka
+ const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+ const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
+ // The nesting depth is limited by 2
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collection,
+ {Ethereum: tokenIdToAddress(collection, nestedToken2)},
+ {nft: {const_data: [], variable_data: []}} as any,
+ ),
+ ), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/); // OuroborosDetected?
+
+ expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
+ });
+ });
+
// ---------- Non-Fungible ----------
it('NFT: disallows to nest token if nesting is disabled', async () => {
@@ -225,22 +234,30 @@
const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
// Try to create a nested token
- await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collection,
- {Ethereum: tokenIdToAddress(collection, targetToken)},
- {nft: {const_data: [], variable_data: []}} as any,
- ))).to.be.rejectedWith(/^common\.NestingIsDisabled$/);
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
+ {nft: {const_data: [], variable_data: []}} as any,
+ ),
+ ), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
// Create a token to be nested
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
// Try to nest
- await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)}); // todo to.be.rejected
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.transfer(
+ normalizeAccountId({Ethereum: tokenIdToAddress(collection, targetToken)}), collection, newToken, 1,
+ ),
+ ), 'while nesting new token').to.be.rejectedWith(/common\.NestingIsDisabled/); // todo to.be.rejected for all
expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
});
});
- it('NFT: disallows to nest token if not Owner', async () => {
+ it('NFT: disallows a non-Owner to nest someone else\'s token', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
@@ -253,16 +270,28 @@
const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
// Try to create a nested token in the wrong collection
- await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
+ {nft: {const_data: [], variable_data: []}} as any,
+ ),
+ ), 'while creating nested token').to.be.rejectedWith(/structure\.DepthLimit/); // todo NestingIsDisabled?
// Try to create and nest a token in the wrong collection
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
- await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.transfer(
+ normalizeAccountId({Ethereum: tokenIdToAddress(collection, targetToken)}), collection, newToken, 1,
+ ),
+ ), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
});
});
- it('NFT: disallows to nest token if not Owner (Restricted nesting)', async () => {
+ it('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[collection]}});
@@ -275,16 +304,28 @@
const targetToken = await createItemExpectSuccess(bob, collection, 'NFT');
// Try to create a nested token in the wrong collection
- await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
+ {nft: {const_data: [], variable_data: []}} as any,
+ ),
+ ), 'while creating nested token').to.be.rejectedWith(/structure\.DepthLimit/);
// Try to create and nest a token in the wrong collection
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
- await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.transfer(
+ normalizeAccountId({Ethereum: tokenIdToAddress(collection, targetToken)}), collection, newToken, 1,
+ ),
+ ), 'while nesting new token').to.be.rejectedWith(/common\.AddressNotInAllowlist/);
expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
});
});
- it('NFT: disallows to nest token to an unlisted collection', async () => {
+ it('NFT: disallows to nest token in an unlisted collection', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: {OwnerRestricted:[]}});
@@ -293,11 +334,23 @@
const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
// Try to create a nested token in the wrong collection
- await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collection,
+ {Ethereum: tokenIdToAddress(collection, targetToken)},
+ {nft: {const_data: [], variable_data: []}} as any,
+ ),
+ ), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
// Try to create and nest a token in the wrong collection
const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
- await transferExpectFailure(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.transfer(
+ normalizeAccountId({Ethereum: tokenIdToAddress(collection, targetToken)}), collection, newToken, 1,
+ ),
+ ), 'while nesting new token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
expect(await getTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
});
});
@@ -314,20 +367,28 @@
const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
// Try to create a nested token
- await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
- {Fungible: {Value: 10}},
- ))).to.be.rejectedWith(/^common\.NestingIsDisabled$/);
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collectionFT,
+ targetAddress,
+ {Fungible: {Value: 10}},
+ )
+ ), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
// Create a token to be nested
const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
// Try to nest
- await transferExpectFailure(collectionFT, newToken, alice, targetAddress, 1);
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.transfer(
+ normalizeAccountId({Ethereum: tokenIdToAddress(collectionFT, targetToken)}), collectionFT, newToken, 1,
+ ),
+ ), 'while nesting new token').to.be.rejectedWith(/fungible\.FungibleDisallowsNesting/);
});
});
- it('Fungible: disallows to nest token if not Owner', async () => {
+ it('Fungible: disallows a non-Owner to nest someone else\'s token', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -343,22 +404,30 @@
const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
// Try to create a nested token in the wrong collection
- await expect(executeTransaction(api, alice, api.tx.unique.createItem(
- collectionFT,
- targetAddress,
- {Fungible: {Value: 10}},
- ))).to.be.rejected;
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.createItem(
+ collectionFT,
+ targetAddress,
+ {Fungible: {Value: 10}},
+ )
+ ), 'while creating nested token').to.be.rejectedWith(/structure\.DepthLimit/);
// Try to create and nest a token in the wrong collection
const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
- await transferExpectFailure(collectionFT, newToken, alice, targetAddress, 1);
+ await expect(executeTransaction(
+ api, alice,
+ api.tx.unique.transfer(
+ normalizeAccountId({Ethereum: tokenIdToAddress(collectionFT, targetToken)}), collectionFT, newToken, 1,
+ ),
+ ), 'while nesting new token').to.be.rejectedWith(/fungible\.FungibleDisallowsNesting/);
});
});
- it('Fungible: disallows to nest token if not Owner (Restricted nesting)', async () => {
+ it('Fungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
- await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[collectionNFT]}}); // todo clear redundant restrictions
+ await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[collectionNFT]}}); // todo clear redundant restrictions?
await addToAllowListExpectSuccess(alice, collectionNFT, bob.address);
await enableAllowListExpectSuccess(alice, collectionNFT);
@@ -383,7 +452,7 @@
});
});
- it('Fungible: disallows to nest token to an unlisted collection', async () => {
+ it('Fungible: disallows to nest token in an unlisted collection', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[]}});
@@ -422,7 +491,7 @@
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
collectionRFT,
targetAddress,
- {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+ {ReFungible: {const_data: [], pieces: 100}},
))).to.be.rejectedWith(/^common\.NestingIsDisabled$/);
// Create a token to be nested
@@ -432,7 +501,7 @@
});
});
- it('ReFungible: disallows to nest token if not Owner', async () => {
+ it('ReFungible: disallows a non-Owner to nest someone else\'s token', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: 'Owner'});
@@ -451,7 +520,7 @@
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
collectionRFT,
targetAddress,
- {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+ {ReFungible: {const_data: [], pieces: 100}},
))).to.be.rejected;
// Try to create and nest a token in the wrong collection
@@ -460,7 +529,7 @@
});
});
- it('ReFungible: disallows to nest token if not Owner (Restricted nesting)', async () => {
+ it('ReFungible: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async () => {
await usingApi(async api => {
const collectionNFT = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collectionNFT, {nestingRule: {OwnerRestricted:[collectionNFT]}});
@@ -479,7 +548,7 @@
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
collectionRFT,
targetAddress,
- {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+ {ReFungible: {const_data: [], pieces: 100}},
))).to.be.rejected;
// Try to create and nest a token in the wrong collection
@@ -503,7 +572,7 @@
await expect(executeTransaction(api, alice, api.tx.unique.createItem(
collectionRFT,
targetAddress,
- {ReFungible: {const_data: [], variable_data: [], pieces: 100}},
+ {ReFungible: {const_data: [], pieces: 100}},
))).to.be.rejected;
// Try to create and nest a token in the wrong collection
tests/src/nesting/properties.test.tsdiffbeforeafterboth1import {expect} from 'chai';2import privateKey from '../substrate/privateKey';3import usingApi, {executeTransaction} from '../substrate/substrate-api';4import {5 addCollectionAdminExpectSuccess,6 createCollectionExpectSuccess,7 createItemExpectSuccess,8 getCreateCollectionResult,9 transferExpectSuccess,10} from '../util/helpers';11import {IKeyringPair} from '@polkadot/types/types';1213let alice: IKeyringPair;14let bob: IKeyringPair;15let charlie: IKeyringPair;1617// ---------- COLLECTION PROPERTIES1819describe('Integration Test: Collection Properties', () => {20 before(async () => {21 await usingApi(async api => {22 alice = privateKey('//Alice');23 bob = privateKey('//Bob');24 });25 });2627 it('Reads properties from a collection', async () => {28 await usingApi(async api => {29 const collection = await createCollectionExpectSuccess();30 const properties = (await api.query.common.collectionProperties(collection)).toJSON();31 expect(properties.map).to.be.empty;32 expect(properties.consumedSpace).to.equal(0);33 });34 });3536 it('Sets properties for a collection', async () => {37 await usingApi(async api => {38 const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));39 const {collectionId} = getCreateCollectionResult(events);4041 // As owner42 await expect(executeTransaction(43 api, 44 bob, 45 api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 46 )).to.not.be.rejected;4748 await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);4950 // As administrator51 await expect(executeTransaction(52 api, 53 alice, 54 api.tx.unique.setCollectionProperties(collectionId, [{key: 'black hole'}]), 55 )).to.not.be.rejected;5657 const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black hole'])).toJSON();58 expect(properties).to.be.deep.equal([59 {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('come bond').toString('hex')}`},60 {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('').toString('hex')}`},61 ]);62 });63 });6465 it('Changes properties of a collection', async () => {66 await usingApi(async api => {67 const collection = await createCollectionExpectSuccess();6869 await expect(executeTransaction(70 api, 71 alice, 72 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole'}]), 73 )).to.not.be.rejected;7475 // Mutate the properties76 await expect(executeTransaction(77 api, 78 alice, 79 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black hole', value: 'LIGO'}]), 80 )).to.not.be.rejected;8182 const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();83 expect(properties).to.be.deep.equal([84 {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('bonded').toString('hex')}`},85 {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`},86 ]);87 });88 });8990 it('Deletes properties of a collection', async () => {91 await usingApi(async api => {92 const collection = await createCollectionExpectSuccess();9394 await expect(executeTransaction(95 api, 96 alice, 97 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), 98 )).to.not.be.rejected;99100 await expect(executeTransaction(101 api, 102 alice, 103 api.tx.unique.deleteCollectionProperties(collection, ['electron']), 104 )).to.not.be.rejected;105106 const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();107 expect(properties).to.be.deep.equal([108 {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`},109 ]);110 });111 });112});113114describe('Negative Integration Test: Collection Properties', () => {115 before(async () => {116 await usingApi(async api => {117 alice = privateKey('//Alice');118 bob = privateKey('//Bob');119 });120 });121 122 it('Fails to set properties in a collection if not its onwer/administrator', async () => {123 await usingApi(async api => {124 const collection = await createCollectionExpectSuccess();125126 await expect(executeTransaction(127 api, 128 bob, 129 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), 130 )).to.be.rejectedWith(/common\.NoPermission/);131 132 const properties = (await api.query.common.collectionProperties(collection)).toJSON();133 expect(properties.map).to.be.empty;134 expect(properties.consumedSpace).to.equal(0);135 });136 });137 138 it('Fails to set properties that exceed the limits', async () => {139 await usingApi(async api => {140 const collection = await createCollectionExpectSuccess();141 const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 142143 // Mute the general tx parsing error, too many bytes to process144 {145 console.error = () => {};146 await expect(executeTransaction(147 api, 148 alice, 149 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 150 )).to.be.rejected;151 }152153 let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();154 expect(properties).to.be.empty;155156 await expect(executeTransaction(157 api, 158 alice, 159 api.tx.unique.setCollectionProperties(collection, [160 {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 161 {key: 'black hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 162 ]), 163 )).to.be.rejectedWith(/common\.NoSpaceForProperty/);164165 properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();166 expect(properties).to.be.empty;167 });168 });169 170 it('Fails to set more properties than it is allowed', async () => {171 await usingApi(async api => {172 const collection = await createCollectionExpectSuccess();173174 const propertiesToBeSet = [];175 for (let i = 0; i < 65; i++) {176 propertiesToBeSet.push({177 key: 'electron ' + i,178 value: Math.random() > 0.5 ? 'high' : 'low',179 });180 }181182 await expect(executeTransaction(183 api, 184 alice, 185 api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 186 )).to.be.rejectedWith(/common\.PropertyLimitReached/);187188 const properties = (await api.query.common.collectionProperties(collection)).toJSON();189 expect(properties.map).to.be.empty;190 expect(properties.consumedSpace).to.equal(0);191 });192 });193});194195// ---------- ACCESS RIGHTS196197describe('Integration Test: Access Rights to Token Properties', () => {198 before(async () => {199 await usingApi(async api => {200 alice = privateKey('//Alice');201 bob = privateKey('//Bob');202 });203 });204 205 it('Reads access rights to properties of a collection', async () => {206 await usingApi(async api => {207 const collection = await createCollectionExpectSuccess();208 const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();209 expect(propertyRights).to.be.empty;210 });211 });212 213 it('Sets access rights to properties of a collection', async () => {214 await usingApi(async api => {215 const collection = await createCollectionExpectSuccess();216217 await expect(executeTransaction(218 api, 219 alice, 220 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 221 )).to.not.be.rejected;222223 await addCollectionAdminExpectSuccess(alice, collection, bob.address);224225 await expect(executeTransaction(226 api, 227 alice, 228 api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 229 )).to.not.be.rejected;230231 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toJSON();232 expect(propertyRights).to.be.deep.equal([233 {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},234 {key: `0x${Buffer.from('mindgame').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},235 ]);236 });237 });238 239 it('Changes access rights to properties of a collection', async () => {240 await usingApi(async api => {241 const collection = await createCollectionExpectSuccess();242243 await expect(executeTransaction(244 api, 245 alice, 246 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 247 )).to.not.be.rejected;248249 await expect(executeTransaction(250 api, 251 alice, 252 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 253 )).to.not.be.rejected;254255 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();256 expect(propertyRights).to.be.deep.equal([257 {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},258 ]);259 });260 });261});262263describe('Negative Integration Test: Access Rights to Token Properties', () => {264 before(async () => {265 await usingApi(async api => {266 alice = privateKey('//Alice');267 bob = privateKey('//Bob');268 });269 });270271 it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {272 await usingApi(async api => {273 const collection = await createCollectionExpectSuccess();274275 await expect(executeTransaction(276 api, 277 bob, 278 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 279 )).to.be.rejectedWith(/common\.NoPermission/);280281 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();282 expect(propertyRights).to.be.empty;283 });284 });285286 it('Prevents from adding too many possible properties', async () => {287 await usingApi(async api => {288 const collection = await createCollectionExpectSuccess();289290 const constitution = [];291 for (let i = 0; i < 65; i++) {292 constitution.push({293 key: 'property ' + i,294 permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},295 });296 }297298 await expect(executeTransaction(299 api, 300 alice, 301 api.tx.unique.setPropertyPermissions(collection, constitution), 302 )).to.be.rejectedWith(/common\.PropertyLimitReached/);303304 const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();305 expect(propertyRights).to.be.empty;306 });307 });308309 it('Prevents access rights to be modified if constant', async () => {310 await usingApi(async api => {311 const collection = await createCollectionExpectSuccess();312313 await expect(executeTransaction(314 api, 315 alice, 316 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 317 )).to.not.be.rejected;318319 await expect(executeTransaction(320 api, 321 alice, 322 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 323 )).to.be.rejectedWith(/common\.NoPermission/);324325 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();326 expect(propertyRights).to.deep.equal([327 {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},328 ]);329 });330 });331});332333// ---------- TOKEN PROPERTIES334335describe('Integration Test: Token Properties', () => {336 let collection: number;337 let token: number;338 let permissions: {permission: any, signers: IKeyringPair[]}[];339340 before(async () => {341 await usingApi(async api => {342 alice = privateKey('//Alice');343 bob = privateKey('//Bob');344 charlie = privateKey('//Charlie');345346 permissions = [347 {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},348 {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},349 {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},350 {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},351 {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},352 {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},353 ];354 });355 });356357 beforeEach(async () => {358 collection = await createCollectionExpectSuccess();359 token = await createItemExpectSuccess(alice, collection, 'NFT');360 await addCollectionAdminExpectSuccess(alice, collection, bob.address);361 await transferExpectSuccess(collection, token, alice, charlie);362 });363 364 it('Reads properties of a token', async () => {365 await usingApi(async api => {366 const collection = await createCollectionExpectSuccess();367 const token = await createItemExpectSuccess(alice, collection, 'NFT');368 369 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();370 expect(properties.map).to.be.empty;371 expect(properties.consumedSpace).to.be.equal(0);372373 const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;374 expect(tokenData).to.be.empty;375 });376 });377378 it('Assigns properties to a token according to permissions', async () => {379 await usingApi(async api => {380 const propertyKeys: string[] = [];381 let i = 0;382 for (const permission of permissions) {383 for (const signer of permission.signers) {384 const key = i + ' ' + signer.address;385 propertyKeys.push(key);386387 await expect(executeTransaction(388 api, 389 alice, 390 api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 391 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;392393 await expect(executeTransaction(394 api, 395 signer, 396 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 397 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;398 }399400 i++;401 }402403 const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];404 const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];405 for (let i = 0; i < properties.length; i++) {406 expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`);407 expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`);408 }409 });410 });411412 it('Changes properties of a token according to permissions', async () => {413 await usingApi(async api => {414 const propertyKeys: string[] = [];415 let i = 0;416 for (const permission of permissions) {417 if (!permission.permission.mutable) continue;418 419 for (const signer of permission.signers) {420 const key = i + ' ' + signer.address;421 propertyKeys.push(key);422423 await expect(executeTransaction(424 api, 425 alice, 426 api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 427 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;428429 await expect(executeTransaction(430 api, 431 signer, 432 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 433 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;434435 await expect(executeTransaction(436 api, 437 signer, 438 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 439 ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;440 }441442 i++;443 }444445 const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];446 const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];447 for (let i = 0; i < properties.length; i++) {448 expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`);449 expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`);450 }451 });452 });453454 it('Deletes properties of a token according to permissions', async () => {455 await usingApi(async api => {456 const propertyKeys: string[] = [];457 let i = 0;458459 for (const permission of permissions) {460 if (!permission.permission.mutable) continue;461 462 for (const signer of permission.signers) {463 const key = i + ' ' + signer.address;464 propertyKeys.push(key);465466 await expect(executeTransaction(467 api, 468 alice, 469 api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 470 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;471472 await expect(executeTransaction(473 api, 474 signer, 475 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 476 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;477478 await expect(executeTransaction(479 api, 480 signer, 481 api.tx.unique.deleteTokenProperties(collection, token, [key]), 482 ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;483 }484 485 i++;486 }487488 const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];489 expect(properties).to.be.empty;490 const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];491 expect(tokensData).to.be.empty;492 expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);493 });494 });495});496497describe('Negative Integration Test: Token Properties', () => {498 let collection: number;499 let token: number;500 let originalSpace: number;501 let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];502503 before(async () => {504 await usingApi(async api => {505 alice = privateKey('//Alice');506 bob = privateKey('//Bob');507 charlie = privateKey('//Charlie');508 const dave = privateKey('//Dave');509510 constitution = [511 {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},512 {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},513 {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},514 {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},515 {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},516 {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},517 ];518 });519 });520521 beforeEach(async () => {522 collection = await createCollectionExpectSuccess();523 token = await createItemExpectSuccess(alice, collection, 'NFT');524 await addCollectionAdminExpectSuccess(alice, collection, bob.address);525 await transferExpectSuccess(collection, token, alice, charlie);526 527 await usingApi(async api => {528 let i = 0;529 for (const passage of constitution) {530 const signer = passage.signers[0];531 532 await expect(executeTransaction(533 api, 534 alice, 535 api.tx.unique.setPropertyPermissions(collection, [{key: i, permission: passage.permission}]), 536 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;537538 await expect(executeTransaction(539 api, 540 signer, 541 api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin increase'}]), 542 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;543544 i++;545 }546547 originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;548 });549 });550551 it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {552 await usingApi(async api => {553 let i = -1;554 for (const forbiddance of constitution) {555 i++;556 if (!forbiddance.permission.mutable) continue;557558 await expect(executeTransaction(559 api, 560 forbiddance.sinner, 561 api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), 562 ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);563564 await expect(executeTransaction(565 api, 566 forbiddance.sinner, 567 api.tx.unique.deleteTokenProperties(collection, token, [forbiddance.permission]), 568 ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);569 }570571 // todo RPC?572 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();573 expect(properties.consumedSpace).to.be.equal(originalSpace);574 });575 });576577 it('Forbids changing/deleting properties of a token if the property is permanent (constant)', async () => {578 await usingApi(async api => {579 let i = -1;580 for (const permission of constitution) {581 i++;582 if (permission.permission.mutable) continue;583584 await expect(executeTransaction(585 api, 586 permission.signers[0], 587 api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), 588 ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);589590 await expect(executeTransaction(591 api, 592 permission.signers[0], 593 api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 594 ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);595 }596597 // todo RPC?598 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();599 expect(properties.consumedSpace).to.be.equal(originalSpace);600 });601 });602603 it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {604 await usingApi(async api => {605 await expect(executeTransaction(606 api, 607 alice, 608 api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 609 ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);610 611 await expect(executeTransaction(612 api, 613 alice, 614 api.tx.unique.setPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 615 ), 'on setting a new non-permitted property').to.not.be.rejected;616617 await expect(executeTransaction(618 api, 619 alice, 620 api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 621 ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);622623 expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;624 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();625 expect(properties.consumedSpace).to.be.equal(originalSpace);626 });627 });628629 it('Forbids adding too many properties to a token', async () => {630 await usingApi(async api => {631 await expect(executeTransaction(632 api, 633 alice, 634 api.tx.unique.setPropertyPermissions(collection, [635 {key: 'a holy book', permission: {collectionAdmin: true, tokenOwner: true}}, 636 {key: 'young years', permission: {collectionAdmin: true, tokenOwner: true}},637 ]), 638 ), 'on setting a new non-permitted property').to.not.be.rejected;639640 // Mute the general tx parsing error641 {642 console.error = () => {};643 await expect(executeTransaction(644 api, 645 alice, 646 api.tx.unique.setCollectionProperties(collection, [{key: 'a holy book', value: 'word '.repeat(6554)}]), 647 )).to.be.rejected;648 }649650 await expect(executeTransaction(651 api, 652 alice, 653 api.tx.unique.setTokenProperties(collection, token, [654 {key: 'a holy book', value: 'word '.repeat(3277)}, 655 {key: 'young years', value: 'neverending'.repeat(1490)},656 ]), 657 )).to.be.rejectedWith(/common\.NoSpaceForProperty/);658659 expect((await api.rpc.unique.tokenProperties(collection, token, ['a holy book', 'young years'])).toJSON()).to.be.empty;660 const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();661 expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);662 });663 });664});1import {expect} from 'chai';2import privateKey from '../substrate/privateKey';3import usingApi, {executeTransaction} from '../substrate/substrate-api';4import {5 addCollectionAdminExpectSuccess,6 createCollectionExpectSuccess,7 createItemExpectSuccess,8 getCreateCollectionResult,9 transferExpectSuccess,10} from '../util/helpers';11import {IKeyringPair} from '@polkadot/types/types';1213let alice: IKeyringPair;14let bob: IKeyringPair;15let charlie: IKeyringPair;1617// ---------- COLLECTION PROPERTIES1819describe('Integration Test: Collection Properties', () => {20 before(async () => {21 await usingApi(async api => {22 alice = privateKey('//Alice');23 bob = privateKey('//Bob');24 });25 });2627 it('Reads properties from a collection', async () => {28 await usingApi(async api => {29 const collection = await createCollectionExpectSuccess();30 const properties = (await api.query.common.collectionProperties(collection)).toJSON();31 expect(properties.map).to.be.empty;32 expect(properties.consumedSpace).to.equal(0);33 });34 });3536 it('Sets properties for a collection', async () => {37 await usingApi(async api => {38 const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));39 const {collectionId} = getCreateCollectionResult(events);4041 // As owner42 await expect(executeTransaction(43 api, 44 bob, 45 api.tx.unique.setCollectionProperties(collectionId, [{key: 'electron', value: 'come bond'}]), 46 )).to.not.be.rejected;4748 await addCollectionAdminExpectSuccess(bob, collectionId, alice.address);4950 // As administrator51 await expect(executeTransaction(52 api, 53 alice, 54 api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 55 )).to.not.be.rejected;5657 const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman();58 expect(properties).to.be.deep.equal([59 {key: 'electron', value: 'come bond'},60 {key: 'black_hole', value: ''},61 ]);62 });63 });6465 it('Changes properties of a collection', async () => {66 await usingApi(async api => {67 const collection = await createCollectionExpectSuccess();6869 await expect(executeTransaction(70 api, 71 alice, 72 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 73 )).to.not.be.rejected;7475 // Mutate the properties76 await expect(executeTransaction(77 api, 78 alice, 79 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 80 )).to.not.be.rejected;8182 const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();83 expect(properties).to.be.deep.equal([84 {key: 'electron', value: 'bonded'},85 {key: 'black_hole', value: 'LIGO'},86 ]);87 });88 });8990 it('Deletes properties of a collection', async () => {91 await usingApi(async api => {92 const collection = await createCollectionExpectSuccess();9394 await expect(executeTransaction(95 api, 96 alice, 97 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 98 )).to.not.be.rejected;99100 await expect(executeTransaction(101 api, 102 alice, 103 api.tx.unique.deleteCollectionProperties(collection, ['electron']), 104 )).to.not.be.rejected;105106 const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();107 expect(properties).to.be.deep.equal([108 {key: 'black_hole', value: 'LIGO'},109 ]);110 });111 });112});113114describe('Negative Integration Test: Collection Properties', () => {115 before(async () => {116 await usingApi(async api => {117 alice = privateKey('//Alice');118 bob = privateKey('//Bob');119 });120 });121 122 it('Fails to set properties in a collection if not its onwer/administrator', async () => {123 await usingApi(async api => {124 const collection = await createCollectionExpectSuccess();125126 await expect(executeTransaction(127 api, 128 bob, 129 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 130 )).to.be.rejectedWith(/common\.NoPermission/);131 132 const properties = (await api.query.common.collectionProperties(collection)).toJSON();133 expect(properties.map).to.be.empty;134 expect(properties.consumedSpace).to.equal(0);135 });136 });137 138 it('Fails to set properties that exceed the limits', async () => {139 await usingApi(async api => {140 const collection = await createCollectionExpectSuccess();141 const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 142143 // Mute the general tx parsing error, too many bytes to process144 {145 console.error = () => {};146 await expect(executeTransaction(147 api, 148 alice, 149 api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 150 )).to.be.rejected;151 }152153 let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();154 expect(properties).to.be.empty;155156 await expect(executeTransaction(157 api, 158 alice, 159 api.tx.unique.setCollectionProperties(collection, [160 {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, 161 {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 162 ]), 163 )).to.be.rejectedWith(/common\.NoSpaceForProperty/);164165 properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();166 expect(properties).to.be.empty;167 });168 });169 170 it('Fails to set more properties than it is allowed', async () => {171 await usingApi(async api => {172 const collection = await createCollectionExpectSuccess();173174 const propertiesToBeSet = [];175 for (let i = 0; i < 65; i++) {176 propertiesToBeSet.push({177 key: 'electron_' + i,178 value: Math.random() > 0.5 ? 'high' : 'low',179 });180 }181182 await expect(executeTransaction(183 api, 184 alice, 185 api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 186 )).to.be.rejectedWith(/common\.PropertyLimitReached/);187188 const properties = (await api.query.common.collectionProperties(collection)).toJSON();189 expect(properties.map).to.be.empty;190 expect(properties.consumedSpace).to.equal(0);191 });192 });193194 it('Fails to set properties with invalid names', async () => {195 await usingApi(async api => {196 const collection = await createCollectionExpectSuccess();197198 const invalidProperties = [199 [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],200 [{key: 'Mr.Sandman', value: 'Bring me a gene'}],201 [{key: 'déjà vu', value: 'hmm...'}],202 ];203204 for (let i = 0; i < invalidProperties.length; i++) {205 await expect(executeTransaction(206 api, 207 alice, 208 api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 209 ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);210 }211212 await expect(executeTransaction(213 api, 214 alice, 215 api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 216 ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/);217218 await expect(executeTransaction(219 api, 220 alice, 221 api.tx.unique.setCollectionProperties(collection, [222 {key: 'CRISPR-Cas9', value: 'rewriting nature!'}223 ]), 224 ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;225226 const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');227228 const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();229 expect(properties).to.be.deep.equal([230 {key: 'CRISPR-Cas9', value: 'rewriting nature!'},231 ]);232233 for (let i = 0; i < invalidProperties.length; i++) {234 await expect(executeTransaction(235 api, 236 alice, 237 api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), 238 ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);239 }240 });241 });242});243244// ---------- ACCESS RIGHTS245246describe('Integration Test: Access Rights to Token Properties', () => {247 before(async () => {248 await usingApi(async api => {249 alice = privateKey('//Alice');250 bob = privateKey('//Bob');251 });252 });253 254 it('Reads access rights to properties of a collection', async () => {255 await usingApi(async api => {256 const collection = await createCollectionExpectSuccess();257 const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();258 expect(propertyRights).to.be.empty;259 });260 });261 262 it('Sets access rights to properties of a collection', async () => {263 await usingApi(async api => {264 const collection = await createCollectionExpectSuccess();265266 await expect(executeTransaction(267 api, 268 alice, 269 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 270 )).to.not.be.rejected;271272 await addCollectionAdminExpectSuccess(alice, collection, bob.address);273274 await expect(executeTransaction(275 api, 276 alice, 277 api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 278 )).to.not.be.rejected;279280 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();281 expect(propertyRights).to.be.deep.equal([282 {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},283 {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},284 ]);285 });286 });287 288 it('Changes access rights to properties of a collection', async () => {289 await usingApi(async api => {290 const collection = await createCollectionExpectSuccess();291292 await expect(executeTransaction(293 api, 294 alice, 295 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 296 )).to.not.be.rejected;297298 await expect(executeTransaction(299 api, 300 alice, 301 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 302 )).to.not.be.rejected;303304 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();305 expect(propertyRights).to.be.deep.equal([306 {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},307 ]);308 });309 });310});311312describe('Negative Integration Test: Access Rights to Token Properties', () => {313 before(async () => {314 await usingApi(async api => {315 alice = privateKey('//Alice');316 bob = privateKey('//Bob');317 });318 });319320 it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {321 await usingApi(async api => {322 const collection = await createCollectionExpectSuccess();323324 await expect(executeTransaction(325 api, 326 bob, 327 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 328 )).to.be.rejectedWith(/common\.NoPermission/);329330 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();331 expect(propertyRights).to.be.empty;332 });333 });334335 it('Prevents from adding too many possible properties', async () => {336 await usingApi(async api => {337 const collection = await createCollectionExpectSuccess();338339 const constitution = [];340 for (let i = 0; i < 65; i++) {341 constitution.push({342 key: 'property_' + i,343 permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},344 });345 }346347 await expect(executeTransaction(348 api, 349 alice, 350 api.tx.unique.setPropertyPermissions(collection, constitution), 351 )).to.be.rejectedWith(/common\.PropertyLimitReached/);352353 const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();354 expect(propertyRights).to.be.empty;355 });356 });357358 it('Prevents access rights to be modified if constant', async () => {359 await usingApi(async api => {360 const collection = await createCollectionExpectSuccess();361362 await expect(executeTransaction(363 api, 364 alice, 365 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 366 )).to.not.be.rejected;367368 await expect(executeTransaction(369 api, 370 alice, 371 api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 372 )).to.be.rejectedWith(/common\.NoPermission/);373374 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();375 expect(propertyRights).to.deep.equal([376 {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},377 ]);378 });379 });380381 it('Prevents adding properties with invalid names', async () => {382 await usingApi(async api => {383 const collection = await createCollectionExpectSuccess();384385 const invalidProperties = [386 [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],387 [{key: 'G#4', permission: {tokenOwner: true}}],388 [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],389 ];390391 for (let i = 0; i < invalidProperties.length; i++) {392 await expect(executeTransaction(393 api, 394 alice, 395 api.tx.unique.setPropertyPermissions(collection, invalidProperties[i]), 396 ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);397 }398399 await expect(executeTransaction(400 api, 401 alice, 402 api.tx.unique.setPropertyPermissions(collection, [{key: '', permission: {}}]), 403 ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/);404405 const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string406 await expect(executeTransaction(407 api, 408 alice, 409 api.tx.unique.setPropertyPermissions(collection, [410 {key: correctKey, permission: {collectionAdmin: true}},411 ]), 412 ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;413414 const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');415416 const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();417 expect(propertyRights).to.be.deep.equal([418 {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},419 ]);420 });421 });422});423424// ---------- TOKEN PROPERTIES425426describe('Integration Test: Token Properties', () => {427 let collection: number;428 let token: number;429 let permissions: {permission: any, signers: IKeyringPair[]}[];430431 before(async () => {432 await usingApi(async api => {433 alice = privateKey('//Alice');434 bob = privateKey('//Bob');435 charlie = privateKey('//Charlie');436437 permissions = [438 {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},439 {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},440 {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},441 {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},442 {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},443 {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},444 ];445 });446 });447448 beforeEach(async () => {449 collection = await createCollectionExpectSuccess();450 token = await createItemExpectSuccess(alice, collection, 'NFT');451 await addCollectionAdminExpectSuccess(alice, collection, bob.address);452 await transferExpectSuccess(collection, token, alice, charlie);453 });454 455 it('Reads yet empty properties of a token', async () => {456 await usingApi(async api => {457 const collection = await createCollectionExpectSuccess();458 const token = await createItemExpectSuccess(alice, collection, 'NFT');459 460 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();461 expect(properties.map).to.be.empty;462 expect(properties.consumedSpace).to.be.equal(0);463464 const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;465 expect(tokenData).to.be.empty;466 });467 });468469 it('Assigns properties to a token according to permissions', async () => {470 await usingApi(async api => {471 const propertyKeys: string[] = [];472 let i = 0;473 for (const permission of permissions) {474 for (const signer of permission.signers) {475 const key = i + '_' + signer.address;476 propertyKeys.push(key);477478 await expect(executeTransaction(479 api, 480 alice, 481 api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 482 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;483484 await expect(executeTransaction(485 api, 486 signer, 487 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 488 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;489 }490491 i++;492 }493494 const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];495 const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];496 for (let i = 0; i < properties.length; i++) {497 expect(properties[i].value).to.be.equal('Serotonin increase');498 expect(tokensData[i].value).to.be.equal('Serotonin increase');499 }500 });501 });502503 it('Changes properties of a token according to permissions', async () => {504 await usingApi(async api => {505 const propertyKeys: string[] = [];506 let i = 0;507 for (const permission of permissions) {508 if (!permission.permission.mutable) continue;509 510 for (const signer of permission.signers) {511 const key = i + '_' + signer.address;512 propertyKeys.push(key);513514 await expect(executeTransaction(515 api, 516 alice, 517 api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 518 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;519520 await expect(executeTransaction(521 api, 522 signer, 523 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 524 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;525526 await expect(executeTransaction(527 api, 528 signer, 529 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 530 ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;531 }532533 i++;534 }535536 const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];537 const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];538 for (let i = 0; i < properties.length; i++) {539 expect(properties[i].value).to.be.equal('Serotonin stable');540 expect(tokensData[i].value).to.be.equal('Serotonin stable');541 }542 });543 });544545 it('Deletes properties of a token according to permissions', async () => {546 await usingApi(async api => {547 const propertyKeys: string[] = [];548 let i = 0;549550 for (const permission of permissions) {551 if (!permission.permission.mutable) continue;552 553 for (const signer of permission.signers) {554 const key = i + '_' + signer.address;555 propertyKeys.push(key);556557 await expect(executeTransaction(558 api, 559 alice, 560 api.tx.unique.setPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 561 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;562563 await expect(executeTransaction(564 api, 565 signer, 566 api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 567 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;568569 await expect(executeTransaction(570 api, 571 signer, 572 api.tx.unique.deleteTokenProperties(collection, token, [key]), 573 ), `on deleting property ${i} by ${signer.address}`).to.not.be.rejected;574 }575 576 i++;577 }578579 const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];580 expect(properties).to.be.empty;581 const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];582 expect(tokensData).to.be.empty;583 expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);584 });585 });586});587588describe('Negative Integration Test: Token Properties', () => {589 let collection: number;590 let token: number;591 let originalSpace: number;592 let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];593594 before(async () => {595 await usingApi(async api => {596 alice = privateKey('//Alice');597 bob = privateKey('//Bob');598 charlie = privateKey('//Charlie');599 const dave = privateKey('//Dave');600601 constitution = [602 {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},603 {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},604 {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},605 {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},606 {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},607 {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},608 ];609 });610 });611612 beforeEach(async () => {613 collection = await createCollectionExpectSuccess();614 token = await createItemExpectSuccess(alice, collection, 'NFT');615 await addCollectionAdminExpectSuccess(alice, collection, bob.address);616 await transferExpectSuccess(collection, token, alice, charlie);617 618 await usingApi(async api => {619 let i = 0;620 for (const passage of constitution) {621 const signer = passage.signers[0];622 623 await expect(executeTransaction(624 api, 625 alice, 626 api.tx.unique.setPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 627 ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;628629 await expect(executeTransaction(630 api, 631 signer, 632 api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 633 ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;634635 i++;636 }637638 originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;639 });640 });641642 it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {643 await usingApi(async api => {644 let i = -1;645 for (const forbiddance of constitution) {646 i++;647 if (!forbiddance.permission.mutable) continue;648649 await expect(executeTransaction(650 api, 651 forbiddance.sinner, 652 api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 653 ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);654655 await expect(executeTransaction(656 api, 657 forbiddance.sinner, 658 api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 659 ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);660 }661662 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();663 expect(properties.consumedSpace).to.be.equal(originalSpace);664 });665 });666667 it('Forbids changing/deleting properties of a token if the property is permanent (constant)', async () => {668 await usingApi(async api => {669 let i = -1;670 for (const permission of constitution) {671 i++;672 if (permission.permission.mutable) continue;673674 await expect(executeTransaction(675 api, 676 permission.signers[0], 677 api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 678 ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);679680 await expect(executeTransaction(681 api, 682 permission.signers[0], 683 api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 684 ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);685 }686687 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();688 expect(properties.consumedSpace).to.be.equal(originalSpace);689 });690 });691692 it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {693 await usingApi(async api => {694 await expect(executeTransaction(695 api, 696 alice, 697 api.tx.unique.setTokenProperties(collection, token, [{key: 'non-existent', value: 'I exist!'}]), 698 ), 'on failing to add a previously non-existent property').to.be.rejectedWith(/common\.NoPermission/);699 700 await expect(executeTransaction(701 api, 702 alice, 703 api.tx.unique.setPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 704 ), 'on setting a new non-permitted property').to.not.be.rejected;705706 await expect(executeTransaction(707 api, 708 alice, 709 api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 710 ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);711712 expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;713 const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();714 expect(properties.consumedSpace).to.be.equal(originalSpace);715 });716 });717718 it('Forbids adding too many properties to a token', async () => {719 await usingApi(async api => {720 await expect(executeTransaction(721 api, 722 alice, 723 api.tx.unique.setPropertyPermissions(collection, [724 {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, 725 {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},726 ]), 727 ), 'on setting a new non-permitted property').to.not.be.rejected;728729 // Mute the general tx parsing error730 {731 console.error = () => {};732 await expect(executeTransaction(733 api, 734 alice, 735 api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 736 )).to.be.rejected;737 }738739 await expect(executeTransaction(740 api, 741 alice, 742 api.tx.unique.setTokenProperties(collection, token, [743 {key: 'a_holy_book', value: 'word '.repeat(3277)}, 744 {key: 'young_years', value: 'neverending'.repeat(1490)},745 ]), 746 )).to.be.rejectedWith(/common\.NoSpaceForProperty/);747748 expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young years'])).toJSON()).to.be.empty;749 const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();750 expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);751 });752 });753});tests/src/nesting/unnest.test.tsdiffbeforeafterboth--- a/tests/src/nesting/unnest.test.ts
+++ b/tests/src/nesting/unnest.test.ts
@@ -6,11 +6,13 @@
createCollectionExpectSuccess,
createItemExpectFailure,
createItemExpectSuccess,
+ getBalance,
getTokenOwner,
getTopmostTokenOwner,
normalizeAccountId,
setCollectionLimitsExpectSuccess,
transferExpectSuccess,
+ transferFromExpectSuccess,
} from '../util/helpers';
import {IKeyringPair} from '@polkadot/types/types';
@@ -25,7 +27,7 @@
});
});
- it('Allows the owner to successfully unnest a token', async () => {
+ it('NFT: allows the owner to successfully unnest a token', async () => {
await usingApi(async api => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
@@ -40,7 +42,7 @@
api,
alice,
api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(alice), collection, nestedToken, 1),
- )).to.not.be.rejected;
+ ), 'while unnesting').to.not.be.rejected;
expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Substrate: alice.address});
// Nest and burn
@@ -48,13 +50,64 @@
await expect(executeTransaction(
api,
alice,
- api.tx.unique.burnFrom(collection, normalizeAccountId(alice.address), nestedToken, 1),
- )).to.not.be.rejected;
- await expect(getTokenOwner(api, collection, nestedToken)).to.be.rejected; // 'owner == null'
+ api.tx.unique.burnFrom(collection, normalizeAccountId(targetAddress), nestedToken, 1),
+ ), 'while burning').to.not.be.rejected;
+ await expect(getTokenOwner(api, collection, nestedToken)).to.be.rejected;
});
});
- // todo refungible-fungible test just in case
+ it('Fungible: allows the owner to successfully unnest a token', async () => {
+ await usingApi(async api => {
+ const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+ await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
+ const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+ const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
+
+ const collectionFT = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
+ const nestedToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
+
+ // Nest and unnest
+ await transferExpectSuccess(collectionFT, nestedToken, alice, targetAddress, 1, 'Fungible');
+ await transferFromExpectSuccess(collectionFT, nestedToken, alice, targetAddress, alice, 1, 'Fungible');
+
+ // Nest and burn
+ await transferExpectSuccess(collectionFT, nestedToken, alice, targetAddress, 1, 'Fungible');
+ const balanceBefore = await getBalance(api, collectionFT, normalizeAccountId(targetAddress), nestedToken);
+ await expect(executeTransaction(
+ api,
+ alice,
+ api.tx.unique.burnFrom(collectionFT, normalizeAccountId(targetAddress), nestedToken, 1),
+ ), 'while burning').to.not.be.rejected;
+ const balanceAfter = await getBalance(api, collectionFT, normalizeAccountId(targetAddress), nestedToken);
+ expect(balanceAfter + BigInt(1)).to.be.equal(balanceBefore);
+ });
+ });
+
+ it('ReFungible: allows the owner to successfully unnest a token', async () => {
+ await usingApi(async api => {
+ const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+ await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
+ const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
+ const targetAddress = {Ethereum: tokenIdToAddress(collection, targetToken)};
+
+ const collectionRFT = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+ const nestedToken = await createItemExpectSuccess(alice, collectionRFT, 'ReFungible');
+
+ // Nest and unnest
+ await transferExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, 1, 'ReFungible');
+ await transferFromExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, alice, 1, 'ReFungible');
+
+ // Nest and burn
+ await transferExpectSuccess(collectionRFT, nestedToken, alice, targetAddress, 1, 'ReFungible');
+ await expect(executeTransaction(
+ api,
+ alice,
+ api.tx.unique.burnFrom(collectionRFT, normalizeAccountId(targetAddress), nestedToken, 1),
+ ), 'while burning').to.not.be.rejected;
+ const balance = await getBalance(api, collectionRFT, normalizeAccountId(targetAddress), nestedToken);
+ expect(balance).to.be.equal(0n);
+ });
+ });
});
describe('Negative Test: Unnesting', () => {
@@ -80,8 +133,7 @@
api,
bob,
api.tx.unique.transferFrom(normalizeAccountId(targetAddress), normalizeAccountId(bob), collection, nestedToken, 1),
- )).to.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
- //await transferFromExpectSuccess(collection, nestedToken, bob, targetAddress, {Substrate: bob.address});
+ ), 'while unnesting').to.be.rejectedWith(/^structure\.DepthLimit$/); // todo ApprovedValueTooLow?
expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
// Try to burn
@@ -89,31 +141,15 @@
api,
bob,
api.tx.unique.burnFrom(collection, normalizeAccountId(bob.address), nestedToken, 1),
- )).to.not.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
+ ), 'while burning').to.not.be.rejectedWith(/^common\.ApprovedValueTooLow$/);
expect(await getTokenOwner(api, collection, nestedToken)).to.be.deep.equal({Ethereum: tokenIdToAddress(collection, targetToken).toLowerCase()});
});
});
- it('Disallows excessive token nesting', async () => {
- await usingApi(async api => {
- const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
- await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
- const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
-
- // Create a nested token matryoshka
- const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
- const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
- // The nesting depth is limited by 2
- await createItemExpectFailure(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken2)});
+ // todo another test for creating excessive depth matryoshka with Ethereum?
- expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
- });
- });
-
- // todo another test for creating excessive depth matryoshka with Ethereum, move this one to nest ^
-
// Recursive nesting
- it('Prevents ouroboros creation', async () => {
+ it('Prevents Ouroboros creation', async () => {
const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
await setCollectionLimitsExpectSuccess(alice, collection, {nestingRule: 'Owner'});
const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
tests/src/util/helpers.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -903,7 +903,7 @@
const from = normalizeAccountId(accountFrom);
const to = normalizeAccountId(accountTo);
let balanceBefore = 0n;
- if (type === 'Fungible') {
+ if (type === 'Fungible' || type === 'ReFungible') {
balanceBefore = await getBalance(api, collectionId, to, tokenId);
}
const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom), to, collectionId, tokenId, value);
@@ -923,7 +923,7 @@
}
}
if (type === 'ReFungible') {
- expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(BigInt(value));
+ expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value));
}
});
}