diff --git a/src/app/contract/contractParser.js b/src/app/contract/contractParser.js new file mode 100644 index 0000000000..c9c74ab542 --- /dev/null +++ b/src/app/contract/contractParser.js @@ -0,0 +1,148 @@ +'use strict' + +var $ = require('jquery') +var txHelper = require('../execution/txHelper') + +module.exports = (contractName, contract, compiledSource) => { + return getDetails(contractName, contract, compiledSource) +} + +var getDetails = function (contractName, contract, source) { + var detail = {} + detail.name = contractName + detail.metadata = contract.metadata + if (contract.bytecode) { + detail.bytecode = contract.bytecode + } + + detail.interface = contract.interface + + if (contract.bytecode) { + detail.bytecode = contract.bytecode + detail.web3Deploy = gethDeploy(contractName.toLowerCase(), contract['interface'], contract.bytecode) + + detail.metadataHash = retrieveMetadataHash(contract.bytecode) + if (detail.metadataHash) { + detail.swarmLocation = 'bzz://' + detail.metadataHash + } + } + + detail.functionHashes = {} + for (var fun in contract.functionHashes) { + detail.functionHashes[contract.functionHashes[fun]] = fun + } + + detail.gasEstimates = formatGasEstimates(contract.gasEstimates) + + if (contract.runtimeBytecode && contract.runtimeBytecode.length > 0) { + detail['Runtime Bytecode'] = contract.runtimeBytecode + } + + if (contract.opcodes !== undefined && contract.opcodes !== '') { + detail['Opcodes'] = contract.opcodes + } + + if (contract.assembly !== null) { + detail['Assembly'] = formatAssemblyText(contract.assembly, '', source) + } + + return detail +} + +var retrieveMetadataHash = function (bytecode) { + var match = /a165627a7a72305820([0-9a-f]{64})0029$/.exec(bytecode) + if (match) { + return match[1] + } +} + +var formatAssemblyText = function (asm, prefix, source) { + if (typeof asm === typeof '' || asm === null || asm === undefined) { + return prefix + asm + '\n' + } + var text = prefix + '.code\n' + $.each(asm['.code'], function (i, item) { + var v = item.value === undefined ? '' : item.value + var src = '' + if (item.begin !== undefined && item.end !== undefined) { + src = source.slice(item.begin, item.end).replace('\n', '\\n', 'g') + } + if (src.length > 30) { + src = src.slice(0, 30) + '...' + } + if (item.name !== 'tag') { + text += ' ' + } + text += prefix + item.name + ' ' + v + '\t\t\t' + src + '\n' + }) + text += prefix + '.data\n' + if (asm['.data']) { + $.each(asm['.data'], function (i, item) { + text += ' ' + prefix + '' + i + ':\n' + text += formatAssemblyText(item, prefix + ' ', source) + }) + } + return text +} + +var gethDeploy = function (contractName, jsonInterface, bytecode) { + var code = '' + var funABI = txHelper.getConstructorInterface(JSON.parse(jsonInterface)) + + funABI.inputs.forEach(function (inp) { + code += 'var ' + inp.name + ' = /* var of type ' + inp.type + ' here */ ;\n' + }) + + contractName = contractName.replace(/[:./]/g, '_') + code += 'var ' + contractName + 'Contract = web3.eth.contract(' + jsonInterface.replace('\n', '') + ');' + + '\nvar ' + contractName + ' = ' + contractName + 'Contract.new(' + + funABI.inputs.forEach(function (inp) { + code += '\n ' + inp.name + ',' + }) + + code += '\n {' + + '\n from: web3.eth.accounts[0], ' + + "\n data: '0x" + bytecode + "', " + + "\n gas: '4700000'" + + '\n }, function (e, contract){' + + '\n console.log(e, contract);' + + "\n if (typeof contract.address !== 'undefined') {" + + "\n console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);" + + '\n }' + + '\n })' + + return code +} + +var formatGasEstimates = function (data) { + // FIXME: the whole gasEstimates object should be nil instead + if (data.creation === undefined && data.external === undefined && data.internal === undefined) { + return + } + + var gasToText = function (g) { + return g === null ? 'unknown' : g + } + + var ret = {} + var fun + if ('creation' in data) { + ret['Creation'] = gasToText(data.creation[0]) + ' + ' + gasToText(data.creation[1]) + '\n' + } + + if ('external' in data) { + ret['External'] = {} + for (fun in data.external) { + ret['External'][fun] = gasToText(data.external[fun]) + } + } + + if ('internal' in data) { + ret['Internal'] = {} + for (fun in data.internal) { + ret['Internal'][fun] = gasToText(data.internal[fun]) + } + } + return ret +} diff --git a/src/app/contract/publishOnSwarm.js b/src/app/contract/publishOnSwarm.js new file mode 100644 index 0000000000..fb10b99a53 --- /dev/null +++ b/src/app/contract/publishOnSwarm.js @@ -0,0 +1,65 @@ +'use strict' + +var async = require('async') +var swarmgw = require('swarmgw') + +module.exports = (contract, appAPI, cb) => { + // gather list of files to publish + var sources = [] + + sources.push({ + content: contract.metadata, + hash: contract.metadataHash + }) + + var metadata + try { + metadata = JSON.parse(contract.metadata) + } catch (e) { + return cb(e) + } + + if (metadata === undefined) { + return cb('No metadata') + } + + async.eachSeries(Object.keys(metadata.sources), function (fileName, cb) { + // find hash + var hash + try { + hash = metadata.sources[fileName].urls[0].match('bzzr://(.+)')[1] + } catch (e) { + return cb('Metadata inconsistency') + } + + appAPI.fileProviderOf(fileName).get(fileName, (error, content) => { + if (error) { + console.log(error) + } else { + sources.push({ + content: content, + hash: hash + }) + } + cb() + }) + }, function () { + // publish the list of sources in order, fail if any failed + async.eachSeries(sources, function (item, cb) { + swarmVerifiedPublish(item.content, item.hash, cb) + }, cb) + }) +} + + +function swarmVerifiedPublish (content, expectedHash, cb) { + swarmgw.put(content, function (err, ret) { + if (err) { + cb(err) + } else if (ret !== expectedHash) { + cb('Hash mismatch') + } else { + cb() + } + }) +} diff --git a/src/app/contract/txExecution.js b/src/app/contract/txExecution.js new file mode 100644 index 0000000000..29515216e0 --- /dev/null +++ b/src/app/contract/txExecution.js @@ -0,0 +1,33 @@ +'use strict' + +module.exports = { + /** + * deploy the given contract + * + * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). + * @param {Object} udap - udapp. + * @param {Function} callback - callback. + */ + createContract: function (data, udapp, callback) { + udapp.runTx({data: data, useCall: false}, (error, txResult) => { + // see universaldapp.js line 660 => 700 to check possible values of txResult (error case) + callback(error, txResult) + }) + }, + + /** + * call the current given contract + * + * @param {String} to - address of the contract to call. + * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). + * @param {Object} funAbi - abi definition of the function to call. + * @param {Object} udap - udapp. + * @param {Function} callback - callback. + */ + callFunction: function (to, data, funAbi, udapp, callback) { + udapp.runTx({to: to, data: data, useCall: funAbi.constant}, (error, txResult) => { + // see universaldapp.js line 660 => 700 to check possible values of txResult (error case) + callback(error, txResult) + }) + } +} \ No newline at end of file diff --git a/src/app/contract/txFormat.js b/src/app/contract/txFormat.js new file mode 100644 index 0000000000..bc5e71cec5 --- /dev/null +++ b/src/app/contract/txFormat.js @@ -0,0 +1,153 @@ +'use strict' +var $ = require('jquery') +var ethJSABI = require('ethereumjs-abi') +var helper = require('./txHelper') + +module.exports = { + /** + * build the transaction data + * + * @param {Object} contract - abi definition of the current contract. + * @param {Object} contracts - map of all compiled contracts. + * @param {Bool} isConstructor - isConstructor. + * @param {Object} funAbi - abi definition of the function to call. null if building data for the ctor. + * @param {Object} params - input paramater of the function to call + * @param {Object} udapp - udapp + * @param {Object} executionContext - executionContext + * @param {Function} callback - callback + */ + buildData: function (contract, contracts, isConstructor, funAbi, params, udapp, executionContext, callback) { + var funArgs = '' + try { + funArgs = $.parseJSON('[' + params + ']') + } catch (e) { + callback('Error encoding arguments: ' + e) + return + } + var data = '' + var dataHex = '' + if (!isConstructor || funArgs.length > 0) { + try { + data = helper.encodeParams(funAbi, funArgs) + dataHex = data.toString('hex') + } catch (e) { + callback('Error encoding arguments: ' + e) + return + } + } + if (data.slice(0, 9) === 'undefined') { + dataHex = data.slice(9) + } + if (data.slice(0, 2) === '0x') { + dataHex = data.slice(2) + } + if (isConstructor) { + var bytecodeToDeploy = contract.bytecode + if (bytecodeToDeploy.indexOf('_') >= 0) { + this.linkBytecode(contract, contracts, executionContext, udapp, (err, bytecode) => { + if (err) { + callback('Error deploying required libraries: ' + err) + } else { + bytecodeToDeploy = bytecode + dataHex + return callback(null, bytecodeToDeploy) + } + }) + return + } else { + dataHex = bytecodeToDeploy + dataHex + } + } else { + dataHex = Buffer.concat([helper.encodeFunctionId(funAbi), data]).toString('hex') + } + callback(null, dataHex) + }, + + atAddress: function () {}, + + linkBytecode: function (contract, contracts, executionContext, udapp, callback) { + var bytecode = contract.bytecode + if (bytecode.indexOf('_') < 0) { + return callback(null, bytecode) + } + var m = bytecode.match(/__([^_]{1,36})__/) + if (!m) { + return callback('Invalid bytecode format.') + } + var libraryName = m[1] + var libraryabi = helper.getContractByName(libraryName, contracts) + if (!libraryabi) { + return callback('Library ' + libraryName + ' not found.') + } + this.deployLibrary(libraryabi, executionContext, udapp, (err, address) => { + if (err) { + return callback(err) + } + var libLabel = '__' + libraryName + Array(39 - libraryName.length).join('_') + var hexAddress = address.toString('hex') + if (hexAddress.slice(0, 2) === '0x') { + hexAddress = hexAddress.slice(2) + } + hexAddress = Array(40 - hexAddress.length + 1).join('0') + hexAddress + while (bytecode.indexOf(libLabel) >= 0) { + bytecode = bytecode.replace(libLabel, hexAddress) + } + contract.bytecode = bytecode + this.linkBytecode(contract, contracts, executionContext, udapp, callback) + }) + }, + + deployLibrary: function (libraryName, library, executionContext, udapp, callback) { + var address = library.address + if (address) { + return callback(null, address) + } + var bytecode = library.bytecode + if (bytecode.indexOf('_') >= 0) { + this.linkBytecode(libraryName, (err, bytecode) => { + if (err) callback(err) + else this.deployLibrary(libraryName, callback) + }) + } else { + udapp.runTx({ data: bytecode, useCall: false }, (err, txResult) => { + if (err) { + return callback(err) + } + var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress + library.address = address + callback(err, address) + }) + } + }, + + decodeResponse: function (response, fnabi, callback) { + // Only decode if there supposed to be fields + if (fnabi.outputs && fnabi.outputs.length > 0) { + try { + var i + + var outputTypes = [] + for (i = 0; i < fnabi.outputs.length; i++) { + outputTypes.push(fnabi.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 = fnabi.outputs[i].name + if (name.length > 0) { + decodedObj[i] = outputTypes[i] + ' ' + name + ': ' + decodedObj[i] + } else { + decodedObj[i] = outputTypes[i] + ': ' + decodedObj[i] + } + } + + return callback(null, decodedObj) + } catch (e) { + return callback('Failed to decode output: ' + e) + } + } + } +} diff --git a/src/app/contract/txHelper.js b/src/app/contract/txHelper.js new file mode 100644 index 0000000000..ee0bc46904 --- /dev/null +++ b/src/app/contract/txHelper.js @@ -0,0 +1,106 @@ +'use strict' +var ethJSABI = require('ethereumjs-abi') +var $ = require('jquery') + +module.exports = { + encodeParams: function (funABI, args) { + var types = [] + if (funABI.inputs && funABI.inputs.length) { + 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) + }, + + encodeFunctionId: function (funABI) { + var types = [] + if (funABI.inputs && funABI.inputs.length) { + for (var i = 0; i < funABI.inputs.length; i++) { + types.push(funABI.inputs[i].type) + } + } + + return ethJSABI.methodID(funABI.name, types) + }, + + sortAbiFunction: function (contract) { + 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 + } + }) + return abi + }, + + getConstructorInterface: function (abi) { + var funABI = { 'name': '', 'inputs': [], 'type': 'constructor', 'outputs': [] } + if (typeof abi === 'string') { + try { + abi = JSON.parse(abi) + } catch (e) { + console.log('exception retrieving ctor abi ' + abi) + return funABI + } + } + + for (var i = 0; i < abi.length; i++) { + if (abi[i].type === 'constructor') { + funABI.inputs = abi[i].inputs || [] + break + } + } + + return funABI + }, + + getFunction: function (abi, fnName) { + for (var i = 0; i < abi.length; i++) { + if (abi[i].name === fnName) { + return abi[i] + } + } + return null + }, + + getFallbackInterface: function (abi) { + for (var i = 0; i < abi.length; i++) { + if (abi[i].type === 'fallback') { + return abi[i] + } + } + }, + + getContractByName: function (contractName, contracts) { + for (var c in contracts) { + if (contracts[c].name === contractName) { + return contracts[c] + } + } + return null + }, + + inputParametersDeclarationToString: function (abiinputs) { + var inputs = '' + if (abiinputs) { + $.each(abiinputs, function (i, inp) { + if (inputs !== '') { + inputs += ', ' + } + inputs += inp.type + ' ' + inp.name + }) + } + return inputs + } +}