git.delta.rocks / unique-network / refs/commits / 6a580826c34a

difftreelog

source

js-packages/tests/eth/nonFungible.test.ts48.5 KiBsourcehistory
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 {itEth, usingEthPlaygrounds, expect} from './util/index.js';18import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';19import type {IKeyringPair} from '@polkadot/types/types';20import {Contract} from 'web3-eth-contract';21import type {ITokenPropertyPermission} from '@unique/playgrounds/types.js';22import {CREATE_COLLECTION_DATA_DEFAULTS, TokenPermissionField} from './util/playgrounds/types.js';2324describe('Check ERC721 token URI for NFT', () => {25  let donor: IKeyringPair;2627  before(async function() {28    await usingEthPlaygrounds(async (_helper, privateKey) => {29      donor = await privateKey({url: import.meta.url});30    });31  });3233  async function setup(helper: EthUniqueHelper, baseUri: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> {34    const owner = await helper.eth.createAccountWithBalance(donor);35    const receiver = helper.eth.createAccount();3637    const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', 'a', 'b', baseUri);38    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);3940    const result = await contract.methods.mint(receiver).send();41    const tokenId = result.events.Transfer.returnValues.tokenId;42    expect(tokenId).to.be.equal('1');4344    if(propertyKey && propertyValue) {45      // Set URL or suffix46      await contract.methods.setProperties(tokenId, [{key: propertyKey, value: Buffer.from(propertyValue)}]).send();47    }4849    const event = result.events.Transfer;50    expect(event.address).to.be.equal(collectionAddress);51    expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');52    expect(event.returnValues.to).to.be.equal(receiver);53    expect(event.returnValues.tokenId).to.be.equal(tokenId);5455    return {contract, nextTokenId: tokenId};56  }5758  itEth('Empty tokenURI', async ({helper}) => {59    const {contract, nextTokenId} = await setup(helper, '');60    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('');61  });6263  itEth('TokenURI from url', async ({helper}) => {64    const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URI', 'Token URI');65    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI');66  });6768  itEth('TokenURI from baseURI', async ({helper}) => {69    const {contract, nextTokenId} = await setup(helper, 'BaseURI_');70    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_');71  });7273  itEth('TokenURI from baseURI + suffix', async ({helper}) => {74    const suffix = '/some/suffix';75    const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URISuffix', suffix);76    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix);77  });78});7980describe('NFT: Plain calls', () => {81  let donor: IKeyringPair;82  let minter: IKeyringPair;83  let bob: IKeyringPair;84  let charlie: IKeyringPair;8586  before(async function() {87    await usingEthPlaygrounds(async (helper, privateKey) => {88      donor = await privateKey({url: import.meta.url});89      [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);90    });91  });9293  // TODO combine all minting tests in one place94  [95    'substrate' as const,96    'ethereum' as const,97  ].map(testCase => {98    itEth(`Can perform mintCross() for ${testCase} address`, async ({helper}) => {99      const collectionAdmin = await helper.eth.createAccountWithBalance(donor);100101      const receiverEth = helper.eth.createAccount();102      const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);103      const receiverSub = bob;104      const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);105106      // const receiverCross = helper.ethCrossAccount.fromKeyringPair(bob);107      const properties = Array(5).fill(0).map((_, i) => ({key: `key_${i}`, value: Buffer.from(`value_${i}`)}));108      const permissions: ITokenPropertyPermission[] = properties109        .map(p => ({110          key: p.key, permission: {111            tokenOwner: false,112            collectionAdmin: true,113            mutable: false,114          },115        }));116117      const collection = await helper.nft.mintCollection(minter, {118        tokenPrefix: 'ethp',119        tokenPropertyPermissions: permissions,120      });121      await collection.addAdmin(minter, {Ethereum: collectionAdmin});122123      const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);124      const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', collectionAdmin, true);125      let expectedTokenId = await contract.methods.nextTokenId().call();126      let result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, []).send();127      let tokenId = result.events.Transfer.returnValues.tokenId;128      expect(tokenId).to.be.equal(expectedTokenId);129130      let event = result.events.Transfer;131      expect(event.address).to.be.equal(collectionAddress);132      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');133      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));134      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);135136      expectedTokenId = await contract.methods.nextTokenId().call();137      result = await contract.methods.mintCross(testCase === 'ethereum' ? receiverCrossEth : receiverCrossSub, properties).send();138      event = result.events.Transfer;139      expect(event.address).to.be.equal(collectionAddress);140      expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');141      expect(event.returnValues.to).to.be.equal(testCase === 'ethereum' ? receiverEth : helper.address.substrateToEth(bob.address));142      expect(await contract.methods.properties(tokenId, []).call()).to.be.like([]);143144      tokenId = result.events.Transfer.returnValues.tokenId;145146      expect(tokenId).to.be.equal(expectedTokenId);147148      expect(await contract.methods.properties(tokenId, []).call()).to.be.like(properties149        .map(p => helper.ethProperty.property(p.key, p.value.toString())));150151      expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId))152        .to.deep.eq(testCase === 'ethereum' ? {Ethereum: receiverEth.toLowerCase()} : {Substrate: receiverSub.address});153    });154  });155156  itEth('Non-owner and non admin cannot mintCross', async ({helper}) => {157    const nonOwner = await helper.eth.createAccountWithBalance(donor);158    const nonOwnerCross = helper.ethCrossAccount.fromAddress(nonOwner);159160    const collection = await helper.nft.mintCollection(minter);161    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);162    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft');163164    await expect(collectionEvm.methods.mintCross(nonOwnerCross, []).call({from: nonOwner}))165      .to.be.rejectedWith('PublicMintingNotAllowed');166  });167168  //TODO: CORE-302 add eth methods169  itEth.skip('Can perform mintBulk()', async ({helper}) => {170    const caller = await helper.eth.createAccountWithBalance(donor);171    const receiver = helper.eth.createAccount();172173    const collection = await helper.nft.mintCollection(minter);174    await collection.addAdmin(minter, {Ethereum: caller});175176    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);177    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', caller);178    {179      const bulkSize = 3;180      const nextTokenId = await contract.methods.nextTokenId().call();181      expect(nextTokenId).to.be.equal('1');182      const result = await contract.methods.mintBulkWithTokenURI(183        receiver,184        Array.from({length: bulkSize}, (_, i) => (185          [+nextTokenId + i, `Test URI ${i}`]186        )),187      ).send({from: caller});188189      const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);190      for(let i = 0; i < bulkSize; i++) {191        const event = events[i];192        expect(event.address).to.equal(collectionAddress);193        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');194        expect(event.returnValues.to).to.equal(receiver);195        expect(event.returnValues.tokenId).to.equal(`${+nextTokenId + i}`);196197        expect(await contract.methods.tokenURI(+nextTokenId + i).call()).to.be.equal(`Test URI ${i}`);198      }199    }200  });201202  itEth('Can perform mintBulkCross()', async ({helper}) => {203    const caller = await helper.eth.createAccountWithBalance(donor);204    const callerCross = helper.ethCrossAccount.fromAddress(caller);205    const receiver = helper.eth.createAccount();206    const receiverCross = helper.ethCrossAccount.fromAddress(receiver);207208    const permissions = [209      {code: TokenPermissionField.Mutable, value: true},210      {code: TokenPermissionField.TokenOwner, value: true},211      {code: TokenPermissionField.CollectionAdmin, value: true},212    ];213    const {collectionAddress} = await helper.eth.createCollection(214      caller,215      {216        ...CREATE_COLLECTION_DATA_DEFAULTS,217        name: 'A',218        description: 'B',219        tokenPrefix: 'C',220        collectionMode: 'nft',221        adminList: [callerCross],222        tokenPropertyPermissions: [223          {key: 'key_0_0', permissions},224          {key: 'key_1_0', permissions},225          {key: 'key_1_1', permissions},226          {key: 'key_2_0', permissions},227          {key: 'key_2_1', permissions},228          {key: 'key_2_2', permissions},229        ],230      },231    ).send();232233    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', caller);234    {235      const nextTokenId = await contract.methods.nextTokenId().call();236      expect(nextTokenId).to.be.equal('1');237      const result = await contract.methods.mintBulkCross([238        {239          owner: receiverCross,240          properties: [241            {key: 'key_0_0', value: Buffer.from('value_0_0')},242          ],243        },244        {245          owner: receiverCross,246          properties: [247            {key: 'key_1_0', value: Buffer.from('value_1_0')},248            {key: 'key_1_1', value: Buffer.from('value_1_1')},249          ],250        },251        {252          owner: receiverCross,253          properties: [254            {key: 'key_2_0', value: Buffer.from('value_2_0')},255            {key: 'key_2_1', value: Buffer.from('value_2_1')},256            {key: 'key_2_2', value: Buffer.from('value_2_2')},257          ],258        },259      ]).send({from: caller});260      const events = result.events.Transfer.sort((a: any, b: any) => +a.returnValues.tokenId - b.returnValues.tokenId);261      const bulkSize = 3;262      for(let i = 0; i < bulkSize; i++) {263        const event = events[i];264        expect(event.address).to.equal(collectionAddress);265        expect(event.returnValues.from).to.equal('0x0000000000000000000000000000000000000000');266        expect(event.returnValues.to).to.equal(receiver);267        expect(event.returnValues.tokenId).to.equal(`${+nextTokenId + i}`);268      }269270      const properties = [271        await contract.methods.properties(+nextTokenId, []).call(),272        await contract.methods.properties(+nextTokenId + 1, []).call(),273        await contract.methods.properties(+nextTokenId + 2, []).call(),274      ];275      expect(properties).to.be.deep.equal([276        [277          ['key_0_0', helper.getWeb3().utils.toHex('value_0_0')],278        ],279        [280          ['key_1_0', helper.getWeb3().utils.toHex('value_1_0')],281          ['key_1_1', helper.getWeb3().utils.toHex('value_1_1')],282        ],283        [284          ['key_2_0', helper.getWeb3().utils.toHex('value_2_0')],285          ['key_2_1', helper.getWeb3().utils.toHex('value_2_1')],286          ['key_2_2', helper.getWeb3().utils.toHex('value_2_2')],287        ],288      ]);289    }290  });291292  itEth('Can perform burn()', async ({helper}) => {293    const caller = await helper.eth.createAccountWithBalance(donor);294295    const collection = await helper.nft.mintCollection(minter, {});296    const {tokenId} = await collection.mintToken(minter, {Ethereum: caller});297298    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);299    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', caller);300301    {302      const result = await contract.methods.burn(tokenId).send({from: caller});303304      const event = result.events.Transfer;305      expect(event.address).to.be.equal(collectionAddress);306      expect(event.returnValues.from).to.be.equal(caller);307      expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');308      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);309    }310  });311312  itEth('Can perform approve()', async ({helper}) => {313    const owner = await helper.eth.createAccountWithBalance(donor);314    const spender = helper.eth.createAccount();315316    const collection = await helper.nft.mintCollection(minter, {});317    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});318319    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);320    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);321322    {323      const badTokenId = await contract.methods.nextTokenId().call() + 1;324      await expect(contract.methods.getApproved(badTokenId).call()).to.be.rejectedWith('revert TokenNotFound');325    }326    {327      const approved = await contract.methods.getApproved(tokenId).call();328      expect(approved).to.be.equal('0x0000000000000000000000000000000000000000');329    }330    {331      const result = await contract.methods.approve(spender, tokenId).send({from: owner});332333      const event = result.events.Approval;334      expect(event.address).to.be.equal(collectionAddress);335      expect(event.returnValues.owner).to.be.equal(owner);336      expect(event.returnValues.approved).to.be.equal(spender);337      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);338    }339    {340      const approved = await contract.methods.getApproved(tokenId).call();341      expect(approved).to.be.equal(spender);342    }343  });344345  itEth('Can perform setApprovalForAll()', async ({helper}) => {346    const owner = await helper.eth.createAccountWithBalance(donor);347    const operator = helper.eth.createAccount();348349    const collection = await helper.nft.mintCollection(minter, {});350351    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);352    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);353354    const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();355    expect(approvedBefore).to.be.equal(false);356357    {358      const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});359360      expect(result.events.ApprovalForAll).to.be.like({361        address: collectionAddress,362        event: 'ApprovalForAll',363        returnValues: {364          owner,365          operator,366          approved: true,367        },368      });369370      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();371      expect(approvedAfter).to.be.equal(true);372    }373374    {375      const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});376377      expect(result.events.ApprovalForAll).to.be.like({378        address: collectionAddress,379        event: 'ApprovalForAll',380        returnValues: {381          owner,382          operator,383          approved: false,384        },385      });386387      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();388      expect(approvedAfter).to.be.equal(false);389    }390  });391392  itEth('Can perform burn with ApprovalForAll', async ({helper}) => {393    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});394395    const owner = await helper.eth.createAccountWithBalance(donor);396    const operator = await helper.eth.createAccountWithBalance(donor);397398    const token = await collection.mintToken(minter, {Ethereum: owner});399400    const address = helper.ethAddress.fromCollectionId(collection.collectionId);401    const contract = await helper.ethNativeContract.collection(address, 'nft');402403    {404      await contract.methods.setApprovalForAll(operator, true).send({from: owner});405      const ownerCross = helper.ethCrossAccount.fromAddress(owner);406      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});407      const events = result.events.Transfer;408409      expect(events).to.be.like({410        address,411        event: 'Transfer',412        returnValues: {413          from: owner,414          to: '0x0000000000000000000000000000000000000000',415          tokenId: token.tokenId.toString(),416        },417      });418    }419420    expect(await helper.nft.doesTokenExist(collection.collectionId, token.tokenId)).to.be.false;421  });422423  itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {424    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});425426    const owner = await helper.eth.createAccountWithBalance(donor);427    const operator = await helper.eth.createAccountWithBalance(donor);428    const receiver = charlie;429430    const token = await collection.mintToken(minter, {Ethereum: owner});431432    const address = helper.ethAddress.fromCollectionId(collection.collectionId);433    const contract = await helper.ethNativeContract.collection(address, 'nft');434435    {436      await contract.methods.setApprovalForAll(operator, true).send({from: owner});437      const ownerCross = helper.ethCrossAccount.fromAddress(owner);438      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);439      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});440      const event = result.events.Transfer;441      expect(event).to.be.like({442        address: helper.ethAddress.fromCollectionId(collection.collectionId),443        event: 'Transfer',444        returnValues: {445          from: owner,446          to: helper.address.substrateToEth(receiver.address),447          tokenId: token.tokenId.toString(),448        },449      });450    }451452    expect(await token.getOwner()).to.be.like({Substrate: receiver.address});453  });454455  itEth('Can perform burnFromCross()', async ({helper}) => {456    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});457    const ownerSub = bob;458    const ownerCrossSub = helper.ethCrossAccount.fromKeyringPair(ownerSub);459    const ownerEth = await helper.eth.createAccountWithBalance(donor);460    const ownerCrossEth = helper.ethCrossAccount.fromAddress(ownerEth);461462    const burnerEth = await helper.eth.createAccountWithBalance(donor);463    const burnerCrossEth = helper.ethCrossAccount.fromAddress(burnerEth);464465    const token1 = await collection.mintToken(minter, {Substrate: ownerSub.address});466    const token2 = await collection.mintToken(minter, {Ethereum: ownerEth});467468    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);469    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft');470471    // Approve tokens from substrate and ethereum:472    await token1.approve(ownerSub, {Ethereum: burnerEth});473    await collectionEvm.methods.approveCross(burnerCrossEth, token2.tokenId).send({from: ownerEth});474475    // can burnFromCross:476    const result1 = await collectionEvm.methods.burnFromCross(ownerCrossSub, token1.tokenId).send({from: burnerEth});477    const result2 = await collectionEvm.methods.burnFromCross(ownerCrossEth, token2.tokenId).send({from: burnerEth});478    const events1 = result1.events.Transfer;479    const events2 = result2.events.Transfer;480481    // Check events for burnFromCross (substrate and ethereum):482    [483      [events1, token1, helper.address.substrateToEth(ownerSub.address)],484      [events2, token2, ownerEth],485    ].map(burnData => {486      expect(burnData[0]).to.be.like({487        address: collectionAddress,488        event: 'Transfer',489        returnValues: {490          from: burnData[2],491          to: '0x0000000000000000000000000000000000000000',492          tokenId: burnData[1].tokenId.toString(),493        },494      });495    });496497    expect(await token1.doesExist()).to.be.false;498    expect(await token2.doesExist()).to.be.false;499  });500501  // TODO combine all approve tests in one place502  itEth('Can perform approveCross()', async ({helper}) => {503    // arrange: create accounts504    const owner = await helper.eth.createAccountWithBalance(donor);505    const ownerCross = helper.ethCrossAccount.fromAddress(owner);506    const receiverSub = charlie;507    const recieverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);508    const receiverEth = await helper.eth.createAccountWithBalance(donor);509    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);510511    // arrange: create collection and tokens:512    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});513    const token1 = await collection.mintToken(minter, {Ethereum: owner});514    const token2 = await collection.mintToken(minter, {Ethereum: owner});515516    const collectionEvm = await helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');517518    // Can approveCross substrate and ethereum address:519    const resultSub = await collectionEvm.methods.approveCross(recieverCrossSub, token1.tokenId).send({from: owner});520    const resultEth = await collectionEvm.methods.approveCross(receiverCrossEth, token2.tokenId).send({from: owner});521    const eventSub = resultSub.events.Approval;522    const eventEth = resultEth.events.Approval;523    expect(eventSub).to.be.like({524      address: helper.ethAddress.fromCollectionId(collection.collectionId),525      event: 'Approval',526      returnValues: {527        owner,528        approved: helper.address.substrateToEth(receiverSub.address),529        tokenId: token1.tokenId.toString(),530      },531    });532    expect(eventEth).to.be.like({533      address: helper.ethAddress.fromCollectionId(collection.collectionId),534      event: 'Approval',535      returnValues: {536        owner,537        approved: receiverEth,538        tokenId: token2.tokenId.toString(),539      },540    });541542    // Substrate address can transferFrom approved tokens:543    await helper.nft.transferTokenFrom(receiverSub, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiverSub.address});544    expect(await helper.nft.getTokenOwner(collection.collectionId, token1.tokenId)).to.deep.eq({Substrate: receiverSub.address});545    // Ethereum address can transferFromCross approved tokens:546    await collectionEvm.methods.transferFromCross(ownerCross, receiverCrossEth, token2.tokenId).send({from: receiverEth});547    expect(await helper.nft.getTokenOwner(collection.collectionId, token2.tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});548  });549550  itEth('Non-owner and non admin cannot approveCross', async ({helper}) => {551    const nonOwner = await helper.eth.createAccountWithBalance(donor);552    const nonOwnerCross = helper.ethCrossAccount.fromAddress(nonOwner);553    const owner = await helper.eth.createAccountWithBalance(donor);554    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});555    const collectionEvm = await helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');556    const token = await collection.mintToken(minter, {Ethereum: owner});557558    await expect(collectionEvm.methods.approveCross(nonOwnerCross, token.tokenId).call({from: nonOwner})).to.be.rejectedWith('CantApproveMoreThanOwned');559  });560561  itEth('Can reaffirm approved address', async ({helper}) => {562    const owner = await helper.eth.createAccountWithBalance(donor);563    const ownerCrossEth = helper.ethCrossAccount.fromAddress(owner);564    const [receiver1, receiver2] = await helper.arrange.createAccounts([100n, 100n], donor);565    const receiver1Cross = helper.ethCrossAccount.fromKeyringPair(receiver1);566    const receiver2Cross = helper.ethCrossAccount.fromKeyringPair(receiver2);567    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});568    const token1 = await collection.mintToken(minter, {Ethereum: owner});569    const token2 = await collection.mintToken(minter, {Ethereum: owner});570    const collectionEvm = await helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');571572    // Can approve and reaffirm approved address:573    await collectionEvm.methods.approveCross(receiver1Cross, token1.tokenId).send({from: owner});574    await collectionEvm.methods.approveCross(receiver2Cross, token1.tokenId).send({from: owner});575576    // receiver1 cannot transferFrom:577    await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;578    // receiver2 can transferFrom:579    await helper.nft.transferTokenFrom(receiver2, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver2.address});580581    // can set approved address to self address to remove approval:582    await collectionEvm.methods.approveCross(receiver1Cross, token2.tokenId).send({from: owner});583    await collectionEvm.methods.approveCross(ownerCrossEth, token2.tokenId).send({from: owner});584585    // receiver1 cannot transfer token anymore:586    await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token2.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;587  });588589  itEth('Can perform transferFrom()', async ({helper}) => {590    const owner = await helper.eth.createAccountWithBalance(donor);591    const spender = await helper.eth.createAccountWithBalance(donor);592    const receiver = helper.eth.createAccount();593594    const collection = await helper.nft.mintCollection(minter, {});595    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});596597    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);598    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);599600    await contract.methods.approve(spender, tokenId).send({from: owner});601602    {603      const result = await contract.methods.transferFrom(owner, receiver, tokenId).send({from: spender});604605      const event = result.events.Transfer;606      expect(event.address).to.be.equal(collectionAddress);607      expect(event.returnValues.from).to.be.equal(owner);608      expect(event.returnValues.to).to.be.equal(receiver);609      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);610    }611612    {613      const balance = await contract.methods.balanceOf(receiver).call();614      expect(+balance).to.equal(1);615    }616617    {618      const balance = await contract.methods.balanceOf(owner).call();619      expect(+balance).to.equal(0);620    }621  });622623  itEth('Can perform transferFromCross()', async ({helper}) => {624    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});625626    const [owner, receiver] = await helper.arrange.createAccounts([100n, 100n], donor);627    const spender = await helper.eth.createAccountWithBalance(donor);628629    const token = await collection.mintToken(minter, {Substrate: owner.address});630631    const address = helper.ethAddress.fromCollectionId(collection.collectionId);632    const contract = await helper.ethNativeContract.collection(address, 'nft');633634    await token.approve(owner, {Ethereum: spender});635636    {637      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);638      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);639      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});640      const event = result.events.Transfer;641      expect(event).to.be.like({642        address: helper.ethAddress.fromCollectionId(collection.collectionId),643        event: 'Transfer',644        returnValues: {645          from: helper.address.substrateToEth(owner.address),646          to: helper.address.substrateToEth(receiver.address),647          tokenId: token.tokenId.toString(),648        },649      });650    }651652    expect(await token.getOwner()).to.be.like({Substrate: receiver.address});653  });654655  itEth('Can perform transfer()', async ({helper}) => {656    const collection = await helper.nft.mintCollection(minter, {});657    const owner = await helper.eth.createAccountWithBalance(donor);658    const receiver = helper.eth.createAccount();659660    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});661662    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);663    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);664665    {666      const result = await contract.methods.transfer(receiver, tokenId).send({from: owner});667668      const event = result.events.Transfer;669      expect(event.address).to.be.equal(collectionAddress);670      expect(event.returnValues.from).to.be.equal(owner);671      expect(event.returnValues.to).to.be.equal(receiver);672      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);673    }674675    {676      const balance = await contract.methods.balanceOf(owner).call();677      expect(+balance).to.equal(0);678    }679680    {681      const balance = await contract.methods.balanceOf(receiver).call();682      expect(+balance).to.equal(1);683    }684  });685686  itEth('Can perform transferCross()', async ({helper}) => {687    const collection = await helper.nft.mintCollection(minter, {});688    const owner = await helper.eth.createAccountWithBalance(donor);689    const receiverEth = await helper.eth.createAccountWithBalance(donor);690    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);691    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);692693    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});694695    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);696    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);697698    {699      // Can transferCross to ethereum address:700      const result = await collectionEvm.methods.transferCross(receiverCrossEth, tokenId).send({from: owner});701      // Check events:702      const event = result.events.Transfer;703      expect(event.address).to.be.equal(collectionAddress);704      expect(event.returnValues.from).to.be.equal(owner);705      expect(event.returnValues.to).to.be.equal(receiverEth);706      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);707708      // owner has balance = 0:709      const ownerBalance = await collectionEvm.methods.balanceOf(owner).call();710      expect(+ownerBalance).to.equal(0);711      // receiver owns token:712      const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();713      expect(+receiverBalance).to.equal(1);714      expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});715    }716717    {718      // Can transferCross to substrate address:719      const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, tokenId).send({from: receiverEth});720      // Check events:721      const event = substrateResult.events.Transfer;722      expect(event.address).to.be.equal(collectionAddress);723      expect(event.returnValues.from).to.be.equal(receiverEth);724      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));725      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);726727      // owner has balance = 0:728      const ownerBalance = await collectionEvm.methods.balanceOf(receiverEth).call();729      expect(+ownerBalance).to.equal(0);730      // receiver owns token:731      const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});732      expect(receiverBalance).to.contain(tokenId);733    }734  });735736  ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {737    const sender = await helper.eth.createAccountWithBalance(donor);738    const tokenOwner = await helper.eth.createAccountWithBalance(donor);739    const receiverSub = minter;740    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);741742    const collection = await helper.nft.mintCollection(minter, {});743    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);744    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', sender);745746    await collection.mintToken(minter, {Ethereum: sender});747    const nonSendersToken = await collection.mintToken(minter, {Ethereum: tokenOwner});748749    // Cannot transferCross someone else's token:750    const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;751    await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;752    // Cannot transfer token if it does not exist:753    await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;754  }));755756  itEth('Check balanceOfCross()', async ({helper}) => {757    const collection = await helper.nft.mintCollection(minter, {});758    const owner = await helper.ethCrossAccount.createAccountWithBalance(donor);759    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);760    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);761762    expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq('0');763764    for(let i = 1; i < 10; i++) {765      await collection.mintToken(minter, {Ethereum: owner.eth});766      expect(await collectionEvm.methods.balanceOfCross(owner).call({from: owner.eth})).to.be.eq(i.toString());767    }768  });769770  itEth('Check ownerOfCross()', async ({helper}) => {771    const collection = await helper.nft.mintCollection(minter, {});772    let owner = await helper.ethCrossAccount.createAccountWithBalance(donor);773    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);774    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner.eth);775    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner.eth});776777    for(let i = 1n; i < 10n; i++) {778      const ownerCross = await collectionEvm.methods.ownerOfCross(tokenId).call({from: owner.eth});779      expect(ownerCross.eth).to.be.eq(owner.eth);780      expect(ownerCross.sub).to.be.eq(owner.sub);781782      const newOwner = await helper.ethCrossAccount.createAccountWithBalance(donor);783      await collectionEvm.methods.transferCross(newOwner, tokenId).send({from: owner.eth});784      owner = newOwner;785    }786  });787});788789describe('NFT: Fees', () => {790  let donor: IKeyringPair;791  let alice: IKeyringPair;792  let bob: IKeyringPair;793  let charlie: IKeyringPair;794795  before(async function() {796    await usingEthPlaygrounds(async (helper, privateKey) => {797      donor = await privateKey({url: import.meta.url});798      [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);799    });800  });801802  itEth('approve() call fee is less than 0.2UNQ', async ({helper}) => {803    const owner = await helper.eth.createAccountWithBalance(donor);804    const spender = helper.eth.createAccount();805806    const collection = await helper.nft.mintCollection(alice, {});807    const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});808809    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);810811    const cost = await helper.eth.recordCallFee(owner, () => contract.methods.approve(spender, tokenId).send({from: owner}));812    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));813  });814815  itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {816    const owner = await helper.eth.createAccountWithBalance(donor);817    const spender = await helper.eth.createAccountWithBalance(donor);818819    const collection = await helper.nft.mintCollection(alice, {});820    const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});821822    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);823824    await contract.methods.approve(spender, tokenId).send({from: owner});825826    const cost = await helper.eth.recordCallFee(spender, () => contract.methods.transferFrom(owner, spender, tokenId).send({from: spender}));827    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));828  });829830  itEth('Can perform transferFromCross()', async ({helper}) => {831    const collectionMinter = alice;832    const owner = bob;833    const receiver = charlie;834    const collection = await helper.nft.mintCollection(collectionMinter, {name: 'A', description: 'B', tokenPrefix: 'C'});835836    const spender = await helper.eth.createAccountWithBalance(donor);837838    const token = await collection.mintToken(collectionMinter, {Substrate: owner.address});839840    const address = helper.ethAddress.fromCollectionId(collection.collectionId);841    const contract = await helper.ethNativeContract.collection(address, 'nft');842843    await token.approve(owner, {Ethereum: spender});844845    {846      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);847      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);848      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});849      const event = result.events.Transfer;850      expect(event).to.be.like({851        address: helper.ethAddress.fromCollectionId(collection.collectionId),852        event: 'Transfer',853        returnValues: {854          from: helper.address.substrateToEth(owner.address),855          to: helper.address.substrateToEth(receiver.address),856          tokenId: token.tokenId.toString(),857        },858      });859    }860861    expect(await token.getOwner()).to.be.like({Substrate: receiver.address});862  });863864  itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {865    const owner = await helper.eth.createAccountWithBalance(donor);866    const receiver = helper.eth.createAccount();867868    const collection = await helper.nft.mintCollection(alice, {});869    const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});870871    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);872873    const cost = await helper.eth.recordCallFee(owner, () => contract.methods.transfer(receiver, tokenId).send({from: owner}));874    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));875  });876});877878describe('NFT: Substrate calls', () => {879  let donor: IKeyringPair;880  let alice: IKeyringPair;881882  before(async function() {883    await usingEthPlaygrounds(async (helper, privateKey) => {884      donor = await privateKey({url: import.meta.url});885      [alice] = await helper.arrange.createAccounts([20n], donor);886    });887  });888889  itEth('Events emitted for mint()', async ({helper}) => {890    const collection = await helper.nft.mintCollection(alice, {});891    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);892    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');893894    const events: any = [];895    contract.events.allEvents((_: any, event: any) => {896      events.push(event);897    });898899    const {tokenId} = await collection.mintToken(alice);900    if(events.length == 0) await helper.wait.newBlocks(1);901    const event = events[0];902903    expect(event.event).to.be.equal('Transfer');904    expect(event.address).to.be.equal(collectionAddress);905    expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');906    expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(alice.address));907    expect(event.returnValues.tokenId).to.be.equal(tokenId.toString());908  });909910  itEth('Events emitted for burn()', async ({helper}) => {911    const collection = await helper.nft.mintCollection(alice, {});912    const token = await collection.mintToken(alice);913914    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);915    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');916917    const events: any = [];918    contract.events.allEvents((_: any, event: any) => {919      events.push(event);920    });921922    await token.burn(alice);923    if(events.length == 0) await helper.wait.newBlocks(1);924    const event = events[0];925926    expect(event.event).to.be.equal('Transfer');927    expect(event.address).to.be.equal(collectionAddress);928    expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));929    expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');930    expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());931  });932933  itEth('Events emitted for approve()', async ({helper}) => {934    const receiver = helper.eth.createAccount();935936    const collection = await helper.nft.mintCollection(alice, {});937    const token = await collection.mintToken(alice);938939    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);940    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');941942    const events: any = [];943    contract.events.allEvents((_: any, event: any) => {944      events.push(event);945    });946947    await token.approve(alice, {Ethereum: receiver});948    if(events.length == 0) await helper.wait.newBlocks(1);949    const event = events[0];950951    expect(event.event).to.be.equal('Approval');952    expect(event.address).to.be.equal(collectionAddress);953    expect(event.returnValues.owner).to.be.equal(helper.address.substrateToEth(alice.address));954    expect(event.returnValues.approved).to.be.equal(receiver);955    expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());956  });957958  itEth('Events emitted for transferFrom()', async ({helper}) => {959    const [bob] = await helper.arrange.createAccounts([10n], donor);960    const receiver = helper.eth.createAccount();961962    const collection = await helper.nft.mintCollection(alice, {});963    const token = await collection.mintToken(alice);964    await token.approve(alice, {Substrate: bob.address});965966    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);967    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');968969    const events: any = [];970    contract.events.allEvents((_: any, event: any) => {971      events.push(event);972    });973974    await token.transferFrom(bob, {Substrate: alice.address}, {Ethereum: receiver});975976    if(events.length == 0) await helper.wait.newBlocks(1);977    const event = events[0];978979    expect(event.address).to.be.equal(collectionAddress);980    expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));981    expect(event.returnValues.to).to.be.equal(receiver);982    expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);983  });984985  itEth('Events emitted for transfer()', async ({helper}) => {986    const receiver = helper.eth.createAccount();987988    const collection = await helper.nft.mintCollection(alice, {});989    const token = await collection.mintToken(alice);990991    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);992    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');993994    const events: any = [];995    contract.events.allEvents((_: any, event: any) => {996      events.push(event);997    });998999    await token.transfer(alice, {Ethereum: receiver});10001001    if(events.length == 0) await helper.wait.newBlocks(1);1002    const event = events[0];10031004    expect(event.address).to.be.equal(collectionAddress);1005    expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));1006    expect(event.returnValues.to).to.be.equal(receiver);1007    expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);1008  });1009});10101011describe('Common metadata', () => {1012  let donor: IKeyringPair;1013  let alice: IKeyringPair;10141015  before(async function() {1016    await usingEthPlaygrounds(async (helper, privateKey) => {1017      donor = await privateKey({url: import.meta.url});1018      [alice] = await helper.arrange.createAccounts([20n], donor);1019    });1020  });10211022  itEth('Returns collection name', async ({helper}) => {1023    const caller = helper.eth.createAccount();1024    const tokenPropertyPermissions = [{1025      key: 'URI',1026      permission: {1027        mutable: true,1028        collectionAdmin: true,1029        tokenOwner: false,1030      },1031    }];1032    const collection = await helper.nft.mintCollection(1033      alice,1034      {1035        name: 'oh River',1036        tokenPrefix: 'CHANGE',1037        properties: [{key: 'ERC721Metadata', value: '1'}],1038        tokenPropertyPermissions,1039      },1040    );10411042    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);1043    const name = await contract.methods.name().call();1044    expect(name).to.equal('oh River');1045  });10461047  itEth('Returns symbol name', async ({helper}) => {1048    const caller = await helper.eth.createAccountWithBalance(donor);1049    const tokenPropertyPermissions = [{1050      key: 'URI',1051      permission: {1052        mutable: true,1053        collectionAdmin: true,1054        tokenOwner: false,1055      },1056    }];1057    const collection = await helper.nft.mintCollection(1058      alice,1059      {1060        name: 'oh River',1061        tokenPrefix: 'CHANGE',1062        properties: [{key: 'ERC721Metadata', value: '1'}],1063        tokenPropertyPermissions,1064      },1065    );10661067    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);1068    const symbol = await contract.methods.symbol().call();1069    expect(symbol).to.equal('CHANGE');1070  });1071});10721073describe('Negative tests', () => {1074  let donor: IKeyringPair;1075  let minter: IKeyringPair;1076  let alice: IKeyringPair;10771078  before(async function() {1079    await usingEthPlaygrounds(async (helper, privateKey) => {1080      donor = await privateKey({url: import.meta.url});1081      [minter, alice] = await helper.arrange.createAccounts([100n, 100n], donor);1082    });1083  });10841085  itEth('[negative] Cant perform burn without approval', async ({helper}) => {1086    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});10871088    const owner = await helper.eth.createAccountWithBalance(donor);1089    const spender = await helper.eth.createAccountWithBalance(donor);10901091    const token = await collection.mintToken(minter, {Ethereum: owner});10921093    const address = helper.ethAddress.fromCollectionId(collection.collectionId);1094    const contract = await helper.ethNativeContract.collection(address, 'nft');10951096    const ownerCross = helper.ethCrossAccount.fromAddress(owner);1097    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;10981099    await contract.methods.setApprovalForAll(spender, true).send({from: owner});1100    await contract.methods.setApprovalForAll(spender, false).send({from: owner});11011102    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;1103  });11041105  itEth('[negative] Cant perform transfer without approval', async ({helper}) => {1106    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});1107    const receiver = alice;11081109    const owner = await helper.eth.createAccountWithBalance(donor);1110    const spender = await helper.eth.createAccountWithBalance(donor);11111112    const token = await collection.mintToken(minter, {Ethereum: owner});11131114    const address = helper.ethAddress.fromCollectionId(collection.collectionId);1115    const contract = await helper.ethNativeContract.collection(address, 'nft');11161117    const ownerCross = helper.ethCrossAccount.fromAddress(owner);1118    const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);11191120    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;11211122    await contract.methods.setApprovalForAll(spender, true).send({from: owner});1123    await contract.methods.setApprovalForAll(spender, false).send({from: owner});11241125    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;1126  });1127});