diff --git a/src/app.js b/src/app.js index b2ffe72169..87379f67a3 100644 --- a/src/app.js +++ b/src/app.js @@ -359,13 +359,16 @@ Please make a backup of your contracts and start using http://remix.ethereum.org registry.put({api: self._components.compilersArtefacts, name: 'compilersartefacts'}) // ----------------- UniversalDApp ----------------- - var udapp = new UniversalDApp({ - removable: false, - removable_instances: true - }) + var udapp = new UniversalDApp(registry) + // TODO: to remove when possible registry.put({api: udapp, name: 'udapp'}) + udapp.event.register('transactionBroadcasted', (txhash, networkName) => { + var txLink = executionContext.txDetailsLink(networkName, txhash) + if (txLink) registry.get('logCallback').api.logCallback(yo`${txLink}`) + }) - var udappUI = new UniversalDAppUI(udapp) + var udappUI = new UniversalDAppUI(udapp, registry) + // TODO: to remove when possible registry.put({api: udappUI, name: 'udappUI'}) // ----------------- Tx listener ----------------- diff --git a/src/app/execution/confirmDialog.js b/src/app/execution/confirmDialog.js index c8921ac3a8..c139cbf6c1 100644 --- a/src/app/execution/confirmDialog.js +++ b/src/app/execution/confirmDialog.js @@ -16,6 +16,7 @@ var css = csjs` } ` +// TODO: self is not actually used and can be removed function confirmDialog (tx, amount, gasEstimation, self, newGasPriceCb, initialParamsCb) { var onGasPriceChange = function () { var gasPrice = el.querySelector('#gasprice').value diff --git a/src/app/files/compiler-metadata.js b/src/app/files/compiler-metadata.js index 3790651c3a..59959aae9e 100644 --- a/src/app/files/compiler-metadata.js +++ b/src/app/files/compiler-metadata.js @@ -74,6 +74,7 @@ class CompilerMetadata { return metadata } + // TODO: is only called by dropdownLogic and can be moved there deployMetadataOf (contractName, callback) { var self = this var provider = self._opts.fileManager.currentFileProvider() diff --git a/src/app/tabs/run-tab.js b/src/app/tabs/run-tab.js index 4067e69e63..d624fc894d 100644 --- a/src/app/tabs/run-tab.js +++ b/src/app/tabs/run-tab.js @@ -1,42 +1,28 @@ -'use strict' var $ = require('jquery') var yo = require('yo-yo') -var remixLib = require('remix-lib') -var ethJSUtil = require('ethereumjs-util') -var csjs = require('csjs-inject') -var txExecution = remixLib.execution.txExecution -var txFormat = remixLib.execution.txFormat -var txHelper = remixLib.execution.txHelper var EventManager = require('../../lib/events') var globlalRegistry = require('../../global/registry') -var helper = require('../../lib/helper.js') var executionContext = require('../../execution-context') -var modalDialogCustom = require('../ui/modal-dialog-custom') -var copyToClipboard = require('../ui/copy-to-clipboard') -const Buffer = require('safe-buffer').Buffer -var Personal = require('web3-eth-personal') var Card = require('../ui/card') -var Recorder = require('../../recorder') -var addTooltip = require('../ui/tooltip') var css = require('./styles/run-tab-styles') -var MultiParamManager = require('../../multiParamManager') -var modalDialog = require('../ui/modaldialog') -var CompilerAbstract = require('../compiler/compiler-abstract') -var tootip = require('../ui/tooltip') + +var Settings = require('./runTab/model/settings.js') +var SettingsUI = require('./runTab/settings.js') + +var DropdownLogic = require('./runTab/model/dropdownlogic.js') +var ContractDropdownUI = require('./runTab/contractDropdown.js') + +var Recorder = require('./runTab/model/recorder.js') +var RecorderUI = require('./runTab/recorder.js') function runTab (opts, localRegistry) { - /* ------------------------- - VARIABLES - --------------------------- */ var self = this self.event = new EventManager() self._view = {} self.data = { count: 0, - text: `All transactions (deployed contracts and function executions) - in this environment can be saved and replayed in - another environment. e.g Transactions created in - Javascript VM can be replayed in the Injected Web3.` + text: `All transactions (deployed contracts and function executions) in this environment can be saved and replayed in + another environment. e.g Transactions created in Javascript VM can be replayed in the Injected Web3.` } self._components = {} self._components.registry = localRegistry || globlalRegistry @@ -51,14 +37,8 @@ function runTab (opts, localRegistry) { var index = select.selectedIndex var selectedUnit = select.querySelectorAll('option')[index].dataset.unit var unit = 'ether' // default - if (selectedUnit === 'ether') { - unit = 'ether' - } else if (selectedUnit === 'finney') { - unit = 'finney' - } else if (selectedUnit === 'gwei') { - unit = 'gwei' - } else if (selectedUnit === 'wei') { - unit = 'wei' + if (['ether', 'finney', 'gwei', 'wei'].indexOf(selectedUnit) >= 0) { + unit = selectedUnit } cb(null, executionContext.web3().toWei(number, unit)) } catch (e) { @@ -100,7 +80,21 @@ function runTab (opts, localRegistry) { ` var container = yo`
` - var recorderInterface = makeRecorder(localRegistry, self.event, self) + + var recorder = new Recorder(self._deps.udapp, self._deps.fileManager, self._deps.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`
@@ -135,620 +129,56 @@ function runTab (opts, localRegistry) { status.appendChild(self._view.collapsedView) } }) - /* ------------------------- - MAIN HTML ELEMENT - --------------------------- */ - var el = yo` -
- ${settings(container, self)} - ${contractDropdown(self.event, self)} - ${recorderCard.render()} - ${self._view.instanceContainer} -
- ` - container.appendChild(el) - return { render () { return container } } -} + var settings = new Settings(self._deps.udapp) + var settingsUI = new SettingsUI(settings) -var accountListCallId = 0 -var loadedAccounts = {} -function fillAccountsList (container, self) { - accountListCallId++ - (function (callid) { - var txOrigin = container.querySelector('#txorigin') - self._deps.udapp.getAccounts((err, accounts) => { - if (accountListCallId > callid) return - accountListCallId++ - if (err) { addTooltip(`Cannot get account list: ${err}`) } - for (var loadedaddress in loadedAccounts) { - if (accounts.indexOf(loadedaddress) === -1) { - txOrigin.removeChild(txOrigin.querySelector('option[value="' + loadedaddress + '"]')) - delete loadedAccounts[loadedaddress] - } - } - for (var i in accounts) { - var address = accounts[i] - if (!loadedAccounts[address]) { - txOrigin.appendChild(yo``) - loadedAccounts[address] = 1 - } - } - txOrigin.setAttribute('value', accounts[0]) - }) - })(accountListCallId) -} - -function updateAccountBalances (container, self) { - var accounts = $(container.querySelector('#txorigin')).children('option') - accounts.each(function (index, value) { - (function (acc) { - self._deps.udapp.getBalanceInEther(accounts[acc].value, function (err, res) { - if (!err) { - accounts[acc].innerText = helper.shortenAddress(accounts[acc].value, res) - } - }) - })(index) - }) -} - -/* ------------------------------------------------ - RECORDER ------------------------------------------------- */ -function makeRecorder (registry, runTabEvent, self) { - var recorder = new Recorder(self._deps.udapp, self._deps.logCallback) - - recorder.event.register('newTxRecorded', (count) => { - self.data.count = count - self._view.recorderCount.innerText = count + self.event.register('clearInstance', () => { + this._view.instanceContainer.innerHTML = '' // clear the instances list + this._view.instanceContainer.appendChild(self._view.instanceContainerTitle) + this._view.instanceContainer.appendChild(self._view.noInstancesText) }) - recorder.event.register('cleared', () => { - self.data.count = 0 - self._view.recorderCount.innerText = 0 + settingsUI.event.register('clearInstance', () => { + this.event.trigger('clearInstance', []) }) - executionContext.event.register('contextChanged', () => { - recorder.clearAll() + var dropdownLogic = new DropdownLogic( + this._deps.fileManager, + this._deps.pluginManager, + this._deps.compilersArtefacts, + this._deps.compiler, + this._deps.config, + this._deps.editor, + this._deps.udapp, + this._deps.filePanel + ) + var contractDropdownUI = new ContractDropdownUI(dropdownLogic, this._deps.logCallback) + + contractDropdownUI.event.register('clearInstance', () => { + var noInstancesText = this._view.noInstancesText + if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) } }) - - runTabEvent.register('clearInstance', () => { - recorder.clearAll() + contractDropdownUI.event.register('newContractABIAdded', (abi, address) => { + this._view.instanceContainer.appendChild(this._deps.udappUI.renderInstanceFromABI(abi, address, address)) }) - - var css2 = csjs` - .container {} - .runTxs {} - .recorder {} - ` - - var runButton = yo`` - var recordButton = yo` - ` - - function triggerRecordButton () { - var txJSON = JSON.stringify(recorder.getAll(), null, 2) - var fileManager = self._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) { - 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)) { - modalDialogCustom.alert('Failed to create file ' + newFile) - } else { - fileManager.switchFile(newFile) - } - }) - } - }) - } - - runButton.onclick = () => { - /* - @TODO - update account address in scenario.json - popup if scenario.json not open - "Open a file with transactions you want to replay and click play again" - */ - var currentFile = self._deps.config.get('currentFile') - self._deps.fileManager.fileProviderOf(currentFile).get(currentFile, (error, json) => { - if (error) { - modalDialogCustom.alert('Invalid Scenario File ' + error) - } else { - if (currentFile.match('.json$')) { - 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 = self._view.noInstancesText - if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) } - recorder.run(txArray, accounts, options, abis, linkReferences, self._deps.udapp, (abi, address, contractName) => { - self._view.instanceContainer.appendChild(self._deps.udappUI.renderInstanceFromABI(abi, address, contractName)) - }) - } - } else { - 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.') - } - } - }) - } - - return { recordButton, runButton } -} -/* ------------------------------------------------ - CONTRACT (deploy or access deployed) ------------------------------------------------- */ - -function contractDropdown (events, self) { - var instanceContainer = self._view.instanceContainer - var instanceContainerTitle = self._view.instanceContainerTitle - instanceContainer.appendChild(instanceContainerTitle) - instanceContainer.appendChild(self._view.noInstancesText) - var compFails = yo`` - var info = yo`` - - var newlyCompiled = (success, data, source, compiler, compilerFullName) => { - getContractNames(success, data, compiler, compilerFullName) - if (success) { - compFails.style.display = 'none' - document.querySelector(`.${css.contractNames}`).classList.remove(css.contractNamesError) - } else { - compFails.style.display = 'block' - document.querySelector(`.${css.contractNames}`).classList.add(css.contractNamesError) - } - } - - self._deps.pluginManager.event.register('sendCompilationResult', (file, source, languageVersion, data) => { - let compiler = new CompilerAbstract(languageVersion, data, source) - newlyCompiled(true, data, source, compiler, languageVersion) - }) - - var deployAction = (value) => { - self._view.createPanel.style.display = value - self._view.orLabel.style.display = value - } - - self._deps.fileManager.event.register('currentFileChanged', (currentFile) => { - document.querySelector(`.${css.contractNames}`).classList.remove(css.contractNamesError) - var contractNames = document.querySelector(`.${css.contractNames.classNames[0]}`) - contractNames.innerHTML = '' - if (/.(.abi)$/.exec(currentFile)) { - deployAction('none') - compFails.style.display = 'none' - contractNames.appendChild(yo``) - selectContractNames.setAttribute('disabled', true) - } else if (/.(.sol)$/.exec(currentFile)) { - deployAction('block') - } + contractDropdownUI.event.register('newContractInstanceAdded', (contractObject, address, value) => { + this._view.instanceContainer.appendChild(this._deps.udappUI.renderInstance(contractObject, address, value)) }) - var atAddressButtonInput = yo`` - var selectContractNames = yo`` - - function getSelectedContract () { - var contract = selectContractNames.children[selectContractNames.selectedIndex] - var contractName = contract.innerHTML - var compiler = self._deps.compilersArtefacts['__last'] - if (!compiler) return null - - if (contractName) { - return { - name: contractName, - contract: compiler.getContract(contractName), - compiler - } - } - return null - } - - self._view.createPanel = yo`
` - self._view.orLabel = yo`
or
` - var el = yo` -
-
- ${selectContractNames} ${compFails} ${info} -
-
- ${self._view.createPanel} - ${self._view.orLabel} -
-
At Address
- ${atAddressButtonInput} -
-
-
- ` - - function setInputParamsPlaceHolder () { - self._view.createPanel.innerHTML = '' - if (selectContractNames.selectedIndex >= 0 && selectContractNames.children.length > 0) { - var selectedContract = getSelectedContract() - var ctrabi = txHelper.getConstructorInterface(selectedContract.contract.object.abi) - var ctrEVMbc = selectedContract.contract.object.evm.bytecode.object - var createConstructorInstance = new MultiParamManager(0, ctrabi, (valArray, inputsValues) => { - createInstance(inputsValues, selectedContract.compiler) - }, txHelper.inputParametersDeclarationToString(ctrabi.inputs), 'Deploy', ctrEVMbc) - self._view.createPanel.appendChild(createConstructorInstance.render()) - return - } else { - self._view.createPanel.innerHTML = 'No compiled contracts' - } - } - - selectContractNames.addEventListener('change', setInputParamsPlaceHolder) - - function createInstanceCallback (selectedContract, data) { - self._deps.logCallback(`creation of ${selectedContract.name} pending...`) - if (data) { - data.contractName = selectedContract.name - data.linkReferences = selectedContract.contract.object.evm.bytecode.linkReferences - data.contractABI = selectedContract.contract.object.abi - } - self._deps.udapp.createContract(data, (error, txResult) => { - if (!error) { - var isVM = executionContext.isVM() - if (isVM) { - var vmError = txExecution.checkVMError(txResult) - if (vmError.error) { - self._deps.logCallback(vmError.message) - return - } - } - if (txResult.result.status && txResult.result.status === '0x0') { - self._deps.logCallback(`creation of ${selectedContract.name} errored: transaction execution failed`) - return - } - var noInstancesText = self._view.noInstancesText - if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) } - var address = isVM ? txResult.result.createdAddress : txResult.result.contractAddress - instanceContainer.appendChild(self._deps.udappUI.renderInstance(selectedContract.contract.object, address, selectContractNames.value)) - } else { - self._deps.logCallback(`creation of ${selectedContract.name} errored: ${error}`) - } - }) - } - - // DEPLOY INSTANCE - function createInstance (args, compiler) { - var selectedContract = getSelectedContract() + this._view.instanceContainer.appendChild(this._view.instanceContainerTitle) + this._view.instanceContainer.appendChild(this._view.noInstancesText) - if (selectedContract.contract.object.evm.bytecode.object.length === 0) { - modalDialogCustom.alert('This contract may be abstract, not implement an abstract parent\'s methods completely or not invoke an inherited contract\'s constructor correctly.') - return - } - - var forceSend = () => { - var constructor = txHelper.getConstructorInterface(selectedContract.contract.object.abi) - self._deps.filePanel.compilerMetadata().deployMetadataOf(selectedContract.name, (error, contractMetadata) => { - if (error) return self._deps.logCallback(`creation of ${selectedContract.name} errored: ` + error) - if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { - txFormat.buildData(selectedContract.name, selectedContract.contract.object, compiler.getContracts(), true, constructor, args, (error, data) => { - if (error) return self._deps.logCallback(`creation of ${selectedContract.name} errored: ` + error) - createInstanceCallback(selectedContract, data) - }, (msg) => { - self._deps.logCallback(msg) - }, (data, runTxCallback) => { - // called for libraries deployment - self._deps.udapp.runTx(data, runTxCallback) - }) - } else { - if (Object.keys(selectedContract.contract.object.evm.bytecode.linkReferences).length) self._deps.logCallback(`linking ${JSON.stringify(selectedContract.contract.object.evm.bytecode.linkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`) - txFormat.encodeConstructorCallAndLinkLibraries(selectedContract.contract.object, args, constructor, contractMetadata.linkReferences, selectedContract.contract.object.evm.bytecode.linkReferences, (error, data) => { - if (error) return self._deps.logCallback(`creation of ${selectedContract.name} errored: ` + error) - createInstanceCallback(selectedContract, data) - }) - } - }) - } - - if (selectedContract.contract.object.evm.deployedBytecode && selectedContract.contract.object.evm.deployedBytecode.object.length / 2 > 24576) { - modalDialog('Contract code size over limit', yo`
Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails.
- More info: eip-170 -
`, - { - label: 'Force Send', - fn: () => { - forceSend() - }}, { - label: 'Cancel', - fn: () => { - self._deps.logCallback(`creation of ${selectedContract.name} canceled by user.`) - } - }) - } else { - forceSend() - } - } - - // ACCESS DEPLOYED INSTANCE - function loadFromAddress () { - var noInstancesText = self._view.noInstancesText - if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) } - var address = atAddressButtonInput.value - if (!ethJSUtil.isValidAddress(address)) { - return modalDialogCustom.alert('Invalid address.') - } - if (/[a-f]/.test(address) && /[A-F]/.test(address) && !ethJSUtil.isValidChecksumAddress(address)) { - return modalDialogCustom.alert('Invalid checksum address.') - } - if (/.(.abi)$/.exec(self._deps.config.get('currentFile'))) { - modalDialogCustom.confirm(null, 'Do you really want to interact with ' + address + ' using the current ABI definition ?', () => { - var abi - try { - abi = JSON.parse(self._deps.editor.currentContent()) - } catch (e) { - return modalDialogCustom.alert('Failed to parse the current file as JSON ABI.') - } - instanceContainer.appendChild(self._deps.udappUI.renderInstanceFromABI(abi, address, address)) - }) - } else { - var selectedContract = getSelectedContract() - instanceContainer.appendChild(self._deps.udappUI.renderInstance(selectedContract.contract.object, address, selectContractNames.value)) - } - } - - // GET NAMES OF ALL THE CONTRACTS - function getContractNames (success, data, compiler, compilerFullName) { - var contractNames = document.querySelector(`.${css.contractNames.classNames[0]}`) - contractNames.innerHTML = '' - if (success) { - selectContractNames.removeAttribute('disabled') - compiler.visitContracts((contract) => { - contractNames.appendChild(yo``) - }) - } else { - selectContractNames.setAttribute('disabled', true) - } - setInputParamsPlaceHolder() - } - - return el -} -/* ------------------------------------------------ - section SETTINGS: Environment, Account, Gas, Value ------------------------------------------------- */ -function settings (container, self) { - // VARIABLES - var net = yo`` - var networkcallid = 0 - const updateNetwork = (cb) => { - networkcallid++ - (function (callid) { - executionContext.detectNetwork((err, { id, name } = {}) => { - if (networkcallid > callid) return - networkcallid++ - if (err) { - console.error(err) - net.innerHTML = 'can\'t detect network ' - } else { - net.innerHTML = ` ${name} (${id || '-'})` - } - if (cb) cb(err, {id, name}) - }) - })(networkcallid) - } - var environmentEl = yo` -
-
- Environment -
-
- ${net} - - -
-
- ` - var accountEl = yo` -
-
- Account - -
-
- - ${copyToClipboard(() => document.querySelector('#runTabView #txorigin').value)} - -
-
- ` - var gasPriceEl = yo` -
-
Gas limit
- -
- ` - var valueEl = yo` -
-
Value
- - -
- ` - // DOM ELEMENT var el = yo` -
- ${environmentEl} - ${accountEl} - ${gasPriceEl} - ${valueEl} -
+
+ ${settingsUI.render()} + ${contractDropdownUI.render()} + ${recorderCard.render()} + ${self._view.instanceContainer} +
` - // HELPER FUNCTIONS AND EVENTS - self._deps.udapp.event.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { - if (error) return - if (!lookupOnly) el.querySelector('#value').value = '0' - updateAccountBalances(container, self) - }) - - // DROPDOWN - var selectExEnv = environmentEl.querySelector('#selectExEnvOptions') - - function setFinalContext () { - // set the final context. Cause it is possible that this is not the one we've originaly selected - selectExEnv.value = executionContext.getProvider() - self.event.trigger('clearInstance', []) - updateNetwork() - fillAccountsList(el, self) - } - - self.event.register('clearInstance', () => { - var instanceContainer = self._view.instanceContainer - var instanceContainerTitle = self._view.instanceContainerTitle - instanceContainer.innerHTML = '' // clear the instances list - instanceContainer.appendChild(instanceContainerTitle) - instanceContainer.appendChild(self._view.noInstancesText) - }) - - executionContext.event.register('addProvider', (network) => { - selectExEnv.appendChild(yo``) - tootip(`${network.name} [${network.url}] added`) - }) - - executionContext.event.register('removeProvider', (name) => { - var env = selectExEnv.querySelector(`option[value="${name}"]`) - if (env) { - selectExEnv.removeChild(env) - tootip(`${name} removed`) - } - }) - - selectExEnv.addEventListener('change', function (event) { - let context = selectExEnv.options[selectExEnv.selectedIndex].value - executionContext.executionContextChange(context, null, () => { - modalDialogCustom.confirm(null, 'Are you sure you want to connect to an ethereum node?', () => { - modalDialogCustom.prompt(null, 'Web3 Provider Endpoint', 'http://localhost:8545', (target) => { - executionContext.setProviderFromEndpoint(target, context, (alertMsg) => { - if (alertMsg) { - modalDialogCustom.alert(alertMsg) - } - setFinalContext() - }) - }, setFinalContext) - }, setFinalContext) - }, (alertMsg) => { - modalDialogCustom.alert(alertMsg) - }, setFinalContext) - }) - - selectExEnv.value = executionContext.getProvider() - executionContext.event.register('contextChanged', (context, silent) => { - setFinalContext() - }) - - setInterval(() => { - updateNetwork() - fillAccountsList(el, self) - }, 5000) - - setInterval(() => { - updateAccountBalances(container, self) - }, 10000) - - function newAccount () { - self._deps.udapp.newAccount('', (error, address) => { - if (!error) { - addTooltip(`account ${address} created`) - } else { - addTooltip('Cannot create an account: ' + error) - } - }) - } - function signMessage (event) { - self._deps.udapp.getAccounts((err, accounts) => { - if (err) { addTooltip(`Cannot get account list: ${err}`) } - var signMessageDialog = { 'title': 'Sign a message', 'text': 'Enter a message to sign', 'inputvalue': 'Message to sign' } - var $txOrigin = container.querySelector('#txorigin') - var account = $txOrigin.selectedOptions[0].value - var isVM = executionContext.isVM() - var isInjected = executionContext.getProvider() === 'injected' - function alertSignedData (error, hash, signedData) { - if (error && error.message !== '') { - console.log(error) - addTooltip(error.message) - } else { - modalDialogCustom.alert(yo`
hash:${hash}
signature:${signedData}
`) - } - } - if (isVM) { - modalDialogCustom.promptMulti(signMessageDialog, (message) => { - const personalMsg = ethJSUtil.hashPersonalMessage(Buffer.from(message)) - var privKey = self._deps.udapp.accounts[account].privateKey - try { - var rsv = ethJSUtil.ecsign(personalMsg, privKey) - var signedData = ethJSUtil.toRpcSig(rsv.v, rsv.r, rsv.s) - alertSignedData(null, '0x' + personalMsg.toString('hex'), signedData) - } catch (e) { - addTooltip(e.message) - return - } - }, false) - } else if (isInjected) { - modalDialogCustom.promptMulti(signMessageDialog, (message) => { - const hashedMsg = executionContext.web3().sha3(message) - try { - executionContext.web3().eth.sign(account, hashedMsg, (error, signedData) => { - alertSignedData(error, hashedMsg, signedData) - }) - } catch (e) { - addTooltip(e.message) - console.log(e) - return - } - }) - } else { - modalDialogCustom.promptPassphrase('Passphrase to sign a message', 'Enter your passphrase for this account to sign the message', '', (passphrase) => { - modalDialogCustom.promptMulti(signMessageDialog, (message) => { - const hashedMsg = executionContext.web3().sha3(message) - try { - var personal = new Personal(executionContext.web3().currentProvider) - personal.sign(hashedMsg, account, passphrase, (error, signedData) => { - alertSignedData(error, hashedMsg, signedData) - }) - } catch (e) { - addTooltip(e.message) - console.log(e) - return - } - }) - }, false) - } - }) - } + container.appendChild(el) - return el + return { render () { return container } } } module.exports = runTab diff --git a/src/app/tabs/runTab/contractDropdown.js b/src/app/tabs/runTab/contractDropdown.js new file mode 100644 index 0000000000..5d4fd60aec --- /dev/null +++ b/src/app/tabs/runTab/contractDropdown.js @@ -0,0 +1,200 @@ +var yo = require('yo-yo') +var css = require('../styles/run-tab-styles') +var modalDialogCustom = require('../../ui/modal-dialog-custom') +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var confirmDialog = require('../../execution/confirmDialog') +var modalDialog = require('../../ui/modaldialog') +var MultiParamManager = require('../../../multiParamManager') + +class ContractDropdownUI { + constructor (dropdownLogic, logCallback) { + this.dropdownLogic = dropdownLogic + this.logCallback = logCallback + this.event = new EventManager() + + this.listenToEvents() + } + + listenToEvents () { + this.dropdownLogic.event.register('newlyCompiled', (success, data, source, compiler, compilerFullName) => { + var contractNames = document.querySelector(`.${css.contractNames.classNames[0]}`) + contractNames.innerHTML = '' + if (success) { + this.selectContractNames.removeAttribute('disabled') + this.dropdownLogic.getCompiledContracts(compiler, compilerFullName).forEach((contract) => { + contractNames.appendChild(yo``) + }) + } else { + this.selectContractNames.setAttribute('disabled', true) + } + this.setInputParamsPlaceHolder() + + if (success) { + this.compFails.style.display = 'none' + document.querySelector(`.${css.contractNames}`).classList.remove(css.contractNamesError) + } else { + this.compFails.style.display = 'block' + document.querySelector(`.${css.contractNames}`).classList.add(css.contractNamesError) + } + }) + + this.dropdownLogic.event.register('currentFileChanged', this.changeCurrentFile.bind(this)) + } + + render () { + this.compFails = yo`` + var info = yo`` + + this.atAddressButtonInput = yo`` + this.selectContractNames = yo`` + + this.createPanel = yo`
` + this.orLabel = yo`
or
` + var el = yo` +
+
+ ${this.selectContractNames} ${this.compFails} ${info} +
+
+ ${this.createPanel} + ${this.orLabel} +
+
At Address
+ ${this.atAddressButtonInput} +
+
+
+ ` + this.selectContractNames.addEventListener('change', this.setInputParamsPlaceHolder.bind(this)) + + return el + } + + changeCurrentFile (currentFile) { + document.querySelector(`.${css.contractNames}`).classList.remove(css.contractNamesError) + var contractNames = document.querySelector(`.${css.contractNames.classNames[0]}`) + contractNames.innerHTML = '' + if (/.(.abi)$/.exec(currentFile)) { + this.createPanel.style.display = 'none' + this.orLabel.style.display = 'none' + this.compFails.style.display = 'none' + contractNames.appendChild(yo``) + this.selectContractNames.setAttribute('disabled', true) + } else if (/.(.sol)$/.exec(currentFile)) { + this.createPanel.style.display = 'block' + this.orLabel.style.display = 'block' + } + } + + setInputParamsPlaceHolder () { + this.createPanel.innerHTML = '' + if (this.selectContractNames.selectedIndex < 0 || this.selectContractNames.children.length <= 0) { + this.createPanel.innerHTML = 'No compiled contracts' + return + } + + var selectedContract = this.getSelectedContract() + var createConstructorInstance = new MultiParamManager(0, selectedContract.getConstructorInterface(), (valArray, inputsValues) => { + this.createInstance(inputsValues) + }, selectedContract.getConstructorInputs(), 'Deploy', selectedContract.bytecodeObject) + this.createPanel.appendChild(createConstructorInstance.render()) + } + + getSelectedContract () { + var contract = this.selectContractNames.children[this.selectContractNames.selectedIndex] + var contractName = contract.innerHTML + var compilerAtributeName = contract.getAttribute('compiler') + + return this.dropdownLogic.getSelectedContract(contractName, compilerAtributeName) + } + + createInstance (args) { + var selectedContract = this.getSelectedContract() + + if (selectedContract.bytecodeObject.length === 0) { + return modalDialogCustom.alert('This contract may be abstract, not implement an abstract parent\'s methods completely or not invoke an inherited contract\'s constructor correctly.') + } + + var continueCb = (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() + } + } + + var promptCb = (okCb, cancelCb) => { + modalDialogCustom.promptPassphrase(null, 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) + } + + var statusCb = (msg) => { + return this.logCallback(msg) + } + + var finalCb = (error, contractObject, address) => { + this.event.trigger('clearInstance') + + if (error) { + return this.logCallback(error) + } + + this.event.trigger('newContractInstanceAdded', [contractObject, address, this.selectContractNames.value]) + } + + if (selectedContract.isOverSizeLimit()) { + return modalDialog('Contract code size over limit', yo`
Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails.
+ More info: eip-170 +
`, + { + label: 'Force Send', + fn: () => { + this.dropdownLogic.forceSend(selectedContract, args, continueCb, promptCb, modalDialogCustom, confirmDialog, statusCb, finalCb) + }}, { + label: 'Cancel', + fn: () => { + this.logCallback(`creation of ${selectedContract.name} canceled by user.`) + } + }) + } + this.dropdownLogic.forceSend(selectedContract, args, continueCb, promptCb, modalDialogCustom, confirmDialog, statusCb, finalCb) + } + + loadFromAddress () { + this.event.trigger('clearInstance') + + var address = this.atAddressButtonInput.value + this.dropdownLogic.loadContractFromAddress(address, + (cb) => { + modalDialogCustom.confirm(null, 'Do you really want to interact with ' + address + ' using the current ABI definition ?', cb) + }, + (error, loadType, abi) => { + if (error) { + return modalDialogCustom.alert(error) + } + if (loadType === 'abi') { + return this.event.trigger('newContractABIAdded', [abi, address]) + } + var selectedContract = this.getSelectedContract() + this.event.trigger('newContractInstanceAdded', [selectedContract.object, address, this.selectContractNames.value]) + } + ) + } + +} + +module.exports = ContractDropdownUI diff --git a/src/app/tabs/runTab/model/dropdownlogic.js b/src/app/tabs/runTab/model/dropdownlogic.js new file mode 100644 index 0000000000..f05b5a9ef8 --- /dev/null +++ b/src/app/tabs/runTab/model/dropdownlogic.js @@ -0,0 +1,295 @@ +var ethJSUtil = require('ethereumjs-util') +var remixLib = require('remix-lib') +var txHelper = remixLib.execution.txHelper +var txFormat = remixLib.execution.txFormat +var executionContext = remixLib.execution.executionContext +var typeConversion = remixLib.execution.typeConversion +var txExecution = remixLib.execution.txExecution +var CompilerAbstract = require('../../../compiler/compiler-abstract') +var EventManager = remixLib.EventManager + +class DropdownLogic { + constructor (fileManager, pluginManager, compilersArtefacts, compiler, config, editor, udapp, filePanel) { + this.pluginManager = pluginManager + this.compilersArtefacts = compilersArtefacts + this.compiler = compiler + this.config = config + this.editor = editor + this.udapp = udapp + this.filePanel = filePanel + + this.event = new EventManager() + + this.listenToCompilationEvents() + + fileManager.event.register('currentFileChanged', (currentFile) => { + this.event.trigger('currentFileChanged', [currentFile]) + }) + } + + listenToCompilationEvents () { + this.pluginManager.event.register('sendCompilationResult', (file, source, languageVersion, data) => { + // TODO check whether the tab is configured + let compiler = new CompilerAbstract(languageVersion, data, source) + this.compilersArtefacts[languageVersion] = compiler + this.compilersArtefacts['__last'] = compiler + this.event.trigger('newlyCompiled', [true, data, source, compiler, languageVersion]) + }) + } + + loadContractFromAddress (address, confirmCb, cb) { + if (!ethJSUtil.isValidAddress(address)) { + return cb('Invalid address.') + } + if (/[a-f]/.test(address) && /[A-F]/.test(address) && !ethJSUtil.isValidChecksumAddress(address)) { + return cb('Invalid checksum address.') + } + if (/.(.abi)$/.exec(this.config.get('currentFile'))) { + confirmCb(() => { + var abi + try { + abi = JSON.parse(this.editor.currentContent()) + } catch (e) { + return cb('Failed to parse the current file as JSON ABI.') + } + cb(null, 'abi', abi) + }) + } + cb(null, 'instance') + } + + getCompiledContracts (compiler, compilerFullName) { + var contracts = [] + compiler.visitContracts((contract) => { + contracts.push(contract) + }) + return contracts + } + + getSelectedContract (contractName, compilerAtributeName) { + if (!contractName) return null + + var compiler = this.compilersArtefacts[compilerAtributeName] + if (!compiler) return null + + var contract = compiler.getContract(contractName) + + return { + name: contractName, + contract: contract, + compiler: compiler, + abi: contract.object.abi, + bytecodeObject: contract.object.evm.bytecode.object, + bytecodeLinkReferences: contract.object.evm.bytecode.linkReferences, + object: contract.object, + deployedBytecode: contract.object.evm.deployedBytecode, + getConstructorInterface: () => { + return txHelper.getConstructorInterface(contract.object.abi) + }, + getConstructorInputs: () => { + var constructorInteface = txHelper.getConstructorInterface(contract.object.abi) + return txHelper.inputParametersDeclarationToString(constructorInteface.inputs) + }, + isOverSizeLimit: () => { + var deployedBytecode = contract.object.evm.deployedBytecode + return (deployedBytecode && deployedBytecode.object.length / 2 > 24576) + } + } + } + + fromWei (value, doTypeConversion, unit) { + if (doTypeConversion) { + return executionContext.web3().fromWei(typeConversion.toInt(value), unit || 'ether') + } + return executionContext.web3().fromWei(value.toString(10), unit || 'ether') + } + + toWei (value, unit) { + return executionContext.web3().toWei(value, unit || 'gwei') + } + + calculateFee (gas, gasPrice, unit) { + return executionContext.web3().toBigNumber(gas).mul(executionContext.web3().toBigNumber(executionContext.web3().toWei(gasPrice.toString(10), unit || 'gwei'))) + } + + getGasPrice (cb) { + return executionContext.web3().eth.getGasPrice(cb) + } + + isVM () { + return executionContext.isVM() + } + + // TODO: check if selectedContract and data can be joined + createContract (selectedContract, data, continueCb, promptCb, confirmDialog, modalDialog, finalCb) { + if (data) { + data.contractName = selectedContract.name + data.linkReferences = selectedContract.bytecodeLinkReferences + data.contractABI = selectedContract.abi + } + + 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.udapp.createContract(data, confirmationCb, continueCb, promptCb, + (error, txResult) => { + if (error) { + return finalCb(`creation of ${selectedContract.name} errored: ${error}`) + } + var isVM = executionContext.isVM() + if (isVM) { + var vmError = txExecution.checkVMError(txResult) + if (vmError.error) { + return finalCb(vmError.message) + } + } + if (txResult.result.status && txResult.result.status === '0x0') { + return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`) + } + var address = isVM ? txResult.result.createdAddress : txResult.result.contractAddress + finalCb(null, selectedContract, address) + } + ) + } + + runTransaction (data, continueCb, promptCb, modalDialog, confirmDialog, finalCb) { + var confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { + if (network.name !== 'Main') { + return continueTxExecution(null) + } + var amount = this.fromWei(tx.value, true, 'ether') + var content = confirmDialog(tx, amount, gasEstimation, null, + (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 = this.calculateFee(tx.gas, gasPrice) + txFeeText = ' ' + this.fromWei(fee, false, 'ether') + ' Ether' + priceStatus = true + } catch (e) { + txFeeText = ' Please fix this issue before sending any transaction. ' + e.message + priceStatus = false + } + cb(txFeeText, priceStatus) + }, + (cb) => { + this.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 = this.fromWei(gasPrice, false, '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 = this.toWei(content.querySelector('#gasprice').value, 'gwei') + continueTxExecution(gasPrice) + } + }}, { + label: 'Cancel', + fn: () => { + return cancelCb('Transaction canceled by user.') + } + } + ) + } + + this.udapp.runTx(data, confirmationCb, continueCb, promptCb, finalCb) + } + + forceSend (selectedContract, args, continueCb, promptCb, modalDialog, confirmDialog, statusCb, cb) { + var constructor = selectedContract.getConstructorInterface() + // TODO: deployMetadataOf can be moved here + this.filePanel.compilerMetadata().deployMetadataOf(selectedContract.name, (error, contractMetadata) => { + if (error) return statusCb(`creation of ${selectedContract.name} errored: ` + error) + if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { + return txFormat.buildData(selectedContract.name, selectedContract.object, this.compilersArtefacts['__last'].getData().contracts, true, constructor, args, (error, data) => { + if (error) return statusCb(`creation of ${selectedContract.name} errored: ` + error) + + statusCb(`creation of ${selectedContract.name} pending...`) + this.createContract(selectedContract, data, continueCb, promptCb, modalDialog, confirmDialog, cb) + }, statusCb, (data, runTxCallback) => { + // called for libraries deployment + this.runTransaction(data, continueCb, promptCb, modalDialog, confirmDialog, runTxCallback) + }) + } + if (Object.keys(selectedContract.bytecodeLinkReferences).length) statusCb(`linking ${JSON.stringify(selectedContract.bytecodeLinkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`) + txFormat.encodeConstructorCallAndLinkLibraries(selectedContract.object, args, constructor, contractMetadata.linkReferences, selectedContract.bytecodeLinkReferences, (error, data) => { + if (error) return statusCb(`creation of ${selectedContract.name} errored: ` + error) + + statusCb(`creation of ${selectedContract.name} pending...`) + this.createContract(selectedContract, data, continueCb, promptCb, modalDialog, confirmDialog, cb) + }) + }) + } + +} + +module.exports = DropdownLogic diff --git a/src/recorder.js b/src/app/tabs/runTab/model/recorder.js similarity index 50% rename from src/recorder.js rename to src/app/tabs/runTab/model/recorder.js index 32ac7b648e..747aab6e5a 100644 --- a/src/recorder.js +++ b/src/app/tabs/runTab/model/recorder.js @@ -1,11 +1,12 @@ -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 async = require('async') -var modal = require('./app/ui/modal-dialog-custom') +var typeConversion = remixLib.execution.typeConversion +var helper = require('../../../../lib/helper.js') /** * Record transaction as long as the user create them. @@ -13,13 +14,15 @@ var modal = require('./app/ui/modal-dialog-custom') * */ 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 @@ -52,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 @@ -61,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 @@ -172,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 */ @@ -204,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 } @@ -224,49 +227,153 @@ 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, function (err, txResult) { - if (err) { - console.error(err) - self.logCallBack(err + '. Execution failed at ' + index) - } else { - var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress - if (address) { - address = 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 - newContractFn(abi, address, record.contractName) + self.udapp.runTx(record, confirmationCb, continueCb, promptCb, + function (err, txResult) { + if (err) { + console.error(err) + logCallBack(err + '. Execution failed at ' + index) + } else { + var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress + if (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 + newContractFn(abi, address, record.contractName) + } } + cb(err) } - cb(err) - }) + ) }, () => { 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) + }) + }) + } + + 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) + }) + }) } - return address + } module.exports = Recorder diff --git a/src/app/tabs/runTab/model/settings.js b/src/app/tabs/runTab/model/settings.js new file mode 100644 index 0000000000..5d961c077c --- /dev/null +++ b/src/app/tabs/runTab/model/settings.js @@ -0,0 +1,117 @@ +var ethJSUtil = require('ethereumjs-util') +var Personal = require('web3-eth-personal') +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var executionContext = remixLib.execution.executionContext + +class Settings { + + constructor (udapp) { + this.udapp = udapp + this.event = new EventManager() + + this.udapp.event.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { + this.event.trigger('transactionExecuted', [error, from, to, data, lookupOnly, txResult]) + }) + + executionContext.event.register('contextChanged', (context, silent) => { + this.event.trigger('contextChanged', [context, silent]) + }) + + executionContext.event.register('addProvider', (network) => { + this.event.trigger('addProvider', [network]) + }) + + executionContext.event.register('removeProvider', (name) => { + this.event.trigger('removeProvider', [name]) + }) + + this.networkcallid = 0 + } + + changeExecutionContext (context, cb) { + return executionContext.executionContextChange(context, null, cb) + } + + setProviderFromEndpoint (target, context, cb) { + return executionContext.setProviderFromEndpoint(target, context, cb) + } + + getProvider () { + return executionContext.getProvider() + } + + getAccountBalanceForAddress (address, cb) { + return this.udapp.getBalanceInEther(address, cb) + } + + updateNetwork (cb) { + this.networkcallid++ + ((callid) => { + executionContext.detectNetwork((err, { id, name } = {}) => { + if (this.networkcallid > callid) return + this.networkcallid++ + if (err) { + return cb(err) + } + cb(null, {id, name}) + }) + })(this.networkcallid) + } + + newAccount (passphraseCb, cb) { + return this.udapp.newAccount('', passphraseCb, cb) + } + + getAccounts (cb) { + return this.udapp.getAccounts(cb) + } + + isWeb3Provider () { + var isVM = executionContext.isVM() + var isInjected = executionContext.getProvider() === 'injected' + return (!isVM && !isInjected) + } + + signMessage (message, account, passphrase, cb) { + var isVM = executionContext.isVM() + var isInjected = executionContext.getProvider() === 'injected' + + if (isVM) { + const personalMsg = ethJSUtil.hashPersonalMessage(Buffer.from(message)) + var privKey = this.udapp.accounts[account].privateKey + try { + var rsv = ethJSUtil.ecsign(personalMsg, privKey) + var signedData = ethJSUtil.toRpcSig(rsv.v, rsv.r, rsv.s) + cb(null, '0x' + personalMsg.toString('hex'), signedData) + } catch (e) { + cb(e.message) + } + return + } + if (isInjected) { + const hashedMsg = executionContext.web3().sha3(message) + try { + executionContext.web3().eth.sign(account, hashedMsg, (error, signedData) => { + cb(error, hashedMsg, signedData) + }) + } catch (e) { + cb(e.message) + } + return + } + + const hashedMsg = executionContext.web3().sha3(message) + try { + var personal = new Personal(executionContext.web3().currentProvider) + personal.sign(hashedMsg, account, passphrase, (error, signedData) => { + cb(error, hashedMsg, signedData) + }) + } catch (e) { + cb(e.message) + } + } + +} + +module.exports = Settings diff --git a/src/app/tabs/runTab/recorder.js b/src/app/tabs/runTab/recorder.js new file mode 100644 index 0000000000..c14f221efc --- /dev/null +++ b/src/app/tabs/runTab/recorder.js @@ -0,0 +1,90 @@ +var yo = require('yo-yo') +var csjs = require('csjs-inject') +var css = require('../styles/run-tab-styles') + +var modalDialogCustom = require('../../ui/modal-dialog-custom') +var modalDialog = require('../../ui/modaldialog') +var confirmDialog = require('../../execution/confirmDialog') + +class RecorderUI { + + constructor (recorder, parentSelf) { + this.recorder = recorder + this.parentSelf = parentSelf + this.logCallBack = this.parentSelf._deps.logCallback + } + + render () { + var css2 = csjs` + .container {} + .runTxs {} + .recorder {} + ` + + this.runButton = yo`` + this.recordButton = yo` + ` + + this.runButton.onclick = this.runScenario.bind(this) + } + + runScenario () { + var continueCb = (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() + } + } + + 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 () { + 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) + } + ) + } + +} + +module.exports = RecorderUI diff --git a/src/app/tabs/runTab/settings.js b/src/app/tabs/runTab/settings.js new file mode 100644 index 0000000000..2a60b0f91e --- /dev/null +++ b/src/app/tabs/runTab/settings.js @@ -0,0 +1,264 @@ +var $ = require('jquery') +var yo = require('yo-yo') +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var css = require('../styles/run-tab-styles') +var copyToClipboard = require('../../ui/copy-to-clipboard') +var modalDialogCustom = require('../../ui/modal-dialog-custom') +var addTooltip = require('../../ui/tooltip') +var helper = require('../../../lib/helper.js') + +class SettingsUI { + + constructor (settings) { + this.settings = settings + this.event = new EventManager() + + this.settings.event.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { + if (error) return + if (!lookupOnly) this.el.querySelector('#value').value = '0' + this.updateAccountBalances() + }) + + setInterval(() => { + this.updateAccountBalances() + }, 10 * 1000) + + this.accountListCallId = 0 + this.loadedAccounts = {} + } + + updateAccountBalances () { + if (!this.el) return + var accounts = $(this.el.querySelector('#txorigin')).children('option') + accounts.each((index, account) => { + this.settings.getAccountBalanceForAddress(account.value, (err, balance) => { + if (err) return + account.innerText = helper.shortenAddress(account.value, balance) + }) + }) + } + + render () { + this.netUI = yo`` + + var environmentEl = yo` +
+
+ Environment +
+
+ ${this.netUI} + + +
+
+ ` + + var accountEl = yo` +
+
+ Account + +
+
+ + ${copyToClipboard(() => document.querySelector('#runTabView #txorigin').value)} + +
+
+ ` + + var gasPriceEl = yo` +
+
Gas limit
+ +
+ ` + + var valueEl = yo` +
+
Value
+ + +
+ ` + + var el = yo` +
+ ${environmentEl} + ${accountEl} + ${gasPriceEl} + ${valueEl} +
+ ` + + var selectExEnv = environmentEl.querySelector('#selectExEnvOptions') + this.setDropdown(selectExEnv) + + this.settings.event.register('contextChanged', (context, silent) => { + this.setFinalContext() + }) + + setInterval(() => { + this.updateNetwork() + this.fillAccountsList() + }, 5000) + + this.el = el + return el + } + + setDropdown (selectExEnv) { + this.selectExEnv = selectExEnv + + this.settings.event.register('addProvider', (network) => { + selectExEnv.appendChild(yo``) + addTooltip(`${network.name} [${network.url}] added`) + }) + + this.settings.event.register('removeProvider', (name) => { + var env = selectExEnv.querySelector(`option[value="${name}"]`) + if (env) { + selectExEnv.removeChild(env) + addTooltip(`${name} removed`) + } + }) + + selectExEnv.addEventListener('change', (event) => { + let context = selectExEnv.options[selectExEnv.selectedIndex].value + this.settings.changeExecutionContext(context, () => { + modalDialogCustom.confirm(null, 'Are you sure you want to connect to an ethereum node?', () => { + modalDialogCustom.prompt(null, 'Web3 Provider Endpoint', 'http://localhost:8545', (target) => { + this.settings.setProviderFromEndpoint(target, context, (alertMsg) => { + if (alertMsg) { + modalDialogCustom.alert(alertMsg) + } + this.setFinalContext() + }) + }, this.setFinalContext.bind(this)) + }, this.setFinalContext.bind(this)) + }, (alertMsg) => { + modalDialogCustom.alert(alertMsg) + }, this.setFinalContext.bind(this)) + }) + + selectExEnv.value = this.settings.getProvider() + } + + setFinalContext () { + // set the final context. Cause it is possible that this is not the one we've originaly selected + this.selectExEnv.value = this.settings.getProvider() + this.event.trigger('clearInstance', []) + this.updateNetwork() + this.fillAccountsList() + } + + newAccount () { + this.settings.newAccount( + (cb) => { + modalDialogCustom.promptPassphraseCreation((error, passphrase) => { + if (error) { + return modalDialogCustom.alert(error) + } + cb(passphrase) + }, () => {}) + }, + (error, address) => { + if (error) { + return addTooltip('Cannot create an account: ' + error) + } + addTooltip(`account ${address} created`) + } + ) + } + + signMessage () { + this.settings.getAccounts((err, accounts) => { + if (err) { + return addTooltip(`Cannot get account list: ${err}`) + } + + var signMessageDialog = { 'title': 'Sign a message', 'text': 'Enter a message to sign', 'inputvalue': 'Message to sign' } + var $txOrigin = this.el.querySelector('#txorigin') + var account = $txOrigin.selectedOptions[0].value + + var promptCb = (passphrase) => { + modalDialogCustom.promptMulti(signMessageDialog, (message) => { + this.settings.signMessage(message, account, passphrase, (err, msgHash, signedData) => { + if (err) { + return addTooltip(err) + } + modalDialogCustom.alert(yo`
hash:${msgHash}
signature:${signedData}
`) + }) + }, false) + } + + if (this.settings.isWeb3Provider()) { + return modalDialogCustom.promptPassphrase('Passphrase to sign a message', 'Enter your passphrase for this account to sign the message', '', promptCb, false) + } + promptCb() + }) + } + + updateNetwork () { + this.settings.updateNetwork((err, {id, name} = {}) => { + if (err) { + this.netUI.innerHTML = 'can\'t detect network ' + return + } + this.netUI.innerHTML = ` ${name} (${id || '-'})` + }) + } + + // TODO: unclear what's the goal of accountListCallId, feels like it can be simplified + fillAccountsList () { + this.accountListCallId++ + var callid = this.accountListCallId + var txOrigin = this.el.querySelector('#txorigin') + this.settings.getAccounts((err, accounts) => { + if (this.accountListCallId > callid) return + this.accountListCallId++ + if (err) { addTooltip(`Cannot get account list: ${err}`) } + for (var loadedaddress in this.loadedAccounts) { + if (accounts.indexOf(loadedaddress) === -1) { + txOrigin.removeChild(txOrigin.querySelector('option[value="' + loadedaddress + '"]')) + delete this.loadedAccounts[loadedaddress] + } + } + for (var i in accounts) { + var address = accounts[i] + if (!this.loadedAccounts[address]) { + txOrigin.appendChild(yo``) + this.loadedAccounts[address] = 1 + } + } + txOrigin.setAttribute('value', accounts[0]) + }) + } + +} + +module.exports = SettingsUI diff --git a/src/app/ui/util.js b/src/app/ui/util.js deleted file mode 100644 index 41f14324ec..0000000000 --- a/src/app/ui/util.js +++ /dev/null @@ -1,23 +0,0 @@ -var TreeView = require('./TreeView') -var ethJSUtil = require('ethereumjs-util') -var BN = ethJSUtil.BN -var remixLib = require('remix-lib') -var txFormat = remixLib.execution.txFormat - -module.exports = { - decodeResponseToTreeView: function (response, fnabi) { - var treeView = new TreeView({ - extractData: (item, parent, key) => { - var ret = {} - if (BN.isBN(item)) { - ret.self = item.toString(10) - ret.children = [] - } else { - ret = treeView.extractDataDefault(item, parent, key) - } - return ret - } - }) - return treeView.render(txFormat.decodeResponse(response, fnabi)) - } -} diff --git a/src/config.js b/src/config.js index 6a41ebd133..d5095b919b 100644 --- a/src/config.js +++ b/src/config.js @@ -53,6 +53,8 @@ function Config (storage) { return this.unpersistedItems[key] } + // TODO: this only used for *one* property "doNotShowTransactionConfirmationAgain" + // and can be removed once it's refactored away in txRunner this.setUnpersistedProperty = function (key, value) { this.unpersistedItems[key] = value } diff --git a/src/universal-dapp-ui.js b/src/universal-dapp-ui.js index a900ff2cd9..276abf5186 100644 --- a/src/universal-dapp-ui.js +++ b/src/universal-dapp-ui.js @@ -3,13 +3,48 @@ var $ = require('jquery') var yo = require('yo-yo') +var ethJSUtil = require('ethereumjs-util') +var BN = ethJSUtil.BN var helper = require('./lib/helper') var copyToClipboard = require('./app/ui/copy-to-clipboard') var css = require('./universal-dapp-styles') var MultiParamManager = require('./multiParamManager') +var remixLib = require('remix-lib') +var typeConversion = remixLib.execution.typeConversion +var txExecution = remixLib.execution.txExecution +var txFormat = remixLib.execution.txFormat -function UniversalDAppUI (udapp, opts = {}) { +var executionContext = require('./execution-context') + +var confirmDialog = require('./app/execution/confirmDialog') +var modalCustom = require('./app/ui/modal-dialog-custom') +var modalDialog = require('./app/ui/modaldialog') +var TreeView = require('./app/ui/TreeView') + +function UniversalDAppUI (udapp, registry) { this.udapp = udapp + this.registry = registry + + this.compilerData = {contractsDetails: {}} + this._deps = { + compilersartefacts: registry.get('compilersartefacts').api + } +} + +function decodeResponseToTreeView (response, fnabi) { + var treeView = new TreeView({ + extractData: (item, parent, key) => { + var ret = {} + if (BN.isBN(item)) { + ret.self = item.toString(10) + ret.children = [] + } else { + ret = treeView.extractDataDefault(item, parent, key) + } + return ret + } + }) + return treeView.render(txFormat.decodeResponse(response, fnabi)) } UniversalDAppUI.prototype.renderInstance = function (contract, address, contractName) { @@ -38,10 +73,8 @@ UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address ${copyToClipboard(() => address)}
` - if (self.udapp.removable_instances) { - var close = yo`
` - title.appendChild(close) - } + var close = yo`
` + title.appendChild(close) function remove () { instance.remove() @@ -92,9 +125,135 @@ UniversalDAppUI.prototype.getCallButton = function (args) { var outputOverride = yo`
` // show return value function clickButton (valArr, inputsValues) { - self.udapp.call(true, args, inputsValues, lookupOnly, (decoded) => { + var logMsg + if (!args.funABI.constant) { + logMsg = `transact to ${args.contractName}.${(args.funABI.name) ? args.funABI.name : '(fallback)'}` + } else { + logMsg = `call to ${args.contractName}.${(args.funABI.name) ? args.funABI.name : '(fallback)'}` + } + + var value = inputsValues + + var confirmationCb = (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.udapp, + (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: () => { + self.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.') + } + }) + } + + var continueCb = (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() + } + } + + var outputCb = (decoded) => { outputOverride.innerHTML = '' outputOverride.appendChild(decoded) + } + + var promptCb = (okCb, cancelCb) => { + modalCustom.promptPassphrase(null, 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) + } + + // contractsDetails is used to resolve libraries + txFormat.buildData(args.contractName, args.contractAbi, self._deps.compilersartefacts['__last'].getData().contracts, false, args.funABI, args.funABI.type !== 'fallback' ? value : '', (error, data) => { + if (!error) { + if (!args.funABI.constant) { + self.registry.get('logCallback').api(`${logMsg} pending ... `) + } else { + self.registry.get('logCallback').api(`${logMsg}`) + } + if (args.funABI.type === 'fallback') data.dataHex = value + self.udapp.callFunction(args.address, data, args.funABI, confirmationCb, continueCb, promptCb, (error, txResult) => { + if (!error) { + var isVM = executionContext.isVM() + if (isVM) { + var vmError = txExecution.checkVMError(txResult) + if (vmError.error) { + self.registry.get('logCallback').api(`${logMsg} errored: ${vmError.message} `) + return + } + } + if (lookupOnly) { + var decoded = decodeResponseToTreeView(executionContext.isVM() ? txResult.result.vm.return : ethJSUtil.toBuffer(txResult.result), args.funABI) + outputCb(decoded) + } + } else { + self.registry.get('logCallback').api(`${logMsg} errored: ${error} `) + } + }) + } else { + self.registry.get('logCallback').api(`${logMsg} errored: ${error} `) + } + }, (msg) => { + self.registry.get('logCallback').api(msg) + }, (data, runTxCallback) => { + // called for libraries deployment + self.udapp.runTx(data, confirmationCb, runTxCallback) }) } diff --git a/src/universal-dapp.js b/src/universal-dapp.js index 70a5b48adf..2262b576ac 100644 --- a/src/universal-dapp.js +++ b/src/universal-dapp.js @@ -1,42 +1,19 @@ -/* global */ -'use strict' - -var yo = require('yo-yo') var async = require('async') var ethJSUtil = require('ethereumjs-util') var BN = ethJSUtil.BN var remixLib = require('remix-lib') -var EventManager = require('./lib/events') var crypto = require('crypto') var TxRunner = remixLib.execution.txRunner -var txExecution = remixLib.execution.txExecution -var txFormat = remixLib.execution.txFormat var txHelper = remixLib.execution.txHelper -var executionContext = require('./execution-context') -var modalCustom = require('./app/ui/modal-dialog-custom') -var uiUtil = require('./app/ui/util') -var globalRegistry = require('./global/registry') - -var modalDialog = require('./app/ui/modaldialog') -var typeConversion = remixLib.execution.typeConversion -var confirmDialog = require('./app/execution/confirmDialog') +var EventManager = remixLib.EventManager +var executionContext = remixLib.execution.executionContext -function UniversalDApp (opts, localRegistry) { +function UniversalDApp (registry) { this.event = new EventManager() var self = this - self.data = {} - self._components = {} - self._components.registry = localRegistry || globalRegistry - self.removable = opts.removable - self.removable_instances = opts.removable_instances self._deps = { - config: self._components.registry.get('config').api, - compilersartefacts: self._components.registry.get('compilersartefacts').api, - logCallback: self._components.registry.get('logCallback').api + config: registry.get('config').api } - executionContext.event.register('contextChanged', this, function (context) { - self.resetEnvironment() - }) self._txRunnerAPI = { config: self._deps.config, detectNetwork: (cb) => { @@ -49,6 +26,14 @@ function UniversalDApp (opts, localRegistry) { self.txRunner = new TxRunner({}, self._txRunnerAPI) self.accounts = {} self.resetEnvironment() + executionContext.event.register('contextChanged', this.resetEnvironment.bind(this)) +} + +UniversalDApp.prototype.profile = function () { + return { + type: 'udapp', + methods: ['runTestTx', 'getAccounts', 'createVMAccount'] + } } UniversalDApp.prototype.profile = function () { @@ -68,13 +53,22 @@ UniversalDApp.prototype.resetEnvironment = function () { this._addAccount('71975fbf7fe448e004ac7ae54cad0a383c3906055a65468714156a07385e96ce', '0x56BC75E2D63100000') executionContext.vm().stateManager.cache.flush(function () {}) } - this.txRunner = new TxRunner(this.accounts, this._txRunnerAPI) + // 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) => { + executionContext.detectNetwork(cb) + }, + personalMode: () => { + return this.config.get('settings/personal-mode') + } + }) this.txRunner.event.register('transactionBroadcasted', (txhash) => { executionContext.detectNetwork((error, network) => { - if (!error && network) { - var txLink = executionContext.txDetailsLink(network.name, txhash) - if (txLink) this._deps.logCallback(yo`${txLink}`) - } + if (error || !network) return + this.event.trigger('transactionBroadcasted', [txhash, network.name]) }) }) } @@ -91,18 +85,14 @@ UniversalDApp.prototype.createVMAccount = function (privateKey, balance, cb) { cb(null, '0x' + ethJSUtil.privateToAddress(privateKey).toString('hex')) } -UniversalDApp.prototype.newAccount = function (password, cb) { +UniversalDApp.prototype.newAccount = function (password, passwordPromptCb, cb) { if (!executionContext.isVM()) { - if (!this._deps.config.get('settings/personal-mode')) { + if (!this.config.get('settings/personal-mode')) { return cb('Not running in personal mode') } - modalCustom.promptPassphraseCreation((error, passphrase) => { - if (error) { - modalCustom.alert(error) - } else { - executionContext.web3().personal.newAccount(passphrase, cb) - } - }, () => {}) + passwordPromptCb((passphrase) => { + executionContext.web3().personal.newAccount(passphrase, cb) + }) } else { var privateKey do { @@ -138,7 +128,7 @@ UniversalDApp.prototype.getAccounts = function (cb) { // Weirdness of web3: listAccounts() is sync, `getListAccounts()` is async // See: https://github.com/ethereum/web3.js/issues/442 if (this._deps.config.get('settings/personal-mode')) { - executionContext.web3().personal.getListAccounts(cb) + return executionContext.web3().personal.getListAccounts(cb) } else { executionContext.web3().eth.getAccounts(cb) } @@ -190,71 +180,18 @@ UniversalDApp.prototype.getBalanceInEther = function (address, callback) { }) } -UniversalDApp.prototype.pendingTransactions = function () { - return this.txRunner.pendingTxs -} - UniversalDApp.prototype.pendingTransactionsCount = function () { return Object.keys(this.txRunner.pendingTxs).length } -UniversalDApp.prototype.call = function (isUserAction, args, value, lookupOnly, outputCb) { - const self = this - var logMsg - if (isUserAction) { - if (!args.funABI.constant) { - logMsg = `transact to ${args.contractName}.${(args.funABI.name) ? args.funABI.name : '(fallback)'}` - } else { - logMsg = `call to ${args.contractName}.${(args.funABI.name) ? args.funABI.name : '(fallback)'}` - } - } - txFormat.buildData(args.contractName, args.contractAbi, self._deps.compilersartefacts['__last'].getData().contracts, false, args.funABI, args.funABI.type !== 'fallback' ? value : '', (error, data) => { - if (!error) { - if (isUserAction) { - if (!args.funABI.constant) { - self._deps.logCallback(`${logMsg} pending ... `) - } else { - self._deps.logCallback(`${logMsg}`) - } - } - if (args.funABI.type === 'fallback') data.dataHex = value - self.callFunction(args.address, data, args.funABI, (error, txResult) => { - if (!error) { - var isVM = executionContext.isVM() - if (isVM) { - var vmError = txExecution.checkVMError(txResult) - if (vmError.error) { - self._deps.logCallback(`${logMsg} errored: ${vmError.message} `) - return - } - } - if (lookupOnly) { - var decoded = uiUtil.decodeResponseToTreeView(executionContext.isVM() ? txResult.result.vm.return : ethJSUtil.toBuffer(txResult.result), args.funABI) - outputCb(decoded) - } - } else { - self._deps.logCallback(`${logMsg} errored: ${error} `) - } - }) - } else { - self._deps.logCallback(`${logMsg} errored: ${error} `) - } - }, (msg) => { - self._deps.logCallback(msg) - }, (data, runTxCallback) => { - // called for libraries deployment - self.runTx(data, runTxCallback) - }) -} - /** * deploy the given contract * * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). * @param {Function} callback - callback. */ -UniversalDApp.prototype.createContract = function (data, callback) { - this.runTx({data: data, useCall: false}, (error, txResult) => { +UniversalDApp.prototype.createContract = function (data, confirmationCb, continueCb, promptCb, callback) { + this.runTx({data: data, useCall: false}, confirmationCb, continueCb, promptCb, (error, txResult) => { // see universaldapp.js line 660 => 700 to check possible values of txResult (error case) callback(error, txResult) }) @@ -268,8 +205,8 @@ UniversalDApp.prototype.createContract = function (data, callback) { * @param {Object} funAbi - abi definition of the function to call. * @param {Function} callback - callback. */ -UniversalDApp.prototype.callFunction = function (to, data, funAbi, callback) { - this.runTx({to: to, data: data, useCall: funAbi.constant}, (error, txResult) => { +UniversalDApp.prototype.callFunction = function (to, data, funAbi, confirmationCb, continueCb, promptCb, callback) { + this.runTx({to: to, data: data, useCall: funAbi.constant}, confirmationCb, continueCb, promptCb, (error, txResult) => { // see universaldapp.js line 660 => 700 to check possible values of txResult (error case) callback(error, txResult) }) @@ -338,7 +275,7 @@ UniversalDApp.prototype.silentRunTx = function (tx, cb) { cb) } -UniversalDApp.prototype.runTx = function (args, cb) { +UniversalDApp.prototype.runTx = function (args, confirmationCb, continueCb, promptCb, cb) { const self = this async.waterfall([ function getGasLimit (next) { @@ -384,85 +321,7 @@ UniversalDApp.prototype.runTx = function (args, cb) { var timestamp = Date.now() self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad]) - self.txRunner.rawRun(tx, - - (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: () => { - self._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 ' + tx.from, '', okCb, cancelCb) - }, + self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb, function (error, result) { let eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted') self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad])