--- a/tests/package.json +++ b/tests/package.json @@ -30,6 +30,7 @@ "testEth": "mocha --timeout 9999999 -r ts-node/register './**/eth/**/*.test.ts'", "testEthMarketplace": "mocha --timeout 9999999 -r ts-node/register './**/eth/marketplace/**/*.test.ts'", "testEthNesting": "mocha --timeout 9999999 -r ts-node/register './**/eth/nesting/**/*.test.ts'", + "testEthPayable": "mocha --timeout 9999999 -r ts-node/register './**/eth/payable.test.ts'", "load": "mocha --timeout 9999999 -r ts-node/register './**/*.load.ts'", "loadTransfer": "ts-node src/transfer.nload.ts", "testCollision": "mocha --timeout 9999999 -r ts-node/register ./src/collision-tests/*.test.ts", --- a/tests/src/eth/payable.test.ts +++ b/tests/src/eth/payable.test.ts @@ -14,97 +14,91 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {expect} from 'chai'; -import {submitTransactionAsync} from '../substrate/substrate-api'; -import {createEthAccountWithBalance, deployCollector, GAS_ARGS, itWeb3, subToEth, transferBalanceToEth} from './util/helpers'; -import {evmToAddress} from '@polkadot/util-crypto'; -import {getGenericResult, UNIQUE} from '../util/helpers'; -import {getBalanceSingle, transferBalanceExpectSuccess} from '../substrate/get-balance'; +import {IKeyringPair} from '@polkadot/types/types'; + +import {itEth, expect, usingEthPlaygrounds} from './util/playgrounds'; describe('EVM payable contracts', () => { - itWeb3('Evm contract can receive wei from eth account', async ({api, web3, privateKeyWrapper}) => { - const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const contract = await deployCollector(web3, deployer); + let donor: IKeyringPair; - await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: '10000', ...GAS_ARGS}); + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + donor = privateKey('//Alice'); + }); + }); + + itEth('Evm contract can receive wei from eth account', async ({helper}) => { + const deployer = await helper.eth.createAccountWithBalance(donor); + const contract = await helper.eth.deployCollectorContract(deployer); + + const web3 = helper.getWeb3(); + + await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: '10000', gas: helper.eth.DEFAULT_GAS}); expect(await contract.methods.getCollected().call()).to.be.equal('10000'); }); - itWeb3('Evm contract can receive wei from substrate account', async ({api, web3, privateKeyWrapper}) => { - const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const contract = await deployCollector(web3, deployer); - const alice = privateKeyWrapper('//Alice'); + itEth('Evm contract can receive wei from substrate account', async ({helper}) => { + const deployer = await helper.eth.createAccountWithBalance(donor); + const contract = await helper.eth.deployCollectorContract(deployer); + const [alice] = await helper.arrange.createAccounts([10n], donor); + + const weiCount = '10000'; // Transaction fee/value will be payed from subToEth(sender) evm balance, // which is backed by evmToAddress(subToEth(sender)) substrate balance - await transferBalanceToEth(api, alice, subToEth(alice.address)); + await helper.eth.transferBalanceFromSubstrate(alice, helper.address.substrateToEth(alice.address), 5n); + - { - const tx = api.tx.evm.call( - subToEth(alice.address), - contract.options.address, - contract.methods.giveMoney().encodeABI(), - '10000', - GAS_ARGS.gas, - await web3.eth.getGasPrice(), - null, - null, - [], - ); - const events = await submitTransactionAsync(alice, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; - } + await helper.eth.callEVM(alice, contract.options.address, contract.methods.giveMoney().encodeABI(), weiCount); - expect(await contract.methods.getCollected().call()).to.be.equal('10000'); + expect(await contract.methods.getCollected().call()).to.be.equal(weiCount); }); // We can't handle sending balance to backing storage of evm balance, because evmToAddress operation is irreversible - itWeb3('Wei sent directly to backing storage of evm contract balance is unaccounted', async({api, web3, privateKeyWrapper}) => { - const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const contract = await deployCollector(web3, deployer); - const alice = privateKeyWrapper('//Alice'); + itEth('Wei sent directly to backing storage of evm contract balance is unaccounted', async({helper}) => { + const deployer = await helper.eth.createAccountWithBalance(donor); + const contract = await helper.eth.deployCollectorContract(deployer); + const [alice] = await helper.arrange.createAccounts([10n], donor); + + const weiCount = 10_000n; - await transferBalanceExpectSuccess(api, alice, evmToAddress(contract.options.address), '10000'); + await helper.eth.transferBalanceFromSubstrate(alice, contract.options.address, weiCount, false); - expect(await contract.methods.getUnaccounted().call()).to.be.equal('10000'); + expect(await contract.methods.getUnaccounted().call()).to.be.equal(weiCount.toString()); }); - itWeb3('Balance can be retrieved from evm contract', async({api, web3, privateKeyWrapper}) => { - const FEE_BALANCE = 1000n * UNIQUE; - const CONTRACT_BALANCE = 1n * UNIQUE; + itEth('Balance can be retrieved from evm contract', async({helper, privateKey}) => { + const FEE_BALANCE = 10n * helper.balance.getOneTokenNominal(); + const CONTRACT_BALANCE = 1n * helper.balance.getOneTokenNominal(); + + const deployer = await helper.eth.createAccountWithBalance(donor); + const contract = await helper.eth.deployCollectorContract(deployer); + const [alice] = await helper.arrange.createAccounts([20n], donor); - const deployer = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const contract = await deployCollector(web3, deployer); - const alice = privateKeyWrapper('//Alice'); + const web3 = helper.getWeb3(); - await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: CONTRACT_BALANCE.toString(), ...GAS_ARGS}); + await web3.eth.sendTransaction({from: deployer, to: contract.options.address, value: CONTRACT_BALANCE.toString(), gas: helper.eth.DEFAULT_GAS}); - const receiver = privateKeyWrapper(`//Receiver${Date.now()}`); + const receiver = privateKey(`//Receiver${Date.now()}`); // First receive balance on eth balance of bob { - const ethReceiver = subToEth(receiver.address); + const ethReceiver = helper.address.substrateToEth(receiver.address); expect(await web3.eth.getBalance(ethReceiver)).to.be.equal('0'); await contract.methods.withdraw(ethReceiver).send({from: deployer}); expect(await web3.eth.getBalance(ethReceiver)).to.be.equal(CONTRACT_BALANCE.toString()); } // Some balance is required to pay fee for evm.withdraw call - await transferBalanceExpectSuccess(api, alice, receiver.address, FEE_BALANCE.toString()); + await helper.balance.transferToSubstrate(alice, receiver.address, FEE_BALANCE); + // await transferBalanceExpectSuccess(api, alice, receiver.address, FEE_BALANCE.toString()); // Withdraw balance from eth to substrate { - const initialReceiverBalance = await getBalanceSingle(api, receiver.address); - const tx = api.tx.evm.withdraw( - subToEth(receiver.address), - CONTRACT_BALANCE.toString(), - ); - const events = await submitTransactionAsync(receiver, tx); - const result = getGenericResult(events); - expect(result.success).to.be.true; - const finalReceiverBalance = await getBalanceSingle(api, receiver.address); + const initialReceiverBalance = await helper.balance.getSubstrate(receiver.address); + await helper.executeExtrinsic(receiver, 'api.tx.evm.withdraw', [helper.address.substrateToEth(receiver.address), CONTRACT_BALANCE.toString()], true); + const finalReceiverBalance = await helper.balance.getSubstrate(receiver.address); expect(finalReceiverBalance > initialReceiverBalance).to.be.true; } --- /dev/null +++ b/tests/src/eth/util/playgrounds/types.ts @@ -0,0 +1,9 @@ +export interface ContractImports { + solPath: string; + fsPath: string; +} + +export interface CompiledContract { + abi: any; + object: string; +} \ No newline at end of file --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -3,15 +3,21 @@ /* eslint-disable function-call-argument-newline */ +import {readFile} from 'fs/promises'; + import Web3 from 'web3'; import {WebsocketProvider} from 'web3-core'; import {Contract} from 'web3-eth-contract'; +import * as solc from 'solc'; + import {evmToAddress} from '@polkadot/util-crypto'; import {IKeyringPair} from '@polkadot/types/types'; import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev'; +import {ContractImports, CompiledContract} from './types'; + // Native contracts ABI import collectionHelpersAbi from '../../collectionHelpersAbi.json'; import fungibleAbi from '../../fungibleAbi.json'; @@ -27,19 +33,75 @@ this.helper = helper; } } + + +class ContractGroup extends EthGroupBase { + async findImports(imports?: ContractImports[]){ + if(!imports) return function(path: string) { + return {error: 'File not found'}; + }; + + const knownImports = {} as any; + for(let imp of imports) { + knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString(); + } + + return function(path: string) { + if(knownImports.hasOwnProperty(path)) return {contents: knownImports[path]}; + return {error: 'File not found'}; + } + } + + async compile(name: string, src: string, imports?: ContractImports[]): Promise { + const out = JSON.parse(solc.compile(JSON.stringify({ + language: 'Solidity', + sources: { + [`${name}.sol`]: { + content: src, + }, + }, + settings: { + outputSelection: { + '*': { + '*': ['*'], + }, + }, + }, + }), {import: await this.findImports(imports)})).contracts[`${name}.sol`][name]; + return { + abi: out.abi, + object: '0x' + out.evm.bytecode.object, + }; + } + + async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[]): Promise { + const compiledContract = await this.compile(name, src, imports); + return this.deployByAbi(signer, compiledContract.abi, compiledContract.object); + } + + async deployByAbi(signer: string, abi: any, object: string): Promise { + const web3 = this.helper.getWeb3(); + const contract = new web3.eth.Contract(abi, undefined, { + data: object, + from: signer, + gas: this.helper.eth.DEFAULT_GAS + }); + return await contract.deploy({data: object}).send({from: signer}); + } + +} 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}); + return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, gas: this.helper.eth.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}); + return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.helper.eth.DEFAULT_GAS}); } collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract { @@ -49,12 +111,12 @@ 'ft': fungibleAbi }[mode]; const web3 = this.helper.getWeb3(); - return new web3.eth.Contract(abi as any, address, {gas: this.DEFAULT_GAS, ...(caller ? {from: caller} : {})}); + return new web3.eth.Contract(abi as any, address, {gas: this.helper.eth.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} : {})}); + return new web3.eth.Contract(refungibleTokenAbi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})}); } rftToken(collectionId: number, tokenId: number, caller?: string): Contract { @@ -64,6 +126,8 @@ class EthGroup extends EthGroupBase { + DEFAULT_GAS = 2_500_000; + createAccount() { const web3 = this.helper.getWeb3(); const account = web3.eth.accounts.create(); @@ -78,8 +142,20 @@ 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 transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=1000n, inTokens=true) { + return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n)); + } + + async callEVM(signer: IKeyringPair, contractAddress: string, abi: any, value: string, gasLimit?: number) { + if(!gasLimit) gasLimit = this.DEFAULT_GAS; + const web3 = this.helper.getWeb3(); + const gasPrice = await web3.eth.getGasPrice(); + // TODO: check execution status + await this.helper.executeExtrinsic( + signer, + 'api.tx.evm.call', [this.helper.address.substrateToEth(signer.address), contractAddress, abi, value, gasLimit, gasPrice, null, null, []], + true, `Unable to perform evm.call` + ); } async createNonfungibleCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { @@ -92,6 +168,34 @@ return {collectionId, collectionAddress}; } + + async deployCollectorContract(signer: string): Promise { + return await this.helper.ethContract.deployByCode(signer, 'Collector', ` + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.6; + + contract Collector { + uint256 collected; + fallback() external payable { + giveMoney(); + } + function giveMoney() public payable { + collected += msg.value; + } + function getCollected() public view returns (uint256) { + return collected; + } + function getUnaccounted() public view returns (uint256) { + return address(this).balance - collected; + } + + function withdraw(address payable target) public { + target.transfer(collected); + collected = 0; + } + } + `); + } } @@ -134,12 +238,14 @@ eth: EthGroup; ethAddress: EthAddressGroup; ethNativeContract: NativeContractGroup; + ethContract: ContractGroup; 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); + this.ethContract = new ContractGroup(this); } getWeb3(): Web3 { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -388,6 +388,7 @@ type: this.chainLogType.EXTRINSIC, status: result.status, call: extrinsic, + signer: this.getSignerAddress(sender), params, } as IUniqueHelperLog;