diff options
55 files changed, 615 insertions, 229 deletions
diff --git a/packages/0x.js/test/artifacts_test.ts b/packages/0x.js/test/artifacts_test.ts index ca554a4f5..982fb8e63 100644 --- a/packages/0x.js/test/artifacts_test.ts +++ b/packages/0x.js/test/artifacts_test.ts @@ -15,7 +15,7 @@ const TIMEOUT = 10000; describe('Artifacts', () => { describe('contracts are deployed on kovan', () => { const kovanRpcUrl = constants.KOVAN_RPC_URL; - const provider = web3Factory.create({ rpcUrl: kovanRpcUrl }).currentProvider; + const provider = web3Factory.getRpcProvider({ rpcUrl: kovanRpcUrl }); const config = { networkId: constants.KOVAN_NETWORK_ID, }; @@ -32,7 +32,7 @@ describe('Artifacts', () => { }); describe('contracts are deployed on ropsten', () => { const ropstenRpcUrl = constants.ROPSTEN_RPC_URL; - const provider = web3Factory.create({ rpcUrl: ropstenRpcUrl }).currentProvider; + const provider = web3Factory.getRpcProvider({ rpcUrl: ropstenRpcUrl }); const config = { networkId: constants.ROPSTEN_NETWORK_ID, }; diff --git a/packages/0x.js/test/utils/web3_wrapper.ts b/packages/0x.js/test/utils/web3_wrapper.ts index b0ccfa546..71a0dc1c2 100644 --- a/packages/0x.js/test/utils/web3_wrapper.ts +++ b/packages/0x.js/test/utils/web3_wrapper.ts @@ -2,8 +2,7 @@ import { devConstants, web3Factory } from '@0xproject/dev-utils'; import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; -const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); -const provider: Provider = web3.currentProvider; -const web3Wrapper = new Web3Wrapper(web3.currentProvider); +const provider: Provider = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true }); +const web3Wrapper = new Web3Wrapper(provider); export { provider, web3Wrapper }; diff --git a/packages/contract-wrappers/test/artifacts_test.ts b/packages/contract-wrappers/test/artifacts_test.ts index 446b8f9d1..eaaa89c48 100644 --- a/packages/contract-wrappers/test/artifacts_test.ts +++ b/packages/contract-wrappers/test/artifacts_test.ts @@ -15,7 +15,7 @@ const TIMEOUT = 10000; describe('Artifacts', () => { describe('contracts are deployed on kovan', () => { const kovanRpcUrl = constants.KOVAN_RPC_URL; - const provider = web3Factory.create({ rpcUrl: kovanRpcUrl }).currentProvider; + const provider = web3Factory.getRpcProvider({ rpcUrl: kovanRpcUrl }); const config = { networkId: constants.KOVAN_NETWORK_ID, }; @@ -32,7 +32,7 @@ describe('Artifacts', () => { }); describe('contracts are deployed on ropsten', () => { const ropstenRpcUrl = constants.ROPSTEN_RPC_URL; - const provider = web3Factory.create({ rpcUrl: ropstenRpcUrl }).currentProvider; + const provider = web3Factory.getRpcProvider({ rpcUrl: ropstenRpcUrl }); const config = { networkId: constants.ROPSTEN_NETWORK_ID, }; diff --git a/packages/contract-wrappers/test/utils/web3_wrapper.ts b/packages/contract-wrappers/test/utils/web3_wrapper.ts index b0ccfa546..71a0dc1c2 100644 --- a/packages/contract-wrappers/test/utils/web3_wrapper.ts +++ b/packages/contract-wrappers/test/utils/web3_wrapper.ts @@ -2,8 +2,7 @@ import { devConstants, web3Factory } from '@0xproject/dev-utils'; import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; -const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); -const provider: Provider = web3.currentProvider; -const web3Wrapper = new Web3Wrapper(web3.currentProvider); +const provider: Provider = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true }); +const web3Wrapper = new Web3Wrapper(provider); export { provider, web3Wrapper }; diff --git a/packages/contracts/package.json b/packages/contracts/package.json index be3ca3bc4..1c66a3b86 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -44,6 +44,8 @@ "@0xproject/abi-gen": "^0.3.0", "@0xproject/dev-utils": "^0.4.2", "@0xproject/tslint-config": "^0.4.18", + "@0xproject/subproviders": "^0.10.1", + "@0xproject/sol-cov": "^0.0.11", "@types/lodash": "4.14.104", "@types/node": "^8.0.53", "@types/yargs": "^10.0.0", diff --git a/packages/contracts/src/utils/coverage.ts b/packages/contracts/src/utils/coverage.ts new file mode 100644 index 000000000..a37939464 --- /dev/null +++ b/packages/contracts/src/utils/coverage.ts @@ -0,0 +1,21 @@ +import { devConstants } from '@0xproject/dev-utils'; +import { CoverageSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; +import * as fs from 'fs'; +import * as _ from 'lodash'; + +let coverageSubprovider: CoverageSubprovider; + +export const coverage = { + getCoverageSubproviderSingleton(): CoverageSubprovider { + if (_.isUndefined(coverageSubprovider)) { + coverageSubprovider = coverage._getCoverageSubprovider(); + } + return coverageSubprovider; + }, + _getCoverageSubprovider(): CoverageSubprovider { + const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(); + const subprovider = new CoverageSubprovider(solCompilerArtifactAdapter, defaultFromAddress); + return subprovider; + }, +}; diff --git a/packages/contracts/test/utils/match_order_tester.ts b/packages/contracts/src/utils/match_order_tester.ts index 43196728c..30039937f 100644 --- a/packages/contracts/test/utils/match_order_tester.ts +++ b/packages/contracts/src/utils/match_order_tester.ts @@ -5,24 +5,24 @@ import * as chai from 'chai'; import ethUtil = require('ethereumjs-util'); import * as _ from 'lodash'; -import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; -import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; -import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; -import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_proxy'; +import { DummyERC20TokenContract } from '../contract_wrappers/generated/dummy_e_r_c20_token'; +import { DummyERC721TokenContract } from '../contract_wrappers/generated/dummy_e_r_c721_token'; +import { ERC20ProxyContract } from '../contract_wrappers/generated/e_r_c20_proxy'; +import { ERC721ProxyContract } from '../contract_wrappers/generated/e_r_c721_proxy'; import { CancelContractEventArgs, ExchangeContract, FillContractEventArgs, -} from '../../src/contract_wrappers/generated/exchange'; -import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; -import { chaiSetup } from '../../src/utils/chai_setup'; -import { constants } from '../../src/utils/constants'; -import { crypto } from '../../src/utils/crypto'; -import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; -import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; -import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; -import { OrderFactory } from '../../src/utils/order_factory'; -import { orderUtils } from '../../src/utils/order_utils'; +} from '../contract_wrappers/generated/exchange'; +import { assetProxyUtils } from '../utils/asset_proxy_utils'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { crypto } from '../utils/crypto'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { orderUtils } from '../utils/order_utils'; import { AssetProxyId, ContractName, @@ -31,8 +31,8 @@ import { ExchangeStatus, SignedOrder, TransferAmountsByMatchOrders as TransferAmounts, -} from '../../src/utils/types'; -import { provider, web3Wrapper } from '../../src/utils/web3_wrapper'; +} from '../utils/types'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; diff --git a/packages/contracts/src/utils/web3_wrapper.ts b/packages/contracts/src/utils/web3_wrapper.ts index ed1c488a2..02595506b 100644 --- a/packages/contracts/src/utils/web3_wrapper.ts +++ b/packages/contracts/src/utils/web3_wrapper.ts @@ -1,12 +1,19 @@ -import { devConstants, web3Factory } from '@0xproject/dev-utils'; +import { devConstants, env, EnvVars, web3Factory } from '@0xproject/dev-utils'; +import { prependSubprovider } from '@0xproject/subproviders'; import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { coverage } from './coverage'; + export const txDefaults = { from: devConstants.TESTRPC_FIRST_ADDRESS, gas: devConstants.GAS_ESTIMATE, }; const providerConfigs = { shouldUseInProcessGanache: true }; -export const web3 = web3Factory.create(providerConfigs); -export const provider = web3.currentProvider; +export const provider = web3Factory.getRpcProvider(providerConfigs); +const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); +if (isCoverageEnabled) { + const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); + prependSubprovider(provider, coverageSubprovider); +} export const web3Wrapper = new Web3Wrapper(provider); diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts index e732c90db..8a3c91520 100644 --- a/packages/contracts/test/exchange/match_orders.ts +++ b/packages/contracts/test/exchange/match_orders.ts @@ -37,7 +37,7 @@ import { } from '../../src/utils/types'; import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper'; -import { MatchOrderTester } from '../utils/match_order_tester'; +import { MatchOrderTester } from '../../src/utils/match_order_tester'; chaiSetup.configure(); const expect = chai.expect; diff --git a/packages/contracts/test/global_hooks.ts b/packages/contracts/test/global_hooks.ts index 089521d94..8b48b030d 100644 --- a/packages/contracts/test/global_hooks.ts +++ b/packages/contracts/test/global_hooks.ts @@ -1,4 +1,6 @@ -import { coverage, env, EnvVars } from '@0xproject/dev-utils'; +import { env, EnvVars } from '@0xproject/dev-utils'; + +import { coverage } from '../src/utils/coverage'; after('generate coverage report', async () => { if (env.parseBoolean(EnvVars.SolidityCoverage)) { diff --git a/packages/dev-utils/CHANGELOG.json b/packages/dev-utils/CHANGELOG.json index 19f2591d2..ecb43a42e 100644 --- a/packages/dev-utils/CHANGELOG.json +++ b/packages/dev-utils/CHANGELOG.json @@ -3,6 +3,10 @@ "version": "0.4.2", "changes": [ { + "note": "Pass SolCompilerArtifactAdapter to CoverageSubprovider", + "pr": 589 + }, + { "note": "Move callbackErrorReporter over from 0x.js", "pr": 579 } diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 96180b2ce..0b11029f6 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -44,7 +44,6 @@ "typescript": "2.7.1" }, "dependencies": { - "@0xproject/sol-cov": "^0.0.11", "@0xproject/subproviders": "^0.10.2", "@0xproject/types": "^0.7.0", "@0xproject/typescript-typings": "^0.3.2", diff --git a/packages/dev-utils/src/coverage.ts b/packages/dev-utils/src/coverage.ts deleted file mode 100644 index 6f7640835..000000000 --- a/packages/dev-utils/src/coverage.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CoverageSubprovider } from '@0xproject/sol-cov'; -import * as _ from 'lodash'; - -import { constants } from './constants'; - -let coverageSubprovider: CoverageSubprovider; - -export const coverage = { - getCoverageSubproviderSingleton(): CoverageSubprovider { - if (_.isUndefined(coverageSubprovider)) { - coverageSubprovider = coverage._getCoverageSubprovider(); - } - return coverageSubprovider; - }, - _getCoverageSubprovider(): CoverageSubprovider { - const artifactsPath = '../migrations/artifacts/1.0.0'; - const contractsPath = 'src/contracts'; - const defaultFromAddress = constants.TESTRPC_FIRST_ADDRESS; - return new CoverageSubprovider(artifactsPath, contractsPath, defaultFromAddress); - }, -}; diff --git a/packages/dev-utils/src/index.ts b/packages/dev-utils/src/index.ts index 9124f3e28..d4c19f1bf 100644 --- a/packages/dev-utils/src/index.ts +++ b/packages/dev-utils/src/index.ts @@ -1,6 +1,5 @@ export { BlockchainLifecycle } from './blockchain_lifecycle'; export { web3Factory } from './web3_factory'; export { constants as devConstants } from './constants'; -export { coverage } from './coverage'; export { env, EnvVars } from './env'; export { callbackErrorReporter } from './callback_error_reporter'; diff --git a/packages/dev-utils/src/web3_factory.ts b/packages/dev-utils/src/web3_factory.ts index 4cd343c44..5b32d3930 100644 --- a/packages/dev-utils/src/web3_factory.ts +++ b/packages/dev-utils/src/web3_factory.ts @@ -13,16 +13,8 @@ import * as _ from 'lodash'; import * as process from 'process'; import { constants } from './constants'; -import { coverage } from './coverage'; import { env, EnvVars } from './env'; -// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang -// because they are using the wrong XHR package. -// importing web3 after subproviders fixes this issue -// Filed issue: https://github.com/ethereum/web3.js/issues/844 -// tslint:disable-next-line:ordered-imports -import * as Web3 from 'web3'; - export interface Web3Config { hasAddresses?: boolean; // default: true shouldUseInProcessGanache?: boolean; // default: false @@ -30,18 +22,8 @@ export interface Web3Config { } export const web3Factory = { - create(config: Web3Config = {}): Web3 { - const provider = this.getRpcProvider(config); - const web3 = new Web3(); - web3.setProvider(provider); - return web3; - }, - getRpcProvider(config: Web3Config = {}): Provider { + getRpcProvider(config: Web3Config = {}): ProviderEngine { const provider = new ProviderEngine(); - const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); - if (isCoverageEnabled) { - provider.addProvider(coverage.getCoverageSubproviderSingleton()); - } const hasAddresses = _.isUndefined(config.hasAddresses) || config.hasAddresses; if (!hasAddresses) { provider.addProvider(new EmptyWalletSubprovider()); diff --git a/packages/metacoin/test/utils/config.ts b/packages/metacoin/test/utils/config.ts index 389edb388..ef4932845 100644 --- a/packages/metacoin/test/utils/config.ts +++ b/packages/metacoin/test/utils/config.ts @@ -3,8 +3,8 @@ import * as path from 'path'; export const config = { networkId: 50, - artifactsDir: path.resolve(__dirname, '../../artifacts'), - contractsDir: path.resolve(__dirname, '../../contracts'), + artifactsDir: 'artifacts', + contractsDir: 'contracts', ganacheLogFile: 'ganache.log', txDefaults: { from: devConstants.TESTRPC_FIRST_ADDRESS, diff --git a/packages/metacoin/test/utils/coverage.ts b/packages/metacoin/test/utils/coverage.ts index debd544ed..945afb0a7 100644 --- a/packages/metacoin/test/utils/coverage.ts +++ b/packages/metacoin/test/utils/coverage.ts @@ -1,5 +1,5 @@ import { devConstants } from '@0xproject/dev-utils'; -import { CoverageSubprovider } from '@0xproject/sol-cov'; +import { CoverageSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov'; import * as _ from 'lodash'; import { config } from './config'; @@ -15,6 +15,7 @@ export const coverage = { }, _getCoverageSubprovider(): CoverageSubprovider { const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; - return new CoverageSubprovider(config.artifactsDir, config.contractsDir, defaultFromAddress); + const zeroExArtifactsAdapter = new SolCompilerArtifactAdapter(config.artifactsDir, config.contractsDir); + return new CoverageSubprovider(zeroExArtifactsAdapter, defaultFromAddress); }, }; diff --git a/packages/metacoin/test/utils/web3_wrapper.ts b/packages/metacoin/test/utils/web3_wrapper.ts index b4bb61f09..724ed4e1f 100644 --- a/packages/metacoin/test/utils/web3_wrapper.ts +++ b/packages/metacoin/test/utils/web3_wrapper.ts @@ -1,5 +1,5 @@ import { env, EnvVars } from '@0xproject/dev-utils'; -import { GanacheSubprovider } from '@0xproject/subproviders'; +import { GanacheSubprovider, prependSubprovider } from '@0xproject/subproviders'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as fs from 'fs'; import * as _ from 'lodash'; @@ -9,10 +9,6 @@ import { config } from './config'; import { coverage } from './coverage'; export const provider = new ProviderEngine(); -const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); -if (isCoverageEnabled) { - provider.addProvider(coverage.getCoverageSubproviderSingleton()); -} provider.addProvider( new GanacheSubprovider({ logger: { @@ -27,4 +23,10 @@ provider.addProvider( ); provider.start(); +const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage); +if (isCoverageEnabled) { + const coverageSubprovider = coverage.getCoverageSubproviderSingleton(); + prependSubprovider(provider, coverageSubprovider); +} + export const web3Wrapper = new Web3Wrapper(provider); diff --git a/packages/migrations/package.json b/packages/migrations/package.json index 6d8ff591c..08f7aa6b2 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -9,7 +9,7 @@ "types": "lib/index.d.ts", "scripts": { "watch": "tsc -w", - "prebuild": "run-s clean compile copy_artifacts generate_contract_wrappers", + "prebuild": "run-s clean copy_artifacts generate_contract_wrappers", "copy_artifacts": "copyfiles 'artifacts/1.0.0/**/*' ./lib", "build": "tsc", "clean": "shx rm -rf lib src/contract_wrappers", diff --git a/packages/migrations/src/migrate.ts b/packages/migrations/src/migrate.ts index b00ba698f..1230f376e 100644 --- a/packages/migrations/src/migrate.ts +++ b/packages/migrations/src/migrate.ts @@ -11,8 +11,7 @@ import { runMigrationsAsync } from './migration'; from: devConstants.TESTRPC_FIRST_ADDRESS, }; const providerConfigs = { shouldUseInProcessGanache: false }; - const web3 = web3Factory.create(providerConfigs); - const provider = web3.currentProvider; + const provider: Provider = web3Factory.getRpcProvider(providerConfigs); const artifactsDir = 'artifacts/1.0.0'; await runMigrationsAsync(provider, artifactsDir, txDefaults); process.exit(0); diff --git a/packages/order-utils/test/utils/web3_wrapper.ts b/packages/order-utils/test/utils/web3_wrapper.ts index b0ccfa546..71a0dc1c2 100644 --- a/packages/order-utils/test/utils/web3_wrapper.ts +++ b/packages/order-utils/test/utils/web3_wrapper.ts @@ -2,8 +2,7 @@ import { devConstants, web3Factory } from '@0xproject/dev-utils'; import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; -const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); -const provider: Provider = web3.currentProvider; -const web3Wrapper = new Web3Wrapper(web3.currentProvider); +const provider: Provider = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true }); +const web3Wrapper = new Web3Wrapper(provider); export { provider, web3Wrapper }; diff --git a/packages/order-watcher/test/utils/web3_wrapper.ts b/packages/order-watcher/test/utils/web3_wrapper.ts index b0ccfa546..71a0dc1c2 100644 --- a/packages/order-watcher/test/utils/web3_wrapper.ts +++ b/packages/order-watcher/test/utils/web3_wrapper.ts @@ -2,8 +2,7 @@ import { devConstants, web3Factory } from '@0xproject/dev-utils'; import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; -const web3 = web3Factory.create({ shouldUseInProcessGanache: true }); -const provider: Provider = web3.currentProvider; -const web3Wrapper = new Web3Wrapper(web3.currentProvider); +const provider: Provider = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true }); +const web3Wrapper = new Web3Wrapper(provider); export { provider, web3Wrapper }; diff --git a/packages/sol-compiler/CHANGELOG.json b/packages/sol-compiler/CHANGELOG.json index c558a98c5..33fd99e4f 100644 --- a/packages/sol-compiler/CHANGELOG.json +++ b/packages/sol-compiler/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Properly export the executable binary", "pr": 588 + }, + { + "note": "Add the ability to define a specific solidity version", + "pr": 589 } ], "timestamp": 1527009133 diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index c17616246..2ebc5228e 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -74,6 +74,7 @@ export class Compiler { private _contractsDir: string; private _compilerSettings: solc.CompilerSettings; private _artifactsDir: string; + private _solcVersionIfExists: string | undefined; private _specifiedContracts: string[] | TYPE_ALL_FILES_IDENTIFIER; /** * Instantiates a new instance of the Compiler class. @@ -85,6 +86,7 @@ export class Compiler { ? JSON.parse(fs.readFileSync(CONFIG_FILE).toString()) : {}; this._contractsDir = opts.contractsDir || config.contractsDir || DEFAULT_CONTRACTS_DIR; + this._solcVersionIfExists = opts.solcVersion || config.solcVersion; this._compilerSettings = opts.compilerSettings || config.compilerSettings || DEFAULT_COMPILER_SETTINGS; this._artifactsDir = opts.artifactsDir || config.artifactsDir || DEFAULT_ARTIFACTS_DIR; this._specifiedContracts = opts.contracts || config.contracts || ALL_CONTRACTS_IDENTIFIER; @@ -139,9 +141,12 @@ export class Compiler { if (!shouldCompile) { return; } - const solcVersionRange = parseSolidityVersionRange(contractSource.source); - const availableCompilerVersions = _.keys(binPaths); - const solcVersion = semver.maxSatisfying(availableCompilerVersions, solcVersionRange); + let solcVersion = this._solcVersionIfExists; + if (_.isUndefined(solcVersion)) { + const solcVersionRange = parseSolidityVersionRange(contractSource.source); + const availableCompilerVersions = _.keys(binPaths); + solcVersion = semver.maxSatisfying(availableCompilerVersions, solcVersionRange); + } const fullSolcVersion = binPaths[solcVersion]; const compilerBinFilename = path.join(SOLC_BIN_DIR, fullSolcVersion); let solcjs: string; @@ -229,7 +234,7 @@ export class Compiler { sourceTreeHashHex, compiler: { name: 'solc', - version: solcVersion, + version: fullSolcVersion, settings: this._compilerSettings, }, }; diff --git a/packages/sol-compiler/src/index.ts b/packages/sol-compiler/src/index.ts index 4b4c51de2..15c166992 100644 --- a/packages/sol-compiler/src/index.ts +++ b/packages/sol-compiler/src/index.ts @@ -1,2 +1,3 @@ export { Compiler } from './compiler'; +export { CompilerOptions } from './utils/types'; export { ContractArtifact, ContractNetworks } from './utils/types'; diff --git a/packages/sol-compiler/src/utils/types.ts b/packages/sol-compiler/src/utils/types.ts index b12a11b79..d43347fa6 100644 --- a/packages/sol-compiler/src/utils/types.ts +++ b/packages/sol-compiler/src/utils/types.ts @@ -55,6 +55,7 @@ export interface CompilerOptions { artifactsDir?: string; compilerSettings?: solc.CompilerSettings; contracts?: string[] | '*'; + solcVersion?: string; } export interface ContractSourceData { diff --git a/packages/sol-compiler/test/util/provider.ts b/packages/sol-compiler/test/util/provider.ts index e0fcb362a..2bd178129 100644 --- a/packages/sol-compiler/test/util/provider.ts +++ b/packages/sol-compiler/test/util/provider.ts @@ -3,7 +3,6 @@ import { Provider } from '@0xproject/types'; import * as Web3 from 'web3'; const providerConfigs = { shouldUseInProcessGanache: true }; -const web3Instance = web3Factory.create(providerConfigs); -const provider: Provider = web3Instance.currentProvider; +const provider: Provider = web3Factory.getRpcProvider(providerConfigs); export { provider }; diff --git a/packages/sol-cov/CHANGELOG.json b/packages/sol-cov/CHANGELOG.json index 167d5077e..0d3303231 100644 --- a/packages/sol-cov/CHANGELOG.json +++ b/packages/sol-cov/CHANGELOG.json @@ -1,6 +1,28 @@ [ { "timestamp": 1527009134, + "version": "0.1.0", + "changes": [ + { + "note": "Add artifact adapter as a parameter for CoverageSubprovider. Export AbstractArtifactAdapter", + "pr": 589 + }, + { + "note": "Implement SolCompilerArtifactAdapter and TruffleArtifactAdapter", + "pr": 589 + }, + { + "note": "Properly parse multi-level traces", + "pr": 589 + }, + { + "note": "Add support for solidity libraries", + "pr": 589 + } + ] + }, + { + "timestamp": 1527009133, "version": "0.0.11", "changes": [ { diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json index f3c8ab67e..f18315a26 100644 --- a/packages/sol-cov/package.json +++ b/packages/sol-cov/package.json @@ -46,6 +46,7 @@ }, "homepage": "https://github.com/0xProject/0x.js/packages/sol-cov/README.md", "dependencies": { + "@0xproject/sol-compiler": "^0.5.0", "@0xproject/subproviders": "^0.10.2", "@0xproject/types": "^0.7.0", "@0xproject/typescript-typings": "^0.3.2", @@ -54,17 +55,21 @@ "glob": "^7.1.2", "istanbul": "^0.4.5", "lodash": "^4.17.4", + "loglevel": "^1.6.1", "mkdirp": "^0.5.1", + "rimraf": "^2.6.2", "semaphore-async-await": "^1.5.1", - "solidity-parser-antlr": "^0.2.8" + "solidity-parser-antlr": "^0.2.11" }, "devDependencies": { "@0xproject/monorepo-scripts": "^0.1.20", "@0xproject/tslint-config": "^0.4.18", "@types/istanbul": "^0.4.30", + "@types/loglevel": "^1.5.3", "@types/mkdirp": "^0.5.1", "@types/mocha": "^2.2.42", "@types/node": "^8.0.53", + "@types/rimraf": "^2.0.2", "chai": "^4.0.1", "copyfiles": "^1.2.0", "dirty-chai": "^2.0.1", diff --git a/packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts new file mode 100644 index 000000000..fcc6562ad --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts @@ -0,0 +1,5 @@ +import { ContractData } from '../types'; + +export abstract class AbstractArtifactAdapter { + public abstract async collectContractsDataAsync(): Promise<ContractData[]>; +} diff --git a/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts new file mode 100644 index 000000000..d08828bf6 --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import { ContractData } from '../types'; + +import { AbstractArtifactAdapter } from './abstract_artifact_adapter'; + +const CONFIG_FILE = 'compiler.json'; + +export class SolCompilerArtifactAdapter extends AbstractArtifactAdapter { + private _artifactsPath: string; + private _sourcesPath: string; + constructor(artifactsPath?: string, sourcesPath?: string) { + super(); + const config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString()); + if (_.isUndefined(artifactsPath) && _.isUndefined(config.artifactsDir)) { + throw new Error(`artifactsDir not found in ${CONFIG_FILE}`); + } + this._artifactsPath = config.artifactsDir; + if (_.isUndefined(sourcesPath) && _.isUndefined(config.contractsDir)) { + throw new Error(`contractsDir not found in ${CONFIG_FILE}`); + } + this._sourcesPath = config.contractsDir; + } + public async collectContractsDataAsync(): Promise<ContractData[]> { + const artifactsGlob = `${this._artifactsPath}/**/*.json`; + const artifactFileNames = glob.sync(artifactsGlob, { absolute: true }); + const contractsData: ContractData[] = []; + for (const artifactFileName of artifactFileNames) { + const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString()); + let sources = _.keys(artifact.sources); + sources = _.map(sources, relativeFilePath => path.resolve(this._sourcesPath, relativeFilePath)); + const contractName = artifact.contractName; + const sourceCodes = _.map(sources, (source: string) => fs.readFileSync(source).toString()); + const contractData = { + sourceCodes, + sources, + bytecode: artifact.compilerOutput.evm.bytecode.object, + sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap, + runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object, + sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap, + }; + contractsData.push(contractData); + } + return contractsData; + } +} diff --git a/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts new file mode 100644 index 000000000..c7f21b6eb --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts @@ -0,0 +1,43 @@ +import { Compiler, CompilerOptions } from '@0xproject/sol-compiler'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; + +import { ContractData } from '../types'; + +import { AbstractArtifactAdapter } from './abstract_artifact_adapter'; +import { SolCompilerArtifactAdapter } from './sol_compiler_artifact_adapter'; + +export class TruffleArtifactAdapter extends AbstractArtifactAdapter { + private _solcVersion: string; + private _sourcesPath: string; + constructor(sourcesPath: string, solcVersion: string) { + super(); + this._solcVersion = solcVersion; + this._sourcesPath = sourcesPath; + } + public async collectContractsDataAsync(): Promise<ContractData[]> { + const artifactsDir = '.0x-artifacts'; + const compilerOptions: CompilerOptions = { + contractsDir: this._sourcesPath, + artifactsDir, + compilerSettings: { + outputSelection: { + ['*']: { + ['*']: ['abi', 'evm.bytecode.object', 'evm.deployedBytecode.object'], + }, + }, + }, + contracts: '*', + solcVersion: this._solcVersion, + }; + const compiler = new Compiler(compilerOptions); + await compiler.compileAsync(); + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(artifactsDir, this._sourcesPath); + const contractsDataFrom0xArtifacts = await solCompilerArtifactAdapter.collectContractsDataAsync(); + rimraf.sync(artifactsDir); + return contractsDataFrom0xArtifacts; + } +} diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts deleted file mode 100644 index 2c2a12835..000000000 --- a/packages/sol-cov/src/collect_contract_data.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as fs from 'fs'; -import * as glob from 'glob'; -import * as _ from 'lodash'; -import * as path from 'path'; - -import { ContractData } from './types'; - -export const collectContractsData = (artifactsPath: string, sourcesPath: string) => { - const artifactsGlob = `${artifactsPath}/**/*.json`; - const artifactFileNames = glob.sync(artifactsGlob, { absolute: true }); - const contractsData: ContractData[] = []; - _.forEach(artifactFileNames, artifactFileName => { - const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString()); - const sources = _.keys(artifact.sources); - const contractName = artifact.contractName; - // We don't compute coverage for dependencies - const sourceCodes = _.map(sources, (source: string) => - fs.readFileSync(path.join(sourcesPath, source)).toString(), - ); - const contractData = { - sourceCodes, - sources, - bytecode: artifact.compilerOutput.evm.bytecode.object, - sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap, - runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object, - sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap, - }; - contractsData.push(contractData); - }); - return contractsData; -}; diff --git a/packages/sol-cov/src/constants.ts b/packages/sol-cov/src/constants.ts index 64d2228a3..34d62b537 100644 --- a/packages/sol-cov/src/constants.ts +++ b/packages/sol-cov/src/constants.ts @@ -1,6 +1,6 @@ // tslint:disable:number-literal-format export const constants = { - NEW_CONTRACT: 'NEW_CONTRACT', + NEW_CONTRACT: 'NEW_CONTRACT' as 'NEW_CONTRACT', PUSH1: 0x60, PUSH2: 0x61, PUSH32: 0x7f, diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 800ca96dd..31b0e6fbc 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -1,12 +1,13 @@ import { promisify } from '@0xproject/utils'; -import { addHexPrefix } from 'ethereumjs-util'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; import * as fs from 'fs'; import { Collector } from 'istanbul'; import * as _ from 'lodash'; +import { getLogger, levels, Logger, LogLevel } from 'loglevel'; import * as mkdirp from 'mkdirp'; import * as path from 'path'; -import { collectContractsData } from './collect_contract_data'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { collectCoverageEntries } from './collect_coverage_entries'; import { constants } from './constants'; import { parseSourceMap } from './source_maps'; @@ -34,41 +35,23 @@ import { utils } from './utils'; const mkdirpAsync = promisify<undefined>(mkdirp); export class CoverageManager { - private _sourcesPath: string; + private _artifactAdapter: AbstractArtifactAdapter; + private _logger: Logger; private _traceInfos: TraceInfo[] = []; - private _contractsData: ContractData[] = []; private _getContractCodeAsync: (address: string) => Promise<string>; - constructor( - artifactsPath: string, - sourcesPath: string, - getContractCodeAsync: (address: string) => Promise<string>, - ) { - this._getContractCodeAsync = getContractCodeAsync; - this._sourcesPath = sourcesPath; - this._contractsData = collectContractsData(artifactsPath, this._sourcesPath); - } - public appendTraceInfo(traceInfo: TraceInfo): void { - this._traceInfos.push(traceInfo); - } - public async writeCoverageAsync(): Promise<void> { - const finalCoverage = await this._computeCoverageAsync(); - const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); - await mkdirpAsync('coverage'); - fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); - } - private _getSingleFileCoverageForTrace( + private static _getSingleFileCoverageForTrace( contractData: ContractData, coveredPcs: number[], pcToSourceRange: { [programCounter: number]: SourceRange }, fileIndex: number, ): Coverage { - const fileName = contractData.sources[fileIndex]; + const absoluteFileName = contractData.sources[fileIndex]; const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]); sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them. // By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings. sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction - sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === fileName); + sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName); const branchCoverage: BranchCoverage = {}; const branchIds = _.keys(coverageEntriesDescription.branchMap); for (const branchId of branchIds) { @@ -118,7 +101,6 @@ export class CoverageManager { ); statementCoverage[modifierStatementId] = isModifierCovered; } - const absoluteFileName = path.join(this._sourcesPath, fileName); const partialCoverage = { [absoluteFileName]: { ...coverageEntriesDescription, @@ -131,18 +113,63 @@ export class CoverageManager { }; return partialCoverage; } + private static _bytecodeToBytecodeRegex(bytecode: string): string { + const bytecodeRegex = bytecode + // Library linking placeholder: __ConvertLib____________________________ + .replace(/_.*_/, '.*') + // Last 86 characters is solidity compiler metadata that's different between compilations + .replace(/.{86}$/, '') + // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance + .replace(/^0x730000000000000000000000000000000000000000/, '0x73........................................'); + return bytecodeRegex; + } + private static _getContractDataIfExists(contractsData: ContractData[], bytecode: string): ContractData | undefined { + if (!bytecode.startsWith('0x')) { + throw new Error(`0x hex prefix missing: ${bytecode}`); + } + const contractData = _.find(contractsData, contractDataCandidate => { + const bytecodeRegex = CoverageManager._bytecodeToBytecodeRegex(contractDataCandidate.bytecode); + const runtimeBytecodeRegex = CoverageManager._bytecodeToBytecodeRegex( + contractDataCandidate.runtimeBytecode, + ); + // We use that function to find by bytecode or runtimeBytecode. Those are quasi-random strings so + // collisions are practically impossible and it allows us to reuse that code + return !_.isNull(bytecode.match(bytecodeRegex)) || !_.isNull(bytecode.match(runtimeBytecodeRegex)); + }); + return contractData; + } + constructor( + artifactAdapter: AbstractArtifactAdapter, + getContractCodeAsync: (address: string) => Promise<string>, + isVerbose: boolean, + ) { + this._getContractCodeAsync = getContractCodeAsync; + this._artifactAdapter = artifactAdapter; + this._logger = getLogger('sol-cov'); + this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); + } + public appendTraceInfo(traceInfo: TraceInfo): void { + this._traceInfos.push(traceInfo); + } + public async writeCoverageAsync(): Promise<void> { + const finalCoverage = await this._computeCoverageAsync(); + const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); + await mkdirpAsync('coverage'); + fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); + } private async _computeCoverageAsync(): Promise<Coverage> { + const contractsData = await this._artifactAdapter.collectContractsDataAsync(); const collector = new Collector(); for (const traceInfo of this._traceInfos) { if (traceInfo.address !== constants.NEW_CONTRACT) { // Runtime transaction - let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; - runtimeBytecode = addHexPrefix(runtimeBytecode); - const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; + const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; + const contractData = CoverageManager._getContractDataIfExists(contractsData, runtimeBytecode); if (_.isUndefined(contractData)) { - throw new Error(`Transaction to an unknown address: ${traceInfo.address}`); + this._logger.warn(`Transaction to an unknown address: ${traceInfo.address}`); + continue; } - const bytecodeHex = contractData.runtimeBytecode.slice(2); + const bytecodeHex = stripHexPrefix(runtimeBytecode); const sourceMap = contractData.sourceMapRuntime; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -151,7 +178,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, @@ -161,15 +188,13 @@ export class CoverageManager { } } else { // Contract creation transaction - let bytecode = (traceInfo as TraceInfoNewContract).bytecode; - bytecode = addHexPrefix(bytecode); - const contractData = _.find(this._contractsData, contractDataCandidate => - bytecode.startsWith(contractDataCandidate.bytecode), - ) as ContractData; + const bytecode = (traceInfo as TraceInfoNewContract).bytecode; + const contractData = CoverageManager._getContractDataIfExists(contractsData, bytecode); if (_.isUndefined(contractData)) { - throw new Error(`Unknown contract creation transaction`); + this._logger.warn(`Unknown contract creation transaction`); + continue; } - const bytecodeHex = contractData.bytecode.slice(2); + const bytecodeHex = stripHexPrefix(bytecode); const sourceMap = contractData.sourceMap; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -178,7 +203,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index 08efeaa24..438339a3f 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -1,11 +1,14 @@ import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders'; import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from '@0xproject/types'; +import * as fs from 'fs'; import * as _ from 'lodash'; import { Lock } from 'semaphore-async-await'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { constants } from './constants'; import { CoverageManager } from './coverage_manager'; -import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; +import { getTracesByContractAddress } from './trace'; +import { BlockParamLiteral, TraceInfoExistingContract, TraceInfoNewContract } from './types'; interface MaybeFakeTxData extends TxData { isFakeTransaction?: boolean; @@ -26,15 +29,15 @@ export class CoverageSubprovider extends Subprovider { private _defaultFromAddress: string; /** * Instantiates a CoverageSubprovider instance - * @param artifactsPath Path to the smart contract artifacts - * @param sourcesPath Path to the smart contract source files + * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.) * @param defaultFromAddress default from address to use when sending transactions + * @param isVerbose If true, we will log any unknown transactions. Otherwise we will ignore them */ - constructor(artifactsPath: string, sourcesPath: string, defaultFromAddress: string) { + constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, isVerbose: boolean = true) { super(); this._lock = new Lock(); this._defaultFromAddress = defaultFromAddress; - this._coverageManager = new CoverageManager(artifactsPath, sourcesPath, this._getContractCodeAsync.bind(this)); + this._coverageManager = new CoverageManager(artifactAdapter, this._getContractCodeAsync.bind(this), isVerbose); } /** * Write the test coverage results to a file in Istanbul format. @@ -86,7 +89,7 @@ export class CoverageSubprovider extends Subprovider { } else { const payload = { method: 'eth_getBlockByNumber', - params: ['latest', true], + params: [BlockParamLiteral.Latest, true], }; const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const transactions = jsonRPCResponsePayload.result.transactions; @@ -115,29 +118,54 @@ export class CoverageSubprovider extends Subprovider { private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { let payload = { method: 'debug_traceTransaction', - params: [txHash, { disableMemory: true, disableStack: true, disableStorage: true }], // TODO For now testrpc just ignores those parameters https://github.com/trufflesuite/ganache-cli/issues/489 + params: [txHash, { disableMemory: true, disableStack: false, disableStorage: true }], }; - const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + let jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const trace: TransactionTrace = jsonRPCResponsePayload.result; - const coveredPcs = _.map(trace.structLogs, log => log.pc); + const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address); + const subcallAddresses = _.keys(tracesByContractAddress); if (address === constants.NEW_CONTRACT) { - const traceInfo: TraceInfoNewContract = { - coveredPcs, - txHash, - address: address as 'NEW_CONTRACT', - bytecode: data as string, - }; - this._coverageManager.appendTraceInfo(traceInfo); + for (const subcallAddress of subcallAddresses) { + let traceInfo: TraceInfoNewContract | TraceInfoExistingContract; + if (subcallAddress === 'NEW_CONTRACT') { + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + traceInfo = { + coveredPcs, + txHash, + address: constants.NEW_CONTRACT, + bytecode: data as string, + }; + } else { + payload = { method: 'eth_getCode', params: [subcallAddress, BlockParamLiteral.Latest] }; + jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + const runtimeBytecode = jsonRPCResponsePayload.result; + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + traceInfo = { + coveredPcs, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + } + this._coverageManager.appendTraceInfo(traceInfo); + } } else { - payload = { method: 'eth_getCode', params: [address, 'latest'] }; - const runtimeBytecode = (await this.emitPayloadAsync(payload)).result; - const traceInfo: TraceInfoExistingContract = { - coveredPcs, - txHash, - address, - runtimeBytecode, - }; - this._coverageManager.appendTraceInfo(traceInfo); + for (const subcallAddress of subcallAddresses) { + payload = { method: 'eth_getCode', params: [subcallAddress, BlockParamLiteral.Latest] }; + jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + const runtimeBytecode = jsonRPCResponsePayload.result; + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + const traceInfo: TraceInfoExistingContract = { + coveredPcs, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } } } private async _recordCallTraceAsync(callData: Partial<CallData>, blockNumber: BlockParam): Promise<void> { @@ -168,7 +196,7 @@ export class CoverageSubprovider extends Subprovider { private async _getContractCodeAsync(address: string): Promise<string> { const payload = { method: 'eth_getCode', - params: [address, 'latest'], + params: [address, BlockParamLiteral.Latest], }; const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const contractCode: string = jsonRPCResponsePayload.result; diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts index e5c5e5be3..7a2afbe80 100644 --- a/packages/sol-cov/src/index.ts +++ b/packages/sol-cov/src/index.ts @@ -1 +1,5 @@ export { CoverageSubprovider } from './coverage_subprovider'; +export { SolCompilerArtifactAdapter } from './artifact_adapters/sol_compiler_artifact_adapter'; +export { TruffleArtifactAdapter } from './artifact_adapters/truffle_artifact_adapter'; +export { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; +export { ContractData } from './types'; diff --git a/packages/sol-cov/src/trace.ts b/packages/sol-cov/src/trace.ts new file mode 100644 index 000000000..6caea1610 --- /dev/null +++ b/packages/sol-cov/src/trace.ts @@ -0,0 +1,105 @@ +import { OpCode, StructLog, TransactionTrace } from '@0xproject/types'; +import { addressUtils, BigNumber, logUtils } from '@0xproject/utils'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; +import * as fs from 'fs'; +import * as _ from 'lodash'; + +export interface TraceByContractAddress { + [contractAddress: string]: StructLog[]; +} + +function getAddressFromStackEntry(stackEntry: string): string { + const hexBase = 16; + return addressUtils.padZeros(new BigNumber(addHexPrefix(stackEntry)).toString(hexBase)); +} + +export function getTracesByContractAddress(structLogs: StructLog[], startAddress: string): TraceByContractAddress { + const traceByContractAddress: TraceByContractAddress = {}; + let currentTraceSegment = []; + const callStack = [startAddress]; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < structLogs.length; i++) { + const structLog = structLogs[i]; + if (structLog.depth !== callStack.length - 1) { + throw new Error("Malformed trace. Trace depth doesn't match call stack depth"); + } + // After that check we have a guarantee that call stack is never empty + // If it would: callStack.length - 1 === structLog.depth === -1 + // That means that we can always safely pop from it + currentTraceSegment.push(structLog); + + const isCallLike = _.includes( + [OpCode.CallCode, OpCode.StaticCall, OpCode.Call, OpCode.DelegateCall], + structLog.op, + ); + const isEndOpcode = _.includes( + [OpCode.Return, OpCode.Stop, OpCode.Revert, OpCode.Invalid, OpCode.SelfDestruct], + structLog.op, + ); + if (isCallLike) { + const currentAddress = _.last(callStack) as string; + const jumpAddressOffset = 1; + const newAddress = getAddressFromStackEntry( + structLog.stack[structLog.stack.length - jumpAddressOffset - 1], + ); + if (structLog === _.last(structLogs)) { + throw new Error('Malformed trace. CALL-like opcode can not be the last one'); + } + // Sometimes calls don't change the execution context (current address). When we do a transfer to an + // externally owned account - it does the call and immediately returns because there is no fallback + // function. We manually check if the call depth had changed to handle that case. + const nextStructLog = structLogs[i + 1]; + if (nextStructLog.depth !== structLog.depth) { + callStack.push(newAddress); + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } + } else if (isEndOpcode) { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + if (structLog.op === OpCode.SelfDestruct) { + // After contract execution, we look at all sub-calls to external contracts, and for each one, fetch + // the bytecode and compute the coverage for the call. If the contract is destroyed with a call + // to `selfdestruct`, we are unable to fetch it's bytecode and compute coverage. + // TODO: Refactor this logic to fetch the sub-called contract bytecode before the selfdestruct is called + // in order to handle this edge-case. + logUtils.warn( + "Detected a selfdestruct. Sol-cov currently doesn't support that scenario. We'll just skip the trace part for a destructed contract", + ); + } + } else if (structLog.op === OpCode.Create) { + // TODO: Extract the new contract address from the stack and handle that scenario + logUtils.warn( + "Detected a contract created from within another contract. Sol-cov currently doesn't support that scenario. We'll just skip that trace", + ); + return traceByContractAddress; + } else { + if (structLog !== _.last(structLogs)) { + const nextStructLog = structLogs[i + 1]; + if (nextStructLog.depth === structLog.depth) { + continue; + } else if (nextStructLog.depth === structLog.depth - 1) { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } else { + throw new Error('Malformed trace. Unexpected call depth change'); + } + } + } + } + if (callStack.length !== 0) { + throw new Error('Malformed trace. Call stack non empty at the end'); + } + if (currentTraceSegment.length !== 0) { + throw new Error('Malformed trace. Current trace segment non empty at the end'); + } + return traceByContractAddress; +} diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts index 01359d858..4c3de55a1 100644 --- a/packages/sol-cov/src/types.ts +++ b/packages/sol-cov/src/types.ts @@ -98,3 +98,7 @@ export interface TraceInfoExistingContract extends TraceInfoBase { } export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract; + +export enum BlockParamLiteral { + Latest = 'latest', +} diff --git a/packages/sol-cov/test/collect_contracts_data_test.ts b/packages/sol-cov/test/sol_compiler_artifact_adapter_test.ts index d84ac5a39..0ebad669b 100644 --- a/packages/sol-cov/test/collect_contracts_data_test.ts +++ b/packages/sol-cov/test/sol_compiler_artifact_adapter_test.ts @@ -4,16 +4,17 @@ import 'make-promises-safe'; import 'mocha'; import * as path from 'path'; -import { collectContractsData } from '../src/collect_contract_data'; +import { SolCompilerArtifactAdapter } from '../src/artifact_adapters/sol_compiler_artifact_adapter'; const expect = chai.expect; -describe('Collect contracts data', () => { +describe('SolCompilerArtifactAdapter', () => { describe('#collectContractsData', () => { - it('correctly collects contracts data', () => { + it('correctly collects contracts data', async () => { const artifactsPath = path.resolve(__dirname, 'fixtures/artifacts'); const sourcesPath = path.resolve(__dirname, 'fixtures/contracts'); - const contractsData = collectContractsData(artifactsPath, sourcesPath); + const zeroExArtifactsAdapter = new SolCompilerArtifactAdapter(artifactsPath, sourcesPath); + const contractsData = await zeroExArtifactsAdapter.collectContractsDataAsync(); _.forEach(contractsData, contractData => { expect(contractData).to.have.keys([ 'sourceCodes', diff --git a/packages/sol-cov/test/trace_test.ts b/packages/sol-cov/test/trace_test.ts new file mode 100644 index 000000000..c140cba0d --- /dev/null +++ b/packages/sol-cov/test/trace_test.ts @@ -0,0 +1,57 @@ +import { OpCode, StructLog } from '@0xproject/types'; +import * as chai from 'chai'; +import * as fs from 'fs'; +import * as _ from 'lodash'; +import 'mocha'; +import * as path from 'path'; + +import { getTracesByContractAddress } from '../src/trace'; + +const expect = chai.expect; + +const DEFAULT_STRUCT_LOG: StructLog = { + depth: 0, + error: '', + gas: 0, + gasCost: 0, + memory: [], + op: OpCode.Invalid, + pc: 0, + stack: [], + storage: {}, +}; + +function addDefaultStructLogFields(compactStructLog: Partial<StructLog> & { op: OpCode; depth: number }): StructLog { + return { ...DEFAULT_STRUCT_LOG, ...compactStructLog }; +} + +describe('Trace', () => { + describe('#getTracesByContractAddress', () => { + it('correctly splits trace by contract address', () => { + const delegateCallAddress = '0x0000000000000000000000000000000000000002'; + const trace = [ + { + op: OpCode.DelegateCall, + stack: [delegateCallAddress, '0x'], + depth: 0, + }, + { + op: OpCode.Return, + depth: 1, + }, + { + op: OpCode.Return, + depth: 0, + }, + ]; + const fullTrace = _.map(trace, compactStructLog => addDefaultStructLogFields(compactStructLog)); + const startAddress = '0x0000000000000000000000000000000000000001'; + const traceByContractAddress = getTracesByContractAddress(fullTrace, startAddress); + const expectedTraceByContractAddress = { + [startAddress]: [fullTrace[0], fullTrace[2]], + [delegateCallAddress]: [fullTrace[1]], + }; + expect(traceByContractAddress).to.be.deep.equal(expectedTraceByContractAddress); + }); + }); +}); diff --git a/packages/sol-resolver/CHANGELOG.json b/packages/sol-resolver/CHANGELOG.json index 44de3825e..895bd2104 100644 --- a/packages/sol-resolver/CHANGELOG.json +++ b/packages/sol-resolver/CHANGELOG.json @@ -1,5 +1,18 @@ [ { + "version": "0.0.5", + "changes": [ + { + "note": "Fix a bug in FsResolver where it tries to read directories as files", + "pr": 589 + }, + { + "note": "Fix a bug in NameResolver where it is not ignoring .sol files", + "pr": 589 + } + ] + }, + { "timestamp": 1527009133, "version": "0.0.5", "changes": [ diff --git a/packages/sol-resolver/src/resolvers/fs_resolver.ts b/packages/sol-resolver/src/resolvers/fs_resolver.ts index 4f05fba88..63fc3448e 100644 --- a/packages/sol-resolver/src/resolvers/fs_resolver.ts +++ b/packages/sol-resolver/src/resolvers/fs_resolver.ts @@ -7,7 +7,7 @@ import { Resolver } from './resolver'; export class FSResolver extends Resolver { // tslint:disable-next-line:prefer-function-over-method public resolveIfExists(importPath: string): ContractSource | undefined { - if (fs.existsSync(importPath)) { + if (fs.existsSync(importPath) && fs.lstatSync(importPath).isFile()) { const fileContent = fs.readFileSync(importPath).toString(); return { source: fileContent, diff --git a/packages/sol-resolver/src/resolvers/name_resolver.ts b/packages/sol-resolver/src/resolvers/name_resolver.ts index 76bed802e..e489c70a7 100644 --- a/packages/sol-resolver/src/resolvers/name_resolver.ts +++ b/packages/sol-resolver/src/resolvers/name_resolver.ts @@ -6,6 +6,8 @@ import { ContractSource } from '../types'; import { EnumerableResolver } from './enumerable_resolver'; +const SOLIDITY_FILE_EXTENSION = '.sol'; + export class NameResolver extends EnumerableResolver { private _contractsDir: string; constructor(contractsDir: string) { @@ -13,7 +15,6 @@ export class NameResolver extends EnumerableResolver { this._contractsDir = contractsDir; } public resolveIfExists(lookupContractName: string): ContractSource | undefined { - const SOLIDITY_FILE_EXTENSION = '.sol'; let contractSource: ContractSource | undefined; const onFile = (filePath: string) => { const contractName = path.basename(filePath, SOLIDITY_FILE_EXTENSION); @@ -32,7 +33,6 @@ export class NameResolver extends EnumerableResolver { return contractSource; } public getAll(): ContractSource[] { - const SOLIDITY_FILE_EXTENSION = '.sol'; const contractSources: ContractSource[] = []; const onFile = (filePath: string) => { const contractName = path.basename(filePath, SOLIDITY_FILE_EXTENSION); @@ -59,7 +59,12 @@ export class NameResolver extends EnumerableResolver { const absoluteEntryPath = path.join(dirPath, fileName); const isDirectory = fs.lstatSync(absoluteEntryPath).isDirectory(); const entryPath = path.relative(this._contractsDir, absoluteEntryPath); - const isComplete = isDirectory ? this._traverseContractsDir(absoluteEntryPath, onFile) : onFile(entryPath); + let isComplete; + if (isDirectory) { + isComplete = this._traverseContractsDir(absoluteEntryPath, onFile); + } else if (fileName.endsWith(SOLIDITY_FILE_EXTENSION)) { + isComplete = onFile(entryPath); + } if (isComplete) { return isComplete; } diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index ff28b8a8d..6cc650a4d 100644 --- a/packages/subproviders/src/index.ts +++ b/packages/subproviders/src/index.ts @@ -4,6 +4,7 @@ export { ECSignature } from '@0xproject/types'; import { LedgerEthereumClient } from './types'; +export { prependSubprovider } from './utils/subprovider_utils'; export { EmptyWalletSubprovider } from './subproviders/empty_wallet_subprovider'; export { FakeGasEstimateSubprovider } from './subproviders/fake_gas_estimate_subprovider'; export { InjectedWeb3Subprovider } from './subproviders/injected_web3'; diff --git a/packages/subproviders/src/utils/subprovider_utils.ts b/packages/subproviders/src/utils/subprovider_utils.ts new file mode 100644 index 000000000..24ebedd06 --- /dev/null +++ b/packages/subproviders/src/utils/subprovider_utils.ts @@ -0,0 +1,15 @@ +import ProviderEngine = require('web3-provider-engine'); + +import { Subprovider } from '../subproviders/subprovider'; + +/** + * Prepends a subprovider to a provider + * @param provider Given provider + * @param subprovider Subprovider to prepend + */ +export function prependSubprovider(provider: ProviderEngine, subprovider: Subprovider): void { + subprovider.setEngine(provider); + // HACK: We use implementation details of provider engine here + // https://github.com/MetaMask/provider-engine/blob/master/index.js#L68 + (provider as any)._providers = [subprovider, ...(provider as any)._providers]; +} diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 23325fedc..8abc5bd99 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -3,6 +3,10 @@ "version": "0.7.0", "changes": [ { + "note": "Make OpCode type an enum", + "pr": 589 + }, + { "note": "Moved ExchangeContractErrs, DoneCallback, Token, OrderRelevantState, OrderStateValid, OrderStateInvalid, OrderState, OrderAddresses and OrderValues types from 0x.js", "pr": 579 diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f28c2f9a3..055c47e0a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -58,7 +58,18 @@ export interface DataItem { components?: DataItem[]; } -export type OpCode = string; +export enum OpCode { + DelegateCall = 'DELEGATECALL', + Revert = 'REVERT', + Create = 'CREATE', + Stop = 'STOP', + Invalid = 'INVALID', + CallCode = 'CALLCODE', + StaticCall = 'STATICCALL', + Return = 'RETURN', + Call = 'CALL', + SelfDestruct = 'SELFDESTRUCT', +} export interface StructLog { depth: number; diff --git a/packages/typescript-typings/types/web3-provider-engine/index.d.ts b/packages/typescript-typings/types/web3-provider-engine/index.d.ts index 15a8d0005..8d5aef749 100644 --- a/packages/typescript-typings/types/web3-provider-engine/index.d.ts +++ b/packages/typescript-typings/types/web3-provider-engine/index.d.ts @@ -1,8 +1,12 @@ declare module 'web3-provider-engine' { - class Web3ProviderEngine { + import { Provider, JSONRPCRequestPayload, JSONRPCResponsePayload } from '@0xproject/types'; + class Web3ProviderEngine implements Provider { public on(event: string, handler: () => void): void; - public send(payload: any): void; - public sendAsync(payload: any, callback: (error: any, response: any) => void): void; + public send(payload: JSONRPCRequestPayload): void; + public sendAsync( + payload: JSONRPCRequestPayload, + callback: (error: null | Error, response: JSONRPCResponsePayload) => void, + ): void; public addProvider(provider: any): void; public start(): void; public stop(): void; @@ -19,13 +23,13 @@ declare module 'web3-provider-engine/subproviders/subprovider' { export = Subprovider; } declare module 'web3-provider-engine/subproviders/rpc' { - import { JSONRPCRequestPayload } from '@0xproject/types'; + import { JSONRPCRequestPayload, JSONRPCResponsePayload } from '@0xproject/types'; class RpcSubprovider { constructor(options: { rpcUrl: string }); public handleRequest( payload: JSONRPCRequestPayload, next: () => void, - end: (err: Error | null, data?: any) => void, + end: (err: Error | null, data?: JSONRPCResponsePayload) => void, ): void; } export = RpcSubprovider; @@ -37,13 +41,13 @@ declare module 'web3-provider-engine/util/rpc-cache-utils' { export = ProviderEngineRpcUtils; } declare module 'web3-provider-engine/subproviders/fixture' { - import { JSONRPCRequestPayload } from '@0xproject/types'; + import { JSONRPCRequestPayload, JSONRPCResponsePayload } from '@0xproject/types'; class FixtureSubprovider { constructor(staticResponses: any); public handleRequest( payload: JSONRPCRequestPayload, next: () => void, - end: (err: Error | null, data?: any) => void, + end: (err: Error | null, data?: JSONRPCResponsePayload) => void, ): void; } export = FixtureSubprovider; diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 853acae55..616a17d62 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -9,6 +9,15 @@ ] }, { + "version": "0.7.0", + "changes": [ + { + "note": "Add logUtils.warn", + "pr": 589 + } + ] + }, + { "timestamp": 1525477860, "version": "0.6.1", "changes": [ diff --git a/packages/utils/package.json b/packages/utils/package.json index 99d3d9256..24551dd93 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -38,6 +38,7 @@ "@0xproject/types": "^0.7.0", "@0xproject/typescript-typings": "^0.3.2", "@types/node": "^8.0.53", + "ethereumjs-util": "^5.1.1", "bignumber.js": "~4.1.0", "ethers": "^3.0.15", "js-sha3": "^0.7.0", diff --git a/packages/utils/src/abi_decoder.ts b/packages/utils/src/abi_decoder.ts index c78bfa343..d2d8364ca 100644 --- a/packages/utils/src/abi_decoder.ts +++ b/packages/utils/src/abi_decoder.ts @@ -12,21 +12,12 @@ import { import * as ethers from 'ethers'; import * as _ from 'lodash'; +import { addressUtils } from './address_utils'; import { BigNumber } from './configured_bignumber'; export class AbiDecoder { private _savedABIs: AbiDefinition[] = []; private _methodIds: { [signatureHash: string]: EventAbi } = {}; - private static _padZeros(address: string): string { - let formatted = address; - if (_.startsWith(formatted, '0x')) { - formatted = formatted.slice(2); - } - - const addressLength = 40; - formatted = _.padStart(formatted, addressLength, '0'); - return `0x${formatted}`; - } constructor(abiArrays: AbiDefinition[][]) { _.forEach(abiArrays, this.addABI.bind(this)); } @@ -56,7 +47,7 @@ export class AbiDecoder { } if (param.type === SolidityTypes.Address) { const baseHex = 16; - value = AbiDecoder._padZeros(new BigNumber(value).toString(baseHex)); + value = addressUtils.padZeros(new BigNumber(value).toString(baseHex)); } else if (param.type === SolidityTypes.Uint256 || param.type === SolidityTypes.Uint) { value = new BigNumber(value); } else if (param.type === SolidityTypes.Uint8) { diff --git a/packages/utils/src/address_utils.ts b/packages/utils/src/address_utils.ts index e25bde249..1fc960408 100644 --- a/packages/utils/src/address_utils.ts +++ b/packages/utils/src/address_utils.ts @@ -1,7 +1,10 @@ +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; import * as jsSHA3 from 'js-sha3'; +import * as _ from 'lodash'; const BASIC_ADDRESS_REGEX = /^(0x)?[0-9a-f]{40}$/i; const SAME_CASE_ADDRESS_REGEX = /^(0x)?([0-9a-f]{40}|[0-9A-F]{40})$/; +const ADDRESS_LENGTH = 40; export const addressUtils = { isChecksumAddress(address: string): boolean { @@ -9,8 +12,7 @@ export const addressUtils = { const unprefixedAddress = address.replace('0x', ''); const addressHash = jsSHA3.keccak256(unprefixedAddress.toLowerCase()); - const addressLength = 40; - for (let i = 0; i < addressLength; i++) { + for (let i = 0; i < ADDRESS_LENGTH; i++) { // The nth letter should be uppercase if the nth digit of casemap is 1 const hexBase = 16; const lowercaseRange = 7; @@ -38,4 +40,7 @@ export const addressUtils = { return isValidChecksummedAddress; } }, + padZeros(address: string): string { + return addHexPrefix(_.padStart(stripHexPrefix(address), ADDRESS_LENGTH, '0')); + }, }; diff --git a/packages/utils/src/log_utils.ts b/packages/utils/src/log_utils.ts index d0f0e34c9..87f8479b5 100644 --- a/packages/utils/src/log_utils.ts +++ b/packages/utils/src/log_utils.ts @@ -2,4 +2,7 @@ export const logUtils = { log(...args: any[]): void { console.log(...args); // tslint:disable-line:no-console }, + warn(...args: any[]): void { + console.warn(...args); // tslint:disable-line:no-console + }, }; @@ -184,6 +184,10 @@ version "4.14.99" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.99.tgz#e6e10c0a4cc16c7409b3181f1e66880d2fb7d4dc" +"@types/loglevel@^1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8" + "@types/marked@0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" @@ -7081,7 +7085,7 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -loglevel@^1.4.1: +loglevel@^1.4.1, loglevel@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" @@ -10503,9 +10507,9 @@ solc@^0.4.24: semver "^5.3.0" yargs "^4.7.1" -solidity-parser-antlr@^0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/solidity-parser-antlr/-/solidity-parser-antlr-0.2.8.tgz#8eb8547a88dfeaf6cf4c7811e3824084214244d4" +solidity-parser-antlr@^0.2.11: + version "0.2.11" + resolved "https://registry.yarnpkg.com/solidity-parser-antlr/-/solidity-parser-antlr-0.2.11.tgz#b3e4d0aca0d15796e9b59da53a49137aab51c298" sort-keys@^1.0.0: version "1.1.2" |