UNPKG

optioeaque

Version:

Mocha reporter which shows gas used per unit test.

245 lines (213 loc) 7.43 kB
const ethersABI = require("@ethersproject/abi"); const { keccak256 } = require("ethereum-cryptography/keccak"); const { utf8ToBytes, bytesToHex } = require("ethereum-cryptography/utils"); const sha1 = require("sha1"); const utils = require("./utils"); const SyncRequest = require("./syncRequest"); const Artifactor = require("./artifactor"); /** * Data store written to by TransactionWatcher and consumed by the GasTable. */ class GasData { constructor() { this.addressCache = {}; this.methods = {}; this.deployments = []; this.codeHashMap = {}; this.blockLimit; this.sync; } /** * + Compiles pre-test gas usage (e.g. from `truffle migrate`) * + Sets up data structures to store deployments and methods gas usage * + Called in the mocha `start` hook to guarantee it's run later than pre-test deployments * @param {Object} config */ initialize(config) { this.sync = new SyncRequest(config.url); this.provider = config.provider; const artifactor = new Artifactor(config); // Get the current blockLimit; // TODO: This shouldn't be here - should be on the config object & // fetched when the table is written or something. this.blockLimit = config.blockLimit; if (!this.blockLimit && !this.provider) { const block = this.sync.getLatestBlock(); this.blockLimit = utils.gas(block.gasLimit); } for (const contract of artifactor.getContracts()) { const contractInfo = { name: contract.name, bytecode: contract.artifact.bytecode, deployedBytecode: contract.artifact.deployedBytecode, gasData: [] }; this.deployments.push(contractInfo); // Report gas used during pre-test deployments (ex: truffle migrate) if ( contract.artifact.deployed && contract.artifact.deployed.transactionHash && !this.provider ) { const receipt = this.sync.getTransactionReceipt( contract.artifact.deployed.transactionHash ); if (receipt) { // Sync: only runs for Truffle atm... this.trackNameByAddress( contract.name, contract.artifact.deployed.address ); contractInfo.gasData.push(utils.gas(receipt.gasUsed)); } } if (contract.artifact.bytecodeHash) { this.trackNameByPreloadedAddress( contract.name, contract.artifact.address, contract.artifact.bytecodeHash ); } // Decode, getMethodIDs const methodIDs = {}; let methods; try { methods = new ethersABI.Interface(contract.artifact.abi).functions; } catch (err) { utils.warnEthers(contract.name, err); return; } // Generate sighashes and remap ethers to something similar // to abiDecoder.getMethodIDs Object.keys(methods).forEach(key => { const sighash = bytesToHex(keccak256(utf8ToBytes(key))).slice(0, 8); methodIDs[sighash] = Object.assign({ fnSig: key }, methods[key]); }); // Create Method Map; Object.keys(methodIDs).forEach(key => { const isInterface = contract.artifact.bytecode === "0x"; const isCall = methodIDs[key].type === "call"; const methodHasName = methodIDs[key].name !== undefined; if (methodHasName && !isCall && !isInterface) { this.methods[contract.name + "_" + key] = { key: key, contract: contract.name, method: methodIDs[key].name, fnSig: methodIDs[key].fnSig, gasData: [], numberOfCalls: 0 }; } }); } } /** * Map a contract name to the sha1 hash of the code stored at an address * @param {String} name contract name * @param {String} address contract address */ trackNameByAddress(name, address) { if (this.addressIsCached(address)) return; const code = this.sync.getCode(address); const hash = code ? sha1(code) : null; this.codeHashMap[hash] = name; this.addressCache[address] = name; } /** * Map a contract name to pre-generated hash of the code stored at an address * @param {String} name contract name * @param {String} address contract address */ trackNameByPreloadedAddress(name, address, hash) { if (this.addressIsCached(address)) return; this.codeHashMap[hash] = name; this.addressCache[address] = name; } /** * Get the name of the contract stored at contract address * @param {String} address contract address * @return {String} contract name */ getNameByAddress(address) { if (this.addressIsCached(address)) { return this.addressCache[address]; } const code = this.sync.getCode(address); const hash = code ? sha1(code) : null; return this.codeHashMap[hash]; } /** * Map a contract name to the sha1 hash of the code stored at an address * @param {String} name contract name * @param {String} address contract address */ async asyncTrackNameByAddress(name, address) { if (this.addressIsCached(address)) return; const code = await this.provider.getCode(address); const hash = code ? sha1(code) : null; this.codeHashMap[hash] = name; this.addressCache[address] = name; } /** * Get the name of the contract stored at contract address * @param {String} address contract address * @return {String} contract name */ async asyncGetNameByAddress(address) { if (this.addressIsCached(address)) { return this.addressCache[address]; } const code = await this.provider.getCode(address); const hash = code ? sha1(code) : null; return this.codeHashMap[hash]; } /** * Compares existing contract binaries to the input code for a * new deployment transaction and returns the relevant contract. * Ignores interfaces. * @param {String} input tx.input * @return {Object} this.deployments entry */ getContractByDeploymentInput(input) { if (!input) return null; const matches = this.deployments.filter(item => utils.matchBinaries(input, item.bytecode) ); // Filter interfaces return matches && matches.length ? matches.find(item => item.bytecode !== "0x") : null; } /** * Compares code at an address to the deployedBytecode for all * compiled contracts and returns the relevant item. * Ignores interfaces. * @param {String} code result of web3.eth.getCode * @return {Object} this.deployments entry */ getContractByDeployedBytecode(code) { if (!code) return null; const matches = this.deployments.filter(item => utils.matchBinaries(code, item.deployedBytecode) ); // Filter interfaces return matches && matches.length ? matches.find(item => item.deployedBytecode !== "0x") : null; } /** * Returns all contracts with a method matching the requested signature * @param {String} signature method signature hash * @return {Object[]} this.method entries array */ getAllContractsWithMethod(signature) { return Object.values(this.methods).filter(el => el.key === signature); } addressIsCached(address) { return Object.keys(this.addressCache).includes(address); } resetAddressCache() { this.addressCache = {}; } } module.exports = GasData;