--- a/tests/package.json +++ b/tests/package.json @@ -79,7 +79,8 @@ "testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts", "testEnableDisableTransfers": "mocha --timeout 9999999 -r ts-node/register ./**/enableDisableTransfer.test.ts", "testLimits": "mocha --timeout 9999999 -r ts-node/register ./**/limits.test.ts", - "testEthCreateCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createCollection.test.ts", + "testEthCreateNFTCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createNFTCollection.test.ts", + "testEthCreateRFTCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createRFTCollection.test.ts", "testRFT": "mocha --timeout 9999999 -r ts-node/register ./**/refungible.test.ts", "testFT": "mocha --timeout 9999999 -r ts-node/register ./**/fungible.test.ts", "testRPC": "mocha --timeout 9999999 -r ts-node/register ./**/rpc.test.ts", --- a/tests/src/eth/nesting/nest.test.ts +++ b/tests/src/eth/nesting/nest.test.ts @@ -1,217 +1,220 @@ -import {ApiPromise} from '@polkadot/api'; +import {IKeyringPair} from '@polkadot/types/types'; import {Contract} from 'web3-eth-contract'; -import {expect} from 'chai'; -import Web3 from 'web3'; -import {createEthAccountWithBalance, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, tokenIdToAddress} from '../../eth/util/helpers'; -import nonFungibleAbi from '../nonFungibleAbi.json'; +import {itEth, EthUniqueHelper, usingEthPlaygrounds, expect} from '../util/playgrounds' + const createNestingCollection = async ( - api: ApiPromise, - web3: Web3, + helper: EthUniqueHelper, owner: string, ): Promise<{ collectionId: number, collectionAddress: string, contract: Contract }> => { - const collectionHelper = evmCollectionHelpers(web3, owner); - - const result = await collectionHelper.methods - .createNonfungibleCollection('A', 'B', 'C') - .send(); - const {collectionIdAddress: collectionAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const {collectionAddress, collectionId} = await helper.eth.createNonfungibleCollection(owner, 'A', 'B', 'C'); - const contract = new web3.eth.Contract(nonFungibleAbi as any, collectionAddress, {from: owner, ...GAS_ARGS}); + const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); await contract.methods.setCollectionNesting(true).send({from: owner}); return {collectionId, collectionAddress, contract}; }; -describe('Integration Test: EVM Nesting', () => { - itWeb3('NFT: allows an Owner to nest/unnest their token', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const {collectionId, contract} = await createNestingCollection(api, web3, owner); - // Create a token to be nested - const targetNFTTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - targetNFTTokenId, - ).send({from: owner}); - - const targetNftTokenAddress = tokenIdToAddress(collectionId, targetNFTTokenId); - - // Create a nested token - const firstTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - targetNftTokenAddress, - firstTokenId, - ).send({from: owner}); +describe('EVM nesting tests group', () => { + let donor: IKeyringPair; - expect(await contract.methods.ownerOf(firstTokenId).call()).to.be.equal(targetNftTokenAddress); - - // Create a token to be nested and nest - const secondTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - secondTokenId, - ).send({from: owner}); - - await contract.methods.transfer(targetNftTokenAddress, secondTokenId).send({from: owner}); - - expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(targetNftTokenAddress); - - // Unnest token back - await contract.methods.transferFrom(targetNftTokenAddress, owner, secondTokenId).send({from: owner}); - expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(owner); + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + donor = privateKey('//Alice'); + }); }); - itWeb3('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - - const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(api, web3, owner); - const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(api, web3, owner); - await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner}); - - // Create a token to nest into - const targetNftTokenId = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - targetNftTokenId, - ).send({from: owner}); - const nftTokenAddressA1 = tokenIdToAddress(collectionIdA, targetNftTokenId); - - // Create a token for nesting in the same collection as the target - const nftTokenIdA = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - nftTokenIdA, - ).send({from: owner}); - - // Create a token for nesting in a different collection - const nftTokenIdB = await contractB.methods.nextTokenId().call(); - await contractB.methods.mint( - owner, - nftTokenIdB, - ).send({from: owner}); - - // Nest - await contractA.methods.transfer(nftTokenAddressA1, nftTokenIdA).send({from: owner}); - expect(await contractA.methods.ownerOf(nftTokenIdA).call()).to.be.equal(nftTokenAddressA1); - - await contractB.methods.transfer(nftTokenAddressA1, nftTokenIdB).send({from: owner}); - expect(await contractB.methods.ownerOf(nftTokenIdB).call()).to.be.equal(nftTokenAddressA1); - }); -}); - -describe('Negative Test: EVM Nesting', async() => { - itWeb3('NFT: disallows to nest token if nesting is disabled', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - - const {collectionId, contract} = await createNestingCollection(api, web3, owner); - await contract.methods.setCollectionNesting(false).send({from: owner}); - - // Create a token to nest into - const targetNftTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - targetNftTokenId, - ).send({from: owner}); - - const targetNftTokenAddress = tokenIdToAddress(collectionId, targetNftTokenId); - - // Create a token to nest - const nftTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - nftTokenId, - ).send({from: owner}); - - // Try to nest - await expect(contract.methods - .transfer(targetNftTokenAddress, nftTokenId) - .call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest'); - }); - - itWeb3('NFT: disallows a non-Owner to nest someone else\'s token', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const malignant = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - - const {collectionId, contract} = await createNestingCollection(api, web3, owner); - - // Mint a token - const targetTokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - owner, - targetTokenId, - ).send({from: owner}); - const targetTokenAddress = tokenIdToAddress(collectionId, targetTokenId); - - // Mint a token belonging to a different account - const tokenId = await contract.methods.nextTokenId().call(); - await contract.methods.mint( - malignant, - tokenId, - ).send({from: owner}); - - // Try to nest one token in another as a non-owner account - await expect(contract.methods - .transfer(targetTokenAddress, tokenId) - .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest'); - }); - - itWeb3('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const malignant = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - - const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(api, web3, owner); - const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(api, web3, owner); - - await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner}); - - // Create a token in one collection - const nftTokenIdA = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - nftTokenIdA, - ).send({from: owner}); - const nftTokenAddressA = tokenIdToAddress(collectionIdA, nftTokenIdA); - - // Create a token in another collection belonging to someone else - const nftTokenIdB = await contractB.methods.nextTokenId().call(); - await contractB.methods.mint( - malignant, - nftTokenIdB, - ).send({from: owner}); - - // Try to drag someone else's token into the other collection and nest - await expect(contractB.methods - .transfer(nftTokenAddressA, nftTokenIdB) - .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest'); + describe('Integration Test: EVM Nesting', () => { + itEth('NFT: allows an Owner to nest/unnest their token', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const {collectionId, contract} = await createNestingCollection(helper, owner); + + // Create a token to be nested + const targetNFTTokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + owner, + targetNFTTokenId, + ).send({from: owner}); + + const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetNFTTokenId); + + // Create a nested token + const firstTokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + targetNftTokenAddress, + firstTokenId, + ).send({from: owner}); + + expect(await contract.methods.ownerOf(firstTokenId).call()).to.be.equal(targetNftTokenAddress); + + // Create a token to be nested and nest + const secondTokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + owner, + secondTokenId, + ).send({from: owner}); + + await contract.methods.transfer(targetNftTokenAddress, secondTokenId).send({from: owner}); + + expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(targetNftTokenAddress); + + // Unnest token back + await contract.methods.transferFrom(targetNftTokenAddress, owner, secondTokenId).send({from: owner}); + expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(owner); + }); + + itEth('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(helper, owner); + const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(helper, owner); + await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner}); + + // Create a token to nest into + const targetNftTokenId = await contractA.methods.nextTokenId().call(); + await contractA.methods.mint( + owner, + targetNftTokenId, + ).send({from: owner}); + const nftTokenAddressA1 = helper.ethAddress.fromTokenId(collectionIdA, targetNftTokenId); + + // Create a token for nesting in the same collection as the target + const nftTokenIdA = await contractA.methods.nextTokenId().call(); + await contractA.methods.mint( + owner, + nftTokenIdA, + ).send({from: owner}); + + // Create a token for nesting in a different collection + const nftTokenIdB = await contractB.methods.nextTokenId().call(); + await contractB.methods.mint( + owner, + nftTokenIdB, + ).send({from: owner}); + + // Nest + await contractA.methods.transfer(nftTokenAddressA1, nftTokenIdA).send({from: owner}); + expect(await contractA.methods.ownerOf(nftTokenIdA).call()).to.be.equal(nftTokenAddressA1); + + await contractB.methods.transfer(nftTokenAddressA1, nftTokenIdB).send({from: owner}); + expect(await contractB.methods.ownerOf(nftTokenIdB).call()).to.be.equal(nftTokenAddressA1); + }); }); - itWeb3('NFT: disallows to nest token in an unlisted collection', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - - const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(api, web3, owner); - const {contract: contractB} = await createNestingCollection(api, web3, owner); - - await contractA.methods.setCollectionNesting(true, [collectionAddressA]).send({from: owner}); - - // Create a token in one collection - const nftTokenIdA = await contractA.methods.nextTokenId().call(); - await contractA.methods.mint( - owner, - nftTokenIdA, - ).send({from: owner}); - const nftTokenAddressA = tokenIdToAddress(collectionIdA, nftTokenIdA); - - // Create a token in another collection - const nftTokenIdB = await contractB.methods.nextTokenId().call(); - await contractB.methods.mint( - owner, - nftTokenIdB, - ).send({from: owner}); - - // Try to nest into a token in the other collection, disallowed in the first - await expect(contractB.methods - .transfer(nftTokenAddressA, nftTokenIdB) - .call()).to.be.rejectedWith('SourceCollectionIsNotAllowedToNest'); + describe('Negative Test: EVM Nesting', async() => { + itEth('NFT: disallows to nest token if nesting is disabled', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionId, contract} = await createNestingCollection(helper, owner); + await contract.methods.setCollectionNesting(false).send({from: owner}); + + // Create a token to nest into + const targetNftTokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + owner, + targetNftTokenId, + ).send({from: owner}); + + const targetNftTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetNftTokenId); + + // Create a token to nest + const nftTokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + owner, + nftTokenId, + ).send({from: owner}); + + // Try to nest + await expect(contract.methods + .transfer(targetNftTokenAddress, nftTokenId) + .call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest'); + }); + + itEth('NFT: disallows a non-Owner to nest someone else\'s token', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const malignant = await helper.eth.createAccountWithBalance(donor); + + const {collectionId, contract} = await createNestingCollection(helper, owner); + + // Mint a token + const targetTokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + owner, + targetTokenId, + ).send({from: owner}); + const targetTokenAddress = helper.ethAddress.fromTokenId(collectionId, targetTokenId); + + // Mint a token belonging to a different account + const tokenId = await contract.methods.nextTokenId().call(); + await contract.methods.mint( + malignant, + tokenId, + ).send({from: owner}); + + // Try to nest one token in another as a non-owner account + await expect(contract.methods + .transfer(targetTokenAddress, tokenId) + .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest'); + }); + + itEth('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const malignant = await helper.eth.createAccountWithBalance(donor); + + const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(helper, owner); + const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(helper, owner); + + await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner}); + + // Create a token in one collection + const nftTokenIdA = await contractA.methods.nextTokenId().call(); + await contractA.methods.mint( + owner, + nftTokenIdA, + ).send({from: owner}); + const nftTokenAddressA = helper.ethAddress.fromTokenId(collectionIdA, nftTokenIdA); + + // Create a token in another collection belonging to someone else + const nftTokenIdB = await contractB.methods.nextTokenId().call(); + await contractB.methods.mint( + malignant, + nftTokenIdB, + ).send({from: owner}); + + // Try to drag someone else's token into the other collection and nest + await expect(contractB.methods + .transfer(nftTokenAddressA, nftTokenIdB) + .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest'); + }); + + itEth('NFT: disallows to nest token in an unlisted collection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(helper, owner); + const {contract: contractB} = await createNestingCollection(helper, owner); + + await contractA.methods.setCollectionNesting(true, [collectionAddressA]).send({from: owner}); + + // Create a token in one collection + const nftTokenIdA = await contractA.methods.nextTokenId().call(); + await contractA.methods.mint( + owner, + nftTokenIdA, + ).send({from: owner}); + const nftTokenAddressA = helper.ethAddress.fromTokenId(collectionIdA, nftTokenIdA); + + // Create a token in another collection + const nftTokenIdB = await contractB.methods.nextTokenId().call(); + await contractB.methods.mint( + owner, + nftTokenIdB, + ).send({from: owner}); + + // Try to nest into a token in the other collection, disallowed in the first + await expect(contractB.methods + .transfer(nftTokenAddressA, nftTokenIdB) + .call()).to.be.rejectedWith('SourceCollectionIsNotAllowedToNest'); + }); }); }); --- /dev/null +++ b/tests/src/eth/util/playgrounds/index.ts @@ -0,0 +1,67 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +import {IKeyringPair} from '@polkadot/types/types'; + +import config from '../../../config'; + +import {EthUniqueHelper} from './unique.dev'; +import {SilentLogger} from '../../../util/playgrounds/unique.dev'; + +export {EthUniqueHelper} from './unique.dev'; + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised); +export const expect = chai.expect; + +export const usingEthPlaygrounds = async (code: (helper: EthUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + // TODO: Remove, this is temporary: Filter unneeded API output + // (Jaco promised it will be removed in the next version) + const consoleErr = console.error; + const consoleLog = console.log; + const consoleWarn = console.warn; + + const outFn = (printer: any) => (...args: any[]) => { + for (const arg of args) { + if (typeof arg !== 'string') + continue; + if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis: UniqueApi/2, RmrkApi/1') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure') + return; + } + printer(...args); + }; + + console.error = outFn(consoleErr.bind(console)); + console.log = outFn(consoleLog.bind(console)); + console.warn = outFn(consoleWarn.bind(console)); + const helper = new EthUniqueHelper(new SilentLogger()); + + try { + await helper.connect(config.substrateUrl); + await helper.connectWeb3(config.substrateUrl); + const ss58Format = helper.chain.getChainProperties().ss58Format; + const privateKey = (seed: string) => helper.util.fromSeed(seed, ss58Format); + await code(helper, privateKey); + } + finally { + await helper.disconnect(); + await helper.disconnectWeb3(); + console.error = consoleErr; + console.log = consoleLog; + console.warn = consoleWarn; + } +} + +export async function itEth(name: string, cb: (apis: { helper: EthUniqueHelper, privateKey: (seed: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) { + let i: any = it; + if (opts.only) i = i.only; + else if (opts.skip) i = i.skip; + i(name, async () => { + await usingEthPlaygrounds(async (helper, privateKey) => { + await cb({helper, privateKey}); + }); + }); +} +itEth.only = (name: string, cb: (apis: { helper: EthUniqueHelper, privateKey: (seed: string) => IKeyringPair }) => any) => itEth(name, cb, {only: true}); +itEth.skip = (name: string, cb: (apis: { helper: EthUniqueHelper, privateKey: (seed: string) => IKeyringPair }) => any) => itEth(name, cb, {skip: true}); \ No newline at end of file --- /dev/null +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -0,0 +1,162 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable function-call-argument-newline */ + +import Web3 from 'web3'; +import {WebsocketProvider} from 'web3-core'; +import {Contract} from 'web3-eth-contract'; + +import {evmToAddress} from '@polkadot/util-crypto'; +import {IKeyringPair} from '@polkadot/types/types'; + +import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev'; + +// Native contracts ABI +import collectionHelpersAbi from '../../collectionHelpersAbi.json'; +import fungibleAbi from '../../fungibleAbi.json'; +import nonFungibleAbi from '../../nonFungibleAbi.json'; +import refungibleAbi from '../../reFungibleAbi.json'; +import refungibleTokenAbi from '../../reFungibleTokenAbi.json'; +import contractHelpersAbi from './../contractHelpersAbi.json'; + +class EthGroupBase { + helper: EthUniqueHelper; + + constructor(helper: EthUniqueHelper) { + this.helper = helper; + } +} + + +class NativeContractGroup extends EthGroupBase { + DEFAULT_GAS = 2_500_000; + + contractHelpers(caller: string): Contract { + const web3 = this.helper.getWeb3(); + return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, gas: this.DEFAULT_GAS}); + } + + collectionHelpers(caller: string) { + const web3 = this.helper.getWeb3(); + return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.DEFAULT_GAS}); + } + + collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract { + const abi = { + 'nft': nonFungibleAbi, + 'rft': refungibleAbi, + 'ft': fungibleAbi + }[mode]; + const web3 = this.helper.getWeb3(); + return new web3.eth.Contract(abi as any, address, {gas: this.DEFAULT_GAS, ...(caller ? {from: caller} : {})}); + } + + rftTokenByAddress(address: string, caller?: string): Contract { + const web3 = this.helper.getWeb3(); + return new web3.eth.Contract(refungibleTokenAbi as any, address, {gas: this.DEFAULT_GAS, ...(caller ? {from: caller} : {})}); + } + + rftToken(collectionId: number, tokenId: number, caller?: string): Contract { + return this.rftTokenByAddress(this.helper.ethAddress.fromTokenId(collectionId, tokenId), caller); + } +} + + +class EthGroup extends EthGroupBase { + createAccount() { + const web3 = this.helper.getWeb3(); + const account = web3.eth.accounts.create(); + web3.eth.accounts.wallet.add(account.privateKey); + return account.address; + } + + async createAccountWithBalance(donor: IKeyringPair, amount=1000n) { + const account = this.createAccount(); + await this.transferBalanceFromSubstrate(donor, account, amount); + + return account; + } + + async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=1000n) { + return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * this.helper.balance.getOneTokenNominal()); + } + + async createNonfungibleCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { + const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer); + + const result = await collectionHelper.methods.createNonfungibleCollection(name, description, tokenPrefix).send(); + + const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress); + + return {collectionId, collectionAddress}; + } +} + + +class EthAddressGroup extends EthGroupBase { + extractCollectionId(address: string): number { + if (!(address.length === 42 || address.length === 40)) throw new Error('address wrong format'); + return parseInt(address.substr(address.length - 8), 16); + } + + fromCollectionId(collectionId: number): string { + if (collectionId >= 0xffffffff || collectionId < 0) throw new Error('collectionId overflow'); + return Web3.utils.toChecksumAddress(`0x17c4e6453cc49aaaaeaca894e6d9683e${collectionId.toString(16).padStart(8, '0')}`); + } + + extractTokenId(address: string): {collectionId: number, tokenId: number} { + if (!address.startsWith('0x')) + throw 'address not starts with "0x"'; + if (address.length > 42) + throw 'address length is more than 20 bytes'; + return { + collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)), + tokenId: Number('0x' + address.substring(address.length - 8)), + }; + } + + fromTokenId(collectionId: number, tokenId: number): string { + return this.helper.util.getNestingTokenAddress(collectionId, tokenId); + } + + normalizeAddress(address: string): string { + return '0x' + address.substring(address.length - 40); + } +} + + +export class EthUniqueHelper extends DevUniqueHelper { + web3: Web3 | null = null; + web3Provider: WebsocketProvider | null = null; + + eth: EthGroup; + ethAddress: EthAddressGroup; + ethNativeContract: NativeContractGroup; + + constructor(logger: { log: (msg: any, level: any) => void, level: any }) { + super(logger); + this.eth = new EthGroup(this); + this.ethAddress = new EthAddressGroup(this); + this.ethNativeContract = new NativeContractGroup(this); + } + + getWeb3(): Web3 { + if(this.web3 === null) throw Error('Web3 not connected'); + return this.web3; + } + + async connectWeb3(wsEndpoint: string) { + if(this.web3 !== null) return; + this.web3Provider = new Web3.providers.WebsocketProvider(wsEndpoint); + this.web3 = new Web3(this.web3Provider); + } + + async disconnectWeb3() { + if(this.web3 === null) return; + this.web3Provider?.connection.close(); + this.web3 = null; + } +} + \ No newline at end of file