1234567891011121314151617181920import {ApiPromise} from '@polkadot/api';21import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';22import Web3 from 'web3';23import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';24import {IKeyringPair} from '@polkadot/types/types';25import {expect} from 'chai';26import {CrossAccountId, getGenericResult, UNIQUE} from '../../util/helpers';27import * as solc from 'solc';28import config from '../../config';29import privateKey from '../../substrate/privateKey';30import contractHelpersAbi from './contractHelpersAbi.json';31import getBalance from '../../substrate/get-balance';32import waitNewBlocks from '../../substrate/wait-new-blocks';3334export const GAS_ARGS = {gas: 2500000};3536export enum SponsoringMode {37 Disabled = 0,38 Allowlisted = 1,39 Generous = 2,40}4142let web3Connected = false;43export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {44 if (web3Connected) throw new Error('do not nest usingWeb3 calls');45 web3Connected = true;4647 const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);48 const web3 = new Web3(provider);4950 try {51 return await cb(web3);52 } finally {53 54 provider.connection.close();55 web3Connected = false;56 }57}5859function encodeIntBE(v: number): number[] {60 if (v >= 0xffffffff || v < 0) throw new Error('id overflow');61 return [62 v >> 24,63 (v >> 16) & 0xff,64 (v >> 8) & 0xff,65 v & 0xff,66 ];67}6869export function collectionIdToAddress(collection: number): string {70 const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,71 ...encodeIntBE(collection),72 ]);73 return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));74}75export function collectionIdFromAddress(address: string): number {76 return Number('0x' + address.substring(address.length - 8));77}7879export function tokenIdToAddress(collection: number, token: number): string {80 const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0,81 ...encodeIntBE(collection),82 ...encodeIntBE(token),83 ]);84 return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));85}86export function tokenIdToCross(collection: number, token: number): CrossAccountId {87 return {88 Ethereum: tokenIdToAddress(collection, token),89 };9091export function createEthAccount(web3: Web3) {92 const account = web3.eth.accounts.create();93 web3.eth.accounts.wallet.add(account.privateKey);94 return account.address;95}9697export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3) {98 const alice = privateKey('//Alice');99 const account = createEthAccount(web3);100 await transferBalanceToEth(api, alice, account);101102 return account;103}104105export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {106 const tx = api.tx.balances.transfer(evmToAddress(target), amount);107 const events = await submitTransactionAsync(source, tx);108 const result = getGenericResult(events);109 expect(result.success).to.be.true;110}111112export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any, opts: { only?: boolean, skip?: boolean } = {}) {113 let i: any = it;114 if (opts.only) i = i.only;115 else if (opts.skip) i = i.skip;116 i(name, async () => {117 await usingApi(async api => {118 await usingWeb3(async web3 => {119 await cb({api, web3});120 });121 });122 });123}124itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any) => itWeb3(name, cb, {only: true});125itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise }) => any) => itWeb3(name, cb, {skip: true});126127export async function generateSubstrateEthPair(web3: Web3) {128 const account = web3.eth.accounts.create();129 evmToAddress(account.address);130}131132type NormalizedEvent = {133 address: string,134 event: string,135 args: { [key: string]: string }136};137138export function normalizeEvents(events: any): NormalizedEvent[] {139 const output = [];140 for (const key of Object.keys(events)) {141 if (key.match(/^[0-9]+$/)) {142 output.push(events[key]);143 } else if (Array.isArray(events[key])) {144 output.push(...events[key]);145 } else {146 output.push(events[key]);147 }148 }149 output.sort((a, b) => a.logIndex - b.logIndex);150 return output.map(({address, event, returnValues}) => {151 const args: { [key: string]: string } = {};152 for (const key of Object.keys(returnValues)) {153 if (!key.match(/^[0-9]+$/)) {154 args[key] = returnValues[key];155 }156 }157 return {158 address,159 event,160 args,161 };162 });163}164165export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {166 const out: any = [];167 contract.events.allEvents((_: any, event: any) => {168 out.push(event);169 });170 await action();171 return normalizeEvents(out);172}173174export function subToEthLowercase(eth: string): string {175 const bytes = addressToEvm(eth);176 return '0x' + Buffer.from(bytes).toString('hex');177}178179export function subToEth(eth: string): string {180 return Web3.utils.toChecksumAddress(subToEthLowercase(eth));181}182183export function compileContract(name: string, src: string) {184 const out = JSON.parse(solc.compile(JSON.stringify({185 language: 'Solidity',186 sources: {187 [`${name}.sol`]: {188 content: `189 // SPDX-License-Identifier: UNLICENSED190 pragma solidity ^0.8.6;191192 ${src}193 `,194 },195 },196 settings: {197 outputSelection: {198 '*': {199 '*': ['*'],200 },201 },202 },203 }))).contracts[`${name}.sol`][name];204205 return {206 abi: out.abi,207 object: '0x' + out.evm.bytecode.object,208 };209}210211export async function deployFlipper(web3: Web3, deployer: string) {212 const compiled = compileContract('Flipper', `213 contract Flipper {214 bool value = false;215 function flip() public {216 value = !value;217 }218 function getValue() public view returns (bool) {219 return value;220 }221 }222 `);223 const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {224 data: compiled.object,225 from: deployer,226 ...GAS_ARGS,227 });228 const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});229230 return flipper;231}232233export async function deployCollector(web3: Web3, deployer: string) {234 const compiled = compileContract('Collector', `235 contract Collector {236 uint256 collected;237 fallback() external payable {238 giveMoney();239 }240 function giveMoney() public payable {241 collected += msg.value;242 }243 function getCollected() public view returns (uint256) {244 return collected;245 }246 function getUnaccounted() public view returns (uint256) {247 return address(this).balance - collected;248 }249250 function withdraw(address payable target) public {251 target.transfer(collected);252 collected = 0;253 }254 }255 `);256 const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {257 data: compiled.object,258 from: deployer,259 ...GAS_ARGS,260 });261 const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});262263 return collector;264}265266267268269270271272export function contractHelpers(web3: Web3, caller: string) {273 return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});274}275276277278279280281282283284285286287288export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {289 const tx = api.tx.evm.call(290 subToEth(from.address),291 to.options.address,292 mkTx(to.methods).encodeABI(),293 value,294 GAS_ARGS.gas,295 await web3.eth.getGasPrice(),296 null,297 null,298 [],299 );300 const events = await submitTransactionAsync(from, tx);301 expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;302}303304export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {305 return (await getBalance(api, [evmToAddress(address)]))[0];306}307308309310311312313export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {314 const before = await ethBalanceViaSub(api, user);315316 await call();317318 319 await waitNewBlocks(api, 1);320 const after = await ethBalanceViaSub(api, user);321322 323 expect(after < before).to.be.true;324325 return before - after;326}327328type ElementOf<A> = A extends readonly (infer T)[] ? T : never;329330export function* cartesian<T extends Array<Array<any>>, R extends Array<any>>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf<T[K]>}]> {331 if(args.length === 0) {332 yield internalRest as any;333 return;334 }335 for(const value of args[0]) {336 yield* cartesian([...internalRest, value], ...args.slice(1)) as any;337 }338}