diff --git a/apps/remix-ide/src/app/tabs/network-module.js b/apps/remix-ide/src/app/tabs/network-module.js index fdae172c15..c1cc1e12b5 100644 --- a/apps/remix-ide/src/app/tabs/network-module.js +++ b/apps/remix-ide/src/app/tabs/network-module.js @@ -22,18 +22,6 @@ export class NetworkModule extends Plugin { this.blockchain.event.register('contextChanged', (provider) => { this.emit('providerChanged', provider) }) - /* - // Events that could be implemented later - executionContext.event.register('removeProvider', (provider) => { - this.events.emit('networkRemoved', provider) - }) - executionContext.event.register('addProvider', (provider) => { - this.events.emit('networkAdded', provider) - }) - executionContext.event.register('web3EndpointChanged', (provider) => { - this.events.emit('web3EndpointChanged', provider) - }) - */ } /** Return the current network provider (web3, vm, injected) */ diff --git a/apps/remix-ide/src/app/tabs/runTab/model/recorder.js b/apps/remix-ide/src/app/tabs/runTab/model/recorder.js index 126b354927..f9a5d9aa78 100644 --- a/apps/remix-ide/src/app/tabs/runTab/model/recorder.js +++ b/apps/remix-ide/src/app/tabs/runTab/model/recorder.js @@ -63,10 +63,10 @@ class Recorder { } }) - this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload, rawAddress) => { + this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload) => { if (error) return console.log(error) if (call) return - + const rawAddress = txResult.receipt.contractAddress if (!rawAddress) return // not a contract creation const address = helper.addressToString(rawAddress) // save back created addresses for the convertion from tokens to real adresses diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 84062f7ac4..09ca8a05a2 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -3,7 +3,8 @@ const txFormat = remixLib.execution.txFormat const txExecution = remixLib.execution.txExecution const typeConversion = remixLib.execution.typeConversion const Txlistener = remixLib.execution.txListener -const TxRunner = remixLib.execution.txRunner +const TxRunner = remixLib.execution.TxRunner +const TxRunnerWeb3 = remixLib.execution.TxRunnerWeb3 const txHelper = remixLib.execution.txHelper const EventManager = remixLib.EventManager const executionContext = remixLib.execution.executionContext @@ -12,7 +13,7 @@ const Web3 = require('web3') const async = require('async') const { EventEmitter } = require('events') -const { resultToRemixTx } = require('./txResultHelper') +const { resultToRemixTx } = remixLib.helpers.txResultHelper const VMProvider = require('./providers/vm.js') const InjectedProvider = require('./providers/injected.js') @@ -26,8 +27,7 @@ class Blockchain { this.events = new EventEmitter() this.config = config - - this.txRunner = new TxRunner({}, { + const web3Runner = new TxRunnerWeb3({ config: config, detectNetwork: (cb) => { this.executionContext.detectNetwork(cb) @@ -35,7 +35,9 @@ class Blockchain { personalMode: () => { return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false } - }, this.executionContext) + }, _ => this.executionContext.web3(), _ => this.executionContext.currentblockGasLimit()) + this.txRunner = new TxRunner(web3Runner, { runAsync: true }) + this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this)) this.networkcallid = 0 @@ -123,7 +125,7 @@ class Blockchain { if (error) { return finalCb(`creation of ${selectedContract.name} errored: ${(error.message ? error.message : error)}`) } - if (txResult.result.status && txResult.result.status === '0x0') { + if (txResult.receipt.status === false || txResult.receipt.status === '0x0') { return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`) } finalCb(null, selectedContract, address) @@ -309,18 +311,17 @@ class Blockchain { resetEnvironment () { this.getCurrentProvider().resetEnvironment() // TODO: most params here can be refactored away in txRunner - // this.txRunner = new TxRunner(this.providers.vm.accounts, { - this.txRunner = new TxRunner(this.providers.vm.RemixSimulatorProvider.Accounts.accounts, { - // TODO: only used to check value of doNotShowTransactionConfirmationAgain property + const web3Runner = new TxRunnerWeb3({ config: this.config, - // TODO: to refactor, TxRunner already has access to executionContext detectNetwork: (cb) => { this.executionContext.detectNetwork(cb) }, personalMode: () => { return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false } - }, this.executionContext) + }, _ => this.executionContext.web3(), _ => this.executionContext.currentblockGasLimit()) + + this.txRunner = new TxRunner(web3Runner, { runAsync: true }) this.txRunner.event.register('transactionBroadcasted', (txhash) => { this.executionContext.detectNetwork((error, network) => { if (error || !network) return @@ -372,10 +373,11 @@ class Blockchain { (network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() }, (error, continueTxExecution, cancelCb) => { if (error) { reject(error) } else { continueTxExecution() } }, (okCb, cancelCb) => { okCb() }, - (error, result) => { + async (error, result) => { if (error) return reject(error) try { - resolve(resultToRemixTx(result)) + const execResult = await this.web3().eth.getExecutionResultFromSimulator(result.transactionHash) + resolve(resultToRemixTx(result, execResult)) } catch (e) { reject(e) } @@ -429,19 +431,24 @@ class Blockchain { function runTransaction (fromAddress, value, gasLimit, next) { const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp } const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences } - let timestamp = Date.now() - if (tx.timestamp) { - timestamp = tx.timestamp - } + if (!tx.timestamp) tx.timestamp = Date.now() + const timestamp = tx.timestamp self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad]) self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb, - function (error, result) { + async (error, result) => { if (error) return next(error) - const rawAddress = self.executionContext.isVM() ? (result.result.createdAddress && result.result.createdAddress.toBuffer()) : result.result.contractAddress + const isVM = self.executionContext.isVM() + if (isVM && tx.useCall) { + try { + result.transactionHash = await self.web3().eth.getHashFromTagBySimulator(timestamp) + } catch (e) { + console.log('unable to retrieve back the "call" hash', e) + } + } const eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted') - self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad, rawAddress]) + self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad]) if (error && (typeof (error) !== 'string')) { if (error.message) error = error.message @@ -454,25 +461,29 @@ class Blockchain { ) } ], - (error, txResult) => { + async (error, txResult) => { if (error) { return cb(error) } const isVM = this.executionContext.isVM() + let execResult + let returnValue = null if (isVM) { - const vmError = txExecution.checkVMError(txResult) - if (vmError.error) { - return cb(vmError.message) + execResult = await this.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash) + if (execResult) { + // if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value. + returnValue = (execResult && isVM) ? execResult.returnValue : txResult + const vmError = txExecution.checkVMError(execResult) + if (vmError.error) { + return cb(vmError.message) + } } } let address = null - let returnValue = null - if (txResult && txResult.result) { - address = isVM ? (txResult.result.createdAddress && txResult.result.createdAddress.toBuffer()) : txResult.result.contractAddress - // if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value. - returnValue = (txResult.result.execResult && isVM) ? txResult.result.execResult.returnValue : txResult.result + if (txResult && txResult.receipt) { + address = txResult.receipt.contractAddress } cb(error, txResult, address, returnValue) diff --git a/apps/remix-ide/src/blockchain/providers/vm.js b/apps/remix-ide/src/blockchain/providers/vm.js index 8ec6583ff7..c251743e2b 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.js +++ b/apps/remix-ide/src/blockchain/providers/vm.js @@ -1,14 +1,16 @@ const Web3 = require('web3') -const { BN, privateToAddress, hashPersonalMessage } = require('ethereumjs-util') -const RemixSimulator = require('@remix-project/remix-simulator') +const { BN, privateToAddress, stripHexPrefix, hashPersonalMessage } = require('ethereumjs-util') +const { Provider, extend } = require('@remix-project/remix-simulator') class VMProvider { constructor (executionContext) { this.executionContext = executionContext - this.RemixSimulatorProvider = new RemixSimulator.Provider({ executionContext: this.executionContext }) + this.RemixSimulatorProvider = new Provider({}) this.RemixSimulatorProvider.init() this.web3 = new Web3(this.RemixSimulatorProvider) + extend(this.web3) this.accounts = {} + this.executionContext.setWeb3('vm', this.web3) } getAccounts (cb) { diff --git a/apps/remix-ide/src/blockchain/txResultHelper.js b/apps/remix-ide/src/blockchain/txResultHelper.js deleted file mode 100644 index f608324062..0000000000 --- a/apps/remix-ide/src/blockchain/txResultHelper.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' -const { bufferToHex, isHexString } = require('ethereumjs-util') - -function convertToPrefixedHex (input) { - if (input === undefined || input === null || isHexString(input)) { - return input - } else if (Buffer.isBuffer(input)) { - return bufferToHex(input) - } - return '0x' + input.toString(16) -} - -/* - txResult.result can be 3 different things: - - VM call or tx: ethereumjs-vm result object - - Node transaction: object returned from eth.getTransactionReceipt() - - Node call: return value from function call (not an object) - - Also, VM results use BN and Buffers, Node results use hex strings/ints, - So we need to normalize the values to prefixed hex strings -*/ -function resultToRemixTx (txResult) { - const { result, transactionHash } = txResult - const { status, execResult, gasUsed, createdAddress, contractAddress } = result - let returnValue, errorMessage - - if (isHexString(result)) { - returnValue = result - } else if (execResult !== undefined) { - returnValue = execResult.returnValue - errorMessage = execResult.exceptionError - } - - return { - transactionHash, - status, - gasUsed: convertToPrefixedHex(gasUsed), - error: errorMessage, - return: convertToPrefixedHex(returnValue), - createdAddress: convertToPrefixedHex(createdAddress || contractAddress) - } -} - -module.exports = { - resultToRemixTx -} diff --git a/jest.config.js b/jest.config.js index 30b91f3cbe..4b90409756 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,5 +5,18 @@ module.exports = { }, resolver: '@nrwl/jest/plugins/resolver', moduleFileExtensions: ['ts', 'js', 'html'], - coverageReporters: ['html'] + coverageReporters: ['html'], + moduleNameMapper:{ + "@remix-project/remix-analyzer": "/../../dist/libs/remix-analyzer/index.js", + "@remix-project/remix-astwalker": "/../../dist/libs/remix-astwalker/index.js", + "@remix-project/remix-debug": "/../../dist/libs/remix-debug/src/index.js", + "@remix-project/remix-lib": "/../../dist/libs/remix-lib/src/index.js", + "@remix-project/remix-simulator": "/../../dist/libs/remix-simulator/src/index.js", + "@remix-project/remix-solidity": "/../../dist/libs/remix-solidity/index.js", + "@remix-project/remix-tests": "/../../dist/libs/remix-tests/src/index.js", + "@remix-project/remix-url-resolver": + "/../../dist/libs/remix-url-resolver/index.js" + , + "@remix-project/remixd": "/../../dist/libs/remixd/index.js" + } }; diff --git a/libs/remix-lib/src/execution/execution-context.ts b/libs/remix-lib/src/execution/execution-context.ts index 31212412ff..ba067e7035 100644 --- a/libs/remix-lib/src/execution/execution-context.ts +++ b/libs/remix-lib/src/execution/execution-context.ts @@ -4,7 +4,6 @@ import Web3 from 'web3' import { EventManager } from '../eventManager' import { rlp, keccak, bufferToHex } from 'ethereumjs-util' import { Web3VmProvider } from '../web3Provider/web3VmProvider' -import { LogsManager } from './logsManager' import VM from '@ethereumjs/vm' import Common from '@ethereumjs/common' import StateManager from '@ethereumjs/vm/dist/state/stateManager' @@ -100,22 +99,21 @@ class StateManagerCommonStorageDump extends StateManager { */ export class ExecutionContext { event - logsManager - blockGasLimitDefault - blockGasLimit + blockGasLimitDefault: number + blockGasLimit: number customNetWorks blocks latestBlockNumber txs - executionContext + executionContext: string listenOnLastBlockId currentFork: string vms mainNetGenesisHash: string + customWeb3: { [key: string]: Web3 } constructor () { this.event = new EventManager() - this.logsManager = new LogsManager() this.executionContext = null this.blockGasLimitDefault = 4300000 this.blockGasLimit = this.blockGasLimitDefault @@ -134,6 +132,7 @@ export class ExecutionContext { this.blocks = {} this.latestBlockNumber = 0 this.txs = {} + this.customWeb3 = {} // mapping between a context name and a web3.js instance } init (config) { @@ -172,7 +171,12 @@ export class ExecutionContext { return this.executionContext === 'vm' } + setWeb3 (context: string, web3: Web3) { + this.customWeb3[context] = web3 + } + web3 () { + if (this.customWeb3[this.executionContext]) return this.customWeb3[this.executionContext] return this.isVM() ? this.vms[this.currentFork].web3vm : web3 } @@ -339,23 +343,5 @@ export class ExecutionContext { if (transactionDetailsLinks[network]) { return transactionDetailsLinks[network] + hash } - } - - addBlock (block) { - let blockNumber = '0x' + block.header.number.toString('hex') - if (blockNumber === '0x') { - blockNumber = '0x0' - } - blockNumber = web3.utils.toHex(web3.utils.toBN(blockNumber)) - - this.blocks['0x' + block.hash().toString('hex')] = block - this.blocks[blockNumber] = block - this.latestBlockNumber = blockNumber - - this.logsManager.checkBlock(blockNumber, block, this.web3()) - } - - trackTx (tx, block) { - this.txs[tx] = block - } + } } diff --git a/libs/remix-lib/src/execution/txExecution.ts b/libs/remix-lib/src/execution/txExecution.ts index 84edfc1f8e..8eb1c927dd 100644 --- a/libs/remix-lib/src/execution/txExecution.ts +++ b/libs/remix-lib/src/execution/txExecution.ts @@ -53,10 +53,10 @@ export function callFunction (from, to, data, value, gasLimit, funAbi, txRunner, /** * check if the vm has errored * - * @param {Object} txResult - the value returned by the vm + * @param {Object} execResult - execution result given by the VM * @return {Object} - { error: true/false, message: DOMNode } */ -export function checkVMError (txResult) { +export function checkVMError (execResult) { const errorCode = { OUT_OF_GAS: 'out of gas', STACK_UNDERFLOW: 'stack underflow', @@ -74,10 +74,10 @@ export function checkVMError (txResult) { error: false, message: '' } - if (!txResult.result.execResult.exceptionError) { + if (!execResult.exceptionError) { return ret } - const exceptionError = txResult.result.execResult.exceptionError.error || '' + const exceptionError = execResult.exceptionError.error || '' const error = `VM error: ${exceptionError}.\n` let msg if (exceptionError === errorCode.INVALID_OPCODE) { @@ -87,7 +87,7 @@ export function checkVMError (txResult) { msg = '\tThe transaction ran out of gas. Please increase the Gas Limit.\n' ret.error = true } else if (exceptionError === errorCode.REVERT) { - const returnData = txResult.result.execResult.returnValue + const returnData = execResult.returnValue // It is the hash of Error(string) if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) { const abiCoder = new ethers.utils.AbiCoder() diff --git a/libs/remix-lib/src/execution/txFormat.ts b/libs/remix-lib/src/execution/txFormat.ts index 7d3188a1d0..62f90be1a8 100644 --- a/libs/remix-lib/src/execution/txFormat.ts +++ b/libs/remix-lib/src/execution/txFormat.ts @@ -314,7 +314,7 @@ export function deployLibrary (libraryName, libraryShortName, library, contracts if (err) { return callback(err) } - const address = txResult.result.createdAddress || txResult.result.contractAddress + const address = txResult.receipt.contractAddress library.address = address callback(err, address) }) diff --git a/libs/remix-lib/src/execution/txListener.ts b/libs/remix-lib/src/execution/txListener.ts index fea5cdf270..967b8ee3a8 100644 --- a/libs/remix-lib/src/execution/txListener.ts +++ b/libs/remix-lib/src/execution/txListener.ts @@ -8,13 +8,13 @@ import { ExecutionContext } from './execution-context' import { decodeResponse } from './txFormat' import { getFunction, getReceiveInterface, getConstructorInterface, visitContracts, makeFullTypeDefinition } from './txHelper' -function addExecutionCosts (txResult, tx) { - if (txResult && txResult.result) { - if (txResult.result.execResult) { - tx.returnValue = txResult.result.execResult.returnValue - if (txResult.result.execResult.gasUsed) tx.executionCost = txResult.result.execResult.gasUsed.toString(10) +function addExecutionCosts (txResult, tx, execResult) { + if (txResult) { + if (execResult) { + tx.returnValue = execResult.returnValue + if (execResult.gasUsed) tx.executionCost = execResult.gasUsed.toString(10) } - if (txResult.result.gasUsed) tx.transactionCost = txResult.result.gasUsed.toString(10) + if (txResult.receipt && txResult.receipt.gasUsed) tx.transactionCost = txResult.receipt.gasUsed.toString(10) } } @@ -55,7 +55,7 @@ export class TxListener { } }) - opt.event.udapp.register('callExecuted', (error, from, to, data, lookupOnly, txResult) => { + opt.event.udapp.register('callExecuted', async (error, from, to, data, lookupOnly, txResult) => { if (error) return // we go for that case if // in VM mode @@ -63,17 +63,25 @@ export class TxListener { if (!this._isListening) return // we don't listen if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network + let returnValue + let execResult + if (this.executionContext.isVM()) { + execResult = await this.executionContext.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash) + returnValue = execResult.returnValue + } else { + returnValue = toBuffer(txResult.result) + } const call = { from: from, to: to, input: data, hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data, isCall: true, - returnValue: this.executionContext.isVM() ? txResult.result.execResult.returnValue : toBuffer(txResult.result), + returnValue, envMode: this.executionContext.getProvider() } - addExecutionCosts(txResult, call) + addExecutionCosts(txResult, call, execResult) this._resolveTx(call, call, (error, resolvedData) => { if (!error) { this.event.trigger('newCall', [call]) @@ -89,12 +97,17 @@ export class TxListener { // in web3 mode && listen remix txs only if (!this._isListening) return // we don't listen if (this._loopId && this.executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network - this.executionContext.web3().eth.getTransaction(txResult.transactionHash, (error, tx) => { + this.executionContext.web3().eth.getTransaction(txResult.transactionHash, async (error, tx) => { if (error) return console.log(error) - addExecutionCosts(txResult, tx) + let execResult + if (this.executionContext.isVM()) { + execResult = await this.executionContext.web3().eth.getExecutionResultFromSimulator(txResult.transactionHash) + } + + addExecutionCosts(txResult, tx, execResult) tx.envMode = this.executionContext.getProvider() - tx.status = txResult.result.status // 0x0 or 0x1 + tx.status = txResult.receipt.status // 0x0 or 0x1 this._resolve([tx], () => { }) }) diff --git a/libs/remix-lib/src/execution/txRunner.ts b/libs/remix-lib/src/execution/txRunner.ts index ddc2456fb5..254b4b0474 100644 --- a/libs/remix-lib/src/execution/txRunner.ts +++ b/libs/remix-lib/src/execution/txRunner.ts @@ -1,91 +1,26 @@ 'use strict' -import { Transaction } from '@ethereumjs/tx' -import { Block } from '@ethereumjs/block' -import { BN, bufferToHex, Address } from 'ethereumjs-util' -import { ExecutionContext } from './execution-context' import { EventManager } from '../eventManager' export class TxRunner { event - executionContext - _api - blockNumber runAsync pendingTxs - vmaccounts queusTxs - blocks - commonContext - - constructor (vmaccounts, api, executionContext) { + opt + internalRunner + constructor (internalRunner, opt) { + this.opt = opt || {} + this.internalRunner = internalRunner this.event = new EventManager() - // has a default for now for backwards compatability - this.executionContext = executionContext || new ExecutionContext() - this.commonContext = this.executionContext.vmObject().common - this._api = api - this.blockNumber = 0 - this.runAsync = true - if (this.executionContext.isVM()) { - // this.blockNumber = 1150000 // The VM is running in Homestead mode, which started at this block. - this.blockNumber = 0 // The VM is running in Homestead mode, which started at this block. - this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. - } + + this.runAsync = this.opt.runAsync || true // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. + this.pendingTxs = {} - this.vmaccounts = vmaccounts this.queusTxs = [] - this.blocks = [] } rawRun (args, confirmationCb, gasEstimationForceSend, promptCb, cb) { - let timestamp = Date.now() - if (args.timestamp) { - timestamp = args.timestamp - } - run(this, args, timestamp, confirmationCb, gasEstimationForceSend, promptCb, cb) - } - - _executeTx (tx, gasPrice, api, promptCb, callback) { - if (gasPrice) tx.gasPrice = this.executionContext.web3().utils.toHex(gasPrice) - if (api.personalMode()) { - promptCb( - (value) => { - this._sendTransaction(this.executionContext.web3().personal.sendTransaction, tx, value, callback) - }, - () => { - return callback('Canceled by user.') - } - ) - } else { - this._sendTransaction(this.executionContext.web3().eth.sendTransaction, tx, null, callback) - } - } - - _sendTransaction (sendTx, tx, pass, callback) { - const cb = (err, resp) => { - if (err) { - return callback(err, resp) - } - this.event.trigger('transactionBroadcasted', [resp]) - var listenOnResponse = () => { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - const result = await tryTillReceiptAvailable(resp, this.executionContext) - tx = await tryTillTxAvailable(resp, this.executionContext) - resolve({ - result, - tx, - transactionHash: result ? result['transactionHash'] : null - }) - }) - } - listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) }) - } - const args = pass !== null ? [tx, pass, cb] : [tx, cb] - try { - sendTx.apply({}, args) - } catch (e) { - return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) - } + run(this, args, args.timestamp || Date.now(), confirmationCb, gasEstimationForceSend, promptCb, cb) } execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { @@ -93,175 +28,10 @@ export class TxRunner { if (data.slice(0, 2) !== '0x') { data = '0x' + data } - - if (!this.executionContext.isVM()) { - return this.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, confirmationCb, gasEstimationForceSend, promptCb, callback) - } - try { - this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback) - } catch (e) { - callback(e, null) - } + this.internalRunner.execute(args, confirmationCb, gasEstimationForceSend, promptCb, callback) } - - runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) { - const self = this - const account = self.vmaccounts[from] - if (!account) { - return callback('Invalid account selected') - } - - if (Number.isInteger(gasLimit)) { - gasLimit = '0x' + gasLimit.toString(16) - } - - this.executionContext.vm().stateManager.getAccount(Address.fromString(from)).then((res) => { - // See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor - // for initialization fields and their types - value = value ? parseInt(value) : 0 - const tx = Transaction.fromTxData({ - nonce: new BN(res.nonce), - gasPrice: '0x1', - gasLimit: gasLimit, - to: to, - value: value, - data: Buffer.from(data.slice(2), 'hex') - }, { common: this.commonContext }).sign(account.privateKey) - - const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e'] - const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)] - - var block = Block.fromBlockData({ - header: { - timestamp: timestamp || (new Date().getTime() / 1000 | 0), - number: self.blockNumber, - coinbase: coinbases[self.blockNumber % coinbases.length], - difficulty: difficulties[self.blockNumber % difficulties.length], - gasLimit: new BN(gasLimit.replace('0x', ''), 16).imuln(2) - }, - transactions: [tx] - }, { common: this.commonContext }) - - if (!useCall) { - ++self.blockNumber - this.runBlockInVm(tx, block, callback) - } else { - this.executionContext.vm().stateManager.checkpoint().then(() => { - this.runBlockInVm(tx, block, (err, result) => { - this.executionContext.vm().stateManager.revert().then(() => { - callback(err, result) - }) - }) - }) - } - }).catch((e) => { - callback(e) - }) - } - - runBlockInVm (tx, block, callback) { - this.executionContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then((results) => { - const result = results.results[0] - if (result) { - const status = result.execResult.exceptionError ? 0 : 1 - result.status = `0x${status}` - } - this.executionContext.addBlock(block) - this.executionContext.trackTx('0x' + tx.hash().toString('hex'), block) - callback(null, { - result: result, - transactionHash: bufferToHex(Buffer.from(tx.hash())) - }) - }).catch((err) => { - callback(err) - }) - } - - runInNode (from, to, data, value, gasLimit, useCall, confirmCb, gasEstimationForceSend, promptCb, callback) { - const tx = { from: from, to: to, data: data, value: value } - - if (useCall) { - tx['gas'] = gasLimit - return this.executionContext.web3().eth.call(tx, function (error, result) { - callback(error, { - result: result, - transactionHash: result ? result.transactionHash : null - }) - }) - } - this.executionContext.web3().eth.estimateGas(tx, (err, gasEstimation) => { - if (err && err.message.indexOf('Invalid JSON RPC response') !== -1) { - // // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed - err = 'Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.' - } - gasEstimationForceSend(err, () => { - // callback is called whenever no error - tx['gas'] = !gasEstimation ? gasLimit : gasEstimation - - if (this._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) { - return this._executeTx(tx, null, this._api, promptCb, callback) - } - - this._api.detectNetwork((err, network) => { - if (err) { - console.log(err) - return - } - - confirmCb(network, tx, tx['gas'], (gasPrice) => { - return this._executeTx(tx, gasPrice, this._api, promptCb, callback) - }, (error) => { - callback(error) - }) - }) - }, () => { - const blockGasLimit = this.executionContext.currentblockGasLimit() - // NOTE: estimateGas very likely will return a large limit if execution of the code failed - // we want to be able to run the code in order to debug and find the cause for the failure - if (err) return callback(err) - - let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' - warnEstimation += ' ' + err - - if (gasEstimation > gasLimit) { - return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) - } - if (gasEstimation > blockGasLimit) { - return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) - } - }) - }) - } -} - -async function tryTillReceiptAvailable (txhash, executionContext) { - return new Promise((resolve, reject) => { - executionContext.web3().eth.getTransactionReceipt(txhash, async (err, receipt) => { - if (err || !receipt) { - // Try again with a bit of delay if error or if result still null - await pause() - return resolve(await tryTillReceiptAvailable(txhash, executionContext)) - } - return resolve(receipt) - }) - }) } -async function tryTillTxAvailable (txhash, executionContext) { - return new Promise((resolve, reject) => { - executionContext.web3().eth.getTransaction(txhash, async (err, tx) => { - if (err || !tx) { - // Try again with a bit of delay if error or if result still null - await pause() - return resolve(await tryTillTxAvailable(txhash, executionContext)) - } - return resolve(tx) - }) - }) -} - -async function pause () { return new Promise((resolve, reject) => { setTimeout(resolve, 500) }) } - function run (self, tx, stamp, confirmationCb, gasEstimationForceSend = null, promptCb = null, callback = null) { if (!self.runAsync && Object.keys(self.pendingTxs).length) { return self.queusTxs.push({ tx, stamp, callback }) @@ -275,4 +45,4 @@ function run (self, tx, stamp, confirmationCb, gasEstimationForceSend = null, pr run(self, next.tx, next.stamp, next.callback) } }) -} +} \ No newline at end of file diff --git a/libs/remix-lib/src/execution/txRunnerVM.ts b/libs/remix-lib/src/execution/txRunnerVM.ts new file mode 100644 index 0000000000..3b221f8b75 --- /dev/null +++ b/libs/remix-lib/src/execution/txRunnerVM.ts @@ -0,0 +1,120 @@ +'use strict' +import { Transaction } from 'ethereumjs-tx' +import Block from 'ethereumjs-block' +import { BN, bufferToHex } from 'ethereumjs-util' +import { EventManager } from '../eventManager' +import { LogsManager } from './logsManager' + +export class TxRunnerVM { + event + _api + blockNumber + runAsync + pendingTxs + vmaccounts + queusTxs + blocks + txs + logsManager + getVM: () => any + + constructor (vmaccounts, api, getVM) { + this.event = new EventManager() + this.logsManager = new LogsManager() + // has a default for now for backwards compatability + this.getVM = getVM + this._api = api + this.blockNumber = 0 + this.runAsync = true + this.blockNumber = 0 // The VM is running in Homestead mode, which started at this block. + this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. + this.pendingTxs = {} + this.vmaccounts = vmaccounts + this.queusTxs = [] + this.blocks = [] + } + + execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { + let data = args.data + if (data.slice(0, 2) !== '0x') { + data = '0x' + data + } + + try { + this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback) + } catch (e) { + callback(e, null) + } + } + + runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) { + const self = this + const account = self.vmaccounts[from] + if (!account) { + return callback('Invalid account selected') + } + + this.getVM().stateManager.getAccount(Buffer.from(from.replace('0x', ''), 'hex'), (err, res) => { + if (err) { + callback('Account not found') + } else { + // See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor + // for initialization fields and their types + value = value ? parseInt(value) : 0 + const tx = new Transaction({ + nonce: new BN(res.nonce), + gasPrice: '0x1', + gasLimit: gasLimit, + to: to, + value: value, + data: Buffer.from(data.slice(2), 'hex') + }) + tx.sign(account.privateKey) + const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e'] + const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)] + const block = new Block({ + header: { + timestamp: timestamp || (new Date().getTime() / 1000 | 0), + number: self.blockNumber, + coinbase: coinbases[self.blockNumber % coinbases.length], + difficulty: difficulties[self.blockNumber % difficulties.length], + gasLimit: new BN(gasLimit, 10).imuln(2) + }, + transactions: [tx], + uncleHeaders: [] + }) + if (!useCall) { + ++self.blockNumber + this.runBlockInVm(tx, block, callback) + } else { + this.getVM().stateManager.checkpoint(() => { + this.runBlockInVm(tx, block, (err, result) => { + this.getVM().stateManager.revert(() => { + callback(err, result) + }) + }) + }) + } + } + }) + } + + runBlockInVm (tx, block, callback) { + this.getVM().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then((results) => { + const result = results.results[0] + if (result) { + const status = result.execResult.exceptionError ? 0 : 1 + result.status = `0x${status}` + } + callback(null, { + result: result, + transactionHash: bufferToHex(Buffer.from(tx.hash())), + block, + tx, + }) + }).catch(function (err) { + callback(err) + }) + } +} + diff --git a/libs/remix-lib/src/execution/txRunnerWeb3.ts b/libs/remix-lib/src/execution/txRunnerWeb3.ts new file mode 100644 index 0000000000..f735b3401c --- /dev/null +++ b/libs/remix-lib/src/execution/txRunnerWeb3.ts @@ -0,0 +1,156 @@ +'use strict' +import { EventManager } from '../eventManager' +import Web3 from 'web3' + +export class TxRunnerWeb3 { + event + _api + getWeb3: () => Web3 + currentblockGasLimit: () => number + + constructor (api, getWeb3, currentblockGasLimit) { + this.event = new EventManager() + this.getWeb3 = getWeb3 + this.currentblockGasLimit = currentblockGasLimit + this._api = api + } + + _executeTx (tx, gasPrice, api, promptCb, callback) { + if (gasPrice) tx.gasPrice = this.getWeb3().utils.toHex(gasPrice) + if (api.personalMode()) { + promptCb( + (value) => { + this._sendTransaction((this.getWeb3() as any).personal.sendTransaction, tx, value, callback) + }, + () => { + return callback('Canceled by user.') + } + ) + } else { + this._sendTransaction(this.getWeb3().eth.sendTransaction, tx, null, callback) + } + } + + _sendTransaction (sendTx, tx, pass, callback) { + const cb = (err, resp) => { + if (err) { + return callback(err, resp) + } + this.event.trigger('transactionBroadcasted', [resp]) + var listenOnResponse = () => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + const receipt = await tryTillReceiptAvailable(resp, this.getWeb3()) + tx = await tryTillTxAvailable(resp, this.getWeb3()) + resolve({ + receipt, + tx, + transactionHash: receipt ? receipt['transactionHash'] : null + }) + }) + } + listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) }) + } + const args = pass !== null ? [tx, pass, cb] : [tx, cb] + try { + sendTx.apply({}, args) + } catch (e) { + return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) + } + } + + execute (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { + let data = args.data + if (data.slice(0, 2) !== '0x') { + data = '0x' + data + } + + return this.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, confirmationCb, gasEstimationForceSend, promptCb, callback) + } + + runInNode (from, to, data, value, gasLimit, useCall, timestamp, confirmCb, gasEstimationForceSend, promptCb, callback) { + const tx = { from: from, to: to, data: data, value: value } + + if (useCall) { + const tag = Date.now() // for e2e reference + tx['gas'] = gasLimit + tx['timestamp'] = timestamp + return this.getWeb3().eth.call(tx, function (error, result: any) { + if (error) return callback(error) + callback(null, { + result: result + }) + }) + } + this.getWeb3().eth.estimateGas(tx, (err, gasEstimation) => { + if (err && err.message.indexOf('Invalid JSON RPC response') !== -1) { + // // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed + new Error('Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.') + } + gasEstimationForceSend(err, () => { + // callback is called whenever no error + tx['gas'] = !gasEstimation ? gasLimit : gasEstimation + + if (this._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) { + return this._executeTx(tx, null, this._api, promptCb, callback) + } + + this._api.detectNetwork((err, network) => { + if (err) { + console.log(err) + return + } + + confirmCb(network, tx, tx['gas'], (gasPrice) => { + return this._executeTx(tx, gasPrice, this._api, promptCb, callback) + }, (error) => { + callback(error) + }) + }) + }, () => { + const blockGasLimit = this.currentblockGasLimit() + // NOTE: estimateGas very likely will return a large limit if execution of the code failed + // we want to be able to run the code in order to debug and find the cause for the failure + if (err) return callback(err) + + let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' + warnEstimation += ' ' + err + + if (gasEstimation > gasLimit) { + return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) + } + if (gasEstimation > blockGasLimit) { + return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) + } + }) + }) + } +} + +async function tryTillReceiptAvailable (txhash, web3) { + return new Promise((resolve, reject) => { + web3.eth.getTransactionReceipt(txhash, async (err, receipt) => { + if (err || !receipt) { + // Try again with a bit of delay if error or if result still null + await pause() + return resolve(await tryTillReceiptAvailable(txhash, web3)) + } + return resolve(receipt) + }) + }) +} + +async function tryTillTxAvailable (txhash, web3) { + return new Promise((resolve, reject) => { + web3.eth.getTransaction(txhash, async (err, tx) => { + if (err || !tx) { + // Try again with a bit of delay if error or if result still null + await pause() + return resolve(await tryTillTxAvailable(txhash, web3)) + } + return resolve(tx) + }) + }) +} + +async function pause () { return new Promise((resolve, reject) => { setTimeout(resolve, 500) }) } diff --git a/libs/remix-lib/src/helpers/txResultHelper.ts b/libs/remix-lib/src/helpers/txResultHelper.ts index 7aa7967f0d..3264e5b771 100644 --- a/libs/remix-lib/src/helpers/txResultHelper.ts +++ b/libs/remix-lib/src/helpers/txResultHelper.ts @@ -20,9 +20,9 @@ function convertToPrefixedHex (input) { Also, VM results use BN and Buffers, Node results use hex strings/ints, So we need to normalize the values to prefixed hex strings */ -export function resultToRemixTx (txResult) { - const { result, transactionHash } = txResult - const { status, execResult, gasUsed, createdAddress, contractAddress } = result +export function resultToRemixTx (txResult, execResult) { + const { receipt, transactionHash, result } = txResult + const { status, gasUsed, contractAddress } = receipt let returnValue, errorMessage if (isHexString(result)) { @@ -38,6 +38,6 @@ export function resultToRemixTx (txResult) { gasUsed: convertToPrefixedHex(gasUsed), error: errorMessage, return: convertToPrefixedHex(returnValue), - createdAddress: convertToPrefixedHex(createdAddress || contractAddress) + createdAddress: convertToPrefixedHex(contractAddress) } } diff --git a/libs/remix-lib/src/index.ts b/libs/remix-lib/src/index.ts index 5b1108ff70..afcd0beae6 100644 --- a/libs/remix-lib/src/index.ts +++ b/libs/remix-lib/src/index.ts @@ -12,9 +12,12 @@ import * as txHelper from './execution/txHelper' import * as txFormat from './execution/txFormat' import { TxListener } from './execution/txListener' import { TxRunner } from './execution/txRunner' +import { LogsManager } from './execution/logsManager' import { ExecutionContext } from './execution/execution-context' import * as typeConversion from './execution/typeConversion' -import { UniversalDApp } from './universalDapp' +import { TxRunnerVM } from './execution/txRunnerVM' +import { TxRunnerWeb3 } from './execution/txRunnerWeb3' +import * as txResultHelper from './helpers/txResultHelper' export = modules() @@ -23,7 +26,8 @@ function modules () { EventManager: EventManager, helpers: { ui: uiHelper, - compiler: compilerHelper + compiler: compilerHelper, + txResultHelper }, vm: { Web3Providers: Web3Providers, @@ -39,9 +43,11 @@ function modules () { executionContext: new ExecutionContext(), txFormat: txFormat, txListener: TxListener, - txRunner: TxRunner, - typeConversion: typeConversion - }, - UniversalDApp: UniversalDApp + TxRunner: TxRunner, + TxRunnerWeb3: TxRunnerWeb3, + TxRunnerVM: TxRunnerVM, + typeConversion: typeConversion, + LogsManager + } } } diff --git a/libs/remix-lib/src/universalDapp.ts b/libs/remix-lib/src/universalDapp.ts deleted file mode 100644 index 030a8da811..0000000000 --- a/libs/remix-lib/src/universalDapp.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { waterfall } from 'async' -import { BN, privateToAddress, isValidPrivate, toChecksumAddress, Address } from 'ethereumjs-util' -import { randomBytes } from 'crypto' -import { EventEmitter } from 'events' -import { TxRunner } from './execution/txRunner' -import { sortAbiFunction, getFallbackInterface, getReceiveInterface, inputParametersDeclarationToString } from './execution/txHelper' -import { EventManager } from './eventManager' -import { ExecutionContext } from './execution/execution-context' -import { resultToRemixTx } from './helpers/txResultHelper' - -export class UniversalDApp { - events - event - executionContext - config - txRunner - accounts - transactionContextAPI - - constructor (config, executionContext) { - this.events = new EventEmitter() - this.event = new EventManager() - // has a default for now for backwards compatability - this.executionContext = executionContext || new ExecutionContext() - this.config = config - - this.txRunner = new TxRunner({}, { - config: config, - detectNetwork: (cb) => { - this.executionContext.detectNetwork(cb) - }, - personalMode: () => { - return this.executionContext.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false - } - }, this.executionContext) - this.accounts = {} - this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this)) - } - - // TODO : event should be triggered by Udapp instead of TxListener - /** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */ - startListening (txlistener) { - txlistener.event.register('newTransaction', (tx) => { - this.events.emit('newTransaction', tx) - }) - } - - resetEnvironment () { - this.accounts = {} - if (this.executionContext.isVM()) { - this._addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511', '0x56BC75E2D63100000') - this._addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c', '0x56BC75E2D63100000') - this._addAccount('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', '0x56BC75E2D63100000') - this._addAccount('d74aa6d18aa79a05f3473dd030a97d3305737cbc8337d940344345c1f6b72eea', '0x56BC75E2D63100000') - this._addAccount('71975fbf7fe448e004ac7ae54cad0a383c3906055a65468714156a07385e96ce', '0x56BC75E2D63100000') - } - // TODO: most params here can be refactored away in txRunner - this.txRunner = new TxRunner(this.accounts, { - // TODO: only used to check value of doNotShowTransactionConfirmationAgain property - config: this.config, - // TODO: to refactor, TxRunner already has access to executionContext - detectNetwork: (cb) => { - this.executionContext.detectNetwork(cb) - }, - personalMode: () => { - return this.executionContext.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false - } - }, this.executionContext) - this.txRunner.event.register('transactionBroadcasted', (txhash) => { - this.executionContext.detectNetwork((error, network) => { - if (error || !network) return - this.event.trigger('transactionBroadcasted', [txhash, network.name]) - }) - }) - } - - resetAPI (transactionContextAPI) { - this.transactionContextAPI = transactionContextAPI - } - - /** - * Create a VM Account - * @param {{privateKey: string, balance: string}} newAccount The new account to create - */ - createVMAccount (newAccount) { - const { privateKey, balance } = newAccount - if (this.executionContext.getProvider() !== 'vm') { - throw new Error('plugin API does not allow creating a new account through web3 connection. Only vm mode is allowed') - } - this._addAccount(privateKey, balance) - const privKey = Buffer.from(privateKey, 'hex') - return '0x' + privateToAddress(privKey).toString('hex') - } - - newAccount (password, passwordPromptCb, cb) { - if (!this.executionContext.isVM()) { - if (!this.config.get('settings/personal-mode')) { - return cb('Not running in personal mode') - } - return passwordPromptCb((passphrase) => { - this.executionContext.web3().personal.newAccount(passphrase, cb) - }) - } - let privateKey - do { - privateKey = randomBytes(32) - } while (!isValidPrivate(privateKey)) - this._addAccount(privateKey, '0x56BC75E2D63100000') - cb(null, '0x' + privateToAddress(privateKey).toString('hex')) - } - - /** Add an account to the list of account (only for Javascript VM) */ - _addAccount (privateKey, balance) { - if (!this.executionContext.isVM()) { - throw new Error('_addAccount() cannot be called in non-VM mode') - } - - if (!this.accounts) { - return - } - privateKey = Buffer.from(privateKey, 'hex') - const address = privateToAddress(privateKey) - - // FIXME: we don't care about the callback, but we should still make this proper - const stateManager = this.executionContext.vm().stateManager - stateManager.getAccount(address).then((account) => { - account.balance = new BN(balance.replace('0x', '') || 'f00000000000000001', 16) - stateManager.putAccount(address, account).catch((error) => { - console.log(error) - }) - }).catch((error) => { - console.log(error) - }) - - this.accounts[toChecksumAddress('0x' + address.toString('hex'))] = { privateKey, nonce: 0 } - } - - /** Return the list of accounts */ - getAccounts (cb) { - return new Promise((resolve, reject) => { - const provider = this.executionContext.getProvider() - switch (provider) { - case 'vm': - if (!this.accounts) { - if (cb) cb('No accounts?') - reject(new Error('No accounts?')) - return - } - if (cb) cb(null, Object.keys(this.accounts)) - resolve(Object.keys(this.accounts)) - break - case 'web3': - if (this.config.get('settings/personal-mode')) { - return this.executionContext.web3().personal.getListAccounts((error, accounts) => { - if (cb) cb(error, accounts) - if (error) return reject(error) - resolve(accounts) - }) - } else { - this.executionContext.web3().eth.getAccounts((error, accounts) => { - if (cb) cb(error, accounts) - if (error) return reject(error) - resolve(accounts) - }) - } - break - case 'injected': { - this.executionContext.web3().eth.getAccounts((error, accounts) => { - if (cb) cb(error, accounts) - if (error) return reject(error) - resolve(accounts) - }) - } - } - }) - } - - /** Get the balance of an address */ - getBalance (address, cb) { - if (!this.executionContext.isVM()) { - return this.executionContext.web3().eth.getBalance(address, (err, res) => { - if (err) { - return cb(err) - } - cb(null, res.toString(10)) - }) - } - if (!this.accounts) { - return cb('No accounts?') - } - - this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((res) => { - cb(null, new BN(res.balance).toString(10)) - }).catch(() => { - cb('Account not found') - }) - } - - /** Get the balance of an address, and convert wei to ether */ - getBalanceInEther (address, callback) { - this.getBalance(address, (error, balance) => { - if (error) { - return callback(error) - } - callback(null, this.executionContext.web3().utils.fromWei(balance, 'ether')) - }) - } - - pendingTransactionsCount () { - return Object.keys(this.txRunner.pendingTxs).length - } - - /** - * deploy the given contract - * - * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). - * @param {Function} callback - callback. - */ - createContract (data, confirmationCb, continueCb, promptCb, callback) { - this.runTx({ data: data, useCall: false }, confirmationCb, continueCb, promptCb, callback) - } - - /** - * call the current given contract - * - * @param {String} to - address of the contract to call. - * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). - * @param {Object} funAbi - abi definition of the function to call. - * @param {Function} callback - callback. - */ - callFunction (to, data, funAbi, confirmationCb, continueCb, promptCb, callback) { - const useCall = funAbi.stateMutability === 'view' || funAbi.stateMutability === 'pure' - this.runTx({ to, data, useCall }, confirmationCb, continueCb, promptCb, callback) - } - - /** - * call the current given contract - * - * @param {String} to - address of the contract to call. - * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). - * @param {Function} callback - callback. - */ - sendRawTransaction (to, data, confirmationCb, continueCb, promptCb, callback) { - this.runTx({ to, data, useCall: false }, confirmationCb, continueCb, promptCb, callback) - } - - context () { - return (this.executionContext.isVM() ? 'memory' : 'blockchain') - } - - getABI (contract) { - return sortAbiFunction(contract.abi) - } - - getFallbackInterface (contractABI) { - return getFallbackInterface(contractABI) - } - - getReceiveInterface (contractABI) { - return getReceiveInterface(contractABI) - } - - getInputs (funABI) { - if (!funABI.inputs) { - return '' - } - return inputParametersDeclarationToString(funABI.inputs) - } - - /** - * This function send a tx only to javascript VM or testnet, will return an error for the mainnet - * SHOULD BE TAKEN CAREFULLY! - * - * @param {Object} tx - transaction. - */ - sendTransaction (tx) { - return new Promise((resolve, reject) => { - this.executionContext.detectNetwork((error, network) => { - if (error) return reject(error) - if (network.name === 'Main' && network.id === '1') { - return reject(new Error('It is not allowed to make this action against mainnet')) - } - this.silentRunTx(tx, (error, result) => { - if (error) return reject(error) - try { - resolve(resultToRemixTx(result)) - } catch (e) { - reject(e) - } - }) - }) - }) - } - - /** - * This function send a tx without alerting the user (if mainnet or if gas estimation too high). - * SHOULD BE TAKEN CAREFULLY! - * - * @param {Object} tx - transaction. - * @param {Function} callback - callback. - */ - silentRunTx (tx, cb) { - this.txRunner.rawRun( - tx, - (network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() }, - (error, continueTxExecution, cancelCb) => { if (error) { cb(error) } else { continueTxExecution() } }, - (okCb, cancelCb) => { okCb() }, - cb - ) - } - - runTx (args, confirmationCb, continueCb, promptCb, cb) { - const self = this - waterfall([ - function getGasLimit (next) { - if (self.transactionContextAPI.getGasLimit) { - return self.transactionContextAPI.getGasLimit(next) - } - next(null, 3000000) - }, - function queryValue (gasLimit, next) { - if (args.value) { - return next(null, args.value, gasLimit) - } - if (args.useCall || !self.transactionContextAPI.getValue) { - return next(null, 0, gasLimit) - } - self.transactionContextAPI.getValue(function (err, value) { - next(err, value, gasLimit) - }) - }, - function getAccount (value, gasLimit, next) { - if (args.from) { - return next(null, args.from, value, gasLimit) - } - if (self.transactionContextAPI.getAddress) { - return self.transactionContextAPI.getAddress(function (err, address) { - next(err, address, value, gasLimit) - }) - } - self.getAccounts(function (err, accounts) { - const address = accounts[0] - - if (err) return next(err) - if (!address) return next('No accounts available') - if (self.executionContext.isVM() && !self.accounts[address]) { - return next('Invalid account selected') - } - next(null, address, value, gasLimit) - }) - }, - function runTransaction (fromAddress, value, gasLimit, next) { - const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp } - const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences } - let timestamp = Date.now() - if (tx.timestamp) { - timestamp = tx.timestamp - } - - self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad]) - self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb, - function (error, result) { - const eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted') - self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad]) - - if (error && (typeof (error) !== 'string')) { - if (error.message) error = error.message - else { - // eslint-disable-next-line no-empty - try { error = 'error: ' + JSON.stringify(error) } catch (e) {} - } - } - next(error, result) - } - ) - } - ], cb) - } -} diff --git a/libs/remix-lib/src/web3Provider/web3VmProvider.ts b/libs/remix-lib/src/web3Provider/web3VmProvider.ts index 367370f928..361c2b7161 100644 --- a/libs/remix-lib/src/web3Provider/web3VmProvider.ts +++ b/libs/remix-lib/src/web3Provider/web3VmProvider.ts @@ -31,6 +31,9 @@ export class Web3VmProvider { toBigNumber isAddress utils + txsMapBlock + blocks + latestBlockNumber constructor () { this.web3 = new Web3() @@ -69,6 +72,9 @@ export class Web3VmProvider { this.toBigNumber = (...args) => this.web3.utils.toBN(...args) this.isAddress = (...args) => this.web3.utils.isAddress(...args) this.utils = Web3.utils || [] + this.txsMapBlock = {} + this.blocks = {} + this.latestBlockNumber } setVM (vm) { diff --git a/libs/remix-lib/test/txFormat.ts b/libs/remix-lib/test/txFormat.ts index d33760f9f9..2ab4a59b63 100644 --- a/libs/remix-lib/test/txFormat.ts +++ b/libs/remix-lib/test/txFormat.ts @@ -161,8 +161,8 @@ tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t) } const callbackDeployLibraries = (param, callback) => { callback(null, { - result: { - createdAddress: fakeDeployedContracts[param.data.contractName] + receipt: { + contractAddress: fakeDeployedContracts[param.data.contractName] } }) } // fake diff --git a/libs/remix-lib/test/txResultHelper.ts b/libs/remix-lib/test/txResultHelper.ts index 8fa2e5cdf2..67a136dc84 100644 --- a/libs/remix-lib/test/txResultHelper.ts +++ b/libs/remix-lib/test/txResultHelper.ts @@ -18,12 +18,13 @@ const GAS_USED_INT = 75427 const GAS_USED_HEX = '0x126a3' const NODE_CALL_RESULT = { + receipt: {}, result: RETURN_VALUE_HEX, transactionHash: undefined } const NODE_TX_RESULT = { - result: { + receipt: { blockHash: '0x380485a4e6372a42e36489783c7f7cb66257612133cd245859c206fd476e9c44', blockNumber: 5994, contractAddress: CONTRACT_ADDRESS_HEX, @@ -39,26 +40,31 @@ const NODE_TX_RESULT = { } const VM_RESULT = { - result: { + receipt: { amountSpent: new BN(1), - createdAddress: CONTRACT_ADDRESS_BUFFER, + contractAddress: CONTRACT_ADDRESS_BUFFER, gasRefund: new BN(0), gasUsed: new BN(GAS_USED_INT), status: STATUS_OK, - execResult: { - exceptionError: null, - gasRefund: new BN(0), - gasUsed: new BN(GAS_USED_INT), - returnValue: RETURN_VALUE_BUFFER - } }, transactionHash: TRANSACTION_HASH } +const EXEC_RESULT = { + exceptionError: null, + gasRefund: new BN(0), + gasUsed: new BN(GAS_USED_INT), + returnValue: RETURN_VALUE_BUFFER +} + +const EXEC_RESULT_ERROR = { + exceptionError: 'this is an error' +} + tape('converts node transaction result to RemixTx', function (t) { // contract creation let txResult = { ...NODE_TX_RESULT } - let remixTx = resultToRemixTx(txResult) + let remixTx = resultToRemixTx(txResult, {}) t.equal(remixTx.transactionHash, TRANSACTION_HASH) t.equal(remixTx.createdAddress, CONTRACT_ADDRESS_HEX) @@ -68,8 +74,8 @@ tape('converts node transaction result to RemixTx', function (t) { t.equal(remixTx.error, undefined) // contract method tx - txResult.result.contractAddress = null - remixTx = resultToRemixTx(txResult) + txResult.receipt.contractAddress = null + remixTx = resultToRemixTx(txResult, {}) t.equal(remixTx.createdAddress, null) t.end() @@ -77,7 +83,7 @@ tape('converts node transaction result to RemixTx', function (t) { tape('converts node call result to RemixTx', function (t) { let txResult = { ...NODE_CALL_RESULT } - let remixTx = resultToRemixTx(txResult) + let remixTx = resultToRemixTx(txResult, {}) t.equal(remixTx.transactionHash, undefined) t.equal(remixTx.createdAddress, undefined) @@ -91,7 +97,7 @@ tape('converts node call result to RemixTx', function (t) { tape('converts VM result to RemixTx', function (t) { let txResult = { ...VM_RESULT } - let remixTx = resultToRemixTx(txResult) + let remixTx = resultToRemixTx(txResult, EXEC_RESULT) t.equal(remixTx.transactionHash, TRANSACTION_HASH) @@ -101,8 +107,7 @@ tape('converts VM result to RemixTx', function (t) { t.equal(remixTx.return, RETURN_VALUE_HEX) t.equal(remixTx.error, null) - txResult.result.execResult.exceptionError = 'this is an error' - remixTx = resultToRemixTx(txResult) + remixTx = resultToRemixTx(VM_RESULT, EXEC_RESULT_ERROR) t.equal(remixTx.error, 'this is an error') t.end() diff --git a/libs/remix-simulator/src/genesis.ts b/libs/remix-simulator/src/genesis.ts index f3d7d0509e..839a0c4cb6 100644 --- a/libs/remix-simulator/src/genesis.ts +++ b/libs/remix-simulator/src/genesis.ts @@ -1,7 +1,7 @@ import { Block } from '@ethereumjs/block' import { BN } from 'ethereumjs-util' -export function generateBlock (executionContext) { +export function generateBlock (vmContext) { return new Promise((resolve, reject) => { const block: Block = Block.fromBlockData({ header: { @@ -11,10 +11,10 @@ export function generateBlock (executionContext) { difficulty: new BN('69762765929000', 10), gasLimit: new BN('8000000').imuln(1) } - }, { common: executionContext.vmObject().common }) + }, { common: vmContext.vmObject().common }) - executionContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then(() => { - executionContext.addBlock(block) + vmContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then(() => { + vmContext.addBlock(block) resolve({}) }).catch((e) => reject(e)) }) diff --git a/libs/remix-simulator/src/index.ts b/libs/remix-simulator/src/index.ts index fd73199808..19ca3ab802 100644 --- a/libs/remix-simulator/src/index.ts +++ b/libs/remix-simulator/src/index.ts @@ -1 +1 @@ -export { Provider } from './provider' +export { Provider, extend } from './provider' diff --git a/libs/remix-simulator/src/methods/accounts.ts b/libs/remix-simulator/src/methods/accounts.ts index 6fd359594b..8d18498afc 100644 --- a/libs/remix-simulator/src/methods/accounts.ts +++ b/libs/remix-simulator/src/methods/accounts.ts @@ -6,22 +6,20 @@ export class Accounts { web3 accounts: Record accountsKeys: Record - executionContext + vmContext - constructor (executionContext) { + constructor (vmContext) { this.web3 = new Web3() - this.executionContext = executionContext + this.vmContext = vmContext // TODO: make it random and/or use remix-libs this.accounts = {} this.accountsKeys = {} - this.executionContext.init({ get: () => { return true } }) } async resetAccounts (): Promise { - // TODO: setting this to {} breaks the app currently, unclear why still - // this.accounts = {} - // this.accountsKeys = {} + this.accounts = {} + this.accountsKeys = {} await this._addAccount('503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb', '0x56BC75E2D63100000') await this._addAccount('7e5bfb82febc4c2c8529167104271ceec190eafdca277314912eaabdb67c6e5f', '0x56BC75E2D63100000') await this._addAccount('cc6d63f85de8fef05446ebdd3c537c72152d0fc437fd7aa62b3019b79bd1fdd4', '0x56BC75E2D63100000') @@ -47,7 +45,7 @@ export class Accounts { this.accounts[addressStr] = { privateKey, nonce: 0 } this.accountsKeys[addressStr] = '0x' + privateKey.toString('hex') - const stateManager = this.executionContext.vm().stateManager + const stateManager = this.vmContext.vm().stateManager stateManager.getAccount(Address.fromString(addressStr)).then((account) => { account.balance = new BN(balance.replace('0x', '') || 'f00000000000000001', 16) stateManager.putAccount(Address.fromString(addressStr), account).catch((error) => { @@ -85,7 +83,7 @@ export class Accounts { eth_getBalance (payload, cb) { const address = payload.params[0] - this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { + this.vmContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { cb(null, new BN(account.balance).toString(10)) }).catch((error) => { cb(error) diff --git a/libs/remix-simulator/src/methods/blocks.ts b/libs/remix-simulator/src/methods/blocks.ts index bfed840284..7ac7bc179f 100644 --- a/libs/remix-simulator/src/methods/blocks.ts +++ b/libs/remix-simulator/src/methods/blocks.ts @@ -1,10 +1,10 @@ export class Blocks { - executionContext + vmContext coinbase: string blockNumber: number - constructor (executionContext, _options) { - this.executionContext = executionContext + constructor (vmContext, _options) { + this.vmContext = vmContext const options = _options || {} this.coinbase = options.coinbase || '0x0000000000000000000000000000000000000000' this.blockNumber = 0 @@ -28,13 +28,13 @@ export class Blocks { eth_getBlockByNumber (payload, cb) { let blockIndex = payload.params[0] if (blockIndex === 'latest') { - blockIndex = this.executionContext.latestBlockNumber + blockIndex = this.vmContext.latestBlockNumber } if (Number.isInteger(blockIndex)) { blockIndex = '0x' + blockIndex.toString(16) } - const block = this.executionContext.blocks[blockIndex] + const block = this.vmContext.blocks[blockIndex] if (!block) { return cb(new Error('block not found')) @@ -70,7 +70,7 @@ export class Blocks { } eth_getBlockByHash (payload, cb) { - const block = this.executionContext.blocks[payload.params[0]] + const block = this.vmContext.blocks[payload.params[0]] const b = { number: this.toHex(block.header.number), @@ -109,13 +109,13 @@ export class Blocks { } eth_getBlockTransactionCountByHash (payload, cb) { - const block = this.executionContext.blocks[payload.params[0]] + const block = this.vmContext.blocks[payload.params[0]] cb(null, block.transactions.length) } eth_getBlockTransactionCountByNumber (payload, cb) { - const block = this.executionContext.blocks[payload.params[0]] + const block = this.vmContext.blocks[payload.params[0]] cb(null, block.transactions.length) } @@ -131,7 +131,7 @@ export class Blocks { eth_getStorageAt (payload, cb) { const [address, position, blockNumber] = payload.params - this.executionContext.web3().debug.storageRangeAt(blockNumber, 'latest', address.toLowerCase(), position, 1, (err, result) => { + this.vmContext.web3().debug.storageRangeAt(blockNumber, 'latest', address.toLowerCase(), position, 1, (err, result) => { if (err || (result.storage && Object.values(result.storage).length === 0)) { return cb(err, '') } diff --git a/libs/remix-simulator/src/methods/debug.ts b/libs/remix-simulator/src/methods/debug.ts index ae7f3067b8..b9bf886e50 100644 --- a/libs/remix-simulator/src/methods/debug.ts +++ b/libs/remix-simulator/src/methods/debug.ts @@ -1,8 +1,8 @@ export class Debug { - executionContext + vmContext - constructor (executionContext) { - this.executionContext = executionContext + constructor (vmContext) { + this.vmContext = vmContext } methods () { @@ -14,15 +14,15 @@ export class Debug { } debug_traceTransaction (payload, cb) { - this.executionContext.web3().debug.traceTransaction(payload.params[0], {}, cb) + this.vmContext.web3().debug.traceTransaction(payload.params[0], {}, cb) } debug_preimage (payload, cb) { - this.executionContext.web3().debug.preimage(payload.params[0], cb) + this.vmContext.web3().debug.preimage(payload.params[0], cb) } debug_storageRangeAt (payload, cb) { - this.executionContext.web3().debug.storageRangeAt( + this.vmContext.web3().debug.storageRangeAt( payload.params[0], payload.params[1], payload.params[2], diff --git a/libs/remix-simulator/src/methods/filters.ts b/libs/remix-simulator/src/methods/filters.ts index c27cc49bd6..43404a1c31 100644 --- a/libs/remix-simulator/src/methods/filters.ts +++ b/libs/remix-simulator/src/methods/filters.ts @@ -1,8 +1,8 @@ export class Filters { - executionContext + vmContext - constructor (executionContext) { - this.executionContext = executionContext + constructor (vmContext) { + this.vmContext = vmContext } methods () { @@ -14,49 +14,49 @@ export class Filters { } eth_getLogs (payload, cb) { - const results = this.executionContext.logsManager.getLogsFor(payload.params[0]) + const results = this.vmContext.logsManager.getLogsFor(payload.params[0]) cb(null, results) } eth_subscribe (payload, cb) { - const subscriptionId = this.executionContext.logsManager.subscribe(payload.params) + const subscriptionId = this.vmContext.logsManager.subscribe(payload.params) cb(null, subscriptionId) } eth_unsubscribe (payload, cb) { - this.executionContext.logsManager.unsubscribe(payload.params[0]) + this.vmContext.logsManager.unsubscribe(payload.params[0]) cb(null, true) } eth_newFilter (payload, cb) { - const filterId = this.executionContext.logsManager.newFilter('filter', payload.params[0]) + const filterId = this.vmContext.logsManager.newFilter('filter', payload.params[0]) cb(null, filterId) } eth_newBlockFilter (payload, cb) { - const filterId = this.executionContext.logsManager.newFilter('block') + const filterId = this.vmContext.logsManager.newFilter('block') cb(null, filterId) } eth_newPendingTransactionFilter (payload, cb) { - const filterId = this.executionContext.logsManager.newFilter('pendingTransactions') + const filterId = this.vmContext.logsManager.newFilter('pendingTransactions') cb(null, filterId) } eth_uninstallfilter (payload, cb) { - const result = this.executionContext.logsManager.uninstallFilter(payload.params[0]) + const result = this.vmContext.logsManager.uninstallFilter(payload.params[0]) cb(null, result) } eth_getFilterChanges (payload, cb) { const filterId = payload.params[0] - const results = this.executionContext.logsManager.getLogsForFilter(filterId) + const results = this.vmContext.logsManager.getLogsForFilter(filterId) cb(null, results) } eth_getFilterLogs (payload, cb) { const filterId = payload.params[0] - const results = this.executionContext.logsManager.getLogsForFilter(filterId, true) + const results = this.vmContext.logsManager.getLogsForFilter(filterId, true) cb(null, results) } } diff --git a/libs/remix-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts index f3e8ca3f43..04ef986403 100644 --- a/libs/remix-simulator/src/methods/transactions.ts +++ b/libs/remix-simulator/src/methods/transactions.ts @@ -3,11 +3,14 @@ import { toChecksumAddress, BN, Address } from 'ethereumjs-util' import { processTx } from './txProcess' export class Transactions { - executionContext + vmContext accounts + tags - constructor (executionContext) { - this.executionContext = executionContext + + constructor (vmContext) { + this.vmContext = vmContext + this.tags = {} } init (accounts) { @@ -24,7 +27,9 @@ export class Transactions { eth_getTransactionCount: this.eth_getTransactionCount.bind(this), eth_getTransactionByHash: this.eth_getTransactionByHash.bind(this), eth_getTransactionByBlockHashAndIndex: this.eth_getTransactionByBlockHashAndIndex.bind(this), - eth_getTransactionByBlockNumberAndIndex: this.eth_getTransactionByBlockNumberAndIndex.bind(this) + eth_getTransactionByBlockNumberAndIndex: this.eth_getTransactionByBlockNumberAndIndex.bind(this), + eth_getExecutionResultFromSimulator: this.eth_getExecutionResultFromSimulator.bind(this), + eth_getHashFromTagBySimulator: this.eth_getHashFromTagBySimulator.bind(this) } } @@ -33,16 +38,30 @@ export class Transactions { if (payload.params && payload.params.length > 0 && payload.params[0].from) { payload.params[0].from = toChecksumAddress(payload.params[0].from) } - processTx(this.executionContext, this.accounts, payload, false, cb) + processTx(this.vmContext, this.accounts, payload, false, (error, result) => { + if (!error && result) { + this.vmContext.addBlock(result.block) + const hash = '0x' + result.tx.hash().toString('hex') + this.vmContext.trackTx(hash, result.block) + this.vmContext.trackExecResult(hash, result.result.execResult) + return cb (null, result.transactionHash) + } + cb(error) + }) + } + + eth_getExecutionResultFromSimulator (payload, cb) { + const txHash = payload.params[0] + cb(null, this.vmContext.exeResults[txHash]) } eth_getTransactionReceipt (payload, cb) { - this.executionContext.web3().eth.getTransactionReceipt(payload.params[0], (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(payload.params[0], (error, receipt) => { if (error) { return cb(error) } - const txBlock = this.executionContext.txs[receipt.hash] + const txBlock = this.vmContext.txs[receipt.hash] const r: Record = { transactionHash: receipt.hash, @@ -72,7 +91,7 @@ export class Transactions { eth_getCode (payload, cb) { const address = payload.params[0] - this.executionContext.web3().eth.getCode(address, (error, result) => { + this.vmContext.web3().eth.getCode(address, (error, result) => { if (error) { console.dir('error getting code') console.dir(error) @@ -91,14 +110,32 @@ export class Transactions { } payload.params[0].value = undefined + + const tag = payload.params[0].timestamp // e2e reference + + processTx(this.vmContext, this.accounts, payload, true, (error, result) => { + if (!error && result) { + this.vmContext.addBlock(result.block) + const hash = '0x' + result.tx.hash().toString('hex') + this.vmContext.trackTx(hash, result.block) + this.vmContext.trackExecResult(hash, result.result.execResult) + this.tags[tag] = result.transactionHash + // calls are not supposed to return a transaction hash. we do this for keeping track of it and allowing debugging calls. + const returnValue = `0x${result.result.execResult.returnValue.toString('hex') || '0'}` + return cb (null, returnValue) + } + cb(error) + }) + } - processTx(this.executionContext, this.accounts, payload, true, cb) + eth_getHashFromTagBySimulator (payload, cb) { + return cb(null, this.tags[payload.params[0]]) } eth_getTransactionCount (payload, cb) { const address = payload.params[0] - this.executionContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { + this.vmContext.vm().stateManager.getAccount(Address.fromString(address)).then((account) => { const nonce = new BN(account.nonce).toString(10) cb(null, nonce) }).catch((error) => { @@ -109,12 +146,12 @@ export class Transactions { eth_getTransactionByHash (payload, cb) { const address = payload.params[0] - this.executionContext.web3().eth.getTransactionReceipt(address, (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(address, (error, receipt) => { if (error) { return cb(error) } - const txBlock = this.executionContext.txs[receipt.transactionHash] + const txBlock = this.vmContext.txs[receipt.transactionHash] // TODO: params to add later const r: Record = { @@ -154,10 +191,10 @@ export class Transactions { eth_getTransactionByBlockHashAndIndex (payload, cb) { const txIndex = payload.params[1] - const txBlock = this.executionContext.blocks[payload.params[0]] + const txBlock = this.vmContext.blocks[payload.params[0]] const txHash = '0x' + txBlock.transactions[Web3.utils.toDecimal(txIndex)].hash().toString('hex') - this.executionContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { if (error) { return cb(error) } @@ -196,10 +233,10 @@ export class Transactions { eth_getTransactionByBlockNumberAndIndex (payload, cb) { const txIndex = payload.params[1] - const txBlock = this.executionContext.blocks[payload.params[0]] + const txBlock = this.vmContext.blocks[payload.params[0]] const txHash = '0x' + txBlock.transactions[Web3.utils.toDecimal(txIndex)].hash().toString('hex') - this.executionContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { + this.vmContext.web3().eth.getTransactionReceipt(txHash, (error, receipt) => { if (error) { return cb(error) } diff --git a/libs/remix-simulator/src/methods/txProcess.ts b/libs/remix-simulator/src/methods/txProcess.ts index 8f8acb5b2f..821206eae8 100644 --- a/libs/remix-simulator/src/methods/txProcess.ts +++ b/libs/remix-simulator/src/methods/txProcess.ts @@ -1,15 +1,15 @@ import { execution } from '@remix-project/remix-lib' const TxExecution = execution.txExecution -const TxRunner = execution.txRunner +const TxRunnerVM = execution.TxRunnerVM +const TxRunner = execution.TxRunner + function runCall (payload, from, to, data, value, gasLimit, txRunner, callbacks, callback) { const finalCallback = function (err, result) { if (err) { return callback(err) - } - const returnValue = result.result.execResult.returnValue.toString('hex') - const toReturn = `0x${returnValue || '0'}` - return callback(null, toReturn) + } + return callback(null, result) } TxExecution.callFunction(from, to, data, value, gasLimit, { constant: true }, txRunner, callbacks, finalCallback) @@ -20,7 +20,7 @@ function runTx (payload, from, to, data, value, gasLimit, txRunner, callbacks, c if (err) { return callback(err) } - callback(null, result.transactionHash) + callback(null, result) } TxExecution.callFunction(from, to, data, value, gasLimit, { constant: false }, txRunner, callbacks, finalCallback) @@ -31,15 +31,16 @@ function createContract (payload, from, data, value, gasLimit, txRunner, callbac if (err) { return callback(err) } - callback(null, result.transactionHash) + callback(null, result) } TxExecution.createContract(from, data, value, gasLimit, txRunner, callbacks, finalCallback) } +let txRunnerVMInstance let txRunnerInstance -export function processTx (executionContext, accounts, payload, isCall, callback) { +export function processTx (vmContext, accounts, payload, isCall, callback) { const api = { logMessage: (msg) => { }, @@ -61,11 +62,11 @@ export function processTx (executionContext, accounts, payload, isCall, callback } } - executionContext.init(api.config) - - // let txRunner = new TxRunner(accounts, api) + if (!txRunnerVMInstance) { + txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => vmContext.vm()) + } if (!txRunnerInstance) { - txRunnerInstance = new TxRunner(accounts, api, executionContext) + txRunnerInstance = new TxRunner(txRunnerVMInstance, { runAsync: false }) } txRunnerInstance.vmaccounts = accounts let { from, to, data, value, gas } = payload.params[0] diff --git a/libs/remix-simulator/src/provider.ts b/libs/remix-simulator/src/provider.ts index 8fdb5b5aba..5046660d7f 100644 --- a/libs/remix-simulator/src/provider.ts +++ b/libs/remix-simulator/src/provider.ts @@ -1,5 +1,4 @@ import { Blocks } from './methods/blocks' -import { execution } from '@remix-project/remix-lib' import { info } from './utils/logs' import merge from 'merge' @@ -11,11 +10,11 @@ import { methods as netMethods } from './methods/net' import { Transactions } from './methods/transactions' import { Debug } from './methods/debug' import { generateBlock } from './genesis' -const { executionContext } = execution +import { VMContext } from './vm-context' export class Provider { options: Record - executionContext + vmContext Accounts Transactions methods @@ -26,23 +25,23 @@ export class Provider { this.options = options this.host = host this.connected = true - // TODO: init executionContext here - this.executionContext = executionContext - this.Accounts = new Accounts(this.executionContext) - this.Transactions = new Transactions(this.executionContext) + this.vmContext = new VMContext() + + this.Accounts = new Accounts(this.vmContext) + this.Transactions = new Transactions(this.vmContext) this.methods = {} this.methods = merge(this.methods, this.Accounts.methods()) - this.methods = merge(this.methods, (new Blocks(this.executionContext, options)).methods()) + this.methods = merge(this.methods, (new Blocks(this.vmContext, options)).methods()) this.methods = merge(this.methods, miscMethods()) - this.methods = merge(this.methods, (new Filters(this.executionContext)).methods()) + this.methods = merge(this.methods, (new Filters(this.vmContext)).methods()) this.methods = merge(this.methods, netMethods()) this.methods = merge(this.methods, this.Transactions.methods()) - this.methods = merge(this.methods, (new Debug(this.executionContext)).methods()) + this.methods = merge(this.methods, (new Debug(this.vmContext)).methods()) } async init () { - await generateBlock(this.executionContext) + await generateBlock(this.vmContext) await this.Accounts.resetAccounts() this.Transactions.init(this.Accounts.accounts) } @@ -87,6 +86,40 @@ export class Provider { }; on (type, cb) { - this.executionContext.logsManager.addListener(type, cb) + this.vmContext.logsManager.addListener(type, cb) } } + +export function extend (web3) { + if (!web3.extend) { + return + } + // DEBUG + const methods = [] + if (!(web3.eth && web3.eth.getExecutionResultFromSimulator)) { + methods.push(new web3.extend.Method({ + name: 'getExecutionResultFromSimulator', + call: 'eth_getExecutionResultFromSimulator', + inputFormatter: [null], + params: 1 + })) + } + + if (!(web3.eth && web3.eth.getHashFromTagBySimulator)) { + methods.push(new web3.extend.Method({ + name: 'getHashFromTagBySimulator', + call: 'eth_getHashFromTagBySimulator', + inputFormatter: [null], + params: 1 + })) + } + + if (methods.length > 0) { + web3.extend({ + property: 'eth', + methods: methods, + properties: [] + }) + } +} + diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts new file mode 100644 index 0000000000..481f5f4057 --- /dev/null +++ b/libs/remix-simulator/src/vm-context.ts @@ -0,0 +1,147 @@ +/* global ethereum */ +'use strict' +import Web3 from 'web3' +import { rlp, keccak, bufferToHex } from 'ethereumjs-util' +import { vm, execution } from '@remix-project/remix-lib' +const EthJSVM = require('ethereumjs-vm').default +const StateManager = require('ethereumjs-vm/dist/state/stateManager').default + +/* + extend vm state manager and instanciate VM +*/ + +class StateManagerCommonStorageDump extends StateManager { + constructor (arg) { + super(arg) + this.keyHashes = {} + } + + putContractStorage (address, key, value, cb) { + this.keyHashes[keccak(key).toString('hex')] = bufferToHex(key) + super.putContractStorage(address, key, value, cb) + } + + dumpStorage (address, cb) { + this._getStorageTrie(address, (err, trie) => { + if (err) { + return cb(err) + } + const storage = {} + const stream = trie.createReadStream() + stream.on('data', (val) => { + const value = rlp.decode(val.value) + storage['0x' + val.key.toString('hex')] = { + key: this.keyHashes[val.key.toString('hex')], + value: '0x' + value.toString('hex') + } + }) + stream.on('end', function () { + cb(storage) + }) + }) + } + + getStateRoot (cb) { + const checkpoint = this._checkpointCount + this._checkpointCount = 0 + super.getStateRoot((err, stateRoot) => { + this._checkpointCount = checkpoint + cb(err, stateRoot) + }) + } + + setStateRoot (stateRoot, cb) { + const checkpoint = this._checkpointCount + this._checkpointCount = 0 + super.setStateRoot(stateRoot, (err) => { + this._checkpointCount = checkpoint + cb(err) + }) + } +} + +/* + trigger contextChanged, web3EndpointChanged +*/ +export class VMContext { + currentFork: string + blockGasLimitDefault: number + blockGasLimit: number + customNetWorks + blocks + latestBlockNumber + txs + vms + web3vm + logsManager + exeResults + + constructor () { + this.blockGasLimitDefault = 4300000 + this.blockGasLimit = this.blockGasLimitDefault + this.currentFork = 'muirGlacier' + this.vms = { + /* + byzantium: createVm('byzantium'), + constantinople: createVm('constantinople'), + petersburg: createVm('petersburg'), + istanbul: createVm('istanbul'), + */ + muirGlacier: this.createVm('muirGlacier') + } + this.blocks = {} + this.latestBlockNumber = 0 + this.txs = {} + this.exeResults = {} + this.logsManager = new execution.LogsManager() + + } + + createVm (hardfork) { + const stateManager = new StateManagerCommonStorageDump({}) + stateManager.checkpoint(() => {}) + const ethvm = new EthJSVM({ + activatePrecompiles: true, + blockchain: stateManager.blockchain, + stateManager: stateManager, + hardfork: hardfork + }) + ethvm.blockchain.validate = false + this.web3vm = new vm.Web3VMProvider() + this.web3vm.setVM(ethvm) + return { vm: ethvm, web3vm: this.web3vm, stateManager } + } + + web3 () { + return this.web3vm + } + + blankWeb3 () { + return new Web3() + } + + vm () { + return this.vms[this.currentFork].vm + } + + addBlock (block) { + let blockNumber = '0x' + block.header.number.toString('hex') + if (blockNumber === '0x') { + blockNumber = '0x0' + } + + this.blocks['0x' + block.hash().toString('hex')] = block + this.blocks[blockNumber] = block + this.latestBlockNumber = blockNumber + + this.logsManager.checkBlock(blockNumber, block, this.web3vm) + } + + trackTx (tx, block) { + this.txs[tx] = block + } + + trackExecResult (tx, execReult) { + this.exeResults[tx] = execReult + } +} diff --git a/libs/remix-tests/jest.config.js b/libs/remix-tests/jest.config.js index 7923ddf044..dcbd823a5c 100644 --- a/libs/remix-tests/jest.config.js +++ b/libs/remix-tests/jest.config.js @@ -1,7 +1,7 @@ module.exports = { name: 'remix-tests', preset: '../../jest.config.js', - verbose: true, + verbose: false, silent: false, // Silent console messages, specially the 'remix-simulator' ones transform: { '^.+\\.[tj]sx?$': 'ts-jest', @@ -18,6 +18,6 @@ module.exports = { "!src/types.ts", "!src/logger.ts" ], - coverageDirectory: '../../coverage/libs/remix-tests', + coverageDirectory: '../../coverage/libs/remix-tests' }; \ No newline at end of file diff --git a/libs/remix-tests/src/deployer.ts b/libs/remix-tests/src/deployer.ts index c6e00456fa..1e3fcb8d88 100644 --- a/libs/remix-tests/src/deployer.ts +++ b/libs/remix-tests/src/deployer.ts @@ -79,7 +79,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with contracts[contractName] = contractObject contracts[contractName].filename = filename - callback(null, { result: { createdAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM + callback(null, { receipt: { contractAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM }).on('error', function (err) { console.error(err) callback(err) diff --git a/libs/remix-tests/tests/testRunner.cli.spec.ts b/libs/remix-tests/tests/testRunner.cli.spec.ts index 3d906095f3..a865bbc649 100644 --- a/libs/remix-tests/tests/testRunner.cli.spec.ts +++ b/libs/remix-tests/tests/testRunner.cli.spec.ts @@ -3,17 +3,23 @@ import { resolve } from 'path' describe('testRunner: remix-tests CLI', () => { // remix-tests binary, after build, is used as executable + const executablePath = resolve(__dirname + '/../../../dist/libs/remix-tests/bin/remix-tests') + const result = spawnSync('ls', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) if(result) { const dirContent = result.stdout.toString() // Install dependencies if 'node_modules' is not already present - if(!dirContent.includes('node_modules')) execSync('npm install', { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) + if(!dirContent.includes('node_modules')) execSync( + 'ln -s ' + __dirname + '/../../../node_modules node_modules', + { cwd: resolve(__dirname + '/../../../dist/libs/remix-tests') }) } + describe('test various CLI options', () => { test('remix-tests version', () => { const res = spawnSync(executablePath, ['-V']) + console.log(res.stdout.toString()) expect(res.stdout.toString().trim()).toBe(require('../package.json').version) }) diff --git a/libs/remix-tests/tsconfig.json b/libs/remix-tests/tsconfig.json index 4422abf942..cf7076c1d1 100644 --- a/libs/remix-tests/tsconfig.json +++ b/libs/remix-tests/tsconfig.json @@ -3,9 +3,9 @@ "compilerOptions": { "types": ["node", "jest"], "module": "commonjs", + "esModuleInterop": true, "allowJs": true, "rootDir": "./", - "esModuleInterop": true }, "include": ["**/*.ts"] } \ No newline at end of file diff --git a/libs/remix-tests/tsconfig.lib.json b/libs/remix-tests/tsconfig.lib.json index ac117282b1..aec3a4c785 100644 --- a/libs/remix-tests/tsconfig.lib.json +++ b/libs/remix-tests/tsconfig.lib.json @@ -1,16 +1,15 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "commonjs", - "outDir": "../../dist/out-tsc", - "declaration": true, - "rootDir": "./", - "types": ["node"] - }, - "exclude": [ - "**/*.spec.ts", - "tests/" - ], - "include": ["**/*.ts"] + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "rootDir": "./", + "types": ["node"] + }, + "exclude": [ + "**/*.spec.ts", + "test/" + ], + "include": ["**/*.ts"] } - \ No newline at end of file diff --git a/libs/remix-tests/tsconfig.spec.json b/libs/remix-tests/tsconfig.spec.json index 118a64f08e..559410b96a 100644 --- a/libs/remix-tests/tsconfig.spec.json +++ b/libs/remix-tests/tsconfig.spec.json @@ -1,16 +1,15 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "**/*.spec.ts", - "**/*.spec.tsx", - "**/*.spec.js", - "**/*.spec.jsx", - "**/*.d.ts" - ] - } - \ No newline at end of file + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +}