parent
61f04c1cb1
commit
d4e0f1dd55
@ -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