git.delta.rocks / unique-network / refs/commits / 2e1cabd3ad66

difftreelog

feat configure min gas price

Yaroslav Bolyukin2021-11-05parent: #540e85d.patch.diff
in: master

9 files changed

modifiedruntime/src/lib.rsdiffbeforeafterboth
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -243,7 +243,8 @@
 pub struct FixedFee;
 impl FeeCalculator for FixedFee {
 	fn min_gas_price() -> U256 {
-		1.into()
+		// Targeting 0.15 UNQ per transfer
+		(1 * MICROUNIQUE).into()
 	}
 }
 
modifiedtests/src/creditFeesToTreasury.test.tsdiffbeforeafterboth
--- a/tests/src/creditFeesToTreasury.test.ts
+++ b/tests/src/creditFeesToTreasury.test.ts
@@ -14,6 +14,7 @@
   createItemExpectSuccess,
   getGenericResult,
   transferExpectSuccess,
+  UNIQUE,
 } from './util/helpers';
 
 import {default as waitNewBlocks} from './substrate/wait-new-blocks';
@@ -169,12 +170,11 @@
       const aliceBalanceBefore: bigint = (await api.query.system.account(alicesPublicKey)).data.free.toBigInt();
       await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT');
       const aliceBalanceAfter: bigint = (await api.query.system.account(alicesPublicKey)).data.free.toBigInt();
-      const fee = aliceBalanceBefore - aliceBalanceAfter;
+      const fee = Number(aliceBalanceBefore - aliceBalanceAfter) / Number(UNIQUE);
 
-      // console.log(fee.toString());
       const expectedTransferFee = 0.1;
       const tolerance = 0.001;
-      expect(Number(fee) / 1e15 - expectedTransferFee).to.be.lessThan(tolerance);
+      expect(Number(fee) / Number(UNIQUE) - expectedTransferFee).to.be.lessThan(tolerance);
     });
   });
 
modifiedtests/src/eth/base.test.tsdiffbeforeafterboth
--- a/tests/src/eth/base.test.ts
+++ b/tests/src/eth/base.test.ts
@@ -1,7 +1,9 @@
 
-import {createEthAccount, createEthAccountWithBalance, deployFlipper, ethBalanceViaSub, GAS_ARGS, itWeb3, recordEthFee} from './util/helpers';
+import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, deployFlipper, ethBalanceViaSub, GAS_ARGS, itWeb3, recordEthFee} from './util/helpers';
 import {expect} from 'chai';
-import {UNIQUE} from '../util/helpers';
+import {createCollectionExpectSuccess, createItemExpectSuccess, UNIQUE} from '../util/helpers';
+import nonFungibleAbi from './nonFungibleAbi.json';
+import privateKey from '../substrate/privateKey';
 
 describe('Contract calls', () => {
   itWeb3('Call of simple contract fee is less than 0.2 UNQ', async ({web3, api}) => {
@@ -19,4 +21,26 @@
     const cost = await recordEthFee(api, userA, () => web3.eth.sendTransaction({from: userA, to: userB, value: '1000000', ...GAS_ARGS}));
     expect(cost - await ethBalanceViaSub(api, userB) < BigInt(0.2 * Number(UNIQUE))).to.be.true;
   });
+
+  itWeb3('NFT transfer is close to 0.15 UNQ', async ({web3, api}) => {
+    const caller = await createEthAccountWithBalance(api, web3);
+    const receiver = createEthAccount(web3);
+
+    const alice = privateKey('//Alice');
+    const collection = await createCollectionExpectSuccess({
+      mode: {type: 'NFT'},
+    });
+    const itemId = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: caller});
+
+    const address = collectionIdToAddress(collection);
+    const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+
+    const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, itemId).send(caller));
+
+    const fee = Number(cost) / Number(UNIQUE);
+    const expectedFee = 0.15;
+    const tolerance = 0.002;
+
+    expect(Math.abs(fee - expectedFee) < tolerance).to.be.true;
+  });
 });
modifiedtests/src/eth/fungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/fungible.test.ts
+++ b/tests/src/eth/fungible.test.ts
@@ -95,12 +95,12 @@
     const alice = privateKey('//Alice');
 
     const owner = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, owner, 999999999999999);
