From 9a63d0977da263aeb1c1df2534f2401f781d0bcd Mon Sep 17 00:00:00 2001 From: Iuri Matias Date: Thu, 27 Dec 2018 07:22:59 -0500 Subject: [PATCH] refactor recorder; separate logic from view code --- src/app/tabs/run-tab.js | 16 +- src/{ => app/tabs/runTab/model}/recorder.js | 247 +++++++++++--------- src/app/tabs/runTab/recorder.js | 103 ++++---- 3 files changed, 200 insertions(+), 166 deletions(-) rename src/{ => app/tabs/runTab/model}/recorder.js (56%) diff --git a/src/app/tabs/run-tab.js b/src/app/tabs/run-tab.js index ad9930b210..8e4fa9d51e 100644 --- a/src/app/tabs/run-tab.js +++ b/src/app/tabs/run-tab.js @@ -8,6 +8,8 @@ var css = require('./styles/run-tab-styles') var SettingsUI = require('./runTab/settings.js') var ContractDropdownUI = require('./runTab/contractDropdown.js') + +var Recorder = require('./runTab/model/recorder.js') var RecorderUI = require('./runTab/recorder.js') function runTab (opts, localRegistry) { @@ -76,7 +78,19 @@ function runTab (opts, localRegistry) { var container = yo`
` - var recorderInterface = new RecorderUI(self.event, self) + var recorder = new Recorder(self._deps.udapp, self._deps.fileManager, self._deps.udapp.config) + recorder.event.register('newTxRecorded', (count) => { + this.data.count = count + this._view.recorderCount.innerText = count + }) + recorder.event.register('cleared', () => { + this.data.count = 0 + this._view.recorderCount.innerText = 0 + }) + executionContext.event.register('contextChanged', recorder.clearAll.bind(recorder)) + self.event.register('clearInstance', recorder.clearAll.bind(recorder)) + + var recorderInterface = new RecorderUI(recorder, self) recorderInterface.render() self._view.collapsedView = yo` diff --git a/src/recorder.js b/src/app/tabs/runTab/model/recorder.js similarity index 56% rename from src/recorder.js rename to src/app/tabs/runTab/model/recorder.js index a12bc9fa22..747aab6e5a 100644 --- a/src/recorder.js +++ b/src/app/tabs/runTab/model/recorder.js @@ -1,17 +1,12 @@ -var yo = require('yo-yo') -var remixLib = require('remix-lib') -var EventManager = require('./lib/events') +var async = require('async') var ethutil = require('ethereumjs-util') -var executionContext = require('./execution-context') +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var executionContext = remixLib.execution.executionContext var format = remixLib.execution.txFormat var txHelper = remixLib.execution.txHelper var typeConversion = remixLib.execution.typeConversion -var async = require('async') -var modal = require('./app/ui/modal-dialog-custom') - -var confirmDialog = require('./app/execution/confirmDialog') -var modalCustom = require('./app/ui/modal-dialog-custom') -var modalDialog = require('./app/ui/modaldialog') +var helper = require('../../../../lib/helper.js') /** * Record transaction as long as the user create them. @@ -19,13 +14,15 @@ var modalDialog = require('./app/ui/modaldialog') * */ class Recorder { - constructor (udapp, logCallBack) { + constructor (udapp, fileManager, config) { var self = this - self.logCallBack = logCallBack self.event = new EventManager() self.data = { _listen: true, _replay: false, journal: [], _createdContracts: {}, _createdContractsReverse: {}, _usedAccounts: {}, _abis: {}, _contractABIReferences: {}, _linkReferences: {} } + this.udapp = udapp + this.fileManager = fileManager + this.config = config - udapp.event.register('initiatingTransaction', (timestamp, tx, payLoad) => { + this.udapp.event.register('initiatingTransaction', (timestamp, tx, payLoad) => { if (tx.useCall) return var { from, to, value } = tx @@ -58,7 +55,7 @@ class Recorder { record.inputs = txHelper.serializeInputs(payLoad.funAbi) record.type = payLoad.funAbi.type - udapp.getAccounts((error, accounts) => { + this.udapp.getAccounts((error, accounts) => { if (error) return console.log(error) record.from = `account{${accounts.indexOf(from)}}` self.data._usedAccounts[record.from] = from @@ -67,13 +64,13 @@ class Recorder { } }) - udapp.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp) => { + this.udapp.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp) => { if (error) return console.log(error) if (call) return var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress if (!address) return // not a contract creation - address = addressToString(address) + address = this.addressToString(address) // save back created addresses for the convertion from tokens to real adresses this.data._createdContracts[address] = timestamp this.data._createdContractsReverse[timestamp] = address @@ -178,15 +175,15 @@ class Recorder { * @param {Function} newContractFn * */ - run (records, accounts, options, abis, linkReferences, udapp, newContractFn) { + run (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, newContractFn) { var self = this self.setListen(false) - self.logCallBack(`Running ${records.length} transaction(s) ...`) + logCallBack(`Running ${records.length} transaction(s) ...`) async.eachOfSeries(records, function (tx, index, cb) { var record = self.resolveAddress(tx.record, accounts, options) var abi = abis[tx.record.abi] if (!abi) { - modal.alert('cannot find ABI for ' + tx.record.abi + '. Execution stopped at ' + index) + alertCb('cannot find ABI for ' + tx.record.abi + '. Execution stopped at ' + index) return } /* Resolve Library */ @@ -210,7 +207,7 @@ class Recorder { fnABI = txHelper.getFunction(abi, record.name + record.inputs) } if (!fnABI) { - modal.alert('cannot resolve abi of ' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index) + alertCb('cannot resolve abi of ' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index) cb('cannot resolve abi') return } @@ -230,107 +227,29 @@ class Recorder { tx.record.parameters[index] = value }) } catch (e) { - modal.alert('cannot resolve input parameters ' + JSON.stringify(tx.record.parameters) + '. Execution stopped at ' + index) + alertCb('cannot resolve input parameters ' + JSON.stringify(tx.record.parameters) + '. Execution stopped at ' + index) return } } var data = format.encodeData(fnABI, tx.record.parameters, tx.record.bytecode) if (data.error) { - modal.alert(data.error + '. Record:' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index) + alertCb(data.error + '. Record:' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index) cb(data.error) return } else { - self.logCallBack(`(${index}) ${JSON.stringify(record, null, '\t')}`) - self.logCallBack(`(${index}) data: ${data.data}`) + logCallBack(`(${index}) ${JSON.stringify(record, null, '\t')}`) + logCallBack(`(${index}) data: ${data.data}`) record.data = { dataHex: data.data, funArgs: tx.record.parameters, funAbi: fnABI, contractBytecode: tx.record.bytecode, contractName: tx.record.contractName } } - udapp.runTx(record, - - (network, tx, gasEstimation, continueTxExecution, cancelCb) => { - if (network.name !== 'Main') { - return continueTxExecution(null) - } - var amount = executionContext.web3().fromWei(typeConversion.toInt(tx.value), 'ether') - var content = confirmDialog(tx, amount, gasEstimation, self, - (gasPrice, cb) => { - let txFeeText, priceStatus - // TODO: this try catch feels like an anti pattern, can/should be - // removed, but for now keeping the original logic - try { - var fee = executionContext.web3().toBigNumber(tx.gas).mul(executionContext.web3().toBigNumber(executionContext.web3().toWei(gasPrice.toString(10), 'gwei'))) - txFeeText = ' ' + executionContext.web3().fromWei(fee.toString(10), 'ether') + ' Ether' - priceStatus = true - } catch (e) { - txFeeText = ' Please fix this issue before sending any transaction. ' + e.message - priceStatus = false - } - cb(txFeeText, priceStatus) - }, - (cb) => { - executionContext.web3().eth.getGasPrice((error, gasPrice) => { - var warnMessage = ' Please fix this issue before sending any transaction. ' - if (error) { - return cb('Unable to retrieve the current network gas price.' + warnMessage + error) - } - try { - var gasPriceValue = executionContext.web3().fromWei(gasPrice.toString(10), 'gwei') - cb(null, gasPriceValue) - } catch (e) { - cb(warnMessage + e.message, null, false) - } - }) - } - ) - modalDialog('Confirm transaction', content, - { label: 'Confirm', - fn: () => { - udapp._deps.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked) - // TODO: check if this is check is still valid given the refactor - if (!content.gasPriceStatus) { - cancelCb('Given gas price is not correct') - } else { - var gasPrice = executionContext.web3().toWei(content.querySelector('#gasprice').value, 'gwei') - continueTxExecution(gasPrice) - } - }}, { - label: 'Cancel', - fn: () => { - return cancelCb('Transaction canceled by user.') - } - }) - }, - (error, continueTxExecution, cancelCb) => { - if (error) { - var msg = typeof error !== 'string' ? error.message : error - modalDialog('Gas estimation failed', yo`
Gas estimation errored with the following message (see below). - The transaction execution will likely fail. Do you want to force sending?
- ${msg} -
`, - { - label: 'Send Transaction', - fn: () => { - continueTxExecution() - }}, { - label: 'Cancel Transaction', - fn: () => { - cancelCb() - } - }) - } else { - continueTxExecution() - } - }, - function (okCb, cancelCb) { - modalCustom.promptPassphrase(null, 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) - }, + self.udapp.runTx(record, confirmationCb, continueCb, promptCb, function (err, txResult) { if (err) { console.error(err) - self.logCallBack(err + '. Execution failed at ' + index) + logCallBack(err + '. Execution failed at ' + index) } else { var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress if (address) { - address = addressToString(address) + address = self.addressToString(address) // save back created addresses for the convertion from tokens to real adresses self.data._createdContracts[address] = tx.timestamp self.data._createdContractsReverse[tx.timestamp] = address @@ -342,17 +261,119 @@ class Recorder { ) }, () => { self.setListen(true); self.clearAll() }) } -} -function addressToString (address) { - if (!address) return null - if (typeof address !== 'string') { - address = address.toString('hex') + addressToString (address) { + if (!address) return null + if (typeof address !== 'string') { + address = address.toString('hex') + } + if (address.indexOf('0x') === -1) { + address = '0x' + address + } + return address } - if (address.indexOf('0x') === -1) { - address = '0x' + address + + runScenario (continueCb, promptCb, alertCb, confirmDialog, modalDialog, logCallBack, cb) { + var currentFile = this.config.get('currentFile') + this.fileManager.fileProviderOf(currentFile).get(currentFile, (error, json) => { + if (error) { + return cb('Invalid Scenario File ' + error) + } + if (!currentFile.match('.json$')) { + return cb('A scenario file is required. Please make sure a scenario file is currently displayed in the editor. The file must be of type JSON. Use the "Save Transactions" Button to generate a new Scenario File.') + } + try { + var obj = JSON.parse(json) + var txArray = obj.transactions || [] + var accounts = obj.accounts || [] + var options = obj.options || {} + var abis = obj.abis || {} + var linkReferences = obj.linkReferences || {} + } catch (e) { + return cb('Invalid Scenario File, please try again') + } + + if (!txArray.length) { + return + } + + var confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { + if (network.name !== 'Main') { + return continueTxExecution(null) + } + var amount = executionContext.web3().fromWei(typeConversion.toInt(tx.value), 'ether') + + // TODO: there is still a UI dependency to remove here, it's still too coupled at this point to remove easily + var content = confirmDialog(tx, amount, gasEstimation, this.recorder, + (gasPrice, cb) => { + let txFeeText, priceStatus + // TODO: this try catch feels like an anti pattern, can/should be + // removed, but for now keeping the original logic + try { + var fee = executionContext.web3().toBigNumber(tx.gas).mul(executionContext.web3().toBigNumber(executionContext.web3().toWei(gasPrice.toString(10), 'gwei'))) + txFeeText = ' ' + executionContext.web3().fromWei(fee.toString(10), 'ether') + ' Ether' + priceStatus = true + } catch (e) { + txFeeText = ' Please fix this issue before sending any transaction. ' + e.message + priceStatus = false + } + cb(txFeeText, priceStatus) + }, + (cb) => { + executionContext.web3().eth.getGasPrice((error, gasPrice) => { + var warnMessage = ' Please fix this issue before sending any transaction. ' + if (error) { + return cb('Unable to retrieve the current network gas price.' + warnMessage + error) + } + try { + var gasPriceValue = executionContext.web3().fromWei(gasPrice.toString(10), 'gwei') + cb(null, gasPriceValue) + } catch (e) { + cb(warnMessage + e.message, null, false) + } + }) + } + ) + modalDialog('Confirm transaction', content, + { label: 'Confirm', + fn: () => { + this.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked) + // TODO: check if this is check is still valid given the refactor + if (!content.gasPriceStatus) { + cancelCb('Given gas price is not correct') + } else { + var gasPrice = executionContext.web3().toWei(content.querySelector('#gasprice').value, 'gwei') + continueTxExecution(gasPrice) + } + }}, { + label: 'Cancel', + fn: () => { + return cancelCb('Transaction canceled by user.') + } + }) + } + + this.run(txArray, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, (abi, address, contractName) => { + cb(null, abi, address, contractName) + }) + }) } - return address + + saveScenario (promptCb, cb) { + var txJSON = JSON.stringify(this.getAll(), null, 2) + var path = this.fileManager.currentPath() + promptCb(path, input => { + var fileProvider = this.fileManager.fileProviderOf(path) + if (!fileProvider) return + var newFile = path + '/' + input + helper.createNonClashingName(newFile, fileProvider, (error, newFile) => { + if (error) return cb('Failed to create file. ' + newFile + ' ' + error) + if (!fileProvider.set(newFile, txJSON)) return cb('Failed to create file ' + newFile) + this.fileManager.switchFile(newFile) + }) + }) + } + } module.exports = Recorder diff --git a/src/app/tabs/runTab/recorder.js b/src/app/tabs/runTab/recorder.js index 6ac5be0e73..c14f221efc 100644 --- a/src/app/tabs/runTab/recorder.js +++ b/src/app/tabs/runTab/recorder.js @@ -1,28 +1,17 @@ var yo = require('yo-yo') var csjs = require('csjs-inject') var css = require('../styles/run-tab-styles') -var helper = require('../../../lib/helper.js') -var executionContext = require('../../../execution-context') -var Recorder = require('../../../recorder') + var modalDialogCustom = require('../../ui/modal-dialog-custom') +var modalDialog = require('../../ui/modaldialog') +var confirmDialog = require('../../execution/confirmDialog') class RecorderUI { - constructor (runTabEvent, parentSelf) { + constructor (recorder, parentSelf) { + this.recorder = recorder this.parentSelf = parentSelf - this.recorder = new Recorder(this.parentSelf._deps.udapp, this.parentSelf._deps.logCallback) - - this.recorder.event.register('newTxRecorded', (count) => { - this.parentSelf.data.count = count - this.parentSelf._view.recorderCount.innerText = count - }) - this.recorder.event.register('cleared', () => { - this.parentSelf.data.count = 0 - this.parentSelf._view.recorderCount.innerText = 0 - }) - - executionContext.event.register('contextChanged', this.recorder.clearAll.bind(this.recorder)) - runTabEvent.register('clearInstance', this.recorder.clearAll.bind(this.recorder)) + this.logCallBack = this.parentSelf._deps.logCallback } render () { @@ -42,48 +31,58 @@ class RecorderUI { } runScenario () { - var currentFile = this.parentSelf._deps.config.get('currentFile') - this.parentSelf._deps.fileManager.fileProviderOf(currentFile).get(currentFile, (error, json) => { + var continueCb = (error, continueTxExecution, cancelCb) => { if (error) { - return modalDialogCustom.alert('Invalid Scenario File ' + error) - } - if (!currentFile.match('.json$')) { - modalDialogCustom.alert('A scenario file is required. Please make sure a scenario file is currently displayed in the editor. The file must be of type JSON. Use the "Save Transactions" Button to generate a new Scenario File.') + var msg = typeof error !== 'string' ? error.message : error + modalDialog('Gas estimation failed', yo`
Gas estimation errored with the following message (see below). + The transaction execution will likely fail. Do you want to force sending?
+ ${msg} +
`, + { + label: 'Send Transaction', + fn: () => { + continueTxExecution() + }}, { + label: 'Cancel Transaction', + fn: () => { + cancelCb() + } + }) + } else { + continueTxExecution() } - try { - var obj = JSON.parse(json) - var txArray = obj.transactions || [] - var accounts = obj.accounts || [] - var options = obj.options || {} - var abis = obj.abis || {} - var linkReferences = obj.linkReferences || {} - } catch (e) { - return modalDialogCustom.alert('Invalid Scenario File, please try again') - } - if (txArray.length) { - var noInstancesText = this.parentSelf._view.noInstancesText - if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) } - this.recorder.run(txArray, accounts, options, abis, linkReferences, this.parentSelf._deps.udapp, (abi, address, contractName) => { - this.parentSelf._view.instanceContainer.appendChild(this.parentSelf._deps.udappUI.renderInstanceFromABI(abi, address, contractName)) - }) + } + + var promptCb = (okCb, cancelCb) => { + modalDialogCustom.promptPassphrase(null, 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) + } + + var alertCb = (msg) => { + modalDialogCustom.alert(msg) + } + + // TODO: there is still a UI dependency to remove here, it's still too coupled at this point to remove easily + this.recorder.runScenario(continueCb, promptCb, alertCb, confirmDialog, modalDialog, this.logCallBack, (error, abi, address, contractName) => { + if (error) { + return modalDialogCustom.alert(error) } + + var noInstancesText = this.parentSelf._view.noInstancesText + if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) } + + this.parentSelf._view.instanceContainer.appendChild(this.parentSelf._deps.udappUI.renderInstanceFromABI(abi, address, contractName)) }) } triggerRecordButton () { - var txJSON = JSON.stringify(this.recorder.getAll(), null, 2) - var fileManager = this.parentSelf._deps.fileManager - var path = fileManager.currentPath() - modalDialogCustom.prompt(null, 'Transactions will be saved in a file under ' + path, 'scenario.json', input => { - var fileProvider = fileManager.fileProviderOf(path) - if (!fileProvider) return - var newFile = path + '/' + input - helper.createNonClashingName(newFile, fileProvider, (error, newFile) => { - if (error) return modalDialogCustom.alert('Failed to create file. ' + newFile + ' ' + error) - if (!fileProvider.set(newFile, txJSON)) return modalDialogCustom.alert('Failed to create file ' + newFile) - fileManager.switchFile(newFile) - }) - }) + this.recorder.saveScenario( + (path, cb) => { + modalDialogCustom.prompt(null, 'Transactions will be saved in a file under ' + path, 'scenario.json', cb) + }, + (error) => { + if (error) return modalDialogCustom.alert(error) + } + ) } }