diff --git a/packages/embark-typings/src/contract.d.ts b/packages/embark-typings/src/contract.d.ts index 7f467a84a..f56c9f06d 100644 --- a/packages/embark-typings/src/contract.d.ts +++ b/packages/embark-typings/src/contract.d.ts @@ -4,4 +4,5 @@ export interface Contract { abiDefinition: ABIDefinition[]; deployedAddress: string; className: string; + silent?: boolean; } diff --git a/packages/embark/src/lib/modules/console_listener/index.js b/packages/embark/src/lib/modules/console_listener/index.js index 6a9c56292..377f557e2 100644 --- a/packages/embark/src/lib/modules/console_listener/index.js +++ b/packages/embark/src/lib/modules/console_listener/index.js @@ -1,5 +1,6 @@ const async = require('async'); const utils = require('../../utils/utils.js'); +const {getAddressToContract, getTransactionParams} = require('../../utils/transactionUtils'); class ConsoleListener { constructor(embark, options) { @@ -27,7 +28,7 @@ class ConsoleListener { this.contractsDeployed = true; this._getContractsList((contractsList) => { - this._updateContractList(contractsList); + this.addressToContract = getAddressToContract(contractsList, this.addressToContract); }); }); @@ -57,37 +58,6 @@ class ConsoleListener { }); } - _updateContractList(contractsList) { - if (!contractsList) return; - contractsList.forEach(contract => { - if (!contract.deployedAddress) return; - - let address = contract.deployedAddress.toLowerCase(); - if (!this.addressToContract[address]) { - let funcSignatures = {}; - contract.abiDefinition - .filter(func => func.type === "function") - .map(func => { - const name = func.name + - '(' + - (func.inputs ? func.inputs.map(input => input.type).join(',') : '') + - ')'; - funcSignatures[utils.sha3(name).substring(0, 10)] = { - name, - abi: func, - functionName: func.name - }; - }); - - this.addressToContract[address] = { - name: contract.className, - functions: funcSignatures, - silent: contract.silent - }; - } - }); - } - _listenForLogRequests() { this.events.on('deploy:contract:receipt', receipt => { this.events.emit('contracts:log', { @@ -121,26 +91,14 @@ class ConsoleListener { if (!contract) { this.logger.info(`Contract log for unknown contract: ${JSON.stringify(request)}`); return this._getContractsList((contractsList) => { - this._updateContractList(contractsList); + this.addressToContract = getAddressToContract(contractsList, this.addressToContract); }); } const {name, silent} = contract; if (silent && !this.outputDone) { return; } - - const func = contract.functions[data.substring(0, 10)]; - const functionName = func.functionName; - - const decodedParameters = utils.decodeParams(func.abi.inputs, data.substring(10)); - let paramString = ""; - if (func.abi.inputs) { - func.abi.inputs.forEach((input) => { - let quote = input.type.indexOf("int") === -1 ? '"' : ''; - paramString += quote + decodedParameters[input.name] + quote + ", "; - }); - paramString = paramString.substring(0, paramString.length - 2); - } + const {functionName, paramString} = getTransactionParams(contract, data); gasUsed = utils.hexToNumber(gasUsed); blockNumber = utils.hexToNumber(blockNumber); diff --git a/packages/embark/src/lib/modules/tests/reporter.js b/packages/embark/src/lib/modules/tests/reporter.js index fe41d8200..34eebb549 100644 --- a/packages/embark/src/lib/modules/tests/reporter.js +++ b/packages/embark/src/lib/modules/tests/reporter.js @@ -1,6 +1,7 @@ const Base = require('mocha/lib/reporters/base'); const ms = require('mocha/lib/ms'); const color = Base.color; +const {getAddressToContract, getTransactionParams} = require('../../utils/transactionUtils'); class EmbarkApiSpec extends Base { constructor(runner, options) { @@ -43,28 +44,63 @@ class EmbarkSpec extends Base { self.stats.totalGasCost = 0; self.stats.test = {}; self.stats.test.gasUsed = 0; + self.contracts = []; + self.addressToContract = {}; + self.txLogs = []; function onContractReceipt(receipt) { - const fmt = color('bright pass', ' ') + - color('suite', ' %s') + - color('light', ' deployed for ') + - color(self.getGasColor(receipt.gasUsed), '%s') + - color('light', ' gas'); + self.embarkEvents.request('contracts:contract', receipt.className, (contract) => { + if (contract) { + self.contracts.push(contract); + self.addressToContract = getAddressToContract(self.contracts, self.addressToContract); + } + }); - console.log(fmt, receipt.className, receipt.gasUsed); + if (self.gasDetails) { + const fmt = color('bright pass', ' ') + + color('suite', ' %s') + + color('light', ' deployed for ') + + color(self.getGasColor(receipt.gasUsed), '%s') + + color('light', ' gas'); + + console.log(fmt, receipt.className, receipt.gasUsed); + } } - function onBlockHeader(blockHeader) { + async function onBlockHeader(blockHeader) { if(!self.listenForGas) { return; } self.stats.totalGasCost += blockHeader.gasUsed; self.stats.test.gasUsed += blockHeader.gasUsed; + + self.embarkEvents.request("blockchain:block:byNumber", blockHeader.number, (err, block) => { + if (err) { + return this.logger.error('Error getting block header', err.message || err); + } + // Don't know why, but sometimes we receive nothing + if (!block || !block.transactions) { + return; + } + block.transactions.forEach(transaction => { + self.contracts.find(contract => { + if (!contract.silent && contract.deployedAddress && transaction.to && contract.deployedAddress.toLowerCase() === transaction.to.toLowerCase()) { + const c = self.addressToContract[contract.deployedAddress.toLowerCase()]; + if (!c) { + return; + } + const {functionName, paramString} = getTransactionParams(c, transaction.input); + + self.txLogs.push(`\t\t- ${contract.className}.${functionName}(${paramString}) [${transaction.gas} gas]`); + return true; + } + return false; + }); + }); + }); } - if (self.gasDetails) { - self.embarkEvents.on("deploy:contract:receipt", onContractReceipt); - } + self.embarkEvents.on("deploy:contract:receipt", onContractReceipt); self.embarkEvents.on("block:header", onBlockHeader); self.embarkEvents.setCommandHandler("reporter:toggleGasListener", () => { self.listenForGas = !self.listenForGas; @@ -101,6 +137,7 @@ class EmbarkSpec extends Base { runner.on('test', function () { self.stats.test.gasUsed = 0; + self.contracts = []; }); runner.on('pass', function (test) { @@ -112,11 +149,15 @@ class EmbarkSpec extends Base { ' - ' + color(self.getGasColor(self.stats.test.gasUsed), '[%d gas]'); console.log(fmt, test.title, test.duration, self.stats.test.gasUsed); + self.txLogs.forEach(log => console.log(log)); + self.txLogs = []; }); runner.on('fail', function (test) { console.log(indent() + color('fail', ' %d) %s') + ' - ' + color(self.getGasColor(self.stats.test.gasUsed), '[%d gas]'), ++n, test.title, self.stats.test.gasUsed); + self.txLogs.forEach(log => console.log(log)); + self.txLogs = []; }); runner.once('end', function () { diff --git a/packages/embark/src/lib/utils/transactionUtils.ts b/packages/embark/src/lib/utils/transactionUtils.ts new file mode 100644 index 000000000..43e9540e3 --- /dev/null +++ b/packages/embark/src/lib/utils/transactionUtils.ts @@ -0,0 +1,73 @@ +import {Contract} from "embark"; +import {ABIDefinition} from "web3/eth/abi"; + +const utils = require("./utils"); + +export interface AddressToContract { + name: string; + functions: { [functionName: string]: FunctionSignature; }; + silent?: boolean; +} + +export interface AddressToContractArray { + [address: string]: AddressToContract; +} + +export interface FunctionSignature { + abi: ABIDefinition; + functionName?: string; + name: string; +} + +export function getAddressToContract(contractsList: Contract[], addressToContract: AddressToContractArray): AddressToContractArray { + if (!contractsList) { + return addressToContract; + } + contractsList.forEach((contract: Contract) => { + if (!contract.deployedAddress) { + return; + } + + const address = contract.deployedAddress.toLowerCase(); + if (addressToContract[address]) { + return; + } + const funcSignatures: { [name: string]: FunctionSignature } = {}; + contract.abiDefinition + .filter((func: ABIDefinition) => func.type === "function") + .map((func: ABIDefinition) => { + const name = `${func.name}(${func.inputs ? func.inputs.map((input) => input.type).join(",") : ""})`; + funcSignatures[utils.sha3(name).substring(0, 10)] = { + abi: func, + functionName: func.name, + name, + }; + }); + + addressToContract[address] = { + functions: funcSignatures, + name: contract.className, + silent: contract.silent, + }; + }); + return addressToContract; +} + +export function getTransactionParams(contract: AddressToContract, transactionInput: string): object { + const func = contract.functions[transactionInput.substring(0, 10)]; + const functionName = func.functionName; + + const decodedParameters = utils.decodeParams(func.abi.inputs, transactionInput.substring(10)); + let paramString = ""; + if (func.abi.inputs) { + func.abi.inputs.forEach((input) => { + const quote = input.type.indexOf("int") === -1 ? '"' : ""; + paramString += quote + decodedParameters[input.name] + quote + ", "; + }); + paramString = paramString.substring(0, paramString.length - 2); + } + return { + functionName, + paramString, + }; +} diff --git a/packages/embark/src/test/modules/console_listener.js b/packages/embark/src/test/modules/console_listener.js index 17335436a..0ca68fb34 100644 --- a/packages/embark/src/test/modules/console_listener.js +++ b/packages/embark/src/test/modules/console_listener.js @@ -3,6 +3,7 @@ const {expect} = require('chai'); const sinon = require('sinon'); const Events = require('../../lib/core/events'); const Logger = require('../../lib/core/logger'); +const transactionUtils = require('../../lib/utils/transactionUtils'); const ConsoleListener = require('../../lib/modules/console_listener'); const IPC = require('../../lib/core/ipc.js'); require('colors'); @@ -99,8 +100,8 @@ function resetTest() { events, logger, fs: { - existsSync: () => { return false }, - dappPath: () => { return "ok" } + existsSync: () => { return false; }, + dappPath: () => { return "ok"; } }, config: { contractsConfig: {} @@ -128,85 +129,9 @@ describe('Console Listener', function () { done(); }); - describe('#updateContractList', function () { - it('should not update contracts list', function (done) { - contractsList.deployedAddress = undefined; - consoleListener._updateContractList(contractsList); - - expect(consoleListener.addressToContract.length).to.be.equal(0); - done(); - }); - - it('should update contracts list', function (done) { - consoleListener._updateContractList(contractsList); - - expect(consoleListener.addressToContract["0x12345"]).to.deep.equal({ - name: "SimpleStorage", - functions: { - "0x2a1afcd9": { - "abi": { - "constant": true, - "inputs": [], - "name": "storedData", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - "functionName": "storedData", - "name": "storedData()" - }, - "0x60fe47b1": { - "abi": { - "constant": false, - "inputs": [ - { - "name": "x", - "type": "uint256" - } - ], - "name": "set", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - "functionName": "set", - "name": "set(uint256)" - }, - "0x6d4ce63c": { - "abi": { - "constant": true, - "inputs": [], - "name": "get", - "outputs": [ - { - "name": "retVal", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - "functionName": "get", - "name": "get()" - } - }, - silent: true - }); - done(); - }); - }); - describe('#listenForLogRequests', function () { it('should emit the correct contracts logs', function (done) { - consoleListener._updateContractList(contractsList); + transactionUtils.getAddressToContract(contractsList, consoleListener.addressToContract); consoleListener._onIpcLogRequest(ipcRequest); const expectedContractLog = { @@ -249,7 +174,7 @@ describe('Console Listener', function () { it('should emit a log for a non-contract log', function (done) { ipcRequest.type = 'something-other-than-contract-log'; - consoleListener._updateContractList(contractsList); + transactionUtils.getAddressToContract(contractsList, consoleListener.addressToContract); consoleListener._onIpcLogRequest(ipcRequest); expect(loggerInfos[0]).to.be.equal(JSON.stringify(ipcRequest)); diff --git a/packages/embark/src/test/transactionUtils.js b/packages/embark/src/test/transactionUtils.js new file mode 100644 index 000000000..af95e4b0c --- /dev/null +++ b/packages/embark/src/test/transactionUtils.js @@ -0,0 +1,170 @@ +/*globals describe, it, beforeEach*/ +const {expect} = require('chai'); +const transactionUtils = require('../lib/utils/transactionUtils'); +require('colors'); + +let contractsList; + +function resetTest() { + contractsList = [ + { + abiDefinition: [ + { + "constant": true, + "inputs": [], + "name": "storedData", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "x", + "type": "uint256" + } + ], + "name": "set", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "get", + "outputs": [ + { + "name": "retVal", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "initialValue", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + } + ], + deployedAddress: "0x12345", + className: "SimpleStorage", + silent: true + } + ]; +} + +describe('Transaction Utils', () => { + beforeEach(() => { + resetTest(); + }); + + describe('#getAddressToContract', () => { + it('should not update contracts list when no contracts', () => { + contractsList = []; + const result = transactionUtils.getAddressToContract(contractsList, {}); + + expect(result).to.deep.equal({}); + }); + + it('should not update contracts list when not deployed', () => { + contractsList[0].deployedAddress = undefined; + const result = transactionUtils.getAddressToContract(contractsList, {}); + + expect(result).to.deep.equal({}); + }); + + it('should update contracts list', () => { + const result = transactionUtils.getAddressToContract(contractsList, {}); + + expect(result).to.deep.equal({ + "0x12345": { + name: "SimpleStorage", + functions: { + "0x2a1afcd9": { + "abi": { + "constant": true, + "inputs": [], + "name": "storedData", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + "functionName": "storedData", + "name": "storedData()" + }, + "0x60fe47b1": { + "abi": { + "constant": false, + "inputs": [ + { + "name": "x", + "type": "uint256" + } + ], + "name": "set", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + "functionName": "set", + "name": "set(uint256)" + }, + "0x6d4ce63c": { + "abi": { + "constant": true, + "inputs": [], + "name": "get", + "outputs": [ + { + "name": "retVal", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + "functionName": "get", + "name": "get()" + } + }, + silent: true + } + }); + }); + }); + + describe('#getTransactionParams', () => { + it('should return the param string and function name', () => { + const result = transactionUtils.getAddressToContract(contractsList, {}); + const {functionName, paramString} = transactionUtils.getTransactionParams(result['0x12345'], '0x60fe47b100000000000000000000000099db99c77ad807f89829f5bda99527438f64a798'); + expect(functionName).to.equal('set'); + expect(paramString).to.equal('878372847193751743539905734564138820017777321880'); + + }); + }); +});