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
--- 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');
+    });
   });
 });
addedtests/src/eth/util/playgrounds/index.tsdiffbeforeafterboth

no content

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