git.delta.rocks / unique-network / refs/commits / d68299c35c5b

difftreelog

fix PR

Trubnikov Sergey2023-03-09parent: #0595f0d.patch.diff
in: master

2 files changed

modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -1088,8 +1088,7 @@
 		token: TokenId,
 	) -> Result<Option<T::CrossAccountId>, DispatchError> {
 		ensure! {
-		<TokenData<T>>::iter_keys().find(
-			|(c, t)| return *c == collection.id && *t == token).is_some()
+		<TokenData<T>>::get((collection.id, token)).is_some()
 		,<CommonError<T>>::TokenNotFound
 		};
 		Ok(<Allowance<T>>::get((collection.id, token)))
modifiedtests/src/eth/nonFungible.test.tsdiffbeforeafterboth
before · tests/src/eth/nonFungible.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 {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util';18import {IKeyringPair} from '@polkadot/types/types';19import {Contract} from 'web3-eth-contract';20import {ITokenPropertyPermission} from '../util/playgrounds/types';2122describe('Check ERC721 token URI for NFT', () => {23  let donor: IKeyringPair;2425  before(async function() {26    await usingEthPlaygrounds(async (_helper, privateKey) => {27      donor = await privateKey({filename: __filename});28    });29  });3031  async function setup(helper: EthUniqueHelper, baseUri: string, propertyKey?: string, propertyValue?: string): Promise<{contract: Contract, nextTokenId: string}> {32    const owner = await helper.eth.createAccountWithBalance(donor);33    const receiver = helper.eth.createAccount();3435    const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, 'Mint collection', 'a', 'b', baseUri);36    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);3738    const result = await contract.methods.mint(receiver).send();39    const tokenId = result.events.Transfer.returnValues.tokenId;40    expect(tokenId).to.be.equal('1');4142    if (propertyKey && propertyValue) {43      // Set URL or suffix44      await contract.methods.setProperties(tokenId, [{key: propertyKey, value: Buffer.from(propertyValue)}]).send();45    }4647    const event = result.events.Transfer;48    expect(event.address).to.be.equal(collectionAddress);49    expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');50    expect(event.returnValues.to).to.be.equal(receiver);51    expect(event.returnValues.tokenId).to.be.equal(tokenId);5253    return {contract, nextTokenId: tokenId};54  }5556  itEth('Empty tokenURI', async ({helper}) => {57    const {contract, nextTokenId} = await setup(helper, '');58    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('');59  });6061  itEth('TokenURI from url', async ({helper}) => {62    const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URI', 'Token URI');63    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Token URI');64  });6566  itEth('TokenURI from baseURI', async ({helper}) => {67    const {contract, nextTokenId} = await setup(helper, 'BaseURI_');68    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_');69  });7071  itEth('TokenURI from baseURI + suffix', async ({helper}) => {72    const suffix = '/some/suffix';73    const {contract, nextTokenId} = await setup(helper, 'BaseURI_', 'URISuffix', suffix);74    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('BaseURI_' + suffix);75  });76});7778describe('NFT: Plain calls', () => {79  let donor: IKeyringPair;80  let minter: IKeyringPair;81  let bob: IKeyringPair;82  let charlie: IKeyringPair;8384  before(async function() {85    await usingEthPlaygrounds(async (helper, privateKey) => {86      donor = await privateKey({filename: __filename});87      [minter, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);88    });89  });9091  // TODO combine all minting tests in one place92  [93    'substrate' as const,94    'ethereum' as const,95  ].map(testCase => {96    itEth(`Can perform mintCross() for ${testCase} address`, async ({helper}) => {97      const collectionAdmin = await helper.eth.createAccountWithBalance(donor);9899      const receiverEth = helper.eth.createAccount();100      const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);101      const receiverSub = bob;102      const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);103104      // const receiverCross = helper.ethCrossAccount.fromKeyringPair(bob);105      const properties = Array(5).fill(0).map((_, i) => { return {key: `key_${i}`, value: Buffer.from(`value_${i}`)}; });106      const permissions: ITokenPropertyPermission[] = properties107        .map(p => {108          return {109            key: p.key, permission: {110              tokenOwner: false,111              collectionAdmin: true,112              mutable: false,113            },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 => { return 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 burn()', async ({helper}) => {203    const caller = await helper.eth.createAccountWithBalance(donor);204205    const collection = await helper.nft.mintCollection(minter, {});206    const {tokenId} = await collection.mintToken(minter, {Ethereum: caller});207208    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);209    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', caller);210211    {212      const result = await contract.methods.burn(tokenId).send({from: caller});213214      const event = result.events.Transfer;215      expect(event.address).to.be.equal(collectionAddress);216      expect(event.returnValues.from).to.be.equal(caller);217      expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');218      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);219    }220  });221222  itEth('Can perform approve()', async ({helper}) => {223    const owner = await helper.eth.createAccountWithBalance(donor);224    const spender = helper.eth.createAccount();225226    const collection = await helper.nft.mintCollection(minter, {});227    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});228229    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);230    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);231232    {233      const result = await contract.methods.approve(spender, tokenId).send({from: owner});234235      const event = result.events.Approval;236      expect(event.address).to.be.equal(collectionAddress);237      expect(event.returnValues.owner).to.be.equal(owner);238      expect(event.returnValues.approved).to.be.equal(spender);239      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);240    }241  });242243  itEth('Can perform setApproval()', async ({helper}) => {244    const owner = await helper.eth.createAccountWithBalance(donor);245    const operator = helper.eth.createAccount();246247    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C');248    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);249250    const result = await contract.methods.mint(owner).send({from: owner});251    const tokenId = result.events.Transfer.returnValues.tokenId;252253    {254      const approved = await contract.methods.getApproved(tokenId).call();255      expect(approved).to.be.equal('0x0000000000000000000000000000000000000000');256    }257    await contract.methods.approve(operator, tokenId).send({from: owner});258    {259      const approved = await contract.methods.getApproved(tokenId).call();260      expect(approved).to.be.equal(operator);261    }262  });263264265  itEth('Can perform setApprovalForAll()', async ({helper}) => {266    const owner = await helper.eth.createAccountWithBalance(donor);267    const operator = helper.eth.createAccount();268269    const collection = await helper.nft.mintCollection(minter, {});270271    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);272    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);273274    const approvedBefore = await contract.methods.isApprovedForAll(owner, operator).call();275    expect(approvedBefore).to.be.equal(false);276277    {278      const result = await contract.methods.setApprovalForAll(operator, true).send({from: owner});279280      expect(result.events.ApprovalForAll).to.be.like({281        address: collectionAddress,282        event: 'ApprovalForAll',283        returnValues: {284          owner,285          operator,286          approved: true,287        },288      });289290      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();291      expect(approvedAfter).to.be.equal(true);292    }293294    {295      const result = await contract.methods.setApprovalForAll(operator, false).send({from: owner});296297      expect(result.events.ApprovalForAll).to.be.like({298        address: collectionAddress,299        event: 'ApprovalForAll',300        returnValues: {301          owner,302          operator,303          approved: false,304        },305      });306307      const approvedAfter = await contract.methods.isApprovedForAll(owner, operator).call();308      expect(approvedAfter).to.be.equal(false);309    }310  });311312  itEth('Can perform burn with ApprovalForAll', async ({helper}) => {313    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});314315    const owner = await helper.eth.createAccountWithBalance(donor);316    const operator = await helper.eth.createAccountWithBalance(donor, 100n);317318    const token = await collection.mintToken(minter, {Ethereum: owner});319320    const address = helper.ethAddress.fromCollectionId(collection.collectionId);321    const contract = await helper.ethNativeContract.collection(address, 'nft');322323    {324      await contract.methods.setApprovalForAll(operator, true).send({from: owner});325      const ownerCross = helper.ethCrossAccount.fromAddress(owner);326      const result = await contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: operator});327      const events = result.events.Transfer;328329      expect(events).to.be.like({330        address,331        event: 'Transfer',332        returnValues: {333          from: owner,334          to: '0x0000000000000000000000000000000000000000',335          tokenId: token.tokenId.toString(),336        },337      });338    }339340    expect(await helper.nft.doesTokenExist(collection.collectionId, token.tokenId)).to.be.false;341  });342343  itEth('Can perform transfer with ApprovalForAll', async ({helper}) => {344    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});345346    const owner = await helper.eth.createAccountWithBalance(donor);347    const operator = await helper.eth.createAccountWithBalance(donor);348    const receiver = charlie;349350    const token = await collection.mintToken(minter, {Ethereum: owner});351352    const address = helper.ethAddress.fromCollectionId(collection.collectionId);353    const contract = await helper.ethNativeContract.collection(address, 'nft');354355    {356      await contract.methods.setApprovalForAll(operator, true).send({from: owner});357      const ownerCross = helper.ethCrossAccount.fromAddress(owner);358      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);359      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: operator});360      const event = result.events.Transfer;361      expect(event).to.be.like({362        address: helper.ethAddress.fromCollectionId(collection.collectionId),363        event: 'Transfer',364        returnValues: {365          from: owner,366          to: helper.address.substrateToEth(receiver.address),367          tokenId: token.tokenId.toString(),368        },369      });370    }371372    expect(await token.getOwner()).to.be.like({Substrate: receiver.address});373  });374375  itEth('Can perform burnFromCross()', async ({helper}) => {376    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});377    const ownerSub = bob;378    const ownerCrossSub = helper.ethCrossAccount.fromKeyringPair(ownerSub);379    const ownerEth = await helper.eth.createAccountWithBalance(donor, 100n);380    const ownerCrossEth = helper.ethCrossAccount.fromAddress(ownerEth);381382    const burnerEth = await helper.eth.createAccountWithBalance(donor, 100n);383    const burnerCrossEth = helper.ethCrossAccount.fromAddress(burnerEth);384385    const token1 = await collection.mintToken(minter, {Substrate: ownerSub.address});386    const token2 = await collection.mintToken(minter, {Ethereum: ownerEth});387388    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);389    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft');390391    // Approve tokens from substrate and ethereum:392    await token1.approve(ownerSub, {Ethereum: burnerEth});393    await collectionEvm.methods.approveCross(burnerCrossEth, token2.tokenId).send({from: ownerEth});394395    // can burnFromCross:396    const result1 = await collectionEvm.methods.burnFromCross(ownerCrossSub, token1.tokenId).send({from: burnerEth});397    const result2 = await collectionEvm.methods.burnFromCross(ownerCrossEth, token2.tokenId).send({from: burnerEth});398    const events1 = result1.events.Transfer;399    const events2 = result2.events.Transfer;400401    // Check events for burnFromCross (substrate and ethereum):402    [403      [events1, token1, helper.address.substrateToEth(ownerSub.address)],404      [events2, token2, ownerEth],405    ].map(burnData => {406      expect(burnData[0]).to.be.like({407        address: collectionAddress,408        event: 'Transfer',409        returnValues: {410          from: burnData[2],411          to: '0x0000000000000000000000000000000000000000',412          tokenId: burnData[1].tokenId.toString(),413        },414      });415    });416417    expect(await token1.doesExist()).to.be.false;418    expect(await token2.doesExist()).to.be.false;419  });420421  // TODO combine all approve tests in one place422  itEth('Can perform approveCross()', async ({helper}) => {423    // arrange: create accounts424    const owner = await helper.eth.createAccountWithBalance(donor, 100n);425    const ownerCross = helper.ethCrossAccount.fromAddress(owner);426    const receiverSub = charlie;427    const recieverCrossSub = helper.ethCrossAccount.fromKeyringPair(receiverSub);428    const receiverEth = await helper.eth.createAccountWithBalance(donor, 100n);429    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);430431    // arrange: create collection and tokens:432    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});433    const token1 = await collection.mintToken(minter, {Ethereum: owner});434    const token2 = await collection.mintToken(minter, {Ethereum: owner});435436    const collectionEvm = await helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');437438    // Can approveCross substrate and ethereum address:439    const resultSub = await collectionEvm.methods.approveCross(recieverCrossSub, token1.tokenId).send({from: owner});440    const resultEth = await collectionEvm.methods.approveCross(receiverCrossEth, token2.tokenId).send({from: owner});441    const eventSub = resultSub.events.Approval;442    const eventEth = resultEth.events.Approval;443    expect(eventSub).to.be.like({444      address: helper.ethAddress.fromCollectionId(collection.collectionId),445      event: 'Approval',446      returnValues: {447        owner,448        approved: helper.address.substrateToEth(receiverSub.address),449        tokenId: token1.tokenId.toString(),450      },451    });452    expect(eventEth).to.be.like({453      address: helper.ethAddress.fromCollectionId(collection.collectionId),454      event: 'Approval',455      returnValues: {456        owner,457        approved: receiverEth,458        tokenId: token2.tokenId.toString(),459      },460    });461462    // Substrate address can transferFrom approved tokens:463    await helper.nft.transferTokenFrom(receiverSub, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiverSub.address});464    expect(await helper.nft.getTokenOwner(collection.collectionId, token1.tokenId)).to.deep.eq({Substrate: receiverSub.address});465    // Ethereum address can transferFromCross approved tokens:466    await collectionEvm.methods.transferFromCross(ownerCross, receiverCrossEth, token2.tokenId).send({from: receiverEth});467    expect(await helper.nft.getTokenOwner(collection.collectionId, token2.tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});468  });469470  itEth('Non-owner and non admin cannot approveCross', async ({helper}) => {471    const nonOwner = await helper.eth.createAccountWithBalance(donor);472    const nonOwnerCross = helper.ethCrossAccount.fromAddress(nonOwner);473    const owner = await helper.eth.createAccountWithBalance(donor);474    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});475    const collectionEvm = await helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');476    const token = await collection.mintToken(minter, {Ethereum: owner});477478    await expect(collectionEvm.methods.approveCross(nonOwnerCross, token.tokenId).call({from: nonOwner})).to.be.rejectedWith('CantApproveMoreThanOwned');479  });480481  itEth('Can reaffirm approved address', async ({helper}) => {482    const owner = await helper.eth.createAccountWithBalance(donor, 100n);483    const ownerCrossEth = helper.ethCrossAccount.fromAddress(owner);484    const [receiver1, receiver2] = await helper.arrange.createAccounts([100n, 100n], donor);485    const receiver1Cross = helper.ethCrossAccount.fromKeyringPair(receiver1);486    const receiver2Cross = helper.ethCrossAccount.fromKeyringPair(receiver2);487    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});488    const token1 = await collection.mintToken(minter, {Ethereum: owner});489    const token2 = await collection.mintToken(minter, {Ethereum: owner});490    const collectionEvm = await helper.ethNativeContract.collection(helper.ethAddress.fromCollectionId(collection.collectionId), 'nft');491492    // Can approve and reaffirm approved address:493    await collectionEvm.methods.approveCross(receiver1Cross, token1.tokenId).send({from: owner});494    await collectionEvm.methods.approveCross(receiver2Cross, token1.tokenId).send({from: owner});495496    // receiver1 cannot transferFrom:497    await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;498    // receiver2 can transferFrom:499    await helper.nft.transferTokenFrom(receiver2, collection.collectionId, token1.tokenId, {Ethereum: owner}, {Substrate: receiver2.address});500501    // can set approved address to self address to remove approval:502    await collectionEvm.methods.approveCross(receiver1Cross, token2.tokenId).send({from: owner});503    await collectionEvm.methods.approveCross(ownerCrossEth, token2.tokenId).send({from: owner});504505    // receiver1 cannot transfer token anymore:506    await expect(helper.nft.transferTokenFrom(receiver1, collection.collectionId, token2.tokenId, {Ethereum: owner}, {Substrate: receiver1.address})).to.be.rejected;507  });508509  itEth('Can perform transferFrom()', async ({helper}) => {510    const owner = await helper.eth.createAccountWithBalance(donor);511    const spender = await helper.eth.createAccountWithBalance(donor);512    const receiver = helper.eth.createAccount();513514    const collection = await helper.nft.mintCollection(minter, {});515    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});516517    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);518    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);519520    await contract.methods.approve(spender, tokenId).send({from: owner});521522    {523      const result = await contract.methods.transferFrom(owner, receiver, tokenId).send({from: spender});524525      const event = result.events.Transfer;526      expect(event.address).to.be.equal(collectionAddress);527      expect(event.returnValues.from).to.be.equal(owner);528      expect(event.returnValues.to).to.be.equal(receiver);529      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);530    }531532    {533      const balance = await contract.methods.balanceOf(receiver).call();534      expect(+balance).to.equal(1);535    }536537    {538      const balance = await contract.methods.balanceOf(owner).call();539      expect(+balance).to.equal(0);540    }541  });542543  itEth('Can perform transferFromCross()', async ({helper}) => {544    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});545546    const [owner, receiver] = await helper.arrange.createAccounts([100n, 100n], donor);547    const spender = await helper.eth.createAccountWithBalance(donor);548549    const token = await collection.mintToken(minter, {Substrate: owner.address});550551    const address = helper.ethAddress.fromCollectionId(collection.collectionId);552    const contract = await helper.ethNativeContract.collection(address, 'nft');553554    await token.approve(owner, {Ethereum: spender});555556    {557      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);558      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);559      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});560      const event = result.events.Transfer;561      expect(event).to.be.like({562        address: helper.ethAddress.fromCollectionId(collection.collectionId),563        event: 'Transfer',564        returnValues: {565          from: helper.address.substrateToEth(owner.address),566          to: helper.address.substrateToEth(receiver.address),567          tokenId: token.tokenId.toString(),568        },569      });570    }571572    expect(await token.getOwner()).to.be.like({Substrate: receiver.address});573  });574575  itEth('Can perform transfer()', async ({helper}) => {576    const collection = await helper.nft.mintCollection(minter, {});577    const owner = await helper.eth.createAccountWithBalance(donor);578    const receiver = helper.eth.createAccount();579580    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});581582    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);583    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);584585    {586      const result = await contract.methods.transfer(receiver, tokenId).send({from: owner});587588      const event = result.events.Transfer;589      expect(event.address).to.be.equal(collectionAddress);590      expect(event.returnValues.from).to.be.equal(owner);591      expect(event.returnValues.to).to.be.equal(receiver);592      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);593    }594595    {596      const balance = await contract.methods.balanceOf(owner).call();597      expect(+balance).to.equal(0);598    }599600    {601      const balance = await contract.methods.balanceOf(receiver).call();602      expect(+balance).to.equal(1);603    }604  });605606  itEth('Can perform transferCross()', async ({helper}) => {607    const collection = await helper.nft.mintCollection(minter, {});608    const owner = await helper.eth.createAccountWithBalance(donor);609    const receiverEth = await helper.eth.createAccountWithBalance(donor);610    const receiverCrossEth = helper.ethCrossAccount.fromAddress(receiverEth);611    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);612613    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});614615    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);616    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);617618    {619      // Can transferCross to ethereum address:620      const result = await collectionEvm.methods.transferCross(receiverCrossEth, tokenId).send({from: owner});621      // Check events:622      const event = result.events.Transfer;623      expect(event.address).to.be.equal(collectionAddress);624      expect(event.returnValues.from).to.be.equal(owner);625      expect(event.returnValues.to).to.be.equal(receiverEth);626      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);627628      // owner has balance = 0:629      const ownerBalance = await collectionEvm.methods.balanceOf(owner).call();630      expect(+ownerBalance).to.equal(0);631      // receiver owns token:632      const receiverBalance = await collectionEvm.methods.balanceOf(receiverEth).call();633      expect(+receiverBalance).to.equal(1);634      expect(await helper.nft.getTokenOwner(collection.collectionId, tokenId)).to.deep.eq({Ethereum: receiverEth.toLowerCase()});635    }636637    {638      // Can transferCross to substrate address:639      const substrateResult = await collectionEvm.methods.transferCross(receiverCrossSub, tokenId).send({from: receiverEth});640      // Check events:641      const event = substrateResult.events.Transfer;642      expect(event.address).to.be.equal(collectionAddress);643      expect(event.returnValues.from).to.be.equal(receiverEth);644      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));645      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);646647      // owner has balance = 0:648      const ownerBalance = await collectionEvm.methods.balanceOf(receiverEth).call();649      expect(+ownerBalance).to.equal(0);650      // receiver owns token:651      const receiverBalance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});652      expect(receiverBalance).to.contain(tokenId);653    }654  });655656  ['transfer', 'transferCross'].map(testCase => itEth(`Cannot ${testCase} non-owned token`, async ({helper}) => {657    const sender = await helper.eth.createAccountWithBalance(donor);658    const tokenOwner = await helper.eth.createAccountWithBalance(donor);659    const receiverSub = minter;660    const receiverCrossSub = helper.ethCrossAccount.fromKeyringPair(minter);661662    const collection = await helper.nft.mintCollection(minter, {});663    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);664    const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', sender);665666    await collection.mintToken(minter, {Ethereum: sender});667    const nonSendersToken = await collection.mintToken(minter, {Ethereum: tokenOwner});668669    // Cannot transferCross someone else's token:670    const receiver = testCase === 'transfer' ? helper.address.substrateToEth(receiverSub.address) : receiverCrossSub;671    await expect(collectionEvm.methods[testCase](receiver, nonSendersToken.tokenId).send({from: sender})).to.be.rejected;672    // Cannot transfer token if it does not exist:673    await expect(collectionEvm.methods[testCase](receiver, 999999).send({from: sender})).to.be.rejected;674  }));675});676677describe('NFT: Fees', () => {678  let donor: IKeyringPair;679  let alice: IKeyringPair;680  let bob: IKeyringPair;681  let charlie: IKeyringPair;682683  before(async function() {684    await usingEthPlaygrounds(async (helper, privateKey) => {685      donor = await privateKey({filename: __filename});686      [alice, bob, charlie] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);687    });688  });689690  itEth('approve() call fee is less than 0.2UNQ', async ({helper}) => {691    const owner = await helper.eth.createAccountWithBalance(donor);692    const spender = helper.eth.createAccount();693694    const collection = await helper.nft.mintCollection(alice, {});695    const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});696697    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);698699    const cost = await helper.eth.recordCallFee(owner, () => contract.methods.approve(spender, tokenId).send({from: owner}));700    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));701  });702703  itEth('transferFrom() call fee is less than 0.2UNQ', async ({helper}) => {704    const owner = await helper.eth.createAccountWithBalance(donor);705    const spender = await helper.eth.createAccountWithBalance(donor);706707    const collection = await helper.nft.mintCollection(alice, {});708    const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});709710    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);711712    await contract.methods.approve(spender, tokenId).send({from: owner});713714    const cost = await helper.eth.recordCallFee(spender, () => contract.methods.transferFrom(owner, spender, tokenId).send({from: spender}));715    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));716  });717718  itEth('Can perform transferFromCross()', async ({helper}) => {719    const collectionMinter = alice;720    const owner = bob;721    const receiver = charlie;722    const collection = await helper.nft.mintCollection(collectionMinter, {name: 'A', description: 'B', tokenPrefix: 'C'});723724    const spender = await helper.eth.createAccountWithBalance(donor, 100n);725726    const token = await collection.mintToken(collectionMinter, {Substrate: owner.address});727728    const address = helper.ethAddress.fromCollectionId(collection.collectionId);729    const contract = await helper.ethNativeContract.collection(address, 'nft');730731    await token.approve(owner, {Ethereum: spender});732733    {734      const ownerCross = helper.ethCrossAccount.fromKeyringPair(owner);735      const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);736      const result = await contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender});737      const event = result.events.Transfer;738      expect(event).to.be.like({739        address: helper.ethAddress.fromCollectionId(collection.collectionId),740        event: 'Transfer',741        returnValues: {742          from: helper.address.substrateToEth(owner.address),743          to: helper.address.substrateToEth(receiver.address),744          tokenId: token.tokenId.toString(),745        },746      });747    }748749    expect(await token.getOwner()).to.be.like({Substrate: receiver.address});750  });751752  itEth('transfer() call fee is less than 0.2UNQ', async ({helper}) => {753    const owner = await helper.eth.createAccountWithBalance(donor);754    const receiver = helper.eth.createAccount();755756    const collection = await helper.nft.mintCollection(alice, {});757    const {tokenId} = await collection.mintToken(alice, {Ethereum: owner});758759    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', owner);760761    const cost = await helper.eth.recordCallFee(owner, () => contract.methods.transfer(receiver, tokenId).send({from: owner}));762    expect(cost < BigInt(0.2 * Number(helper.balance.getOneTokenNominal())));763  });764});765766describe('NFT: Substrate calls', () => {767  let donor: IKeyringPair;768  let alice: IKeyringPair;769770  before(async function() {771    await usingEthPlaygrounds(async (helper, privateKey) => {772      donor = await privateKey({filename: __filename});773      [alice] = await helper.arrange.createAccounts([20n], donor);774    });775  });776777  itEth('Events emitted for mint()', async ({helper}) => {778    const collection = await helper.nft.mintCollection(alice, {});779    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);780    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');781782    const events: any = [];783    contract.events.allEvents((_: any, event: any) => {784      events.push(event);785    });786787    const {tokenId} = await collection.mintToken(alice);788    if (events.length == 0) await helper.wait.newBlocks(1);789    const event = events[0];790791    expect(event.event).to.be.equal('Transfer');792    expect(event.address).to.be.equal(collectionAddress);793    expect(event.returnValues.from).to.be.equal('0x0000000000000000000000000000000000000000');794    expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(alice.address));795    expect(event.returnValues.tokenId).to.be.equal(tokenId.toString());796  });797798  itEth('Events emitted for burn()', async ({helper}) => {799    const collection = await helper.nft.mintCollection(alice, {});800    const token = await collection.mintToken(alice);801802    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);803    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');804805    const events: any = [];806    contract.events.allEvents((_: any, event: any) => {807      events.push(event);808    });809810    await token.burn(alice);811    if (events.length == 0) await helper.wait.newBlocks(1);812    const event = events[0];813814    expect(event.event).to.be.equal('Transfer');815    expect(event.address).to.be.equal(collectionAddress);816    expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));817    expect(event.returnValues.to).to.be.equal('0x0000000000000000000000000000000000000000');818    expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());819  });820821  itEth('Events emitted for approve()', async ({helper}) => {822    const receiver = helper.eth.createAccount();823824    const collection = await helper.nft.mintCollection(alice, {});825    const token = await collection.mintToken(alice);826827    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);828    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');829830    const events: any = [];831    contract.events.allEvents((_: any, event: any) => {832      events.push(event);833    });834835    await token.approve(alice, {Ethereum: receiver});836    if (events.length == 0) await helper.wait.newBlocks(1);837    const event = events[0];838839    expect(event.event).to.be.equal('Approval');840    expect(event.address).to.be.equal(collectionAddress);841    expect(event.returnValues.owner).to.be.equal(helper.address.substrateToEth(alice.address));842    expect(event.returnValues.approved).to.be.equal(receiver);843    expect(event.returnValues.tokenId).to.be.equal(token.tokenId.toString());844  });845846  itEth('Events emitted for transferFrom()', async ({helper}) => {847    const [bob] = await helper.arrange.createAccounts([10n], donor);848    const receiver = helper.eth.createAccount();849850    const collection = await helper.nft.mintCollection(alice, {});851    const token = await collection.mintToken(alice);852    await token.approve(alice, {Substrate: bob.address});853854    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);855    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');856857    const events: any = [];858    contract.events.allEvents((_: any, event: any) => {859      events.push(event);860    });861862    await token.transferFrom(bob, {Substrate: alice.address}, {Ethereum: receiver});863864    if (events.length == 0) await helper.wait.newBlocks(1);865    const event = events[0];866867    expect(event.address).to.be.equal(collectionAddress);868    expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));869    expect(event.returnValues.to).to.be.equal(receiver);870    expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);871  });872873  itEth('Events emitted for transfer()', async ({helper}) => {874    const receiver = helper.eth.createAccount();875876    const collection = await helper.nft.mintCollection(alice, {});877    const token = await collection.mintToken(alice);878879    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);880    const contract = await helper.ethNativeContract.collection(collectionAddress, 'nft');881882    const events: any = [];883    contract.events.allEvents((_: any, event: any) => {884      events.push(event);885    });886887    await token.transfer(alice, {Ethereum: receiver});888889    if (events.length == 0) await helper.wait.newBlocks(1);890    const event = events[0];891892    expect(event.address).to.be.equal(collectionAddress);893    expect(event.returnValues.from).to.be.equal(helper.address.substrateToEth(alice.address));894    expect(event.returnValues.to).to.be.equal(receiver);895    expect(event.returnValues.tokenId).to.be.equal(`${token.tokenId}`);896  });897});898899describe('Common metadata', () => {900  let donor: IKeyringPair;901  let alice: IKeyringPair;902903  before(async function() {904    await usingEthPlaygrounds(async (helper, privateKey) => {905      donor = await privateKey({filename: __filename});906      [alice] = await helper.arrange.createAccounts([20n], donor);907    });908  });909910  itEth('Returns collection name', async ({helper}) => {911    const caller = await helper.eth.createAccountWithBalance(donor);912    const tokenPropertyPermissions = [{913      key: 'URI',914      permission: {915        mutable: true,916        collectionAdmin: true,917        tokenOwner: false,918      },919    }];920    const collection = await helper.nft.mintCollection(921      alice,922      {923        name: 'oh River',924        tokenPrefix: 'CHANGE',925        properties: [{key: 'ERC721Metadata', value: '1'}],926        tokenPropertyPermissions,927      },928    );929930    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);931    const name = await contract.methods.name().call();932    expect(name).to.equal('oh River');933  });934935  itEth('Returns symbol name', async ({helper}) => {936    const caller = await helper.eth.createAccountWithBalance(donor);937    const tokenPropertyPermissions = [{938      key: 'URI',939      permission: {940        mutable: true,941        collectionAdmin: true,942        tokenOwner: false,943      },944    }];945    const collection = await helper.nft.mintCollection(946      alice,947      {948        name: 'oh River',949        tokenPrefix: 'CHANGE',950        properties: [{key: 'ERC721Metadata', value: '1'}],951        tokenPropertyPermissions,952      },953    );954955    const contract = await helper.ethNativeContract.collectionById(collection.collectionId, 'nft', caller);956    const symbol = await contract.methods.symbol().call();957    expect(symbol).to.equal('CHANGE');958  });959});960961describe('Negative tests', () => {962  let donor: IKeyringPair;963  let minter: IKeyringPair;964  let alice: IKeyringPair;965966  before(async function() {967    await usingEthPlaygrounds(async (helper, privateKey) => {968      donor = await privateKey({filename: __filename});969      [minter, alice] = await helper.arrange.createAccounts([100n, 100n], donor);970    });971  });972973  itEth('[negative] Cant perform burn without approval', async ({helper}) => {974    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});975976    const owner = await helper.eth.createAccountWithBalance(donor, 100n);977    const spender = await helper.eth.createAccountWithBalance(donor, 100n);978979    const token = await collection.mintToken(minter, {Ethereum: owner});980981    const address = helper.ethAddress.fromCollectionId(collection.collectionId);982    const contract = await helper.ethNativeContract.collection(address, 'nft');983984    const ownerCross = helper.ethCrossAccount.fromAddress(owner);985    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;986987    await contract.methods.setApprovalForAll(spender, true).send({from: owner});988    await contract.methods.setApprovalForAll(spender, false).send({from: owner});989990    await expect(contract.methods.burnFromCross(ownerCross, token.tokenId).send({from: spender})).to.be.rejected;991  });992993  itEth('[negative] Cant perform transfer without approval', async ({helper}) => {994    const collection = await helper.nft.mintCollection(minter, {name: 'A', description: 'B', tokenPrefix: 'C'});995    const receiver = alice;996997    const owner = await helper.eth.createAccountWithBalance(donor, 100n);998    const spender = await helper.eth.createAccountWithBalance(donor, 100n);9991000    const token = await collection.mintToken(minter, {Ethereum: owner});10011002    const address = helper.ethAddress.fromCollectionId(collection.collectionId);1003    const contract = await helper.ethNativeContract.collection(address, 'nft');10041005    const ownerCross = helper.ethCrossAccount.fromAddress(owner);1006    const recieverCross = helper.ethCrossAccount.fromKeyringPair(receiver);10071008    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;10091010    await contract.methods.setApprovalForAll(spender, true).send({from: owner});1011    await contract.methods.setApprovalForAll(spender, false).send({from: owner});10121013    await expect(contract.methods.transferFromCross(ownerCross, recieverCross, token.tokenId).send({from: spender})).to.be.rejected;1014  });1015});