/* global prompt */ var $ = require('jquery'); var EthJSVM = require('ethereumjs-vm'); var ethJSUtil = require('ethereumjs-util'); var EthJSTX = require('ethereumjs-tx'); var ethJSABI = require('ethereumjs-abi'); var EthJSBlock = require('ethereumjs-block'); var BN = ethJSUtil.BN; function UniversalDApp (contracts, options) { var self = this; self.options = options || {}; self.$el = $('
'); self.contracts = contracts; self.renderOutputModifier = options.renderOutputModifier || function (name, content) { return content; }; self.web3 = options.web3; if (options.mode === 'vm') { // FIXME: use `options.vm` or `self.vm` consistently options.vm = true; self.accounts = {}; self.vm = new EthJSVM(null, null, { activatePrecompiles: true, enableHomestead: true }); self.addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511'); self.addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c'); } else if (options.mode !== 'web3') { throw new Error('Either VM or Web3 mode must be selected'); } } UniversalDApp.prototype.addAccount = function (privateKey, balance) { var self = this; 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.vm) { 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.vm) { 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; if (self.contracts.length === 0) { self.$el.append(self.getABIInputForm()); } else { 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)); } } var $legend = $('') .append($('').text('Attach')) .append($('').text('Transact')) .append($('').text('Call')); self.$el.append($('') .html('Universal ÐApp powered by The Blockchain')); self.$el.append($legend); 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.getABIInputForm = function (cb) { var self = this; var $el = $(''); var $jsonInput = $(''); var $createButton = $('').text('Create a Universal ÐApp'); $createButton.click(function (ev) { var contracts = $.parseJSON($jsonInput.val()); if (cb) { var err = null; var dapp = null; try { dapp = new UniversalDApp(contracts, self.options); } catch (e) { err = e; } cb(err, dapp); } else { self.contracts = contracts; self.$el.empty().append(self.render()); } }); $el.append($jsonInput).append($createButton); return $el; }; 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 $newButton = self.getInstanceInterface(contract); var $atButton = $('').text('At Address').click(function () { self.clickContractAt(self, $container.find('.createContract'), contract); }); $createInterface.append($atButton).append($newButton); 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); 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.options.vm ? 'memory' : 'blockchain'; var $title = $('').text(contract.name + ' at ' + (self.options.vm ? '0x' : '') + address.toString('hex') + ' (' + 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.options.vm) { // 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 abi = eventABI[log[1][0].toString('hex')]; var event = abi.event; var decoded; try { 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 $instance.append(self.getCallButton({ abi: { constant: false, inputs: [], name: '(fallback)', outputs: [], type: 'function' }, 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 || $createInterface).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) { var funABI = { 'name': '', 'inputs': [], 'type': 'constructor', 'outputs': [] }; for (var i = 0; i < abi.length; i++) { if (abi[i].type === 'constructor') { funABI.inputs = abi[i].inputs || []; break; } } return funABI; }; 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 = ''; $.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 $('