git.delta.rocks / unique-network / refs/commits / 752e2b099754

difftreelog

fix update weighting for mintBulkCross and forbid multiple owners + multipe tokens

Grigoriy Simonov2023-09-22parent: #0c121c7.patch.diff
in: master

2 files changed

modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -1041,7 +1041,11 @@
 
 	/// @notice Function to mint a token.
 	/// @param tokenProperties Properties of minted token
-	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_properties.len() as u32) + <SelfWeightOf<T>>::set_token_properties(token_properties.len() as u32))]
+	#[weight(if token_properties.len() == 1 {
+		<SelfWeightOf<T>>::create_multiple_items_ex_multiple_owners(token_properties.iter().next().unwrap().owners.len() as u32)
+	} else {
+		<SelfWeightOf<T>>::create_multiple_items_ex_multiple_items(token_properties.len() as u32)
+	} + <SelfWeightOf<T>>::set_token_properties(token_properties.len() as u32))]
 	fn mint_bulk_cross(
 		&mut self,
 		caller: Caller,
@@ -1051,9 +1055,17 @@
 		let budget = self
 			.recorder
 			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+		let has_multiple_tokens = token_properties.len() > 1;
 
 		let mut create_rft_data = Vec::with_capacity(token_properties.len());
 		for MintTokenData { owners, properties } in token_properties {
+			let has_multiple_owners = owners.len() > 1;
+			if has_multiple_tokens & has_multiple_owners {
+				return Err(
+					"creation of multiple tokens supported only if they have single owner each"
+						.into(),
+				);
+			}
 			let users: BoundedBTreeMap<_, _, _> = owners
 				.into_iter()
 				.map(|data| Ok((data.owner.into_sub_cross_account::<T>()?, data.pieces)))
modifiedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
before · tests/src/eth/reFungible.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {Pallets, requirePalletsOrSkip} from '../util';18import {expect, itEth, usingEthPlaygrounds} from './util';19import {IKeyringPair} from '@polkadot/types/types';20import {ITokenPropertyPermission} from '../util/playgrounds/types';21import {CREATE_COLLECTION_DATA_DEFAULTS, TokenPermissionField} from './util/playgrounds/types';2223describe('Refungible: Plain calls', () => {24  let donor: IKeyringPair;25  let minter: IKeyringPair;26  let bob: IKeyringPair;27  let charlie: IKeyringPair;2829  before(async function() {30    await usingEthPlaygrounds(async (helper, privateKey) => {31      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);3233      donor = await privateKey({url: import.meta.url});34      [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);35    });36  });3738  [39    'substrate' as const,40    'ethereum' as const,41  ].map(testCase => {42    itEth(`Can perform mintCross() for ${testCase} address`, async ({helper}) => {43      const collectionAdmin = await helper.eth.createAccountWithBalance(donor);4445      const receiverEth = helper.eth.createAccount();46      const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);47      const receiverSub = bob;48      const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);4950      const properties = Array(5).fill(0).map((_, i) => ({key: `key_${i}`, value: Buffer.from(`value_${i}`)}));51      const permissions: ITokenPropertyPermission[] = properties.map(p => ({key: p.key, permission: {52        tokenOwner: false,53        collectionAdmin: true,54        mutable: false}}));555657      const collection = await helper.rft.mintCollection(minter, {58        tokenPrefix: 'ethp',59        tokenPropertyPermissions: permissions,60      });61      await collection.addAdmin(minter, {Ethereum: collectionAdmin});6263      const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);64      const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', collectionAdmin, true);65      let expectedTokenId = await contract.methods.nextTokenId().call();66      let result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, []).send();67      let tokenId = result.events.Transfer.returnValues.tokenId;68      expect(tokenId).to.be.equal(expectedTokenId);6970      let event = result.events.Transfer;71      expect(event.address).to.be.equal(collectionAddress);72      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');73      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));74      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);7576      expectedTokenId = await contract.methods.nextTokenId().call();77      result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, properties).send();78      event = result.events.Transfer;79      expect(event.address).to.be.equal(collectionAddress);80      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');81      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));82      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);8384      tokenId = result.events.Transfer.returnValues.tokenId;8586      expect(tokenId).to.be.equal(expectedTokenId);8788      expect(await contract.methods.properties(tokenId, []).call()).to.be.like(properties89        .map(p => helper.ethProperty.property(p.key, p.value.toString())));9091      expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId))92        .to.deep.eq(testCase === 'ethereum' ? {Ethereum: receiverEth.toLowerCase()} : {Substrate: receiverSub.address});93    });94  });9596  itEth.skip('Can perform mintBulk()', async ({helper}) => {97    const owner = await helper.eth.createAccountWithBalance(donor);98    const receiver = helper.eth.createAccount();99    const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'MintBulky', '6', '6', '');100    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', owner);101102    {103      const nextTokenId = await contract.methods.nextTokenId().call();104      expect(nextTokenId).to.be.equal('1');105      const result = await contract.methods.mintBulkWithTokenURI(106        receiver,107        [108          [nextTokenId, 'Test URI 0'],109          [+nextTokenId + 1, 'Test URI 1'],110          [+nextTokenId + 2, 'Test URI 2'],111        ],112      ).send();113114      const events = result.events.Transfer;115      for(let i = 0; i < 2; i++) {116        const event = events[i];117        expect(event.address).to.equal(collectionAddress);118        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');119        expect(event.returnValues.to).to.equal(receiver);120        expect(event.returnValues.tokenId).to.equal(String(+nextTokenId + i));121      }122123      expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');124      expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');125      expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');126    }127  });128129  itEth('Can perform mintBulkCross()', async ({helper}) => {130    const caller = await helper.eth.createAccountWithBalance(donor);131    const callerCross = helper.ethCrossAccount.fromAddress(caller);132    const receiver = helper.eth.createAccount();133    const receiverCross = helper.ethCrossAccount.fromAddress(receiver);134    const receiver2 = helper.eth.createAccount();135    const receiver2Cross = helper.ethCrossAccount.fromAddress(receiver2);136137    const permissions = [138      {code: TokenPermissionField.Mutable, value: true},139      {code: TokenPermissionField.TokenOwner, value: true},140      {code: TokenPermissionField.CollectionAdmin, value: true},141    ];142    const {collectionAddress} = await helper.eth.createCollection(143      caller,144      {145        ...CREATE_COLLECTION_DATA_DEFAULTS,146        name: 'A',147        description: 'B',148        tokenPrefix: 'C',149        collectionMode: 'rft',150        adminList: [callerCross],151        tokenPropertyPermissions: [152          {key: 'key_0_0', permissions},153          {key: 'key_1_0', permissions},154          {key: 'key_1_1', permissions},155          {key: 'key_2_0', permissions},156          {key: 'key_2_1', permissions},157          {key: 'key_2_2', permissions},158        ],159      },160    ).send();161162    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);163    {164      const nextTokenId = await contract.methods.nextTokenId().call();165      expect(nextTokenId).to.be.equal('1');166      const result = await contract.methods.mintBulkCross([167        {168          owners: [{169            owner: receiverCross,170            pieces: 1,171          }],172          properties: [173            {key: 'key_0_0', value: Buffer.from('value_0_0')},174          ],175        },176        {177          owners: [{178            owner: receiverCross,179            pieces: 2,180          }],181          properties: [182            {key: 'key_1_0', value: Buffer.from('value_1_0')},183            {key: 'key_1_1', value: Buffer.from('value_1_1')},184          ],185        },186        {187          owners: [188            {189              owner: receiverCross,190              pieces: 1,191            },192            {193              owner: receiver2Cross,194              pieces: 2,195            },196          ],197          properties: [198            {key: 'key_2_0', value: Buffer.from('value_2_0')},199            {key: 'key_2_1', value: Buffer.from('value_2_1')},200            {key: 'key_2_2', value: Buffer.from('value_2_2')},201          ],202        },203      ]).send({from: caller});204      const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);205      const bulkSize = 3;206      for(let i = 0; i < bulkSize; i++) {207        const event = events[i];208        expect(event.address).to.equal(collectionAddress);209        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');210        if(i == 0 || i == 1)211          expect(event.returnValues.to).to.equal(receiver);212        else213          expect(event.returnValues.to).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');214215        expect(event.returnValues.tokenId).to.equal(`${+nextTokenId + i}`);216      }217218      const properties = [219        await contract.methods.properties(+nextTokenId, []).call(),220        await contract.methods.properties(+nextTokenId + 1, []).call(),221        await contract.methods.properties(+nextTokenId + 2, []).call(),222      ];223      expect(properties).to.be.deep.equal([224        [225          ['key_0_0', helper.getWeb3().utils.toHex('value_0_0')],226        ],227        [228          ['key_1_0', helper.getWeb3().utils.toHex('value_1_0')],229          ['key_1_1', helper.getWeb3().utils.toHex('value_1_1')],230        ],231        [232          ['key_2_0', helper.getWeb3().utils.toHex('value_2_0')],233          ['key_2_1', helper.getWeb3().utils.toHex('value_2_1')],234          ['key_2_2', helper.getWeb3().utils.toHex('value_2_2')],235        ],236      ]);237    }238  });239240  itEth('Can perform setApprovalForAll()', async ({helper}) => {241    const owner = await helper.eth.createAccountWithBalance(donor);242    const operator = helper.eth.createAccount();243244    const collection = await helper.rft.mintCollection(minter, {});245246    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);247    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', owner);248249    const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();250    expect(approvedBefore).to.be.equal(false);251252    {253      const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});254255      expect(result.events.ApprovalForAll).to.be.like({256        address: collectionAddress,257        event: 'ApprovalForAll',258        returnValues: {259          owner,260          operator,261          approved: true,262        },263      });264265      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();266      expect(approvedAfter).to.be.equal(true);267    }268269    {270      const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});271272      expect(result.events.ApprovalForAll).to.be.like({273        address: collectionAddress,274        event: 'ApprovalForAll',275        returnValues: {276          owner,277          operator,278          approved: false,279        },280      });281282      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();283      expect(approvedAfter).to.be.equal(false);284    }285  });286287  itEth('Can perform burn with ApprovalForAll', async ({helper}) => {288    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});289290    const owner = await helper.eth.createAccountWithBalance(donor);291    const operator = await helper.eth.createAccountWithBalance(donor);292293    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});294295    const address = helper.ethAddress.fromCollectionId(collection.collectionId);296    const contract = await helper.ethNativeContract.collection(address, 'rft');297298    {299      await contract.methods.setApprovalForAll(operator, true).send({from: owner});300      const ownerCross = helper.ethCrossAccount.fromAddress(owner);301      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});302      const events = result.events.Transfer;303304      expect(events).to.be.like({305        address,306        event: 'Transfer',307        returnValues: {308          from: owner,309          to: '0x0000000000000000000000000000000000000000',310          tokenId: token.tokenId.toString(),311        },312      });313    }314  });315316  itEth('Can perform burn with approve and approvalForAll', async ({helper}) => {317    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});318319    const owner = await helper.eth.createAccountWithBalance(donor);320    const operator = await helper.eth.createAccountWithBalance(donor);321322    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});323324    const address = helper.ethAddress.fromCollectionId(collection.collectionId);325    const contract = await helper.ethNativeContract.collection(address, 'rft');326327    const rftToken = await helper.ethNativeContract.rftTokenById(token.collectionId, token.tokenId, owner, true);328329    {330      await rftToken.methods.approve(operator, 15n).send({from: owner});331      await contract.methods.setApprovalForAll(operator, true).send({from: owner});332      await rftToken.methods.burnFrom(owner, 10n).send({from: operator});333    }334    {335      const allowance = await rftToken.methods.allowance(owner, operator).call();336      expect(+allowance).to.be.equal(5);337    }338    {339      const ownerCross = helper.ethCrossAccount.fromAddress(owner);340      const operatorCross = helper.ethCrossAccount.fromAddress(operator);341      const allowance = await rftToken.methods.allowanceCross(ownerCross, operatorCross).call();342      expect(+allowance).to.equal(5);343    }344  });345346  itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {347    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});348349    const owner = await helper.eth.createAccountWithBalance(donor);350    const operator = await helper.eth.createAccountWithBalance(donor);351    const receiver = charlie;352353    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});354355    const address = helper.ethAddress.fromCollectionId(collection.collectionId);356    const contract = await helper.ethNativeContract.collection(address, 'rft');357358    {359      await contract.methods.setApprovalForAll(operator, true).send({from: owner});360      const ownerCross = helper.ethCrossAccount.fromAddress(owner);361      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);362      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});363      const event = result.events.Transfer;364      expect(event).to.be.like({365        address: helper.ethAddress.fromCollectionId(collection.collectionId),366        event: 'Transfer',367        returnValues: {368          from: owner,369          to: helper.address.substrateToEth(receiver.address),370          tokenId: token.tokenId.toString(),371        },372      });373    }374375    expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);376  });377378  itEth('Can perform burn()', async ({helper}) => {379    const caller = await helper.eth.createAccountWithBalance(donor);380    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Burny', '6', '6');381    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);382383    const result = await contract.methods.mint(caller).send();384    const tokenId = result.events.Transfer.returnValues.tokenId;385    {386      const result = await contract.methods.burn(tokenId).send();387      const event = result.events.Transfer;388      expect(event.address).to.equal(collectionAddress);389      expect(event.returnValues.from).to.equal(caller);390      expect(event.returnValues.to).to.equal('0x0000000000000000000000000000000000000000');391      expect(event.returnValues.tokenId).to.equal(tokenId.toString());392    }393  });394395  itEth('Can perform transferFrom()', async ({helper}) => {396    const caller = await helper.eth.createAccountWithBalance(donor);397    const receiver = helper.eth.createAccount();398    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'TransferFromy', '6', '6');399    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);400401    const result = await contract.methods.mint(caller).send();402    const tokenId = result.events.Transfer.returnValues.tokenId;403404    const tokenAddress = helper.ethAddress.fromTokenId(collectionId, tokenId);405406    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, caller);407    await tokenContract.methods.repartition(15).send();408409    {410      const tokenEvents: any = [];411      tokenContract.events.allEvents((_: any, event: any) => {412        tokenEvents.push(event);413      });414      const result = await contract.methods.transferFrom(caller, receiver, tokenId).send();415      if(tokenEvents.length == 0) await helper.wait.newBlocks(1);416417      let event = result.events.Transfer;418      expect(event.address).to.equal(collectionAddress);419      expect(event.returnValues.from).to.equal(caller);420      expect(event.returnValues.to).to.equal(receiver);421      expect(event.returnValues.tokenId).to.equal(tokenId.toString());422423      event = tokenEvents[0];424      expect(event.address).to.equal(tokenAddress);425      expect(event.returnValues.from).to.equal(caller);426      expect(event.returnValues.to).to.equal(receiver);427      expect(event.returnValues.value).to.equal('15');428    }429430    {431      const balance = await contract.methods.balanceOf(receiver).call();432      expect(+balance).to.equal(1);433    }434435    {436      const balance = await contract.methods.balanceOf(caller).call();437      expect(+balance).to.equal(0);438    }439  });440441  // Soft-deprecated442  itEth('Can perform burnFrom()', async ({helper}) => {443    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});444445    const owner = await helper.eth.createAccountWithBalance(donor);446    const spender = await helper.eth.createAccountWithBalance(donor);447448    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});449450    const address = helper.ethAddress.fromCollectionId(collection.collectionId);451    const contract = await helper.ethNativeContract.collection(address, 'rft', spender, true);452453    const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, token.tokenId);454    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner);455    await tokenContract.methods.repartition(15).send();456    await tokenContract.methods.approve(spender, 15).send();457458    {459      const result = await contract.methods.burnFrom(owner, token.tokenId).send();460      const event = result.events.Transfer;461      expect(event).to.be.like({462        address: helper.ethAddress.fromCollectionId(collection.collectionId),463        event: 'Transfer',464        returnValues: {465          from: owner,466          to: '0x0000000000000000000000000000000000000000',467          tokenId: token.tokenId.toString(),468        },469      });470    }471472    expect(await collection.getTokenBalance(token.tokenId, {Ethereum: owner})).to.be.eq(0n);473  });474475  itEth('Can perform burnFromCross()', async ({helper}) => {476    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});477478    const owner = bob;479    const spender = await helper.eth.createAccountWithBalance(donor);480481    const token = await collection.mintToken(minter, 100n, {Substrate: owner.address});482483    const address = helper.ethAddress.fromCollectionId(collection.collectionId);484    const contract = await helper.ethNativeContract.collection(address, 'rft');485486    await token.repartition(owner, 15n);487    await token.approve(owner, {Ethereum: spender}, 15n);488489    {490      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);491      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender});492      const event = result.events.Transfer;493      expect(event).to.be.like({494        address: helper.ethAddress.fromCollectionId(collection.collectionId),495        event: 'Transfer',496        returnValues: {497          from: helper.address.substrateToEth(owner.address),498          to: '0x0000000000000000000000000000000000000000',499          tokenId: token.tokenId.toString(),500        },501      });502    }503504    expect(await collection.getTokenBalance(token.tokenId, {Substrate: owner.address})).to.be.eq(0n);505  });506507  itEth('Can perform transferFromCross()', async ({helper}) => {508    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});509510    const owner = bob;511    const spender = await helper.eth.createAccountWithBalance(donor);512    const receiver = charlie;513514    const token = await collection.mintToken(minter, 100n, {Substrate: owner.address});515516    const address = helper.ethAddress.fromCollectionId(collection.collectionId);517    const contract = await helper.ethNativeContract.collection(address, 'rft');518519    await token.repartition(owner, 15n);520    await token.approve(owner, {Ethereum: spender}, 15n);521522    {523      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);524      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);525      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});526      const event = result.events.Transfer;527      expect(event).to.be.like({528        address: helper.ethAddress.fromCollectionId(collection.collectionId),529        event: 'Transfer',530        returnValues: {531          from: helper.address.substrateToEth(owner.address),532          to: helper.address.substrateToEth(receiver.address),533          tokenId: token.tokenId.toString(),534        },535      });536    }537538    expect(await token.getTop10Owners()).to.be.like([{Substrate: receiver.address}]);539  });540541  itEth('Can perform transfer()', async ({helper}) => {542    const caller = await helper.eth.createAccountWithBalance(donor);543    const receiver = helper.eth.createAccount();544    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry', '6', '6');545    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);546547    const result = await contract.methods.mint(caller).send();548    const tokenId = result.events.Transfer.returnValues.tokenId;549550    {551      const result = await contract.methods.transfer(receiver, tokenId).send();552553      const event = result.events.Transfer;554      expect(event.address).to.equal(collectionAddress);555      expect(event.returnValues.from).to.equal(caller);556      expect(event.returnValues.to).to.equal(receiver);557      expect(event.returnValues.tokenId).to.equal(tokenId.toString());558    }559560    {561      const balance = await contract.methods.balanceOf(caller).call();562      expect(+balance).to.equal(0);563    }564565    {566      const balance = await contract.methods.balanceOf(receiver).call();567      expect(+balance).to.equal(1);568    }569  });570571  itEth('Can perform transferCross()', async ({helper}) => {572    const sender = await helper.eth.createAccountWithBalance(donor);573    const receiverEth = await helper.eth.createAccountWithBalance(donor);574    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);575    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);576577    const collection = await helper.rft.mintCollection(minter, {});578    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);579    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'rft', sender);580581    const token = await collection.mintToken(minter, 50n, {Ethereum: sender});582583    {584      // Can transferCross to ethereum address:585      const result = await collectionEvm.methods.transferCross(receiverCrossEth, token.tokenId).send({from: sender});586      // Check events:587      const event = result.events.Transfer;588      expect(event.address).to.equal(collectionAddress);589      expect(event.returnValues.from).to.equal(sender);590      expect(event.returnValues.to).to.equal(receiverEth);591      expect(event.returnValues.tokenId).to.equal(token.tokenId.toString());592      // Sender's balance decreased:593      const senderBalance = await collectionEvm.methods.balanceOf(sender).call();594      expect(+senderBalance).to.equal(0);595      expect(await token.getBalance({Ethereum: sender})).to.eq(0n);596      // Receiver's balance increased:597      const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();598      expect(+receiverBalance).to.equal(1);599      expect(await token.getBalance({Ethereum: receiverEth})).to.eq(50n);600    }601602    {603      // Can transferCross to substrate address:604      const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, token.tokenId).send({from: receiverEth});605      // Check events:606      const event = substrateResult.events.Transfer;607      expect(event.address).to.be.equal(collectionAddress);608      expect(event.returnValues.from).to.be.equal(receiverEth);609      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));610      expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);611      // Sender's balance decreased:612      const senderBalance = await collectionEvm.methods.balanceOf(receiverEth).call();613      expect(+senderBalance).to.equal(0);614      expect(await token.getBalance({Ethereum: receiverEth})).to.eq(0n);615      // Receiver's balance increased:616      const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});617      expect(receiverBalance).to.contain(token.tokenId);618      expect(await token.getBalance({Substrate: minter.address})).to.eq(50n);619    }620  });621622  ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {623    const sender = await helper.eth.createAccountWithBalance(donor);624    const tokenOwner = await helper.eth.createAccountWithBalance(donor);625    const receiverSub = minter;626    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);627628    const collection = await helper.rft.mintCollection(minter, {});629    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);630    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'rft', sender);631632    await collection.mintToken(minter, 50n, {Ethereum: sender});633    const nonSendersToken = await collection.mintToken(minter, 50n, {Ethereum: tokenOwner});634635    // Cannot transferCross someone else's token:636    const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;637    await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;638    // Cannot transfer token if it does not exist:639    await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;640  }));641642  itEth('transfer event on transfer from partial ownership to full ownership', async ({helper}) => {643    const caller = await helper.eth.createAccountWithBalance(donor);644    const receiver = helper.eth.createAccount();645    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Partial-to-Full', '6', '6');646    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);647648    const result = await contract.methods.mint(caller).send();649    const tokenId = result.events.Transfer.returnValues.tokenId;650651    const tokenContract = await helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller);652653    await tokenContract.methods.repartition(2).send();654    await tokenContract.methods.transfer(receiver, 1).send();655656    const events: any = [];657    contract.events.allEvents((_: any, event: any) => {658      events.push(event);659    });660661    await tokenContract.methods.transfer(receiver, 1).send();662    if(events.length == 0) await helper.wait.newBlocks(1);663    const event = events[0];664665    expect(event.address).to.equal(collectionAddress);666    expect(event.returnValues.from).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');667    expect(event.returnValues.to).to.equal(receiver);668    expect(event.returnValues.tokenId).to.equal(tokenId.toString());669  });670671  itEth('transfer event on transfer from full ownership to partial ownership', async ({helper}) => {672    const caller = await helper.eth.createAccountWithBalance(donor);673    const receiver = helper.eth.createAccount();674    const {collectionId, collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry-Full-to-Partial', '6', '6');675    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);676677    const result = await contract.methods.mint(caller).send();678    const tokenId = result.events.Transfer.returnValues.tokenId;679680    const tokenContract = await helper.ethNativeContract.rftTokenById(collectionId, tokenId, caller);681682    await tokenContract.methods.repartition(2).send();683684    const events: any = [];685    contract.events.allEvents((_: any, event: any) => {686      events.push(event);687    });688689    await tokenContract.methods.transfer(receiver, 1).send();690    if(events.length == 0) await helper.wait.newBlocks(1);691    const event = events[0];692693    expect(event.address).to.equal(collectionAddress);694    expect(event.returnValues.from).to.equal(caller);695    expect(event.returnValues.to).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');696    expect(event.returnValues.tokenId).to.equal(tokenId.toString());697  });698699  itEth('Check balanceOfCross()', async ({helper}) => {700    const collection = await helper.rft.mintCollection(minter, {});701    const owner = await helper.ethCrossAccount.createAccountWithBalance(donor);702    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);703    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);704705    expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq('0');706707    for(let i = 1n; i < 10n; i++) {708      await collection.mintToken(minter, 100n, {Ethereum: owner.eth});709      expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq(i.toString());710    }711  });712713  itEth('Check ownerOfCross()', async ({helper}) => {714    const collection = await helper.rft.mintCollection(minter, {});715    let owner = await helper.ethCrossAccount.createAccountWithBalance(donor);716    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);717    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);718    const {tokenId} = await collection.mintToken(minter, 100n,{Ethereum: owner.eth});719720    for(let i = 1n; i < 10n; i++) {721      const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});722      expect(ownerCross.eth).to.be.eq(owner.eth);723      expect(ownerCross.sub).to.be.eq(owner.sub);724725      const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);726      await collectionEvm.methods.transferCross(newOwner, tokenId).send({from: owner.eth});727      owner = newOwner;728    }729730    const tokenAddress = helper.ethAddress.fromTokenId(collection.collectionId, tokenId);731    const tokenContract = await helper.ethNativeContract.rftToken(tokenAddress, owner.eth, true);732    const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);733    await tokenContract.methods.transferCross(newOwner, 50).send({from: owner.eth});734    const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});735    expect(ownerCross.eth.toUpperCase()).to.be.eq('0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');736    expect(ownerCross.sub).to.be.eq('0');737  });738});739740describe('RFT: Fees', () => {741  let donor: IKeyringPair;742743  before(async function() {744    await usingEthPlaygrounds(async (helper, privateKey) => {745      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);746747      donor = await privateKey({url: import.meta.url});748    });749  });750751  itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {752    const caller = await helper.eth.createAccountWithBalance(donor);753    const receiver = helper.eth.createAccount();754    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer-From', '6', '6');755    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);756757    const result = await contract.methods.mint(caller).send();758    const tokenId = result.events.Transfer.returnValues.tokenId;759760    const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transferFrom(caller, receiver, tokenId).send());761    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));762    expect(cost > 0n);763  });764765  itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {766    const caller = await helper.eth.createAccountWithBalance(donor);767    const receiver = helper.eth.createAccount();768    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Feeful-Transfer', '6', '6');769    const contract = await helper.ethNativeContract.collection(collectionAddress, 'rft', caller);770771    const result = await contract.methods.mint(caller).send();772    const tokenId = result.events.Transfer.returnValues.tokenId;773774    const cost = await helper.eth.recordCallFee(caller, () => contract.methods.transfer(receiver, tokenId).send());775    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));776    expect(cost > 0n);777  });778});779780describe('Common metadata', () => {781  let donor: IKeyringPair;782  let alice: IKeyringPair;783784  before(async function() {785    await usingEthPlaygrounds(async (helper, privateKey) => {786      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);787788      donor = await privateKey({url: import.meta.url});789      [alice] = await helper.arrange.createAccounts([1000n], donor);790    });791  });792793  itEth('Returns collection name', async ({helper}) => {794    const caller = helper.eth.createAccount();795    const tokenPropertyPermissions = [{796      key: 'URI',797      permission: {798        mutable: true,799        collectionAdmin: true,800        tokenOwner: false,801      },802    }];803    const collection = await helper.rft.mintCollection(804      alice,805      {806        name: 'Leviathan',807        tokenPrefix: '11',808        properties: [{key: 'ERC721Metadata', value: '1'}],809        tokenPropertyPermissions,810      },811    );812813    const contract = helper.ethNativeContract.collectionById(collection.collectionId, 'rft', caller);814    const name = await contract.methods.name().call();815    expect(name).to.equal('Leviathan');816  });817818  itEth('Returns symbol name', async ({helper}) => {819    const caller = await helper.eth.createAccountWithBalance(donor);820    const tokenPropertyPermissions = [{821      key: 'URI',822      permission: {823        mutable: true,824        collectionAdmin: true,825        tokenOwner: false,826      },827    }];828    const {collectionId} = await helper.rft.mintCollection(829      alice,830      {831        name: 'Leviathan',832        tokenPrefix: '12',833        properties: [{key: 'ERC721Metadata', value: '1'}],834        tokenPropertyPermissions,835      },836    );837838    const contract = await helper.ethNativeContract.collectionById(collectionId, 'rft', caller);839    const symbol = await contract.methods.symbol().call();840    expect(symbol).to.equal('12');841  });842});843844describe('Negative tests', () => {845  let donor: IKeyringPair;846  let minter: IKeyringPair;847  let alice: IKeyringPair;848849  before(async function() {850    await usingEthPlaygrounds(async (helper, privateKey) => {851      requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);852853      donor = await privateKey({url: import.meta.url});854      [minter, alice] = await helper.arrange.createAccounts([100n, 100n], donor);855    });856  });857858  itEth('[negative] Cant perform burn without approval', async ({helper}) => {859    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});860861    const owner = await helper.eth.createAccountWithBalance(donor);862    const spender = await helper.eth.createAccountWithBalance(donor);863864    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});865866    const address = helper.ethAddress.fromCollectionId(collection.collectionId);867    const contract = await helper.ethNativeContract.collection(address, 'rft');868869    const ownerCross = helper.ethCrossAccount.fromAddress(owner);870871    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;872873    await contract.methods.setApprovalForAll(spender, true).send({from: owner});874    await contract.methods.setApprovalForAll(spender, false).send({from: owner});875876    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;877  });878879  itEth('[negative] Cant perform transfer without approval', async ({helper}) => {880    const collection = await helper.rft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});881    const owner = await helper.eth.createAccountWithBalance(donor);882    const receiver = alice;883884    const spender = await helper.eth.createAccountWithBalance(donor);885886    const token = await collection.mintToken(minter, 100n, {Ethereum: owner});887888    const address = helper.ethAddress.fromCollectionId(collection.collectionId);889    const contract = await helper.ethNativeContract.collection(address, 'rft');890891    const ownerCross = helper.ethCrossAccount.fromAddress(owner);892    const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);893894    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;895896    await contract.methods.setApprovalForAll(spender, true).send({from: owner});897    await contract.methods.setApprovalForAll(spender, false).send({from: owner});898899    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;900  });901});