Add sol-cover implementation

This commit is contained in:
Leonid Logvinov
2018-03-04 19:05:26 -08:00
parent a6571b09d2
commit 13299158d1
110 changed files with 3704 additions and 1570 deletions

View File

@@ -11,14 +11,14 @@ import { chaiSetup } from './utils/chai_setup';
import { constants } from './utils/constants';
import { TokenUtils } from './utils/token_utils';
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
chaiSetup.configure();
const expect = chai.expect;
const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false;
describe('ZeroEx library', () => {
const web3 = web3Factory.create();
const config = {
networkId: constants.TESTRPC_NETWORK_ID,
};

View File

@@ -26,7 +26,8 @@ import { TokenUtils } from './utils/token_utils';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
// Since the address depositing/withdrawing ETH/WETH also needs to pay gas costs for the transaction,
// a small amount of ETH will be used to pay this gas cost. We therefore check that the difference between
@@ -35,7 +36,6 @@ const blockchainLifecycle = new BlockchainLifecycle();
const MAX_REASONABLE_GAS_COST_IN_WEI = 62517;
describe('EtherTokenWrapper', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let tokens: Token[];
let userAddresses: string[];
@@ -54,7 +54,6 @@ describe('EtherTokenWrapper', () => {
const depositAmount = new BigNumber(42);
const withdrawalAmount = new BigNumber(42);
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, zeroExConfig);
tokens = await zeroEx.tokenRegistry.getTokensAsync();
userAddresses = await zeroEx.getAvailableAddressesAsync();

View File

@@ -12,10 +12,10 @@ import { constants } from './utils/constants';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
describe('ExchangeTransferSimulator', () => {
const web3 = web3Factory.create();
const config = {
networkId: constants.TESTRPC_NETWORK_ID,
};

View File

@@ -29,12 +29,12 @@ import { TokenUtils } from './utils/token_utils';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777';
describe('ExchangeWrapper', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let tokenUtils: TokenUtils;
let tokens: Token[];
@@ -46,7 +46,6 @@ describe('ExchangeWrapper', () => {
networkId: constants.TESTRPC_NETWORK_ID,
};
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
exchangeContractAddress = zeroEx.exchange.getContractAddress();
userAddresses = await zeroEx.getAvailableAddressesAsync();

View File

@@ -19,10 +19,10 @@ import { TokenUtils } from './utils/token_utils';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
describe('ExpirationWatcher', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let tokenUtils: TokenUtils;
let tokens: Token[];
@@ -41,7 +41,6 @@ describe('ExpirationWatcher', () => {
let timer: Sinon.SinonFakeTimers;
let expirationWatcher: ExpirationWatcher;
before(async () => {
web3 = web3Factory.create();
const config = {
networkId: constants.TESTRPC_NETWORK_ID,
};

View File

@@ -27,10 +27,10 @@ const TIMEOUT_MS = 150;
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
describe('OrderStateWatcher', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let tokens: Token[];
let tokenUtils: TokenUtils;
@@ -49,7 +49,6 @@ describe('OrderStateWatcher', () => {
const decimals = constants.ZRX_DECIMALS;
const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals);
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
exchangeContractAddress = zeroEx.exchange.getContractAddress();
userAddresses = await zeroEx.getAvailableAddressesAsync();

View File

@@ -17,10 +17,10 @@ import { TokenUtils } from './utils/token_utils';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
describe('OrderValidation', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let userAddresses: string[];
let tokens: Token[];
@@ -40,7 +40,6 @@ describe('OrderValidation', () => {
networkId: constants.TESTRPC_NETWORK_ID,
};
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
exchangeContractAddress = zeroEx.exchange.getContractAddress();
userAddresses = await zeroEx.getAvailableAddressesAsync();

View File

@@ -13,10 +13,10 @@ import { constants } from './utils/constants';
import { assertNodeCallbackError } from './utils/report_callback_errors';
chaiSetup.configure();
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
describe('SubscriptionTest', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let userAddresses: string[];
let tokens: Token[];
@@ -26,7 +26,6 @@ describe('SubscriptionTest', () => {
networkId: constants.TESTRPC_NETWORK_ID,
};
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
userAddresses = await zeroEx.getAvailableAddressesAsync();
tokens = await zeroEx.tokenRegistry.getTokensAsync();

View File

@@ -11,7 +11,8 @@ import { constants } from './utils/constants';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
const TOKEN_REGISTRY_SIZE_AFTER_MIGRATION = 7;
@@ -30,7 +31,6 @@ describe('TokenRegistryWrapper', () => {
networkId: constants.TESTRPC_NETWORK_ID,
};
before(async () => {
const web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
tokens = await zeroEx.tokenRegistry.getTokensAsync();
_.map(tokens, token => {

View File

@@ -8,6 +8,7 @@ import { constants } from './utils/constants';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
describe('TokenTransferProxyWrapper', () => {
let zeroEx: ZeroEx;
@@ -15,7 +16,6 @@ describe('TokenTransferProxyWrapper', () => {
networkId: constants.TESTRPC_NETWORK_ID,
};
before(async () => {
const web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
});
describe('#isAuthorizedAsync', () => {

View File

@@ -25,10 +25,10 @@ import { TokenUtils } from './utils/token_utils';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const web3 = web3Factory.create();
const blockchainLifecycle = new BlockchainLifecycle(web3);
describe('TokenWrapper', () => {
let web3: Web3;
let zeroEx: ZeroEx;
let userAddresses: string[];
let tokens: Token[];
@@ -40,7 +40,6 @@ describe('TokenWrapper', () => {
networkId: constants.TESTRPC_NETWORK_ID,
};
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider, config);
web3Wrapper = new Web3Wrapper(web3.currentProvider);
userAddresses = await zeroEx.getAvailableAddressesAsync();
@@ -194,7 +193,7 @@ describe('TokenWrapper', () => {
let zeroExWithoutAccounts: ZeroEx;
before(async () => {
const hasAddresses = false;
const web3WithoutAccounts = web3Factory.create(hasAddresses);
const web3WithoutAccounts = web3Factory.create({ hasAddresses });
zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider, config);
});
it('should return balance even when called with Web3 provider instance without addresses', async () => {
@@ -306,7 +305,7 @@ describe('TokenWrapper', () => {
let zeroExWithoutAccounts: ZeroEx;
before(async () => {
const hasAddresses = false;
const web3WithoutAccounts = web3Factory.create(hasAddresses);
const web3WithoutAccounts = web3Factory.create({ hasAddresses });
zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider, config);
});
it('should get the proxy allowance', async () => {

View File

@@ -1,4 +1,4 @@
import { MultiSigConfigByNetwork } from '../../types';
import { MultiSigConfigByNetwork } from '../types';
// Make a copy of this file named `multisig.js` and input custom params as needed
export const multiSig: MultiSigConfigByNetwork = {

View File

@@ -1,5 +1,5 @@
import { Token } from '../../types';
import { constants } from '../../utils/constants';
import { constants } from '../../util/constants';
import { Token } from '../types';
export const tokenInfo: Token[] = [
{

View File

@@ -0,0 +1,20 @@
import { Deployer } from '@0xproject/deployer';
import { devConstants } from '@0xproject/dev-utils';
import * as path from 'path';
import { constants } from '../util/constants';
import { runMigrationsAsync } from './migrate';
const deployerOpts = {
artifactsDir: path.resolve('src', 'artifacts'),
jsonrpcPort: devConstants.RPC_PORT,
networkId: constants.TESTRPC_NETWORK_ID,
defaults: {
gas: devConstants.GAS_ESTIMATE,
},
};
export const deployer = new Deployer(deployerOpts);
runMigrationsAsync(deployer).catch(console.log);

View File

@@ -0,0 +1,90 @@
import { Deployer } from '@0xproject/deployer';
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import { constants } from '../util/constants';
import { ContractName } from '../util/types';
import { tokenInfo } from './config/token_info';
/**
* Custom migrations should be defined in this function. This will be called with the CLI 'migrate' command.
* Some operations might be completed in parallel, but we don't do that on purpose.
* That way the addresses are deterministic.
* @param deployer Deployer instance.
*/
export const runMigrationsAsync = async (deployer: Deployer) => {
const web3Wrapper: Web3Wrapper = deployer.web3Wrapper;
const accounts: string[] = await web3Wrapper.getAvailableAddressesAsync();
const tokenTransferProxy = await deployer.deployAndSaveAsync(ContractName.TokenTransferProxy);
const zrxToken = await deployer.deployAndSaveAsync(ContractName.ZRXToken);
const etherToken = await deployer.deployAndSaveAsync(ContractName.EtherToken);
const tokenReg = await deployer.deployAndSaveAsync(ContractName.TokenRegistry);
const exchangeArgs = [zrxToken.address, tokenTransferProxy.address];
const owners = [accounts[0], accounts[1]];
const confirmationsRequired = new BigNumber(2);
const secondsRequired = new BigNumber(0);
const multiSigArgs = [owners, confirmationsRequired, secondsRequired, tokenTransferProxy.address];
const exchange = await deployer.deployAndSaveAsync(ContractName.Exchange, exchangeArgs);
const multiSig = await deployer.deployAndSaveAsync(
ContractName.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,
multiSigArgs,
);
const owner = accounts[0];
await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner });
await tokenTransferProxy.transferOwnership.sendTransactionAsync(multiSig.address, { from: owner });
const addTokenGasEstimate = await tokenReg.addToken.estimateGasAsync(
zrxToken.address,
tokenInfo[0].name,
tokenInfo[0].symbol,
tokenInfo[0].decimals,
tokenInfo[0].ipfsHash,
tokenInfo[0].swarmHash,
{ from: owner },
);
await tokenReg.addToken.sendTransactionAsync(
zrxToken.address,
'0x Protocol Token',
'ZRX',
18,
constants.NULL_BYTES,
constants.NULL_BYTES,
{
from: owner,
gas: addTokenGasEstimate,
},
);
await tokenReg.addToken.sendTransactionAsync(
etherToken.address,
'Ether Token',
'WETH',
18,
constants.NULL_BYTES,
constants.NULL_BYTES,
{
from: owner,
gas: addTokenGasEstimate,
},
);
for (const token of tokenInfo) {
const totalSupply = new BigNumber(0);
const args = [token.name, token.symbol, token.decimals, totalSupply];
const dummyToken = await deployer.deployAsync(ContractName.DummyToken, args);
await tokenReg.addToken.sendTransactionAsync(
dummyToken.address,
token.name,
token.symbol,
token.decimals,
token.ipfsHash,
token.swarmHash,
{
from: owner,
gas: addTokenGasEstimate,
},
);
}
};

View File

@@ -13,13 +13,18 @@
"copy_artifacts": "copyfiles './src/artifacts/**/*' ./lib",
"build": "tsc",
"test": "run-s build run_mocha",
"run_mocha": "mocha 'lib/test/**/*.js' --timeout 10000 --bail --exit",
"test:coverage": "COVERAGE=true run-s build run_mocha coverage:report:text",
"run_mocha": "mocha 'lib/test/**/*.js' --timeout 10000000 --bail --exit",
"compile:comment": "Yarn workspaces do not link binaries correctly so we need to reference them directly https://github.com/yarnpkg/yarn/issues/3846",
"compile": "node ../deployer/lib/src/cli.js compile --contracts ${npm_package_config_contracts} --contracts-dir src/contracts --artifacts-dir src/artifacts",
"clean": "shx rm -rf ./lib",
"generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis 'src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken|Arbitrage|EtherDelta|AccountLevels).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers && prettier --write 'src/contract_wrappers/generated/**.ts'",
"migrate": "node ../deployer/lib/src/cli.js migrate",
"migrate": "yarn build && yarn compile && node ./lib/migrations/index.js",
"lint": "tslint --project . 'migrations/**/*.ts' 'test/**/*.ts' 'util/**/*.ts' 'deploy/**/*.ts'",
"coverage:report:text": "istanbul report text",
"coverage:report:html": "istanbul report html && open coverage/lcov-report/index.html",
"coverage:report:lcov": "istanbul report lcov",
"coverage:report:coveralls": "yarn coverage:report:lcov && cat coverage/lcov.info | coveralls",
"test:circleci": "yarn test"
},
"config": {
@@ -49,6 +54,7 @@
"chai-bignumber": "^2.0.1",
"chai-typescript-typings": "^0.0.4",
"copyfiles": "^1.2.0",
"coveralls": "^3.0.0",
"dirty-chai": "^2.0.1",
"ethers-typescript-typings": "^0.0.2",
"mocha": "^4.0.1",

View File

@@ -363,4 +363,3 @@ contract MultiSigWallet {
_transactionIds[i - from] = transactionIdsTemp[i];
}
}

View File

@@ -9,12 +9,11 @@ import { ContractName } from '../util/types';
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { web3, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('EtherToken', () => {
let account: string;

View File

@@ -22,12 +22,11 @@ import { OrderFactory } from '../../util/order_factory';
import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { deployer } from '../utils/deployer';
import { web3, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('Exchange', () => {
let maker: string;

View File

@@ -17,13 +17,12 @@ import { OrderFactory } from '../../util/order_factory';
import { ContractName } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { deployer } from '../utils/deployer';
import { web3, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('Exchange', () => {
let maker: string;

View File

@@ -22,12 +22,11 @@ import { OrderFactory } from '../../util/order_factory';
import { BalancesByOwner, ContractName } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { deployer } from '../utils/deployer';
import { web3, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('Exchange', () => {
let maker: string;

View File

@@ -0,0 +1,8 @@
import { getCoverageSubprovider } from '@0xproject/dev-utils';
after('generate coverage report', async () => {
if (process.env.COVERAGE) {
const coverageSubprovider = getCoverageSubprovider();
await coverageSubprovider.writeCoverageAsync();
}
});

View File

@@ -1,5 +1,5 @@
import { LogWithDecodedArgs, ZeroEx } from '0x.js';
import { BlockchainLifecycle, RPC, web3Factory } from '@0xproject/dev-utils';
import { BlockchainLifecycle, web3Factory } from '@0xproject/dev-utils';
import { AbiDecoder, BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
@@ -15,14 +15,12 @@ import { ContractName, SubmissionContractEventArgs } from '../util/types';
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { web3, web3Wrapper } from './utils/web3_wrapper';
const MULTI_SIG_ABI = artifacts.MultiSigWalletWithTimeLockArtifact.networks[constants.TESTRPC_NETWORK_ID].abi;
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const zeroEx = new ZeroEx(web3.currentProvider, { networkId: constants.TESTRPC_NETWORK_ID });
const abiDecoder = new AbiDecoder([MULTI_SIG_ABI]);
@@ -39,11 +37,6 @@ describe('MultiSigWalletWithTimeLock', () => {
let multiSigWrapper: MultiSigWrapper;
let txId: BigNumber;
let initialSecondsTimeLocked: number;
let rpc: RPC;
before(async () => {
rpc = new RPC();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
@@ -192,7 +185,7 @@ describe('MultiSigWalletWithTimeLock', () => {
});
it('should execute if it has enough confirmations and is past the time lock', async () => {
await rpc.increaseTimeAsync(SECONDS_TIME_LOCKED.toNumber());
await web3Wrapper.increaseTimeAsync(SECONDS_TIME_LOCKED.toNumber());
await multiSig.executeTransaction.sendTransactionAsync(txId, { from: owners[0] });
const secondsTimeLocked = new BigNumber(await multiSig.secondsTimeLocked.callAsync());

View File

@@ -16,6 +16,7 @@ import { ContractName, SubmissionContractEventArgs, TransactionDataParams } from
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { web3, web3Wrapper } from './utils/web3_wrapper';
const PROXY_ABI = artifacts.TokenTransferProxyArtifact.networks[constants.TESTRPC_NETWORK_ID].abi;
const MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI =
artifacts.MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddressArtifact.networks[constants.TESTRPC_NETWORK_ID]
@@ -23,9 +24,7 @@ const MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI =
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const abiDecoder = new AbiDecoder([MUTISIG_WALLET_WITH_TIME_LOCK_EXCEPT_REMOVE_AUTHORIZED_ADDRESS_ABI]);
describe('MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress', () => {

View File

@@ -14,12 +14,11 @@ import { ContractName } from '../util/types';
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { web3, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('TokenRegistry', () => {
let owner: string;

View File

@@ -8,12 +8,11 @@ import { constants } from '../../util/constants';
import { ContractName } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { deployer } from '../utils/deployer';
import { web3, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('TokenTransferProxy', () => {
let owner: string;

View File

@@ -11,12 +11,11 @@ import { constants } from '../../util/constants';
import { ContractName } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { deployer } from '../utils/deployer';
import { web3, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('TokenTransferProxy', () => {
let accounts: string[];

View File

@@ -17,12 +17,11 @@ import { OrderFactory } from '../../util/order_factory';
import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { deployer } from '../utils/deployer';
import { web3, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('Arbitrage', () => {
let coinbase: string;

View File

@@ -11,12 +11,11 @@ import { ContractName } from '../util/types';
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { web3, web3Wrapper } from './utils/web3_wrapper';
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('UnlimitedAllowanceToken', () => {
let owner: string;

View File

@@ -4,7 +4,10 @@ import * as path from 'path';
import { constants } from '../../util/constants';
import { web3 } from './web3_wrapper';
const deployerOpts = {
web3Provider: web3.currentProvider,
artifactsDir: path.resolve('src', 'artifacts'),
jsonrpcPort: devConstants.RPC_PORT,
networkId: constants.TESTRPC_NETWORK_ID,

View File

@@ -0,0 +1,5 @@
import { web3Factory } from '@0xproject/dev-utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
export const web3 = web3Factory.create();
export const web3Wrapper = new Web3Wrapper(web3.currentProvider);

View File

@@ -11,12 +11,11 @@ import { ContractName } from '../util/types';
import { chaiSetup } from './utils/chai_setup';
import { deployer } from './utils/deployer';
import { web3, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const blockchainLifecycle = new BlockchainLifecycle();
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('ZRXToken', () => {
let owner: string;

View File

@@ -8,6 +8,11 @@
* Check dependencies when determining if contracts should be recompiled (#408)
* Improve an error message for when deployer is supplied with an incorrect number of constructor arguments (#419)
* Export the `Compiler` (#426)
* Load solc from remote source instead of having it locally (#426)
* Add `bytecode`, `runtime_bytecode`, `source_map`, `source_map_runtime` and `sources` fields to artifacts (#426)
* Remove 0x-specific `migrate` command (#426)
* Allow deployer to accept a provider instead of port and host. This makes it possible to run it with in-process ganache-core (#426)
## v0.1.0 - _February 16, 2018_

View File

@@ -16,7 +16,6 @@ cli.js [command]
Commands:
cli.js compile compile contracts
cli.js migrate compile and deploy contracts using migration scripts
cli.js deploy deploy a single contract with provided arguments
Options:

View File

@@ -6,7 +6,7 @@
"types": "lib/src/index.d.ts",
"scripts": {
"build:watch": "tsc -w",
"build": "yarn clean && copyfiles 'test/fixtures/contracts/**/*' src/solc/solc_bin/* ./lib && tsc",
"build": "yarn clean && copyfiles 'test/fixtures/contracts/**/*' ./lib && tsc",
"test": "npm run build; mocha lib/test/*_test.js",
"compile": "npm run build; node lib/src/cli.js compile",
"clean": "shx rm -rf ./lib",
@@ -29,6 +29,7 @@
"homepage": "https://github.com/0xProject/0x-monorepo/packages/deployer/README.md",
"devDependencies": {
"@0xproject/tslint-config": "^0.4.10",
"@types/require-from-string": "^1.2.0",
"chai": "^4.0.1",
"copyfiles": "^1.2.0",
"ethers-typescript-typings": "^0.0.2",
@@ -45,7 +46,9 @@
"@0xproject/utils": "^0.4.1",
"@0xproject/web3-wrapper": "^0.2.1",
"ethereumjs-util": "^5.1.1",
"isomorphic-fetch": "^2.2.1",
"lodash": "^4.17.4",
"require-from-string": "^2.0.1",
"solc": "^0.4.18",
"web3": "^0.20.0",
"web3-eth-abi": "^1.0.0-beta.24",

1
packages/deployer/solc_bin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.js

View File

@@ -31,37 +31,6 @@ async function onCompileCommand(argv: CliOptions): Promise<void> {
};
await commands.compileAsync(opts);
}
/**
* Compiles all contracts and runs migration script with options passed in through CLI.
* Uses network ID of running node.
* @param argv Instance of process.argv provided by yargs.
*/
async function onMigrateCommand(argv: CliOptions): Promise<void> {
const url = `http://localhost:${argv.jsonrpcPort}`;
const web3Provider = new Web3.providers.HttpProvider(url);
const web3Wrapper = new Web3Wrapper(web3Provider);
const networkId = await web3Wrapper.getNetworkIdAsync();
const compilerOpts: CompilerOptions = {
contractsDir: argv.contractsDir,
networkId,
optimizerEnabled: argv.shouldOptimize ? 1 : 0,
artifactsDir: argv.artifactsDir,
specifiedContracts: getContractsSetFromList(argv.contracts),
};
await commands.compileAsync(compilerOpts);
const defaults = {
gasPrice: new BigNumber(argv.gasPrice),
from: argv.account,
};
const deployerOpts = {
artifactsDir: argv.artifactsDir,
jsonrpcPort: argv.jsonrpcPort,
networkId,
defaults,
};
await commands.migrateAsync(deployerOpts);
}
/**
* Deploys a single contract with provided name and args.
* @param argv Instance of process.argv provided by yargs.
@@ -171,12 +140,6 @@ function deployCommandBuilder(yargsInstance: any) {
description: 'comma separated list of contracts to compile',
})
.command('compile', 'compile contracts', identityCommandBuilder, onCompileCommand)
.command(
'migrate',
'compile and deploy contracts using migration scripts',
identityCommandBuilder,
onMigrateCommand,
)
.command('deploy', 'deploy a single contract with provided arguments', deployCommandBuilder, onDeployCommand)
.help().argv;
})();

View File

@@ -1,6 +1,5 @@
import { Compiler } from './compiler';
import { Deployer } from './deployer';
import { migrator } from './migrations/migrate';
import { CompilerOptions, DeployerOptions } from './utils/types';
export const commands = {
@@ -8,10 +7,6 @@ export const commands = {
const compiler = new Compiler(opts);
await compiler.compileAllAsync();
},
async migrateAsync(opts: DeployerOptions): Promise<void> {
const deployer = new Deployer(opts);
await migrator.runMigrationsAsync(deployer);
},
async deployAsync(contractName: string, args: any[], opts: DeployerOptions): Promise<void> {
const deployer = new Deployer(opts);
await deployer.deployAndSaveAsync(contractName, args);

View File

@@ -1,6 +1,10 @@
import { promisify } from '@0xproject/utils';
import * as ethUtil from 'ethereumjs-util';
import * as fs from 'fs';
import 'isomorphic-fetch';
import * as _ from 'lodash';
import * as path from 'path';
import * as requireFromString from 'require-from-string';
import solc = require('solc');
import * as Web3 from 'web3';
@@ -15,7 +19,6 @@ import {
ContractSourceData,
ContractSources,
ContractSpecificSourceData,
ImportContents,
} from './utils/types';
import { utils } from './utils/utils';
@@ -186,9 +189,18 @@ export class Compiler {
}
const fullSolcVersion = binPaths[contractSpecificSourceData.solcVersion];
const solcBinPath = `./solc/solc_bin/${fullSolcVersion}`;
const solcBin = require(solcBinPath);
const solcInstance = solc.setupMethods(solcBin);
const compiler_bin_filename = path.join(__dirname, '../../solc_bin', fullSolcVersion);
let solcjs: string;
if (fs.existsSync(compiler_bin_filename)) {
solcjs = fs.readFileSync(compiler_bin_filename).toString();
} else {
utils.consoleLog(`Downloading ${fullSolcVersion}...`);
const url = `https://ethereum.github.io/solc-bin/bin/${fullSolcVersion}`;
const response = await fetch(url);
solcjs = await response.text();
fs.writeFileSync(compiler_bin_filename, solcjs);
}
const solcInstance = solc.setupMethods(requireFromString(solcjs, compiler_bin_filename));
utils.consoleLog(`Compiling ${fileName}...`);
const source = this._contractSources[fileName];
@@ -210,11 +222,14 @@ export class Compiler {
this._solcErrors.add(normalizedErrMsg);
});
}
const contractName = path.basename(fileName, constants.SOLIDITY_FILE_EXTENSION);
const contractIdentifier = `${fileName}:${contractName}`;
const abi: Web3.ContractAbi = JSON.parse(compiled.contracts[contractIdentifier].interface);
const unlinked_binary = `0x${compiled.contracts[contractIdentifier].bytecode}`;
const bytecode = `0x${compiled.contracts[contractIdentifier].bytecode}`;
const runtimeBytecode = `0x${compiled.contracts[contractIdentifier].runtimeBytecode}`;
const sourceMap = compiled.contracts[contractIdentifier].srcmap;
const sourceMapRuntime = compiled.contracts[contractIdentifier].srcmapRuntime;
const sources = _.keys(compiled.sources);
const updated_at = Date.now();
const contractNetworkData: ContractNetworkData = {
solc_version: contractSpecificSourceData.solcVersion,
@@ -222,8 +237,12 @@ export class Compiler {
source_tree_hash: sourceTreeHash,
optimizer_enabled: this._optimizerEnabled,
abi,
unlinked_binary,
bytecode,
runtime_bytecode: runtimeBytecode,
updated_at,
source_map: sourceMap,
source_map_runtime: sourceMapRuntime,
sources,
};
let newArtifact: ContractArtifact;
@@ -284,13 +303,13 @@ export class Compiler {
* @param importPath Path to an imported dependency.
* @return Import contents object containing source code of dependency.
*/
private _findImportsIfSourcesExist(importPath: string): ImportContents {
private _findImportsIfSourcesExist(importPath: string): solc.ImportContents {
const fileName = path.basename(importPath);
const source = this._contractSources[fileName];
if (_.isUndefined(source)) {
throw new Error(`Contract source not found for ${fileName}`);
}
const importContents: ImportContents = {
const importContents: solc.ImportContents = {
contents: source,
};
return importContents;

View File

@@ -6,7 +6,13 @@ import * as Web3 from 'web3';
import { Contract } from './utils/contract';
import { encoder } from './utils/encoder';
import { fsWrapper } from './utils/fs_wrapper';
import { ContractArtifact, ContractNetworkData, DeployerOptions } from './utils/types';
import {
ContractArtifact,
ContractNetworkData,
DeployerOptions,
PortDeployerOptions,
ProviderDeployerOptions,
} from './utils/types';
import { utils } from './utils/utils';
// Gas added to gas estimate to make sure there is sufficient gas for deployment.
@@ -15,17 +21,16 @@ const EXTRA_GAS = 200000;
export class Deployer {
public web3Wrapper: Web3Wrapper;
private _artifactsDir: string;
private _jsonrpcPort: number;
private _networkId: number;
private _defaults: Partial<TxData>;
constructor(opts: DeployerOptions) {
this._artifactsDir = opts.artifactsDir;
this._jsonrpcPort = opts.jsonrpcPort;
this._networkId = opts.networkId;
const jsonrpcUrl = `http://localhost:${this._jsonrpcPort}`;
const web3Provider = new Web3.providers.HttpProvider(jsonrpcUrl);
this._defaults = opts.defaults;
const web3Provider = _.isUndefined((opts as ProviderDeployerOptions).web3Provider)
? new Web3.providers.HttpProvider(`http://localhost:${(opts as PortDeployerOptions).jsonrpcPort}`)
: (opts as ProviderDeployerOptions).web3Provider;
this.web3Wrapper = new Web3Wrapper(web3Provider, this._defaults);
}
/**
@@ -39,7 +44,7 @@ export class Deployer {
const contractNetworkDataIfExists: ContractNetworkData = this._getContractNetworkDataFromArtifactIfExists(
contractArtifactIfExists,
);
const data = contractNetworkDataIfExists.unlinked_binary;
const data = contractNetworkDataIfExists.bytecode;
const from = await this._getFromAddressAsync();
const gas = await this._getAllowableGasEstimateAsync(data);
const txData = {

View File

@@ -1,11 +1,41 @@
declare module 'solc' {
// tslint:disable:completed-docs
export function compile(sources: any, optimizerEnabled: number, findImports: (importPath: string) => any): any;
export function setupMethods(solcBin: any): any;
// tslint:enable:completed-docs
declare module 'solc' {
import * as Web3 from 'web3';
export interface ContractCompilationResult {
srcmap: string;
srcmapRuntime: string;
bytecode: string;
runtimeBytecode: string;
interface: string;
}
export interface CompilationResult {
errors: string[];
contracts: {
[contractIdentifier: string]: ContractCompilationResult;
};
sources: {
[sourceName: string]: any;
};
}
export interface ImportContents {
contents: string;
}
export interface InputSources {
sources: {
[fileName: string]: string;
};
}
export interface SolcInstance {
compile(
sources: InputSources,
optimizerEnabled: number,
findImports: (importPath: string) => ImportContents,
): CompilationResult;
}
export function loadRemoteVersion(versionName: string, cb: (err: Error | null, res?: SolcInstance) => void): void;
export function setupMethods(solcBin: any): SolcInstance;
}
declare module 'web3-eth-abi' {
// tslint:disable-next-line:completed-docs
export function encodeParameters(typesArray: string[], parameters: any[]): string;
}

View File

@@ -1 +1,2 @@
export { Deployer } from './deployer';
export { Compiler } from './compiler';

View File

@@ -1,91 +0,0 @@
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import { Deployer } from '../deployer';
import { constants } from '../utils/constants';
import { tokenInfo } from './config/token_info';
export const migrator = {
/**
* Custom migrations should be defined in this function. This will be called with the CLI 'migrate' command.
* Some operations might be completed in parallel, but we don't do that on purpose.
* That way the addresses are deterministic.
* @param deployer Deployer instance.
*/
async runMigrationsAsync(deployer: Deployer): Promise<void> {
const web3Wrapper: Web3Wrapper = deployer.web3Wrapper;
const accounts: string[] = await web3Wrapper.getAvailableAddressesAsync();
const tokenTransferProxy = await deployer.deployAndSaveAsync('TokenTransferProxy');
const zrxToken = await deployer.deployAndSaveAsync('ZRXToken');
const etherToken = await deployer.deployAndSaveAsync('WETH9');
const tokenReg = await deployer.deployAndSaveAsync('TokenRegistry');
const exchangeArgs = [zrxToken.address, tokenTransferProxy.address];
const owners = [accounts[0], accounts[1]];
const confirmationsRequired = new BigNumber(2);
const secondsRequired = new BigNumber(0);
const multiSigArgs = [owners, confirmationsRequired, secondsRequired, tokenTransferProxy.address];
const exchange = await deployer.deployAndSaveAsync('Exchange', exchangeArgs);
const multiSig = await deployer.deployAndSaveAsync(
'MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress',
multiSigArgs,
);
const owner = accounts[0];
await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner });
await tokenTransferProxy.transferOwnership.sendTransactionAsync(multiSig.address, { from: owner });
const addTokenGasEstimate = await tokenReg.addToken.estimateGasAsync(
zrxToken.address,
tokenInfo[0].name,
tokenInfo[0].symbol,
tokenInfo[0].decimals,
tokenInfo[0].ipfsHash,
tokenInfo[0].swarmHash,
{ from: owner },
);
await tokenReg.addToken.sendTransactionAsync(
zrxToken.address,
'0x Protocol Token',
'ZRX',
18,
constants.NULL_BYTES,
constants.NULL_BYTES,
{
from: owner,
gas: addTokenGasEstimate,
},
);
await tokenReg.addToken.sendTransactionAsync(
etherToken.address,
'Ether Token',
'WETH',
18,
constants.NULL_BYTES,
constants.NULL_BYTES,
{
from: owner,
gas: addTokenGasEstimate,
},
);
for (const token of tokenInfo) {
const totalSupply = new BigNumber(0);
const args = [token.name, token.symbol, token.decimals, totalSupply];
const dummyToken = await deployer.deployAsync('DummyToken', args);
await tokenReg.addToken.sendTransactionAsync(
dummyToken.address,
token.name,
token.symbol,
token.decimals,
token.ipfsHash,
token.swarmHash,
{
from: owner,
gas: addTokenGasEstimate,
},
);
}
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
export const constants = {
NULL_BYTES: '0x',
SOLIDITY_FILE_EXTENSION: '.sol',
};

View File

@@ -24,10 +24,14 @@ export interface ContractNetworkData {
keccak256: string;
source_tree_hash: string;
abi: Web3.ContractAbi;
unlinked_binary: string;
bytecode: string;
runtime_bytecode: string;
address?: string;
constructor_args?: string;
updated_at: number;
source_map: string;
source_map_runtime: string;
sources: string[];
}
export interface SolcErrors {
@@ -54,13 +58,22 @@ export interface CompilerOptions {
specifiedContracts: Set<string>;
}
export interface DeployerOptions {
export interface BaseDeployerOptions {
artifactsDir: string;
jsonrpcPort: number;
networkId: number;
defaults: Partial<TxData>;
}
export interface ProviderDeployerOptions extends BaseDeployerOptions {
web3Provider: Web3.Provider;
}
export interface PortDeployerOptions extends BaseDeployerOptions {
jsonrpcPort: number;
}
export type DeployerOptions = PortDeployerOptions | ProviderDeployerOptions;
export interface ContractSources {
[key: string]: string;
}
@@ -76,10 +89,6 @@ export interface ContractSpecificSourceData {
sourceTreeHashIfExists?: Buffer;
}
export interface ImportContents {
contents: string;
}
// TODO: Consolidate with 0x.js definitions once types are moved into a separate package.
export enum ZeroExError {
ContractDoesNotExist = 'CONTRACT_DOES_NOT_EXIST',

View File

@@ -53,7 +53,7 @@ describe('#Compiler', () => {
const exchangeArtifact: ContractArtifact = JSON.parse(exchangeArtifactString);
const exchangeContractData: ContractNetworkData = exchangeArtifact.networks[constants.networkId];
// The last 43 bytes of the binaries are metadata which may not be equivalent
const unlinkedBinaryWithoutMetadata = exchangeContractData.unlinked_binary.slice(0, -86);
const unlinkedBinaryWithoutMetadata = exchangeContractData.bytecode.slice(0, -86);
const exchangeBinaryWithoutMetadata = exchange_binary.slice(0, -86);
expect(unlinkedBinaryWithoutMetadata).to.equal(exchangeBinaryWithoutMetadata);
});

View File

@@ -1,5 +1,11 @@
# CHANGELOG
## v0.3.0 - _TBD, 2018_
* Add coverage subprovider if enabled (#426)
* Refactor `BlockchainLifecycle` to work with in-process ganache (#426)
* Remove `RPC` class and move it's logic to `Web3Wrapper` (#426)
## v0.2.0 - _February 16, 2018_
* Remove subproviders (#392)

View File

@@ -38,6 +38,7 @@
"typescript": "2.7.1"
},
"dependencies": {
"@0xproject/sol-cov": "^0.0.1",
"@0xproject/subproviders": "^0.7.0",
"@0xproject/types": "^0.3.1",
"@0xproject/utils": "^0.4.1",

View File

@@ -1,21 +1,24 @@
import { RPC } from './rpc';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as Web3 from 'web3';
export class BlockchainLifecycle {
private _rpc: RPC;
private _web3Wrapper: Web3Wrapper;
private _snapshotIdsStack: number[];
constructor() {
this._rpc = new RPC();
constructor(web3Orweb3Wrapper: Web3Wrapper | Web3) {
this._web3Wrapper = (web3Orweb3Wrapper as Web3Wrapper).isZeroExWeb3Wrapper
? (web3Orweb3Wrapper as Web3Wrapper)
: new Web3Wrapper((web3Orweb3Wrapper as Web3).currentProvider);
this._snapshotIdsStack = [];
}
// TODO: In order to run these tests on an actual node, we should check if we are running against
// TestRPC, if so, use snapshots, otherwise re-deploy contracts before every test
public async startAsync(): Promise<void> {
const snapshotId = await this._rpc.takeSnapshotAsync();
const snapshotId = await this._web3Wrapper.takeSnapshotAsync();
this._snapshotIdsStack.push(snapshotId);
}
public async revertAsync(): Promise<void> {
const snapshotId = this._snapshotIdsStack.pop() as number;
const didRevert = await this._rpc.revertSnapshotAsync(snapshotId);
const didRevert = await this._web3Wrapper.revertSnapshotAsync(snapshotId);
if (!didRevert) {
throw new Error(`Snapshot with id #${snapshotId} failed to revert`);
}

View File

@@ -1,5 +1,5 @@
export const constants = {
RPC_URL: 'http://localhost:8545',
RPC_PORT: 8545,
GAS_ESTIMATE: 1000000,
GAS_ESTIMATE: 5000000,
};

View File

@@ -0,0 +1,14 @@
import { CoverageSubprovider } from '@0xproject/sol-cov';
import * as _ from 'lodash';
let coverageSubprovider: CoverageSubprovider;
export function getCoverageSubprovider(): CoverageSubprovider {
if (_.isUndefined(coverageSubprovider)) {
const artifactsPath = './src/artifacts';
const contractsPath = './src/contracts';
const networkId = 50;
coverageSubprovider = new CoverageSubprovider(artifactsPath, contractsPath, networkId);
}
return coverageSubprovider;
}

View File

@@ -1,4 +1,4 @@
export { RPC } from './rpc';
export { BlockchainLifecycle } from './blockchain_lifecycle';
export { web3Factory } from './web3_factory';
export { constants as devConstants } from './constants';
export { getCoverageSubprovider } from './coverage';

View File

@@ -1,62 +0,0 @@
import * as ethUtil from 'ethereumjs-util';
import * as request from 'request-promise-native';
import { constants } from './constants';
export class RPC {
private _url: string;
private _id: number;
constructor() {
this._url = constants.RPC_URL;
this._id = 0;
}
public async takeSnapshotAsync(): Promise<number> {
const method = 'evm_snapshot';
const params: any[] = [];
const payload = this._toPayload(method, params);
const snapshotIdHex = await this._sendAsync(payload);
const snapshotId = ethUtil.bufferToInt(ethUtil.toBuffer(snapshotIdHex));
return snapshotId;
}
public async revertSnapshotAsync(snapshotId: number): Promise<boolean> {
const method = 'evm_revert';
const params = [snapshotId];
const payload = this._toPayload(method, params);
const didRevert = await this._sendAsync(payload);
return didRevert;
}
public async increaseTimeAsync(time: number) {
const method = 'evm_increaseTime';
const params = [time];
const payload = this._toPayload(method, params);
return this._sendAsync(payload);
}
public async mineBlockAsync(): Promise<void> {
const method = 'evm_mine';
const params: any[] = [];
const payload = this._toPayload(method, params);
await this._sendAsync(payload);
}
private _toPayload(method: string, params: any[] = []): string {
const payload = JSON.stringify({
id: this._id,
method,
params,
});
this._id += 1;
return payload;
}
private async _sendAsync(payload: string): Promise<any> {
const opts = {
method: 'POST',
uri: this._url,
body: payload,
headers: {
'content-type': 'application/json',
},
};
const bodyString = await request(opts);
const body = JSON.parse(bodyString);
return body.result;
}
}

View File

@@ -6,9 +6,13 @@
import ProviderEngine = require('web3-provider-engine');
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
import { EmptyWalletSubprovider, FakeGasEstimateSubprovider } from '@0xproject/subproviders';
import { EmptyWalletSubprovider, FakeGasEstimateSubprovider, GanacheSubprovider } from '@0xproject/subproviders';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as process from 'process';
import { constants } from './constants';
import { getCoverageSubprovider } from './coverage';
// HACK: web3 leaks XMLHttpRequest into the global scope and causes requests to hang
// because they are using the wrong XHR package.
@@ -17,24 +21,51 @@ import { constants } from './constants';
// tslint:disable-next-line:ordered-imports
import * as Web3 from 'web3';
export interface Web3Config {
hasAddresses?: boolean; // default: true
useInProcessGanache?: boolean; // default: false
}
export const web3Factory = {
create(hasAddresses: boolean = true): Web3 {
const provider = this.getRpcProvider(hasAddresses);
create(config: Web3Config = {}): Web3 {
const provider = this.getRpcProvider(config);
const web3 = new Web3();
web3.setProvider(provider);
return web3;
},
getRpcProvider(hasAddresses: boolean = true): Web3.Provider {
getRpcProvider(config: Web3Config = {}): Web3.Provider {
const provider = new ProviderEngine();
if (process.env.COVERAGE) {
provider.addProvider(getCoverageSubprovider());
}
const hasAddresses = _.isUndefined(config.hasAddresses) || config.hasAddresses;
if (!hasAddresses) {
provider.addProvider(new EmptyWalletSubprovider());
}
provider.addProvider(new FakeGasEstimateSubprovider(constants.GAS_ESTIMATE));
const logger = {
log: (arg: any) => {
fs.appendFileSync('ganache.log', `${arg}\n`);
},
};
const useInProcessGanache = config.useInProcessGanache;
if (useInProcessGanache) {
provider.addProvider(
new GanacheSubprovider({
logger,
verbose: process.env.VERBOSE_GANACHE,
port: 8545,
networkId: 50,
mnemonic: 'concert load couple harbor equip island argue ramp clarify fence smart topic',
}),
);
} else {
provider.addProvider(
new RpcSubprovider({
rpcUrl: constants.RPC_URL,
}),
);
}
provider.start();
return provider;
},

View File

@@ -3,20 +3,19 @@ import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
import 'mocha';
import { BlockchainLifecycle, RPC, web3Factory } from '../src';
import { BlockchainLifecycle, web3Factory } from '../src';
const expect = chai.expect;
describe('BlockchainLifecycle tests', () => {
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const rpc = new RPC();
const blockchainLifecycle = new BlockchainLifecycle();
const web3Provider = web3Factory.getRpcProvider();
const web3Wrapper = new Web3Wrapper(web3Provider);
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('#startAsync/revertAsync', () => {
it('reverts changes in between', async () => {
const blockNumberBefore = await web3Wrapper.getBlockNumberAsync();
await blockchainLifecycle.startAsync();
await rpc.mineBlockAsync();
await web3Wrapper.mineBlockAsync();
const blockNumberAfter = await web3Wrapper.getBlockNumberAsync();
expect(blockNumberAfter).to.be.equal(blockNumberBefore + 1);
await blockchainLifecycle.revertAsync();

View File

@@ -3,18 +3,17 @@ import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
import 'mocha';
import { RPC, web3Factory } from '../src';
import { web3Factory } from '../src';
const expect = chai.expect;
describe('RPC tests', () => {
const web3 = web3Factory.create();
const web3Wrapper = new Web3Wrapper(web3.currentProvider);
const rpc = new RPC();
const web3Provider = web3Factory.getRpcProvider();
const web3Wrapper = new Web3Wrapper(web3Provider);
describe('#mineBlockAsync', () => {
it('increases block number when called', async () => {
const blockNumberBefore = await web3Wrapper.getBlockNumberAsync();
await rpc.mineBlockAsync();
await web3Wrapper.mineBlockAsync();
const blockNumberAfter = await web3Wrapper.getBlockNumberAsync();
expect(blockNumberAfter).to.be.equal(blockNumberBefore + 1);
});
@@ -23,8 +22,8 @@ describe('RPC tests', () => {
it('increases time when called', async () => {
const TIME_DELTA = 1000;
const blockTimestampBefore = await web3Wrapper.getBlockTimestampAsync(BlockParamLiteral.Latest);
await rpc.increaseTimeAsync(TIME_DELTA);
await rpc.mineBlockAsync();
await web3Wrapper.increaseTimeAsync(TIME_DELTA);
await web3Wrapper.mineBlockAsync();
const blockTimestampAfter = await web3Wrapper.getBlockTimestampAsync(BlockParamLiteral.Latest);
expect(blockTimestampAfter).to.be.at.least(blockTimestampBefore + TIME_DELTA);
});
@@ -32,9 +31,9 @@ describe('RPC tests', () => {
describe('#takeSnapshotAsync/revertSnapshotAsync', () => {
it('reverts changes in between', async () => {
const blockNumberBefore = await web3Wrapper.getBlockNumberAsync();
const snapshotId = await rpc.takeSnapshotAsync();
await rpc.mineBlockAsync();
await rpc.revertSnapshotAsync(snapshotId);
const snapshotId = await web3Wrapper.takeSnapshotAsync();
await web3Wrapper.mineBlockAsync();
await web3Wrapper.revertSnapshotAsync(snapshotId);
const blockNumberAfter = await web3Wrapper.getBlockNumberAsync();
expect(blockNumberAfter).to.be.equal(blockNumberBefore);
});

View File

@@ -1,3 +1,6 @@
{
"extends": ["@0xproject/tslint-config"]
"extends": ["@0xproject/tslint-config"],
"rules": {
"completed-docs": false
}
}

View File

@@ -0,0 +1,5 @@
.*
yarn-error.log
/src/
/scripts/
tsconfig.json

View File

@@ -0,0 +1,5 @@
# CHANGELOG
## v0.0.1 - _TBD, 2018_
* Initial implementation (#TBD)

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,42 @@
{
"name": "@0xproject/sol-cov",
"version": "0.0.1",
"description": "Generate coverage reports for solidity code",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build:watch": "tsc -w",
"lint": "tslint --project . 'src/**/*.ts'",
"clean": "shx rm -rf lib",
"build": "tsc"
},
"repository": {
"type": "git",
"url": "https://github.com/0xProject/0x.js.git"
},
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/0xProject/0x.js/issues"
},
"homepage": "https://github.com/0xProject/0x.js/packages/sol-cov/README.md",
"dependencies": {
"@0xproject/subproviders": "^0.7.0",
"@0xproject/utils": "^0.3.4",
"glob": "^7.1.2",
"istanbul": "^0.4.5",
"lodash": "^4.17.4",
"solidity-coverage": "^0.4.10",
"solidity-parser-sc": "^0.4.4",
"web3": "^0.20.0"
},
"devDependencies": {
"@0xproject/tslint-config": "^0.4.9",
"@types/istanbul": "^0.4.29",
"@types/node": "^8.0.53",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",
"tslint": "5.8.0",
"typescript": "2.7.1",
"web3-typescript-typings": "^0.9.11"
}
}

View File

@@ -0,0 +1,5 @@
const postpublish_utils = require('../../../scripts/postpublish_utils');
const packageJSON = require('../package.json');
const subPackageName = packageJSON.name;
postpublish_utils.standardPostPublishAsync(subPackageName);

View File

@@ -0,0 +1,115 @@
import * as _ from 'lodash';
import * as SolidityParser from 'solidity-parser-sc';
import { BranchMap, FnMap, LocationByOffset, SingleFileSourceRange, StatementMap } from './types';
export interface CoverageEntriesDescription {
fnMap: FnMap;
branchMap: BranchMap;
statementMap: StatementMap;
}
export class ASTVisitor {
private _entryId = 0;
private _fnMap: FnMap = {};
private _branchMap: BranchMap = {};
private _statementMap: StatementMap = {};
private _locationByOffset: LocationByOffset;
private static _doesLookLikeAnASTNode(ast: any): boolean {
const isAST = _.isObject(ast) && _.isString(ast.type) && _.isNumber(ast.start) && _.isNumber(ast.end);
return isAST;
}
constructor(locationByOffset: LocationByOffset) {
this._locationByOffset = locationByOffset;
}
public walkAST(astNode: SolidityParser.AST): void {
if (_.isArray(astNode) || _.isObject(astNode)) {
if (ASTVisitor._doesLookLikeAnASTNode(astNode)) {
const nodeType = astNode.type;
const visitorFunctionName = `_visit${nodeType}`;
// tslint:disable-next-line:no-this-assignment
const self: { [visitorFunctionName: string]: (ast: SolidityParser.AST) => void } = this as any;
if (_.isFunction(self[visitorFunctionName])) {
self[visitorFunctionName](astNode);
}
}
_.forEach(astNode, subtree => {
this.walkAST(subtree);
});
}
}
public getCollectedCoverageEntries(): CoverageEntriesDescription {
const coverageEntriesDescription = {
fnMap: this._fnMap,
branchMap: this._branchMap,
statementMap: this._statementMap,
};
return coverageEntriesDescription;
}
private _visitConditionalExpression(ast: SolidityParser.AST): void {
this._visitBinaryBranch(ast, ast.consequent, ast.alternate, 'cond-expr');
}
private _visitFunctionDeclaration(ast: SolidityParser.AST): void {
const loc = this._getExpressionRange(ast);
this._fnMap[this._entryId++] = {
name: ast.name,
line: loc.start.line,
loc,
};
}
private _visitBinaryExpression(ast: SolidityParser.AST): void {
this._visitBinaryBranch(ast, ast.left, ast.right, 'binary-expr');
}
private _visitIfStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
this._visitBinaryBranch(ast, ast.consequent, ast.alternate || ast, 'if');
}
private _visitBreakStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitContractStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitExpressionStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitForStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitPlaceholderStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitReturnStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitModifierArgument(ast: SolidityParser.AST): void {
const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure'];
if (!_.includes(BUILTIN_MODIFIERS, ast.name)) {
this._visitStatement(ast);
}
}
private _visitBinaryBranch(
ast: SolidityParser.AST,
left: SolidityParser.AST,
right: SolidityParser.AST,
type: 'if' | 'cond-expr' | 'binary-expr',
): void {
this._branchMap[this._entryId++] = {
line: this._getExpressionRange(ast).start.line,
type,
locations: [this._getExpressionRange(left), this._getExpressionRange(right)],
};
}
private _visitStatement(ast: SolidityParser.AST): void {
this._statementMap[this._entryId++] = this._getExpressionRange(ast);
}
private _getExpressionRange(ast: SolidityParser.AST): SingleFileSourceRange {
const start = this._locationByOffset[ast.start - 1];
const end = this._locationByOffset[ast.end - 1];
const range = {
start,
end,
};
return range;
}
}

View File

@@ -0,0 +1,40 @@
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, networkId: number) => {
const sourcesGlob = `${sourcesPath}/**/*.sol`;
const sourceFileNames = glob.sync(sourcesGlob, { absolute: true });
const contractsDataIfExists: Array<ContractData | {}> = _.map(sourceFileNames, sourceFileName => {
const baseName = path.basename(sourceFileName, '.sol');
const artifactFileName = path.join(artifactsPath, `${baseName}.json`);
if (!fs.existsSync(artifactFileName)) {
// If the contract isn't directly compiled, but is imported as the part of the other contract - we don't have an artifact for it and therefore can't do anything usefull with it
return {};
}
const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
const sources = _.map(artifact.networks[networkId].sources, source => {
const includedFileName = glob.sync(`${sourcesPath}/**/${source}`, { absolute: true })[0];
return includedFileName;
});
const sourceCodes = _.map(sources, source => {
const includedSourceCode = fs.readFileSync(source).toString();
return includedSourceCode;
});
const contractData = {
baseName,
sourceCodes,
sources,
sourceMap: artifact.networks[networkId].source_map,
sourceMapRuntime: artifact.networks[networkId].source_map_runtime,
runtimeBytecode: artifact.networks[networkId].runtime_bytecode,
bytecode: artifact.networks[networkId].bytecode,
};
return contractData;
});
const contractsData = _.filter(contractsDataIfExists, contractData => !_.isEmpty(contractData)) as ContractData[];
return contractsData;
};

View File

@@ -0,0 +1,3 @@
export const constants = {
NEW_CONTRACT: 'NEW_CONTRACT',
};

View File

@@ -0,0 +1,166 @@
import * as fs from 'fs';
import { Collector } from 'istanbul';
import * as _ from 'lodash';
import * as path from 'path';
import { collectContractsData } from './collect_contract_data';
import { constants } from './constants';
import { collectCoverageEntries } from './instrument_solidity';
import { parseSourceMap } from './source_maps';
import {
BranchCoverage,
BranchDescription,
BranchMap,
ContractData,
Coverage,
FnMap,
FunctionCoverage,
FunctionDescription,
LineColumn,
SingleFileSourceRange,
SourceRange,
StatementCoverage,
StatementDescription,
StatementMap,
TraceInfo,
} from './types';
import { utils } from './utils';
function getSingleFileCoverageForTrace(
contractData: ContractData,
coveredPcs: number[],
pcToSourceRange: { [programCounter: number]: SourceRange },
fileIndex: number,
): Coverage {
const fileName = contractData.sources[fileIndex];
const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex], fileName);
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.
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);
const branchCoverage: BranchCoverage = {};
for (const branchId of _.keys(coverageEntriesDescription.branchMap)) {
const branchDescription = coverageEntriesDescription.branchMap[branchId];
const isCovered = _.map(branchDescription.locations, location =>
_.some(sourceRanges, range => utils.isRangeInside(range.location, location)),
);
branchCoverage[branchId] = isCovered;
}
const statementCoverage: StatementCoverage = {};
for (const statementId of _.keys(coverageEntriesDescription.statementMap)) {
const statementDescription = coverageEntriesDescription.statementMap[statementId];
const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription));
statementCoverage[statementId] = isCovered;
}
const functionCoverage: FunctionCoverage = {};
for (const fnId of _.keys(coverageEntriesDescription.fnMap)) {
const functionDescription = coverageEntriesDescription.fnMap[fnId];
const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, functionDescription.loc));
functionCoverage[fnId] = isCovered;
}
const partialCoverage = {
[contractData.sources[fileIndex]]: {
...coverageEntriesDescription,
l: {}, // It's able to derive it from statement coverage
path: fileName,
f: functionCoverage,
s: statementCoverage,
b: branchCoverage,
},
};
return partialCoverage;
}
export class CoverageManager {
private _traceInfoByAddress: { [address: string]: TraceInfo[] } = {};
private _contractsData: ContractData[] = [];
private _txDataByHash: { [txHash: string]: string } = {};
private _getContractCodeAsync: (address: string) => Promise<string>;
constructor(
artifactsPath: string,
sourcesPath: string,
networkId: number,
getContractCodeAsync: (address: string) => Promise<string>,
) {
this._getContractCodeAsync = getContractCodeAsync;
this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId);
}
public setTxDataByHash(txHash: string, data: string): void {
this._txDataByHash[txHash] = data;
}
public appendTraceInfo(address: string, traceInfo: TraceInfo): void {
if (_.isUndefined(this._traceInfoByAddress[address])) {
this._traceInfoByAddress[address] = [];
}
this._traceInfoByAddress[address].push(traceInfo);
}
public async writeCoverageAsync(): Promise<void> {
const finalCoverage = await this._computeCoverageAsync();
const jsonReplacer: null = null;
const numberOfJsonSpaces = 4;
const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces);
fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
}
private async _computeCoverageAsync(): Promise<Coverage> {
const collector = new Collector();
for (const address of _.keys(this._traceInfoByAddress)) {
if (address !== constants.NEW_CONTRACT) {
// Runtime transaction
const runtimeBytecode = await this._getContractCodeAsync(address);
const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData;
if (_.isUndefined(contractData)) {
throw new Error(`Transaction to an unknown address: ${address}`);
}
const bytecodeHex = contractData.runtimeBytecode.slice(2);
const sourceMap = contractData.sourceMapRuntime;
const pcToSourceRange = parseSourceMap(
contractData.sourceCodes,
sourceMap,
bytecodeHex,
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
_.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => {
const singleFileCoverageForTrace = getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,
fileIndex,
);
collector.add(singleFileCoverageForTrace);
});
}
} else {
// Contract creation transaction
_.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => {
const bytecode = this._txDataByHash[traceInfo.txHash];
const contractData = _.find(this._contractsData, contractDataCandidate =>
bytecode.startsWith(contractDataCandidate.bytecode),
) as ContractData;
if (_.isUndefined(contractData)) {
throw new Error(`Unknown contract creation transaction`);
}
const bytecodeHex = contractData.bytecode.slice(2);
const sourceMap = contractData.sourceMap;
const pcToSourceRange = parseSourceMap(
contractData.sourceCodes,
sourceMap,
bytecodeHex,
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
const singleFileCoverageForTrace = getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,
fileIndex,
);
collector.add(singleFileCoverageForTrace);
}
});
}
}
// TODO: Submit a PR to DT
return (collector as any).getFinalCoverage();
}
}

View File

@@ -0,0 +1,124 @@
import { Callback, NextCallback, Subprovider } from '@0xproject/subproviders';
import { promisify } from '@0xproject/utils';
import * as _ from 'lodash';
import * as Web3 from 'web3';
import { constants } from './constants';
import { CoverageManager } from './coverage_manager';
/*
* This class implements the web3-provider-engine subprovider interface and collects traces of all transactions that were sent and all calls that were executed.
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class CoverageSubprovider extends Subprovider {
private _coverageManager: CoverageManager;
constructor(artifactsPath: string, sourcesPath: string, networkId: number) {
super();
this._coverageManager = new CoverageManager(
artifactsPath,
sourcesPath,
networkId,
this._getContractCodeAsync.bind(this),
);
}
public handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: NextCallback,
end: (err: Error | null, result: any) => void,
) {
switch (payload.method) {
case 'eth_sendTransaction':
const txData = payload.params[0];
next(this._onTransactionSentAsync.bind(this, txData));
return;
case 'eth_call':
const callData = payload.params[0];
const blockNumber = payload.params[1];
next(this._onCallExecutedAsync.bind(this, callData, blockNumber));
return;
default:
next();
return;
}
}
public async writeCoverageAsync(): Promise<void> {
await this._coverageManager.writeCoverageAsync();
}
private async _onTransactionSentAsync(
txData: Web3.TxData,
err: Error | null,
txHash?: string,
cb?: Callback,
): Promise<void> {
if (_.isNull(err)) {
await this._recordTxTraceAsync(txData.to || constants.NEW_CONTRACT, txData.data, txHash as string);
} else {
const payload = {
method: 'eth_getBlockByNumber',
params: ['latest', true],
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const transactions = jsonRPCResponsePayload.result.transactions;
for (const transaction of transactions) {
await this._recordTxTraceAsync(
transaction.to || constants.NEW_CONTRACT,
transaction.data,
transaction.hash,
);
}
}
if (!_.isUndefined(cb)) {
cb();
}
}
private async _onCallExecutedAsync(
callData: Partial<Web3.CallData>,
blockNumber: Web3.BlockParam,
err: Error | null,
callResult: string,
cb: Callback,
): Promise<void> {
await this._recordCallTraceAsync(callData, blockNumber);
cb();
}
private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
this._coverageManager.setTxDataByHash(txHash, data || '');
const 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
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const trace: Web3.TransactionTrace = jsonRPCResponsePayload.result;
const coveredPcs = _.map(trace.structLogs, log => log.pc);
this._coverageManager.appendTraceInfo(address, { coveredPcs, txHash });
}
private async _recordCallTraceAsync(callData: Partial<Web3.CallData>, blockNumber: Web3.BlockParam): Promise<void> {
const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result);
const txData = callData;
if (_.isUndefined(txData.from)) {
txData.from = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; // TODO
}
const txDataWithFromAddress = txData as Web3.TxData & { from: string };
try {
const txHash = (await this.emitPayloadAsync({
method: 'eth_sendTransaction',
params: [txDataWithFromAddress],
})).result;
await this._onTransactionSentAsync(txDataWithFromAddress, null, txHash);
} catch (err) {
await this._onTransactionSentAsync(txDataWithFromAddress, err, undefined);
}
const didRevert = (await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] })).result;
}
private async _getContractCodeAsync(address: string): Promise<string> {
const payload = {
method: 'eth_getCode',
params: [address, 'latest'],
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const contractCode: string = jsonRPCResponsePayload.result;
return contractCode;
}
}

6
packages/sol-cov/src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
// tslint:disable:completed-docs
declare module 'solidity-parser-sc' {
// This is too time-consuming to define and we don't rely on it anyway
export type AST = any;
export function parse(sourceCode: string): AST;
}

View File

@@ -0,0 +1 @@
export { CoverageSubprovider } from './coverage_subprovider';

View File

@@ -0,0 +1,24 @@
// tslint:disable:number-literal-format
const PUSH1 = 0x60;
const PUSH32 = 0x7f;
const isPush = (inst: number) => inst >= PUSH1 && inst <= PUSH32;
const pushDataLength = (inst: number) => inst - PUSH1 + 1;
const instructionLength = (inst: number) => (isPush(inst) ? pushDataLength(inst) + 1 : 1);
export const getPcToInstructionIndexMapping = (bytecode: Uint8Array) => {
const result: {
[programCounter: number]: number;
} = {};
let byteIndex = 0;
let instructionIndex = 0;
while (byteIndex < bytecode.length) {
const instruction = bytecode[byteIndex];
const length = instructionLength(instruction);
result[byteIndex] = instructionIndex;
byteIndex += length;
instructionIndex += 1;
}
return result;
};

View File

@@ -0,0 +1,16 @@
import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as SolidityParser from 'solidity-parser-sc';
import { ASTVisitor, CoverageEntriesDescription } from './ast_visitor';
import { getLocationByOffset } from './source_maps';
export const collectCoverageEntries = (contractSource: string, fileName: string) => {
const ast = SolidityParser.parse(contractSource);
const locationByOffset = getLocationByOffset(contractSource);
const astVisitor = new ASTVisitor(locationByOffset);
astVisitor.walkAST(ast);
const coverageEntries = astVisitor.getCollectedCoverageEntries();
return coverageEntries;
};

View File

@@ -0,0 +1,77 @@
import * as _ from 'lodash';
import { getPcToInstructionIndexMapping } from './instructions';
import { LineColumn, LocationByOffset, SourceRange } from './types';
const RADIX = 10;
export interface SourceLocation {
offset: number;
length: number;
fileIndex: number;
}
export const getLocationByOffset = (str: string) => {
const locationByOffset: LocationByOffset = {};
let currentOffset = 0;
for (const char of str.split('')) {
const location = locationByOffset[currentOffset - 1] || { line: 1, column: 0 };
const isNewline = char === '\n';
locationByOffset[currentOffset] = {
line: location.line + (isNewline ? 1 : 0),
column: isNewline ? 0 : location.column + 1,
};
currentOffset++;
}
return locationByOffset;
};
// Parses a sourcemap string
// The solidity sourcemap format is documented here: https://github.com/ethereum/solidity/blob/develop/docs/miscellaneous.rst#source-mappings
export const parseSourceMap = (sourceCodes: string[], srcMap: string, bytecodeHex: string, sources: string[]) => {
const bytecode = Uint8Array.from(Buffer.from(bytecodeHex, 'hex'));
const pcToInstructionIndex: { [programCounter: number]: number } = getPcToInstructionIndexMapping(bytecode);
const locationByOffsetByFileIndex = _.map(sourceCodes, getLocationByOffset);
const entries = srcMap.split(';');
const parsedEntries: SourceLocation[] = [];
let lastParsedEntry: SourceLocation = {} as any;
const instructionIndexToSourceRange: { [instructionIndex: number]: SourceRange } = {};
_.each(entries, (entry: string, i: number) => {
const [instructionIndexStrIfExists, lengthStrIfExists, fileIndexStrIfExists, jumpTypeStrIfExists] = entry.split(
':',
);
const instructionIndexIfExists = parseInt(instructionIndexStrIfExists, RADIX);
const lengthIfExists = parseInt(lengthStrIfExists, RADIX);
const fileIndexIfExists = parseInt(fileIndexStrIfExists, RADIX);
const offset = _.isNaN(instructionIndexIfExists) ? lastParsedEntry.offset : instructionIndexIfExists;
const length = _.isNaN(lengthIfExists) ? lastParsedEntry.length : lengthIfExists;
const fileIndex = _.isNaN(fileIndexIfExists) ? lastParsedEntry.fileIndex : fileIndexIfExists;
const parsedEntry = {
offset,
length,
fileIndex,
};
if (parsedEntry.fileIndex !== -1) {
const sourceRange = {
location: {
start: locationByOffsetByFileIndex[parsedEntry.fileIndex][parsedEntry.offset - 1],
end:
locationByOffsetByFileIndex[parsedEntry.fileIndex][parsedEntry.offset + parsedEntry.length - 1],
},
fileName: sources[parsedEntry.fileIndex],
};
instructionIndexToSourceRange[i] = sourceRange;
} else {
// Some assembly code generated by Solidity can't be mapped back to a line of source code.
// Source: https://github.com/ethereum/solidity/issues/3629
}
lastParsedEntry = parsedEntry;
});
const pcsToSourceRange: { [programCounter: number]: SourceRange } = {};
for (const programCounterKey of _.keys(pcToInstructionIndex)) {
const pc = parseInt(programCounterKey, RADIX);
const instructionIndex: number = pcToInstructionIndex[pc];
pcsToSourceRange[pc] = instructionIndexToSourceRange[instructionIndex];
}
return pcsToSourceRange;
};

View File

@@ -0,0 +1,89 @@
export interface LineColumn {
line: number;
column: number;
}
export interface SourceRange {
location: SingleFileSourceRange;
fileName: string;
}
export interface SingleFileSourceRange {
start: LineColumn;
end: LineColumn;
}
export interface LocationByOffset {
[offset: number]: LineColumn;
}
export interface FunctionDescription {
name: string;
line: number;
loc: SingleFileSourceRange;
skip?: boolean;
}
export type StatementDescription = SingleFileSourceRange;
export interface BranchDescription {
line: number;
type: 'if' | 'switch' | 'cond-expr' | 'binary-expr';
locations: SingleFileSourceRange[];
}
export interface FnMap {
[functionId: string]: FunctionDescription;
}
export interface BranchMap {
[branchId: string]: BranchDescription;
}
export interface StatementMap {
[statementId: string]: StatementDescription;
}
export interface LineCoverage {
[lineNo: number]: boolean;
}
export interface FunctionCoverage {
[functionId: string]: boolean;
}
export interface StatementCoverage {
[statementId: string]: boolean;
}
export interface BranchCoverage {
[branchId: string]: boolean[];
}
export interface Coverage {
[fineName: string]: {
l: LineCoverage;
f: FunctionCoverage;
s: StatementCoverage;
b: BranchCoverage;
fnMap: FnMap;
branchMap: BranchMap;
statementMap: StatementMap;
path: string;
};
}
export interface ContractData {
bytecode: string;
sourceMap: string;
runtimeBytecode: string;
sourceMapRuntime: string;
sourceCodes: string[];
baseName: string;
sources: string[];
}
export interface TraceInfo {
coveredPcs: number[];
txHash: string;
}

View File

@@ -0,0 +1,13 @@
import { LineColumn, SingleFileSourceRange } from './types';
export const utils = {
compareLineColumn(lhs: LineColumn, rhs: LineColumn): number {
return lhs.line !== rhs.line ? lhs.line - rhs.line : lhs.column - rhs.column;
},
isRangeInside(childRange: SingleFileSourceRange, parentRange: SingleFileSourceRange): boolean {
return (
utils.compareLineColumn(parentRange.start, childRange.start) <= 0 &&
utils.compareLineColumn(childRange.end, parentRange.end) <= 0
);
},
};

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "lib"
},
"include": ["./src/**/*", "../../node_modules/web3-typescript-typings/index.d.ts"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": ["@0xproject/tslint-config"]
}

View File

@@ -3,6 +3,9 @@
## v0.7.0 - _March 8, 2018_
* Updated legerco packages. Removed node-hid package as a dependency and make it an optional dependency. It is still used in integration tests but is causing problems for users on Linux distros. (#437)
* Export `GanacheSubprovider` and `Subprovider` (#426)
* Make all subproviders to derive from `Subprovider` (#426)
* Add types for `NextCallback`, `OnNextCompleted` (#426)
## v0.6.0 - _March 4, 2018_

View File

@@ -23,10 +23,12 @@
"@0xproject/utils": "^0.4.1",
"@ledgerhq/hw-app-eth": "^4.3.0",
"@ledgerhq/hw-transport-u2f": "^4.3.0",
"@0xproject/deployer": "^0.1.0",
"bn.js": "^4.11.8",
"es6-promisify": "^5.0.0",
"ethereumjs-tx": "^1.3.3",
"ethereumjs-util": "^5.1.1",
"ganache-core": "^2.0.2",
"hdkey": "^0.7.1",
"lodash": "^4.17.4",
"semaphore-async-await": "^1.5.1",

View File

@@ -134,3 +134,18 @@ declare module 'hdkey' {
}
export = HDNode;
}
// hdkey declarations
declare module 'ganache-core' {
import * as Web3 from 'web3';
export interface GanacheOpts {
verbose: boolean;
logger: {
log(msg: string): void;
};
port: number;
networkId: number;
mnemonic: string;
}
export function provider(opts: GanacheOpts): Web3.Provider;
}

View File

@@ -2,12 +2,15 @@ import Eth from '@ledgerhq/hw-app-eth';
import TransportU2F from '@ledgerhq/hw-transport-u2f';
import { LedgerEthereumClient } from './types';
export { Callback, NextCallback } from './types';
export { EmptyWalletSubprovider } from './subproviders/empty_wallet_subprovider';
export { FakeGasEstimateSubprovider } from './subproviders/fake_gas_estimate_subprovider';
export { InjectedWeb3Subprovider } from './subproviders/injected_web3';
export { RedundantRPCSubprovider } from './subproviders/redundant_rpc';
export { LedgerSubprovider } from './subproviders/ledger';
export { GanacheSubprovider } from './subproviders/ganache';
export { Subprovider } from './subproviders/subprovider';
export { NonceTrackerSubprovider } from './subproviders/nonce_tracker';
export { ECSignature, LedgerWalletSubprovider, LedgerCommunicationClient, NonceSubproviderErrors } from './types';

View File

@@ -1,14 +1,20 @@
import { JSONRPCPayload } from '@0xproject/types';
import * as Web3 from 'web3';
import { Subprovider } from './subprovider';
/*
* This class implements the web3-provider-engine subprovider interface and returns
* that the provider has no addresses when queried.
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class EmptyWalletSubprovider {
export class EmptyWalletSubprovider extends Subprovider {
// This method needs to be here to satisfy the interface but linter wants it to be static.
// tslint:disable-next-line:prefer-function-over-method
public handleRequest(payload: JSONRPCPayload, next: () => void, end: (err: Error | null, result: any) => void) {
public handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: () => void,
end: (err: Error | null, result: any) => void,
) {
switch (payload.method) {
case 'eth_accounts':
end(null, []);
@@ -19,9 +25,4 @@ export class EmptyWalletSubprovider {
return;
}
}
// Required to implement this method despite not needing it for this subprovider
// tslint:disable-next-line:prefer-function-over-method
public setEngine(engine: any) {
// noop
}
}

View File

@@ -1,4 +1,6 @@
import { JSONRPCPayload } from '@0xproject/types';
import * as Web3 from 'web3';
import { Subprovider } from './subprovider';
/*
* This class implements the web3-provider-engine subprovider interface and returns
@@ -8,14 +10,19 @@ import { JSONRPCPayload } from '@0xproject/types';
* Source: https://github.com/trufflesuite/ganache-cli/issues/437
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class FakeGasEstimateSubprovider {
export class FakeGasEstimateSubprovider extends Subprovider {
private _constantGasAmount: number;
constructor(constantGasAmount: number) {
super();
this._constantGasAmount = constantGasAmount;
}
// This method needs to be here to satisfy the interface but linter wants it to be static.
// tslint:disable-next-line:prefer-function-over-method
public handleRequest(payload: JSONRPCPayload, next: () => void, end: (err: Error | null, result: any) => void) {
public handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: () => void,
end: (err: Error | null, result: any) => void,
) {
switch (payload.method) {
case 'eth_estimateGas':
end(null, this._constantGasAmount);
@@ -26,9 +33,4 @@ export class FakeGasEstimateSubprovider {
return;
}
}
// Required to implement this method despite not needing it for this subprovider
// tslint:disable-next-line:prefer-function-over-method
public setEngine(engine: any) {
// noop
}
}

View File

@@ -0,0 +1,28 @@
import * as Ganache from 'ganache-core';
import * as Web3 from 'web3';
import { Subprovider } from './subprovider';
/*
* This class implements the web3-provider-engine subprovider interface and returns
* the provider connected to a in-process ganache.
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class GanacheSubprovider extends Subprovider {
private _ganacheProvider: Web3.Provider;
constructor(opts: any) {
super();
this._ganacheProvider = Ganache.provider(opts);
}
// This method needs to be here to satisfy the interface but linter wants it to be static.
// tslint:disable-next-line:prefer-function-over-method
public handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: () => void,
end: (err: Error | null, result: any) => void,
) {
this._ganacheProvider.sendAsync(payload, (err: Error | null, result: any) => {
end(err, result && result.result);
});
}
}

View File

@@ -1,5 +1,7 @@
import * as _ from 'lodash';
import Web3 = require('web3');
import * as Web3 from 'web3';
import { Subprovider } from './subprovider';
/*
* This class implements the web3-provider-engine subprovider interface and forwards
@@ -7,9 +9,10 @@ import Web3 = require('web3');
* provider instance in their browser.
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class InjectedWeb3Subprovider {
export class InjectedWeb3Subprovider extends Subprovider {
private _injectedWeb3: Web3;
constructor(subprovider: Web3.Provider) {
super();
this._injectedWeb3 = new Web3(subprovider);
}
public handleRequest(
@@ -40,11 +43,4 @@ export class InjectedWeb3Subprovider {
return;
}
}
// Required to implement this method despite not needing it for this subprovider
// The engine argument type should be Web3ProviderEngine, but we've decided to keep it as type any
// to remove the provider engine depdency given this method is a noop
// tslint:disable-next-line:prefer-function-over-method
public setEngine(engine: any) {
// noop
}
}

View File

@@ -5,7 +5,7 @@ import ethUtil = require('ethereumjs-util');
import HDNode = require('hdkey');
import * as _ from 'lodash';
import Semaphore from 'semaphore-async-await';
import Web3 = require('web3');
import * as Web3 from 'web3';
import {
LedgerEthereumClient,
@@ -147,7 +147,7 @@ export class LedgerSubprovider extends Subprovider {
hdKey.publicKey = new Buffer(ledgerResponse.publicKey, 'hex');
hdKey.chainCode = new Buffer(ledgerResponse.chainCode, 'hex');
const accounts = [];
const accounts: string[] = [];
for (let i = 0; i < numberOfAccounts; i++) {
const derivedHDNode = hdKey.derive(`m/${i + this._derivationPathIndex}`);
const derivedPublicKey = derivedHDNode.publicKey;

View File

@@ -1,12 +1,12 @@
import * as _ from 'lodash';
import { BlockParamLiteral } from '@0xproject/types';
import EthereumTx = require('ethereumjs-tx');
import ethUtil = require('ethereumjs-util');
import * as Web3 from 'web3';
import providerEngineUtils = require('web3-provider-engine/util/rpc-cache-utils');
import { BlockParamLiteral } from '@0xproject/types';
import { ErrorCallback, JSONRPCPayload, NonceSubproviderErrors, OptionalNextCallback } from '../types';
import { Callback, ErrorCallback, NextCallback, NonceSubproviderErrors } from '../types';
import { Subprovider } from './subprovider';
@@ -19,7 +19,7 @@ const NONCE_TOO_LOW_ERROR_MESSAGE = 'Transaction nonce is too low';
*/
export class NonceTrackerSubprovider extends Subprovider {
private _nonceCache: { [address: string]: string } = {};
private static _reconstructTransaction(payload: JSONRPCPayload): EthereumTx {
private static _reconstructTransaction(payload: Web3.JSONRPCRequestPayload): EthereumTx {
const raw = payload.params[0];
if (_.isUndefined(raw)) {
throw new Error(NonceSubproviderErrors.EmptyParametersFound);
@@ -28,7 +28,7 @@ export class NonceTrackerSubprovider extends Subprovider {
const transaction = new EthereumTx(rawData);
return transaction;
}
private static _determineAddress(payload: JSONRPCPayload): string {
private static _determineAddress(payload: Web3.JSONRPCRequestPayload): string {
let address: string;
switch (payload.method) {
case 'eth_getTransactionCount':
@@ -48,7 +48,11 @@ export class NonceTrackerSubprovider extends Subprovider {
}
// Required to implement this public interface which doesn't conform to our linting rule.
// tslint:disable-next-line:async-suffix
public async handleRequest(payload: JSONRPCPayload, next: OptionalNextCallback, end: ErrorCallback): Promise<void> {
public async handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: NextCallback,
end: ErrorCallback,
): Promise<void> {
switch (payload.method) {
case 'eth_getTransactionCount':
const requestDefaultBlock = providerEngineUtils.blockTagForPayload(payload);
@@ -58,7 +62,7 @@ export class NonceTrackerSubprovider extends Subprovider {
if (!_.isUndefined(cachedResult)) {
return end(null, cachedResult);
} else {
return next((requestError: Error | null, requestResult: any, cb: any) => {
return next((requestError: Error | null, requestResult: any, cb: Callback) => {
if (_.isNull(requestError)) {
this._nonceCache[address] = requestResult as string;
}
@@ -69,7 +73,7 @@ export class NonceTrackerSubprovider extends Subprovider {
return next();
}
case 'eth_sendRawTransaction':
return next((sendTransactionError: Error | null, txResult: any, cb: any) => {
return next((sendTransactionError: Error | null, txResult: any, cb: Callback) => {
if (_.isNull(sendTransactionError)) {
this._handleSuccessfulTransaction(payload);
} else {
@@ -81,7 +85,7 @@ export class NonceTrackerSubprovider extends Subprovider {
return next();
}
}
private _handleSuccessfulTransaction(payload: JSONRPCPayload): void {
private _handleSuccessfulTransaction(payload: Web3.JSONRPCRequestPayload): void {
const address = NonceTrackerSubprovider._determineAddress(payload);
const transaction = NonceTrackerSubprovider._reconstructTransaction(payload);
// Increment the nonce from the previous successfully submitted transaction
@@ -94,7 +98,7 @@ export class NonceTrackerSubprovider extends Subprovider {
const nextPrefixedHexNonce = `0x${nextHexNonce}`;
this._nonceCache[address] = nextPrefixedHexNonce;
}
private _handleSendTransactionError(payload: JSONRPCPayload, err: Error): void {
private _handleSendTransactionError(payload: Web3.JSONRPCRequestPayload, err: Error): void {
const address = NonceTrackerSubprovider._determineAddress(payload);
if (this._nonceCache[address] && _.includes(err.message, NONCE_TOO_LOW_ERROR_MESSAGE)) {
delete this._nonceCache[address];

View File

@@ -1,16 +1,15 @@
import { promisify } from '@0xproject/utils';
import * as _ from 'lodash';
import * as Web3 from 'web3';
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
import { JSONRPCPayload } from '../types';
import { Subprovider } from './subprovider';
export class RedundantRPCSubprovider extends Subprovider {
private _rpcs: RpcSubprovider[];
private static async _firstSuccessAsync(
rpcs: RpcSubprovider[],
payload: JSONRPCPayload,
payload: Web3.JSONRPCRequestPayload,
next: () => void,
): Promise<any> {
let lastErr: Error | undefined;
@@ -38,7 +37,7 @@ export class RedundantRPCSubprovider extends Subprovider {
// Required to implement this public interface which doesn't conform to our linting rule.
// tslint:disable-next-line:async-suffix
public async handleRequest(
payload: JSONRPCPayload,
payload: Web3.JSONRPCRequestPayload,
next: () => void,
end: (err: Error | null, data?: any) => void,
): Promise<void> {

Some files were not shown because too many files have changed in this diff Show More