/* 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 = $('').text('At Address').click(function () { self.clickContractAt(self, $container.find('.createContract'), contract) }) $createInterface.append($atButton) var $newButton = self.getInstanceInterface(contract) if (!$newButton) { return $createInterface } $createInterface.append($newButton) // Only display creation interface for non-abstract contracts. // FIXME: maybe have a flag for this in the JSON? // FIXME: maybe fix getInstanceInterface() below for this case if (contract.bytecode.length === 0) { var $createButton = $newButton.find('.constructor .call') // NOTE: we must show the button to have CSS properly lined up $createButton.text('Create') $createButton.attr('disabled', 'disabled') $createButton.attr('title', 'This contract does not implement all functions and thus cannot be created.') } return $createInterface } UniversalDApp.prototype.getInstanceInterface = function (contract, address, $target) { var self = this var abi = JSON.parse(contract.interface).sort(function (a, b) { if (a.name > b.name) { return -1 } else { return 1 } }).sort(function (a, b) { if (a.constant === true) { return -1 } else { return 1 } }) var funABI = self.getConstructorInterface(abi) if (!funABI) { return } var $createInterface = $('') var appendFunctions = function (address, $el) { var $instance = $('') if (self.options.removable_instances) { var $close = $('') $close.click(function () { $instance.remove() }) $instance.append($close) } var context = self.executionContext.isVM() ? 'memory' : 'blockchain' address = (address.slice(0, 2) === '0x' ? '' : '0x') + address.toString('hex') var $title = $('').text(contract.name + ' at ' + address + ' (' + context + ')') $title.click(function () { $instance.toggleClass('hide') }) var $events = $('') var parseLogs = function (err, response) { if (err) { return } var $event = $('') var $close = $('') $close.click(function () { $event.remove() }) $event.append($('').text(response.event)) .append($('').text(JSON.stringify(response.args, null, 2))) .append($close) $events.append($event) } if (self.executionContext.isVM()) { // FIXME: support indexed events var eventABI = {} $.each(abi, function (i, funABI) { if (funABI.type !== 'event') { return } var hash = ethJSABI.eventID(funABI.name, funABI.inputs.map(function (item) { return item.type })) eventABI[hash.toString('hex')] = { event: funABI.name, inputs: funABI.inputs } }) self.vm.on('afterTx', function (response) { for (var i in response.vm.logs) { // [address, topics, mem] var log = response.vm.logs[i] var event var decoded try { var abi = eventABI[log[1][0].toString('hex')] event = abi.event var types = abi.inputs.map(function (item) { return item.type }) decoded = ethJSABI.rawDecode(types, log[2]) decoded = ethJSABI.stringify(types, decoded) } catch (e) { decoded = '0x' + log[2].toString('hex') } parseLogs(null, { event: event, args: decoded }) } }) } else { var eventFilter = self.web3.eth.contract(abi).at(address).allEvents() eventFilter.watch(parseLogs) } $instance.append($title) // Add the fallback function var fallback = self.getFallbackInterface(abi) if (fallback) { $instance.append(self.getCallButton({ abi: fallback, encode: function (args) { return '' }, address: address })) } $.each(abi, function (i, funABI) { if (funABI.type !== 'function') { return } // @todo getData cannot be used with overloaded functions $instance.append(self.getCallButton({ abi: funABI, encode: function (args) { var types = [] for (var i = 0; i < funABI.inputs.length; i++) { types.push(funABI.inputs[i].type) } return Buffer.concat([ ethJSABI.methodID(funABI.name, types), ethJSABI.rawEncode(types, args) ]).toString('hex') }, address: address })) }) $el = $el || $createInterface $el.append($instance.append($events)) } if (!address || !$target) { $createInterface.append(self.getCallButton({ abi: funABI, encode: function (args) { var types = [] for (var i = 0; i < funABI.inputs.length; i++) { types.push(funABI.inputs[i].type) } // NOTE: the caller will concatenate the bytecode and this // it could be done here too for consistency return ethJSABI.rawEncode(types, args).toString('hex') }, contractName: contract.name, bytecode: contract.bytecode, appendFunctions: appendFunctions })) } else { appendFunctions(address, $target) } return $createInterface } UniversalDApp.prototype.getConstructorInterface = function (abi) { for (var i = 0; i < abi.length; i++) { if (abi[i].type === 'constructor') { return abi[i] } } return { 'type': 'constructor', 'payable': false, 'inputs': [] } } UniversalDApp.prototype.getFallbackInterface = function (abi) { for (var i = 0; i < abi.length; i++) { if (abi[i].type === 'fallback') { return abi[i] } } } UniversalDApp.prototype.getCallButton = function (args) { var self = this // args.abi, args.encode, args.bytecode [constr only], args.address [fun only] // args.contractName [constr only], args.appendFunctions [constr only] var isConstructor = args.bytecode !== undefined var lookupOnly = (args.abi.constant && !isConstructor) var inputs = '' if (args.abi.inputs) { $.each(args.abi.inputs, function (i, inp) { if (inputs !== '') { inputs += ', ' } inputs += inp.type + ' ' + inp.name }) } var inputField = $('').attr('placeholder', inputs).attr('title', inputs) var $outputOverride = $('') var outputSpan = $('') var getReturnOutput = function (result) { var returnName = lookupOnly ? 'Value' : 'Result' var returnCls = lookupOnly ? 'value' : 'returned' return $('