var $ = require('jquery'); var EthJSVM = require('ethereumjs-vm'); var Trie = require('merkle-patricia-tree'); var ethJSUtil = require('ethereumjs-util'); var EthJSTX = require('ethereumjs-tx'); var ethJSABI = require('ethereumjs-abi'); var EthJSBlock = require('ethereumjs-block'); var web3 = require('./web3-adapter.js'); function UniversalDApp (contracts, options) { this.options = options || {}; this.$el = $('
'); this.contracts = contracts; this.renderOutputModifier = options.renderOutputModifier || function(name, content) { return content; }; if (options.vm) { this.accounts = {}; this.BN = ethJSUtil.BN; this.stateTrie = new Trie(); this.vm = new EthJSVM(this.stateTrie, null, { activatePrecompiles: true }); this.addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511'); this.addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c'); } else if (!web3.currentProvider) { var host = options.host || "localhost"; var port = options.port || "8545"; var rpc_url = options.getWeb3endpoint ? options.getWeb3endpoint() : ('http://' + host + ':' + port); web3.setProvider( new web3.providers.HttpProvider( rpc_url ) ); } } UniversalDApp.prototype.addAccount = function (privateKey, balance) { if (this.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 this.vm.stateManager.putAccountBalance(address, balance || 'f00000000000000001', function cb() {} ); this.accounts['0x' + address.toString('hex')] = { privateKey: privateKey, nonce: 0 }; } }; UniversalDApp.prototype.getAccounts = function (cb) { if (!this.vm) { web3.eth.getAccounts(cb); } else { if (!this.accounts) return cb("No accounts?"); cb(null, Object.keys(this.accounts)); } }; UniversalDApp.prototype.getBalance = function (address, cb) { address = ethJSUtil.stripHexPrefix(address); if (!this.vm) { web3.eth.getBalance(address, function (err, res) { if (err) { cb(err); } else { cb(null, res.toString(10)); } }); } else { if (!this.accounts) return cb("No accounts?"); this.vm.stateManager.getAccountBalance(new Buffer(address, 'hex'), function (err, res) { if (err) { cb("Account not found"); } else { cb(null, new ethJSUtil.BN(res).toString(10)); } }); } }; UniversalDApp.prototype.render = function () { if (this.contracts.length == 0) { this.$el.append( this.getABIInputForm() ); } else { for (var c in this.contracts) { var $contractEl = $(''); if (this.contracts[c].address) { this.getInstanceInterface(this.contracts[c], this.contracts[c].address, $contractEl ); } else { var $title = $('').text( this.contracts[c].name ); if (this.contracts[c].bytecode) { $title.append($('').text((this.contracts[c].bytecode.length / 2) + ' bytes')); } $contractEl.append( $title ).append( this.getCreateInterface( $contractEl, this.contracts[c]) ); } this.$el.append(this.renderOutputModifier(this.contracts[c].name, $contractEl)); } } $legend = $('') .append( $('').text('Attach') ) .append( $('').text('Transact') ) .append( $('').text('Call') ); this.$el.append( $('') .html("Universal ÐApp powered by The Blockchain") ); this.$el.append( $legend ); return this.$el; }; UniversalDApp.prototype.getContractByName = function(contractName) { for (var c in this.contracts) if (this.contracts[c].name == contractName) return this.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 (this.options.removable) { var $close = $(''); $close.click( function(){ self.$el.remove(); } ); $createInterface.append( $close ); } var $newButton = this.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 web3contract = web3.eth.contract(abi); var funABI = this.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'); }); $events = $(''); var parseLogs = function(err,response) { if (err) return; $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 = web3contract.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 obj = web3contract.at('0x00')[funABI.name]; return obj.getData.apply(obj, args); }, address: address })); }); ($el || $createInterface ).append( $instance.append( $events ) ); }; if (!address || !$target) { $createInterface.append( this.getCallButton({ abi: funABI, encode: function(args) { var obj = web3contract.new; return obj.getData.apply(obj, args); }, 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 $('