Merge pull request #684 from ethereum/refactor/move_tx_to_remix_lib
move most tx code from browser-soliditypull/3094/head
commit
58864e98b6
@ -0,0 +1,123 @@ |
||||
'use strict' |
||||
var ethJSABI = require('ethereumjs-abi') |
||||
var txHelper = require('./txHelper') |
||||
|
||||
/** |
||||
* Register to txListener and extract events |
||||
* |
||||
*/ |
||||
class EventsDecoder { |
||||
constructor (opt = {}) { |
||||
this._api = opt.api |
||||
} |
||||
|
||||
/** |
||||
* use Transaction Receipt to decode logs. assume that the transaction as already been resolved by txListener. |
||||
* logs are decoded only if the contract if known by remix. |
||||
* |
||||
* @param {Object} tx - transaction object |
||||
* @param {Function} cb - callback |
||||
*/ |
||||
parseLogs (tx, contractName, compiledContracts, cb) { |
||||
if (tx.isCall) return cb(null, { decoded: [], raw: [] }) |
||||
this._api.resolveReceipt(tx, (error, receipt) => { |
||||
if (error) return cb(error) |
||||
this._decodeLogs(tx, receipt, contractName, compiledContracts, cb) |
||||
}) |
||||
} |
||||
|
||||
_decodeLogs (tx, receipt, contract, contracts, cb) { |
||||
if (!contract || !receipt) { |
||||
return cb('cannot decode logs - contract or receipt not resolved ') |
||||
} |
||||
if (!receipt.logs) { |
||||
return cb(null, { decoded: [], raw: [] }) |
||||
} |
||||
this._decodeEvents(tx, receipt.logs, contract, contracts, cb) |
||||
} |
||||
|
||||
_eventABI (contract) { |
||||
var eventABI = {} |
||||
contract.abi.forEach(function (funABI, i) { |
||||
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 } |
||||
}) |
||||
return eventABI |
||||
} |
||||
|
||||
_eventsABI (compiledContracts) { |
||||
var eventsABI = {} |
||||
txHelper.visitContracts(compiledContracts, (contract) => { |
||||
eventsABI[contract.name] = this._eventABI(contract.object) |
||||
}) |
||||
return eventsABI |
||||
} |
||||
|
||||
_event (hash, eventsABI) { |
||||
for (var k in eventsABI) { |
||||
if (eventsABI[k][hash]) { |
||||
return eventsABI[k][hash] |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
_decodeEvents (tx, logs, contractName, compiledContracts, cb) { |
||||
var eventsABI = this._eventsABI(compiledContracts) |
||||
var events = [] |
||||
for (var i in logs) { |
||||
// [address, topics, mem]
|
||||
var log = logs[i] |
||||
var topicId = log.topics[0] |
||||
var abi = this._event(topicId.replace('0x', ''), eventsABI) |
||||
if (abi) { |
||||
var event |
||||
try { |
||||
var decoded = new Array(abi.inputs.length) |
||||
event = abi.event |
||||
var indexed = 1 |
||||
var nonindexed = [] |
||||
// decode indexed param
|
||||
abi.inputs.map(function (item, index) { |
||||
if (item.indexed) { |
||||
var encodedData = log.topics[indexed].replace('0x', '') |
||||
try { |
||||
decoded[index] = ethJSABI.rawDecode([item.type], new Buffer(encodedData, 'hex'))[0] |
||||
if (typeof decoded[index] !== 'string') { |
||||
decoded[index] = ethJSABI.stringify([item.type], decoded[index]) |
||||
} |
||||
} catch (e) { |
||||
decoded[index] = encodedData |
||||
} |
||||
indexed++ |
||||
} else { |
||||
nonindexed.push(item.type) |
||||
} |
||||
}) |
||||
// decode non indexed param
|
||||
var nonindexededResult = ethJSABI.rawDecode(nonindexed, new Buffer(log.data.replace('0x', ''), 'hex')) |
||||
nonindexed = ethJSABI.stringify(nonindexed, nonindexededResult) |
||||
// ordering
|
||||
var j = 0 |
||||
abi.inputs.map(function (item, index) { |
||||
if (!item.indexed) { |
||||
decoded[index] = nonindexed[j] |
||||
j++ |
||||
} |
||||
}) |
||||
} catch (e) { |
||||
decoded = log.data |
||||
} |
||||
events.push({ topic: topicId, event: event, args: decoded }) |
||||
} else { |
||||
events.push({ data: log.data, topics: log.topics }) |
||||
} |
||||
} |
||||
cb(null, { decoded: events, raw: logs }) |
||||
} |
||||
} |
||||
|
||||
module.exports = EventsDecoder |
@ -0,0 +1,227 @@ |
||||
'use strict' |
||||
var Web3 = require('web3') |
||||
var EventManager = require('../eventManager') |
||||
var EthJSVM = require('ethereumjs-vm') |
||||
var ethUtil = require('ethereumjs-util') |
||||
var StateManager = require('ethereumjs-vm/lib/stateManager') |
||||
var Web3VMProvider = require('../web3Provider/web3VmProvider') |
||||
|
||||
var rlp = ethUtil.rlp |
||||
|
||||
var injectedProvider |
||||
|
||||
var web3 |
||||
if (typeof window !== 'undefined' && typeof window.web3 !== 'undefined') { |
||||
injectedProvider = window.web3.currentProvider |
||||
web3 = new Web3(injectedProvider) |
||||
} else { |
||||
web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) |
||||
} |
||||
|
||||
var blankWeb3 = new Web3() |
||||
|
||||
/* |
||||
extend vm state manager and instanciate VM |
||||
*/ |
||||
|
||||
class StateManagerCommonStorageDump extends StateManager { |
||||
constructor (arg) { |
||||
super(arg) |
||||
this.keyHashes = {} |
||||
} |
||||
|
||||
putContractStorage (address, key, value, cb) { |
||||
this.keyHashes[ethUtil.sha3(key).toString('hex')] = ethUtil.bufferToHex(key) |
||||
super.putContractStorage(address, key, value, cb) |
||||
} |
||||
|
||||
dumpStorage (address, cb) { |
||||
var self = this |
||||
this._getStorageTrie(address, function (err, trie) { |
||||
if (err) { |
||||
return cb(err) |
||||
} |
||||
var storage = {} |
||||
var stream = trie.createReadStream() |
||||
stream.on('data', function (val) { |
||||
var value = rlp.decode(val.value) |
||||
storage['0x' + val.key.toString('hex')] = { |
||||
key: self.keyHashes[val.key.toString('hex')], |
||||
value: '0x' + value.toString('hex') |
||||
} |
||||
}) |
||||
stream.on('end', function () { |
||||
cb(storage) |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
var stateManager = new StateManagerCommonStorageDump({}) |
||||
var vm = new EthJSVM({ |
||||
enableHomestead: true, |
||||
activatePrecompiles: true |
||||
}) |
||||
|
||||
// FIXME: move state manager in EthJSVM ctr
|
||||
vm.stateManager = stateManager |
||||
vm.blockchain = stateManager.blockchain |
||||
vm.trie = stateManager.trie |
||||
vm.stateManager.checkpoint() |
||||
|
||||
var web3VM = new Web3VMProvider() |
||||
web3VM.setVM(vm) |
||||
|
||||
var mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3' |
||||
|
||||
/* |
||||
trigger contextChanged, web3EndpointChanged |
||||
*/ |
||||
function ExecutionContext () { |
||||
var self = this |
||||
this.event = new EventManager() |
||||
|
||||
var executionContext = null |
||||
|
||||
this.init = function (config) { |
||||
if (config.get('settings/always-use-vm')) { |
||||
executionContext = 'vm' |
||||
} else { |
||||
executionContext = injectedProvider ? 'injected' : 'vm' |
||||
} |
||||
} |
||||
|
||||
this.getProvider = function () { |
||||
return executionContext |
||||
} |
||||
|
||||
this.isVM = function () { |
||||
return executionContext === 'vm' |
||||
} |
||||
|
||||
this.web3 = function () { |
||||
return this.isVM() ? web3VM : web3 |
||||
} |
||||
|
||||
this.detectNetwork = function (callback) { |
||||
if (this.isVM()) { |
||||
callback(null, { id: '-', name: 'VM' }) |
||||
} else { |
||||
this.web3().version.getNetwork((err, id) => { |
||||
var name = null |
||||
if (err) name = 'Unknown' |
||||
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
|
||||
else if (id === '1') name = 'Main' |
||||
else if (id === '2') name = 'Morden (deprecated)' |
||||
else if (id === '3') name = 'Ropsten' |
||||
else if (id === '4') name = 'Rinkeby' |
||||
else if (id === '42') name = 'Kovan' |
||||
else name = 'Custom' |
||||
|
||||
if (id === '1') { |
||||
this.web3().eth.getBlock(0, (error, block) => { |
||||
if (error) console.log('cant query first block') |
||||
if (block && block.hash !== mainNetGenesisHash) name = 'Custom' |
||||
callback(err, { id, name }) |
||||
}) |
||||
} else { |
||||
callback(err, { id, name }) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
this.internalWeb3 = function () { |
||||
return web3 |
||||
} |
||||
|
||||
this.blankWeb3 = function () { |
||||
return blankWeb3 |
||||
} |
||||
|
||||
this.vm = function () { |
||||
return vm |
||||
} |
||||
|
||||
this.setContext = function (context, endPointUrl, confirmCb, infoCb) { |
||||
executionContext = context |
||||
this.executionContextChange(context, endPointUrl, confirmCb, infoCb) |
||||
} |
||||
|
||||
this.executionContextChange = function (context, endPointUrl, confirmCb, infoCb, cb) { |
||||
if (!cb) cb = () => {} |
||||
|
||||
if (context === 'vm') { |
||||
executionContext = context |
||||
vm.stateManager.revert(function () { |
||||
vm.stateManager.checkpoint() |
||||
}) |
||||
self.event.trigger('contextChanged', ['vm']) |
||||
return cb() |
||||
} |
||||
|
||||
if (context === 'injected') { |
||||
if (injectedProvider === undefined) { |
||||
var alertMsg = 'No injected Web3 provider found. ' |
||||
alertMsg += 'Make sure your provider (e.g. MetaMask) is active and running ' |
||||
alertMsg += '(when recently activated you may have to reload the page).' |
||||
infoCb(alertMsg) |
||||
return cb() |
||||
} else { |
||||
executionContext = context |
||||
web3.setProvider(injectedProvider) |
||||
self.event.trigger('contextChanged', ['injected']) |
||||
return cb() |
||||
} |
||||
} |
||||
|
||||
if (context === 'web3') { |
||||
confirmCb(cb) |
||||
} |
||||
} |
||||
|
||||
this.currentblockGasLimit = function () { |
||||
return this.blockGasLimit |
||||
} |
||||
|
||||
this.blockGasLimitDefault = 4300000 |
||||
this.blockGasLimit = this.blockGasLimitDefault |
||||
setInterval(() => { |
||||
if (this.getProvider() !== 'vm') { |
||||
web3.eth.getBlock('latest', (err, block) => { |
||||
if (!err) { |
||||
// we can't use the blockGasLimit cause the next blocks could have a lower limit : https://github.com/ethereum/remix/issues/506
|
||||
this.blockGasLimit = (block && block.gasLimit) ? Math.floor(block.gasLimit - (5 * block.gasLimit) / 1024) : this.blockGasLimitDefault |
||||
} else { |
||||
this.blockGasLimit = this.blockGasLimitDefault |
||||
} |
||||
}) |
||||
} |
||||
}, 15000) |
||||
|
||||
// TODO: not used here anymore and needs to be moved
|
||||
function setProviderFromEndpoint (endpoint, context, cb) { |
||||
var oldProvider = web3.currentProvider |
||||
|
||||
if (endpoint === 'ipc') { |
||||
web3.setProvider(new web3.providers.IpcProvider()) |
||||
} else { |
||||
web3.setProvider(new web3.providers.HttpProvider(endpoint)) |
||||
} |
||||
if (web3.isConnected()) { |
||||
executionContext = context |
||||
self.event.trigger('contextChanged', ['web3']) |
||||
self.event.trigger('web3EndpointChanged') |
||||
cb() |
||||
} else { |
||||
web3.setProvider(oldProvider) |
||||
var alertMsg = 'Not possible to connect to the Web3 provider. ' |
||||
alertMsg += 'Make sure the provider is running and a connection is open (via IPC or RPC).' |
||||
cb(alertMsg) |
||||
} |
||||
} |
||||
this.setProviderFromEndpoint = setProviderFromEndpoint |
||||
} |
||||
|
||||
module.exports = new ExecutionContext() |
||||
|
@ -0,0 +1,76 @@ |
||||
'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) |
||||
}) |
||||
}, |
||||
|
||||
/** |
||||
* check if the vm has errored |
||||
* |
||||
* @param {Object} txResult - the value returned by the vm |
||||
* @return {Object} - { error: true/false, message: DOMNode } |
||||
*/ |
||||
checkVMError: function (txResult) { |
||||
var errorCode = { |
||||
OUT_OF_GAS: 'out of gas', |
||||
STACK_UNDERFLOW: 'stack underflow', |
||||
STACK_OVERFLOW: 'stack overflow', |
||||
INVALID_JUMP: 'invalid JUMP', |
||||
INVALID_OPCODE: 'invalid opcode', |
||||
REVERT: 'revert', |
||||
STATIC_STATE_CHANGE: 'static state change' |
||||
} |
||||
var ret = { |
||||
error: false, |
||||
message: '' |
||||
} |
||||
if (!txResult.result.vm.exceptionError) { |
||||
return ret |
||||
} |
||||
var error = `VM error: ${txResult.result.vm.exceptionError}.\n` |
||||
var msg |
||||
if (txResult.result.vm.exceptionError === errorCode.INVALID_OPCODE) { |
||||
msg = `\t\n\tThe execution might have thrown.\n` |
||||
ret.error = true |
||||
} else if (txResult.result.vm.exceptionError === errorCode.OUT_OF_GAS) { |
||||
msg = `\tThe transaction ran out of gas. Please increase the Gas Limit.\n` |
||||
ret.error = true |
||||
} else if (txResult.result.vm.exceptionError === errorCode.REVERT) { |
||||
msg = `\tThe transaction has been reverted to the initial state.\nNote: The constructor should be payable if you send value.` |
||||
ret.error = true |
||||
} else if (txResult.result.vm.exceptionError === errorCode.STATIC_STATE_CHANGE) { |
||||
msg = `\tState changes is not allowed in Static Call context\n` |
||||
ret.error = true |
||||
} |
||||
ret.message = `${error}${txResult.result.vm.exceptionError}${msg}\tDebug the transaction to get more information.` |
||||
return ret |
||||
} |
||||
} |
||||
|
@ -0,0 +1,229 @@ |
||||
'use strict' |
||||
var ethJSABI = require('ethereumjs-abi') |
||||
var helper = require('./txHelper') |
||||
var executionContext = require('./execution-context') |
||||
|
||||
module.exports = { |
||||
|
||||
/** |
||||
* build the transaction data |
||||
* |
||||
* @param {Object} function abi |
||||
* @param {Object} values to encode |
||||
* @param {String} contractbyteCode |
||||
*/ |
||||
encodeData: function (funABI, values, contractbyteCode) { |
||||
var encoded |
||||
var encodedHex |
||||
try { |
||||
encoded = helper.encodeParams(funABI, values) |
||||
encodedHex = encoded.toString('hex') |
||||
} catch (e) { |
||||
return { error: 'cannot encode arguments' } |
||||
} |
||||
if (contractbyteCode) { |
||||
return { data: contractbyteCode + encodedHex } |
||||
} else { |
||||
return { data: Buffer.concat([helper.encodeFunctionId(funABI), encoded]).toString('hex') } |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* build the transaction data |
||||
* |
||||
* @param {String} contractName |
||||
* @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 {Function} callback - callback |
||||
* @param {Function} callbackStep - callbackStep |
||||
*/ |
||||
buildData: function (contractName, contract, contracts, isConstructor, funAbi, params, udapp, callback, callbackStep) { |
||||
var funArgs = '' |
||||
var data = '' |
||||
var dataHex = '' |
||||
|
||||
if (params.indexOf('0x') === 0) { |
||||
dataHex = params.replace('0x', '') |
||||
data = Buffer.from(dataHex, 'hex') |
||||
} else { |
||||
try { |
||||
funArgs = JSON.parse('[' + params + ']') |
||||
} catch (e) { |
||||
callback('Error encoding arguments: ' + e) |
||||
return |
||||
} |
||||
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) |
||||
} |
||||
} |
||||
var contractBytecode |
||||
if (isConstructor) { |
||||
contractBytecode = contract.evm.bytecode.object |
||||
var bytecodeToDeploy = contract.evm.bytecode.object |
||||
if (bytecodeToDeploy.indexOf('_') >= 0) { |
||||
this.linkBytecode(contract, contracts, udapp, (err, bytecode) => { |
||||
if (err) { |
||||
callback('Error deploying required libraries: ' + err) |
||||
} else { |
||||
bytecodeToDeploy = bytecode + dataHex |
||||
return callback(null, {dataHex: bytecodeToDeploy, funAbi, funArgs, contractBytecode, contractName: contractName}) |
||||
} |
||||
}, callbackStep) |
||||
return |
||||
} else { |
||||
dataHex = bytecodeToDeploy + dataHex |
||||
} |
||||
} else { |
||||
dataHex = Buffer.concat([helper.encodeFunctionId(funAbi), data]).toString('hex') |
||||
} |
||||
callback(null, { dataHex, funAbi, funArgs, contractBytecode, contractName: contractName }) |
||||
}, |
||||
|
||||
atAddress: function () {}, |
||||
|
||||
linkBytecode: function (contract, contracts, udapp, callback, callbackStep) { |
||||
if (contract.evm.bytecode.object.indexOf('_') < 0) { |
||||
return callback(null, contract.evm.bytecode.object) |
||||
} |
||||
var libraryRefMatch = contract.evm.bytecode.object.match(/__([^_]{1,36})__/) |
||||
if (!libraryRefMatch) { |
||||
return callback('Invalid bytecode format.') |
||||
} |
||||
var libraryName = libraryRefMatch[1] |
||||
// file_name:library_name
|
||||
var libRef = libraryName.match(/(.*):(.*)/) |
||||
if (!libRef) { |
||||
return callback('Cannot extract library reference ' + libraryName) |
||||
} |
||||
if (!contracts[libRef[1]] || !contracts[libRef[1]][libRef[2]]) { |
||||
return callback('Cannot find library reference ' + libraryName) |
||||
} |
||||
var libraryShortName = libRef[2] |
||||
var library = contracts[libRef[1]][libraryShortName] |
||||
if (!library) { |
||||
return callback('Library ' + libraryName + ' not found.') |
||||
} |
||||
this.deployLibrary(libraryName, libraryShortName, library, contracts, udapp, (err, address) => { |
||||
if (err) { |
||||
return callback(err) |
||||
} |
||||
var hexAddress = address.toString('hex') |
||||
if (hexAddress.slice(0, 2) === '0x') { |
||||
hexAddress = hexAddress.slice(2) |
||||
} |
||||
contract.evm.bytecode.object = this.linkLibraryStandard(libraryShortName, hexAddress, contract) |
||||
contract.evm.bytecode.object = this.linkLibrary(libraryName, hexAddress, contract.evm.bytecode.object) |
||||
this.linkBytecode(contract, contracts, udapp, callback, callbackStep) |
||||
}, callbackStep) |
||||
}, |
||||
|
||||
deployLibrary: function (libraryName, libraryShortName, library, contracts, udapp, callback, callbackStep) { |
||||
var address = library.address |
||||
if (address) { |
||||
return callback(null, address) |
||||
} |
||||
var bytecode = library.evm.bytecode.object |
||||
if (bytecode.indexOf('_') >= 0) { |
||||
this.linkBytecode(library, contracts, udapp, (err, bytecode) => { |
||||
if (err) callback(err) |
||||
else this.deployLibrary(libraryName, libraryShortName, library, contracts, udapp, callback, callbackStep) |
||||
}, callbackStep) |
||||
} else { |
||||
callbackStep(`creation of library ${libraryName} pending...`) |
||||
var data = {dataHex: bytecode, funAbi: {type: 'constructor'}, funArgs: [], contractBytecode: bytecode, contractName: libraryShortName} |
||||
udapp.runTx({ data: data, 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) |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
linkLibraryStandardFromlinkReferences: function (libraryName, address, bytecode, linkReferences) { |
||||
for (var file in linkReferences) { |
||||
for (var libName in linkReferences[file]) { |
||||
if (libraryName === libName) { |
||||
bytecode = this.setLibraryAddress(address, bytecode, linkReferences[file][libName]) |
||||
} |
||||
} |
||||
} |
||||
return bytecode |
||||
}, |
||||
|
||||
linkLibraryStandard: function (libraryName, address, contract) { |
||||
return this.linkLibraryStandardFromlinkReferences(libraryName, address, contract.evm.bytecode.object, contract.evm.bytecode.linkReferences) |
||||
}, |
||||
|
||||
setLibraryAddress: function (address, bytecodeToLink, positions) { |
||||
if (positions) { |
||||
for (var pos of positions) { |
||||
var regpos = bytecodeToLink.match(new RegExp(`(.{${2 * pos.start}})(.{${2 * pos.length}})(.*)`)) |
||||
if (regpos) { |
||||
bytecodeToLink = regpos[1] + address + regpos[3] |
||||
} |
||||
} |
||||
} |
||||
return bytecodeToLink |
||||
}, |
||||
|
||||
linkLibrary: function (libraryName, address, bytecodeToLink) { |
||||
var libLabel = '__' + libraryName + Array(39 - libraryName.length).join('_') |
||||
if (bytecodeToLink.indexOf(libLabel) === -1) return bytecodeToLink |
||||
|
||||
address = Array(40 - address.length + 1).join('0') + address |
||||
while (bytecodeToLink.indexOf(libLabel) >= 0) { |
||||
bytecodeToLink = bytecodeToLink.replace(libLabel, address) |
||||
} |
||||
return bytecodeToLink |
||||
}, |
||||
|
||||
decodeResponse: function (response, fnabi) { |
||||
// 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) |
||||
var json = {} |
||||
for (i = 0; i < outputTypes.length; i++) { |
||||
var name = fnabi.outputs[i].name |
||||
json[i] = outputTypes[i] + ': ' + (name ? name + ' ' + decodedObj[i] : decodedObj[i]) |
||||
} |
||||
|
||||
return json |
||||
} catch (e) { |
||||
return { error: 'Failed to decode output: ' + e } |
||||
} |
||||
} |
||||
return {} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,122 @@ |
||||
'use strict' |
||||
var ethJSABI = require('ethereumjs-abi') |
||||
|
||||
module.exports = { |
||||
encodeParams: function (funABI, args) { |
||||
var types = [] |
||||
if (funABI.inputs && funABI.inputs.length) { |
||||
for (var i = 0; i < funABI.inputs.length; i++) { |
||||
var type = funABI.inputs[i].type |
||||
types.push(type) |
||||
if (args.length < types.length) { |
||||
args.push('') |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 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 (contractabi) { |
||||
var abi = contractabi.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] |
||||
} |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* return the contract obj of the given @arg name. Uses last compilation result. |
||||
* return null if not found |
||||
* @param {String} name - contract name |
||||
* @returns contract obj and associated file: { contract, file } or null |
||||
*/ |
||||
getContract: (contractName, contracts) => { |
||||
for (var file in contracts) { |
||||
if (contracts[file][contractName]) { |
||||
return { object: contracts[file][contractName], file: file } |
||||
} |
||||
} |
||||
return null |
||||
}, |
||||
|
||||
/** |
||||
* call the given @arg cb (function) for all the contracts. Uses last compilation result |
||||
* stop visiting when cb return true |
||||
* @param {Function} cb - callback |
||||
*/ |
||||
visitContracts: (contracts, cb) => { |
||||
for (var file in contracts) { |
||||
for (var name in contracts[file]) { |
||||
if (cb({ name: name, object: contracts[file][name], file: file })) return |
||||
} |
||||
} |
||||
}, |
||||
|
||||
inputParametersDeclarationToString: function (abiinputs) { |
||||
var inputs = (abiinputs || []).map((inp) => inp.type + ' ' + inp.name) |
||||
return inputs.join(', ') |
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,365 @@ |
||||
'use strict' |
||||
var async = require('async') |
||||
var ethJSABI = require('ethereumjs-abi') |
||||
var ethJSUtil = require('ethereumjs-util') |
||||
var EventManager = require('../eventManager') |
||||
var codeUtil = require('../util') |
||||
|
||||
var executionContext = require('./execution-context') |
||||
var txFormat = require('./txFormat') |
||||
var txHelper = require('./txHelper') |
||||
|
||||
/** |
||||
* poll web3 each 2s if web3 |
||||
* listen on transaction executed event if VM |
||||
* attention: blocks returned by the event `newBlock` have slightly different json properties whether web3 or the VM is used |
||||
* trigger 'newBlock' |
||||
* |
||||
*/ |
||||
class TxListener { |
||||
constructor (opt) { |
||||
this.event = new EventManager() |
||||
this._api = opt.api |
||||
this._resolvedTransactions = {} |
||||
this._resolvedContracts = {} |
||||
this._isListening = false |
||||
this._listenOnNetwork = false |
||||
this._loopId = null |
||||
this.init() |
||||
executionContext.event.register('contextChanged', (context) => { |
||||
if (this._isListening) { |
||||
this.stopListening() |
||||
this.startListening() |
||||
} |
||||
}) |
||||
|
||||
opt.event.udapp.register('callExecuted', (error, from, to, data, lookupOnly, txResult) => { |
||||
if (error) return |
||||
// we go for that case if
|
||||
// in VM mode
|
||||
// in web3 mode && listen remix txs only
|
||||
if (!this._isListening) return // we don't listen
|
||||
if (this._loopId && executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network
|
||||
|
||||
var call = { |
||||
from: from, |
||||
to: to, |
||||
input: data, |
||||
hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data, |
||||
isCall: true, |
||||
returnValue: executionContext.isVM() ? txResult.result.vm.return : ethJSUtil.toBuffer(txResult.result), |
||||
envMode: executionContext.getProvider() |
||||
} |
||||
|
||||
addExecutionCosts(txResult, call) |
||||
this._resolveTx(call, (error, resolvedData) => { |
||||
if (!error) { |
||||
this.event.trigger('newCall', [call]) |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
opt.event.udapp.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { |
||||
if (error) return |
||||
if (lookupOnly) return |
||||
// we go for that case if
|
||||
// in VM mode
|
||||
// in web3 mode && listen remix txs only
|
||||
if (!this._isListening) return // we don't listen
|
||||
if (this._loopId && executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network
|
||||
executionContext.web3().eth.getTransaction(txResult.transactionHash, (error, tx) => { |
||||
if (error) return console.log(error) |
||||
|
||||
addExecutionCosts(txResult, tx) |
||||
tx.envMode = executionContext.getProvider() |
||||
tx.status = txResult.result.status // 0x0 or 0x1
|
||||
this._resolve([tx], () => { |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
function addExecutionCosts (txResult, tx) { |
||||
if (txResult && txResult.result) { |
||||
if (txResult.result.vm) { |
||||
tx.returnValue = txResult.result.vm.return |
||||
if (txResult.result.vm.gasUsed) tx.executionCost = txResult.result.vm.gasUsed.toString(10) |
||||
} |
||||
if (txResult.result.gasUsed) tx.transactionCost = txResult.result.gasUsed.toString(10) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* define if txlistener should listen on the network or if only tx created from remix are managed |
||||
* |
||||
* @param {Bool} type - true if listen on the network |
||||
*/ |
||||
setListenOnNetwork (listenOnNetwork) { |
||||
this._listenOnNetwork = listenOnNetwork |
||||
if (this._loopId) { |
||||
clearInterval(this._loopId) |
||||
} |
||||
if (this._listenOnNetwork) { |
||||
this._startListenOnNetwork() |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* reset recorded transactions |
||||
*/ |
||||
init () { |
||||
this.blocks = [] |
||||
this.lastBlock = null |
||||
} |
||||
|
||||
/** |
||||
* start listening for incoming transactions |
||||
* |
||||
* @param {String} type - type/name of the provider to add |
||||
* @param {Object} obj - provider |
||||
*/ |
||||
startListening () { |
||||
this.init() |
||||
this._isListening = true |
||||
if (this._listenOnNetwork && executionContext.getProvider() !== 'vm') { |
||||
this._startListenOnNetwork() |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* stop listening for incoming transactions. do not reset the recorded pool. |
||||
* |
||||
* @param {String} type - type/name of the provider to add |
||||
* @param {Object} obj - provider |
||||
*/ |
||||
stopListening () { |
||||
if (this._loopId) { |
||||
clearInterval(this._loopId) |
||||
} |
||||
this._loopId = null |
||||
this._isListening = false |
||||
} |
||||
|
||||
_startListenOnNetwork () { |
||||
this._loopId = setInterval(() => { |
||||
var currentLoopId = this._loopId |
||||
executionContext.web3().eth.getBlockNumber((error, blockNumber) => { |
||||
if (this._loopId === null) return |
||||
if (error) return console.log(error) |
||||
if (currentLoopId === this._loopId && (!this.lastBlock || blockNumber > this.lastBlock)) { |
||||
if (!this.lastBlock) this.lastBlock = blockNumber - 1 |
||||
var current = this.lastBlock + 1 |
||||
this.lastBlock = blockNumber |
||||
while (blockNumber >= current) { |
||||
try { |
||||
this._manageBlock(current) |
||||
} catch (e) { |
||||
console.log(e) |
||||
} |
||||
current++ |
||||
} |
||||
} |
||||
}) |
||||
}, 2000) |
||||
} |
||||
|
||||
_manageBlock (blockNumber) { |
||||
executionContext.web3().eth.getBlock(blockNumber, true, (error, result) => { |
||||
if (!error) { |
||||
this._newBlock(Object.assign({type: 'web3'}, result)) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* try to resolve the contract name from the given @arg address |
||||
* |
||||
* @param {String} address - contract address to resolve |
||||
* @return {String} - contract name |
||||
*/ |
||||
resolvedContract (address) { |
||||
return this._resolvedContracts[address] |
||||
} |
||||
|
||||
/** |
||||
* try to resolve the transaction from the given @arg txHash |
||||
* |
||||
* @param {String} txHash - contract address to resolve |
||||
* @return {String} - contract name |
||||
*/ |
||||
resolvedTransaction (txHash) { |
||||
return this._resolvedTransactions[txHash] |
||||
} |
||||
|
||||
_newBlock (block) { |
||||
this.blocks.push(block) |
||||
this._resolve(block.transactions, () => { |
||||
this.event.trigger('newBlock', [block]) |
||||
}) |
||||
} |
||||
|
||||
_resolve (transactions, callback) { |
||||
async.each(transactions, (tx, cb) => { |
||||
this._resolveTx(tx, (error, resolvedData) => { |
||||
if (error) cb(error) |
||||
if (resolvedData) { |
||||
this.event.trigger('txResolved', [tx, resolvedData]) |
||||
} |
||||
this.event.trigger('newTransaction', [tx]) |
||||
cb() |
||||
}) |
||||
}, () => { |
||||
callback() |
||||
}) |
||||
} |
||||
|
||||
_resolveTx (tx, cb) { |
||||
var contracts = this._api.contracts() |
||||
if (!contracts) return cb() |
||||
var contractName |
||||
if (!tx.to || tx.to === '0x0') { // testrpc returns 0x0 in that case
|
||||
// contract creation / resolve using the creation bytes code
|
||||
// if web3: we have to call getTransactionReceipt to get the created address
|
||||
// if VM: created address already included
|
||||
var code = tx.input |
||||
contractName = this._tryResolveContract(code, contracts, true) |
||||
if (contractName) { |
||||
this._api.resolveReceipt(tx, (error, receipt) => { |
||||
if (error) return cb(error) |
||||
var address = receipt.contractAddress |
||||
this._resolvedContracts[address] = contractName |
||||
var fun = this._resolveFunction(contractName, contracts, tx, true) |
||||
if (this._resolvedTransactions[tx.hash]) { |
||||
this._resolvedTransactions[tx.hash].contractAddress = address |
||||
} |
||||
return cb(null, {to: null, contractName: contractName, function: fun, creationAddress: address}) |
||||
}) |
||||
return |
||||
} |
||||
return cb() |
||||
} else { |
||||
// first check known contract, resolve against the `runtimeBytecode` if not known
|
||||
contractName = this._resolvedContracts[tx.to] |
||||
if (!contractName) { |
||||
executionContext.web3().eth.getCode(tx.to, (error, code) => { |
||||
if (error) return cb(error) |
||||
if (code) { |
||||
var contractName = this._tryResolveContract(code, contracts, false) |
||||
if (contractName) { |
||||
this._resolvedContracts[tx.to] = contractName |
||||
var fun = this._resolveFunction(contractName, contracts, tx, false) |
||||
return cb(null, {to: tx.to, contractName: contractName, function: fun}) |
||||
} |
||||
} |
||||
return cb() |
||||
}) |
||||
return |
||||
} |
||||
if (contractName) { |
||||
var fun = this._resolveFunction(contractName, contracts, tx, false) |
||||
return cb(null, {to: tx.to, contractName: contractName, function: fun}) |
||||
} |
||||
return cb() |
||||
} |
||||
} |
||||
|
||||
_resolveFunction (contractName, compiledContracts, tx, isCtor) { |
||||
var contract = txHelper.getContract(contractName, compiledContracts) |
||||
if (!contract) { |
||||
console.log('txListener: cannot resolve ' + contractName) |
||||
return |
||||
} |
||||
var abi = contract.object.abi |
||||
var inputData = tx.input.replace('0x', '') |
||||
if (!isCtor) { |
||||
var methodIdentifiers = contract.object.evm.methodIdentifiers |
||||
for (var fn in methodIdentifiers) { |
||||
if (methodIdentifiers[fn] === inputData.substring(0, 8)) { |
||||
var fnabi = getFunction(abi, fn) |
||||
this._resolvedTransactions[tx.hash] = { |
||||
contractName: contractName, |
||||
to: tx.to, |
||||
fn: fn, |
||||
params: this._decodeInputParams(inputData.substring(8), fnabi) |
||||
} |
||||
if (tx.returnValue) { |
||||
this._resolvedTransactions[tx.hash].decodedReturnValue = txFormat.decodeResponse(tx.returnValue, fnabi) |
||||
} |
||||
return this._resolvedTransactions[tx.hash] |
||||
} |
||||
} |
||||
// fallback function
|
||||
this._resolvedTransactions[tx.hash] = { |
||||
contractName: contractName, |
||||
to: tx.to, |
||||
fn: '(fallback)', |
||||
params: null |
||||
} |
||||
} else { |
||||
var bytecode = contract.object.evm.bytecode.object |
||||
var params = null |
||||
if (bytecode && bytecode.length) { |
||||
params = this._decodeInputParams(inputData.substring(bytecode.length), getConstructorInterface(abi)) |
||||
} |
||||
this._resolvedTransactions[tx.hash] = { |
||||
contractName: contractName, |
||||
to: null, |
||||
fn: '(constructor)', |
||||
params: params |
||||
} |
||||
} |
||||
return this._resolvedTransactions[tx.hash] |
||||
} |
||||
|
||||
_tryResolveContract (codeToResolve, compiledContracts, isCreation) { |
||||
var found = null |
||||
txHelper.visitContracts(compiledContracts, (contract) => { |
||||
var bytes = isCreation ? contract.object.evm.bytecode.object : contract.object.evm.deployedBytecode.object |
||||
if (codeUtil.compareByteCode(codeToResolve, '0x' + bytes)) { |
||||
found = contract.name |
||||
return true |
||||
} |
||||
}) |
||||
return found |
||||
} |
||||
|
||||
_decodeInputParams (data, abi) { |
||||
data = ethJSUtil.toBuffer('0x' + data) |
||||
var inputTypes = [] |
||||
for (var i = 0; i < abi.inputs.length; i++) { |
||||
inputTypes.push(abi.inputs[i].type) |
||||
} |
||||
var decoded = ethJSABI.rawDecode(inputTypes, data) |
||||
decoded = ethJSABI.stringify(inputTypes, decoded) |
||||
var ret = {} |
||||
for (var k in abi.inputs) { |
||||
ret[abi.inputs[k].type + ' ' + abi.inputs[k].name] = decoded[k] |
||||
} |
||||
return ret |
||||
} |
||||
} |
||||
|
||||
// those function will be duplicate after the merged of the compile and run tabs split
|
||||
function getConstructorInterface (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 |
||||
} |
||||
|
||||
function getFunction (abi, fnName) { |
||||
fnName = fnName.split('(')[0] |
||||
for (var i = 0; i < abi.length; i++) { |
||||
if (abi[i].name === fnName) { |
||||
return abi[i] |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
module.exports = TxListener |
Loading…
Reference in new issue