--- 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", --- 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 { - 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/); }); }); }); --- 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)); } } --- 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 --- a/tests/src/nesting/properties.test.ts +++ b/tests/src/nesting/properties.test.ts @@ -51,13 +51,13 @@ await expect(executeTransaction( api, alice, - api.tx.unique.setCollectionProperties(collectionId, [{key: 'black hole'}]), + api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), )).to.not.be.rejected; - const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black hole'])).toJSON(); + const properties = (await api.rpc.unique.collectionProperties(collectionId, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ - {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('come bond').toString('hex')}`}, - {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('').toString('hex')}`}, + {key: 'electron', value: 'come bond'}, + {key: 'black_hole', value: ''}, ]); }); }); @@ -69,20 +69,20 @@ await expect(executeTransaction( api, alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole'}]), + api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), )).to.not.be.rejected; // Mutate the properties await expect(executeTransaction( api, alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black hole', value: 'LIGO'}]), + api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; - const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON(); + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ - {key: `0x${Buffer.from('electron').toString('hex')}`, value: `0x${Buffer.from('bonded').toString('hex')}`}, - {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`}, + {key: 'electron', value: 'bonded'}, + {key: 'black_hole', value: 'LIGO'}, ]); }); }); @@ -94,7 +94,7 @@ await expect(executeTransaction( api, alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), + api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), )).to.not.be.rejected; await expect(executeTransaction( @@ -103,9 +103,9 @@ api.tx.unique.deleteCollectionProperties(collection, ['electron']), )).to.not.be.rejected; - const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON(); + const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman(); expect(properties).to.be.deep.equal([ - {key: `0x${Buffer.from('black hole').toString('hex')}`, value: `0x${Buffer.from('LIGO').toString('hex')}`}, + {key: 'black_hole', value: 'LIGO'}, ]); }); }); @@ -126,7 +126,7 @@ await expect(executeTransaction( api, bob, - api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black hole', value: 'LIGO'}]), + api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), )).to.be.rejectedWith(/common\.NoPermission/); const properties = (await api.query.common.collectionProperties(collection)).toJSON(); @@ -158,7 +158,7 @@ alice, api.tx.unique.setCollectionProperties(collection, [ {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))}, - {key: 'black hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, + {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); @@ -174,7 +174,7 @@ const propertiesToBeSet = []; for (let i = 0; i < 65; i++) { propertiesToBeSet.push({ - key: 'electron ' + i, + key: 'electron_' + i, value: Math.random() > 0.5 ? 'high' : 'low', }); } @@ -190,6 +190,55 @@ expect(properties.consumedSpace).to.equal(0); }); }); + + it('Fails to set properties with invalid names', async () => { + await usingApi(async api => { + const collection = await createCollectionExpectSuccess(); + + const invalidProperties = [ + [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}], + [{key: 'Mr.Sandman', value: 'Bring me a gene'}], + [{key: 'déjà vu', value: 'hmm...'}], + ]; + + for (let i = 0; i < invalidProperties.length; i++) { + await expect(executeTransaction( + api, + alice, + api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), + ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); + } + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), + ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/); + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setCollectionProperties(collection, [ + {key: 'CRISPR-Cas9', value: 'rewriting nature!'} + ]), + ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; + + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat(''); + + const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman(); + expect(properties).to.be.deep.equal([ + {key: 'CRISPR-Cas9', value: 'rewriting nature!'}, + ]); + + for (let i = 0; i < invalidProperties.length; i++) { + await expect(executeTransaction( + api, + alice, + api.tx.unique.deleteCollectionProperties(collection, invalidProperties[i].map(propertySet => propertySet.key)), + ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); + } + }); + }); }); // ---------- ACCESS RIGHTS @@ -228,10 +277,10 @@ api.tx.unique.setPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), )).to.not.be.rejected; - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toJSON(); + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman(); expect(propertyRights).to.be.deep.equal([ - {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}}, - {key: `0x${Buffer.from('mindgame').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}}, + {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}}, + {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}}, ]); }); }); @@ -252,9 +301,9 @@ api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), )).to.not.be.rejected; - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON(); + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.be.deep.equal([ - {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, + {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, ]); }); }); @@ -290,7 +339,7 @@ const constitution = []; for (let i = 0; i < 65; i++) { constitution.push({ - key: 'property ' + i, + key: 'property_' + i, permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {}, }); } @@ -322,9 +371,51 @@ api.tx.unique.setPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), )).to.be.rejectedWith(/common\.NoPermission/); - const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON(); + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman(); expect(propertyRights).to.deep.equal([ - {key: `0x${Buffer.from('skullduggery').toString('hex')}`, permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, + {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}}, + ]); + }); + }); + + it('Prevents adding properties with invalid names', async () => { + await usingApi(async api => { + const collection = await createCollectionExpectSuccess(); + + const invalidProperties = [ + [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}], + [{key: 'G#4', permission: {tokenOwner: true}}], + [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}], + ]; + + for (let i = 0; i < invalidProperties.length; i++) { + await expect(executeTransaction( + api, + alice, + api.tx.unique.setPropertyPermissions(collection, invalidProperties[i]), + ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/); + } + + await expect(executeTransaction( + api, + alice, + api.tx.unique.setPropertyPermissions(collection, [{key: '', permission: {}}]), + ), `on rejecting an unnamed property`).to.be.rejectedWith(/common\.EmptyPropertyKey/); + + const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string + await expect(executeTransaction( + api, + alice, + api.tx.unique.setPropertyPermissions(collection, [ + {key: correctKey, permission: {collectionAdmin: true}}, + ]), + ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected; + + const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat(''); + + const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman(); + expect(propertyRights).to.be.deep.equal([ + {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}}, ]); }); }); @@ -361,7 +452,7 @@ await transferExpectSuccess(collection, token, alice, charlie); }); - it('Reads properties of a token', async () => { + it('Reads yet empty properties of a token', async () => { await usingApi(async api => { const collection = await createCollectionExpectSuccess(); const token = await createItemExpectSuccess(alice, collection, 'NFT'); @@ -381,7 +472,7 @@ let i = 0; for (const permission of permissions) { for (const signer of permission.signers) { - const key = i + ' ' + signer.address; + const key = i + '_' + signer.address; propertyKeys.push(key); await expect(executeTransaction( @@ -400,11 +491,11 @@ i++; } - const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[]; - const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[]; + const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[]; + const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[]; for (let i = 0; i < properties.length; i++) { - expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`); - expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin increase').toString('hex')}`); + expect(properties[i].value).to.be.equal('Serotonin increase'); + expect(tokensData[i].value).to.be.equal('Serotonin increase'); } }); }); @@ -417,7 +508,7 @@ if (!permission.permission.mutable) continue; for (const signer of permission.signers) { - const key = i + ' ' + signer.address; + const key = i + '_' + signer.address; propertyKeys.push(key); await expect(executeTransaction( @@ -442,11 +533,11 @@ i++; } - const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[]; - const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[]; + const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[]; + const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[]; for (let i = 0; i < properties.length; i++) { - expect(properties[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`); - expect(tokensData[i].value).to.be.equal(`0x${Buffer.from('Serotonin stable').toString('hex')}`); + expect(properties[i].value).to.be.equal('Serotonin stable'); + expect(tokensData[i].value).to.be.equal('Serotonin stable'); } }); }); @@ -460,7 +551,7 @@ if (!permission.permission.mutable) continue; for (const signer of permission.signers) { - const key = i + ' ' + signer.address; + const key = i + '_' + signer.address; propertyKeys.push(key); await expect(executeTransaction( @@ -532,13 +623,13 @@ await expect(executeTransaction( api, alice, - api.tx.unique.setPropertyPermissions(collection, [{key: i, permission: passage.permission}]), + api.tx.unique.setPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected; await expect(executeTransaction( api, signer, - api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin increase'}]), + api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected; i++; @@ -558,17 +649,16 @@ await expect(executeTransaction( api, forbiddance.sinner, - api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), + api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); await expect(executeTransaction( api, forbiddance.sinner, - api.tx.unique.deleteTokenProperties(collection, token, [forbiddance.permission]), + api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/); } - // todo RPC? const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); }); @@ -584,7 +674,7 @@ await expect(executeTransaction( api, permission.signers[0], - api.tx.unique.setTokenProperties(collection, token, [{key: i, value: 'Serotonin down'}]), + api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); await expect(executeTransaction( @@ -594,7 +684,6 @@ ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/); } - // todo RPC? const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(properties.consumedSpace).to.be.equal(originalSpace); }); @@ -632,8 +721,8 @@ api, alice, api.tx.unique.setPropertyPermissions(collection, [ - {key: 'a holy book', permission: {collectionAdmin: true, tokenOwner: true}}, - {key: 'young years', permission: {collectionAdmin: true, tokenOwner: true}}, + {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}}, + {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}}, ]), ), 'on setting a new non-permitted property').to.not.be.rejected; @@ -643,7 +732,7 @@ await expect(executeTransaction( api, alice, - api.tx.unique.setCollectionProperties(collection, [{key: 'a holy book', value: 'word '.repeat(6554)}]), + api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), )).to.be.rejected; } @@ -651,12 +740,12 @@ api, alice, api.tx.unique.setTokenProperties(collection, token, [ - {key: 'a holy book', value: 'word '.repeat(3277)}, - {key: 'young years', value: 'neverending'.repeat(1490)}, + {key: 'a_holy_book', value: 'word '.repeat(3277)}, + {key: 'young_years', value: 'neverending'.repeat(1490)}, ]), )).to.be.rejectedWith(/common\.NoSpaceForProperty/); - expect((await api.rpc.unique.tokenProperties(collection, token, ['a holy book', 'young years'])).toJSON()).to.be.empty; + expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young years'])).toJSON()).to.be.empty; const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON(); expect(propertiesMap.consumedSpace).to.be.equal(originalSpace); }); --- 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'); --- 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)); } }); }