+    await transferBalanceToEth(api, alice, owner);
 
     await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});
 
     const spender = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, spender, 999999999999999);
+    await transferBalanceToEth(api, alice, spender);
 
     const receiver = createEthAccount(web3);
 
@@ -153,12 +153,12 @@
     const alice = privateKey('//Alice');
 
     const owner = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, owner, 999999999999999);
+    await transferBalanceToEth(api, alice, owner);
 
     await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});
 
     const receiver = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, receiver, 999999999999999);
+    await transferBalanceToEth(api, alice, receiver);
 
     const address = collectionIdToAddress(collection);
     const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});
modifiedtests/src/eth/marketplace/marketplace.test.tsdiffbeforeafterboth
--- a/tests/src/eth/marketplace/marketplace.test.ts
+++ b/tests/src/eth/marketplace/marketplace.test.ts
@@ -38,8 +38,8 @@
 
     // Ask
     {
-      await executeEthTxOnSub(api, seller, evmCollection, m => m.approve(matcher.options.address, tokenId));
-      await executeEthTxOnSub(api, seller, matcher, m => m.setAsk(PRICE, '0x0000000000000000000000000000000000000000', evmCollection.options.address, tokenId, 1));
+      await executeEthTxOnSub(web3, api, seller, evmCollection, m => m.approve(matcher.options.address, tokenId));
+      await executeEthTxOnSub(web3, api, seller, matcher, m => m.setAsk(PRICE, '0x0000000000000000000000000000000000000000', evmCollection.options.address, tokenId, 1));
     }
 
     // Token is transferred to matcher
@@ -49,7 +49,7 @@
     {
       const sellerBalanceBeforePurchase = await getBalanceSingle(api, seller.address);
       // There is two functions named 'buy', so we should provide full signature
-      await executeEthTxOnSub(api, alice, matcher, m => m['buy(address,uint256)'](evmCollection.options.address, tokenId), {value: PRICE});
+      await executeEthTxOnSub(web3, api, alice, matcher, m => m['buy(address,uint256)'](evmCollection.options.address, tokenId), {value: PRICE});
       expect(await getBalanceSingle(api, seller.address) - sellerBalanceBeforePurchase === PRICE);
     }
 
@@ -95,8 +95,8 @@
 
     // Ask
     {
-      await executeEthTxOnSub(api, seller, evmCollection, m => m.approve(matcher.options.address, tokenId));
-      await executeEthTxOnSub(api, seller, matcher, m => m.setAsk(PRICE, evmFungible.options.address, evmCollection.options.address, tokenId, 1));
+      await executeEthTxOnSub(web3, api, seller, evmCollection, m => m.approve(matcher.options.address, tokenId));
+      await executeEthTxOnSub(web3, api, seller, matcher, m => m.setAsk(PRICE, evmFungible.options.address, evmCollection.options.address, tokenId, 1));
     }
 
     // Token is transferred to matcher
@@ -107,9 +107,9 @@
       const sellerBalanceBeforePurchase = BigInt(await evmFungible.methods.balanceOf(subToEth(seller.address)).call());
       const buyerBalanceBeforePurchase = BigInt(await evmFungible.methods.balanceOf(subToEth(alice.address)).call());
 
-      await executeEthTxOnSub(api, alice, evmFungible, m => m.approve(matcher.options.address, PRICE));
+      await executeEthTxOnSub(web3, api, alice, evmFungible, m => m.approve(matcher.options.address, PRICE));
       // There is two functions named 'buy', so we should provide full signature
