1234567891011121314151617181920import {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 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}284285286287288289290291export function contractHelpers(web3: Web3, caller: string) {292 return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});293}294295296297298299300301export function evmCollectionHelpers(web3: Web3, caller: string) {302 return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS});303}304305306307308309310311export 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}332333334335336337338339340341342343344345export 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}364365366367368369370export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {371 const before = await ethBalanceViaSub(api, user);372373 await call();374375 376 await waitNewBlocks(api, 1);377 const after = await ethBalanceViaSub(api, user);378379 380 expect(after < before).to.be.true;381382 return before - after;383}384385type ElementOf<A> = A extends readonly (infer T)[] ? T : never;386387export 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}