Merge pull request #684 from ethereum/refactor/move_tx_to_remix_lib

move most tx code from browser-solidity
pull/3094/head
yann300 7 years ago committed by GitHub
commit 58864e98b6
  1. 15
      remix-lib/index.js
  2. 6
      remix-lib/package.json
  3. 123
      remix-lib/src/execution/eventsDecoder.js
  4. 227
      remix-lib/src/execution/execution-context.js
  5. 76
      remix-lib/src/execution/txExecution.js
  6. 229
      remix-lib/src/execution/txFormat.js
  7. 122
      remix-lib/src/execution/txHelper.js
  8. 365
      remix-lib/src/execution/txListener.js

@ -17,6 +17,13 @@ var styleGuideDark = require('./src/ui/styleGuideDark')
var themeChooser = require('./src/ui/theme-chooser')
var Storage = require('./src/storage')
var EventsDecoder = require('./src/execution/eventsDecoder')
var txExecution = require('./src/execution/txExecution')
var txHelper = require('./src/execution/txHelper')
var txFormat = require('./src/execution/txFormat')
var txListener = require('./src/execution/txListener')
var executionContext = require('./src/execution/execution-context')
if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') {
module.exports = modules()
}
@ -49,6 +56,14 @@ function modules () {
styleGuide: styleGuide,
styleGuideDark: styleGuideDark,
themeChooser: themeChooser
},
execution: {
EventsDecoder: EventsDecoder,
txExecution: txExecution,
txHelper: txHelper,
executionContext: executionContext,
txFormat: txFormat,
txListener: txListener
}
}
}

@ -17,9 +17,13 @@
"babel-preset-es2015": "^6.24.0",
"babel-plugin-transform-object-assign": "^6.22.0",
"babel-eslint": "^7.1.1",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-0": "^6.24.1",
"babelify": "^7.3.0",
"fast-async": "^6.1.2",
"ethereumjs-util": "^4.5.0",
"ethereumjs-util": "^5.1.2",
"ethereumjs-abi": "https://github.com/ethereumjs/ethereumjs-abi",
"ethereumjs-vm": "2.3.1",
"web3": "^0.15.3",
"solc": "^0.4.13",
"standard": "^7.0.1",

@ -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…
Cancel
Save