From 4bf2d990bfba8150111da3341c740befc6fab3b6 Mon Sep 17 00:00:00 2001 From: yann300 Date: Thu, 22 Jun 2017 19:42:35 +0200 Subject: [PATCH] tx listener --- src/app.js | 42 ++++++++ src/app/txListener.js | 226 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 src/app/txListener.js diff --git a/src/app.js b/src/app.js index 58d6f5e2af..d38e5e2aed 100644 --- a/src/app.js +++ b/src/app.js @@ -29,6 +29,7 @@ var FilePanel = require('./app/file-panel') var EditorPanel = require('./app/editor-panel') var RighthandPanel = require('./app/righthand-panel') var examples = require('./app/example-contracts') +var Txlistener = require('./app/txListener') var css = csjs` html { box-sizing: border-box; } @@ -847,6 +848,47 @@ function run () { var node = document.getElementById('staticanalysisView') node.insertBefore(staticanalysis.render(), node.childNodes[0]) + // ----------------- Tx listener ----------------- + + var txlistener = new Txlistener({ + appAPI: { + web3: function () { return executionContext.web3() }, + isVM: function () { return executionContext.isVM() }, + vm: function () { return executionContext.vm() }, + contracts: function () { + if (compiler.lastCompilationResult && compiler.lastCompilationResult.data) { + return compiler.lastCompilationResult.data.contracts + } + return null + }, + context: function () { + return executionContext.getProvider() + } + }, + appEvent: { + executionContext: executionContext.event, + udapp: udapp.event + } + }) + + txlistener.startListening() + + txlistener.event.register('newBlock', (block) => { + for (var t in block.transactions) { + var tx = block.transactions[t] + var resolvedTransaction = txlistener.resolvedTransaction(tx.hash) + var address = null + if (resolvedTransaction) { + address = resolvedTransaction.contractAddress ? resolvedTransaction.contractAddress : tx.to + } + console.log({ + tx: tx, + resolvedContract: txlistener.resolvedContract(address), + resolvedTransaction: resolvedTransaction + }) + } + }) + // ----------------- autoCompile ----------------- var autoCompile = document.querySelector('#autoCompile').checked if (config.exists('autoCompile')) { diff --git a/src/app/txListener.js b/src/app/txListener.js new file mode 100644 index 0000000000..f5a4e2340c --- /dev/null +++ b/src/app/txListener.js @@ -0,0 +1,226 @@ +'use strict' +var async = require('async') +var EventManager = require('ethereum-remix').lib.EventManager +var remix = require('ethereum-remix') +var codeUtil = remix.util.code +var Web3VMProvider = remix.web3.web3VMProvider + +/** + * 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._appAPI = opt.appAPI + this._web3VMProvider = new Web3VMProvider() // TODO this should maybe be put in app.js + this._web3VMProvider.setVM(opt.appAPI.vm()) + this._resolvedTransactions = {} + this._resolvedContracts = {} + this.init() + opt.appEvent.executionContext.register('contextChanged', (context) => { + if (this.loopId) { + this.startListening(context) + } + }) + opt.appEvent.udapp.register('transactionExecuted', (to, data, lookupOnly, txResult) => { + if (this.loopId && this._appAPI.isVM()) { + this._web3VMProvider.getTransaction(txResult.transactionHash, (error, tx) => { + if (error) return console.log(error) + this._newBlock({ + type: 'VM', + number: -1, + transactions: [tx] + }) + }) + } + }) + } + + /** + * 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.stopListening() + this.init() + if (this._appAPI.context() === 'vm') { + this.loopId = 'vm-listener' + } else { + this.loopId = setInterval(() => { + this._appAPI.web3().eth.getBlockNumber((error, blockNumber) => { + if (error) return console.log(error) + if (!this.lastBlock || blockNumber > this.lastBlock) { + this.lastBlock = blockNumber + this._appAPI.web3().eth.getBlock(this.lastBlock, true, (error, result) => { + if (!error) { + this._newBlock(Object.assign({type: 'web3'}, result)) + } + }) + } + }) + }, 2) + } + } + + currentWeb3 () { // TODO this should maybe be put in app.js + return this._appAPI.isVM() ? this._web3VMProvider : this._appAPI.web3() + } + + /** + * 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 + } + + /** + * 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, () => { + this.event.trigger('newBlock', [block]) + }) + } + + _resolve (block, callback) { + async.each(block.transactions, (tx, cb) => { + this._resolveTx(tx, cb) + }, () => { + callback() + }) + } + + _resolveTx (tx, cb) { + var contracts = this._appAPI.contracts() + if (!contracts) return cb() + var contractName + if (!tx.to) { + // 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, 'bytecode') + if (contractName) { + this._resolveCreationAddress(tx, (error, address) => { + if (error) return cb(error) + this._resolvedContracts[address] = contractName + this._resolveFunction(contractName, contracts, tx, true) + if (this._resolvedTransactions[tx.hash]) { + this._resolvedTransactions[tx.hash].contractAddress = address + } + return cb() + }) + return + } + return cb() + } else { + // first check known contract, resolve against the `runtimeBytecode` if not known + contractName = this._resolvedContracts[tx.to] + if (!contractName) { + this.currentWeb3().eth.getCode(tx.to, (error, code) => { + if (error) return cb(error) + if (code) { + var contractName = this._tryResolveContract(code, contracts, 'runtimeBytecode') + if (contractName) { + this._resolvedContracts[tx.to] = contractName + this._resolveFunction(contractName, contracts, tx, false) + return cb() + } + return cb() + } + return cb() + }) + return + } + if (contractName) { + this._resolveFunction(contractName, contracts, tx, false) + return cb() + } + return cb() + } + } + + _resolveCreationAddress (tx, cb) { + this.currentWeb3().eth.getTransactionReceipt(tx.hash, (error, receipt) => { + if (!error) { + cb(null, receipt.contractAddress) + } else { + cb(error) + } + }) + } + + _resolveFunction (contractName, compiledContracts, tx, isCtor) { + if (!isCtor) { + for (var fn in compiledContracts[contractName].functionHashes) { + if (compiledContracts[contractName].functionHashes[fn] === tx.input.replace('0x', '').substring(0, 8)) { + this._resolvedTransactions[tx.hash] = { + to: tx.to, + fn: fn, + params: null + } + } + // fallback function + this._resolvedTransactions[tx.hash] = { + to: tx.to, + fn: '(fallback)', + params: null + } + } else { + this._resolvedTransactions[tx.hash] = { + to: null, + fn: '(constructor)', + params: null + } + } + } + + _tryResolveContract (codeToResolve, compiledContracts, type) { + for (var k in compiledContracts) { + if (codeUtil.compareByteCode(codeToResolve, '0x' + compiledContracts[k][type])) { + return k + } + } + return null + } +} + +module.exports = TxListener