-      await executeEthTxOnSub(api, alice, matcher, m =>
+      await executeEthTxOnSub(web3, api, alice, matcher, m =>
         m['buy(address,uint256,address,uint256)'](evmCollection.options.address, tokenId, evmFungible.options.address, PRICE));
 
       // Approved price is removed from buyer balance, and added to seller
@@ -158,8 +158,8 @@
 
     // Ask
     {
-      await executeEthTxOnSub(api, seller, evmCollection, m => m.approve(matcher.options.address, tokenId));
-      await executeEthTxOnSub(api, seller, matcher, m => m.setAsk(PRICE, ksmToken, evmCollection.options.address, tokenId, 1));
+      await executeEthTxOnSub(web3, api, seller, evmCollection, m => m.approve(matcher.options.address, tokenId));
+      await executeEthTxOnSub(web3, api, seller, matcher, m => m.setAsk(PRICE, ksmToken, evmCollection.options.address, tokenId, 1));
     }
 
     // Token is transferred to matcher
@@ -173,7 +173,7 @@
       expect(await matcher.methods.escrowBalance(ksmToken, subToEth(seller.address)).call()).to.be.equal('0');
       expect(await matcher.methods.escrowBalance(ksmToken, subToEth(alice.address)).call()).to.be.equal(PRICE.toString());
 
-      await executeEthTxOnSub(api, alice, matcher, m => m.buy(evmCollection.options.address, tokenId));
+      await executeEthTxOnSub(web3, api, alice, matcher, m => m.buy(evmCollection.options.address, tokenId));
 
       // Price is removed from buyer balance, and added to seller
       expect(await matcher.methods.escrowBalance(ksmToken, subToEth(alice.address)).call()).to.be.equal('0');
modifiedtests/src/eth/nonFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -203,7 +203,7 @@
     const alice = privateKey('//Alice');
 
     const owner = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, owner, 999999999999999);
+    await transferBalanceToEth(api, alice, owner);
 
     const tokenId = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: owner});
 
@@ -213,7 +213,7 @@
     const contract = new web3.eth.Contract(nonFungibleAbi as any, address);
 
     {
-      const result = await contract.methods.approve(spender, tokenId).send({from: owner, gas: '0x1000000', gasPrice: '0x01'});
+      const result = await contract.methods.approve(spender, tokenId).send({from: owner, ...GAS_ARGS});
       const events = normalizeEvents(result.events);
 
       expect(events).to.be.deep.equal([
@@ -237,12 +237,12 @@
     const alice = privateKey('//Alice');
 
     const owner = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, owner, 999999999999999);
+    await transferBalanceToEth(api, alice, owner);
 
     const tokenId = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: owner});
 
     const spender = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, spender, 999999999999999);
+    await transferBalanceToEth(api, alice, spender);
 
     const receiver = createEthAccount(web3);
 
@@ -285,12 +285,12 @@
     const alice = privateKey('//Alice');
 
     const owner = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, owner, 999999999999999);
+    await transferBalanceToEth(api, alice, owner);
 
     const tokenId = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: owner});
 
     const receiver = createEthAccount(web3);
-    await transferBalanceToEth(api, alice, receiver, 999999999999999);
+    await transferBalanceToEth(api, alice, receiver);
 
     const address = collectionIdToAddress(collection);
     const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: owner, ...GAS_ARGS});
modifiedtests/src/eth/payable.test.tsdiffbeforeafterboth
--- a/tests/src/eth/payable.test.ts
+++ b/tests/src/eth/payable.test.ts
@@ -32,7 +32,7 @@
         contract.methods.giveMoney().encodeABI(),
         '10000',
         GAS_ARGS.gas,
-        GAS_ARGS.gasPrice,
+        await web3.eth.getGasPrice(),
         null,
       );
       const events = await submitTransactionAsync(alice, tx);
