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

difftreelog

source

tests/src/eth/util/helpers.ts12.3 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// eslint-disable-next-line @typescript-eslint/triple-slash-reference18/// <reference path="helpers.d.ts" />1920import {ApiPromise} from '@polkadot/api';21import {IKeyringPair} from '@polkadot/types/types';22import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';23import {expect} from 'chai';24import * as solc from 'solc';25import Web3 from 'web3';26import config from '../../config';27import getBalance from '../../substrate/get-balance';28import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';29import waitNewBlocks from '../../substrate/wait-new-blocks';30import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers';31import collectionHelpersAbi from '../collectionHelpersAbi.json';32import fungibleAbi from '../fungibleAbi.json';33import nonFungibleAbi from '../nonFungibleAbi.json';34import refungibleAbi from '../refungibleAbi.json';35import contractHelpersAbi from './contractHelpersAbi.json';3637export const GAS_ARGS = {gas: 2500000};3839export enum SponsoringMode {40  Disabled = 0,41  Allowlisted = 1,42  Generous = 2,43}4445let web3Connected = false;46export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {47  if (web3Connected) throw new Error('do not nest usingWeb3 calls');48  web3Connected = true;4950  const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);51  const web3 = new Web3(provider);5253  try {54    return await cb(web3);55  } finally {56    // provider.disconnect(3000, 'normal disconnect');57    provider.connection.close();58    web3Connected = false;59  }60}6162function encodeIntBE(v: number): number[] {63  if (v >= 0xffffffff || v < 0) throw new Error('id overflow');64  return [65    v >> 24,66    (v >> 16) & 0xff,67    (v >> 8) & 0xff,68    v & 0xff,69  ];70}7172export async function getCollectionAddressFromResult(api: ApiPromise, result: any) {73  const collectionIdAddress = normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);74  const collectionId = collectionIdFromAddress(collectionIdAddress);  75  const collection = (await getDetailedCollectionInfo(api, collectionId))!;76  return {collectionIdAddress, collectionId, collection};77}7879export function collectionIdToAddress(collection: number): string {80  const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,81    ...encodeIntBE(collection),82  ]);83  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));84}85export function collectionIdFromAddress(address: string): number {86  if (!address.startsWith('0x'))87    throw 'address not starts with "0x"';88  if (address.length > 42)89    throw 'address length is more than 20 bytes';90  return Number('0x' + address.substring(address.length - 8));91}92  93export function normalizeAddress(address: string): string {94  return '0x' + address.substring(address.length - 40);95}9697export function tokenIdToAddress(collection: number, token: number): string {98  const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0,99    ...encodeIntBE(collection),100    ...encodeIntBE(token),101  ]);102  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));103}104export function tokenIdToCross(collection: number, token: number): CrossAccountId {105  return {106    Ethereum: tokenIdToAddress(collection, token),107  };108}109110export function createEthAccount(web3: Web3) {111  const account = web3.eth.accounts.create();112  web3.eth.accounts.wallet.add(account.privateKey);113  return account.address;114}115116export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair) {117  const alice = privateKeyWrapper('//Alice');118  const account = createEthAccount(web3);119  await transferBalanceToEth(api, alice, account);120121  return account;122}123124export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {125  const tx = api.tx.balances.transfer(evmToAddress(target), amount);126  const events = await submitTransactionAsync(source, tx);127  const result = getGenericResult(events);128  expect(result.success).to.be.true;129}130131export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) {132  let i: any = it;133  if (opts.only) i = i.only;134  else if (opts.skip) i = i.skip;135  i(name, async () => {136    await usingApi(async (api, privateKeyWrapper) => {137      await usingWeb3(async web3 => {138        await cb({api, web3, privateKeyWrapper});139      });140    });141  });142}143itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {only: true});144itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {skip: true});145146export async function generateSubstrateEthPair(web3: Web3) {147  const account = web3.eth.accounts.create();148  evmToAddress(account.address);149}150151type NormalizedEvent = {152    address: string,153    event: string,154    args: { [key: string]: string }155};156157export function normalizeEvents(events: any): NormalizedEvent[] {158  const output = [];159  for (const key of Object.keys(events)) {160    if (key.match(/^[0-9]+$/)) {161      output.push(events[key]);162    } else if (Array.isArray(events[key])) {163      output.push(...events[key]);164    } else {165      output.push(events[key]);166    }167  }168  output.sort((a, b) => a.logIndex - b.logIndex);169  return output.map(({address, event, returnValues}) => {170    const args: { [key: string]: string } = {};171    for (const key of Object.keys(returnValues)) {172      if (!key.match(/^[0-9]+$/)) {173        args[key] = returnValues[key];174      }175    }176    return {177      address,178      event,179      args,180    };181  });182}183184export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {185  const out: any = [];186  contract.events.allEvents((_: any, event: any) => {187    out.push(event);188  });189  await action();190  return normalizeEvents(out);191}192193export function subToEthLowercase(eth: string): string {194  const bytes = addressToEvm(eth);195  return '0x' + Buffer.from(bytes).toString('hex');196}197198export function subToEth(eth: string): string {199  return Web3.utils.toChecksumAddress(subToEthLowercase(eth));200}201202export function compileContract(name: string, src: string) {203  const out = JSON.parse(solc.compile(JSON.stringify({204    language: 'Solidity',205    sources: {206      [`${name}.sol`]: {207        content: `208          // SPDX-License-Identifier: UNLICENSED209          pragma solidity ^0.8.6;210211          ${src}212        `,213      },214    },215    settings: {216      outputSelection: {217        '*': {218          '*': ['*'],219        },220      },221    },222  }))).contracts[`${name}.sol`][name];223224  return {225    abi: out.abi,226    object: '0x' + out.evm.bytecode.object,227  };228}229230export async function deployFlipper(web3: Web3, deployer: string) {231  const compiled = compileContract('Flipper', `232    contract Flipper {233      bool value = false;234      function flip() public {235        value = !value;236      }237      function getValue() public view returns (bool) {238        return value;239      }240    }241  `);242  const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {243    data: compiled.object,244    from: deployer,245    ...GAS_ARGS,246  });247  const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});248249  return flipper;250}251252export async function deployCollector(web3: Web3, deployer: string) {253  const compiled = compileContract('Collector', `254    contract Collector {255      uint256 collected;256      fallback() external payable {257        giveMoney();258      }259      function giveMoney() public payable {260        collected += msg.value;261      }262      function getCollected() public view returns (uint256) {263        return collected;264      }265      function getUnaccounted() public view returns (uint256) {266        return address(this).balance - collected;267      }268269      function withdraw(address payable target) public {270        target.transfer(collected);271        collected = 0;272      }273    }274  `);275  const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {276    data: compiled.object,277    from: deployer,278    ...GAS_ARGS,279  });280  const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});281282  return collector;283}284285/** 286 * pallet evm_contract_helpers287 * @param web3 288 * @param caller - eth address289 * @returns 290 */291export function contractHelpers(web3: Web3, caller: string) {292  return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});293}294295/** 296 * evm collection helper297 * @param web3 298 * @param caller - eth address299 * @returns 300 */301export function evmCollectionHelpers(web3: Web3, caller: string) {302  return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS});303}304305/** 306 * evm collection307 * @param web3 308 * @param caller - eth address309 * @returns 310 */311export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) {312  let abi;313  switch (mode.type) {314    case 'Fungible':315      abi = fungibleAbi;316      break;317    318    case 'NFT':319      abi = nonFungibleAbi;320      break;321    322    case 'ReFungible':323      abi = refungibleAbi;324      break;325326    default:327      throw 'Bad collection mode';328  }329  const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS});330  return contract;331}332333/**334 * Execute ethereum method call using substrate account335 * @param to target contract336 * @param mkTx - closure, receiving `contract.methods`, and returning method call,337 * to be used as following (assuming `to` = erc20 contract):338 * `m => m.transfer(to, amount)`339 *340 * # Example341 * ```ts342 * executeEthTxOnSub(api, alice, erc20Contract, m => m.transfer(target, amount));343 * ```344 */345export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {346  const tx = api.tx.evm.call(347    subToEth(from.address),348    to.options.address,349    mkTx(to.methods).encodeABI(),350    value,351    GAS_ARGS.gas,352    await web3.eth.getGasPrice(),353    null,354    null,355    [],356  );357  const events = await submitTransactionAsync(from, tx);358  expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;359}360361export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {362  return (await getBalance(api, [evmToAddress(address)]))[0];363}364365/**366 * Measure how much gas given closure consumes367 *368 * @param user which user balance will be checked369 */370export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {371  const before = await ethBalanceViaSub(api, user);372373  await call();374375  // In dev mode, the transaction might not finish processing in time376  await waitNewBlocks(api, 1);377  const after = await ethBalanceViaSub(api, user);378379  // Can't use .to.be.less, because chai doesn't supports bigint380  expect(after < before).to.be.true;381382  return before - after;383}384385type ElementOf<A> = A extends readonly (infer T)[] ? T : never;386// I want a fancier api, not a memory efficiency387export function* cartesian<T extends Array<Array<any>>, R extends Array<any>>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf<T[K]>}]> {388  if(args.length === 0) {389    yield internalRest as any;390    return;391  }392  for(const value of args[0]) {393    yield* cartesian([...internalRest, value], ...args.slice(1)) as any;394  }395}