/* global prompt */ 'use strict' var $ = require('jquery') var ethJSUtil = require('ethereumjs-util') var ethJSABI = require('ethereumjs-abi') var BN = ethJSUtil.BN var EventManager = require('./lib/eventManager') var crypto = require('crypto') var async = require('async') var TxRunner = require('./app/txRunner') /* trigger debugRequested */ function UniversalDApp (executionContext, options, txdebugger) { this.event = new EventManager() var self = this self.options = options || {} self.$el = $('
') self.personalMode = self.options.personalMode || false self.contracts self.getAddress self.getValue self.getGasLimit self.txdebugger = txdebugger // temporary: will not be needed anymore when we'll add memory support to the VM var defaultRenderOutputModifier = function (name, content) { return content } self.renderOutputModifier = defaultRenderOutputModifier self.web3 = executionContext.web3() self.vm = executionContext.vm() self.executionContext = executionContext self.executionContext.event.register('contextChanged', this, function (context) { self.reset(self.contracts) }) self.txRunner = new TxRunner(executionContext, {}, { queueTxs: true, personalMode: this.personalMode }) } UniversalDApp.prototype.reset = function (contracts, getAddress, getValue, getGasLimit, renderer) { this.$el.empty() this.contracts = contracts this.getAddress = getAddress this.getValue = getValue this.getGasLimit = getGasLimit this.renderOutputModifier = renderer this.accounts = {} if (this.executionContext.isVM()) { this._addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511') this._addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c') this._addAccount('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a') this._addAccount('d74aa6d18aa79a05f3473dd030a97d3305737cbc8337d940344345c1f6b72eea') this._addAccount('71975fbf7fe448e004ac7ae54cad0a383c3906055a65468714156a07385e96ce') } this.txRunner = new TxRunner(this.executionContext, this.accounts, { queueTxs: true, personalMode: this.personalMode }) } UniversalDApp.prototype.newAccount = function (password) { if (!this.executionContext.isVM()) { if (!this.personalMode) { throw new Error('Not running in personal mode') } this.web3.personal.newAccount(password) } else { var privateKey do { privateKey = crypto.randomBytes(32) } while (!ethJSUtil.isValidPrivate(privateKey)) this._addAccount(privateKey) } } UniversalDApp.prototype._addAccount = function (privateKey, balance) { var self = this if (!self.executionContext.isVM()) { throw new Error('_addAccount() cannot be called in non-VM mode') } if (self.accounts) { privateKey = new Buffer(privateKey, 'hex') var address = ethJSUtil.privateToAddress(privateKey) // FIXME: we don't care about the callback, but we should still make this proper self.vm.stateManager.putAccountBalance(address, balance || 'f00000000000000001', function cb () {}) self.accounts['0x' + address.toString('hex')] = { privateKey: privateKey, nonce: 0 } } } UniversalDApp.prototype.getAccounts = function (cb) { var self = this if (!self.executionContext.isVM()) { // Weirdness of web3: listAccounts() is sync, `getListAccounts()` is async // See: https://github.com/ethereum/web3.js/issues/442 if (self.personalMode) { self.web3.personal.getListAccounts(cb) } else { self.web3.eth.getAccounts(cb) } } else { if (!self.accounts) { return cb('No accounts?') } cb(null, Object.keys(self.accounts)) } } UniversalDApp.prototype.getBalance = function (address, cb) { var self = this address = ethJSUtil.stripHexPrefix(address) if (!self.executionContext.isVM()) { self.web3.eth.getBalance(address, function (err, res) { if (err) { cb(err) } else { cb(null, res.toString(10)) } }) } else { if (!self.accounts) { return cb('No accounts?') } self.vm.stateManager.getAccountBalance(new Buffer(address, 'hex'), function (err, res) { if (err) { cb('Account not found') } else { cb(null, new BN(res).toString(10)) } }) } } UniversalDApp.prototype.render = function () { var self = this // NOTE: don't display anything if there are no contracts to display if (self.contracts.length === 0) { return self.$el } var $legend = $('
') .append($('
').text('Attach')) .append($('
').text('Transact')) .append($('
').text('Call')) self.$el.append($legend) for (var c in self.contracts) { var $contractEl = $('
') if (self.contracts[c].address) { self.getInstanceInterface(self.contracts[c], self.contracts[c].address, $contractEl) } else { var $title = $('').text(self.contracts[c].name) if (self.contracts[c].bytecode) { $title.append($('
').text((self.contracts[c].bytecode.length / 2) + ' bytes')) } $contractEl.append($title).append(self.getCreateInterface($contractEl, self.contracts[c])) } self.$el.append(self.renderOutputModifier(self.contracts[c].name, $contractEl)) } return self.$el } UniversalDApp.prototype.getContractByName = function (contractName) { var self = this for (var c in self.contracts) { if (self.contracts[c].name === contractName) { return self.contracts[c] } } return null } UniversalDApp.prototype.getCreateInterface = function ($container, contract) { var self = this var $createInterface = $('
') if (self.options.removable) { var $close = $('
') $close.click(function () { self.$el.remove() }) $createInterface.append($close) } var $atButton = $('') $button.click(function () { self.event.trigger('debugRequested', [result]) }) $debugTx.append($button) return $debugTx } var getDebugCall = function (result) { var $debugTx = $('
') var $button = $('') $button.click(function () { self.event.trigger('debugRequested', [result]) }) $debugTx.append($button) return $debugTx } var getGasUsedOutput = function (result, vmResult) { var $gasUsed = $('
') var caveat = lookupOnly ? '(caveat)' : '' var gas if (result.gasUsed) { gas = result.gasUsed.toString(10) $gasUsed.html('Transaction cost: ' + gas + ' gas. ' + caveat) } if (vmResult && vmResult.gasUsed) { var $callGasUsed = $('
') gas = vmResult.gasUsed.toString(10) $callGasUsed.append('Execution cost: ' + gas + ' gas.') $gasUsed.append($callGasUsed) } return $gasUsed } var getDecodedOutput = function (result) { var $decoded if (Array.isArray(result)) { $decoded = $('
    ') for (var i = 0; i < result.length; i++) { $decoded.append($('
  1. ').text(result[i])) } } else { $decoded = result } return $('
    ').html('Decoded: ').append($decoded) } var getOutput = function () { var $result = $('
    ') var $close = $('
    ') $close.click(function () { $result.remove() }) $result.append($close) return $result } var clearOutput = function ($result) { $(':not(.udapp-close)', $result).remove() } var replaceOutput = function ($result, message) { clearOutput($result) $result.append(message) } var handleCallButtonClick = function (ev, $result) { if (!$result) { $result = getOutput() if (lookupOnly && !inputs.length) { $outputOverride.empty().append($result) } else { outputSpan.append($result) } } var funArgs = '' try { funArgs = $.parseJSON('[' + inputField.val() + ']') } catch (e) { replaceOutput($result, $('').text('Error encoding arguments: ' + e)) return } var data = '' if (!isConstructor || funArgs.length > 0) { try { data = args.encode(funArgs) } catch (e) { replaceOutput($result, $('').text('Error encoding arguments: ' + e)) return } } if (data.slice(0, 9) === 'undefined') { data = data.slice(9) } if (data.slice(0, 2) === '0x') { data = data.slice(2) } replaceOutput($result, $('Waiting for transaction to be mined...')) if (isConstructor) { if (args.bytecode.indexOf('_') >= 0) { replaceOutput($result, $('Deploying and linking required libraries...')) self.linkBytecode(args.contractName, function (err, bytecode) { if (err) { replaceOutput($result, $('').text('Error deploying required libraries: ' + err)) } else { args.bytecode = bytecode handleCallButtonClick(ev, $result) } }) return } else { data = args.bytecode + data } } var decodeResponse = function (response) { // Only decode if there supposed to be fields if (args.abi.outputs && args.abi.outputs.length > 0) { try { var i var outputTypes = [] for (i = 0; i < args.abi.outputs.length; i++) { outputTypes.push(args.abi.outputs[i].type) } // decode data var decodedObj = ethJSABI.rawDecode(outputTypes, response) // format decoded data decodedObj = ethJSABI.stringify(outputTypes, decodedObj) for (i = 0; i < outputTypes.length; i++) { var name = args.abi.outputs[i].name if (name.length > 0) { decodedObj[i] = outputTypes[i] + ' ' + name + ': ' + decodedObj[i] } else { decodedObj[i] = outputTypes[i] + ': ' + decodedObj[i] } } return getDecodedOutput(decodedObj) } catch (e) { return getDecodedOutput('Failed to decode output: ' + e) } } } var decoded self.runTx({ to: args.address, data: data, useCall: args.abi.constant && !isConstructor }, function (err, txResult) { if (!txResult) { replaceOutput($result, $('').text('callback contain no result ' + err).addClass('error')) return } var result = txResult.result if (err) { replaceOutput($result, $('').text(err).addClass('error')) // VM only } else if (self.executionContext.isVM() && result.vm.exception === 0 && result.vm.exceptionError) { replaceOutput($result, $('').text('VM Exception: ' + result.vm.exceptionError).addClass('error')) $result.append(getDebugTransaction(txResult)) // VM only } else if (self.executionContext.isVM() && result.vm.return === undefined) { replaceOutput($result, $('').text('Exception during execution.').addClass('error')) $result.append(getDebugTransaction(txResult)) } else if (isConstructor) { replaceOutput($result, getGasUsedOutput(result, result.vm)) $result.append(getDebugTransaction(txResult)) args.appendFunctions(self.executionContext.isVM() ? result.createdAddress : result.contractAddress) } else if (self.executionContext.isVM()) { var outputObj = '0x' + result.vm.return.toString('hex') clearOutput($result) $result.append(getReturnOutput(outputObj)).append(getGasUsedOutput(result, result.vm)) decoded = decodeResponse(result.vm.return) if (decoded) { $result.append(decoded) } if (args.abi.constant) { $result.append(getDebugCall(txResult)) } else { $result.append(getDebugTransaction(txResult)) } } else if (args.abi.constant && !isConstructor) { clearOutput($result) $result.append(getReturnOutput(result)).append(getGasUsedOutput({})) decoded = decodeResponse(ethJSUtil.toBuffer(result)) if (decoded) { $result.append(decoded) } } else { clearOutput($result) $result.append(getReturnOutput(result)).append(getGasUsedOutput(result)) $result.append(getDebugTransaction(txResult)) } }) } var title if (isConstructor) { title = 'Create' } else if (args.abi.name) { title = args.abi.name } else { title = '(fallback)' } var button = $('