├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .unlock-protocol.config.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codechecks.js ├── docs ├── advanced.md ├── codechecks.md ├── gasReporterOutput.md ├── gnosis.md ├── lifToken.md └── missingPriceData.md ├── index.js ├── lib ├── artifactor.js ├── codechecksReport.js ├── config.js ├── etherRouter.js ├── gasData.js ├── gasTable.js ├── mochaStats.js ├── proxyResolver.js ├── syncRequest.js ├── transactionWatcher.js └── utils.js ├── mock ├── buidler-metacoinjs-template.js ├── buidler.config.js ├── codechecks.yml ├── config-template.js ├── contracts │ ├── ConvertLib.sol │ ├── EncoderV2.sol │ ├── EtherRouter │ │ ├── EtherRouter.sol │ │ ├── Factory.sol │ │ ├── Resolver.sol │ │ ├── VersionA.sol │ │ └── VersionB.sol │ ├── MetaCoin.sol │ ├── Migrations.sol │ ├── MultiContractFile.sol │ ├── Undeployed.sol │ ├── VariableConstructor.sol │ ├── VariableCosts.sol │ └── Wallets │ │ └── Wallet.sol ├── migrations │ ├── 1_initial_migration.js │ └── 2_deploy_contracts.js ├── package.json ├── scripts │ ├── install_reporter.sh │ ├── integration_tests.sh │ ├── launch_testrpc.sh │ └── test.sh ├── test │ ├── TestMetacoin.sol │ ├── encoderv2.js │ ├── etherrouter.js │ ├── metacoin.js │ ├── multicontract.js │ ├── random.js │ ├── variableconstructor.js │ ├── variablecosts.js │ └── wallet.js └── truffle.js ├── package-lock.json ├── package.json └── scripts └── ci.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | unit-test: 5 | docker: 6 | - image: cimg/node:18.18.0 7 | steps: 8 | - checkout 9 | - run: 10 | name: Install 11 | command: npm install 12 | - run: 13 | name: Test 14 | command: npm test 15 | workflows: 16 | version: 2 17 | build: 18 | jobs: 19 | - unit-test 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://donate.unlock-protocol.com/?r=cgewecke/eth-gas-reporter -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # mocktests 15 | mock/node_modules 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # Mock debris 64 | mock/cache/ 65 | mock/artifacts/ 66 | mock/yarn.lock 67 | mock/codechecks.js 68 | mock/codechecksReport.json 69 | mock/cc.md 70 | 71 | 72 | -------------------------------------------------------------------------------- /.unlock-protocol.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "persistentCheckout": true, 3 | "icon": "", 4 | "callToAction": { 5 | "default": "Support the Eth Gas Reporter team and unlock your membership today! \n\nYou can make a donation by purchasing a key using your Ethereum wallet. The key is a non fungible token which represents your membership. " 6 | }, 7 | "locks": { 8 | "0x2E6caf50CEEC531d60818cd85029C2cf3a398842": { 9 | "name": "Eth Gas Reporter Community" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog: eth-gas-reporter 2 | 3 | # 0.2.27 / 2023-09-30 4 | 5 | - Remove @ethersproject/abi, use ethers.utils instead (https://github.com/cgewecke/eth-gas-reporter/issues/301) 6 | 7 | # 0.2.26 / 2023-09-29 8 | 9 | - Replace request-promise-native with axios / avoid default price API calls (https://github.com/cgewecke/eth-gas-reporter/issues/299) 10 | - Remove request package (https://github.com/cgewecke/eth-gas-reporter/issues/297) 11 | - Bump ethers version (https://github.com/cgewecke/eth-gas-reporter/issues/296) 12 | - Update Mocha to v10 (https://github.com/cgewecke/eth-gas-reporter/issues/295) 13 | 14 | # 0.2.23 / 2021-11-26 15 | 16 | - Add notes to README about missing price data & remote data fetching race condition 17 | - Add support for multiple gas price tokens (BNB, MATIC, AVAX, HR, MOVR) (https://github.com/cgewecke/eth-gas-reporter/pull/251) 18 | - Make @codechecks/client peer dep optional (https://github.com/cgewecke/eth-gas-reporter/pull/257) 19 | - Update @solidity-parser/parser to 0.14.0 (https://github.com/cgewecke/eth-gas-reporter/pull/261) 20 | 21 | # 0.2.22 / 2021-03-04 22 | 23 | - Update @solidity-parser/parser to ^0.12.0 (support Panic keyword in catch blocks) (https://github.com/cgewecke/eth-gas-reporter/issues/243) 24 | 25 | # 0.2.21 / 2021-02-16 26 | 27 | - Fix missing truffle migration deployments data (https://github.com/cgewecke/eth-gas-reporter/issues/240) 28 | - Upgrade solidity-parser/parser to 0.11.1 (https://github.com/cgewecke/eth-gas-reporter/issues/239) 29 | 30 | # 0.2.20 / 2020-12-01 31 | 32 | - Add support for remote contracts data pre-loading (hardhat-gas-reporter feature) 33 | 34 | # 0.2.19 / 2020-10-29 35 | 36 | - Delegate contract loading/parsing to artifactor & make optional (#227) 37 | 38 | # 0.2.18 / 2020-10-13 39 | 40 | - Support multiple codechecks reports per CI run 41 | - Add CI error threshold options: maxMethodDiff, maxDeploymentDiff 42 | - Add async collection methods for BuidlerEVM 43 | - Update solidity-parser/parser to 0.8.0 (contribution: @vicnaum) 44 | - Update dev deps / use Node 12 in CI 45 | 46 | # 0.2.17 / 2020-04-13 47 | 48 | - Use @solidity-parser/parser for better solc 0.6.x parsing 49 | - Upgrade Mocha to ^7.1.1 (to remove minimist vuln warning) 50 | - Stop crashing when parser or ABI Encoder fails 51 | - Update @ethersproject/abi to ^5.0.0-beta.146 (and unpin) 52 | 53 | # 0.2.16 / 2020-03-18 54 | 55 | - Use new coinmarketcap data API / make api key configurable. Old (un-gated) API has been taken offline. 56 | - Fix crashing when artifact transactionHash is stale after deleting previously migrated contracts 57 | 58 | # 0.2.15 / 2020-02-12 59 | 60 | - Use parser-diligence to parse Solidity 0.6.x 61 | - Add option to show full method signature 62 | 63 | # 0.2.14 / 2019-12-01 64 | 65 | - Add ABIEncoderV2 support by using @ethersproject/abi for ABI processing 66 | 67 | # 0.2.12 / 2019-09-30 68 | 69 | - Add try/catch block for codechecks.getValue so it doesn't throw when server is down. 70 | - Pin parser-antlr to 0.4.7 71 | 72 | # 0.2.11 / 2019-08-27 73 | 74 | - Fix syntax err on unresolved provider error msg (contribution: gnidan) 75 | - Add unlock-protocol funding ymls 76 | - Update abi-decoder deps / web3 77 | 78 | # 0.2.10 / 2019-08-08 79 | 80 | - Small codechecks table formatting improvements 81 | - Fix syntax error when codechecks errors on missing gas report 82 | 83 | # 0.2.9 / 2019-07-30 84 | 85 | - Optimize post-transaction data collection (reduce # of calls & cache addresses) 86 | - Catch codechecks server errors 87 | 88 | # 0.2.8 / 2019-07-27 89 | 90 | - Render codechecks CI table as markdown 91 | 92 | # 0.2.7 / 2019-07-27 93 | 94 | - Fix block limit basis bug 95 | - Fix bug affecting Truffle < v5.0.10 (crash because metadata not defined) 96 | - Add percentage diff columns to codechecks ci table / make table narrower 97 | - Slightly randomize gas consumption in tests 98 | - Begin running codechecks in CI for own tests 99 | 100 | # 0.2.6 / 2019-07-16 101 | 102 | - Stopped using npm-shrinkwrap, because it seemed to correlate w/ weird installation problems 103 | - Fix bug which caused outputFile option to crash due to misnamed variable 104 | 105 | # 0.2.5 / 2019-07-15 106 | 107 | - Upgrade lodash for because of vulnerability report (contribution @ppoliani) 108 | 109 | # 0.2.4 / 2019-07-08 110 | 111 | - Update abi-decoder to 2.0.1 to fix npm installation bug with bignumber.js fork 112 | 113 | # 0.2.3 / 2019-07-04 114 | 115 | - Bug fix to invoke user defined artifactType methods correctly 116 | 117 | # 0.2.2 / 2019-07-02 118 | 119 | - Add documentation about codechecks, buidler, advanced use cases. 120 | - Add artifactType option as a user defined function so people use with any compilation artifacts. 121 | - Add codechecks integration 122 | - Add buidler plugin integration 123 | - Remove shelljs due to GH security warning, execute ls command manually 124 | 125 | # 0.2.1 / 2019-06-19 126 | 127 | - Upgrade mocha from 4.1.0 to 5.2.0 128 | - Report solc version and settings info 129 | - Add EtherRouter method resolver logic (as option and example) 130 | - Add proxyResolver option & support discovery of delegated method calls identity 131 | - Add draft of 0x artifact handler 132 | - Add url option for non-truffle, non-buidler use 133 | - Add buidler truffle-v5 plugin support (preface to gas-reporter plugin in next release) 134 | - Completely reorganize and refactor 135 | 136 | # 0.2.0 / 2019-05-07 137 | 138 | - Add E2E tests in CI 139 | - Restore logic that matches tx signatures to contracts as a fallback when it's impossible to 140 | be certain which contract was called (contribution @ItsNickBarry) 141 | - Fix bug which crashed reporter when migrations linked un-deployed contracts 142 | 143 | # 0.1.12 / 2018-09-14 144 | 145 | - Allow contracts to share method signatures (contribution @wighawag) 146 | - Collect gas data for Migrations deployments (contribution @wighawag) 147 | - Add ability allow to specify a different src folder for contracts (contribution @wighawag) 148 | - Handle in-memory provider error correctly / use spec reporter if sync calls impossible (contribution @wighawag) 149 | - Default to only showing invoked methods in report 150 | 151 | # 0.1.10 / 2018-07-18 152 | 153 | - Update mocha from 3.5.3 to 4.10.0 (contribution ldub) 154 | - Update truffle to truffle@next to fix mocha issues (contribution ldub) 155 | - Modify binary checking to allow very long bytecodes / large contracts (contribution ben-kaufman) 156 | 157 | # 0.1.9 / 2018-06-27 158 | 159 | - Fix bug that caused test gas to include before hook gas consumption totals 160 | 161 | # 0.1.8 / 2018-06-26 162 | 163 | - Add showTimeSpent option to also show how long each test took (contribution @ldub) 164 | - Update cli-table2 to cli-table3 (contribution @DanielRuf) 165 | 166 | # 0.1.7 / 2018-05-27 167 | 168 | - Support reStructured text code-block output 169 | 170 | # 0.1.5 / 2018-05-15 171 | 172 | - Support multi-contract files by parsing files w/ solidity-parser-antlr 173 | 174 | # 0.1.4 / 2018-05-14 175 | 176 | - Try to work around web3 websocket provider by attempting connection over http://. 177 | `requestSync` doesn't support this otherwise. 178 | - Detect and identify binaries with library links, add to the deployments table 179 | - Add scripts to run geth in CI (not enabled) 180 | 181 | # 0.1.2 / 2018-04-20 182 | 183 | - Make compatible with Web 1.0 by creating own sync RPC wrapper. (Contribution: @area) 184 | 185 | # 0.1.1 / 2017-12-19 186 | 187 | - Use mochas own reporter options instead of .ethgas (still supported) 188 | - Add onlyCalledMethods option 189 | - Add outputFile option 190 | - Add noColors option 191 | 192 | # 0.1.0 / 2017-12-10 193 | 194 | - Require config gas price to be expressed in gwei (breaking change) 195 | - Use eth gas station API for gas price (it's more accurate) 196 | - Fix bug that caused table not to print if any test failed. 197 | 198 | # 0.0.15 / 2017-12-09 199 | 200 | - Fix ascii colorization bug that caused crashed during table generation. (Use colors/safe). 201 | 202 | # 0.0.14 / 2017-11-30 203 | 204 | - Fix bug that caused the error report at the end of test run not to be printed. 205 | 206 | # 0.0.13 / 2017-11-15 207 | 208 | - Filter throws by receipt.status if possible 209 | - Use testrpc 6.0.2 in tests, add view and pure methods to tests. 210 | 211 | # 0.0.12 / 2017-10-28 212 | 213 | - Add config. Add gasPrice and currency code options 214 | - Improve table clarity 215 | - Derive block.gasLimit from rpc 216 | 217 | # 0.0.11 / 2017-10-23 218 | 219 | - Add Travis CI 220 | - Fix bug that crashed reported when truffle could not find required file 221 | 222 | # 0.0.10 / 2017-10-22 223 | 224 | - Add examples 225 | 226 | # 0.0.10 / 2017-10-22 227 | 228 | - Filter deployment calls that throw from the stats 229 | 230 | # 0.0.8 / 2017-10-22 231 | 232 | - Filter method calls that throw from the stats 233 | - Add deployment stats 234 | - Add number of calls column 235 | 236 | # 0.0.6 / 2017-10-14 237 | 238 | - Stop showing zero gas usage in mocha output 239 | - Show currency rates and gwei gas price rates in table header 240 | \* Alphabetize table 241 | - Fix bug caused by unused methods reporting NaN 242 | - Fix failure to round avg gas use in table 243 | - Update dev deps to truffle4 beta 244 | 245 | # 0.0.5 / 2017-10-12 246 | 247 | - Thanks 248 | - Update image 249 | - Finish table formatting 250 | - Add some variable gas consumption contracts 251 | - Working table 252 | - Get map to work in the runner 253 | - Get gasStats file and percentage of limit working 254 | - Test using npm install 255 | - Add gasPrice data fetch, config logic 256 | - More tests 257 | - Abi encoding map. 258 | 259 | # 0.0.4 / 2017-10-01 260 | 261 | - Add visual inspection test 262 | - Fix bug that counted gas consumed in the test hooks 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 c-g-e-w-e-k-e-> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eth-gas-reporter 2 | 3 | [![npm version](https://badge.fury.io/js/eth-gas-reporter.svg)](https://badge.fury.io/js/eth-gas-reporter) 4 | [![Build Status](https://travis-ci.org/cgewecke/eth-gas-reporter.svg?branch=master)](https://travis-ci.org/cgewecke/eth-gas-reporter) 5 | [![Codechecks](https://raw.githubusercontent.com/codechecks/docs/master/images/badges/badge-default.svg?sanitize=true)](https://codechecks.io) 6 | [![buidler](https://buidler.dev/buidler-plugin-badge.svg?1)](https://github.com/cgewecke/buidler-gas-reporter) 7 | 8 | **A Mocha reporter for Ethereum test suites:** 9 | 10 | - Gas usage per unit test. 11 | - Metrics for method calls and deployments. 12 | - National currency costs of deploying and using your contract system. 13 | - CI integration with [codechecks](http://codechecks.io) 14 | - Simple installation for Truffle and Buidler 15 | - Use ETH, BNB, MATIC, AVAX, HT or MOVR price to calculate the gas price. 16 | 17 | ### Example output 18 | 19 | ![Screen Shot 2019-06-24 at 4 54 47 PM](https://user-images.githubusercontent.com/7332026/60059336-fa502180-96a0-11e9-92b8-3dd436a9b2f1.png) 20 | 21 | ### Installation and Config 22 | 23 | **[Truffle](https://www.trufflesuite.com/docs)** 24 | 25 | ``` 26 | npm install --save-dev eth-gas-reporter 27 | ``` 28 | 29 | ```javascript 30 | /* truffle-config.js */ 31 | module.exports = { 32 | networks: { ... }, 33 | mocha: { 34 | reporter: 'eth-gas-reporter', 35 | reporterOptions : { ... } // See options below 36 | } 37 | }; 38 | ``` 39 | 40 | **[Buidler](https://buidler.dev)** 41 | 42 | ``` 43 | npm install --save-dev buidler-gas-reporter 44 | ``` 45 | 46 | ```javascript 47 | /* buidler.config.js */ 48 | usePlugin('buidler-gas-reporter'); 49 | 50 | module.exports = { 51 | networks: { ... }, 52 | gasReporter: { ... } // See options below 53 | }; 54 | ``` 55 | 56 | **Other** 57 | 58 | This reporter should work with any build platform that uses Mocha and 59 | connects to an Ethereum client running as a separate process. There's more on advanced use cases 60 | [here](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/advanced.md). 61 | 62 | ### Continuous Integration (Travis and CircleCI) 63 | 64 | This reporter comes with a [codechecks](http://codechecks.io) CI integration that 65 | displays a pull request's gas consumption changes relative to its target branch in the Github UI. 66 | It's like coveralls for gas. The codechecks service is free for open source and maintained by MakerDao engineer [@krzkaczor](https://github.com/krzkaczor). 67 | 68 | Complete [set-up guide here](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/codechecks.md) (it's easy). 69 | 70 | ![Screen Shot 2019-06-18 at 12 25 49 PM](https://user-images.githubusercontent.com/7332026/59713894-47298900-91c5-11e9-8083-233572787cfa.png) 71 | 72 | ### Options 73 | 74 | :warning: **CoinMarketCap API change** :warning: 75 | 76 | Beginning March 2020, CoinMarketCap requires an API key to access currency market 77 | price data. The reporter uses an unprotected . You can get your own API key [here][55] and set it with the `coinmarketcap` option. (This service's free tier allows 10k reqs/mo) 78 | 79 | In order to retrieve the gas price of a particular blockchain, you can configure the `token` and `gasPriceApi` (API key rate limit may apply). 80 | 81 | **NOTE**: HardhatEVM and ganache-cli implement the Ethereum blockchain. To get accurate gas measurements for other chains you may need to run your tests against development clients developed specifically for those networks. 82 | 83 | | Option | Type | Default | Description | 84 | | ------------------ | ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 85 | | currency | _String_ | 'EUR' | National currency to represent gas costs in. Exchange rates loaded at runtime from the `coinmarketcap` api. Available currency codes can be found [here](https://coinmarketcap.com/api/documentation/v1/#section/Standards-and-Conventions). | 86 | | coinmarketcap | _String_ | (unprotected API key) | [API key][55] to use when fetching current market price data. (Use this if you stop seeing price data) | 87 | | gasPrice | _Number_ | (varies) | Denominated in `gwei`. Default is loaded at runtime from the `eth gas station` api | 88 | | token | _String_ | 'ETH' | The reference token for gas price | 89 | | gasPriceApi | _String_ | [Etherscan](https://api.etherscan.io/api?module=proxy&action=eth_gasPrice) | The API endpoint to retrieve the gas price. Find below other networks. | 90 | | outputFile | _String_ | stdout | File path to write report output to | 91 | | forceConsoleOutput | _Boolean_ | false | Print report output on console | 92 | | noColors | _Boolean_ | false | Suppress report color. Useful if you are printing to file b/c terminal colorization corrupts the text. | 93 | | onlyCalledMethods | _Boolean_ | true | Omit methods that are never called from report. | 94 | | rst | _Boolean_ | false | Output with a reStructured text code-block directive. Useful if you want to include report in RTD | 95 | | rstTitle | _String_ | "" | Title for reStructured text header (See Travis for example output) | 96 | | showTimeSpent | _Boolean_ | false | Show the amount of time spent as well as the gas consumed | 97 | | excludeContracts | _String[]_ | [] | Contract names to exclude from report. Ex: `['Migrations']` | 98 | | src | _String_ | "contracts" | Folder in root directory to begin search for `.sol` files. This can also be a path to a subfolder relative to the root, e.g. "planets/annares/contracts" | 99 | | url | _String_ | `web3.currentProvider.host` | RPC client url (ex: "http://localhost:8545") | 100 | | proxyResolver | _Function_ | none | Custom method to resolve identity of methods managed by a proxy contract. | 101 | | artifactType | _Function_ or _String_ | "truffle-v5" | Compilation artifact format to consume. (See [advanced use](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/advanced.md).) | 102 | | showMethodSig | _Boolean_ | false | Display complete method signatures. Useful when you have overloaded methods you can't tell apart. | 103 | | maxMethodDiff | _Number_ | undefined | Codechecks failure threshold, triggered when the % diff for any method is greater than `number` (integer) | 104 | | maxDeploymentDiff | _Number_ | undefined | Codechecks failure threshold, triggered when the % diff for any deployment is greater than `number` (integer) | 105 | 106 | [55]: https://coinmarketcap.com/api/pricing/ 107 | 108 | #### `token` and `gasPriceApi` options example 109 | 110 | | Network | token | gasPriceApi | 111 | | ------------------ | ----- | ---------------------------------------------------------------------- | 112 | | Ethereum (default) | ETH | https://api.etherscan.io/api?module=proxy&action=eth_gasPrice | 113 | | Binance | BNB | https://api.bscscan.com/api?module=proxy&action=eth_gasPrice | 114 | | Polygon | MATIC | https://api.polygonscan.com/api?module=proxy&action=eth_gasPrice | 115 | | Avalanche | AVAX | https://api.snowtrace.io/api?module=proxy&action=eth_gasPrice | 116 | | Heco | HT | https://api.hecoinfo.com/api?module=proxy&action=eth_gasPrice | 117 | | Moonriver | MOVR | https://api-moonriver.moonscan.io/api?module=proxy&action=eth_gasPrice | 118 | 119 | These APIs have [rate limits](https://docs.etherscan.io/support/rate-limits). Depending on the usage, it might require an [API Key](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics). 120 | 121 | > NB: Any gas price API call which returns a JSON-RPC response formatted like this is supported: `{"jsonrpc":"2.0","id":73,"result":"0x6fc23ac00"}`. 122 | 123 | ### Advanced Use 124 | 125 | An advanced use guide is available [here](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/advanced.md). Topics include: 126 | 127 | - Getting accurate gas data when using proxy contracts like EtherRouter or ZeppelinOS. 128 | - Configuring the reporter to work with non-truffle, non-buidler projects. 129 | 130 | ### Example Reports 131 | 132 | - [gnosis/gnosis-contracts](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/gnosis.md) 133 | - [windingtree/LifToken](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/lifToken.md) 134 | 135 | ### Usage Notes 136 | 137 | - Requires Node >= 8. 138 | - You cannot use `ganache-core` as an in-process provider for your test suite. The reporter makes sync RPC calls 139 | while collecting data and your tests will hang unless the client is launched as a separate process. 140 | - Method calls that throw are filtered from the stats. 141 | - Contracts that are only ever created by other contracts within Solidity are not shown in the deployments table. 142 | 143 | ### Troubleshooting 144 | 145 | - [Missing price data](./docs/missingPriceData.md) 146 | 147 | ### Contributions 148 | 149 | Feel free to open PRs or issues. There is an integration test and one of the mock test cases is expected to fail. If you're adding an option, you can vaildate it in CI by adding it to the mock options config located [here](https://github.com/cgewecke/eth-gas-reporter/blob/master/mock/config-template.js#L13-L19). 150 | 151 | ### Credits 152 | 153 | All the ideas in this utility have been borrowed from elsewhere. Many thanks to: 154 | 155 | - [@maurelian](https://github.com/maurelian) - Mocha reporting gas instead of time is his idea. 156 | - [@cag](https://github.com/cag) - The table borrows from / is based his gas statistics work for the Gnosis contracts. 157 | - [Neufund](https://github.com/Neufund/ico-contracts) - Block limit size ratios for contract deployments and euro pricing are borrowed from their `ico-contracts` test suite. 158 | 159 | ### Contributors 160 | 161 | - [@cgewecke](https://github.com/cgewecke) 162 | - [@rmuslimov](https://github.com/rmuslimov) 163 | - [@area](https://github.com/area) 164 | - [@ldub](https://github.com/ldub) 165 | - [@ben-kaufman](https://github.com/ben-kaufman) 166 | - [@wighawag](https://github.com/wighawag) 167 | - [@ItsNickBarry](https://github.com/ItsNickBarry) 168 | - [@krzkaczor](https://github.com/krzkaczor) 169 | - [@ppoliani](https://github.com/@ppoliani) 170 | - [@gnidan](https://github.com/gnidan) 171 | - [@fodisi](https://github.com/fodisi) 172 | - [@vicnaum](https://github.com/vicnaum) 173 | - [@markmiro](https://github.com/markmiro) 174 | - [@lucaperret](https://github.com/lucaperret) 175 | - [@ChristopherDedominici](https://github.com/ChristopherDedominici) 176 | -------------------------------------------------------------------------------- /codechecks.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const fs = require("fs"); 3 | const { codechecks } = require("@codechecks/client"); 4 | const CodeChecksReport = require("eth-gas-reporter/lib/codechecksReport"); 5 | 6 | /** 7 | * Consumed by codecheck command when user's .yml lists 8 | * `eth-gas-reporter/codechecks`. The reporter dumps collected 9 | * data to the project root whenever `process.env.CI` is true. This 10 | * file processes it and runs the relevant codechecks routines. 11 | * > 12 | * > Source: krzkaczor/truffle-codechecks. 13 | * > 14 | */ 15 | module.exports.default = async function gasReporter(options = {}) { 16 | let output; 17 | let file = "gasReporterOutput.json"; 18 | 19 | // Load gas reporter output 20 | try { 21 | output = JSON.parse(fs.readFileSync(file, "utf-8")); 22 | } catch (error) { 23 | const message = 24 | `Error: Couldn't load data from "${file}".\n` + 25 | `If you're using codechecks locally make sure you set ` + 26 | `the environment variable "CI" to "true" before running ` + 27 | `your tests. ( ex: CI=true npm test )`; 28 | 29 | console.log(message); 30 | return; 31 | } 32 | 33 | // Lets monorepo subcomponents individuate themselves 34 | output.namespace = options.name 35 | ? `${output.namespace}:${options.name}` 36 | : output.namespace; 37 | 38 | let report = new CodeChecksReport(output.config); 39 | report.generate(output.info); 40 | 41 | try { 42 | await codechecks.saveValue(output.namespace, report.newData); 43 | console.log(`Successful save: output.namespace was: ${output.namespace}`); 44 | } catch (err) { 45 | console.log( 46 | `If you have a chance, report this incident to the eth-gas-reporter github issues.` 47 | ); 48 | console.log(`Codechecks errored running 'saveValue'...\n${err}\n`); 49 | console.log(`output.namespace was: ${output.namespace}`); 50 | console.log(`Saved gas-reporter data was: ${report.newData}`); 51 | } 52 | 53 | // Exit early on merge commit / push build 54 | if (!codechecks.isPr()) { 55 | return; 56 | } 57 | 58 | // Get historical data for each pr commit 59 | try { 60 | output.config.previousData = await codechecks.getValue(output.namespace); 61 | } catch (err) { 62 | console.log( 63 | `If you have a chance, report this incident to the eth-gas-reporter github issues.` 64 | ); 65 | console.log(`Codechecks errored running 'getValue'...\n${err}\n`); 66 | return; 67 | } 68 | 69 | report = new CodeChecksReport(output.config); 70 | const table = report.generate(output.info); 71 | const shortDescription = report.getShortDescription(); 72 | 73 | // Support multiple reports 74 | const checkName = options.name ? `Gas Usage: ${options.name}` : `Gas Usage`; 75 | 76 | // Submit report 77 | try { 78 | const payload = { 79 | name: checkName, 80 | shortDescription: shortDescription, 81 | longDescription: table 82 | }; 83 | 84 | report.success 85 | ? await codechecks.success(payload) 86 | : await codechecks.failure(payload); 87 | } catch (err) { 88 | console.log( 89 | `If you have a chance, report this incident to the eth-gas-reporter github issues.` 90 | ); 91 | console.log(`Codechecks errored running .success or .failure\n${err}\n`); 92 | console.log(`Short description was: ${shortDescription}`); 93 | console.log(`Table was: ${table}`); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | ## Advanced Topics 2 | 3 | ### Configuration for non-buidler, non-truffle projects 4 | 5 | The reporter's only strict requirements are: 6 | 7 | - Mocha 8 | - The Ethereum client it connects to is _in a separate process_ and accepts calls over 9 | http. (You cannot use ganache-core as an in-process provider, for example.) 10 | 11 | Apart from that, it should be possible to run the reporter in any environment by configuring 12 | the following: 13 | 14 | - The root directory to begin searching for `.sol` files in via the `src` option. 15 | 16 | - The client `url` the reporter uses to send calls. 17 | 18 | - The method the reporter uses to acquire necessary info from solc compilation artifacts. 19 | Truffle and Buidler are supported out of the box but you can also use the `artifactType` 20 | option to define a function which meets your use case. This method 21 | receives a contract name (ex: `MetaCoin`) and must return an object as below: 22 | 23 | ```js 24 | // Example function 25 | function myArtifactProcessor(contractName){...} 26 | 27 | // Output 28 | { 29 | // Required 30 | abi: [] 31 | bytecode: "0xabc.." // solc: "0x" + contract.evm.bytecode.object 32 | 33 | // Optional 34 | deployedBytecode: "0xabc.." // solc: "0x" + contract.evm.deployedBytecode.object 35 | metadata: { 36 | compiler: { 37 | version: "0.5.8" 38 | }, 39 | settings: { 40 | optimizer: { 41 | enabled: true, 42 | runs: 500 43 | } 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | Example artifact handlers can be found [here](https://github.com/cgewecke/eth-gas-reporter/blob/master/lib/artifactor.js). 50 | 51 | ### Resolving method identities when using proxy contracts 52 | 53 | Many projects use a proxy contract strategy like 54 | [EtherRouter](https://github.com/PeterBorah/ether-router) or 55 | [ZeppelinOS](https://docs.zeppelinos.org/docs/start.html) to manage their upgradeability requirements. 56 | In practice this means method calls are routed through the 57 | proxy's fallback function and forwarded to the contract system's current implementation. 58 | 59 | You can define a helper method for the `proxyResolver` option 60 | which makes matching methods to contracts in these cases possible. 61 | The reporter automatically detects proxy use when methods are called 62 | on a contract whose ABI does not include their signature. It then 63 | invokes `proxyResolver` to make additional calls to the router contract and establish the true 64 | identity of the transaction target. 65 | 66 | **Resources** 67 | 68 | - An [implementation](https://github.com/cgewecke/eth-gas-reporter/blob/master/lib/etherRouter.js) for EtherRouter. 69 | - The [code](https://github.com/cgewecke/eth-gas-reporter/blob/master/lib/transactionWatcher.js) which consumes the proxyResolver. 70 | 71 | PRs are welcome if you have a proxy mechanism you'd like supported by default. 72 | 73 | ### Gas Reporter JSON output 74 | 75 | The gas reporter now writes the data it collects to a JSON file at `./gasReporterOutput.json` whenever the environment variable `CI` is set to true. An example of this output is [here](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/gasReporterOutput.md). 76 | You may find it useful as an input to more complex / long running gas analyses, better CI integrations, etc. 77 | -------------------------------------------------------------------------------- /docs/codechecks.md: -------------------------------------------------------------------------------- 1 | ## Guide to using Codechecks 2 | 3 | This reporter comes with a [codechecks](http://codechecks.io) CI integration that 4 | displays a pull request's gas consumption changes relative to its target branch in the Github UI. 5 | It's like coveralls for gas. The codechecks service is free for open source and maintained by MakerDao engineer [@krzkaczor](https://github.com/krzkaczor). 6 | 7 | ![Screen Shot 2019-06-18 at 12 25 49 PM](https://user-images.githubusercontent.com/7332026/59713894-47298900-91c5-11e9-8083-233572787cfa.png) 8 | 9 | ## Setup 10 | 11 | - Enable your project on [codechecks.io](https://codechecks.io/). Check out the 12 | [getting started guide](https://github.com/codechecks/docs/blob/master/getting-started.md). (All 13 | you really have to do is toggle your repo 'on' and copy-paste a token into your CI environment 14 | variables settings.) 15 | 16 | - Install the codechecks client library as a dev dependency: 17 | 18 | ``` 19 | npm install --save-dev @codechecks/client 20 | ``` 21 | 22 | - Add a `codechecks.yml` to your project's root directory as below: 23 | 24 | ```yml 25 | checks: 26 | - name: eth-gas-reporter/codechecks 27 | ``` 28 | 29 | - Run `codechecks` as a step in your build 30 | 31 | ```yml 32 | # CircleCI Example 33 | steps: 34 | - checkout 35 | - run: npm install 36 | - run: npm test 37 | - run: npx codechecks 38 | 39 | # Travis 40 | script: 41 | - npm test 42 | - npx codechecks 43 | ``` 44 | 45 | - You're done! :elephant: 46 | 47 | ### Multiple reports (for different CI jobs) 48 | 49 | For each report, create a codechecks.yml file, e.g 50 | 51 | ``` 52 | codechecks.testing.yml 53 | codechecks.production.yml 54 | ``` 55 | 56 | Use the `name` option in your `.yml` config to individuate the report: 57 | 58 | ```yml 59 | # codechecks.production.yml 60 | checks: 61 | - name: eth-gas-reporter/codechecks 62 | options: 63 | name: production 64 | ``` 65 | 66 | When running `codechecks` as a command in CI, specify the relevant codechecks config `.yml` 67 | 68 | ```yml 69 | production: 70 | docker: 71 | - image: circleci/node:10.13.0 72 | steps: 73 | - checkout 74 | - run: npm install 75 | - run: npm test 76 | - run: npx codechecks codechecks.production.yml 77 | ``` 78 | 79 | ### Failure thresholds 80 | 81 | You can ask Codechecks to report the CI run as a failure by using the `maxMethodDiff` and 82 | `maxDeploymentDiff` reporter options. These set the integer percentage difference 83 | over which an increase in gas usage by any method (or deployment) is forbidden. 84 | 85 | **Examples** 86 | 87 | ```js 88 | // truffle-config.js 89 | mocha: { 90 | reporter: "eth-gas-reporter", 91 | reporterOptions: { 92 | maxMethodDiff: 25, 93 | } 94 | } 95 | 96 | // buidler.config.js 97 | gasReporter: { 98 | maxMethodDiff: 25, 99 | } 100 | ``` 101 | 102 | ### Codechecks is new :wrench: 103 | 104 | Codechecks is new and some of its quirks are still being ironed out: 105 | 106 | - If you're using CircleCI and the report seems to be missing from the first 107 | build of a pull request, you can [configure your codechecks.yml's branch setting](https://github.com/codechecks/docs/blob/master/configuration.md#settings) to make it work as expected. 108 | - Both Travis and Circle must be configured to run on commit/push 109 | (this is true by default and will only be a problem if you've turned those builds off to save resources.) 110 | 111 | ### Diff Report Example 112 | 113 | Something like this will be displayed in the `checks` tab of your GitHub pull request. 114 | Increases in gas usage relative to the PR's target branch are highlighted with a red cross, decreases are 115 | highlighted with a green check. 116 | 117 | ## Deployments 118 | 119 | | | Gas | | Diff | Diff % | Block % | chf avg cost | 120 | | :---------------------- | --------: | :------------------------------------------------------------------: | --------------: | -----: | ------: | -----------: | 121 | | **ConvertLib** | 111,791 | | 0 | 0 | 1.4 % | 0.48 | 122 | | **EtherRouter** | 278,020 | | 0 | 0 | 3.5 % | 1.20 | 123 | | **Factory** | 324,331 | ![passed](https://travis-ci.com/images/stroke-icons/icon-passed.png) | [**-14,222**]() | 4% | 4.1 % | 1.40 | 124 | | **MetaCoin** | 358,572 | ![failed](https://travis-ci.com/images/stroke-icons/icon-failed.png) | [**+73,534**]() | 26% | 4.5 % | 1.55 | 125 | | **MultiContractFileA** | 90,745 | | 0 | 0 | 1.1 % | 0.39 | 126 | | **MultiContractFileB** | 90,745 | | 0 | 0 | 1.1 % | 0.39 | 127 | | **Resolver** | 430,580 | | 0 | 0 | 5.4 % | 1.86 | 128 | | **VariableConstructor** | 1,001,890 | | 0 | 0 | 12.5 % | 4.34 | 129 | | **VariableCosts** | 930,528 | | 0 | 0 | 11.6 % | 4.03 | 130 | | **VersionA** | 88,665 | | 0 | 0 | 1.1 % | 0.38 | 131 | | **Wallet** | 217,795 | | 0 | 0 | 2.7 % | 0.94 | 132 | 133 | ## Methods 134 | 135 | | | Gas | | Diff | Diff % | Calls | chf avg cost | 136 | | :--------------------------- | ------: | :------------------------------------------------------------------: | ------------: | -----: | ----: | -----------: | 137 | | **EtherRouter** | | | | | | | 138 | |        *setResolver* | 43,192 | | 0 | 0 | 1 | 0.19 | 139 | | **Factory** | | | | | | | 140 | |        *deployVersionB* | 107,123 | | 0 | 0 | 1 | 0.46 | 141 | | **MetaCoin** | | | | | | | 142 | |        *sendCoin* | 51,019 | | 0 | 0 | 1 | 0.22 | 143 | | **MultiContractFileA** | | | | | | | 144 | |        *hello* | 41,419 | | 0 | 0 | 1 | 0.18 | 145 | | **MultiContractFileB** | | | | | | | 146 | |        *goodbye* | 41,419 | | 0 | 0 | 1 | 0.18 | 147 | | **Resolver** | | | | | | | 148 | |        *register* | 37,633 | | 0 | 0 | 2 | 0.16 | 149 | | **VariableCosts** | | | | | | | 150 | |        *addToMap* | 90,341 | | 0 | 0 | 7 | 0.39 | 151 | |        *methodThatThrows* | 41,599 | | 0 | 0 | 2 | 0.18 | 152 | |        *otherContractMethod* | 57,407 | | 0 | 0 | 1 | 0.25 | 153 | |        *removeFromMap* | 36,481 | | 0 | 0 | 8 | 0.16 | 154 | |        *sendPayment* | 32,335 | | 0 | 0 | 1 | 0.14 | 155 | |        *setString* | 28,787 | ![passed](https://travis-ci.com/images/stroke-icons/icon-passed.png) | [**-2156**]() | 8% | 4 | 0.12 | 156 | |        *transferPayment* | 32,186 | | 0 | 0 | 1 | 0.14 | 157 | | **VersionA** | | | | | | | 158 | |        *setValue* | 25,663 | | 0 | 0 | 1 | 0.11 | 159 | | **VersionB** | | | | | | | 160 | |        *setValue* | 25,685 | | 0 | 0 | 1 | 0.11 | 161 | | **Wallet** | | | | | | | 162 | |        *sendPayment* | 32,181 | | 0 | 0 | 1 | 0.14 | 163 | |        *transferPayment* | 32,164 | | 0 | 0 | 1 | 0.14 | 164 | 165 | ## Build Configuration 166 | 167 | | Option | Settings | 168 | | ---------------------- | --------------------- | 169 | | solc: version | 0.5.0+commit.1d4f565a | 170 | | solc: optimized | false | 171 | | solc: runs | 200 | 172 | | gas: block limit | 8,000,000 | 173 | | gas: price | 21 gwei/gas | 174 | | gas: currency/eth rate | 206.15 chf/eth | 175 | 176 | ### Gas Reporter JSON output 177 | 178 | The gas reporter now writes the data it collects as JSON to a file at `./gasReporterOutput.json` whenever the environment variable `CI` is set to true. You can see an example of this output [here](https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/gasReporterOutput.md). 179 | You may find it useful as a base to generate more complex or long running gas analyses, develop CI integrations with, or make nicer tables. 180 | -------------------------------------------------------------------------------- /docs/gasReporterOutput.md: -------------------------------------------------------------------------------- 1 | ### Gas reporter JSON output 2 | 3 | A sample of the data written to file `gasReporterOutput.json` in the project directory root 4 | when `eth-gas-reporter` is run with the environment variable `CI` set to true. You 5 | can use this as an input to more complex or long running gas analyses, develop 6 | CI integrations with it, make a nicer table, etc. 7 | 8 | ```json 9 | { 10 | "namespace": "ethGasReporter", 11 | "config": { 12 | "blockLimit": 6718946, 13 | "currency": "eur", 14 | "ethPrice": "316.615237512", 15 | "gasPrice": 2, 16 | "outputFile": null, 17 | "rst": false, 18 | "rstTitle": "", 19 | "showTimeSpent": false, 20 | "srcPath": "contracts", 21 | "artifactType": "truffle-v5", 22 | "proxyResolver": null, 23 | "metadata": { 24 | "compiler": { 25 | "version": "0.5.0+commit.1d4f565a" 26 | }, 27 | "settings": { 28 | "evmVersion": "byzantium", 29 | "optimizer": { 30 | "enabled": false, 31 | "runs": 200 32 | }, 33 | }, 34 | }, 35 | "excludeContracts": [], 36 | "onlyCalledMethods": true, 37 | "url": "http://localhost:8545" 38 | }, 39 | "info": { 40 | "methods": { 41 | "EtherRouter_4e543b26": { 42 | "key": "4e543b26", 43 | "contract": "EtherRouter", 44 | "method": "setResolver", 45 | "gasData": [ 46 | 43192 47 | ], 48 | "numberOfCalls": 1 49 | }, 50 | "Resolver_1e59c529": { 51 | "key": "1e59c529", 52 | "contract": "Resolver", 53 | "method": "register", 54 | "gasData": [ 55 | 30133, 56 | 45133 57 | ], 58 | "numberOfCalls": 2 59 | }, 60 | ... 61 | }, 62 | "deployments": [ 63 | { 64 | "name": "ConvertLib", 65 | "bytecode": "0x60dd61002...", 66 | "deployedBytecode": "0x73000...", 67 | "gasData": [ 68 | 111791 69 | ] 70 | }, 71 | { 72 | "name": "EtherRouter", 73 | "bytecode": "0x608060...", 74 | "deployedBytecode": "0x60806040...", 75 | "gasData": [ 76 | 278020 77 | ] 78 | }, 79 | ... 80 | ], 81 | } 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/gnosis.md: -------------------------------------------------------------------------------- 1 | ### gnosis/gnosis-contracts 2 | 3 | at commit [c81cb17](https://github.com/gnosis/gnosis-contracts/tree/c81cb1736556c88a5cb6a7d9d2b3e2046705675b) 4 | 5 | 6 | ![screen shot 2017-10-22 at 6 19 35 pm](https://user-images.githubusercontent.com/7332026/31868872-83122218-b758-11e7-8162-2814e8b384e1.png) -------------------------------------------------------------------------------- /docs/lifToken.md: -------------------------------------------------------------------------------- 1 | ### windingtree/LifToken 2 | 3 | at commit [1b900a8](https://github.com/windingtree/LifToken/tree/1b900a8c24b18e6c284ca783311f75ece227b79b) 4 | 5 | ![screen shot 2017-10-22 at 5 39 25 pm](https://user-images.githubusercontent.com/7332026/31868192-2a580c30-b750-11e7-87b6-20508216e33d.png) 6 | -------------------------------------------------------------------------------- /docs/missingPriceData.md: -------------------------------------------------------------------------------- 1 | # Missing Price Data 2 | 3 | Two possible reasons for this: 4 | - [Default API key](https://github.com/cgewecke/eth-gas-reporter/blob/23fc57687b4e190c7e28571a14773d96cdbf7d63/lib/config.js#L12) reached a usage cap. 5 | - The tests ran too quickly and price data couldn't be fetched before returning the test. You can manually slow down your tests with a dummy 6 | 7 | To slow down unit tests, you can add a dummy test like this: 8 | ```js 9 | // Wait so the reporter has time to fetch and return prices from APIs. 10 | // https://github.com/cgewecke/eth-gas-reporter/issues/254 11 | describe("eth-gas-reporter workaround", () => { 12 | it("should kill time", (done) => { 13 | setTimeout(done, 2000); 14 | }); 15 | }); 16 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const mocha = require("mocha"); 2 | const inherits = require("util").inherits; 3 | const Base = mocha.reporters.Base; 4 | const color = Base.color; 5 | const log = console.log; 6 | const utils = require("./lib/utils"); 7 | const Config = require("./lib/config"); 8 | const TransactionWatcher = require("./lib/transactionWatcher"); 9 | const GasTable = require("./lib/gasTable"); 10 | const SyncRequest = require("./lib/syncRequest"); 11 | const mochaStats = require("./lib/mochaStats"); 12 | 13 | /** 14 | * Based on the Mocha 'Spec' reporter. Watches an Ethereum test suite run 15 | * and collects data about method & deployments gas usage. Mocha executes the hooks 16 | * in this reporter synchronously so any client calls here should be executed 17 | * via low-level RPC interface using sync-request. (see /lib/syncRequest) 18 | * An exception is made for fetching gas & currency price data from coinmarketcap and 19 | * ethgasstation (we hope that single call will complete by the time the tests finish running) 20 | * 21 | * @param {Object} runner mocha's runner 22 | * @param {Object} options reporter.options (see README example usage) 23 | */ 24 | function Gas(runner, options) { 25 | // Spec reporter 26 | Base.call(this, runner, options); 27 | 28 | // Initialize stats for Mocha 6+ epilogue 29 | if (!runner.stats) { 30 | mochaStats(runner); 31 | this.stats = runner.stats; 32 | } 33 | 34 | const self = this; 35 | 36 | let indents = 0; 37 | let n = 0; 38 | let failed = false; 39 | let indent = () => Array(indents).join(" "); 40 | 41 | // Gas reporter setup 42 | const config = new Config(options.reporterOptions); 43 | const sync = new SyncRequest(config.url); 44 | const watch = new TransactionWatcher(config); 45 | const table = new GasTable(config); 46 | 47 | // Expose internal methods to plugins 48 | if (typeof options.attachments === "object") { 49 | options.attachments.recordTransaction = watch.transaction.bind(watch); 50 | } 51 | 52 | // These call the cloud, start running them. 53 | utils.setGasAndPriceRates(config); 54 | 55 | // ------------------------------------ Runners ------------------------------------------------- 56 | 57 | runner.on("start", () => { 58 | watch.data.initialize(config); 59 | }); 60 | 61 | runner.on("suite", suite => { 62 | ++indents; 63 | log(color("suite", "%s%s"), indent(), suite.title); 64 | }); 65 | 66 | runner.on("suite end", () => { 67 | --indents; 68 | if (indents === 1) { 69 | log(); 70 | } 71 | }); 72 | 73 | runner.on("pending", test => { 74 | let fmt = indent() + color("pending", " - %s"); 75 | log(fmt, test.title); 76 | }); 77 | 78 | runner.on("test", () => { 79 | if (!config.provider) { 80 | watch.beforeStartBlock = sync.blockNumber(); 81 | } 82 | watch.data.resetAddressCache(); 83 | }); 84 | 85 | runner.on("hook end", hook => { 86 | if (hook.title.includes("before each") && !config.provider) { 87 | watch.itStartBlock = sync.blockNumber() + 1; 88 | } 89 | }); 90 | 91 | runner.on("pass", test => { 92 | let fmt; 93 | let fmtArgs; 94 | let gasUsedString; 95 | let consumptionString; 96 | let timeSpentString = color(test.speed, "%dms"); 97 | let gasUsed; 98 | 99 | if (!config.provider) { 100 | gasUsed = watch.blocks(); 101 | } 102 | 103 | if (gasUsed) { 104 | gasUsedString = color("checkmark", "%d gas"); 105 | 106 | if (config.showTimeSpent) { 107 | consumptionString = " (" + timeSpentString + ", " + gasUsedString + ")"; 108 | fmtArgs = [test.title, test.duration, gasUsed]; 109 | } else { 110 | consumptionString = " (" + gasUsedString + ")"; 111 | fmtArgs = [test.title, gasUsed]; 112 | } 113 | 114 | fmt = 115 | indent() + 116 | color("checkmark", " " + Base.symbols.ok) + 117 | color("pass", " %s") + 118 | consumptionString; 119 | } else { 120 | if (config.showTimeSpent) { 121 | consumptionString = " (" + timeSpentString + ")"; 122 | fmtArgs = [test.title, test.duration]; 123 | } else { 124 | consumptionString = ""; 125 | fmtArgs = [test.title]; 126 | } 127 | 128 | fmt = 129 | indent() + 130 | color("checkmark", " " + Base.symbols.ok) + 131 | color("pass", " %s") + 132 | consumptionString; 133 | } 134 | log.apply(null, [fmt, ...fmtArgs]); 135 | }); 136 | 137 | runner.on("fail", test => { 138 | failed = true; 139 | let fmt = indent() + color("fail", " %d) %s"); 140 | log(); 141 | log(fmt, ++n, test.title); 142 | }); 143 | 144 | runner.on("end", () => { 145 | table.generate(watch.data); 146 | self.epilogue(); 147 | }); 148 | } 149 | 150 | /** 151 | * Inherit from `Base.prototype`. 152 | */ 153 | inherits(Gas, Base); 154 | 155 | module.exports = Gas; 156 | -------------------------------------------------------------------------------- /lib/artifactor.js: -------------------------------------------------------------------------------- 1 | const SyncRequest = require("./syncRequest"); 2 | const { parseSoliditySources } = require("./utils"); 3 | 4 | /** 5 | * Supplies contract artifact data to the reporter in a format it can use. 6 | */ 7 | class Artifactor { 8 | constructor(config) { 9 | this.config = config; 10 | this.sync = new SyncRequest(config.url); 11 | } 12 | 13 | /** 14 | * Returns an array of contract info objects in the format consumed by ./gasData.js. 15 | * @return {Object[]} 16 | * @example 17 | * ``` 18 | * const artifactor = new Artifactor(config); 19 | * const contracts = artifactor.getContracts(); 20 | * > [ 21 | * > { 22 | * > name: "Example", 23 | * > artifact: { 24 | * > abi: [etc...], 25 | * > bytecode: "0x" + contract.evm.bytecode.object, // (solc key name) 26 | * > deployedBytecode: "0x" + contract.evm.deployedBytecode.object // (solc key name) 27 | * > }, 28 | * > ... 29 | * > ] 30 | */ 31 | getContracts() { 32 | if (typeof this.config.getContracts === "function") { 33 | return this.config.getContracts(); 34 | } 35 | 36 | const contracts = []; 37 | 38 | for (const name of parseSoliditySources(this.config)) { 39 | let artifact; 40 | 41 | try { 42 | artifact = this._require(name); 43 | } catch (e) { 44 | return; 45 | } 46 | 47 | contracts.push({ name: name, artifact: artifact }); 48 | } 49 | return contracts; 50 | } 51 | 52 | /** 53 | * Selects artifact translation strategy 54 | * @param {String} contractName 55 | * @return {Object} egr artifact 56 | */ 57 | _require(contractName) { 58 | // User defined 59 | if (typeof this.config.artifactType === "function") 60 | return this.config.artifactType(contractName); 61 | 62 | // Built-in 63 | switch (this.config.artifactType) { 64 | case "truffle-v5": 65 | return this._truffleArtifactor(contractName); 66 | case "0xProject-v2": 67 | return this._0xArtifactor(contractName); 68 | case "buidler-v1": 69 | return this._buidlerArtifactor(contractName); 70 | case "ethpm": 71 | default: 72 | return this._truffleArtifactor(contractName); 73 | } 74 | } 75 | 76 | /** 77 | * Truffle artifact translator 78 | * @param {String} contractName 79 | * @return {Object} egr artifact 80 | */ 81 | _truffleArtifactor(contractName) { 82 | let deployed; 83 | let metadata; 84 | 85 | const artifact = artifacts.require(contractName); 86 | 87 | const contract = { 88 | abi: artifact.abi, 89 | bytecode: artifact.bytecode, 90 | deployedBytecode: artifact.deployedBytecode 91 | }; 92 | 93 | // These fields are not defined for all conditions 94 | // or truffle versions. Catching because truffle 95 | // is sometimes weird re: artifact access. 96 | try { 97 | const networkId = !this.config.provider ? this.sync.getNetworkId() : null; 98 | deployed = artifact.networks[networkId]; 99 | metadata = artifact.metadata; 100 | } catch (err) {} 101 | 102 | // Migrations deployed data 103 | if (deployed) { 104 | contract.deployed = { 105 | address: deployed.address, 106 | transactionHash: deployed.transactionHash 107 | }; 108 | } 109 | 110 | if (metadata) { 111 | this.config.metadata = JSON.parse(metadata); 112 | } 113 | 114 | return contract; 115 | } 116 | 117 | /** 118 | * [DEPRECATED] 119 | * Buidler artifact translator. Solc info (metadata) is attached to config 120 | * at the buidler plugin 121 | * @param {String} contractName 122 | * @return {Object} egr artifact 123 | */ 124 | _buidlerArtifactor(contractName) { 125 | const artifact = artifacts.require(contractName); 126 | 127 | const contract = { 128 | abi: artifact.abi, 129 | bytecode: this._normalizeBytecode(artifact.bytecode) 130 | }; 131 | 132 | return contract; 133 | } 134 | 135 | /** 136 | * [EXPERIMENTAL] 137 | * 0x artifact translator. Untested stub. 138 | * @param {String} contractName 139 | * @return {Object} egr artifact 140 | */ 141 | _0xArtifactor(contractName) { 142 | const contract = {}; 143 | const artifact = require(`./artifacts/${contractName}.json`); 144 | 145 | contract.abi = artifact.compilerOutput.abi; 146 | contract.bytecode = artifact.compilerOutput.evm.bytecode.object; 147 | contract.deployedBytecode = artifact.compilerOutput.evm.deployedBytecode; 148 | 149 | this.config.metadata = { 150 | compiler: { 151 | version: artifact.compiler.version 152 | }, 153 | settings: { 154 | optimizer: { 155 | enabled: artifact.compiler.settings.optimizer.enabled, 156 | runs: artifact.compiler.settings.optimizer.runs 157 | } 158 | } 159 | }; 160 | 161 | return contract; 162 | } 163 | 164 | _normalizeBytecode(code) { 165 | if (typeof code === "string" && code.length && !this._isHexPrefixed(code)) { 166 | return `0x${code}`; 167 | } else if (!code) { 168 | return `0x`; 169 | } else { 170 | return code; 171 | } 172 | } 173 | 174 | _isHexPrefixed(str) { 175 | return str.slice(0, 2) === "0x"; 176 | } 177 | } 178 | 179 | module.exports = Artifactor; 180 | -------------------------------------------------------------------------------- /lib/codechecksReport.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const ethers = require("ethers"); 3 | const fs = require("fs"); 4 | const table = require("markdown-table"); 5 | const utils = require("./utils"); 6 | const util = require("util"); 7 | 8 | class CodeChecksReport { 9 | constructor(config) { 10 | this.config = config; 11 | this.increases = 0; 12 | this.decreases = 0; 13 | this.reportIsNew = true; 14 | this.success = true; 15 | 16 | this.previousData = config.previousData || { methods: {}, deployments: {} }; 17 | this.newData = { methods: {}, deployments: {} }; 18 | } 19 | 20 | /** 21 | * Generates a gas usage difference report for CodeCheck 22 | * @param {Object} info GasData instance with `methods` and `deployments` data 23 | */ 24 | generate(info) { 25 | let highlightedDiff; 26 | let passFail; 27 | let alignment; 28 | const addedContracts = []; 29 | 30 | // --------------------------------------------------------------------------------------------- 31 | // Assemble section: Build Configuration 32 | // --------------------------------------------------------------------------------------------- 33 | let gwei = "-"; 34 | let currency = "-"; 35 | let rate = "-"; 36 | 37 | const solc = utils.getSolcInfo(this.config.metadata); 38 | 39 | const token = this.config.token.toLowerCase(); 40 | if (this.config.ethPrice && this.config.gasPrice) { 41 | gwei = `${parseInt(this.config.gasPrice)} gwei/gas`; 42 | currency = `${this.config.currency.toLowerCase()}`; 43 | rate = `${parseFloat(this.config.ethPrice).toFixed( 44 | 2 45 | )} ${currency}/${token}`; 46 | } 47 | 48 | const configRows = [ 49 | ["Option", "Settings"], 50 | ["solc: version", solc.version], 51 | ["solc: optimized", solc.optimizer], 52 | ["solc: runs", solc.runs], 53 | ["gas: block limit", ethers.utils.commify(info.blockLimit)], 54 | ["gas: price", gwei], 55 | [`gas: currency/${token} rate`, rate] 56 | ]; 57 | 58 | const configTable = table(configRows); 59 | 60 | // --------------------------------------------------------------------------------------------- 61 | // Assemble section: methods 62 | // --------------------------------------------------------------------------------------------- 63 | 64 | const methodRows = []; 65 | const methodHeader = [ 66 | " ", 67 | "Gas", 68 | " ", 69 | "Diff", 70 | "Diff %", 71 | "Calls", 72 | `${currency} avg` 73 | ]; 74 | 75 | _.forEach(info.methods, (data, methodId) => { 76 | if (!data) return; 77 | 78 | let stats = {}; 79 | 80 | if (data.gasData.length) { 81 | const total = data.gasData.reduce((acc, datum) => acc + datum, 0); 82 | stats.average = Math.round(total / data.gasData.length); 83 | 84 | stats.cost = 85 | this.config.ethPrice && this.config.gasPrice 86 | ? utils.gasToCost( 87 | stats.average, 88 | this.config.ethPrice, 89 | this.config.gasPrice 90 | ) 91 | : "-"; 92 | } 93 | 94 | stats.diff = this.getMethodDiff(methodId, stats.average); 95 | stats.percentDiff = this.getMethodPercentageDiff(methodId, stats.average); 96 | 97 | highlightedDiff = this.getHighlighting(stats.diff); 98 | passFail = this.getPassFail(stats.diff); 99 | 100 | if (data.numberOfCalls > 0) { 101 | // Contracts name row 102 | if (!addedContracts.includes(data.contract)) { 103 | addedContracts.push(data.contract); 104 | 105 | const titleSection = [ 106 | this.entitle(data.contract), 107 | " ", 108 | " ", 109 | " ", 110 | " ", 111 | " ", 112 | " " 113 | ]; 114 | titleSection.contractName = data.contract; 115 | titleSection.methodName = "0"; 116 | methodRows.push(titleSection); 117 | } 118 | 119 | // Method row 120 | const methodSection = [ 121 | this.indent(data.method), 122 | ethers.utils.commify(stats.average), 123 | passFail, 124 | highlightedDiff, 125 | stats.percentDiff, 126 | data.numberOfCalls.toString(), 127 | stats.cost.toString() 128 | ]; 129 | methodSection.contractName = data.contract; 130 | methodSection.methodName = data.method; 131 | 132 | methodRows.push(methodSection); 133 | this.newData.methods[methodId] = stats.average; 134 | } 135 | }); 136 | 137 | methodRows.sort((a, b) => { 138 | const contractName = a.contractName.localeCompare(b.contractName); 139 | const methodName = a.methodName.localeCompare(b.methodName); 140 | return contractName || methodName; 141 | }); 142 | 143 | alignment = { align: ["l", "r", "c", "r", "r", "r", "r", "r"] }; 144 | methodRows.unshift(methodHeader); 145 | const methodTable = table(methodRows, alignment); 146 | 147 | // --------------------------------------------------------------------------------------------- 148 | // Assemble section: deployments 149 | // --------------------------------------------------------------------------------------------- 150 | const deployRows = []; 151 | const deployHeader = [ 152 | " ", 153 | "Gas", 154 | " ", 155 | "Diff", 156 | "Diff %", 157 | "Block %", 158 | `${currency} avg` 159 | ]; 160 | 161 | // Alphabetize contract names 162 | info.deployments.sort((a, b) => a.name.localeCompare(b.name)); 163 | 164 | info.deployments.forEach(contract => { 165 | let stats = {}; 166 | if (!contract.gasData.length) return; 167 | 168 | const total = contract.gasData.reduce((acc, datum) => acc + datum, 0); 169 | stats.average = Math.round(total / contract.gasData.length); 170 | stats.percent = utils.gasToPercentOfLimit(stats.average, info.blockLimit); 171 | 172 | stats.cost = 173 | this.config.ethPrice && this.config.gasPrice 174 | ? utils.gasToCost( 175 | stats.average, 176 | this.config.ethPrice, 177 | this.config.gasPrice 178 | ) 179 | : "-"; 180 | 181 | stats.diff = this.getDeploymentDiff(contract.name, stats.average); 182 | stats.percentDiff = this.getDeploymentPercentageDiff( 183 | contract.name, 184 | stats.average 185 | ); 186 | 187 | highlightedDiff = this.getHighlighting(stats.diff); 188 | passFail = this.getPassFail(stats.diff); 189 | 190 | const section = [ 191 | this.entitle(contract.name), 192 | ethers.utils.commify(stats.average), 193 | passFail, 194 | highlightedDiff, 195 | stats.percentDiff, 196 | `${stats.percent} %`, 197 | stats.cost.toString() 198 | ]; 199 | 200 | deployRows.push(section); 201 | this.newData.deployments[contract.name] = stats.average; 202 | }); 203 | 204 | alignment = { align: ["l", "r", "c", "r", "r", "r", "r"] }; 205 | deployRows.unshift(deployHeader); 206 | const deployTable = table(deployRows, alignment); 207 | 208 | // --------------------------------------------------------------------------------------------- 209 | // Final assembly 210 | // --------------------------------------------------------------------------------------------- 211 | 212 | const configTitle = "## Build Configuration\n"; 213 | const methodTitle = "## Methods\n"; 214 | const deployTitle = "## Deployments\n"; 215 | 216 | const md = 217 | deployTitle + 218 | deployTable + 219 | `\n\n` + 220 | methodTitle + 221 | methodTable + 222 | `\n\n` + 223 | configTitle + 224 | configTable + 225 | `\n\n`; 226 | 227 | // --------------------------------------------------------------------------------------------- 228 | // Finish 229 | // --------------------------------------------------------------------------------------------- 230 | return md; 231 | } 232 | 233 | getDiff(previousVal, currentVal) { 234 | if (typeof previousVal === "number") { 235 | const diff = currentVal - previousVal; 236 | 237 | if (diff > 0) this.increases++; 238 | if (diff < 0) this.decreases++; 239 | 240 | this.reportIsNew = false; 241 | return diff; 242 | } 243 | return "-"; 244 | } 245 | 246 | getPercentageDiff(previousVal, currentVal, maxThreshold) { 247 | let sign = ""; 248 | 249 | if (typeof previousVal === "number") { 250 | const diff = Math.round(((currentVal - previousVal) / previousVal) * 100); 251 | 252 | if (diff > 0) { 253 | sign = "+"; 254 | 255 | if (typeof maxThreshold === "number" && diff > maxThreshold) { 256 | this.success = false; 257 | } 258 | } 259 | 260 | return `${sign}${diff}%`; 261 | } 262 | return "-"; 263 | } 264 | 265 | getMethodDiff(id, currentVal) { 266 | return this.getDiff(this.previousData.methods[id], currentVal); 267 | } 268 | 269 | getMethodPercentageDiff(id, currentVal) { 270 | return this.getPercentageDiff( 271 | this.previousData.methods[id], 272 | currentVal, 273 | this.config.maxMethodDiff 274 | ); 275 | } 276 | 277 | getDeploymentDiff(id, currentVal) { 278 | return this.getDiff(this.previousData.deployments[id], currentVal); 279 | } 280 | 281 | getDeploymentPercentageDiff(id, currentVal) { 282 | return this.getPercentageDiff( 283 | this.previousData.deployments[id], 284 | currentVal, 285 | this.config.maxDeploymentDiff 286 | ); 287 | } 288 | 289 | getPassFail(val) { 290 | const passed = `![passed](https://travis-ci.com/images/stroke-icons/icon-passed.png)`; 291 | const failed = `![failed](https://travis-ci.com/images/stroke-icons/icon-failed.png)`; 292 | 293 | if (val > 0) return failed; 294 | if (val < 0) return passed; 295 | return ""; 296 | } 297 | 298 | getHighlighting(val) { 299 | if (val > 0) return `[**+${ethers.utils.commify(val)}**]()`; 300 | if (val < 0) return `[**${ethers.utils.commify(val)}**]()`; 301 | return val; 302 | } 303 | 304 | getShortDescription() { 305 | const increasesItem = this.increases === 1 ? "item" : "items"; 306 | const decreasesItem = this.decreases === 1 ? "item" : "items"; 307 | 308 | if (this.increases > 0 && this.decreases > 0) { 309 | return ( 310 | `Gas usage increased for ${this.increases} ${increasesItem} and ` + 311 | `decreased for ${this.decreases} ${decreasesItem}` 312 | ); 313 | } else if (this.increases > 0) { 314 | return `Gas usage increased for ${this.increases} ${increasesItem}`; 315 | } else if (this.decreases > 0) { 316 | return `Gas usage decreased for ${this.decreases} ${decreasesItem}`; 317 | } else if (this.reportIsNew) { 318 | return `New gas usage report!`; 319 | } else { 320 | return `Gas usage remained the same`; 321 | } 322 | } 323 | 324 | indent(val) { 325 | return `       *${val}*`; 326 | } 327 | 328 | entitle(val) { 329 | return `**${val}**`; 330 | } 331 | } 332 | 333 | module.exports = CodeChecksReport; 334 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration defaults 3 | */ 4 | 5 | class Config { 6 | constructor(options = {}) { 7 | this.token = options.token || "ETH"; 8 | this.blockLimit = options.blockLimit || 6718946; 9 | this.defaultGasPrice = 5; 10 | 11 | this.currency = options.currency || "eur"; 12 | this.gasPriceApi = 13 | options.gasPriceApi || 14 | "https://api.etherscan.io/api?module=proxy&action=eth_gasPrice"; 15 | this.coinmarketcap = options.coinmarketcap || null; 16 | this.ethPrice = options.ethPrice || null; 17 | this.gasPrice = options.gasPrice || null; 18 | this.outputFile = options.outputFile || null; 19 | this.forceConsoleOutput = options.forceConsoleOutput || false; 20 | this.rst = options.rst || false; 21 | this.rstTitle = options.rstTitle || ""; 22 | this.showTimeSpent = options.showTimeSpent || false; 23 | this.srcPath = options.src || "contracts"; 24 | this.artifactType = options.artifactType || "truffle-v5"; 25 | this.getContracts = options.getContracts || null; 26 | this.noColors = options.noColors; 27 | this.proxyResolver = options.proxyResolver || null; 28 | this.metadata = options.metadata || null; 29 | this.showMethodSig = options.showMethodSig || false; 30 | this.provider = options.provider || null; 31 | this.maxMethodDiff = options.maxMethodDiff; 32 | this.maxDeploymentDiff = options.maxDeploymentDiff; 33 | 34 | this.excludeContracts = Array.isArray(options.excludeContracts) 35 | ? options.excludeContracts 36 | : []; 37 | 38 | this.onlyCalledMethods = options.onlyCalledMethods === false ? false : true; 39 | 40 | this.url = options.url 41 | ? this._normalizeUrl(options.url) 42 | : this.resolveClientUrl(); 43 | } 44 | 45 | /** 46 | * Tries to obtain the client url reporter's sync-requests will target. 47 | * @return {String} url e.g http://localhost:8545 48 | */ 49 | resolveClientUrl() { 50 | // Case: web3 globally available in mocha test context 51 | try { 52 | if (web3 && web3.currentProvider) { 53 | const cp = web3.currentProvider; 54 | 55 | // Truffle/Web3 http 56 | if (cp.host) return cp.host; 57 | 58 | // Truffle/Web3 websockets 59 | if (cp.connection) return this._normalizeUrl(cp.connection.url); 60 | } 61 | } catch (err) { 62 | // Web3 undefined 63 | } 64 | 65 | // Case: Failure 66 | const message = 67 | `ERROR: eth-gas-reporter was unable to resolve a client url ` + 68 | `from the provider available in your test context. Try setting the ` + 69 | `url as a mocha reporter option (ex: url='http://localhost:8545')`; 70 | 71 | console.log(message); 72 | process.exit(1); 73 | } 74 | 75 | /** 76 | * Forces websockets to http 77 | * @param {String} url e.g web3.provider.connection.url 78 | * @return {String} http:// prefixed url 79 | */ 80 | _normalizeUrl(url) { 81 | return url.replace("ws://", "http://"); 82 | } 83 | } 84 | 85 | module.exports = Config; 86 | -------------------------------------------------------------------------------- /lib/etherRouter.js: -------------------------------------------------------------------------------- 1 | const ethers = require("ethers"); 2 | 3 | /** 4 | * Example of a method that resolves the contract names of method calls routed through 5 | * an EtherRouter style contract. This function gets bound to the `this` property of 6 | * eth-gas-reporter's ProxyResolver class and inherits its resources including 7 | * helpers to match methods to contracts and a way of making synchronous calls to the client. 8 | * 9 | * Helper methods of this type receive a web3 transaction object representing a tx the reporter 10 | * could not deterministically associate with any contract. They rely on your knowledge 11 | * of a proxy contract's API to derive the correct contract name. 12 | * 13 | * Returns contract name matching the resolved address. 14 | * @param {Object} transaction result of web3.eth.getTransaction 15 | * @return {String} contract name 16 | */ 17 | function etherRouter(transaction) { 18 | let contractAddress; 19 | let contractName; 20 | 21 | try { 22 | const ABI = ["function resolver()", "function lookup(bytes4 sig)"]; 23 | const iface = new ethers.utils.Interface(ABI); 24 | 25 | // The tx passed to this method had input data which didn't map to any methods on 26 | // the contract it was sent to. It's possible the tx's `to` address points to 27 | // an EtherRouter contract which is designed to forward calls. We'll grab the 28 | // method signature and ask the router if it knows who the intended recipient is. 29 | const signature = transaction.input.slice(0, 10); 30 | 31 | // EtherRouter has a public state variable called `resolver()` which stores the 32 | // address of a contract which maps method signatures to their parent contracts. 33 | // Lets fetch it .... 34 | const resolverAddress = this.sync.call( 35 | { 36 | to: transaction.to, 37 | data: iface.functions.resolver.encode([]) 38 | }, 39 | transaction.blockNumber 40 | ); 41 | 42 | // Now we'll call the Resolver's `lookup(sig)` method to get the address of the contract 43 | // our tx was actually getting forwarded to. 44 | contractAddress = this.sync.call( 45 | { 46 | to: ethers.utils.hexStripZeros(resolverAddress), 47 | data: iface.functions.lookup.encode([signature]) 48 | }, 49 | transaction.blockNumber 50 | ); 51 | // Don't forget this is all a bit speculative... 52 | } catch (err) { 53 | this.unresolvedCalls++; 54 | return; 55 | } 56 | 57 | // With the correct address, we can use the ProxyResolver class's 58 | // data.getNameByAddress and/or resolveByDeployedBytecode methods 59 | // (both are available in this scope, bound to `this`) to derive 60 | // the target contract's name. 61 | if (contractAddress) { 62 | contractAddress = ethers.utils.hexStripZeros(contractAddress); 63 | contractName = this.data.getNameByAddress(contractAddress); 64 | 65 | // Try to resolve by deployedBytecode 66 | if (contractName) return contractName; 67 | else return this.resolveByDeployedBytecode(contractAddress); 68 | } 69 | } 70 | 71 | module.exports = etherRouter; 72 | -------------------------------------------------------------------------------- /lib/gasData.js: -------------------------------------------------------------------------------- 1 | const ethers = require("ethers"); 2 | const { keccak256 } = require("ethereum-cryptography/keccak"); 3 | const { utf8ToBytes, bytesToHex } = require("ethereum-cryptography/utils"); 4 | const sha1 = require("sha1"); 5 | const utils = require("./utils"); 6 | const SyncRequest = require("./syncRequest"); 7 | const Artifactor = require("./artifactor"); 8 | 9 | /** 10 | * Data store written to by TransactionWatcher and consumed by the GasTable. 11 | */ 12 | class GasData { 13 | constructor() { 14 | this.addressCache = {}; 15 | this.methods = {}; 16 | this.deployments = []; 17 | this.codeHashMap = {}; 18 | this.blockLimit; 19 | this.sync; 20 | } 21 | 22 | /** 23 | * + Compiles pre-test gas usage (e.g. from `truffle migrate`) 24 | * + Sets up data structures to store deployments and methods gas usage 25 | * + Called in the mocha `start` hook to guarantee it's run later than pre-test deployments 26 | * @param {Object} config 27 | */ 28 | initialize(config) { 29 | this.sync = new SyncRequest(config.url); 30 | this.provider = config.provider; 31 | const artifactor = new Artifactor(config); 32 | 33 | // Get the current blockLimit; 34 | // TODO: This shouldn't be here - should be on the config object & 35 | // fetched when the table is written or something. 36 | this.blockLimit = config.blockLimit; 37 | 38 | if (!this.blockLimit && !this.provider) { 39 | const block = this.sync.getLatestBlock(); 40 | this.blockLimit = utils.gas(block.gasLimit); 41 | } 42 | 43 | for (const contract of artifactor.getContracts()) { 44 | const contractInfo = { 45 | name: contract.name, 46 | bytecode: contract.artifact.bytecode, 47 | deployedBytecode: contract.artifact.deployedBytecode, 48 | gasData: [] 49 | }; 50 | this.deployments.push(contractInfo); 51 | 52 | // Report gas used during pre-test deployments (ex: truffle migrate) 53 | if ( 54 | contract.artifact.deployed && 55 | contract.artifact.deployed.transactionHash && 56 | !this.provider 57 | ) { 58 | const receipt = this.sync.getTransactionReceipt( 59 | contract.artifact.deployed.transactionHash 60 | ); 61 | if (receipt) { 62 | // Sync: only runs for Truffle atm... 63 | this.trackNameByAddress( 64 | contract.name, 65 | contract.artifact.deployed.address 66 | ); 67 | contractInfo.gasData.push(utils.gas(receipt.gasUsed)); 68 | } 69 | } 70 | 71 | if (contract.artifact.bytecodeHash) { 72 | this.trackNameByPreloadedAddress( 73 | contract.name, 74 | contract.artifact.address, 75 | contract.artifact.bytecodeHash 76 | ); 77 | } 78 | 79 | // Decode, getMethodIDs 80 | const methodIDs = {}; 81 | 82 | let methods; 83 | try { 84 | methods = new ethers.utils.Interface(contract.artifact.abi).functions; 85 | } catch (err) { 86 | utils.warnEthers(contract.name, err); 87 | return; 88 | } 89 | 90 | // Generate sighashes and remap ethers to something similar 91 | // to abiDecoder.getMethodIDs 92 | Object.keys(methods).forEach(key => { 93 | const sighash = bytesToHex(keccak256(utf8ToBytes(key))).slice(0, 8); 94 | methodIDs[sighash] = Object.assign({ fnSig: key }, methods[key]); 95 | }); 96 | 97 | // Create Method Map; 98 | Object.keys(methodIDs).forEach(key => { 99 | const isInterface = contract.artifact.bytecode === "0x"; 100 | const isCall = methodIDs[key].type === "call"; 101 | const methodHasName = methodIDs[key].name !== undefined; 102 | 103 | if (methodHasName && !isCall && !isInterface) { 104 | this.methods[contract.name + "_" + key] = { 105 | key: key, 106 | contract: contract.name, 107 | method: methodIDs[key].name, 108 | fnSig: methodIDs[key].fnSig, 109 | gasData: [], 110 | numberOfCalls: 0 111 | }; 112 | } 113 | }); 114 | } 115 | } 116 | 117 | /** 118 | * Map a contract name to the sha1 hash of the code stored at an address 119 | * @param {String} name contract name 120 | * @param {String} address contract address 121 | */ 122 | trackNameByAddress(name, address) { 123 | if (this.addressIsCached(address)) return; 124 | 125 | const code = this.sync.getCode(address); 126 | const hash = code ? sha1(code) : null; 127 | this.codeHashMap[hash] = name; 128 | this.addressCache[address] = name; 129 | } 130 | 131 | /** 132 | * Map a contract name to pre-generated hash of the code stored at an address 133 | * @param {String} name contract name 134 | * @param {String} address contract address 135 | */ 136 | trackNameByPreloadedAddress(name, address, hash) { 137 | if (this.addressIsCached(address)) return; 138 | this.codeHashMap[hash] = name; 139 | this.addressCache[address] = name; 140 | } 141 | 142 | /** 143 | * Get the name of the contract stored at contract address 144 | * @param {String} address contract address 145 | * @return {String} contract name 146 | */ 147 | getNameByAddress(address) { 148 | if (this.addressIsCached(address)) { 149 | return this.addressCache[address]; 150 | } 151 | 152 | const code = this.sync.getCode(address); 153 | const hash = code ? sha1(code) : null; 154 | return this.codeHashMap[hash]; 155 | } 156 | 157 | /** 158 | * Map a contract name to the sha1 hash of the code stored at an address 159 | * @param {String} name contract name 160 | * @param {String} address contract address 161 | */ 162 | async asyncTrackNameByAddress(name, address) { 163 | if (this.addressIsCached(address)) return; 164 | 165 | const code = await this.provider.getCode(address); 166 | const hash = code ? sha1(code) : null; 167 | this.codeHashMap[hash] = name; 168 | this.addressCache[address] = name; 169 | } 170 | 171 | /** 172 | * Get the name of the contract stored at contract address 173 | * @param {String} address contract address 174 | * @return {String} contract name 175 | */ 176 | async asyncGetNameByAddress(address) { 177 | if (this.addressIsCached(address)) { 178 | return this.addressCache[address]; 179 | } 180 | 181 | const code = await this.provider.getCode(address); 182 | const hash = code ? sha1(code) : null; 183 | return this.codeHashMap[hash]; 184 | } 185 | 186 | /** 187 | * Compares existing contract binaries to the input code for a 188 | * new deployment transaction and returns the relevant contract. 189 | * Ignores interfaces. 190 | * @param {String} input tx.input 191 | * @return {Object} this.deployments entry 192 | */ 193 | getContractByDeploymentInput(input) { 194 | if (!input) return null; 195 | 196 | const matches = this.deployments.filter(item => 197 | utils.matchBinaries(input, item.bytecode) 198 | ); 199 | 200 | // Filter interfaces 201 | return matches && matches.length 202 | ? matches.find(item => item.bytecode !== "0x") 203 | : null; 204 | } 205 | 206 | /** 207 | * Compares code at an address to the deployedBytecode for all 208 | * compiled contracts and returns the relevant item. 209 | * Ignores interfaces. 210 | * @param {String} code result of web3.eth.getCode 211 | * @return {Object} this.deployments entry 212 | */ 213 | getContractByDeployedBytecode(code) { 214 | if (!code) return null; 215 | 216 | const matches = this.deployments.filter(item => 217 | utils.matchBinaries(code, item.deployedBytecode) 218 | ); 219 | 220 | // Filter interfaces 221 | return matches && matches.length 222 | ? matches.find(item => item.deployedBytecode !== "0x") 223 | : null; 224 | } 225 | 226 | /** 227 | * Returns all contracts with a method matching the requested signature 228 | * @param {String} signature method signature hash 229 | * @return {Object[]} this.method entries array 230 | */ 231 | getAllContractsWithMethod(signature) { 232 | return Object.values(this.methods).filter(el => el.key === signature); 233 | } 234 | 235 | addressIsCached(address) { 236 | return Object.keys(this.addressCache).includes(address); 237 | } 238 | 239 | resetAddressCache() { 240 | this.addressCache = {}; 241 | } 242 | } 243 | 244 | module.exports = GasData; 245 | -------------------------------------------------------------------------------- /lib/gasTable.js: -------------------------------------------------------------------------------- 1 | const colors = require("colors/safe"); 2 | const _ = require("lodash"); 3 | const fs = require("fs"); 4 | const Table = require("cli-table3"); 5 | const utils = require("./utils"); 6 | const CodeChecksReport = require("./codechecksReport"); 7 | 8 | class GasTable { 9 | constructor(config) { 10 | this.config = config; 11 | } 12 | /** 13 | * Formats and prints a gas statistics table. Optionally writes to a file. 14 | * Based on Alan Lu's (github.com/@cag) stats for Gnosis 15 | * @param {Object} info GasData instance with `methods` and `deployments` data 16 | */ 17 | generate(info) { 18 | colors.enabled = !this.config.noColors || false; 19 | 20 | // --------------------------------------------------------------------------------------------- 21 | // Assemble section: methods 22 | // --------------------------------------------------------------------------------------------- 23 | const methodRows = []; 24 | 25 | _.forEach(info.methods, (data, methodId) => { 26 | if (!data) return; 27 | 28 | let stats = {}; 29 | 30 | if (data.gasData.length) { 31 | const total = data.gasData.reduce((acc, datum) => acc + datum, 0); 32 | stats.average = Math.round(total / data.gasData.length); 33 | 34 | stats.cost = 35 | this.config.ethPrice && this.config.gasPrice 36 | ? utils.gasToCost( 37 | stats.average, 38 | this.config.ethPrice, 39 | this.config.gasPrice 40 | ) 41 | : colors.grey("-"); 42 | } else { 43 | stats.average = colors.grey("-"); 44 | stats.cost = colors.grey("-"); 45 | } 46 | 47 | const sortedData = data.gasData.sort((a, b) => a - b); 48 | stats.min = sortedData[0]; 49 | stats.max = sortedData[sortedData.length - 1]; 50 | 51 | const uniform = stats.min === stats.max; 52 | stats.min = uniform ? "-" : colors.cyan(stats.min.toString()); 53 | stats.max = uniform ? "-" : colors.red(stats.max.toString()); 54 | 55 | stats.numberOfCalls = colors.grey(data.numberOfCalls.toString()); 56 | 57 | const fnName = this.config.showMethodSig ? data.fnSig : data.method; 58 | 59 | if (!this.config.onlyCalledMethods || data.numberOfCalls > 0) { 60 | const section = []; 61 | section.push(colors.grey(data.contract)); 62 | section.push(fnName); 63 | section.push({ hAlign: "right", content: stats.min }); 64 | section.push({ hAlign: "right", content: stats.max }); 65 | section.push({ hAlign: "right", content: stats.average }); 66 | section.push({ hAlign: "right", content: stats.numberOfCalls }); 67 | section.push({ 68 | hAlign: "right", 69 | content: colors.green(stats.cost.toString()) 70 | }); 71 | 72 | methodRows.push(section); 73 | } 74 | }); 75 | 76 | // --------------------------------------------------------------------------------------------- 77 | // Assemble section: deployments 78 | // --------------------------------------------------------------------------------------------- 79 | const deployRows = []; 80 | 81 | // Alphabetize contract names 82 | info.deployments.sort((a, b) => a.name.localeCompare(b.name)); 83 | 84 | info.deployments.forEach(contract => { 85 | let stats = {}; 86 | if (!contract.gasData.length) return; 87 | 88 | const total = contract.gasData.reduce((acc, datum) => acc + datum, 0); 89 | stats.average = Math.round(total / contract.gasData.length); 90 | stats.percent = utils.gasToPercentOfLimit(stats.average, info.blockLimit); 91 | 92 | stats.cost = 93 | this.config.ethPrice && this.config.gasPrice 94 | ? utils.gasToCost( 95 | stats.average, 96 | this.config.ethPrice, 97 | this.config.gasPrice 98 | ) 99 | : colors.grey("-"); 100 | 101 | const sortedData = contract.gasData.sort((a, b) => a - b); 102 | stats.min = sortedData[0]; 103 | stats.max = sortedData[sortedData.length - 1]; 104 | 105 | const uniform = stats.min === stats.max; 106 | stats.min = uniform ? "-" : colors.cyan(stats.min.toString()); 107 | stats.max = uniform ? "-" : colors.red(stats.max.toString()); 108 | 109 | const section = []; 110 | section.push({ hAlign: "left", colSpan: 2, content: contract.name }); 111 | section.push({ hAlign: "right", content: stats.min }); 112 | section.push({ hAlign: "right", content: stats.max }); 113 | section.push({ hAlign: "right", content: stats.average }); 114 | section.push({ 115 | hAlign: "right", 116 | content: colors.grey(`${stats.percent} %`) 117 | }); 118 | section.push({ 119 | hAlign: "right", 120 | content: colors.green(stats.cost.toString()) 121 | }); 122 | 123 | deployRows.push(section); 124 | }); 125 | 126 | // --------------------------------------------------------------------------------------------- 127 | // Assemble section: headers 128 | // --------------------------------------------------------------------------------------------- 129 | 130 | // Configure indentation for RTD 131 | const leftPad = this.config.rst ? " " : ""; 132 | 133 | // Format table 134 | const table = new Table({ 135 | style: { head: [], border: [], "padding-left": 2, "padding-right": 2 }, 136 | chars: { 137 | mid: "·", 138 | "top-mid": "|", 139 | "left-mid": `${leftPad}·`, 140 | "mid-mid": "|", 141 | "right-mid": "·", 142 | left: `${leftPad}|`, 143 | "top-left": `${leftPad}·`, 144 | "top-right": "·", 145 | "bottom-left": `${leftPad}·`, 146 | "bottom-right": "·", 147 | middle: "·", 148 | top: "-", 149 | bottom: "-", 150 | "bottom-mid": "|" 151 | } 152 | }); 153 | 154 | // Format and load methods metrics 155 | const solc = utils.getSolcInfo(this.config.metadata); 156 | 157 | let title = [ 158 | { 159 | hAlign: "center", 160 | colSpan: 2, 161 | content: colors.grey(`Solc version: ${solc.version}`) 162 | }, 163 | { 164 | hAlign: "center", 165 | colSpan: 2, 166 | content: colors.grey(`Optimizer enabled: ${solc.optimizer}`) 167 | }, 168 | { 169 | hAlign: "center", 170 | colSpan: 1, 171 | content: colors.grey(`Runs: ${solc.runs}`) 172 | }, 173 | { 174 | hAlign: "center", 175 | colSpan: 2, 176 | content: colors.grey(`Block limit: ${info.blockLimit} gas`) 177 | } 178 | ]; 179 | 180 | let methodSubtitle; 181 | if (this.config.ethPrice && this.config.gasPrice) { 182 | const gwei = parseInt(this.config.gasPrice); 183 | const rate = parseFloat(this.config.ethPrice).toFixed(2); 184 | const currency = `${this.config.currency.toLowerCase()}`; 185 | const token = `${this.config.token.toLowerCase()}`; 186 | 187 | methodSubtitle = [ 188 | { hAlign: "left", colSpan: 2, content: colors.green.bold("Methods") }, 189 | { 190 | hAlign: "center", 191 | colSpan: 3, 192 | content: colors.grey(`${gwei} gwei/gas`) 193 | }, 194 | { 195 | hAlign: "center", 196 | colSpan: 2, 197 | content: colors.red(`${rate} ${currency}/${token}`) 198 | } 199 | ]; 200 | } else { 201 | methodSubtitle = [ 202 | { hAlign: "left", colSpan: 7, content: colors.green.bold("Methods") } 203 | ]; 204 | } 205 | 206 | const header = [ 207 | colors.bold("Contract"), 208 | colors.bold("Method"), 209 | colors.green("Min"), 210 | colors.green("Max"), 211 | colors.green("Avg"), 212 | colors.bold("# calls"), 213 | colors.bold(`${this.config.currency.toLowerCase()} (avg)`) 214 | ]; 215 | 216 | // --------------------------------------------------------------------------------------------- 217 | // Final assembly 218 | // --------------------------------------------------------------------------------------------- 219 | table.push(title); 220 | table.push(methodSubtitle); 221 | table.push(header); 222 | 223 | methodRows.sort((a, b) => { 224 | const contractName = a[0].localeCompare(b[0]); 225 | const methodName = a[1].localeCompare(b[1]); 226 | return contractName || methodName; 227 | }); 228 | 229 | methodRows.forEach(row => table.push(row)); 230 | 231 | if (deployRows.length) { 232 | const deploymentsSubtitle = [ 233 | { 234 | hAlign: "left", 235 | colSpan: 2, 236 | content: colors.green.bold("Deployments") 237 | }, 238 | { hAlign: "right", colSpan: 3, content: "" }, 239 | { hAlign: "left", colSpan: 1, content: colors.bold(`% of limit`) } 240 | ]; 241 | table.push(deploymentsSubtitle); 242 | deployRows.forEach(row => table.push(row)); 243 | } 244 | 245 | // --------------------------------------------------------------------------------------------- 246 | // RST / ReadTheDocs / Sphinx output 247 | // --------------------------------------------------------------------------------------------- 248 | let rstOutput = ""; 249 | if (this.config.rst) { 250 | rstOutput += `${this.config.rstTitle}\n`; 251 | rstOutput += `${"=".repeat(this.config.rstTitle.length)}\n\n`; 252 | rstOutput += `.. code-block:: shell\n\n`; 253 | } 254 | 255 | let tableOutput = rstOutput + table.toString(); 256 | 257 | // --------------------------------------------------------------------------------------------- 258 | // Print 259 | // --------------------------------------------------------------------------------------------- 260 | if (this.config.outputFile) { 261 | fs.writeFileSync(this.config.outputFile, tableOutput); 262 | if (this.config.forceConsoleOutput) 263 | console.log(tableOutput); 264 | } else { 265 | console.log(tableOutput); 266 | } 267 | 268 | this.saveCodeChecksData(info); 269 | 270 | // For integration tests 271 | if (process.env.DEBUG_CODECHECKS_TABLE) { 272 | const report = new CodeChecksReport(this.config); 273 | console.log(report.generate(info)); 274 | } 275 | } 276 | 277 | /** 278 | * Writes acccumulated data and the current config to gasReporterOutput.json so it 279 | * can be consumed by codechecks 280 | * @param {Object} info GasData instance 281 | */ 282 | saveCodeChecksData(info) { 283 | delete this.config.provider; 284 | delete info.provider; 285 | 286 | const output = { 287 | namespace: "ethGasReporter", 288 | config: this.config, 289 | info: info 290 | }; 291 | 292 | if (process.env.CI) { 293 | fs.writeFileSync("./gasReporterOutput.json", JSON.stringify(output)); 294 | } 295 | } 296 | } 297 | 298 | module.exports = GasTable; 299 | -------------------------------------------------------------------------------- /lib/mochaStats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file adapted from mocha's stats-collector 3 | * https://github.com/mochajs/mocha/blob/54475eb4ca35a2c9044a1b8c59a60f09c73e6c01/lib/stats-collector.js#L1-L83 4 | */ 5 | const Date = global.Date; 6 | 7 | /** 8 | * Provides stats such as test duration, number of tests passed / failed etc., by 9 | * listening for events emitted by `runner`. 10 | */ 11 | function mochaStatsCollector(runner) { 12 | const stats = { 13 | suites: 0, 14 | tests: 0, 15 | passes: 0, 16 | pending: 0, 17 | failures: 0 18 | }; 19 | 20 | if (!runner) throw new Error("Missing runner argument"); 21 | 22 | runner.stats = stats; 23 | 24 | runner.on("pass", () => stats.passes++); 25 | runner.on("fail", () => stats.failures++); 26 | runner.on("pending", () => stats.pending++); 27 | runner.on("test end", () => stats.tests++); 28 | 29 | runner.once("start", () => (stats.start = new Date())); 30 | 31 | runner.once("end", function() { 32 | stats.end = new Date(); 33 | stats.duration = stats.end - stats.start; 34 | }); 35 | } 36 | 37 | module.exports = mochaStatsCollector; 38 | -------------------------------------------------------------------------------- /lib/proxyResolver.js: -------------------------------------------------------------------------------- 1 | const etherRouter = require("./etherRouter"); 2 | const SyncRequest = require("./syncRequest"); 3 | 4 | class ProxyResolver { 5 | constructor(data, config) { 6 | this.unresolvedCalls = 0; 7 | this.data = data; 8 | this.sync = new SyncRequest(config.url); 9 | this.provider = config.provider; 10 | 11 | if (typeof config.proxyResolver === "function") { 12 | this.resolve = config.proxyResolver.bind(this); 13 | } else if (config.proxyResolver === "EtherRouter") { 14 | this.resolve = etherRouter.bind(this); 15 | } else { 16 | this.resolve = this.resolveByMethodSignature; 17 | } 18 | } 19 | 20 | /** 21 | * Searches all known contracts for the method signature and returns the first 22 | * found (if any). Undefined if none 23 | * @param {Object} transaction result of web3.eth.getTransaction 24 | * @return {String} contract name 25 | */ 26 | resolveByMethodSignature(transaction) { 27 | const signature = transaction.input.slice(2, 10); 28 | const matches = this.data.getAllContractsWithMethod(signature); 29 | 30 | if (matches.length >= 1) return matches[0].contract; 31 | } 32 | 33 | /** 34 | * Tries to match bytecode deployed at address to deployedBytecode listed 35 | * in artifacts. If found, adds this to the code-hash name mapping and 36 | * returns name. 37 | * @param {String} address contract address 38 | * @return {String} contract name 39 | */ 40 | resolveByDeployedBytecode(address) { 41 | const code = this.sync.getCode(address); 42 | const match = this.data.getContractByDeployedBytecode(code); 43 | 44 | if (match) { 45 | this.data.trackNameByAddress(match.name, address); 46 | return match.name; 47 | } 48 | } 49 | 50 | /** 51 | * Tries to match bytecode deployed at address to deployedBytecode listed 52 | * in artifacts. If found, adds this to the code-hash name mapping and 53 | * returns name. 54 | * @param {String} address contract address 55 | * @return {String} contract name 56 | */ 57 | async asyncResolveByDeployedBytecode(address) { 58 | const code = await this.provider.getCode(address); 59 | const match = this.data.getContractByDeployedBytecode(code); 60 | 61 | if (match) { 62 | await this.data.asyncTrackNameByAddress(match.name, address); 63 | return match.name; 64 | } 65 | } 66 | } 67 | 68 | module.exports = ProxyResolver; 69 | -------------------------------------------------------------------------------- /lib/syncRequest.js: -------------------------------------------------------------------------------- 1 | const syncRequest = require("sync-request"); 2 | /** 3 | * A set of sync RPC calls. Synchronicity is necessary to handle build tools that 4 | * revert between test runner blocks (like `suite`). Mocha doesn't support async methods 5 | * in the reporter hook and no modern ethereum providers (web3, ethers) support sync methods 6 | * either so we need to execute them ourselves 7 | * 8 | * @author: Alex Rea, 9 | */ 10 | 11 | class Sync { 12 | constructor(url) { 13 | this.url = url; 14 | } 15 | 16 | getNetworkId() { 17 | return this.request("net_version", []); 18 | } 19 | 20 | getCode(address) { 21 | return this.request("eth_getCode", [address, "latest"]); 22 | } 23 | 24 | blockNumber() { 25 | const val = this.request("eth_blockNumber", []); 26 | return parseInt(val, 16); 27 | } 28 | 29 | getLatestBlock() { 30 | return this.request("eth_getBlockByNumber", ["latest", false]); 31 | } 32 | 33 | getBlockByNumber(number) { 34 | const hexNumber = `0x${number.toString(16)}`; 35 | return this.request("eth_getBlockByNumber", [hexNumber, true]); 36 | } 37 | 38 | blockNumber() { 39 | const block = this.getLatestBlock(); 40 | return parseInt(block.number, 16); 41 | } 42 | 43 | getTransactionByHash(tx) { 44 | return this.request("eth_getTransactionByHash", [tx]); 45 | } 46 | 47 | getTransactionReceipt(tx) { 48 | return this.request("eth_getTransactionReceipt", [tx]); 49 | } 50 | 51 | call(payload, blockNumber) { 52 | return this.request("eth_call", [payload, blockNumber]); 53 | } 54 | 55 | request(method, params) { 56 | const payload = { 57 | json: { 58 | jsonrpc: "2.0", 59 | method: method, 60 | params: params, 61 | id: 1 62 | } 63 | }; 64 | 65 | const res = syncRequest("POST", this.url, payload); 66 | return JSON.parse(res.getBody("utf8")).result; 67 | } 68 | } 69 | 70 | module.exports = Sync; 71 | -------------------------------------------------------------------------------- /lib/transactionWatcher.js: -------------------------------------------------------------------------------- 1 | const utils = require("./utils"); 2 | const GasData = require("./gasData"); 3 | const SyncRequest = require("./syncRequest"); 4 | const ProxyResolver = require("./proxyResolver"); 5 | 6 | /** 7 | * Tracks blocks and cycles across them, extracting gas usage data and 8 | * associating it with the relevant contracts, methods. 9 | */ 10 | class TransactionWatcher { 11 | constructor(config) { 12 | this.itStartBlock = 0; // Tracks within `it` block transactions (gas usage per test) 13 | this.beforeStartBlock = 0; // Tracks from `before/beforeEach` transactions (methods & deploys) 14 | this.data = new GasData(); 15 | this.sync = new SyncRequest(config.url); 16 | this.provider = config.provider; 17 | this.resolver = new ProxyResolver(this.data, config); 18 | } 19 | 20 | /** 21 | * Cycles across a range of blocks, from beforeStartBlock set in the reporter's 22 | * `test` hook to current block when it's called. Collect deployments and methods 23 | * gas usage data. 24 | * @return {Number} Total gas usage for the `it` block 25 | */ 26 | blocks() { 27 | let gasUsed = 0; 28 | const endBlock = this.sync.blockNumber(); 29 | 30 | while (this.beforeStartBlock <= endBlock) { 31 | let block = this.sync.getBlockByNumber(this.beforeStartBlock); 32 | 33 | if (block) { 34 | // Track gas used within `it` blocks 35 | if (this.itStartBlock <= this.beforeStartBlock) { 36 | gasUsed += utils.gas(block.gasUsed); 37 | } 38 | 39 | // Collect methods and deployments data 40 | block.transactions.forEach(transaction => { 41 | const receipt = this.sync.getTransactionReceipt(transaction.hash); 42 | 43 | // Omit transactions that throw 44 | if (parseInt(receipt.status) === 0) return; 45 | 46 | receipt.contractAddress 47 | ? this._collectDeploymentsData(transaction, receipt) 48 | : this._collectMethodsData(transaction, receipt); 49 | }); 50 | } 51 | this.beforeStartBlock++; 52 | } 53 | return gasUsed; 54 | } 55 | 56 | async transaction(receipt, transaction) { 57 | receipt.contractAddress 58 | ? await this._asyncCollectDeploymentsData(transaction, receipt) 59 | : await this._asyncCollectMethodsData(transaction, receipt); 60 | } 61 | 62 | /** 63 | * Extracts and stores deployments gas usage data for a tx 64 | * @param {Object} transaction return value of `getTransactionByHash` 65 | * @param {Object} receipt 66 | */ 67 | _collectDeploymentsData(transaction, receipt) { 68 | const match = this.data.getContractByDeploymentInput(transaction.input); 69 | 70 | if (match) { 71 | this.data.trackNameByAddress(match.name, receipt.contractAddress); 72 | match.gasData.push(utils.gas(receipt.gasUsed)); 73 | } 74 | } 75 | 76 | /** 77 | * Extracts and stores deployments gas usage data for a tx 78 | * @param {Object} transaction return value of `getTransactionByHash` 79 | * @param {Object} receipt 80 | */ 81 | async _asyncCollectDeploymentsData(transaction, receipt) { 82 | const match = this.data.getContractByDeploymentInput(transaction.input); 83 | 84 | if (match) { 85 | await this.data.asyncTrackNameByAddress( 86 | match.name, 87 | receipt.contractAddress 88 | ); 89 | match.gasData.push(utils.gas(receipt.gasUsed)); 90 | } 91 | } 92 | 93 | /** 94 | * Extracts and stores methods gas usage data for a tx 95 | * @param {Object} transaction return value of `getTransactionByHash` 96 | * @param {Object} receipt 97 | */ 98 | _collectMethodsData(transaction, receipt) { 99 | let contractName = this.data.getNameByAddress(transaction.to); 100 | 101 | // Case: proxied call 102 | if (this._isProxied(contractName, transaction.input)) { 103 | contractName = this.resolver.resolve(transaction); 104 | 105 | // Case: hidden contract factory deployment 106 | } else if (!contractName) { 107 | contractName = this.resolver.resolveByDeployedBytecode(transaction.to); 108 | } 109 | 110 | // Case: all else fails, use first match strategy 111 | if (!contractName) { 112 | contractName = this.resolver.resolveByMethodSignature(transaction); 113 | } 114 | 115 | const id = utils.getMethodID(contractName, transaction.input); 116 | 117 | if (this.data.methods[id]) { 118 | this.data.methods[id].gasData.push(utils.gas(receipt.gasUsed)); 119 | this.data.methods[id].numberOfCalls += 1; 120 | } else { 121 | this.resolver.unresolvedCalls++; 122 | } 123 | } 124 | 125 | /** 126 | * Extracts and stores methods gas usage data for a tx 127 | * @param {Object} transaction return value of `getTransactionByHash` 128 | * @param {Object} receipt 129 | */ 130 | async _asyncCollectMethodsData(transaction, receipt) { 131 | let contractName = await this.data.asyncGetNameByAddress(transaction.to); 132 | 133 | // Case: proxied call 134 | if (this._isProxied(contractName, transaction.input)) { 135 | contractName = this.resolver.resolve(transaction); 136 | 137 | // Case: hidden contract factory deployment 138 | } else if (!contractName) { 139 | contractName = await this.resolver.asyncResolveByDeployedBytecode( 140 | transaction.to 141 | ); 142 | } 143 | 144 | // Case: all else fails, use first match strategy 145 | if (!contractName) { 146 | contractName = this.resolver.resolveByMethodSignature(transaction); 147 | } 148 | 149 | const id = utils.getMethodID(contractName, transaction.input); 150 | 151 | if (this.data.methods[id]) { 152 | this.data.methods[id].gasData.push(utils.gas(receipt.gasUsed)); 153 | this.data.methods[id].numberOfCalls += 1; 154 | } else { 155 | this.resolver.unresolvedCalls++; 156 | } 157 | } 158 | 159 | /** 160 | * Returns true if there is a contract name associated with an address 161 | * but method can't be matched to it 162 | * @param {String} name contract name 163 | * @param {String} input code 164 | * @return {Boolean} 165 | */ 166 | _isProxied(name, input) { 167 | return name && !this.data.methods[utils.getMethodID(name, input)]; 168 | } 169 | } 170 | 171 | module.exports = TransactionWatcher; 172 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const parser = require("@solidity-parser/parser"); 3 | const axios = require("axios"); 4 | const path = require("path"); 5 | const read = require("fs-readdir-recursive"); 6 | const colors = require("colors/safe"); 7 | const log = console.log; 8 | 9 | const utils = { 10 | /** 11 | * Expresses gas usage as a nation-state currency price 12 | * @param {Number} gas gas used 13 | * @param {Number} ethPrice e.g chf/eth 14 | * @param {Number} gasPrice in wei e.g 5000000000 (5 gwei) 15 | * @return {Number} cost of gas used (0.00) 16 | */ 17 | gasToCost: function(gas, ethPrice, gasPrice) { 18 | ethPrice = parseFloat(ethPrice); 19 | gasPrice = parseInt(gasPrice); 20 | return ((gasPrice / 1e9) * gas * ethPrice).toFixed(2); 21 | }, 22 | 23 | /** 24 | * Expresses gas usage as a % of the block gasLimit. Source: NeuFund (see issues) 25 | * @param {Number} gasUsed gas value 26 | * @param {Number} blockLimit gas limit of a block 27 | * @return {Number} percent (0.0) 28 | */ 29 | gasToPercentOfLimit: function(gasUsed, blockLimit) { 30 | return Math.round((1000 * gasUsed) / blockLimit) / 10; 31 | }, 32 | 33 | /** 34 | * Generates id for a GasData.methods entry from the input of a web3.eth.getTransaction 35 | * and a contract name 36 | * @param {String} code hex data 37 | * @return {String} id 38 | */ 39 | getMethodID: function(contractName, code) { 40 | return contractName + "_" + code.slice(2, 10); 41 | }, 42 | 43 | /** 44 | * Extracts solc settings and version info from solidity metadata 45 | * @param {Object} metadata solidity metadata 46 | * @return {Object} {version, optimizer, runs} 47 | */ 48 | getSolcInfo: function(metadata) { 49 | const missing = "----"; 50 | const info = { 51 | version: missing, 52 | optimizer: missing, 53 | runs: missing 54 | }; 55 | if (metadata) { 56 | info.version = metadata.compiler.version; 57 | info.optimizer = metadata.settings.optimizer.enabled; 58 | info.runs = metadata.settings.optimizer.runs; 59 | } 60 | return info; 61 | }, 62 | 63 | /** 64 | * Return true if transaction input and bytecode are same, ignoring library link code. 65 | * @param {String} code 66 | * @return {Bool} 67 | */ 68 | matchBinaries: function(input, bytecode) { 69 | const regExp = utils.bytecodeToBytecodeRegex(bytecode); 70 | return input.match(regExp) !== null; 71 | }, 72 | 73 | /** 74 | * Generate a regular expression string which is library link agnostic so we can match 75 | * linked bytecode deployment transaction inputs to the evm.bytecode solc output. 76 | * @param {String} bytecode 77 | * @return {String} 78 | */ 79 | bytecodeToBytecodeRegex: function(bytecode = "") { 80 | const bytecodeRegex = bytecode 81 | .replace(/__.{38}/g, ".{40}") 82 | .replace(/73f{40}/g, ".{42}"); 83 | 84 | // HACK: Node regexes can't be longer that 32767 characters. 85 | // Contracts bytecode can. We just truncate the regexes. It's safe in practice. 86 | const MAX_REGEX_LENGTH = 32767; 87 | const truncatedBytecodeRegex = bytecodeRegex.slice(0, MAX_REGEX_LENGTH); 88 | return truncatedBytecodeRegex; 89 | }, 90 | 91 | /** 92 | * Parses files for contract names 93 | * @param {String} filePath path to file 94 | * @return {String[]} contract names 95 | */ 96 | getContractNames: function(filePath) { 97 | const names = []; 98 | const code = fs.readFileSync(filePath, "utf-8"); 99 | 100 | let ast; 101 | try { 102 | ast = parser.parse(code, { tolerant: true }); 103 | } catch (err) { 104 | utils.warnParser(filePath, err); 105 | return names; 106 | } 107 | 108 | parser.visit(ast, { 109 | ContractDefinition: function(node) { 110 | names.push(node.name); 111 | } 112 | }); 113 | 114 | return names; 115 | }, 116 | 117 | /** 118 | * Message for un-parseable files 119 | * @param {String} filePath 120 | * @param {Error} err 121 | * @return {void} 122 | */ 123 | warnParser: function(filePath, err) { 124 | log(); 125 | log(colors.red(`>>>>> WARNING <<<<<<`)); 126 | log( 127 | `Failed to parse file: "${filePath}". No data will collected for its contract(s).` 128 | ); 129 | log( 130 | `NB: some Solidity 0.6.x syntax is not supported by the JS parser yet.` 131 | ); 132 | log( 133 | `Please report the error below to github.com/consensys/solidity-parser-antlr` 134 | ); 135 | log(colors.red(`>>>>>>>>>>>>>>>>>>>>`)); 136 | log(colors.red(`${err}`)); 137 | log(); 138 | }, 139 | 140 | /** 141 | * Message for un-parseable ABI (ethers) 142 | * @param {String} name contract name 143 | * @param {Error} err 144 | * @return {void} 145 | */ 146 | warnEthers: function(name, err) { 147 | log(); 148 | log(colors.red(`>>>>> WARNING <<<<<<`)); 149 | log( 150 | `Failed to parse ABI for contract: "${name}". (Its method data will not be collected).` 151 | ); 152 | log( 153 | `NB: Some Solidity 0.6.x syntax is not supported by Ethers.js V5 AbiCoder yet.` 154 | ); 155 | log(`Please report the error below to github.com/ethers-io/ethers.js`); 156 | log(colors.red(`>>>>>>>>>>>>>>>>>>>>`)); 157 | log(colors.red(`${err}`)); 158 | log(); 159 | }, 160 | 161 | /** 162 | * Converts hex gas to decimal 163 | * @param {Number} val hex gas returned by RPC 164 | * @return {Number} decimal gas consumed by human eyes. 165 | */ 166 | gas: function(val) { 167 | return parseInt(val, 16); 168 | }, 169 | 170 | /** 171 | * Fetches gasPrices from ethgasstation (defaults to the lowest safe gas price) 172 | * and current market value of eth in currency specified by the config from 173 | * coinmarketcap (defaults to euros). Sets config.ethPrice, config.gasPrice unless these 174 | * are already set as constants in the reporter options 175 | * @param {Object} config 176 | */ 177 | setGasAndPriceRates: async function(config) { 178 | if ((config.ethPrice && config.gasPrice) || !config.coinmarketcap) return; 179 | 180 | const token = config.token.toUpperCase(); 181 | const gasPriceApi = config.gasPriceApi; 182 | 183 | const axiosInstance = axios.create({ 184 | baseURL: `https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/` 185 | }); 186 | 187 | const requestArgs = `latest?symbol=${token}&CMC_PRO_API_KEY=${ 188 | config.coinmarketcap 189 | }&convert=`; 190 | 191 | const currencyKey = config.currency.toUpperCase(); 192 | const currencyPath = `${requestArgs}${currencyKey}`; 193 | 194 | // Currency market data: coinmarketcap 195 | if (!config.ethPrice) { 196 | try { 197 | let response = await axiosInstance.get(currencyPath); 198 | config.ethPrice = response.data.data[token].quote[ 199 | currencyKey 200 | ].price.toFixed(2); 201 | } catch (error) { 202 | config.ethPrice = null; 203 | } 204 | } 205 | 206 | // Gas price data: etherscan (or `gasPriceAPI`) 207 | if (!config.gasPrice) { 208 | try { 209 | let response = await axiosInstance.get(gasPriceApi); 210 | config.gasPrice = Math.round( 211 | parseInt(response.data.result, 16) / Math.pow(10, 9) 212 | ); 213 | } catch (error) { 214 | config.gasPrice = config.defaultGasPrice; 215 | } 216 | } 217 | }, 218 | 219 | listSolidityFiles(srcPath) { 220 | let base = `./${srcPath}/`; 221 | 222 | if (process.platform === "win32") { 223 | base = base.replace(/\\/g, "/"); 224 | } 225 | 226 | const paths = read(base) 227 | .filter(file => path.extname(file) === ".sol") 228 | .map(file => base + file); 229 | 230 | return paths; 231 | }, 232 | 233 | /** 234 | * Loads and parses Solidity files, returning a filtered array of contract names. 235 | * @return {string[]} 236 | */ 237 | parseSoliditySources(config) { 238 | const names = []; 239 | const files = utils.listSolidityFiles(config.srcPath); 240 | files.forEach(file => { 241 | const namesForFile = utils.getContractNames(file); 242 | const filtered = namesForFile.filter( 243 | name => !config.excludeContracts.includes(name) 244 | ); 245 | filtered.forEach(item => names.push(item)); 246 | }); 247 | return names; 248 | }, 249 | 250 | // Debugging helper 251 | pretty: function(msg, obj) { 252 | console.log(`<------ ${msg} ------>\n` + JSON.stringify(obj, null, " ")); 253 | console.log(`<------- END -------->\n`); 254 | } 255 | }; 256 | 257 | module.exports = utils; 258 | -------------------------------------------------------------------------------- /mock/buidler-metacoinjs-template.js: -------------------------------------------------------------------------------- 1 | const MetaCoin = artifacts.require('./MetaCoin.sol'); 2 | const ConvertLib = artifacts.require('./ConvertLib.sol'); 3 | 4 | contract('MetaCoin', function (accounts) { 5 | let meta; 6 | 7 | before(async function(){ 8 | const lib = await ConvertLib.new(); 9 | MetaCoin.link(lib); 10 | }) 11 | 12 | beforeEach(async function () { 13 | meta = await MetaCoin.new() 14 | meta = await MetaCoin.new() 15 | }) 16 | afterEach(async function () { 17 | meta = await MetaCoin.new() 18 | meta = await MetaCoin.new() 19 | }) 20 | 21 | it('should put 10000 MetaCoin in the first account', async function () { 22 | const balance = await meta.getBalance.call(accounts[0]) 23 | assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account") 24 | }) 25 | 26 | it('should call a function that depends on a linked library', function () { 27 | var metaCoinBalance 28 | var metaCoinEthBalance 29 | 30 | return meta.getBalance.call(accounts[0]).then(function (outCoinBalance) { 31 | metaCoinBalance = parseInt(outCoinBalance.toString()) 32 | return meta.getBalanceInEth.call(accounts[0]) 33 | }).then(function (outCoinBalanceEth) { 34 | metaCoinEthBalance = parseInt(outCoinBalanceEth.toString()) 35 | }).then(function () { 36 | assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, 'Library function returned unexpected function, linkage may be broken') 37 | }) 38 | }) 39 | it('should send coin correctly', function () { 40 | // Get initial balances of first and second account. 41 | var account_one = accounts[0] 42 | var account_two = accounts[1] 43 | 44 | var account_one_starting_balance 45 | var account_two_starting_balance 46 | var account_one_ending_balance 47 | var account_two_ending_balance 48 | 49 | var amount = 10 50 | 51 | return meta.getBalance.call(account_one).then(function (balance) { 52 | account_one_starting_balance = parseInt(balance.toString()) 53 | return meta.getBalance.call(account_two) 54 | }).then(function (balance) { 55 | account_two_starting_balance = parseInt(balance.toString()) 56 | return meta.sendCoin(account_two, amount, {from: account_one}) 57 | }).then(function () { 58 | return meta.getBalance.call(account_one) 59 | }).then(function (balance) { 60 | account_one_ending_balance = parseInt(balance.toString()) 61 | return meta.getBalance.call(account_two) 62 | }).then(function (balance) { 63 | account_two_ending_balance = parseInt(balance.toString()) 64 | 65 | assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender") 66 | assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver") 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /mock/buidler.config.js: -------------------------------------------------------------------------------- 1 | usePlugin("@nomiclabs/buidler-truffle5"); 2 | 3 | module.exports = { 4 | solc: { version: "0.5.5" }, 5 | networks: { 6 | development: { 7 | gas: 7000000, 8 | url: "http://localhost:8545" 9 | } 10 | }, 11 | mocha: { 12 | reporter: "eth-gas-reporter", 13 | reporterOptions: { 14 | artifactType: "buidler-v1", 15 | url: "http://localhost:8545" 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /mock/codechecks.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | - name: eth-gas-reporter/codechecks 3 | -------------------------------------------------------------------------------- /mock/config-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8545, 6 | network_id: "*", 7 | websockets: process.env.TEST === "integration" ? true : false 8 | } 9 | }, 10 | mocha: { 11 | reporter: "eth-gas-reporter", 12 | reporterOptions: { 13 | currency: "chf", 14 | token: "ETH", 15 | coinmarketcap: process.env.COINMARKETCAP_API_KEY || null, 16 | gasPriceApi: 17 | "https://api.etherscan.io/api?module=proxy&action=eth_gasPrice", 18 | onlyCalledMethods: false, 19 | noColors: true, 20 | rst: true, 21 | rstTitle: "Gas Usage", 22 | showTimeSpent: true, 23 | excludeContracts: ["Migrations"], 24 | proxyResolver: "EtherRouter", 25 | codechecks: true, 26 | showMethodSig: true 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /mock/contracts/ConvertLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | library ConvertLib{ 4 | function convert(uint amount,uint conversionRate) pure public returns (uint convertedAmount) 5 | { 6 | return amount * conversionRate; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mock/contracts/EncoderV2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | pragma experimental ABIEncoderV2; 3 | 4 | contract EncoderV2 { 5 | uint id; 6 | 7 | struct Asset { 8 | uint a; 9 | uint b; 10 | string c; 11 | } 12 | 13 | Asset a; 14 | 15 | function setAsset44(uint _id, Asset memory _a) public { 16 | id = _id; 17 | a = _a; 18 | } 19 | 20 | function getAsset() public view returns (Asset memory) { 21 | return a; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mock/contracts/EtherRouter/EtherRouter.sol: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of The Colony Network. 3 | The Colony Network is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | The Colony Network is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | You should have received a copy of the GNU General Public License 12 | along with The Colony Network. If not, see . 13 | */ 14 | 15 | pragma solidity ^0.5.0; 16 | 17 | import "./Resolver.sol"; 18 | 19 | contract EtherRouter { 20 | Resolver public resolver; 21 | 22 | function() external payable { 23 | if (msg.sig == 0) { 24 | return; 25 | } 26 | // Contracts that want to receive Ether with a plain "send" have to implement 27 | // a fallback function with the payable modifier. Contracts now throw if no payable 28 | // fallback function is defined and no function matches the signature. 29 | // However, 'send' only provides 2300 gas, which is not enough for EtherRouter 30 | // so we shortcut it here. 31 | // 32 | // Note that this means we can never have a fallback function that 'does' stuff. 33 | // but those only really seem to be ICOs, to date. To be explicit, there is a hard 34 | // decision to be made here. Either: 35 | // 1. Contracts that use 'send' or 'transfer' cannot send money to Colonies/ColonyNetwork 36 | // 2. We commit to never using a fallback function that does anything. 37 | // 38 | // We have decided on option 2 here. In the future, if we wish to have such a fallback function 39 | // for a Colony, it could be in a separate extension contract. 40 | 41 | // Get routing information for the called function 42 | address destination = resolver.lookup(msg.sig); 43 | 44 | // Make the call 45 | assembly { 46 | let size := extcodesize(destination) 47 | if eq(size, 0) { revert(0,0) } 48 | 49 | calldatacopy(mload(0x40), 0, calldatasize) 50 | let result := delegatecall(gas, destination, mload(0x40), calldatasize, mload(0x40), 0) // ignore-swc-112 calls are only to trusted contracts 51 | // as their addresses are controlled by the Resolver which we trust 52 | returndatacopy(mload(0x40), 0, returndatasize) 53 | switch result 54 | case 1 { return(mload(0x40), returndatasize) } // ignore-swc-113 55 | default { revert(mload(0x40), returndatasize) } 56 | } 57 | } 58 | 59 | function setResolver(address _resolver) public 60 | { 61 | resolver = Resolver(_resolver); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /mock/contracts/EtherRouter/Factory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./VersionA.sol"; 4 | import "./VersionB.sol"; 5 | 6 | contract Factory { 7 | 8 | VersionA public versionA; 9 | VersionB public versionB; 10 | 11 | constructor() public { 12 | } 13 | 14 | function deployVersionA() public { 15 | versionA = new VersionA(); 16 | } 17 | 18 | function deployVersionB() public { 19 | versionB = new VersionB(); 20 | } 21 | } -------------------------------------------------------------------------------- /mock/contracts/EtherRouter/Resolver.sol: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of The Colony Network. 3 | The Colony Network is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | The Colony Network is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | You should have received a copy of the GNU General Public License 12 | along with The Colony Network. If not, see . 13 | */ 14 | 15 | pragma solidity ^0.5.0; 16 | 17 | contract Resolver { 18 | mapping (bytes4 => address) public pointers; 19 | 20 | function register(string memory signature, address destination) public 21 | { 22 | pointers[stringToSig(signature)] = destination; 23 | } 24 | 25 | function lookup(bytes4 sig) public view returns(address) { 26 | return pointers[sig]; 27 | } 28 | 29 | function stringToSig(string memory signature) public pure returns(bytes4) { 30 | return bytes4(keccak256(abi.encodePacked(signature))); 31 | } 32 | } -------------------------------------------------------------------------------- /mock/contracts/EtherRouter/VersionA.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract VersionA { 4 | constructor() public { 5 | } 6 | 7 | function setValue() public { 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /mock/contracts/EtherRouter/VersionB.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract VersionB { 4 | 5 | constructor() public{ 6 | } 7 | 8 | function setValue() public { 9 | } 10 | 11 | function setAnotherValue() public { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mock/contracts/MetaCoin.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./ConvertLib.sol"; 4 | 5 | // This is just a simple example of a coin-like contract. 6 | // It is not standards compatible and cannot be expected to talk to other 7 | // coin/token contracts. If you want to create a standards-compliant 8 | // token, see: https://github.com/ConsenSys/Tokens. Cheers! 9 | 10 | contract MetaCoin { 11 | mapping (address => uint) balances; 12 | 13 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 14 | 15 | constructor() public { 16 | balances[tx.origin] = 10000; 17 | } 18 | 19 | function sendCoin(address receiver, uint amount) public returns(bool sufficient) { 20 | if (balances[msg.sender] < amount) return false; 21 | balances[msg.sender] -= amount; 22 | balances[receiver] += amount; 23 | emit Transfer(msg.sender, receiver, amount); 24 | return true; 25 | } 26 | 27 | function getBalanceInEth(address addr) public view returns(uint){ 28 | return ConvertLib.convert(getBalance(addr),2); 29 | } 30 | 31 | function getBalance(address addr) public view returns(uint) { 32 | return balances[addr]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mock/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | constructor() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mock/contracts/MultiContractFile.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract MultiContractFileA { 4 | uint x; 5 | 6 | function hello() public { 7 | x = 5; 8 | } 9 | } 10 | 11 | contract MultiContractFileB { 12 | uint x; 13 | 14 | function goodbye() public { 15 | x = 5; 16 | } 17 | } -------------------------------------------------------------------------------- /mock/contracts/Undeployed.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./ConvertLib.sol"; 4 | 5 | contract Undeployed { 6 | event Amount(uint val); 7 | 8 | function f() public { 9 | uint a = ConvertLib.convert(5,5); 10 | emit Amount(a); 11 | } 12 | } -------------------------------------------------------------------------------- /mock/contracts/VariableConstructor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./VariableCosts.sol"; 4 | 5 | contract VariableConstructor is VariableCosts { 6 | string name; 7 | constructor(string memory _name) public { 8 | name = _name; 9 | } 10 | } -------------------------------------------------------------------------------- /mock/contracts/VariableCosts.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Wallets/Wallet.sol"; 4 | import "./MultiContractFile.sol"; 5 | 6 | contract VariableCosts is Wallet { 7 | uint q; 8 | string someString; 9 | mapping(uint => address) map; 10 | MultiContractFileA multi; 11 | 12 | constructor() public { 13 | multi = new MultiContractFileA(); 14 | } 15 | 16 | function pureFn(uint x) public pure returns (uint){ 17 | return x; 18 | } 19 | 20 | function viewFn(uint x) public view returns (address){ 21 | return map[x]; 22 | } 23 | 24 | function constantFn(uint x) public view returns (address){ 25 | return map[x]; 26 | } 27 | 28 | function addToMap(uint[] memory adds) public { 29 | for(uint i = 0; i < adds.length; i++) 30 | map[adds[i]] = address(this); 31 | } 32 | 33 | function removeFromMap(uint[] memory dels) public { 34 | for(uint i = 0; i < dels.length; i++) 35 | delete map[dels[i]]; 36 | } 37 | 38 | function unusedMethod(address a) public { 39 | map[1000] = a; 40 | } 41 | 42 | function setString(string memory _someString) public { 43 | someString = _someString; 44 | } 45 | 46 | function methodThatThrows(bool err) public { 47 | require(!err); 48 | q = 5; 49 | } 50 | 51 | function otherContractMethod() public { 52 | multi.hello(); // 20,000 gas (sets uint to 5 from zero) 53 | multi.hello(); // 5,000 gas (sets existing storage) 54 | multi.hello(); // 5,000 gas (sets existing storage) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mock/contracts/Wallets/Wallet.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract Wallet { 4 | 5 | event Deposit(address indexed _sender, uint _value); 6 | 7 | function transferPayment(uint payment, address payable recipient) public { 8 | address(recipient).transfer(payment); 9 | } 10 | 11 | function sendPayment(uint payment, address payable recipient) public { 12 | if (!address(recipient).send(payment)) 13 | revert(); 14 | } 15 | 16 | function getBalance() public view returns(uint){ 17 | return address(this).balance; 18 | } 19 | 20 | function() external payable 21 | { 22 | if (msg.value > 0) 23 | emit Deposit(msg.sender, msg.value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mock/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require('./Migrations.sol') 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations) 5 | } 6 | -------------------------------------------------------------------------------- /mock/migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var ConvertLib = artifacts.require('./ConvertLib.sol') 2 | var MetaCoin = artifacts.require('./MetaCoin.sol') 3 | var Wallet = artifacts.require('./Wallets/Wallet.sol') 4 | var VariableCosts = artifacts.require('./VariableCosts.sol') 5 | var VariableConstructor = artifacts.require('./VariableConstructor') 6 | var Undeployed = artifacts.require('./Undeployed') 7 | 8 | module.exports = function (deployer) { 9 | deployer.deploy(ConvertLib) 10 | deployer.link(ConvertLib, MetaCoin) 11 | deployer.link(ConvertLib, Undeployed) 12 | deployer.deploy(MetaCoin) 13 | deployer.deploy(Wallet) 14 | deployer.deploy(VariableCosts) 15 | deployer.deploy(VariableConstructor, 'Exit Visa') 16 | } 17 | -------------------------------------------------------------------------------- /mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-gas-reporter", 3 | "version": "0.2.23", 4 | "description": "Mocha reporter which shows gas used per unit test.", 5 | "main": "index.js", 6 | "scripts": { 7 | "geth": "./scripts/geth.sh", 8 | "test": "./mock/scripts/test.sh", 9 | "ci": "./scripts/ci.sh" 10 | }, 11 | "husky": { 12 | "hooks": { 13 | "pre-commit": "pretty-quick --staged" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cgewecke/eth-gas-reporter.git" 19 | }, 20 | "keywords": [ 21 | "Ethereum", 22 | "solidity", 23 | "unit-testing", 24 | "truffle", 25 | "gas." 26 | ], 27 | "author": "cgewecke ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/cgewecke/eth-gas-reporter/issues" 31 | }, 32 | "homepage": "https://github.com/cgewecke/eth-gas-reporter#readme", 33 | "peerDependencies": { 34 | "@codechecks/client": "^0.1.0" 35 | }, 36 | "peerDependenciesMeta": { 37 | "@codechecks/client": { 38 | "optional": true 39 | } 40 | }, 41 | "dependencies": { 42 | "@ethersproject/abi": "^5.0.0-beta.146", 43 | "@solidity-parser/parser": "^0.14.0", 44 | "cli-table3": "^0.5.0", 45 | "colors": "1.4.0", 46 | "ethereumjs-util": "6.2.0", 47 | "ethers": "^4.0.40", 48 | "fs-readdir-recursive": "^1.1.0", 49 | "lodash": "^4.17.14", 50 | "markdown-table": "^1.1.3", 51 | "mocha": "^7.1.1", 52 | "req-cwd": "^2.0.0", 53 | "request": "^2.88.0", 54 | "request-promise-native": "^1.0.5", 55 | "sha1": "^1.1.1", 56 | "sync-request": "^6.0.0" 57 | }, 58 | "devDependencies": { 59 | "@codechecks/client": "^0.1.10", 60 | "@nomiclabs/buidler": "^1.4.7", 61 | "@nomiclabs/buidler-truffle5": "^1.3.4", 62 | "@nomiclabs/buidler-web3": "^1.3.4", 63 | "fp-ts": "^1.19.0", 64 | "ganache-cli": "6.4.3", 65 | "geth-dev-assistant": "^0.1.7", 66 | "husky": "^2.3.0", 67 | "prettier": "1.17.1", 68 | "pretty-quick": "^1.11.0", 69 | "randomstring": "^1.1.5", 70 | "truffle": "5.0.12", 71 | "web3": "^1.3.0", 72 | "yarn": "^1.16.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mock/scripts/install_reporter.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Installs latest reporter state, including added dependencies 4 | 5 | # Copy over the package and install 6 | # Expects to be run from within ./mock 7 | install_reporter() { 8 | 9 | cp ./../package.json ./package.json 10 | npx yarn 11 | 12 | # Copy over eth-gas-reporter 13 | if [ ! -e node_modules/eth-gas-reporter ]; then 14 | mkdir node_modules/eth-gas-reporter 15 | fi 16 | 17 | cp -r ./../lib node_modules/eth-gas-reporter 18 | cp ./../index.js node_modules/eth-gas-reporter/index.js 19 | cp ./../codechecks.js node_modules/eth-gas-reporter/codechecks.js 20 | cp ./../package.json node_modules/eth-gas-reporter/package.json 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /mock/scripts/integration_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # --------------------------------- Truffle -------------------------------------------------------- 4 | 5 | # Basic, no reporter options specified 6 | test_truffle_v5_basic() { 7 | echo "-----------------------------------------------------------" 8 | echo "> Visual inspection integration test (Truffle - no options)" 9 | echo "> YOU MUST LOOK AT THIS TEST TO DETECT FAILURE" 10 | echo "-----------------------------------------------------------" 11 | 12 | npx truffle test --network development "$@" 13 | 14 | } 15 | 16 | # With options 17 | test_truffle_v5_with_options() { 18 | echo "--------------------------------------------------------------------" 19 | echo "> Visual inspection integration test (Truffle - reporter options)" 20 | echo "> YOU MUST LOOK AT THIS TEST TO DETECT FAILURE" 21 | echo "--------------------------------------------------------------------" 22 | 23 | # Swap out no-options truffle.js for one with config 24 | cp ./truffle.js ./safe_truffle.js 25 | cp ./config-template.js ./truffle.js 26 | 27 | # Test 28 | DEBUG_CODECHECKS_TABLE=true npx truffle test --network development "$@" 29 | 30 | if [ "$CI" = "true" ]; then 31 | npx codechecks 32 | fi 33 | 34 | # Swap in truffle.js 35 | cp ./safe_truffle.js ./truffle.js 36 | rm ./safe_truffle.js 37 | } 38 | 39 | # --------------------------------- Buidler (V5 plugin) -------------------------------------------- 40 | 41 | # Basic, no reporter options specified 42 | # Swaps out TestMetacoin.sol because it throws a truffle/assert.sol not found error 43 | test_buildler_v5_plugin() { 44 | echo "-----------------------------------------------------------" 45 | echo "> Visual inspection integration test (Buidler - no options)" 46 | echo "> YOU MUST LOOK AT THIS TEST TO DETECT FAILURE" 47 | echo "-----------------------------------------------------------" 48 | 49 | mv ./test/metacoin.js ./metacoin.js 50 | mv ./buidler-metacoinjs-template.js ./test/buidler-metacoinjs-template.js 51 | mv ./test/TestMetacoin.sol ./TestMetacoin.sol 52 | 53 | npx buidler test --network development 54 | 55 | mv ./TestMetacoin.sol ./test/TestMetacoin.sol 56 | mv ./metacoin.js ./test/metacoin.js 57 | mv ./test/buidler-metacoinjs-template.js ./buidler-metacoinjs-template.js 58 | } 59 | -------------------------------------------------------------------------------- /mock/scripts/launch_testrpc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cleanup() { 4 | # Kill the testrpc instance that we started (if we started one and if it's still running). 5 | if [ -n "$testrpc_pid" ] && ps -p $testrpc_pid > /dev/null; then 6 | kill -9 $testrpc_pid 7 | fi 8 | } 9 | 10 | testrpc_port=8545 11 | 12 | testrpc_running() { 13 | nc -z localhost "$testrpc_port" 14 | } 15 | 16 | start_testrpc() { 17 | npx ganache-cli --gasLimit 8000000 "${accounts[@]}" > /dev/null & 18 | testrpc_pid=$! 19 | } 20 | -------------------------------------------------------------------------------- /mock/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Executes cleanup function at script exit. 4 | trap cleanup EXIT 5 | 6 | # Load helpers 7 | cd mock 8 | source ./scripts/integration_tests.sh 9 | source ./scripts/install_reporter.sh 10 | source ./scripts/launch_testrpc.sh 11 | 12 | # ----------------------- Conditional TestRPC Launch on 8545 --------------------------------------- 13 | 14 | if testrpc_running; then 15 | echo "Using existing client instance" 16 | else 17 | echo "Starting our own ganache-cli instance" 18 | start_testrpc 19 | fi 20 | 21 | # Buidler is super fast on launch 22 | sleep 5 23 | 24 | # ----------------------- Install Reporter and run tests ------------------------------------------ 25 | install_reporter 26 | test_truffle_v5_basic 27 | test_truffle_v5_with_options 28 | test_buildler_v5_plugin 29 | -------------------------------------------------------------------------------- /mock/test/TestMetacoin.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/MetaCoin.sol"; 6 | 7 | contract TestMetacoin { 8 | 9 | function testInitialBalanceUsingDeployedContract() public { 10 | MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin()); 11 | 12 | uint expected = 10000; 13 | 14 | Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 15 | } 16 | 17 | function testInitialBalanceWithNewMetaCoin() public { 18 | MetaCoin meta = new MetaCoin(); 19 | 20 | uint expected = 10000; 21 | 22 | Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /mock/test/encoderv2.js: -------------------------------------------------------------------------------- 1 | var EncoderV2 = artifacts.require("./EncoderV2.sol"); 2 | 3 | contract("EncoderV2", function(accounts) { 4 | let instance; 5 | 6 | beforeEach(async function() { 7 | instance = await EncoderV2.new(); 8 | }); 9 | 10 | it("should get & set an Asset with a struct", async function() { 11 | const asset = { 12 | a: "5", 13 | b: "7", 14 | c: "wowshuxkluh" 15 | }; 16 | 17 | await instance.setAsset44("44", asset); 18 | const _asset = await instance.getAsset(); 19 | 20 | assert.equal(_asset.a, asset.a); 21 | assert.equal(_asset.b, asset.b); 22 | assert.equal(_asset.c, asset.c); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /mock/test/etherrouter.js: -------------------------------------------------------------------------------- 1 | const EtherRouter = artifacts.require("EtherRouter"); 2 | const Resolver = artifacts.require("Resolver"); 3 | const Factory = artifacts.require("Factory"); 4 | const VersionA = artifacts.require("VersionA"); 5 | const VersionB = artifacts.require("VersionB"); 6 | 7 | contract("EtherRouter Proxy", accounts => { 8 | let router; 9 | let resolver; 10 | let factory; 11 | let versionA; 12 | let versionB; 13 | 14 | beforeEach(async function() { 15 | router = await EtherRouter.new(); 16 | resolver = await Resolver.new(); 17 | factory = await Factory.new(); 18 | versionA = await VersionA.new(); 19 | 20 | // Emulate internal deployment 21 | await factory.deployVersionB(); 22 | const versionBAddress = await factory.versionB(); 23 | versionB = await VersionB.at(versionBAddress); 24 | }); 25 | 26 | it("Resolves methods routed through an EtherRouter proxy", async function() { 27 | let options = { 28 | from: accounts[0], 29 | gas: 4000000, 30 | to: router.address, 31 | gasPrice: 20000000000 32 | }; 33 | 34 | await router.setResolver(resolver.address); 35 | 36 | await resolver.register("setValue()", versionA.address); 37 | options.data = versionA.contract.methods.setValue().encodeABI(); 38 | await web3.eth.sendTransaction(options); 39 | 40 | await resolver.register("setValue()", versionB.address); 41 | options.data = versionB.contract.methods.setValue().encodeABI(); 42 | await web3.eth.sendTransaction(options); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /mock/test/metacoin.js: -------------------------------------------------------------------------------- 1 | var MetaCoin = artifacts.require('./MetaCoin.sol') 2 | 3 | contract('MetaCoin', function (accounts) { 4 | beforeEach(async function () { 5 | await MetaCoin.new() 6 | await MetaCoin.new() 7 | }) 8 | afterEach(async function () { 9 | await MetaCoin.new() 10 | await MetaCoin.new() 11 | }) 12 | 13 | it('should put 10000 MetaCoin in the first account', function () { 14 | return MetaCoin.deployed().then(function (instance) { 15 | return instance.getBalance.call(accounts[0]) 16 | }).then(function (balance) { 17 | assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account") 18 | }) 19 | }) 20 | it('should call a function that depends on a linked library', function () { 21 | var meta 22 | var metaCoinBalance 23 | var metaCoinEthBalance 24 | 25 | return MetaCoin.deployed().then(function (instance) { 26 | meta = instance 27 | return meta.getBalance.call(accounts[0]) 28 | }).then(function (outCoinBalance) { 29 | metaCoinBalance = parseInt(outCoinBalance.toString()) 30 | return meta.getBalanceInEth.call(accounts[0]) 31 | }).then(function (outCoinBalanceEth) { 32 | metaCoinEthBalance = parseInt(outCoinBalanceEth.toString()) 33 | }).then(function () { 34 | assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, 'Library function returned unexpected function, linkage may be broken') 35 | }) 36 | }) 37 | it('should send coin correctly', function () { 38 | var meta 39 | 40 | // Get initial balances of first and second account. 41 | var account_one = accounts[0] 42 | var account_two = accounts[1] 43 | 44 | var account_one_starting_balance 45 | var account_two_starting_balance 46 | var account_one_ending_balance 47 | var account_two_ending_balance 48 | 49 | var amount = 10 50 | 51 | return MetaCoin.deployed().then(function (instance) { 52 | meta = instance 53 | return meta.getBalance.call(account_one) 54 | }).then(function (balance) { 55 | account_one_starting_balance = parseInt(balance.toString()) 56 | return meta.getBalance.call(account_two) 57 | }).then(function (balance) { 58 | account_two_starting_balance = parseInt(balance.toString()) 59 | return meta.sendCoin(account_two, amount, {from: account_one}) 60 | }).then(function () { 61 | return meta.getBalance.call(account_one) 62 | }).then(function (balance) { 63 | account_one_ending_balance = parseInt(balance.toString()) 64 | return meta.getBalance.call(account_two) 65 | }).then(function (balance) { 66 | account_two_ending_balance = parseInt(balance.toString()) 67 | 68 | assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender") 69 | assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver") 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /mock/test/multicontract.js: -------------------------------------------------------------------------------- 1 | const MultiContractFileA = artifacts.require('MultiContractFileA'); 2 | const MultiContractFileB = artifacts.require('MultiContractFileB'); 3 | 4 | contract('MultiContractFiles', accounts => { 5 | let a 6 | let b 7 | 8 | beforeEach(async function () { 9 | a = await MultiContractFileA.new() 10 | b = await MultiContractFileB.new() 11 | }) 12 | 13 | it('a and b', async function(){ 14 | await a.hello(); 15 | await b.goodbye(); 16 | }) 17 | }); -------------------------------------------------------------------------------- /mock/test/random.js: -------------------------------------------------------------------------------- 1 | const randomstring = require("randomstring"); 2 | 3 | function getRandomInt(max) { 4 | return Math.floor(Math.random() * Math.floor(max)); 5 | } 6 | 7 | function random() { 8 | return randomstring.generate(getRandomInt(50)); 9 | } 10 | 11 | module.exports = random; 12 | -------------------------------------------------------------------------------- /mock/test/variableconstructor.js: -------------------------------------------------------------------------------- 1 | const random = require("./random"); 2 | const VariableConstructor = artifacts.require("./VariableConstructor.sol"); 3 | 4 | contract("VariableConstructor", accounts => { 5 | it("should should initialize with a short string", async () => { 6 | await VariableConstructor.new("Exit Visa"); 7 | }); 8 | 9 | it("should should initialize with a medium length string", async () => { 10 | await VariableConstructor.new("Enclosed is my application for residency"); 11 | }); 12 | 13 | it("should should initialize with a long string", async () => { 14 | let msg = 15 | "Enclosed is my application for permanent residency in NewZealand."; 16 | msg += "I am a computer programmer."; 17 | await VariableConstructor.new(msg); 18 | }); 19 | 20 | it("should should initialize with a random length string", async () => { 21 | await VariableConstructor.new(random()); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /mock/test/variablecosts.js: -------------------------------------------------------------------------------- 1 | const random = require("./random"); 2 | const VariableCosts = artifacts.require("./VariableCosts.sol"); 3 | const Wallet = artifacts.require("./Wallet.sol"); 4 | 5 | contract("VariableCosts", accounts => { 6 | const one = [1]; 7 | const three = [2, 3, 4]; 8 | const five = [5, 6, 7, 8, 9]; 9 | let instance; 10 | let walletB; 11 | 12 | beforeEach(async () => { 13 | instance = await VariableCosts.new(); 14 | walletB = await Wallet.new(); 15 | }); 16 | 17 | it("should add one", async () => { 18 | await instance.addToMap(one); 19 | }); 20 | 21 | it("should add three", async () => { 22 | await instance.addToMap(three); 23 | }); 24 | 25 | it("should add even 5!", async () => { 26 | await instance.addToMap(five); 27 | }); 28 | 29 | it("should delete one", async () => { 30 | await instance.removeFromMap(one); 31 | }); 32 | 33 | it("should delete three", async () => { 34 | await instance.removeFromMap(three); 35 | }); 36 | 37 | it("should delete five", async () => { 38 | await instance.removeFromMap(five); 39 | }); 40 | 41 | it("should add five and delete one", async () => { 42 | await instance.addToMap(five); 43 | await instance.removeFromMap(one); 44 | }); 45 | 46 | it("should set a random length string", async () => { 47 | await instance.setString(random()); 48 | await instance.setString(random()); 49 | await instance.setString(random()); 50 | }); 51 | 52 | it("methods that do not throw", async () => { 53 | await instance.methodThatThrows(false); 54 | }); 55 | 56 | it("methods that throw", async () => { 57 | try { 58 | await instance.methodThatThrows(true); 59 | } catch (e) {} 60 | }); 61 | 62 | it("methods that call methods in other contracts", async () => { 63 | await instance.otherContractMethod(); 64 | }); 65 | 66 | it("prints a table at end of test suites with failures", async () => { 67 | assert(false); 68 | }); 69 | 70 | // VariableCosts is Wallet. We also have Wallet tests. So we should see 71 | // separate entries for `sendPayment` / `transferPayment` under VariableCosts 72 | // and Wallet in the report 73 | it("should allow contracts to have identically named methods", async () => { 74 | await instance.sendTransaction({ 75 | value: 100, 76 | from: accounts[0] 77 | }); 78 | await instance.sendPayment(50, walletB.address, { 79 | from: accounts[0] 80 | }); 81 | await instance.transferPayment(50, walletB.address, { 82 | from: accounts[0] 83 | }); 84 | const balance = await walletB.getBalance(); 85 | assert.equal(parseInt(balance.toString()), 100); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /mock/test/wallet.js: -------------------------------------------------------------------------------- 1 | const Wallet = artifacts.require('./Wallet.sol') 2 | 3 | contract('Wallet', accounts => { 4 | let walletA 5 | let walletB 6 | 7 | beforeEach(async function () { 8 | walletA = await Wallet.new() 9 | walletB = await Wallet.new() 10 | }) 11 | 12 | it('should be very expensive to deploy', async() => { 13 | await Wallet.new() 14 | }) 15 | 16 | it('should should allow transfers and sends', async () => { 17 | await walletA.sendTransaction({ 18 | value: 100, from: accounts[0] 19 | }) 20 | await walletA.sendPayment(50, walletB.address, { 21 | from: accounts[0] 22 | }) 23 | await walletA.transferPayment(50, walletB.address, { 24 | from: accounts[0] 25 | }) 26 | const balance = await walletB.getBalance() 27 | assert.equal(parseInt(balance.toString()), 100) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /mock/truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8545, 6 | network_id: "*" // Match any network id 7 | } 8 | }, 9 | mocha: { 10 | reporter: "eth-gas-reporter" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-gas-reporter", 3 | "version": "0.2.27", 4 | "description": "Mocha reporter which shows gas used per unit test.", 5 | "main": "index.js", 6 | "scripts": { 7 | "geth": "./scripts/geth.sh", 8 | "test": "./mock/scripts/test.sh", 9 | "ci": "./scripts/ci.sh" 10 | }, 11 | "husky": { 12 | "hooks": { 13 | "pre-commit": "pretty-quick --staged" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cgewecke/eth-gas-reporter.git" 19 | }, 20 | "keywords": [ 21 | "Ethereum", 22 | "solidity", 23 | "unit-testing", 24 | "truffle", 25 | "gas." 26 | ], 27 | "author": "cgewecke ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/cgewecke/eth-gas-reporter/issues" 31 | }, 32 | "homepage": "https://github.com/cgewecke/eth-gas-reporter#readme", 33 | "peerDependencies": { 34 | "@codechecks/client": "^0.1.0" 35 | }, 36 | "peerDependenciesMeta": { 37 | "@codechecks/client": { 38 | "optional": true 39 | } 40 | }, 41 | "dependencies": { 42 | "@solidity-parser/parser": "^0.14.0", 43 | "axios": "^1.5.1", 44 | "cli-table3": "^0.5.0", 45 | "colors": "1.4.0", 46 | "ethereum-cryptography": "^1.0.3", 47 | "ethers": "^5.7.2", 48 | "fs-readdir-recursive": "^1.1.0", 49 | "lodash": "^4.17.14", 50 | "markdown-table": "^1.1.3", 51 | "mocha": "^10.2.0", 52 | "req-cwd": "^2.0.0", 53 | "sha1": "^1.1.1", 54 | "sync-request": "^6.0.0" 55 | }, 56 | "devDependencies": { 57 | "@codechecks/client": "^0.1.10", 58 | "@nomiclabs/buidler": "^1.4.7", 59 | "@nomiclabs/buidler-truffle5": "^1.3.4", 60 | "@nomiclabs/buidler-web3": "^1.3.4", 61 | "fp-ts": "^1.19.0", 62 | "ganache-cli": "^6.12.2", 63 | "geth-dev-assistant": "^0.1.7", 64 | "husky": "^2.3.0", 65 | "prettier": "1.17.1", 66 | "pretty-quick": "^1.11.0", 67 | "randomstring": "^1.1.5", 68 | "truffle": "5.0.12", 69 | "web3": "^1.3.0", 70 | "yarn": "^1.16.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | 5 | if [ "$TEST" = "integration" ]; then 6 | 7 | npm test 8 | 9 | elif [ "$TEST" = "geth" ]; then 10 | 11 | npx geth-dev-assistant \ 12 | --launch \ 13 | --tag 'latest' \ 14 | --accounts 4 \ 15 | --balance 100 \ 16 | --gasLimit 8000000 17 | 18 | npm test 19 | docker stop geth-client 20 | 21 | elif [ "$TEST" = "colony" ]; then 22 | 23 | npm install -g yarn 24 | git clone https://github.com/JoinColony/colonyNetwork.git 25 | cd colonyNetwork || exit 26 | yarn 27 | yarn remove -W eth-gas-reporter --dev 28 | 29 | env 30 | 31 | SLUG="$TRAVIS_REPO_SLUG" 32 | BRANCH="$TRAVIS_BRANCH" 33 | 34 | if [ -n "$TRAVIS_PULL_REQUEST_SLUG" ]; then 35 | SLUG="$TRAVIS_PULL_REQUEST_SLUG" 36 | fi 37 | 38 | if [ -n "$TRAVIS_PULL_REQUEST_BRANCH" ]; then 39 | BRANCH="$TRAVIS_PULL_REQUEST_BRANCH" 40 | fi 41 | 42 | echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 43 | echo "TESTING BRANCH: https://github.com/$SLUG.git#$BRANCH" 44 | echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 45 | 46 | yarn add -W https://github.com/"$SLUG".git#"$BRANCH" 47 | git submodule update --init 48 | yarn run provision:token:contracts 49 | DEBUG_CODECHECKS_TABLE=true yarn run test:contracts:gasCosts 50 | 51 | fi --------------------------------------------------------------------------------