1234567891011121314151617import {IKeyringPair} from '@polkadot/types/types';18import {evmToAddress} from '@polkadot/util-crypto';19import {Pallets, requirePalletsOrSkip} from '../util';20import {expect, itEth, usingEthPlaygrounds} from './util';21import {CREATE_COLLECTION_DATA_DEFAULTS, CollectionLimitField, CollectionMode, CreateCollectionData, TokenPermissionField, emptyAddress} from './util/playgrounds/types';22import {CollectionFlag, IEthCrossAccountId, TCollectionMode} from '../util/playgrounds/types';2324const DECIMALS = 18;25const CREATE_COLLECTION_DATA_DEFAULTS_ARRAY = [26 [], 27 [], 28 [], 29 [false, false, []], 30 [], 31 emptyAddress, 32 [0], 33];3435type ElementOf<A> = A extends readonly (infer T)[] ? T : never;36function* cartesian<T extends Array<Array<any>>, R extends Array<any>>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf<T[K]>}]> {37 if(args.length === 0) {38 yield internalRest as any;39 return;40 }41 for(const value of args[0]) {42 yield* cartesian([...internalRest, value], ...args.slice(1)) as any;43 }44}4546describe('Create collection from EVM', () => {47 let donor: IKeyringPair;48 let nominal: bigint;4950 before(async function() {51 await usingEthPlaygrounds(async (helper, privateKey) => {52 donor = await privateKey({url: import.meta.url});53 nominal = helper.balance.getOneTokenNominal();54 });55 });5657 describe('Fungible collection', () => {58 before(async function() {59 await usingEthPlaygrounds((helper) => {60 requirePalletsOrSkip(this, helper, [Pallets.Fungible]);61 return Promise.resolve();62 });63 });6465 itEth('Collection address exist', async ({helper}) => {66 const owner = await helper.eth.createAccountWithBalance(donor);67 const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233';68 const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);6970 expect(await collectionHelpers71 .methods.isCollectionExist(collectionAddressForNonexistentCollection).call())72 .to.be.false;7374 const {collectionAddress} = await helper.eth.createCollection(owner, new CreateCollectionData('Exister', 'absolutely anything', 'WIWT', 'ft', DECIMALS)).send();75 expect(await collectionHelpers76 .methods.isCollectionExist(collectionAddress).call())77 .to.be.true;7879 80 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'ft', owner, true);81 const collectionOwner = await collectionEvm.methods.collectionOwner().call();82 expect(helper.address.restoreCrossAccountFromBigInt(BigInt(collectionOwner.sub))).to.eq(helper.address.ethToSubstrate(owner, true));83 });8485 itEth('destroyCollection', async ({helper}) => {86 const owner = await helper.eth.createAccountWithBalance(donor);87 const {collectionAddress, collectionId} = await helper.eth.createCollection(owner, new CreateCollectionData('Exister', 'absolutely anything', 'WIWT', 'ft', DECIMALS)).send();88 const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);8990 const result = await collectionHelper.methods91 .destroyCollection(collectionAddress)92 .send({from: owner});9394 const events = helper.eth.normalizeEvents(result.events);9596 expect(events).to.be.deep.equal([97 {98 address: collectionHelper.options.address,99 event: 'CollectionDestroyed',100 args: {101 collectionId: collectionAddress,102 },103 },104 ]);105106 expect(await collectionHelper.methods107 .isCollectionExist(collectionAddress)108 .call()).to.be.false;109 expect(await helper.collection.getData(collectionId)).to.be.null;110 });111112 itEth('(!negative test!) Create collection (bad lengths)', async ({helper}) => {113 const owner = await helper.eth.createAccountWithBalance(donor);114 const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);115 {116 const MAX_NAME_LENGTH = 64;117 const collectionName = 'A'.repeat(MAX_NAME_LENGTH + 1);118 const description = 'A';119 const tokenPrefix = 'A';120121 await expect(collectionHelper.methods122 .createCollection([123 collectionName,124 description,125 tokenPrefix,126 CollectionMode.Fungible,127 DECIMALS,128 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,129 ])130 .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);131 }132 {133 const MAX_DESCRIPTION_LENGTH = 256;134 const collectionName = 'A';135 const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1);136 const tokenPrefix = 'A';137 await expect(collectionHelper.methods138 .createCollection([139 collectionName,140 description,141 tokenPrefix,142 CollectionMode.Fungible,143 DECIMALS,144 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,145 ])146 .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH);147 }148 {149 const MAX_TOKEN_PREFIX_LENGTH = 16;150 const collectionName = 'A';151 const description = 'A';152 const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1);153 await expect(collectionHelper.methods154 .createCollection([155 collectionName,156 description,157 tokenPrefix,158 CollectionMode.Fungible,159 DECIMALS,160 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,161 ])162 .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);163 }164 });165166 itEth('(!negative test!) cannot create collection if value !== 2', async ({helper}) => {167 const owner = await helper.eth.createAccountWithBalance(donor);168 const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);169 const expects = [0n, 1n, 30n].map(async value => {170 await expect(collectionHelper.methods171 .createCollection([172 'Peasantry',173 'absolutely anything',174 'TWIW',175 CollectionMode.Fungible,176 DECIMALS,177 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,178 ])179 .call({value: Number(value * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');180 });181 await Promise.all(expects);182 });183 });184185 describe('Nonfungible collection', () => {186 before(async function() {187 await usingEthPlaygrounds((helper) => {188 requirePalletsOrSkip(this, helper, [Pallets.NFT]);189 return Promise.resolve();190 });191 });192193 itEth('Create collection', async ({helper}) => {194 const owner = await helper.eth.createAccountWithBalance(donor);195196 const name = 'CollectionEVM';197 const description = 'Some description';198 const prefix = 'token prefix';199200 201 const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;202 const {collectionId, collectionAddress, events} = await helper.eth.createCollection(owner, new CreateCollectionData(name, description, prefix, 'nft')).send();203204 expect(events).to.be.deep.equal([205 {206 address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',207 event: 'CollectionCreated',208 args: {209 owner: owner,210 collectionId: collectionAddress,211 },212 },213 ]);214215 const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;216217 const collection = helper.nft.getCollectionObject(collectionId);218 const data = (await collection.getData())!;219220 221 expect(collectionCountAfter - collectionCountBefore).to.be.gte(1);222 expect(collectionId).to.be.eq(collectionCountAfter);223 expect(data.name).to.be.eq(name);224 expect(data.description).to.be.eq(description);225 expect(data.raw.tokenPrefix).to.be.eq(prefix);226 expect(data.raw.mode).to.be.eq('NFT');227228 const options = await collection.getOptions();229230 expect(options.tokenPropertyPermissions).to.be.empty;231 });232233 234 itEth('Check collection address exist', async ({helper}) => {235 const owner = await helper.eth.createAccountWithBalance(donor);236237 const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1;238 const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId);239 const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);240241 expect(await collectionHelpers.methods242 .isCollectionExist(expectedCollectionAddress)243 .call()).to.be.false;244245 await collectionHelpers.methods246 .createCollection([247 'A',248 'A',249 'A',250 CollectionMode.Nonfungible,251 0,252 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,253 ])254 .send({value: Number(2n * helper.balance.getOneTokenNominal())});255256 expect(await collectionHelpers.methods257 .isCollectionExist(expectedCollectionAddress)258 .call()).to.be.true;259 });260 });261262 describe('Create RFT collection from EVM', () => {263 before(async function() {264 await usingEthPlaygrounds((helper) => {265 requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);266 return Promise.resolve();267 });268 });269270 itEth('Create collection', async ({helper}) => {271 const owner = await helper.eth.createAccountWithBalance(donor);272273 const name = 'CollectionEVM';274 const description = 'Some description';275 const prefix = 'token prefix';276277 const {collectionId} = await helper.eth.createCollection(owner, new CreateCollectionData(name, description, prefix, 'rft')).send();278 const data = (await helper.rft.getData(collectionId))!;279 const collection = helper.rft.getCollectionObject(collectionId);280281 expect(data.name).to.be.eq(name);282 expect(data.description).to.be.eq(description);283 expect(data.raw.tokenPrefix).to.be.eq(prefix);284 expect(data.raw.mode).to.be.eq('ReFungible');285286 const options = await collection.getOptions();287288 expect(options.tokenPropertyPermissions).to.be.empty;289 });290291 itEth('Create collection with properties & get description', async ({helper}) => {292 const owner = await helper.eth.createAccountWithBalance(donor);293294 const name = 'CollectionEVM';295 const description = 'Some description';296 const prefix = 'token prefix';297 const baseUri = 'BaseURI';298299 const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, name, description, prefix, baseUri);300 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft');301302 const collection = helper.rft.getCollectionObject(collectionId);303 const data = (await collection.getData())!;304305 expect(data.name).to.be.eq(name);306 expect(data.description).to.be.eq(description);307 expect(data.raw.tokenPrefix).to.be.eq(prefix);308 expect(data.raw.mode).to.be.eq('ReFungible');309310 expect(await contract.methods.description().call()).to.deep.equal(description);311312 const options = await collection.getOptions();313 expect(options.tokenPropertyPermissions).to.be.deep.equal([314 {315 key: 'URI',316 permission: {mutable: true, collectionAdmin: true, tokenOwner: false},317 },318 {319 key: 'URISuffix',320 permission: {mutable: true, collectionAdmin: true, tokenOwner: false},321 },322 ]);323 });324325 itEth('Set sponsorship', async ({helper}) => {326 const owner = await helper.eth.createAccountWithBalance(donor);327 const sponsor = await helper.eth.createAccountWithBalance(donor);328 const ss58Format = helper.chain.getChainProperties().ss58Format;329 const {collectionId, collectionAddress} = await helper.eth.createCollection(owner, new CreateCollectionData('Sponsor', 'absolutely anything', 'ENVY', 'rft')).send();330331 const collection = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);332 const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor);333 await collection.methods.setCollectionSponsorCross(sponsorCross).send();334335 let data = (await helper.rft.getData(collectionId))!;336 expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));337338 await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmSponsorshipFail');339340 const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor);341 await sponsorCollection.methods.confirmCollectionSponsorship().send();342343 data = (await helper.rft.getData(collectionId))!;344 expect(data.raw.sponsorship.Confirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));345 });346347 itEth('Collection address exist', async ({helper}) => {348 const owner = await helper.eth.createAccountWithBalance(donor);349 const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233';350 const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);351352 expect(await collectionHelpers353 .methods.isCollectionExist(collectionAddressForNonexistentCollection).call())354 .to.be.false;355356 const {collectionAddress} = await helper.eth.createCollection(owner, new CreateCollectionData('Exister', 'absolutely anything', 'WIWT', 'rft')).send();357 expect(await collectionHelpers358 .methods.isCollectionExist(collectionAddress).call())359 .to.be.true;360361 362 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'ft', owner, true);363 const collectionOwner = await collectionEvm.methods.collectionOwner().call();364 expect(helper.address.restoreCrossAccountFromBigInt(BigInt(collectionOwner.sub))).to.eq(helper.address.ethToSubstrate(owner, true));365 });366367 itEth('destroyCollection', async ({helper}) => {368 const owner = await helper.eth.createAccountWithBalance(donor);369 const {collectionAddress, collectionId} = await helper.eth.createCollection(owner, new CreateCollectionData('Limits', 'absolutely anything', 'OLF', 'rft')).send();370 const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);371372 await expect(collectionHelper.methods373 .destroyCollection(collectionAddress)374 .send({from: owner})).to.be.fulfilled;375376 expect(await collectionHelper.methods377 .isCollectionExist(collectionAddress)378 .call()).to.be.false;379 expect(await helper.collection.getData(collectionId)).to.be.null;380 });381382 itEth('(!negative test!) Create collection (bad lengths)', async ({helper}) => {383 const owner = await helper.eth.createAccountWithBalance(donor);384 const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);385 {386 const MAX_NAME_LENGTH = 64;387 const collectionName = 'A'.repeat(MAX_NAME_LENGTH + 1);388 const description = 'A';389 const tokenPrefix = 'A';390391 await expect(collectionHelper.methods392 .createCollection([393 collectionName,394 description,395 tokenPrefix,396 CollectionMode.Refungible,397 DECIMALS,398 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,399 ])400 .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);401 }402 {403 const MAX_DESCRIPTION_LENGTH = 256;404 const collectionName = 'A';405 const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1);406 const tokenPrefix = 'A';407 await expect(collectionHelper.methods408 .createCollection([409 collectionName,410 description,411 tokenPrefix,412 CollectionMode.Refungible,413 DECIMALS,414 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,415 ])416 .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH);417 }418 {419 const MAX_TOKEN_PREFIX_LENGTH = 16;420 const collectionName = 'A';421 const description = 'A';422 const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1);423 await expect(collectionHelper.methods424 .createCollection([425 collectionName,426 description,427 tokenPrefix,428 CollectionMode.Refungible,429 DECIMALS,430 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,431 ])432 .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);433 }434 });435436 itEth('(!negative test!) Create collection (no funds)', async ({helper}) => {437 const owner = await helper.eth.createAccountWithBalance(donor);438 const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);439 await expect(collectionHelper.methods440 .createCollection([441 'Peasantry',442 'absolutely anything',443 'TWIW',444 CollectionMode.Refungible,445 0,446 ...CREATE_COLLECTION_DATA_DEFAULTS_ARRAY,447 ])448 .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');449 });450 });451452 describe('Sponsoring', () => {453 itEth('Сan remove collection sponsor', async ({helper}) => {454 const owner = await helper.eth.createAccountWithBalance(donor);455 const sponsor = await helper.eth.createAccountWithBalance(donor);456 const sponsorCross = helper.ethCrossAccount.fromAddr(sponsor);457458 const {collectionAddress} = await helper.eth.createCollection(459 owner,460 {461 ...CREATE_COLLECTION_DATA_DEFAULTS,462 name: 'Sponsor collection',463 description: '1',464 tokenPrefix: '1',465 collectionMode: 'nft',466 pendingSponsor: sponsorCross,467 },468 ).send();469 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);470471 expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true;472473 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});474 let sponsorStruct = await collectionEvm.methods.collectionSponsor().call({from: owner});475 expect(helper.address.restoreCrossAccountFromBigInt(BigInt(sponsorStruct.sub))).to.be.eq(helper.address.ethToSubstrate(sponsor, true));476 expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false;477478 await collectionEvm.methods.removeCollectionSponsor().send({from: owner});479480 sponsorStruct = await collectionEvm.methods.collectionSponsor().call({from: owner});481 expect(sponsorStruct.eth).to.be.eq('0x0000000000000000000000000000000000000000');482 });483484 itEth('Can sponsor from evm address via access list', async ({helper}) => {485 const owner = await helper.eth.createAccountWithBalance(donor);486 const sponsorEth = await helper.eth.createAccountWithBalance(donor);487 const sponsorCross = helper.ethCrossAccount.fromAddr(sponsorEth);488489 const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleCollection(490 owner,491 {492 ...CREATE_COLLECTION_DATA_DEFAULTS,493 name: 'Sponsor collection',494 description: '1',495 tokenPrefix: '1',496 collectionMode: 'nft',497 pendingSponsor: sponsorCross,498 limits: [{field: CollectionLimitField.SponsoredDataRateLimit, value: 30n}],499 tokenPropertyPermissions: [{key: 'key', permissions: [{code: TokenPermissionField.TokenOwner, value: true}]}],500 },501 '',502 );503504 const collectionSub = helper.nft.getCollectionObject(collectionId);505 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);506507 let sponsorship = (await collectionSub.getData())!.raw.sponsorship;508 expect(sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsorEth, true));509 510 await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmSponsorshipFail');511512 513 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsorEth});514 sponsorship = (await collectionSub.getData())!.raw.sponsorship;515 expect(sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsorEth, true));516517 518 const user = helper.ethCrossAccount.createAccount();519 const nextTokenId = await collectionEvm.methods.nextTokenId().call();520 expect(nextTokenId).to.be.equal('1');521522 523 const oldPermissions = (await collectionSub.getData())!.raw.permissions;524 expect(oldPermissions.mintMode).to.be.false;525 expect(oldPermissions.access).to.be.equal('Normal');526527 await collectionEvm.methods.setCollectionAccess(1 ).send({from: owner});528 await collectionEvm.methods.addToCollectionAllowListCross(user).send({from: owner});529 await collectionEvm.methods.setCollectionMintMode(true).send({from: owner});530531 const newPermissions = (await collectionSub.getData())!.raw.permissions;532 expect(newPermissions.mintMode).to.be.true;533 expect(newPermissions.access).to.be.equal('AllowList');534535 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));536 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));537 const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));538539 540 {541 const result = await collectionEvm.methods.mintCross(user, [{key: 'key', value: Buffer.from('Value')}]).send({from: user.eth});542 const event = helper.eth.normalizeEvents(result.events)543 .find(event => event.event === 'Transfer');544545 expect(event).to.be.deep.equal({546 address: collectionAddress,547 event: 'Transfer',548 args: {549 from: '0x0000000000000000000000000000000000000000',550 to: user.eth,551 tokenId: '1',552 },553 });554555 556557 const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));558 const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsorEth));559 const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user.eth));560561 expect(await collectionEvm.methods.properties(nextTokenId, []).call())562 .to.be.like([563 [564 'key',565 '0x' + Buffer.from('Value').toString('hex'),566 ],567 ]);568 expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter);569 expect(userBalanceAfter).to.be.eq(userBalanceBefore);570 expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true;571 }572 });573574 itEth('Check that transaction via EVM spend money from sponsor address', async ({helper}) => {575 const owner = await helper.eth.createAccountWithBalance(donor);576 const sponsor = await helper.eth.createAccountWithBalance(donor);577 const sponsorCross = helper.ethCrossAccount.fromAddr(sponsor);578 const user = helper.eth.createAccount();579 const userCross = helper.ethCrossAccount.fromAddress(user);580581 const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleCollection(582 owner,583 {584 ...CREATE_COLLECTION_DATA_DEFAULTS,585 name: 'Sponsor collection',586 description: '1',587 tokenPrefix: '1',588 collectionMode: 'nft',589 pendingSponsor: sponsorCross,590 adminList: [userCross],591 },592 '',593 );594595 const collectionSub = helper.nft.getCollectionObject(collectionId);596 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);597 598 let collectionData = (await collectionSub.getData())!;599 expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));600 await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('ConfirmSponsorshipFail');601602 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});603 collectionData = (await collectionSub.getData())!;604 expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true));605606 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));607 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));608609 const mintingResult = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user});610 const tokenId = mintingResult.events.Transfer.returnValues.tokenId;611612 const event = helper.eth.normalizeEvents(mintingResult.events)613 .find(event => event.event === 'Transfer');614 const address = helper.ethAddress.fromCollectionId(collectionId);615616 expect(event).to.be.deep.equal({617 address,618 event: 'Transfer',619 args: {620 from: '0x0000000000000000000000000000000000000000',621 to: user,622 tokenId: '1',623 },624 });625 expect(await collectionEvm.methods.tokenURI(tokenId).call({from: user})).to.be.equal('Test URI');626627 const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));628 expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);629 const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));630 expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;631 });632633 itEth('Can reassign collection sponsor', async ({helper}) => {634 const owner = await helper.eth.createAccountWithBalance(donor);635 const sponsorEth = await helper.eth.createAccountWithBalance(donor);636 const sponsorCrossEth = helper.ethCrossAccount.fromAddr(sponsorEth);637 const [sponsorSub] = await helper.arrange.createAccounts([100n], donor);638 const sponsorCrossSub = helper.ethCrossAccount.fromKeyringPair(sponsorSub);639640 const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleCollection(641 owner,642 {643 ...CREATE_COLLECTION_DATA_DEFAULTS,644 name: 'Sponsor collection',645 description: '1',646 tokenPrefix: '1',647 collectionMode: 'nft',648 pendingSponsor: sponsorCrossEth,649 },650 '',651 );652 const collectionSub = helper.nft.getCollectionObject(collectionId);653 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);654655 656 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsorEth});657658 659 await collectionEvm.methods.setCollectionSponsorCross(sponsorCrossSub).send({from: owner});660 const collectionSponsor = (await collectionSub.getData())?.raw.sponsorship;661 expect(collectionSponsor).to.deep.eq({Unconfirmed: sponsorSub.address});662 });663664 [665 'transfer',666 'transferCross',667 'transferFrom',668 'transferFromCross',669 ].map(testCase =>670 itEth(`[${testCase}] Check that transfer via EVM spend money from sponsor address`, async ({helper}) => {671 const owner = await helper.eth.createAccountWithBalance(donor);672 const sponsor = await helper.eth.createAccountWithBalance(donor);673 const sponsorCross = helper.ethCrossAccount.fromAddr(sponsor);674 const user = await helper.eth.createAccountWithBalance(donor);675 const userCross = helper.ethCrossAccount.fromAddress(user);676677 const {collectionAddress} = await helper.eth.createERC721MetadataCompatibleCollection(678 owner,679 {680 ...CREATE_COLLECTION_DATA_DEFAULTS,681 name: 'Sponsor collection',682 description: '1',683 tokenPrefix: '1',684 collectionMode: 'rft',685 pendingSponsor: sponsorCross,686 adminList: [userCross],687 },688 '',689 );690 const receiver = await helper.eth.createAccountWithBalance(donor);691 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner);692693 await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor});694695 const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user});696 const tokenId = result.events.Transfer.returnValues.tokenId;697698 const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));699 const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));700 const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));701702 switch (testCase) {703 case 'transfer':704 await collectionEvm.methods.transfer(receiver, tokenId).send({from: user});705 break;706 case 'transferCross':707 await collectionEvm.methods.transferCross(helper.ethCrossAccount.fromAddress(receiver), tokenId).send({from: user});708 break;709 case 'transferFrom':710 await collectionEvm.methods.transferFrom(user, receiver, tokenId).send({from: user});711 break;712 case 'transferFromCross':713 await collectionEvm.methods.transferFromCross(helper.ethCrossAccount.fromAddress(user), helper.ethCrossAccount.fromAddress(receiver), tokenId).send({from: user});714 break;715 }716717 const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner));718 expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore);719 const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor));720 expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true;721 const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user));722 expect(userBalanceAfter).to.be.eq(userBalanceBefore);723 }));724 });725726 describe('Collection admins', () => {727 let donor: IKeyringPair;728729 before(async function() {730 await usingEthPlaygrounds(async (_helper, privateKey) => {731 donor = await privateKey({url: import.meta.url});732 });733 });734735 [736 {mode: 'nft' as const, requiredPallets: []},737 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},738 {mode: 'ft' as const, requiredPallets: []},739 ].map(testCase => {740 itEth.ifWithPallets(`can add account admin by owner for ${testCase.mode}`, testCase.requiredPallets, async ({helper, privateKey}) => {741 742 const owner = await helper.eth.createAccountWithBalance(donor);743 const adminSub = await privateKey('//admin2');744 const adminEth = helper.eth.createAccount().toLowerCase();745746 const adminCrossSub = helper.ethCrossAccount.fromKeyringPair(adminSub);747 const adminCrossEth = helper.ethCrossAccount.fromAddress(adminEth);748749 const {collectionAddress, collectionId} = await helper.eth.createCollection(750 owner,751 {752 ...CREATE_COLLECTION_DATA_DEFAULTS,753 name: 'Mint collection',754 description: 'a',755 tokenPrefix: 'b',756 collectionMode: testCase.mode,757 adminList: [adminCrossSub, adminCrossEth],758 },759 ).send();760 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner, true);761762 763 const adminListRpc = await helper.collection.getAdmins(collectionId);764 expect(adminListRpc).to.has.length(2);765 expect(adminListRpc).to.be.deep.contain.members([{Substrate: adminSub.address}, {Ethereum: adminEth}]);766767 768 let adminListEth = await collectionEvm.methods.collectionAdmins().call();769 adminListEth = adminListEth.map((element: IEthCrossAccountId) => helper.address.convertCrossAccountFromEthCrossAccount(element));770 expect(adminListRpc).to.be.like(adminListEth);771772 773 expect(await collectionEvm.methods.isOwnerOrAdminCross(adminCrossSub).call()).to.be.true;774 expect(await collectionEvm.methods.isOwnerOrAdminCross(adminCrossEth).call()).to.be.true;775 });776 });777778 itEth('cross account admin can mint', async ({helper}) => {779 780 const owner = await helper.eth.createAccountWithBalance(donor);781 const adminEth = (await helper.eth.createAccountWithBalance(donor)).toLowerCase();782 const adminCrossEth = helper.ethCrossAccount.fromAddress(adminEth);783 const [adminSub] = await helper.arrange.createAccounts([100n], donor);784 const adminCrossSub = helper.ethCrossAccount.fromKeyringPair(adminSub);785 const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleCollection(786 owner,787 {788 ...CREATE_COLLECTION_DATA_DEFAULTS,789 name: 'Mint collection',790 description: 'a',791 tokenPrefix: 'b',792 collectionMode: 'nft',793 adminList: [adminCrossSub, adminCrossEth],794 },795 'uri',796 );797 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner, true);798799 800 await collectionEvm.methods.mint(owner).send({from: adminEth});801 await helper.nft.mintToken(adminSub, {collectionId, owner: {Ethereum: owner}});802803 expect(await helper.collection.getLastTokenId(collectionId)).to.eq(2);804 });805806 itEth('cannot add invalid cross account admin', async ({helper}) => {807 const owner = await helper.eth.createAccountWithBalance(donor);808 const [admin] = await helper.arrange.createAccounts([100n, 100n], donor);809810 const adminCross = {811 eth: helper.address.substrateToEth(admin.address),812 sub: admin.addressRaw,813 };814815 await expect(helper.eth.createCollection(816 owner,817 {818 ...CREATE_COLLECTION_DATA_DEFAULTS,819 name: 'A',820 description: 'B',821 tokenPrefix: 'C',822 collectionMode: 'nft',823 adminList: [adminCross],824 },825 ).call()).to.be.rejected;826 });827828 itEth('Remove [cross] admin by owner', async ({helper}) => {829 const owner = await helper.eth.createAccountWithBalance(donor);830831 const [adminSub] = await helper.arrange.createAccounts([10n], donor);832 const adminEth = (await helper.eth.createAccountWithBalance(donor)).toLowerCase();833 const adminCrossSub = helper.ethCrossAccount.fromKeyringPair(adminSub);834 const adminCrossEth = helper.ethCrossAccount.fromAddress(adminEth);835836 const {collectionAddress, collectionId} = await helper.eth.createCollection(837 owner,838 {839 ...CREATE_COLLECTION_DATA_DEFAULTS,840 name: 'A',841 description: 'B',842 tokenPrefix: 'C',843 collectionMode: 'nft',844 adminList: [adminCrossSub, adminCrossEth],845 },846 ).send();847848 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);849850 {851 const adminList = await helper.collection.getAdmins(collectionId);852 expect(adminList).to.deep.include({Substrate: adminSub.address});853 expect(adminList).to.deep.include({Ethereum: adminEth});854 }855856 await collectionEvm.methods.removeCollectionAdminCross(adminCrossSub).send();857 await collectionEvm.methods.removeCollectionAdminCross(adminCrossEth).send();858 const adminList = await helper.collection.getAdmins(collectionId);859 expect(adminList.length).to.be.eq(0);860861 862 await expect(helper.nft.mintToken(adminSub, {collectionId, owner: {Substrate: adminSub.address}})).to.be.rejectedWith(/common.PublicMintingNotAllowed/);863 await expect(collectionEvm.methods.mint(adminEth).send({from: adminEth})).to.be.rejected;864 });865 });866867 describe('Collection limits', () => {868 describe('Can set collection limits', () => {869 [870 {case: 'nft' as const},871 {case: 'rft' as const, requiredPallets: [Pallets.ReFungible]},872 {case: 'ft' as const},873 ].map(testCase =>874 itEth.ifWithPallets(`for ${testCase.case}`, testCase.requiredPallets || [], async ({helper}) => {875 const owner = await helper.eth.createAccountWithBalance(donor);876877 const limits = {878 accountTokenOwnershipLimit: 1000n,879 sponsoredDataSize: 1024n,880 sponsoredDataRateLimit: 30n,881 tokenLimit: 1000000n,882 sponsorTransferTimeout: 6n,883 sponsorApproveTimeout: 6n,884 ownerCanTransfer: 1n,885 ownerCanDestroy: 0n,886 transfersEnabled: 0n,887 };888889 const {collectionId, collectionAddress} = await helper.eth.createCollection(890 owner,891 {892 ...CREATE_COLLECTION_DATA_DEFAULTS,893 name: 'Limits',894 description: 'absolutely anything',895 tokenPrefix: 'FLO',896 collectionMode: testCase.case,897 limits: [898 {field: CollectionLimitField.AccountTokenOwnership, value: limits.accountTokenOwnershipLimit},899 {field: CollectionLimitField.SponsoredDataSize, value: limits.sponsoredDataSize},900 {field: CollectionLimitField.SponsoredDataRateLimit, value: limits.sponsoredDataRateLimit},901 {field: CollectionLimitField.TokenLimit, value: limits.tokenLimit},902 {field: CollectionLimitField.SponsorTransferTimeout, value: limits.sponsorTransferTimeout},903 {field: CollectionLimitField.SponsorApproveTimeout, value: limits.sponsorApproveTimeout},904 {field: CollectionLimitField.OwnerCanTransfer, value: limits.ownerCanTransfer},905 {field: CollectionLimitField.OwnerCanDestroy, value: limits.ownerCanDestroy},906 {field: CollectionLimitField.TransferEnabled, value: limits.transfersEnabled},907 ],908 },909 ).send();910911 const expectedLimits = {912 accountTokenOwnershipLimit: 1000,913 sponsoredDataSize: 1024,914 sponsoredDataRateLimit: {blocks: 30},915 tokenLimit: 1000000,916 sponsorTransferTimeout: 6,917 sponsorApproveTimeout: 6,918 ownerCanTransfer: true,919 ownerCanDestroy: false,920 transfersEnabled: false,921 };922923 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, testCase.case, owner);924925 926 const data = (await helper.rft.getData(collectionId))!;927 expect(data.raw.limits).to.deep.eq(expectedLimits);928 expect(await helper.collection.getEffectiveLimits(collectionId)).to.deep.eq(expectedLimits);929 930 const limitsEvm = await collectionEvm.methods.collectionLimits().call({from: owner});931 expect(limitsEvm).to.have.length(9);932 expect(limitsEvm[0]).to.deep.eq([CollectionLimitField.AccountTokenOwnership.toString(), [true, limits.accountTokenOwnershipLimit.toString()]]);933 expect(limitsEvm[1]).to.deep.eq([CollectionLimitField.SponsoredDataSize.toString(), [true, limits.sponsoredDataSize.toString()]]);934 expect(limitsEvm[2]).to.deep.eq([CollectionLimitField.SponsoredDataRateLimit.toString(), [true, limits.sponsoredDataRateLimit.toString()]]);935 expect(limitsEvm[3]).to.deep.eq([CollectionLimitField.TokenLimit.toString(), [true, limits.tokenLimit.toString()]]);936 expect(limitsEvm[4]).to.deep.eq([CollectionLimitField.SponsorTransferTimeout.toString(), [true, limits.sponsorTransferTimeout.toString()]]);937 expect(limitsEvm[5]).to.deep.eq([CollectionLimitField.SponsorApproveTimeout.toString(), [true, limits.sponsorApproveTimeout.toString()]]);938 expect(limitsEvm[6]).to.deep.eq([CollectionLimitField.OwnerCanTransfer.toString(), [true, limits.ownerCanTransfer.toString()]]);939 expect(limitsEvm[7]).to.deep.eq([CollectionLimitField.OwnerCanDestroy.toString(), [true, limits.ownerCanDestroy.toString()]]);940 expect(limitsEvm[8]).to.deep.eq([CollectionLimitField.TransferEnabled.toString(), [true, limits.transfersEnabled.toString()]]);941 }));942 });943944 describe('(!negative test!) Cannot set invalid collection limits', () => {945 [946 {case: 'nft' as const},947 {case: 'rft' as const, requiredPallets: [Pallets.ReFungible]},948 {case: 'ft' as const},949 ].map(testCase =>950 itEth.ifWithPallets(`for ${testCase.case}`, testCase.requiredPallets || [], async ({helper}) => {951 const invalidLimits = {952 accountTokenOwnershipLimit: BigInt(Number.MAX_SAFE_INTEGER),953 transfersEnabled: 3,954 };955956 const createCollectionData = {957 ...CREATE_COLLECTION_DATA_DEFAULTS,958 name: 'Limits',959 description: 'absolutely anything',960 tokenPrefix: 'ISNI',961 collectionMode: testCase.case,962 };963964 const owner = await helper.eth.createAccountWithBalance(donor);965 await expect(helper.eth.createCollection(966 owner,967 {968 ...createCollectionData,969 limits: [{field: 9 as CollectionLimitField, value: 1n}],970 },971 ).call()).to.be.rejectedWith('value not convertible into enum "CollectionLimitField"');972973 await expect(helper.eth.createCollection(974 owner,975 {976 ...createCollectionData,977 limits: [{field: CollectionLimitField.AccountTokenOwnership, value: invalidLimits.accountTokenOwnershipLimit}],978 },979 ).call()).to.be.rejectedWith(`can't convert value to u32 "${invalidLimits.accountTokenOwnershipLimit}"`);980981 await expect(helper.eth.createCollection(982 owner,983 {984 ...createCollectionData,985 limits: [{field: CollectionLimitField.TransferEnabled, value: 3n}],986 },987 ).call()).to.be.rejectedWith(`can't convert value to boolean "${invalidLimits.transfersEnabled}"`);988989 await expect(helper.eth.createCollection(990 owner,991 {992 ...createCollectionData,993 limits: [{field: CollectionLimitField.SponsoredDataSize, value: -1n}],994 },995 ).call()).to.be.rejectedWith('value out-of-bounds');996 }));997 });998 });9991000 describe('Collection properties', () => {10011002 [1003 {mode: 'nft' as const, methodParams: [{key: 'testKey1', value: Buffer.from('testValue1')}, {key: 'testKey2', value: Buffer.from('testValue2')}], expectedProps: [{key: 'testKey1', value: 'testValue1'}, {key: 'testKey2', value: 'testValue2'}]},1004 {mode: 'rft' as const, methodParams: [{key: 'testKey1', value: Buffer.from('testValue1')}, {key: 'testKey2', value: Buffer.from('testValue2')}], expectedProps: [{key: 'testKey1', value: 'testValue1'}, {key: 'testKey2', value: 'testValue2'}]},1005 {mode: 'ft' as const, methodParams: [{key: 'testKey1', value: Buffer.from('testValue1')}, {key: 'testKey2', value: Buffer.from('testValue2')}], expectedProps: [{key: 'testKey1', value: 'testValue1'}, {key: 'testKey2', value: 'testValue2'}]},1006 ].map(testCase =>1007 itEth.ifWithPallets(`Collection properties can be set for ${testCase.mode}`, testCase.mode === 'rft' ? [Pallets.ReFungible] : [], async({helper}) => {1008 const caller = await helper.eth.createAccountWithBalance(donor);1009 const callerCross = helper.ethCrossAccount.fromAddress(caller);1010 const owner = await helper.eth.createAccountWithBalance(donor);1011 const {collectionId, collectionAddress} = await helper.eth.createCollection(1012 owner,1013 {1014 ...CREATE_COLLECTION_DATA_DEFAULTS,1015 name: 'name',1016 description: 'test',1017 tokenPrefix: 'test',1018 collectionMode: testCase.mode,1019 adminList: [callerCross],1020 properties: testCase.methodParams,1021 },1022 ).send();10231024 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, caller);10251026 const raw = (await helper[testCase.mode].getData(collectionId))?.raw;1027 expect(raw.properties).to.deep.equal(testCase.expectedProps);10281029 1030 expect(await collectionEvm.methods.collectionProperties([]).call()).to.be.like(testCase.expectedProps.map(prop => helper.ethProperty.property(prop.key, prop.value)));1031 }));10321033 [1034 {mode: 'nft' as const},1035 {mode: 'rft' as const},1036 {mode: 'ft' as const},1037 ].map(testCase =>1038 itEth.ifWithPallets(`Collection properties can be deleted for ${testCase.mode}`, testCase.mode === 'rft' ? [Pallets.ReFungible] : [], async({helper}) => {1039 const caller = await helper.eth.createAccountWithBalance(donor);1040 const callerCross = helper.ethCrossAccount.fromAddress(caller);1041 const owner = await helper.eth.createAccountWithBalance(donor);1042 const {collectionId, collectionAddress} = await helper.eth.createCollection(1043 owner,1044 {1045 ...CREATE_COLLECTION_DATA_DEFAULTS,1046 name: 'name',1047 description: 'test',1048 tokenPrefix: 'test',1049 collectionMode: testCase.mode,1050 adminList: [callerCross],1051 properties:[1052 {key: 'testKey1', value: Buffer.from('testValue1')},1053 {key: 'testKey2', value: Buffer.from('testValue2')},1054 {key: 'testKey3', value: Buffer.from('testValue3')}],1055 },1056 ).send();10571058 const collectionEvm = helper.ethNativeContract.collection(collectionAddress, testCase.mode, caller);10591060 await collectionEvm.methods.deleteCollectionProperties(['testKey1', 'testKey2']).send({from: caller});10611062 const raw = (await helper[testCase.mode].getData(collectionId))?.raw;10631064 expect(raw.properties.length).to.equal(1);1065 expect(raw.properties).to.deep.equal([{key: 'testKey3', value: 'testValue3'}]);1066 }));10671068 itEth('(!negative test!) Cannot set invalid properties', async({helper}) => {1069 const caller = await helper.eth.createAccountWithBalance(donor);1070 const callerCross = helper.ethCrossAccount.fromAddress(caller);1071 const owner = await helper.eth.createAccountWithBalance(donor);1072 const createCollectionData = {1073 ...CREATE_COLLECTION_DATA_DEFAULTS,1074 name: 'name',1075 description: 'test',1076 tokenPrefix: 'test',1077 collectionMode: 'nft' as TCollectionMode,1078 adminList: [callerCross],1079 };1080 await expect(helper.eth.createCollection(1081 owner,1082 {1083 ...createCollectionData,1084 properties: [{key: '', value: Buffer.from('val1')}],1085 },1086 ).call()).to.be.rejected;10871088 await expect(helper.eth.createCollection(1089 owner,1090 {1091 ...createCollectionData,1092 properties: [{key: 'déjà vu', value: Buffer.from('hmm...')}],1093 },1094 ).call()).to.be.rejected;10951096 await expect(helper.eth.createCollection(1097 owner,1098 {1099 ...createCollectionData,1100 properties: [{key: 'a'.repeat(257), value: Buffer.from('val3')}],1101 },1102 ).call()).to.be.rejected;1103 });11041105 itEth('(!negative test!) cannot delete properties of non-owned collections', async ({helper}) => {1106 const caller = await helper.eth.createAccountWithBalance(donor);1107 const owner = await helper.eth.createAccountWithBalance(donor);1108 const {collectionAddress} = await helper.eth.createCollection(1109 owner,1110 {1111 ...CREATE_COLLECTION_DATA_DEFAULTS,1112 name: 'name',1113 description: 'test',1114 tokenPrefix: 'test',1115 collectionMode: 'nft',1116 properties:[1117 {key: 'testKey1', value: Buffer.from('testValue1')},1118 {key: 'testKey2', value: Buffer.from('testValue2')}],1119 },1120 ).send();11211122 const collectionEvm = await helper.ethNativeContract.collection(collectionAddress, 'nft', caller);11231124 await expect(collectionEvm.methods.deleteCollectionProperties(['testKey2']).send({from: caller})).to.be.rejected;1125 });1126 });11271128 describe('Token property permissions', () => {1129 [1130 {mode: 'nft' as const, requiredPallets: []},1131 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},1132 ].map(testCase =>1133 itEth.ifWithPallets(`[${testCase.mode}] Can set all possible token property permissions`, testCase.requiredPallets, async({helper}) => {1134 const owner = await helper.eth.createAccountWithBalance(donor);1135 const caller = await helper.ethCrossAccount.createAccountWithBalance(donor);1136 for(const [mutable,collectionAdmin, tokenOwner] of cartesian([], [false, true], [false, true], [false, true])) {1137 const {collectionId, collectionAddress} = await helper.eth.createCollection(1138 owner,1139 {1140 ...CREATE_COLLECTION_DATA_DEFAULTS,1141 name: 'A',1142 description: 'B',1143 tokenPrefix: 'C',1144 collectionMode: testCase.mode,1145 adminList: [caller],1146 tokenPropertyPermissions: [1147 {1148 key: 'testKey',1149 permissions: [1150 {code: TokenPermissionField.Mutable, value: mutable},1151 {code: TokenPermissionField.TokenOwner, value: tokenOwner},1152 {code: TokenPermissionField.CollectionAdmin, value: collectionAdmin},1153 ],1154 },1155 ],1156 },1157 ).send();1158 const collection = helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);11591160 expect(await helper[testCase.mode].getPropertyPermissions(collectionId)).to.be.deep.equal([{1161 key: 'testKey',1162 permission: {mutable, collectionAdmin, tokenOwner},1163 }]);11641165 expect(await collection.methods.tokenPropertyPermissions().call({from: caller.eth})).to.be.like([1166 ['testKey', [1167 [TokenPermissionField.Mutable.toString(), mutable],1168 [TokenPermissionField.TokenOwner.toString(), tokenOwner],1169 [TokenPermissionField.CollectionAdmin.toString(), collectionAdmin]],1170 ],1171 ]);1172 }1173 }));11741175 [1176 {mode: 'nft' as const, requiredPallets: []},1177 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},1178 ].map(testCase =>1179 itEth.ifWithPallets(`[${testCase.mode}] Can set multiple token property permissions`, testCase.requiredPallets, async({helper}) => {1180 const owner = await helper.eth.createAccountWithBalance(donor);1181 const {collectionId, collectionAddress} = await helper.eth.createCollection(1182 owner,1183 {1184 ...CREATE_COLLECTION_DATA_DEFAULTS,1185 name: 'A',1186 description: 'B',1187 tokenPrefix: 'C',1188 collectionMode: testCase.mode,1189 tokenPropertyPermissions: [1190 {1191 key: 'testKey_0',1192 permissions: [1193 {code: TokenPermissionField.Mutable, value: true},1194 {code: TokenPermissionField.TokenOwner, value: true},1195 {code: TokenPermissionField.CollectionAdmin, value: true}],1196 },1197 {1198 key: 'testKey_1',1199 permissions: [1200 {code: TokenPermissionField.Mutable, value: true},1201 {code: TokenPermissionField.TokenOwner, value: false},1202 {code: TokenPermissionField.CollectionAdmin, value: true}],1203 },1204 {1205 key: 'testKey_2',1206 permissions: [1207 {code: TokenPermissionField.Mutable, value: false},1208 {code: TokenPermissionField.TokenOwner, value: true},1209 {code: TokenPermissionField.CollectionAdmin, value: false}],1210 },1211 ],1212 },1213 ).send();1214 const collection = await helper.ethNativeContract.collection(collectionAddress, testCase.mode, owner);12151216 expect(await helper[testCase.mode].getPropertyPermissions(collectionId)).to.be.deep.equal([1217 {1218 key: 'testKey_0',1219 permission: {mutable: true, tokenOwner: true, collectionAdmin: true},1220 },1221 {1222 key: 'testKey_1',1223 permission: {mutable: true, tokenOwner: false, collectionAdmin: true},1224 },1225 {1226 key: 'testKey_2',1227 permission: {mutable: false, tokenOwner: true, collectionAdmin: false},1228 },1229 ]);12301231 expect(await collection.methods.tokenPropertyPermissions().call({from: owner})).to.be.like([1232 ['testKey_0', [1233 [TokenPermissionField.Mutable.toString(), true],1234 [TokenPermissionField.TokenOwner.toString(), true],1235 [TokenPermissionField.CollectionAdmin.toString(), true]],1236 ],1237 ['testKey_1', [1238 [TokenPermissionField.Mutable.toString(), true],1239 [TokenPermissionField.TokenOwner.toString(), false],1240 [TokenPermissionField.CollectionAdmin.toString(), true]],1241 ],1242 ['testKey_2', [1243 [TokenPermissionField.Mutable.toString(), false],1244 [TokenPermissionField.TokenOwner.toString(), true],1245 [TokenPermissionField.CollectionAdmin.toString(), false]],1246 ],1247 ]);1248 }));12491250 [1251 {mode: 'nft' as const, requiredPallets: []},1252 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},1253 ].map(testCase =>1254 itEth.ifWithPallets(`Can be deleted for ${testCase.mode}`, testCase.requiredPallets, async({helper}) => {1255 const caller = await helper.eth.createAccountWithBalance(donor);1256 const receiver = await helper.ethCrossAccount.createAccountWithBalance(donor);1257 const {collectionAddress} = await helper.eth.createCollection(1258 caller,1259 {1260 ...CREATE_COLLECTION_DATA_DEFAULTS,1261 name: 'A',1262 description: 'B',1263 tokenPrefix: 'C',1264 collectionMode: testCase.mode,1265 adminList: [receiver],1266 tokenPropertyPermissions: [1267 {1268 key: 'testKey',1269 permissions: [1270 {code: TokenPermissionField.Mutable, value: true},1271 {code: TokenPermissionField.CollectionAdmin, value: true}],1272 },1273 {1274 key: 'testKey_1',1275 permissions: [1276 {code: TokenPermissionField.Mutable, value: true},1277 {code: TokenPermissionField.CollectionAdmin, value: true}],1278 },1279 ],1280 },1281 ).send();12821283 const collection = helper.ethNativeContract.collection(collectionAddress, testCase.mode, caller);1284 const tokenId = (await collection.methods.mintCross(receiver, [{key: 'testKey', value: Buffer.from('testValue')}, {key: 'testKey_1', value: Buffer.from('testValue_1')}]).send()).events.Transfer.returnValues.tokenId;1285 expect(await collection.methods.properties(tokenId, ['testKey', 'testKey_1']).call()).to.has.length(2);12861287 await collection.methods.deleteProperties(tokenId, ['testKey', 'testKey_1']).send({from: caller});1288 expect(await collection.methods.properties(tokenId, ['testKey', 'testKey_1']).call()).to.has.length(0);1289 }));1290 });12911292 describe('Nesting', () => {1293 itEth('NFT: allows an Owner to nest/unnest their token', async ({helper}) => {1294 const owner = await helper.eth.createAccountWithBalance(donor);1295 const {collectionAddress, collectionId} = await helper.eth.createCollection(1296 owner,1297 {1298 ...CREATE_COLLECTION_DATA_DEFAULTS,1299 name: 'A',1300 description: 'B',1301 tokenPrefix: 'C',1302 collectionMode: 'nft',1303 nestingSettings: {token_owner: true, collection_admin: false, restricted: []},1304 },1305 ).send();13061307 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);13081309 1310 const mintingTargetNFTTokenIdResult = await contract.methods.mint(owner).send({from: owner});1311 const targetNFTTokenId = mintingTargetNFTTokenIdResult.events.Transfer.returnValues.tokenId;1312 const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetNFTTokenId);13131314 1315 const mintingFirstTokenIdResult = await contract.methods.mint(targetNftTokenAddress).send({from: owner});1316 const firstTokenId = mintingFirstTokenIdResult.events.Transfer.returnValues.tokenId;1317 expect(await contract.methods.ownerOf(firstTokenId).call()).to.be.equal(targetNftTokenAddress);13181319 1320 const mintingSecondTokenIdResult = await contract.methods.mint(owner).send({from: owner});1321 const secondTokenId = mintingSecondTokenIdResult.events.Transfer.returnValues.tokenId;13221323 await contract.methods.transfer(targetNftTokenAddress, secondTokenId).send({from: owner});1324 expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(targetNftTokenAddress);13251326 1327 await contract.methods.transferFrom(targetNftTokenAddress, owner, secondTokenId).send({from: owner});1328 expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(owner);1329 });13301331 itEth('NFT: collectionNesting()', async ({helper}) => {1332 const owner = await helper.eth.createAccountWithBalance(donor);1333 const {collectionAddress: unnestedCollectionAddress} = await helper.eth.createCollection(1334 owner,1335 new CreateCollectionData('A', 'B', 'C', 'nft'),1336 ).send();13371338 const unnestedContract = helper.ethNativeContract.collection(unnestedCollectionAddress, 'nft', owner);1339 expect(await unnestedContract.methods.collectionNesting().call({from: owner})).to.be.like([false, false, []]);13401341 const {collectionAddress} = await helper.eth.createCollection(1342 owner,1343 {1344 ...CREATE_COLLECTION_DATA_DEFAULTS,1345 name: 'A',1346 description: 'B',1347 tokenPrefix: 'C',1348 collectionMode: 'nft',1349 nestingSettings: {token_owner: true, collection_admin: false, restricted: [unnestedCollectionAddress.toString()]},1350 },1351 ).send();13521353 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);1354 expect(await contract.methods.collectionNesting().call({from: owner})).to.be.like([true, false, [unnestedCollectionAddress.toString()]]);1355 await contract.methods.setCollectionNesting([false, false, []]).send({from: owner});1356 expect(await contract.methods.collectionNesting().call({from: owner})).to.be.like([false, false, []]);1357 });13581359 itEth('NFT: disallows to nest token if nesting is disabled', async ({helper}) => {1360 const owner = await helper.eth.createAccountWithBalance(donor);13611362 const {collectionId, collectionAddress} = await helper.eth.createCollection(1363 owner,1364 {1365 ...CREATE_COLLECTION_DATA_DEFAULTS,1366 name: 'A',1367 description: 'B',1368 tokenPrefix: 'C',1369 collectionMode: 'nft',1370 nestingSettings: {token_owner: false, collection_admin: false, restricted: []},1371 },1372 ).send();13731374 const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);13751376 1377 const mintingTargetTokenIdResult = await contract.methods.mint(owner).send({from: owner});1378 const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;1379 const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetTokenId);13801381 1382 const mintingNftTokenIdResult = await contract.methods.mint(owner).send({from: owner});1383 const nftTokenId = mintingNftTokenIdResult.events.Transfer.returnValues.tokenId;13841385 1386 await expect(contract.methods1387 .transfer(targetNftTokenAddress, nftTokenId)1388 .call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest');1389 });1390 });13911392 describe('Flags', () => {1393 const createCollectionData = {1394 ...CREATE_COLLECTION_DATA_DEFAULTS,1395 name: 'A',1396 description: 'B',1397 tokenPrefix: 'C',1398 collectionMode: 'nft' as TCollectionMode,1399 };14001401 itEth('NFT: use numbers for flags', async ({helper}) => {1402 const owner = await helper.eth.createAccountWithBalance(donor);14031404 {1405 const {collectionId} = await helper.eth.createCollection(owner, {...createCollectionData, flags: 0}).send();1406 expect((await helper.nft.getData(collectionId))?.raw.flags).to.be.deep.equal({foreign: false, erc721metadata: false});1407 }14081409 {1410 const {collectionId} = await helper.eth.createCollection(owner, {...createCollectionData, flags: 64}).send();1411 expect((await helper.nft.getData(collectionId))?.raw.flags).to.be.deep.equal({foreign: false, erc721metadata: true});1412 }1413 });14141415 itEth('NFT: can\'t set foreign flag number', async ({helper}) => {1416 const owner = await helper.eth.createAccountWithBalance(donor);14171418 {1419 await expect(helper.eth.createCollection(owner, {...createCollectionData, flags: 128}).call({from: owner})).to.be.rejectedWith(/internal flags were used/);1420 }14211422 {1423 await expect(helper.eth.createCollection(owner, {...createCollectionData, flags: 192}).call({from: owner})).to.be.rejectedWith(/internal flags were used/);1424 }1425 });14261427 itEth('NFT: use enum for flags', async ({helper}) => {1428 const owner = await helper.eth.createAccountWithBalance(donor);14291430 {1431 const {collectionId} = await helper.eth.createCollection(owner, {...createCollectionData, flags: [CollectionFlag.Erc721metadata]}).send();1432 expect((await helper.nft.getData(collectionId))?.raw.flags).to.be.deep.equal({foreign: false, erc721metadata: true});1433 }1434 });14351436 itEth('NFT: foreign flag enum is ignored', async ({helper}) => {1437 const owner = await helper.eth.createAccountWithBalance(donor);14381439 {1440 await expect(helper.eth.createCollection(owner, {...createCollectionData, flags: [CollectionFlag.Foreign]}).call({from: owner})).to.be.rejectedWith(/internal flags were used/);1441 }14421443 {1444 await expect(helper.eth.createCollection(owner, {...createCollectionData, flags: [CollectionFlag.Erc721metadata | CollectionFlag.Foreign]}).call({from: owner})).to.be.rejectedWith(/internal flags were used/);1445 }1446 });1447 });1448});