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

difftreelog

test Initial eth playgrounds

Andrey2022-09-04parent: #21a2ff1.patch.diff
in: master

4 files changed

modifiedtests/package.jsondiffbeforeafterboth
--- 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",
modifiedtests/src/eth/nesting/nest.test.tsdiffbeforeafterboth
before · tests/src/eth/nesting/nest.test.ts
1import {ApiPromise} from '@polkadot/api';2import {Contract} from 'web3-eth-contract';3import {expect} from 'chai';4import Web3 from 'web3';5import {createEthAccountWithBalance, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, tokenIdToAddress} from '../../eth/util/helpers';6import nonFungibleAbi from '../nonFungibleAbi.json';78const createNestingCollection = async (9  api: ApiPromise,10  web3: Web3,11  owner: string,12): Promise<{ collectionId: number, collectionAddress: string, contract: Contract }> => {13  const collectionHelper = evmCollectionHelpers(web3, owner);14        15  const result = await collectionHelper.methods16    .createNonfungibleCollection('A', 'B', 'C')17    .send();18  const {collectionIdAddress: collectionAddress, collectionId} = await getCollectionAddressFromResult(api, result);1920  const contract = new web3.eth.Contract(nonFungibleAbi as any, collectionAddress, {from: owner, ...GAS_ARGS});21  await contract.methods.setCollectionNesting(true).send({from: owner});2223  return {collectionId, collectionAddress, contract};24};2526describe('Integration Test: EVM Nesting', () => {27  itWeb3('NFT: allows an Owner to nest/unnest their token', async ({api, web3, privateKeyWrapper}) => {28    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);29    const {collectionId, contract} = await createNestingCollection(api, web3, owner);3031    // Create a token to be nested32    const targetNFTTokenId = await contract.methods.nextTokenId().call();33    await contract.methods.mint(34      owner,35      targetNFTTokenId,36    ).send({from: owner});3738    const targetNftTokenAddress = tokenIdToAddress(collectionId, targetNFTTokenId);3940    // Create a nested token41    const firstTokenId = await contract.methods.nextTokenId().call();42    await contract.methods.mint(43      targetNftTokenAddress,44      firstTokenId,45    ).send({from: owner});4647    expect(await contract.methods.ownerOf(firstTokenId).call()).to.be.equal(targetNftTokenAddress);4849    // Create a token to be nested and nest50    const secondTokenId = await contract.methods.nextTokenId().call();51    await contract.methods.mint(52      owner,53      secondTokenId,54    ).send({from: owner});5556    await contract.methods.transfer(targetNftTokenAddress, secondTokenId).send({from: owner});5758    expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(targetNftTokenAddress);5960    // Unnest token back61    await contract.methods.transferFrom(targetNftTokenAddress, owner, secondTokenId).send({from: owner});62    expect(await contract.methods.ownerOf(secondTokenId).call()).to.be.equal(owner);63  });6465  itWeb3('NFT: allows an Owner to nest/unnest their token (Restricted nesting)', async ({api, web3, privateKeyWrapper}) => {66    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);6768    const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(api, web3, owner);69    const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(api, web3, owner);70    await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner});7172    // Create a token to nest into73    const targetNftTokenId = await contractA.methods.nextTokenId().call();74    await contractA.methods.mint(75      owner,76      targetNftTokenId,77    ).send({from: owner});78    const nftTokenAddressA1 = tokenIdToAddress(collectionIdA, targetNftTokenId);7980    // Create a token for nesting in the same collection as the target81    const nftTokenIdA = await contractA.methods.nextTokenId().call();82    await contractA.methods.mint(83      owner,84      nftTokenIdA,85    ).send({from: owner});8687    // Create a token for nesting in a different collection88    const nftTokenIdB = await contractB.methods.nextTokenId().call();89    await contractB.methods.mint(90      owner,91      nftTokenIdB,92    ).send({from: owner});9394    // Nest95    await contractA.methods.transfer(nftTokenAddressA1, nftTokenIdA).send({from: owner});96    expect(await contractA.methods.ownerOf(nftTokenIdA).call()).to.be.equal(nftTokenAddressA1);9798    await contractB.methods.transfer(nftTokenAddressA1, nftTokenIdB).send({from: owner});99    expect(await contractB.methods.ownerOf(nftTokenIdB).call()).to.be.equal(nftTokenAddressA1);100  });101});102103describe('Negative Test: EVM Nesting', async() => {104  itWeb3('NFT: disallows to nest token if nesting is disabled', async ({api, web3, privateKeyWrapper}) => {105    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);106107    const {collectionId, contract} = await createNestingCollection(api, web3, owner);108    await contract.methods.setCollectionNesting(false).send({from: owner});109110    // Create a token to nest into111    const targetNftTokenId = await contract.methods.nextTokenId().call();112    await contract.methods.mint(113      owner,114      targetNftTokenId,115    ).send({from: owner});116117    const targetNftTokenAddress = tokenIdToAddress(collectionId, targetNftTokenId);118119    // Create a token to nest120    const nftTokenId = await contract.methods.nextTokenId().call();121    await contract.methods.mint(122      owner,123      nftTokenId,124    ).send({from: owner});125126    // Try to nest127    await expect(contract.methods128      .transfer(targetNftTokenAddress, nftTokenId)129      .call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest');130  });131132  itWeb3('NFT: disallows a non-Owner to nest someone else\'s token', async ({api, web3, privateKeyWrapper}) => {133    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);134    const malignant = await createEthAccountWithBalance(api, web3, privateKeyWrapper);135136    const {collectionId, contract} = await createNestingCollection(api, web3, owner);137138    // Mint a token139    const targetTokenId = await contract.methods.nextTokenId().call();140    await contract.methods.mint(141      owner,142      targetTokenId,143    ).send({from: owner});144    const targetTokenAddress = tokenIdToAddress(collectionId, targetTokenId);145146    // Mint a token belonging to a different account147    const tokenId = await contract.methods.nextTokenId().call();148    await contract.methods.mint(149      malignant,150      tokenId,151    ).send({from: owner});152153    // Try to nest one token in another as a non-owner account154    await expect(contract.methods155      .transfer(targetTokenAddress, tokenId)156      .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest');157  });158159  itWeb3('NFT: disallows a non-Owner to nest someone else\'s token (Restricted nesting)', async ({api, web3, privateKeyWrapper}) => {160    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);161    const malignant = await createEthAccountWithBalance(api, web3, privateKeyWrapper);162163    const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(api, web3, owner);164    const {collectionAddress: collectionAddressB, contract: contractB} = await createNestingCollection(api, web3, owner);165166    await contractA.methods.setCollectionNesting(true, [collectionAddressA, collectionAddressB]).send({from: owner});167168    // Create a token in one collection169    const nftTokenIdA = await contractA.methods.nextTokenId().call();170    await contractA.methods.mint(171      owner,172      nftTokenIdA,173    ).send({from: owner});174    const nftTokenAddressA = tokenIdToAddress(collectionIdA, nftTokenIdA);175176    // Create a token in another collection belonging to someone else177    const nftTokenIdB = await contractB.methods.nextTokenId().call();178    await contractB.methods.mint(179      malignant,180      nftTokenIdB,181    ).send({from: owner});182183    // Try to drag someone else's token into the other collection and nest184    await expect(contractB.methods185      .transfer(nftTokenAddressA, nftTokenIdB)186      .call({from: malignant})).to.be.rejectedWith('UserIsNotAllowedToNest');187  });188189  itWeb3('NFT: disallows to nest token in an unlisted collection', async ({api, web3, privateKeyWrapper}) => {190    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);191192    const {collectionId: collectionIdA, collectionAddress: collectionAddressA, contract: contractA} = await createNestingCollection(api, web3, owner);193    const {contract: contractB} = await createNestingCollection(api, web3, owner);194195    await contractA.methods.setCollectionNesting(true, [collectionAddressA]).send({from: owner});196197    // Create a token in one collection198    const nftTokenIdA = await contractA.methods.nextTokenId().call();199    await contractA.methods.mint(200      owner,201      nftTokenIdA,202    ).send({from: owner});203    const nftTokenAddressA = tokenIdToAddress(collectionIdA, nftTokenIdA);204205    // Create a token in another collection206    const nftTokenIdB = await contractB.methods.nextTokenId().call();207    await contractB.methods.mint(208      owner,209      nftTokenIdB,210    ).send({from: owner});211212    // Try to nest into a token in the other collection, disallowed in the first213    await expect(contractB.methods214      .transfer(nftTokenAddressA, nftTokenIdB)215      .call()).to.be.rejectedWith('SourceCollectionIsNotAllowedToNest');216  });217});
addedtests/src/eth/util/playgrounds/index.tsdiffbeforeafterboth
--- /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<void>) => {
+  // 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
addedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- /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