modifiedtests/src/eth/proxy/nonFungibleProxy.test.tsdiffbeforeafterboth
--- a/tests/src/eth/proxy/nonFungibleProxy.test.ts
+++ b/tests/src/eth/proxy/nonFungibleProxy.test.ts
@@ -224,7 +224,7 @@
     const tokenId = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: contract.options.address});
 
     {
-      const result = await contract.methods.approve(spender, tokenId).send({from: caller, gas: '0x1000000', gasPrice: '0x01'});
+      const result = await contract.methods.approve(spender, tokenId).send({from: caller, ...GAS_ARGS});
       const events = normalizeEvents(result.events);
 
       expect(events).to.be.deep.equal([
modifiedtests/src/eth/util/helpers.tsdiffbeforeafterboth
before · tests/src/eth/util/helpers.ts
1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56// eslint-disable-next-line @typescript-eslint/triple-slash-reference7/// <reference path="helpers.d.ts" />89import {ApiPromise} from '@polkadot/api';10import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';11import Web3 from 'web3';12import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';13import {IKeyringPair} from '@polkadot/types/types';14import {expect} from 'chai';15import {getGenericResult} from '../../util/helpers';16import * as solc from 'solc';17import config from '../../config';18import privateKey from '../../substrate/privateKey';19import contractHelpersAbi from './contractHelpersAbi.json';20import getBalance from '../../substrate/get-balance';2122export const GAS_ARGS = {gas: 0x1000000, gasPrice: '0x01'};2324let web3Connected = false;25export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {26  if (web3Connected) throw new Error('do not nest usingWeb3 calls');27  web3Connected = true;2829  const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);30  const web3 = new Web3(provider);3132  try {33    return await cb(web3);34  } finally {35    // provider.disconnect(3000, 'normal disconnect');36    provider.connection.close();37    web3Connected = false;38  }39}4041export function collectionIdToAddress(address: number): string {42  if (address >= 0xffffffff || address < 0) throw new Error('id overflow');43  const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,44    address >> 24,45    (address >> 16) & 0xff,46    (address >> 8) & 0xff,47    address & 0xff,48  ]);49  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));50}5152export function createEthAccount(web3: Web3) {53  const account = web3.eth.accounts.create();54  web3.eth.accounts.wallet.add(account.privateKey);55  return account.address;56}5758export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3) {59  const alice = privateKey('//Alice');60  const account = createEthAccount(web3);61  await transferBalanceToEth(api, alice, account);6263  return account;64}6566export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 999999999999999) {67  const tx = api.tx.balances.transfer(evmToAddress(target), amount);68  const events = await submitTransactionAsync(source, tx);69  const result = getGenericResult(events);70  expect(result.success).to.be.true;71}7273export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any, opts: { only?: boolean, skip?: boolean } = {}) {74  let i: any = it;75  if (opts.only) i = i.only;76  else if (opts.skip) i = i.skip;77  i(name, async () => {78    await usingApi(async api => {79      await usingWeb3(async web3 => {80        await cb({api, web3});81      });82    });83  });84}85itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any) => itWeb3(name, cb, {only: true});86itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any) => itWeb3(name, cb, {skip: true});8788export async function generateSubstrateEthPair(web3: Web3) {89  const account = web3.eth.accounts.create();90  evmToAddress(account.address);91}9293type NormalizedEvent = {94    address: string,95    event: string,96    args: { [key: string]: string }97};9899export function normalizeEvents(events: any): NormalizedEvent[] {100  const output = [];101  for (const key of Object.keys(events)) {102    if (key.match(/^[0-9]+$/)) {103      output.push(events[key]);104    } else if (Array.isArray(events[key])) {105      output.push(...events[key]);106    } else {107      output.push(events[key]);108    }109  }110  output.sort((a, b) => a.logIndex - b.logIndex);111  return output.map(({address, event, returnValues}) => {112    const args: { [key: string]: string } = {};113    for (const key of Object.keys(returnValues)) {114      if (!key.match(/^[0-9]+$/)) {115        args[key] = returnValues[key];116      }117    }118    return {119      address,120      event,121      args,122    };123  });124}125126export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {127  const out: any = [];128  contract.events.allEvents((_: any, event: any) => {129    out.push(event);130  });131  await action();132  return normalizeEvents(out);133}134135export function subToEthLowercase(eth: string): string {136  const bytes = addressToEvm(eth);137  return '0x' + Buffer.from(bytes).toString('hex');138}139140export function subToEth(eth: string): string {141  return Web3.utils.toChecksumAddress(subToEthLowercase(eth));142}143144export function compileContract(name: string, src: string) {145  const out = JSON.parse(solc.compile(JSON.stringify({146    language: 'Solidity',147    sources: {148      [`${name}.sol`]: {149        content: `150          // SPDX-License-Identifier: UNLICENSED151          pragma solidity ^0.8.6;152153          ${src}154        `,155      },156    },157    settings: {158      outputSelection: {159        '*': {160          '*': ['*'],161        },162      },163    },164  }))).contracts[`${name}.sol`][name];165166  return {167    abi: out.abi,168    object: '0x' + out.evm.bytecode.object,169  };170}171172export async function deployFlipper(web3: Web3, deployer: string) {173  const compiled = compileContract('Flipper', `174    contract Flipper {175      bool value = false;176      function flip() public {177        value = !value;178      }179      function getValue() public view returns (bool) {180        return value;181      }182    }183  `);184  const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {185    data: compiled.object,186    from: deployer,187    ...GAS_ARGS,188  });189  const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});190191  return flipper;192}193194export async function deployCollector(web3: Web3, deployer: string) {195  const compiled = compileContract('Collector', `196    contract Collector {197      uint256 collected;198      fallback() external payable {199        giveMoney();200      }201      function giveMoney() public payable {202        collected += msg.value;203      }204      function getCollected() public view returns (uint256) {205        return collected;206      }207      function getUnaccounted() public view returns (uint256) {208        return address(this).balance - collected;209      }210211      function withdraw(address payable target) public {212        target.transfer(collected);213        collected = 0;214      }215    }216  `);217  const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {218    data: compiled.object,219    from: deployer,220    ...GAS_ARGS,221  });222  const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});223224  return collector;225}226227export function contractHelpers(web3: Web3, caller: string) {228  return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});229}230231export async function executeEthTxOnSub(api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {232  const tx = api.tx.evm.call(233    subToEth(from.address),234    to.options.address,235    mkTx(to.methods).encodeABI(),236    value,237    GAS_ARGS.gas,238    GAS_ARGS.gasPrice,239    null,240  );241  const events = await submitTransactionAsync(from, tx);242  expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;243}244245export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {246  return (await getBalance(api, [evmToAddress(address)]))[0];247}248249export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {250  const before = await ethBalanceViaSub(api, user);251252  await call();253254  const after = await ethBalanceViaSub(api, user);255256  // Can't use .to.be.less, because chai doesn't supports bigint257  expect(after < before).to.be.true;258259  return before - after;260}
after · tests/src/eth/util/helpers.ts
1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56// eslint-disable-next-line @typescript-eslint/triple-slash-reference7/// <reference path="helpers.d.ts" />89import {ApiPromise} from '@polkadot/api';10import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';11import Web3 from 'web3';12import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';13import {IKeyringPair} from '@polkadot/types/types';14import {expect} from 'chai';15import {getGenericResult, UNIQUE} from '../../util/helpers';16import * as solc from 'solc';17import config from '../../config';18import privateKey from '../../substrate/privateKey';19import contractHelpersAbi from './contractHelpersAbi.json';20import getBalance from '../../substrate/get-balance';2122export const GAS_ARGS = {gas: 1000000};2324let web3Connected = false;25export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {26  if (web3Connected) throw new Error('do not nest usingWeb3 calls');27  web3Connected = true;2829  const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);30  const web3 = new Web3(provider);3132  try {33    return await cb(web3);34  } finally {35    // provider.disconnect(3000, 'normal disconnect');36    provider.connection.close();37    web3Connected = false;38  }39}4041export function collectionIdToAddress(address: number): string {42  if (address >= 0xffffffff || address < 0) throw new Error('id overflow');43  const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,44    address >> 24,45    (address >> 16) & 0xff,46    (address >> 8) & 0xff,47    address & 0xff,48  ]);49  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));50}5152export function createEthAccount(web3: Web3) {53  const account = web3.eth.accounts.create();54  web3.eth.accounts.wallet.add(account.privateKey);55  return account.address;56}5758export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3) {59  const alice = privateKey('//Alice');60  const account = createEthAccount(web3);61  await transferBalanceToEth(api, alice, account);6263  return account;64}6566export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 10000n * UNIQUE) {67  const tx = api.tx.balances.transfer(evmToAddress(target), amount);68  const events = await submitTransactionAsync(source, tx);69  const result = getGenericResult(events);70  expect(result.success).to.be.true;71}7273export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any, opts: { only?: boolean, skip?: boolean } = {}) {74  let i: any = it;75  if (opts.only) i = i.only;76  else if (opts.skip) i = i.skip;77  i(name, async () => {78    await usingApi(async api => {79      await usingWeb3(async web3 => {80        await cb({api, web3});81      });82    });83  });84}85itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any) => itWeb3(name, cb, {only: true});86itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any) => itWeb3(name, cb, {skip: true});8788export async function generateSubstrateEthPair(web3: Web3) {89  const account = web3.eth.accounts.create();90  evmToAddress(account.address);91}9293type NormalizedEvent = {94    address: string,95    event: string,96    args: { [key: string]: string }97};9899export function normalizeEvents(events: any): NormalizedEvent[] {100  const output = [];101  for (const key of Object.keys(events)) {102    if (key.match(/^[0-9]+$/)) {103      output.push(events[key]);104    } else if (Array.isArray(events[key])) {105      output.push(...events[key]);106    } else {107      output.push(events[key]);108    }109  }110  output.sort((a, b) => a.logIndex - b.logIndex);111  return output.map(({address, event, returnValues}) => {112    const args: { [key: string]: string } = {};113    for (const key of Object.keys(returnValues)) {114      if (!key.match(/^[0-9]+$/)) {115        args[key] = returnValues[key];116      }117    }118    return {119      address,120      event,121      args,122    };123  });124}125126export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {127  const out: any = [];128  contract.events.allEvents((_: any, event: any) => {129    out.push(event);130  });131  await action();132  return normalizeEvents(out);133}134135export function subToEthLowercase(eth: string): string {136  const bytes = addressToEvm(eth);137  return '0x' + Buffer.from(bytes).toString('hex');138}139140export function subToEth(eth: string): string {141  return Web3.utils.toChecksumAddress(subToEthLowercase(eth));142}143144export function compileContract(name: string, src: string) {145  const out = JSON.parse(solc.compile(JSON.stringify({146    language: 'Solidity',147    sources: {148      [`${name}.sol`]: {149        content: `150          // SPDX-License-Identifier: UNLICENSED151          pragma solidity ^0.8.6;152153          ${src}154        `,155      },156    },157    settings: {158      outputSelection: {159        '*': {160          '*': ['*'],161        },162      },163    },164  }))).contracts[`${name}.sol`][name];165166  return {167    abi: out.abi,168    object: '0x' + out.evm.bytecode.object,169  };170}171172export async function deployFlipper(web3: Web3, deployer: string) {173  const compiled = compileContract('Flipper', `174    contract Flipper {175      bool value = false;176      function flip() public {177        value = !value;178      }179      function getValue() public view returns (bool) {180        return value;181      }182    }183  `);184  const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {185    data: compiled.object,186    from: deployer,187    ...GAS_ARGS,188  });189  const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});190191  return flipper;192}193194export async function deployCollector(web3: Web3, deployer: string) {195  const compiled = compileContract('Collector', `196    contract Collector {197      uint256 collected;198      fallback() external payable {199        giveMoney();200      }201      function giveMoney() public payable {202        collected += msg.value;203      }204      function getCollected() public view returns (uint256) {205        return collected;206      }207      function getUnaccounted() public view returns (uint256) {208        return address(this).balance - collected;209      }210211      function withdraw(address payable target) public {212        target.transfer(collected);213        collected = 0;214      }215    }216  `);217  const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {218    data: compiled.object,219    from: deployer,220    ...GAS_ARGS,221  });222  const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});223224  return collector;225}226227export function contractHelpers(web3: Web3, caller: string) {228  return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});229}230231export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {232  const tx = api.tx.evm.call(233    subToEth(from.address),234    to.options.address,235    mkTx(to.methods).encodeABI(),236    value,237    GAS_ARGS.gas,238    await web3.eth.getGasPrice(),239    null,240  );241  const events = await submitTransactionAsync(from, tx);242  expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;243}244245export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {246  return (await getBalance(api, [evmToAddress(address)]))[0];247}248249export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {250  const before = await ethBalanceViaSub(api, user);251252  await call();253254  const after = await ethBalanceViaSub(api, user);255256  // Can't use .to.be.less, because chai doesn't supports bigint257  expect(after < before).to.be.true;258259  return before - after;260}