Refactor contracts-core into contracts-multisig, contracts-core and contracts-test-utils
This commit is contained in:
1
contracts/test-utils/CHANGELOG.json
Normal file
1
contracts/test-utils/CHANGELOG.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
129
contracts/test-utils/README.md
Normal file
129
contracts/test-utils/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
## Contracts
|
||||
|
||||
Smart contracts that implement the 0x protocol. Addresses of the deployed contracts can be found in the 0x [wiki](https://0xproject.com/wiki#Deployed-Addresses) or the [CHANGELOG](./CHANGELOG.json) of this package.
|
||||
|
||||
## Usage
|
||||
|
||||
Contracts that make up and interact with version 2.0.0 of the protocol can be found in the [contracts](./contracts) directory. The contents of this directory are broken down into the following subdirectories:
|
||||
|
||||
* [protocol](./contracts/protocol)
|
||||
* This directory contains the contracts that make up version 2.0.0. A full specification can be found [here](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md).
|
||||
* [extensions](./contracts/extensions)
|
||||
* This directory contains contracts that interact with the 2.0.0 contracts and will be used in production, such as the [Forwarder](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md) contract.
|
||||
* [examples](./contracts/examples)
|
||||
* This directory contains example implementations of contracts that interact with the protocol but are _not_ intended for use in production. Examples include [filter](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#filter-contracts) contracts, a [Wallet](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#wallet) contract, and a [Validator](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#validator) contract, among others.
|
||||
* [tokens](./contracts/tokens)
|
||||
* This directory contains implementations of different tokens and token standards, including [wETH](https://weth.io/), ZRX, [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md), and [ERC721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md).
|
||||
* [multisig](./contracts/multisig)
|
||||
* This directory contains the [Gnosis MultiSigWallet](https://github.com/gnosis/MultiSigWallet) and a custom extension that adds a timelock to transactions within the MultiSigWallet.
|
||||
* [utils](./contracts/utils)
|
||||
* This directory contains libraries and utils that are shared across all of the other directories.
|
||||
* [test](./contracts/test)
|
||||
* This directory contains mocks and other contracts that are used solely for testing contracts within the other directories.
|
||||
|
||||
## Bug bounty
|
||||
|
||||
A bug bounty for the 2.0.0 contracts is ongoing! Instructions can be found [here](https://0xproject.com/wiki#Bug-Bounty).
|
||||
|
||||
## Contributing
|
||||
|
||||
We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
|
||||
|
||||
For proposals regarding the 0x protocol's smart contract architecture, message format, or additional functionality, go to the [0x Improvement Proposals (ZEIPs)](https://github.com/0xProject/ZEIPs) repository and follow the contribution guidelines provided therein.
|
||||
|
||||
Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
|
||||
|
||||
```bash
|
||||
yarn config set workspaces-experimental true
|
||||
```
|
||||
|
||||
Then install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory:
|
||||
|
||||
```bash
|
||||
PKG=contracts yarn build
|
||||
```
|
||||
|
||||
Or continuously rebuild on change:
|
||||
|
||||
```bash
|
||||
PKG=contracts yarn watch
|
||||
```
|
||||
|
||||
### Clean
|
||||
|
||||
```bash
|
||||
yarn clean
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
#### Testing options
|
||||
|
||||
###### Revert stack traces
|
||||
|
||||
If you want to see helpful stack traces (incl. line number, code snippet) for smart contract reverts, run the tests with:
|
||||
|
||||
```
|
||||
yarn test:trace
|
||||
```
|
||||
|
||||
**Note:** This currently slows down the test runs and is therefore not enabled by default.
|
||||
|
||||
###### Backing Ethereum node
|
||||
|
||||
By default, our tests run against an in-process [Ganache](https://github.com/trufflesuite/ganache-core) instance. In order to run the tests against [Geth](https://github.com/ethereum/go-ethereum), first follow the instructions in the README for the devnet package to start the devnet Geth node. Then run:
|
||||
|
||||
```bash
|
||||
TEST_PROVIDER=geth yarn test
|
||||
```
|
||||
|
||||
###### Code coverage
|
||||
|
||||
In order to see the Solidity code coverage output generated by `@0x/sol-cov`, run:
|
||||
|
||||
```
|
||||
yarn test:coverage
|
||||
```
|
||||
|
||||
###### Gas profiler
|
||||
|
||||
In order to profile the gas costs for a specific smart contract call/transaction, you can run the tests in `profiler` mode.
|
||||
|
||||
**Note:** Traces emitted by ganache have incorrect gas costs so we recommend using Geth for profiling.
|
||||
|
||||
```
|
||||
TEST_PROVIDER=geth yarn test:profiler
|
||||
```
|
||||
|
||||
You'll see a warning that you need to explicitly enable and disable the profiler before and after the block of code you want to profile.
|
||||
|
||||
```typescript
|
||||
import { profiler } from './utils/profiler';
|
||||
profiler.start();
|
||||
// Some call to a smart contract
|
||||
profiler.stop();
|
||||
```
|
||||
|
||||
Without explicitly starting and stopping the profiler, the profiler output will be too busy, and therefore unusable.
|
76
contracts/test-utils/package.json
Normal file
76
contracts/test-utils/package.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "@0x/contracts-test-utils",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=6.12"
|
||||
},
|
||||
"description": "Test utils for 0x contracts",
|
||||
"main": "lib/src/index.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"build:ci": "yarn build",
|
||||
"test": "yarn run_mocha",
|
||||
"test:coverage": "run-s build run_mocha coverage:report:text coverage:report:lcov",
|
||||
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
|
||||
"clean": "shx rm -rf lib",
|
||||
"lint": "tslint --format stylish --project .",
|
||||
"coverage:report:text": "istanbul report text",
|
||||
"coverage:report:html": "istanbul report html && open coverage/index.html",
|
||||
"profiler:report:html": "istanbul report html && open coverage/index.html",
|
||||
"coverage:report:lcov": "istanbul report lcov",
|
||||
"test:circleci": "yarn test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/0xProject/0x-monorepo.git"
|
||||
},
|
||||
"author": "Amir Bandeali",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/0xProject/0x-monorepo/issues"
|
||||
},
|
||||
"homepage": "https://github.com/0xProject/0x-monorepo/contracts/test-utils/README.md",
|
||||
"devDependencies": {
|
||||
"@0x/abi-gen": "^1.0.17",
|
||||
"@0x/dev-utils": "^1.0.18",
|
||||
"@0x/sol-compiler": "^1.1.13",
|
||||
"@0x/sol-cov": "^2.1.13",
|
||||
"@0x/subproviders": "^2.1.5",
|
||||
"@0x/tslint-config": "^1.0.10",
|
||||
"@types/bn.js": "^4.11.0",
|
||||
"@types/ethereumjs-abi": "^0.6.0",
|
||||
"@types/lodash": "4.14.104",
|
||||
"@types/node": "*",
|
||||
"chai": "^4.0.1",
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-bignumber": "^2.0.1",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"make-promises-safe": "^1.1.0",
|
||||
"mocha": "^4.1.0",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"shx": "^0.2.2",
|
||||
"tslint": "5.11.0",
|
||||
"typescript": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@0x/order-utils": "^3.0.3",
|
||||
"@0x/types": "^1.3.0",
|
||||
"@0x/typescript-typings": "^3.0.4",
|
||||
"@0x/utils": "^2.0.6",
|
||||
"@0x/web3-wrapper": "^3.1.5",
|
||||
"@types/js-combinatorics": "^0.5.29",
|
||||
"bn.js": "^4.11.8",
|
||||
"ethereum-types": "^1.1.2",
|
||||
"ethereumjs-abi": "0.6.5",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"ethers": "~4.0.4",
|
||||
"js-combinatorics": "^0.5.3",
|
||||
"lodash": "^4.17.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
3
contracts/test-utils/src/abstract_asset_wrapper.ts
Normal file
3
contracts/test-utils/src/abstract_asset_wrapper.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export abstract class AbstractAssetWrapper {
|
||||
public abstract getProxyId(): string;
|
||||
}
|
11
contracts/test-utils/src/address_utils.ts
Normal file
11
contracts/test-utils/src/address_utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { generatePseudoRandomSalt } from '@0x/order-utils';
|
||||
import { crypto } from '@0x/order-utils/lib/src/crypto';
|
||||
|
||||
export const addressUtils = {
|
||||
generatePseudoRandomAddress(): string {
|
||||
const randomBigNum = generatePseudoRandomSalt();
|
||||
const randomBuff = crypto.solSHA3([randomBigNum]);
|
||||
const randomAddress = `0x${randomBuff.slice(0, 20).toString('hex')}`;
|
||||
return randomAddress;
|
||||
},
|
||||
};
|
199
contracts/test-utils/src/assertions.ts
Normal file
199
contracts/test-utils/src/assertions.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { RevertReason } from '@0x/types';
|
||||
import { logUtils } from '@0x/utils';
|
||||
import { NodeType } from '@0x/web3-wrapper';
|
||||
import * as chai from 'chai';
|
||||
import { TransactionReceipt, TransactionReceiptStatus, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { web3Wrapper } from './web3_wrapper';
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
let nodeType: NodeType | undefined;
|
||||
|
||||
// Represents the return value of a `sendTransaction` call. The Promise should
|
||||
// resolve with either a transaction receipt or a transaction hash.
|
||||
export type sendTransactionResult = Promise<TransactionReceipt | TransactionReceiptWithDecodedLogs | string>;
|
||||
|
||||
/**
|
||||
* Returns ganacheError if the backing Ethereum node is Ganache and gethError
|
||||
* if it is Geth.
|
||||
* @param ganacheError the error to be returned if the backing node is Ganache.
|
||||
* @param gethError the error to be returned if the backing node is Geth.
|
||||
* @returns either the given ganacheError or gethError depending on the backing
|
||||
* node.
|
||||
*/
|
||||
async function _getGanacheOrGethError(ganacheError: string, gethError: string): Promise<string> {
|
||||
if (_.isUndefined(nodeType)) {
|
||||
nodeType = await web3Wrapper.getNodeTypeAsync();
|
||||
}
|
||||
switch (nodeType) {
|
||||
case NodeType.Ganache:
|
||||
return ganacheError;
|
||||
case NodeType.Geth:
|
||||
return gethError;
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${nodeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function _getInsufficientFundsErrorMessageAsync(): Promise<string> {
|
||||
return _getGanacheOrGethError("sender doesn't have enough funds", 'insufficient funds');
|
||||
}
|
||||
|
||||
async function _getTransactionFailedErrorMessageAsync(): Promise<string> {
|
||||
return _getGanacheOrGethError('revert', 'always failing transaction');
|
||||
}
|
||||
|
||||
async function _getContractCallFailedErrorMessageAsync(): Promise<string> {
|
||||
return _getGanacheOrGethError('revert', 'Contract call failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected error message for an 'invalid opcode' resulting from a
|
||||
* contract call. The exact error message depends on the backing Ethereum node.
|
||||
*/
|
||||
export async function getInvalidOpcodeErrorMessageForCallAsync(): Promise<string> {
|
||||
return _getGanacheOrGethError('invalid opcode', 'Contract call failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected error message for the given revert reason resulting from
|
||||
* a sendTransaction call. The exact error message depends on the backing
|
||||
* Ethereum node and whether it supports revert reasons.
|
||||
* @param reason a specific revert reason.
|
||||
* @returns the expected error message.
|
||||
*/
|
||||
export async function getRevertReasonOrErrorMessageForSendTransactionAsync(reason: RevertReason): Promise<string> {
|
||||
return _getGanacheOrGethError(reason, 'always failing transaction');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects if the given Promise does not reject with an error indicating
|
||||
* insufficient funds.
|
||||
* @param p a promise resulting from a contract call or sendTransaction call.
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectInsufficientFundsAsync<T>(p: Promise<T>): Promise<void> {
|
||||
const errMessage = await _getInsufficientFundsErrorMessageAsync();
|
||||
return expect(p).to.be.rejectedWith(errMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves if the the sendTransaction call fails with the given revert reason.
|
||||
* However, since Geth does not support revert reasons for sendTransaction, this
|
||||
* falls back to expectTransactionFailedWithoutReasonAsync if the backing
|
||||
* Ethereum node is Geth.
|
||||
* @param p a Promise resulting from a sendTransaction call
|
||||
* @param reason a specific revert reason
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectTransactionFailedAsync(p: sendTransactionResult, reason: RevertReason): Promise<void> {
|
||||
// HACK(albrow): This dummy `catch` should not be necessary, but if you
|
||||
// remove it, there is an uncaught exception and the Node process will
|
||||
// forcibly exit. It's possible this is a false positive in
|
||||
// make-promises-safe.
|
||||
p.catch(e => {
|
||||
_.noop(e);
|
||||
});
|
||||
|
||||
if (_.isUndefined(nodeType)) {
|
||||
nodeType = await web3Wrapper.getNodeTypeAsync();
|
||||
}
|
||||
switch (nodeType) {
|
||||
case NodeType.Ganache:
|
||||
return expect(p).to.be.rejectedWith(reason);
|
||||
case NodeType.Geth:
|
||||
logUtils.warn(
|
||||
'WARNING: Geth does not support revert reasons for sendTransaction. This test will pass if the transaction fails for any reason.',
|
||||
);
|
||||
return expectTransactionFailedWithoutReasonAsync(p);
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${nodeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves if the transaction fails without a revert reason, or if the
|
||||
* corresponding transactionReceipt has a status of 0 or '0', indicating
|
||||
* failure.
|
||||
* @param p a Promise resulting from a sendTransaction call
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectTransactionFailedWithoutReasonAsync(p: sendTransactionResult): Promise<void> {
|
||||
return p
|
||||
.then(async result => {
|
||||
let txReceiptStatus: TransactionReceiptStatus;
|
||||
if (_.isString(result)) {
|
||||
// Result is a txHash. We need to make a web3 call to get the
|
||||
// receipt, then get the status from the receipt.
|
||||
const txReceipt = await web3Wrapper.awaitTransactionMinedAsync(result);
|
||||
txReceiptStatus = txReceipt.status;
|
||||
} else if ('status' in result) {
|
||||
// Result is a transaction receipt, so we can get the status
|
||||
// directly.
|
||||
txReceiptStatus = result.status;
|
||||
} else {
|
||||
throw new Error('Unexpected result type: ' + typeof result);
|
||||
}
|
||||
expect(_.toString(txReceiptStatus)).to.equal(
|
||||
'0',
|
||||
'Expected transaction to fail but receipt had a non-zero status, indicating success',
|
||||
);
|
||||
})
|
||||
.catch(async err => {
|
||||
// If the promise rejects, we expect a specific error message,
|
||||
// depending on the backing Ethereum node type.
|
||||
const errMessage = await _getTransactionFailedErrorMessageAsync();
|
||||
expect(err.message).to.include(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves if the the contract call fails with the given revert reason.
|
||||
* @param p a Promise resulting from a contract call
|
||||
* @param reason a specific revert reason
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectContractCallFailedAsync<T>(p: Promise<T>, reason: RevertReason): Promise<void> {
|
||||
return expect(p).to.be.rejectedWith(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves if the contract call fails without a revert reason.
|
||||
* @param p a Promise resulting from a contract call
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectContractCallFailedWithoutReasonAsync<T>(p: Promise<T>): Promise<void> {
|
||||
const errMessage = await _getContractCallFailedErrorMessageAsync();
|
||||
return expect(p).to.be.rejectedWith(errMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves if the contract creation/deployment fails without a revert reason.
|
||||
* @param p a Promise resulting from a contract creation/deployment
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectContractCreationFailedAsync<T>(
|
||||
p: sendTransactionResult,
|
||||
reason: RevertReason,
|
||||
): Promise<void> {
|
||||
return expectTransactionFailedAsync(p, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves if the contract creation/deployment fails without a revert reason.
|
||||
* @param p a Promise resulting from a contract creation/deployment
|
||||
* @returns a new Promise which will reject if the conditions are not met and
|
||||
* otherwise resolve with no value.
|
||||
*/
|
||||
export async function expectContractCreationFailedWithoutReasonAsync<T>(p: Promise<T>): Promise<void> {
|
||||
const errMessage = await _getTransactionFailedErrorMessageAsync();
|
||||
return expect(p).to.be.rejectedWith(errMessage);
|
||||
}
|
43
contracts/test-utils/src/block_timestamp.ts
Normal file
43
contracts/test-utils/src/block_timestamp.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from './constants';
|
||||
import { web3Wrapper } from './web3_wrapper';
|
||||
|
||||
let firstAccount: string | undefined;
|
||||
|
||||
/**
|
||||
* Increases time by the given number of seconds and then mines a block so that
|
||||
* the current block timestamp has the offset applied.
|
||||
* @param seconds the number of seconds by which to incrase the time offset.
|
||||
* @returns a new Promise which will resolve with the new total time offset or
|
||||
* reject if the time could not be increased.
|
||||
*/
|
||||
export async function increaseTimeAndMineBlockAsync(seconds: number): Promise<number> {
|
||||
if (_.isUndefined(firstAccount)) {
|
||||
const accounts = await web3Wrapper.getAvailableAddressesAsync();
|
||||
firstAccount = accounts[0];
|
||||
}
|
||||
|
||||
const offset = await web3Wrapper.increaseTimeAsync(seconds);
|
||||
// Note: we need to send a transaction after increasing time so
|
||||
// that a block is actually mined. The contract looks at the
|
||||
// last mined block for the timestamp.
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(
|
||||
await web3Wrapper.sendTransactionAsync({ from: firstAccount, to: firstAccount, value: 0 }),
|
||||
constants.AWAIT_TRANSACTION_MINED_MS,
|
||||
);
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the latest block in seconds since the Unix epoch.
|
||||
* @returns a new Promise which will resolve with the timestamp in seconds.
|
||||
*/
|
||||
export async function getLatestBlockTimestampAsync(): Promise<number> {
|
||||
const currentBlockIfExists = await web3Wrapper.getBlockIfExistsAsync('latest');
|
||||
if (_.isUndefined(currentBlockIfExists)) {
|
||||
throw new Error(`Unable to fetch latest block.`);
|
||||
}
|
||||
return currentBlockIfExists.timestamp;
|
||||
}
|
13
contracts/test-utils/src/chai_setup.ts
Normal file
13
contracts/test-utils/src/chai_setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as chai from 'chai';
|
||||
import chaiAsPromised = require('chai-as-promised');
|
||||
import ChaiBigNumber = require('chai-bignumber');
|
||||
import * as dirtyChai from 'dirty-chai';
|
||||
|
||||
export const chaiSetup = {
|
||||
configure(): void {
|
||||
chai.config.includeStack = true;
|
||||
chai.use(ChaiBigNumber());
|
||||
chai.use(dirtyChai);
|
||||
chai.use(chaiAsPromised);
|
||||
},
|
||||
};
|
113
contracts/test-utils/src/combinatorial_utils.ts
Normal file
113
contracts/test-utils/src/combinatorial_utils.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as combinatorics from 'js-combinatorics';
|
||||
|
||||
import { testWithReferenceFuncAsync } from './test_with_reference';
|
||||
|
||||
// A set of values corresponding to the uint256 type in Solidity. This set
|
||||
// contains some notable edge cases, including some values which will overflow
|
||||
// the uint256 type when used in different mathematical operations.
|
||||
export const uint256Values = [
|
||||
new BigNumber(0),
|
||||
new BigNumber(1),
|
||||
new BigNumber(2),
|
||||
// Non-trivial big number.
|
||||
new BigNumber(2).pow(64),
|
||||
// Max that does not overflow when squared.
|
||||
new BigNumber(2).pow(128).minus(1),
|
||||
// Min that does overflow when squared.
|
||||
new BigNumber(2).pow(128),
|
||||
// Max that does not overflow when doubled.
|
||||
new BigNumber(2).pow(255).minus(1),
|
||||
// Min that does overflow when doubled.
|
||||
new BigNumber(2).pow(255),
|
||||
// Max that does not overflow.
|
||||
new BigNumber(2).pow(256).minus(1),
|
||||
];
|
||||
|
||||
// A set of values corresponding to the bytes32 type in Solidity.
|
||||
export const bytes32Values = [
|
||||
// Min
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000001',
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000002',
|
||||
// Non-trivial big number.
|
||||
'0x000000000000f000000000000000000000000000000000000000000000000000',
|
||||
// Max
|
||||
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
];
|
||||
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
allValues: [P0[], P1[]],
|
||||
): Promise<void>;
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
allValues: [P0[], P1[], P2[]],
|
||||
): Promise<void>;
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
allValues: [P0[], P1[], P2[], P3[]],
|
||||
): Promise<void>;
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
allValues: [P0[], P1[], P2[], P3[], P4[]],
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Uses combinatorics to test the behavior of a test function by comparing it to
|
||||
* the expected behavior (defined by a reference function) for a large number of
|
||||
* possible input values.
|
||||
*
|
||||
* First generates test cases by taking the cartesian product of the given
|
||||
* values. Each test case is a set of N values corresponding to the N arguments
|
||||
* for the test func and the reference func. For each test case, first the
|
||||
* reference function will be called to obtain an "expected result", or if the
|
||||
* reference function throws/rejects, an "expected error". Next, the test
|
||||
* function will be called to obtain an "actual result", or if the test function
|
||||
* throws/rejects, an "actual error". Each test case passes if at least one of
|
||||
* the following conditions is met:
|
||||
*
|
||||
* 1) Neither the reference function or the test function throw and the
|
||||
* "expected result" equals the "actual result".
|
||||
*
|
||||
* 2) Both the reference function and the test function throw and the "actual
|
||||
* error" message *contains* the "expected error" message.
|
||||
*
|
||||
* The first test case which does not meet one of these conditions will cause
|
||||
* the entire test to fail and this function will throw/reject.
|
||||
*
|
||||
* @param referenceFuncAsync a reference function implemented in pure
|
||||
* JavaScript/TypeScript which accepts N arguments and returns the "expected
|
||||
* result" or "expected error" for a given test case.
|
||||
* @param testFuncAsync a test function which, e.g., makes a call or sends a
|
||||
* transaction to a contract. It accepts the same N arguments returns the
|
||||
* "actual result" or "actual error" for a given test case.
|
||||
* @param values an array of N arrays. Each inner array is a set of possible
|
||||
* values which are passed into both the reference function and the test
|
||||
* function.
|
||||
* @return A Promise that resolves if the test passes and rejects if the test
|
||||
* fails, according to the rules described above.
|
||||
*/
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync(
|
||||
name: string,
|
||||
referenceFuncAsync: (...args: any[]) => Promise<any>,
|
||||
testFuncAsync: (...args: any[]) => Promise<any>,
|
||||
allValues: any[],
|
||||
): Promise<void> {
|
||||
const testCases = combinatorics.cartesianProduct(...allValues);
|
||||
let counter = 0;
|
||||
testCases.forEach(async testCase => {
|
||||
counter += 1;
|
||||
it(`${name} ${counter}/${testCases.length}`, async () => {
|
||||
await testWithReferenceFuncAsync(referenceFuncAsync, testFuncAsync, testCase as any);
|
||||
});
|
||||
});
|
||||
}
|
67
contracts/test-utils/src/constants.ts
Normal file
67
contracts/test-utils/src/constants.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
const TESTRPC_PRIVATE_KEYS_STRINGS = [
|
||||
'0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d',
|
||||
'0x5d862464fe9303452126c8bc94274b8c5f9874cbd219789b3eb2128075a76f72',
|
||||
'0xdf02719c4df8b9b8ac7f551fcb5d9ef48fa27eef7a66453879f4d8fdc6e78fb1',
|
||||
'0xff12e391b79415e941a94de3bf3a9aee577aed0731e297d5cfa0b8a1e02fa1d0',
|
||||
'0x752dd9cf65e68cfaba7d60225cbdbc1f4729dd5e5507def72815ed0d8abc6249',
|
||||
'0xefb595a0178eb79a8df953f87c5148402a224cdf725e88c0146727c6aceadccd',
|
||||
'0x83c6d2cc5ddcf9711a6d59b417dc20eb48afd58d45290099e5987e3d768f328f',
|
||||
'0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2',
|
||||
'0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f',
|
||||
'0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89',
|
||||
];
|
||||
|
||||
export const constants = {
|
||||
BASE_16: 16,
|
||||
INVALID_OPCODE: 'invalid opcode',
|
||||
TESTRPC_NETWORK_ID: 50,
|
||||
// Note(albrow): In practice V8 and most other engines limit the minimum
|
||||
// interval for setInterval to 10ms. We still set it to 0 here in order to
|
||||
// ensure we always use the minimum interval.
|
||||
AWAIT_TRANSACTION_MINED_MS: 0,
|
||||
MAX_ETHERTOKEN_WITHDRAW_GAS: 43000,
|
||||
MAX_EXECUTE_TRANSACTION_GAS: 1000000,
|
||||
MAX_TOKEN_TRANSFERFROM_GAS: 80000,
|
||||
MAX_TOKEN_APPROVE_GAS: 60000,
|
||||
MAX_TRANSFER_FROM_GAS: 150000,
|
||||
DUMMY_TOKEN_NAME: '',
|
||||
DUMMY_TOKEN_SYMBOL: '',
|
||||
DUMMY_TOKEN_DECIMALS: new BigNumber(18),
|
||||
DUMMY_TOKEN_TOTAL_SUPPLY: new BigNumber(0),
|
||||
NULL_BYTES: '0x',
|
||||
NUM_DUMMY_ERC20_TO_DEPLOY: 3,
|
||||
NUM_DUMMY_ERC721_TO_DEPLOY: 2,
|
||||
NUM_ERC721_TOKENS_TO_MINT: 2,
|
||||
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
|
||||
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
|
||||
TESTRPC_PRIVATE_KEYS: _.map(TESTRPC_PRIVATE_KEYS_STRINGS, privateKeyString => ethUtil.toBuffer(privateKeyString)),
|
||||
INITIAL_ERC20_BALANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18),
|
||||
INITIAL_ERC20_ALLOWANCE: Web3Wrapper.toBaseUnitAmount(new BigNumber(10000), 18),
|
||||
STATIC_ORDER_PARAMS: {
|
||||
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 18),
|
||||
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), 18),
|
||||
makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
|
||||
takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18),
|
||||
},
|
||||
WORD_LENGTH: 32,
|
||||
ZERO_AMOUNT: new BigNumber(0),
|
||||
PERCENTAGE_DENOMINATOR: new BigNumber(10).pow(18),
|
||||
FUNCTIONS_WITH_MUTEX: [
|
||||
'FILL_ORDER',
|
||||
'FILL_OR_KILL_ORDER',
|
||||
'BATCH_FILL_ORDERS',
|
||||
'BATCH_FILL_OR_KILL_ORDERS',
|
||||
'MARKET_BUY_ORDERS',
|
||||
'MARKET_SELL_ORDERS',
|
||||
'MATCH_ORDERS',
|
||||
'CANCEL_ORDER',
|
||||
'BATCH_CANCEL_ORDERS',
|
||||
'CANCEL_ORDERS_UP_TO',
|
||||
'SET_SIGNATURE_VALIDATOR_APPROVAL',
|
||||
],
|
||||
};
|
21
contracts/test-utils/src/coverage.ts
Normal file
21
contracts/test-utils/src/coverage.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { devConstants } from '@0x/dev-utils';
|
||||
import { CoverageSubprovider, SolCompilerArtifactAdapter } from '@0x/sol-cov';
|
||||
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 isVerbose = true;
|
||||
const subprovider = new CoverageSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose);
|
||||
return subprovider;
|
||||
},
|
||||
};
|
68
contracts/test-utils/src/formatters.ts
Normal file
68
contracts/test-utils/src/formatters.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from './constants';
|
||||
import { orderUtils } from './order_utils';
|
||||
import { BatchCancelOrders, BatchFillOrders, MarketBuyOrders, MarketSellOrders } from './types';
|
||||
|
||||
export const formatters = {
|
||||
createBatchFill(signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[] = []): BatchFillOrders {
|
||||
const batchFill: BatchFillOrders = {
|
||||
orders: [],
|
||||
signatures: [],
|
||||
takerAssetFillAmounts,
|
||||
};
|
||||
_.forEach(signedOrders, signedOrder => {
|
||||
const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
|
||||
batchFill.orders.push(orderWithoutExchangeAddress);
|
||||
batchFill.signatures.push(signedOrder.signature);
|
||||
if (takerAssetFillAmounts.length < signedOrders.length) {
|
||||
batchFill.takerAssetFillAmounts.push(signedOrder.takerAssetAmount);
|
||||
}
|
||||
});
|
||||
return batchFill;
|
||||
},
|
||||
createMarketSellOrders(signedOrders: SignedOrder[], takerAssetFillAmount: BigNumber): MarketSellOrders {
|
||||
const marketSellOrders: MarketSellOrders = {
|
||||
orders: [],
|
||||
signatures: [],
|
||||
takerAssetFillAmount,
|
||||
};
|
||||
_.forEach(signedOrders, (signedOrder, i) => {
|
||||
const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
|
||||
if (i !== 0) {
|
||||
orderWithoutExchangeAddress.takerAssetData = constants.NULL_BYTES;
|
||||
}
|
||||
marketSellOrders.orders.push(orderWithoutExchangeAddress);
|
||||
marketSellOrders.signatures.push(signedOrder.signature);
|
||||
});
|
||||
return marketSellOrders;
|
||||
},
|
||||
createMarketBuyOrders(signedOrders: SignedOrder[], makerAssetFillAmount: BigNumber): MarketBuyOrders {
|
||||
const marketBuyOrders: MarketBuyOrders = {
|
||||
orders: [],
|
||||
signatures: [],
|
||||
makerAssetFillAmount,
|
||||
};
|
||||
_.forEach(signedOrders, (signedOrder, i) => {
|
||||
const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
|
||||
if (i !== 0) {
|
||||
orderWithoutExchangeAddress.makerAssetData = constants.NULL_BYTES;
|
||||
}
|
||||
marketBuyOrders.orders.push(orderWithoutExchangeAddress);
|
||||
marketBuyOrders.signatures.push(signedOrder.signature);
|
||||
});
|
||||
return marketBuyOrders;
|
||||
},
|
||||
createBatchCancel(signedOrders: SignedOrder[]): BatchCancelOrders {
|
||||
const batchCancel: BatchCancelOrders = {
|
||||
orders: [],
|
||||
};
|
||||
_.forEach(signedOrders, signedOrder => {
|
||||
const orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
|
||||
batchCancel.orders.push(orderWithoutExchangeAddress);
|
||||
});
|
||||
return batchCancel;
|
||||
},
|
||||
};
|
15
contracts/test-utils/src/global_hooks.ts
Normal file
15
contracts/test-utils/src/global_hooks.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { env, EnvVars } from '@0x/dev-utils';
|
||||
|
||||
import { coverage } from './coverage';
|
||||
import { profiler } from './profiler';
|
||||
|
||||
after('generate coverage report', async () => {
|
||||
if (env.parseBoolean(EnvVars.SolidityCoverage)) {
|
||||
const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
|
||||
await coverageSubprovider.writeCoverageAsync();
|
||||
}
|
||||
if (env.parseBoolean(EnvVars.SolidityProfiler)) {
|
||||
const profilerSubprovider = profiler.getProfilerSubproviderSingleton();
|
||||
await profilerSubprovider.writeProfilerOutputAsync();
|
||||
}
|
||||
});
|
54
contracts/test-utils/src/index.ts
Normal file
54
contracts/test-utils/src/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export { AbstractAssetWrapper } from './abstract_asset_wrapper';
|
||||
export { chaiSetup } from './chai_setup';
|
||||
export { constants } from './constants';
|
||||
export {
|
||||
expectContractCallFailedAsync,
|
||||
expectContractCallFailedWithoutReasonAsync,
|
||||
expectContractCreationFailedAsync,
|
||||
expectContractCreationFailedWithoutReasonAsync,
|
||||
expectInsufficientFundsAsync,
|
||||
expectTransactionFailedAsync,
|
||||
sendTransactionResult,
|
||||
expectTransactionFailedWithoutReasonAsync,
|
||||
getInvalidOpcodeErrorMessageForCallAsync,
|
||||
getRevertReasonOrErrorMessageForSendTransactionAsync,
|
||||
} from './assertions';
|
||||
export { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from './block_timestamp';
|
||||
export { provider, txDefaults, web3Wrapper } from './web3_wrapper';
|
||||
export { LogDecoder } from './log_decoder';
|
||||
export { formatters } from './formatters';
|
||||
export { signingUtils } from './signing_utils';
|
||||
export { orderUtils } from './order_utils';
|
||||
export { typeEncodingUtils } from './type_encoding_utils';
|
||||
export { profiler } from './profiler';
|
||||
export { coverage } from './coverage';
|
||||
export { addressUtils } from './address_utils';
|
||||
export { OrderFactory } from './order_factory';
|
||||
export { TransactionFactory } from './transaction_factory';
|
||||
export { testWithReferenceFuncAsync } from './test_with_reference';
|
||||
export {
|
||||
MarketBuyOrders,
|
||||
MarketSellOrders,
|
||||
ERC721TokenIdsByOwner,
|
||||
SignedTransaction,
|
||||
OrderStatus,
|
||||
AllowanceAmountScenario,
|
||||
AssetDataScenario,
|
||||
BalanceAmountScenario,
|
||||
ContractName,
|
||||
ExpirationTimeSecondsScenario,
|
||||
TransferAmountsLoggedByMatchOrders,
|
||||
TransferAmountsByMatchOrders,
|
||||
OrderScenario,
|
||||
TraderStateScenario,
|
||||
TransactionDataParams,
|
||||
Token,
|
||||
FillScenario,
|
||||
FeeRecipientAddressScenario,
|
||||
OrderAssetAmountScenario,
|
||||
TakerAssetFillAmountScenario,
|
||||
TakerScenario,
|
||||
OrderInfo,
|
||||
ERC20BalancesByOwner,
|
||||
FillResults,
|
||||
} from './types';
|
51
contracts/test-utils/src/log_decoder.ts
Normal file
51
contracts/test-utils/src/log_decoder.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AbiDecoder, BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import {
|
||||
AbiDefinition,
|
||||
ContractArtifact,
|
||||
DecodedLogArgs,
|
||||
LogEntry,
|
||||
LogWithDecodedArgs,
|
||||
RawLog,
|
||||
TransactionReceiptWithDecodedLogs,
|
||||
} from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from './constants';
|
||||
|
||||
export class LogDecoder {
|
||||
private readonly _web3Wrapper: Web3Wrapper;
|
||||
private readonly _abiDecoder: AbiDecoder;
|
||||
public static wrapLogBigNumbers(log: any): any {
|
||||
const argNames = _.keys(log.args);
|
||||
for (const argName of argNames) {
|
||||
const isWeb3BigNumber = _.startsWith(log.args[argName].constructor.toString(), 'function BigNumber(');
|
||||
if (isWeb3BigNumber) {
|
||||
log.args[argName] = new BigNumber(log.args[argName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
constructor(web3Wrapper: Web3Wrapper, artifacts: { [contractName: string]: ContractArtifact }) {
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
const abiArrays: AbiDefinition[][] = [];
|
||||
_.forEach(artifacts, (artifact: ContractArtifact) => {
|
||||
const compilerOutput = artifact.compilerOutput;
|
||||
abiArrays.push(compilerOutput.abi);
|
||||
});
|
||||
this._abiDecoder = new AbiDecoder(abiArrays);
|
||||
}
|
||||
public decodeLogOrThrow<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog {
|
||||
const logWithDecodedArgsOrLog = this._abiDecoder.tryToDecodeLogOrNoop(log);
|
||||
// tslint:disable-next-line:no-unnecessary-type-assertion
|
||||
if (_.isUndefined((logWithDecodedArgsOrLog as LogWithDecodedArgs<ArgsType>).args)) {
|
||||
throw new Error(`Unable to decode log: ${JSON.stringify(log)}`);
|
||||
}
|
||||
LogDecoder.wrapLogBigNumbers(logWithDecodedArgsOrLog);
|
||||
return logWithDecodedArgsOrLog;
|
||||
}
|
||||
public async getTxWithDecodedLogsAsync(txHash: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const tx = await this._web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
|
||||
tx.logs = _.map(tx.logs, log => this.decodeLogOrThrow(log));
|
||||
return tx;
|
||||
}
|
||||
}
|
38
contracts/test-utils/src/order_factory.ts
Normal file
38
contracts/test-utils/src/order_factory.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { generatePseudoRandomSalt, orderHashUtils } from '@0x/order-utils';
|
||||
import { Order, SignatureType, SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { getLatestBlockTimestampAsync } from './block_timestamp';
|
||||
import { constants } from './constants';
|
||||
import { signingUtils } from './signing_utils';
|
||||
|
||||
export class OrderFactory {
|
||||
private readonly _defaultOrderParams: Partial<Order>;
|
||||
private readonly _privateKey: Buffer;
|
||||
constructor(privateKey: Buffer, defaultOrderParams: Partial<Order>) {
|
||||
this._defaultOrderParams = defaultOrderParams;
|
||||
this._privateKey = privateKey;
|
||||
}
|
||||
public async newSignedOrderAsync(
|
||||
customOrderParams: Partial<Order> = {},
|
||||
signatureType: SignatureType = SignatureType.EthSign,
|
||||
): Promise<SignedOrder> {
|
||||
const tenMinutesInSeconds = 10 * 60;
|
||||
const currentBlockTimestamp = await getLatestBlockTimestampAsync();
|
||||
const order = ({
|
||||
senderAddress: constants.NULL_ADDRESS,
|
||||
expirationTimeSeconds: new BigNumber(currentBlockTimestamp).add(tenMinutesInSeconds),
|
||||
salt: generatePseudoRandomSalt(),
|
||||
takerAddress: constants.NULL_ADDRESS,
|
||||
...this._defaultOrderParams,
|
||||
...customOrderParams,
|
||||
} as any) as Order;
|
||||
const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
|
||||
const signature = signingUtils.signMessage(orderHashBuff, this._privateKey, signatureType);
|
||||
const signedOrder = {
|
||||
...order,
|
||||
signature: `0x${signature.toString('hex')}`,
|
||||
};
|
||||
return signedOrder;
|
||||
}
|
||||
}
|
58
contracts/test-utils/src/order_utils.ts
Normal file
58
contracts/test-utils/src/order_utils.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { OrderWithoutExchangeAddress, SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { constants } from './constants';
|
||||
import { CancelOrder, MatchOrder } from './types';
|
||||
|
||||
export const orderUtils = {
|
||||
getPartialAmountFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
|
||||
const partialAmount = numerator
|
||||
.mul(target)
|
||||
.div(denominator)
|
||||
.floor();
|
||||
return partialAmount;
|
||||
},
|
||||
createFill: (signedOrder: SignedOrder, takerAssetFillAmount?: BigNumber) => {
|
||||
const fill = {
|
||||
order: orderUtils.getOrderWithoutExchangeAddress(signedOrder),
|
||||
takerAssetFillAmount: takerAssetFillAmount || signedOrder.takerAssetAmount,
|
||||
signature: signedOrder.signature,
|
||||
};
|
||||
return fill;
|
||||
},
|
||||
createCancel(signedOrder: SignedOrder, takerAssetCancelAmount?: BigNumber): CancelOrder {
|
||||
const cancel = {
|
||||
order: orderUtils.getOrderWithoutExchangeAddress(signedOrder),
|
||||
takerAssetCancelAmount: takerAssetCancelAmount || signedOrder.takerAssetAmount,
|
||||
};
|
||||
return cancel;
|
||||
},
|
||||
getOrderWithoutExchangeAddress(signedOrder: SignedOrder): OrderWithoutExchangeAddress {
|
||||
const orderStruct = {
|
||||
senderAddress: signedOrder.senderAddress,
|
||||
makerAddress: signedOrder.makerAddress,
|
||||
takerAddress: signedOrder.takerAddress,
|
||||
feeRecipientAddress: signedOrder.feeRecipientAddress,
|
||||
makerAssetAmount: signedOrder.makerAssetAmount,
|
||||
takerAssetAmount: signedOrder.takerAssetAmount,
|
||||
makerFee: signedOrder.makerFee,
|
||||
takerFee: signedOrder.takerFee,
|
||||
expirationTimeSeconds: signedOrder.expirationTimeSeconds,
|
||||
salt: signedOrder.salt,
|
||||
makerAssetData: signedOrder.makerAssetData,
|
||||
takerAssetData: signedOrder.takerAssetData,
|
||||
};
|
||||
return orderStruct;
|
||||
},
|
||||
createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder): MatchOrder {
|
||||
const fill = {
|
||||
left: orderUtils.getOrderWithoutExchangeAddress(signedOrderLeft),
|
||||
right: orderUtils.getOrderWithoutExchangeAddress(signedOrderRight),
|
||||
leftSignature: signedOrderLeft.signature,
|
||||
rightSignature: signedOrderRight.signature,
|
||||
};
|
||||
fill.right.makerAssetData = constants.NULL_BYTES;
|
||||
fill.right.takerAssetData = constants.NULL_BYTES;
|
||||
return fill;
|
||||
},
|
||||
};
|
27
contracts/test-utils/src/profiler.ts
Normal file
27
contracts/test-utils/src/profiler.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { devConstants } from '@0x/dev-utils';
|
||||
import { ProfilerSubprovider, SolCompilerArtifactAdapter } from '@0x/sol-cov';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
let profilerSubprovider: ProfilerSubprovider;
|
||||
|
||||
export const profiler = {
|
||||
start(): void {
|
||||
profiler.getProfilerSubproviderSingleton().start();
|
||||
},
|
||||
stop(): void {
|
||||
profiler.getProfilerSubproviderSingleton().stop();
|
||||
},
|
||||
getProfilerSubproviderSingleton(): ProfilerSubprovider {
|
||||
if (_.isUndefined(profilerSubprovider)) {
|
||||
profilerSubprovider = profiler._getProfilerSubprovider();
|
||||
}
|
||||
return profilerSubprovider;
|
||||
},
|
||||
_getProfilerSubprovider(): ProfilerSubprovider {
|
||||
const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS;
|
||||
const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter();
|
||||
const isVerbose = true;
|
||||
const subprovider = new ProfilerSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose);
|
||||
return subprovider;
|
||||
},
|
||||
};
|
21
contracts/test-utils/src/revert_trace.ts
Normal file
21
contracts/test-utils/src/revert_trace.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { devConstants } from '@0x/dev-utils';
|
||||
import { RevertTraceSubprovider, SolCompilerArtifactAdapter } from '@0x/sol-cov';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
let revertTraceSubprovider: RevertTraceSubprovider;
|
||||
|
||||
export const revertTrace = {
|
||||
getRevertTraceSubproviderSingleton(): RevertTraceSubprovider {
|
||||
if (_.isUndefined(revertTraceSubprovider)) {
|
||||
revertTraceSubprovider = revertTrace._getRevertTraceSubprovider();
|
||||
}
|
||||
return revertTraceSubprovider;
|
||||
},
|
||||
_getRevertTraceSubprovider(): RevertTraceSubprovider {
|
||||
const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS;
|
||||
const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter();
|
||||
const isVerbose = true;
|
||||
const subprovider = new RevertTraceSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose);
|
||||
return subprovider;
|
||||
},
|
||||
};
|
29
contracts/test-utils/src/signing_utils.ts
Normal file
29
contracts/test-utils/src/signing_utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { SignatureType } from '@0x/types';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
|
||||
export const signingUtils = {
|
||||
signMessage(message: Buffer, privateKey: Buffer, signatureType: SignatureType): Buffer {
|
||||
if (signatureType === SignatureType.EthSign) {
|
||||
const prefixedMessage = ethUtil.hashPersonalMessage(message);
|
||||
const ecSignature = ethUtil.ecsign(prefixedMessage, privateKey);
|
||||
const signature = Buffer.concat([
|
||||
ethUtil.toBuffer(ecSignature.v),
|
||||
ecSignature.r,
|
||||
ecSignature.s,
|
||||
ethUtil.toBuffer(signatureType),
|
||||
]);
|
||||
return signature;
|
||||
} else if (signatureType === SignatureType.EIP712) {
|
||||
const ecSignature = ethUtil.ecsign(message, privateKey);
|
||||
const signature = Buffer.concat([
|
||||
ethUtil.toBuffer(ecSignature.v),
|
||||
ecSignature.r,
|
||||
ecSignature.s,
|
||||
ethUtil.toBuffer(signatureType),
|
||||
]);
|
||||
return signature;
|
||||
} else {
|
||||
throw new Error(`${signatureType} is not a valid signature type`);
|
||||
}
|
||||
},
|
||||
};
|
139
contracts/test-utils/src/test_with_reference.ts
Normal file
139
contracts/test-utils/src/test_with_reference.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { chaiSetup } from './chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
class Value<T> {
|
||||
public value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
class ErrorMessage {
|
||||
public error: string;
|
||||
constructor(message: string) {
|
||||
this.error = message;
|
||||
}
|
||||
}
|
||||
|
||||
type PromiseResult<T> = Value<T> | ErrorMessage;
|
||||
|
||||
// TODO(albrow): This seems like a generic utility function that could exist in
|
||||
// lodash. We should replace it by a library implementation, or move it to our
|
||||
// own.
|
||||
async function evaluatePromise<T>(promise: Promise<T>): Promise<PromiseResult<T>> {
|
||||
try {
|
||||
return new Value<T>(await promise);
|
||||
} catch (e) {
|
||||
return new ErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testWithReferenceFuncAsync<P0, R>(
|
||||
referenceFunc: (p0: P0) => Promise<R>,
|
||||
testFunc: (p0: P0) => Promise<R>,
|
||||
values: [P0],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, R>(
|
||||
referenceFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
values: [P0, P1],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, P2, R>(
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
values: [P0, P1, P2],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, P2, P3, R>(
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
values: [P0, P1, P2, P3],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>(
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
values: [P0, P1, P2, P3, P4],
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Tests the behavior of a test function by comparing it to the expected
|
||||
* behavior (defined by a reference function).
|
||||
*
|
||||
* First the reference function will be called to obtain an "expected result",
|
||||
* or if the reference function throws/rejects, an "expected error". Next, the
|
||||
* test function will be called to obtain an "actual result", or if the test
|
||||
* function throws/rejects, an "actual error". The test passes if at least one
|
||||
* of the following conditions is met:
|
||||
*
|
||||
* 1) Neither the reference function or the test function throw and the
|
||||
* "expected result" equals the "actual result".
|
||||
*
|
||||
* 2) Both the reference function and the test function throw and the "actual
|
||||
* error" message *contains* the "expected error" message.
|
||||
*
|
||||
* @param referenceFuncAsync a reference function implemented in pure
|
||||
* JavaScript/TypeScript which accepts N arguments and returns the "expected
|
||||
* result" or throws/rejects with the "expected error".
|
||||
* @param testFuncAsync a test function which, e.g., makes a call or sends a
|
||||
* transaction to a contract. It accepts the same N arguments returns the
|
||||
* "actual result" or throws/rejects with the "actual error".
|
||||
* @param values an array of N values, where each value corresponds in-order to
|
||||
* an argument to both the test function and the reference function.
|
||||
* @return A Promise that resolves if the test passes and rejects if the test
|
||||
* fails, according to the rules described above.
|
||||
*/
|
||||
export async function testWithReferenceFuncAsync(
|
||||
referenceFuncAsync: (...args: any[]) => Promise<any>,
|
||||
testFuncAsync: (...args: any[]) => Promise<any>,
|
||||
values: any[],
|
||||
): Promise<void> {
|
||||
// Measure correct behaviour
|
||||
const expected = await evaluatePromise(referenceFuncAsync(...values));
|
||||
|
||||
// Measure actual behaviour
|
||||
const actual = await evaluatePromise(testFuncAsync(...values));
|
||||
|
||||
// Compare behaviour
|
||||
if (expected instanceof ErrorMessage) {
|
||||
// If we expected an error, check if the actual error message contains the
|
||||
// expected error message.
|
||||
if (!(actual instanceof ErrorMessage)) {
|
||||
throw new Error(
|
||||
`Expected error containing ${expected.error} but got no error\n\tTest case: ${_getTestCaseString(
|
||||
referenceFuncAsync,
|
||||
values,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
expect(actual.error).to.contain(
|
||||
expected.error,
|
||||
`${actual.error}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`,
|
||||
);
|
||||
} else {
|
||||
// If we do not expect an error, compare actual and expected directly.
|
||||
expect(actual).to.deep.equal(expected, `Test case ${_getTestCaseString(referenceFuncAsync, values)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise<any>, values: any[]): string {
|
||||
const paramNames = _getParameterNames(referenceFuncAsync);
|
||||
return JSON.stringify(_.zipObject(paramNames, values));
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically
|
||||
function _getParameterNames(func: (...args: any[]) => any): string[] {
|
||||
return _.toString(func)
|
||||
.replace(/[/][/].*$/gm, '') // strip single-line comments
|
||||
.replace(/\s+/g, '') // strip white space
|
||||
.replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments
|
||||
.split('){', 1)[0]
|
||||
.replace(/^[^(]*[(]/, '') // extract the parameters
|
||||
.replace(/=[^,]+/g, '') // strip any ES6 defaults
|
||||
.split(',')
|
||||
.filter(Boolean); // split & filter [""]
|
||||
}
|
37
contracts/test-utils/src/transaction_factory.ts
Normal file
37
contracts/test-utils/src/transaction_factory.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { eip712Utils, generatePseudoRandomSalt } from '@0x/order-utils';
|
||||
import { SignatureType } from '@0x/types';
|
||||
import { signTypedDataUtils } from '@0x/utils';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
|
||||
import { signingUtils } from './signing_utils';
|
||||
import { SignedTransaction } from './types';
|
||||
|
||||
export class TransactionFactory {
|
||||
private readonly _signerBuff: Buffer;
|
||||
private readonly _exchangeAddress: string;
|
||||
private readonly _privateKey: Buffer;
|
||||
constructor(privateKey: Buffer, exchangeAddress: string) {
|
||||
this._privateKey = privateKey;
|
||||
this._exchangeAddress = exchangeAddress;
|
||||
this._signerBuff = ethUtil.privateToAddress(this._privateKey);
|
||||
}
|
||||
public newSignedTransaction(data: string, signatureType: SignatureType = SignatureType.EthSign): SignedTransaction {
|
||||
const salt = generatePseudoRandomSalt();
|
||||
const signerAddress = `0x${this._signerBuff.toString('hex')}`;
|
||||
const executeTransactionData = {
|
||||
salt,
|
||||
signerAddress,
|
||||
data,
|
||||
};
|
||||
|
||||
const typedData = eip712Utils.createZeroExTransactionTypedData(executeTransactionData, this._exchangeAddress);
|
||||
const eip712MessageBuffer = signTypedDataUtils.generateTypedDataHash(typedData);
|
||||
const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType);
|
||||
const signedTx = {
|
||||
exchangeAddress: this._exchangeAddress,
|
||||
signature: `0x${signature.toString('hex')}`,
|
||||
...executeTransactionData,
|
||||
};
|
||||
return signedTx;
|
||||
}
|
||||
}
|
21
contracts/test-utils/src/type_encoding_utils.ts
Normal file
21
contracts/test-utils/src/type_encoding_utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import BN = require('bn.js');
|
||||
import ethUtil = require('ethereumjs-util');
|
||||
|
||||
import { constants } from './constants';
|
||||
|
||||
export const typeEncodingUtils = {
|
||||
encodeUint256(value: BigNumber): Buffer {
|
||||
const base = 10;
|
||||
const formattedValue = new BN(value.toString(base));
|
||||
const encodedValue = ethUtil.toBuffer(formattedValue);
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
const paddedValue = ethUtil.setLengthLeft(encodedValue, constants.WORD_LENGTH);
|
||||
return paddedValue;
|
||||
},
|
||||
decodeUint256(encodedValue: Buffer): BigNumber {
|
||||
const formattedValue = ethUtil.bufferToHex(encodedValue);
|
||||
const value = new BigNumber(formattedValue, constants.BASE_16);
|
||||
return value;
|
||||
},
|
||||
};
|
241
contracts/test-utils/src/types.ts
Normal file
241
contracts/test-utils/src/types.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { OrderWithoutExchangeAddress } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { AbiDefinition } from 'ethereum-types';
|
||||
|
||||
export interface ERC20BalancesByOwner {
|
||||
[ownerAddress: string]: {
|
||||
[tokenAddress: string]: BigNumber;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ERC721TokenIdsByOwner {
|
||||
[ownerAddress: string]: {
|
||||
[tokenAddress: string]: BigNumber[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SubmissionContractEventArgs {
|
||||
transactionId: BigNumber;
|
||||
}
|
||||
|
||||
export interface BatchFillOrders {
|
||||
orders: OrderWithoutExchangeAddress[];
|
||||
signatures: string[];
|
||||
takerAssetFillAmounts: BigNumber[];
|
||||
}
|
||||
|
||||
export interface MarketSellOrders {
|
||||
orders: OrderWithoutExchangeAddress[];
|
||||
signatures: string[];
|
||||
takerAssetFillAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface MarketBuyOrders {
|
||||
orders: OrderWithoutExchangeAddress[];
|
||||
signatures: string[];
|
||||
makerAssetFillAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface BatchCancelOrders {
|
||||
orders: OrderWithoutExchangeAddress[];
|
||||
}
|
||||
|
||||
export interface CancelOrdersBefore {
|
||||
salt: BigNumber;
|
||||
}
|
||||
|
||||
export interface TransactionDataParams {
|
||||
name: string;
|
||||
abi: AbiDefinition[];
|
||||
args: any[];
|
||||
}
|
||||
|
||||
export interface MultiSigConfig {
|
||||
owners: string[];
|
||||
confirmationsRequired: number;
|
||||
secondsRequired: number;
|
||||
}
|
||||
|
||||
export interface MultiSigConfigByNetwork {
|
||||
[networkName: string]: MultiSigConfig;
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
address?: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
ipfsHash: string;
|
||||
swarmHash: string;
|
||||
}
|
||||
|
||||
export enum OrderStatus {
|
||||
INVALID,
|
||||
INVALID_MAKER_ASSET_AMOUNT,
|
||||
INVALID_TAKER_ASSET_AMOUNT,
|
||||
FILLABLE,
|
||||
EXPIRED,
|
||||
FULLY_FILLED,
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
export enum ContractName {
|
||||
TokenRegistry = 'TokenRegistry',
|
||||
MultiSigWalletWithTimeLock = 'MultiSigWalletWithTimeLock',
|
||||
Exchange = 'Exchange',
|
||||
ZRXToken = 'ZRXToken',
|
||||
DummyERC20Token = 'DummyERC20Token',
|
||||
EtherToken = 'WETH9',
|
||||
AssetProxyOwner = 'AssetProxyOwner',
|
||||
AccountLevels = 'AccountLevels',
|
||||
EtherDelta = 'EtherDelta',
|
||||
Arbitrage = 'Arbitrage',
|
||||
TestAssetDataDecoders = 'TestAssetDataDecoders',
|
||||
TestAssetProxyDispatcher = 'TestAssetProxyDispatcher',
|
||||
TestLibs = 'TestLibs',
|
||||
TestSignatureValidator = 'TestSignatureValidator',
|
||||
ERC20Proxy = 'ERC20Proxy',
|
||||
ERC721Proxy = 'ERC721Proxy',
|
||||
DummyERC721Receiver = 'DummyERC721Receiver',
|
||||
DummyERC721Token = 'DummyERC721Token',
|
||||
TestLibBytes = 'TestLibBytes',
|
||||
TestWallet = 'TestWallet',
|
||||
Authorizable = 'Authorizable',
|
||||
Whitelist = 'Whitelist',
|
||||
Forwarder = 'Forwarder',
|
||||
}
|
||||
|
||||
export interface SignedTransaction {
|
||||
exchangeAddress: string;
|
||||
salt: BigNumber;
|
||||
signerAddress: string;
|
||||
data: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface TransferAmountsByMatchOrders {
|
||||
// Left Maker
|
||||
amountBoughtByLeftMaker: BigNumber;
|
||||
amountSoldByLeftMaker: BigNumber;
|
||||
feePaidByLeftMaker: BigNumber;
|
||||
// Right Maker
|
||||
amountBoughtByRightMaker: BigNumber;
|
||||
amountSoldByRightMaker: BigNumber;
|
||||
feePaidByRightMaker: BigNumber;
|
||||
// Taker
|
||||
amountReceivedByTaker: BigNumber;
|
||||
feePaidByTakerLeft: BigNumber;
|
||||
feePaidByTakerRight: BigNumber;
|
||||
}
|
||||
|
||||
export interface TransferAmountsLoggedByMatchOrders {
|
||||
makerAddress: string;
|
||||
takerAddress: string;
|
||||
makerAssetFilledAmount: string;
|
||||
takerAssetFilledAmount: string;
|
||||
makerFeePaid: string;
|
||||
takerFeePaid: string;
|
||||
}
|
||||
|
||||
export interface OrderInfo {
|
||||
orderStatus: number;
|
||||
orderHash: string;
|
||||
orderTakerAssetFilledAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface CancelOrder {
|
||||
order: OrderWithoutExchangeAddress;
|
||||
takerAssetCancelAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface MatchOrder {
|
||||
left: OrderWithoutExchangeAddress;
|
||||
right: OrderWithoutExchangeAddress;
|
||||
leftSignature: string;
|
||||
rightSignature: string;
|
||||
}
|
||||
|
||||
// Combinatorial testing types
|
||||
|
||||
export enum FeeRecipientAddressScenario {
|
||||
BurnAddress = 'BURN_ADDRESS',
|
||||
EthUserAddress = 'ETH_USER_ADDRESS',
|
||||
}
|
||||
|
||||
export enum OrderAssetAmountScenario {
|
||||
Zero = 'ZERO',
|
||||
Large = 'LARGE',
|
||||
Small = 'SMALL',
|
||||
}
|
||||
|
||||
export enum TakerScenario {
|
||||
CorrectlySpecified = 'CORRECTLY_SPECIFIED',
|
||||
IncorrectlySpecified = 'INCORRECTLY_SPECIFIED',
|
||||
Unspecified = 'UNSPECIFIED',
|
||||
}
|
||||
|
||||
export enum ExpirationTimeSecondsScenario {
|
||||
InPast = 'IN_PAST',
|
||||
InFuture = 'IN_FUTURE',
|
||||
}
|
||||
|
||||
export enum AssetDataScenario {
|
||||
ERC20ZeroDecimals = 'ERC20_ZERO_DECIMALS',
|
||||
ZRXFeeToken = 'ZRX_FEE_TOKEN',
|
||||
ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS',
|
||||
ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS',
|
||||
ERC721 = 'ERC721',
|
||||
}
|
||||
|
||||
export enum TakerAssetFillAmountScenario {
|
||||
Zero = 'ZERO',
|
||||
GreaterThanRemainingFillableTakerAssetAmount = 'GREATER_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT',
|
||||
LessThanRemainingFillableTakerAssetAmount = 'LESS_THAN_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT',
|
||||
ExactlyRemainingFillableTakerAssetAmount = 'EXACTLY_REMAINING_FILLABLE_TAKER_ASSET_AMOUNT',
|
||||
}
|
||||
|
||||
export interface OrderScenario {
|
||||
takerScenario: TakerScenario;
|
||||
feeRecipientScenario: FeeRecipientAddressScenario;
|
||||
makerAssetAmountScenario: OrderAssetAmountScenario;
|
||||
takerAssetAmountScenario: OrderAssetAmountScenario;
|
||||
makerFeeScenario: OrderAssetAmountScenario;
|
||||
takerFeeScenario: OrderAssetAmountScenario;
|
||||
expirationTimeSecondsScenario: ExpirationTimeSecondsScenario;
|
||||
makerAssetDataScenario: AssetDataScenario;
|
||||
takerAssetDataScenario: AssetDataScenario;
|
||||
}
|
||||
|
||||
export enum BalanceAmountScenario {
|
||||
Exact = 'EXACT',
|
||||
TooLow = 'TOO_LOW',
|
||||
Higher = 'HIGHER',
|
||||
}
|
||||
|
||||
export enum AllowanceAmountScenario {
|
||||
Exact = 'EXACT',
|
||||
TooLow = 'TOO_LOW',
|
||||
Higher = 'HIGHER',
|
||||
Unlimited = 'UNLIMITED',
|
||||
}
|
||||
|
||||
export interface TraderStateScenario {
|
||||
traderAssetBalance: BalanceAmountScenario;
|
||||
traderAssetAllowance: AllowanceAmountScenario;
|
||||
zrxFeeBalance: BalanceAmountScenario;
|
||||
zrxFeeAllowance: AllowanceAmountScenario;
|
||||
}
|
||||
|
||||
export interface FillScenario {
|
||||
orderScenario: OrderScenario;
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario;
|
||||
makerStateScenario: TraderStateScenario;
|
||||
takerStateScenario: TraderStateScenario;
|
||||
}
|
||||
|
||||
export interface FillResults {
|
||||
makerAssetFilledAmount: BigNumber;
|
||||
takerAssetFilledAmount: BigNumber;
|
||||
makerFeePaid: BigNumber;
|
||||
takerFeePaid: BigNumber;
|
||||
}
|
84
contracts/test-utils/src/web3_wrapper.ts
Normal file
84
contracts/test-utils/src/web3_wrapper.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { devConstants, env, EnvVars, web3Factory } from '@0x/dev-utils';
|
||||
import { prependSubprovider, Web3ProviderEngine } from '@0x/subproviders';
|
||||
import { logUtils } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { coverage } from './coverage';
|
||||
import { profiler } from './profiler';
|
||||
import { revertTrace } from './revert_trace';
|
||||
|
||||
enum ProviderType {
|
||||
Ganache = 'ganache',
|
||||
Geth = 'geth',
|
||||
}
|
||||
|
||||
let testProvider: ProviderType;
|
||||
switch (process.env.TEST_PROVIDER) {
|
||||
case undefined:
|
||||
testProvider = ProviderType.Ganache;
|
||||
break;
|
||||
case 'ganache':
|
||||
testProvider = ProviderType.Ganache;
|
||||
break;
|
||||
case 'geth':
|
||||
testProvider = ProviderType.Geth;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown TEST_PROVIDER: ${process.env.TEST_PROVIDER}`);
|
||||
}
|
||||
|
||||
const ganacheTxDefaults = {
|
||||
from: devConstants.TESTRPC_FIRST_ADDRESS,
|
||||
gas: devConstants.GAS_LIMIT,
|
||||
};
|
||||
const gethTxDefaults = {
|
||||
from: devConstants.TESTRPC_FIRST_ADDRESS,
|
||||
};
|
||||
export const txDefaults = testProvider === ProviderType.Ganache ? ganacheTxDefaults : gethTxDefaults;
|
||||
|
||||
const gethConfigs = {
|
||||
shouldUseInProcessGanache: false,
|
||||
rpcUrl: 'http://localhost:8501',
|
||||
shouldUseFakeGasEstimate: false,
|
||||
};
|
||||
const ganacheConfigs = {
|
||||
shouldUseInProcessGanache: true,
|
||||
};
|
||||
const providerConfigs = testProvider === ProviderType.Ganache ? ganacheConfigs : gethConfigs;
|
||||
|
||||
export const provider: Web3ProviderEngine = web3Factory.getRpcProvider(providerConfigs);
|
||||
const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage);
|
||||
const isProfilerEnabled = env.parseBoolean(EnvVars.SolidityProfiler);
|
||||
const isRevertTraceEnabled = env.parseBoolean(EnvVars.SolidityRevertTrace);
|
||||
const enabledSubproviderCount = _.filter(
|
||||
[isCoverageEnabled, isProfilerEnabled, isRevertTraceEnabled],
|
||||
_.identity.bind(_),
|
||||
).length;
|
||||
if (enabledSubproviderCount > 1) {
|
||||
throw new Error(`Only one of coverage, profiler, or revert trace subproviders can be enabled at a time`);
|
||||
}
|
||||
if (isCoverageEnabled) {
|
||||
const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
|
||||
prependSubprovider(provider, coverageSubprovider);
|
||||
}
|
||||
if (isProfilerEnabled) {
|
||||
if (testProvider === ProviderType.Ganache) {
|
||||
logUtils.warn(
|
||||
"Gas costs in Ganache traces are incorrect and we don't recommend using it for profiling. Please switch to Geth",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const profilerSubprovider = profiler.getProfilerSubproviderSingleton();
|
||||
logUtils.log(
|
||||
"By default profilerSubprovider is stopped so that you don't get noise from setup code. Don't forget to start it before the code you want to profile and stop it afterwards",
|
||||
);
|
||||
profilerSubprovider.stop();
|
||||
prependSubprovider(provider, profilerSubprovider);
|
||||
}
|
||||
if (isRevertTraceEnabled) {
|
||||
const revertTraceSubprovider = revertTrace.getRevertTraceSubproviderSingleton();
|
||||
prependSubprovider(provider, revertTraceSubprovider);
|
||||
}
|
||||
|
||||
export const web3Wrapper = new Web3Wrapper(provider);
|
63
contracts/test-utils/test/test_with_reference.ts
Normal file
63
contracts/test-utils/test/test_with_reference.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as chai from 'chai';
|
||||
|
||||
import { chaiSetup } from '../src/chai_setup';
|
||||
import { testWithReferenceFuncAsync } from '../src/test_with_reference';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
async function divAsync(x: number, y: number): Promise<number> {
|
||||
if (y === 0) {
|
||||
throw new Error('MathError: divide by zero');
|
||||
}
|
||||
return x / y;
|
||||
}
|
||||
|
||||
// returns an async function that always returns the given value.
|
||||
function alwaysValueFunc(value: number): (x: number, y: number) => Promise<number> {
|
||||
return async (x: number, y: number) => value;
|
||||
}
|
||||
|
||||
// returns an async function which always throws/rejects with the given error
|
||||
// message.
|
||||
function alwaysFailFunc(errMessage: string): (x: number, y: number) => Promise<number> {
|
||||
return async (x: number, y: number) => {
|
||||
throw new Error(errMessage);
|
||||
};
|
||||
}
|
||||
|
||||
describe('testWithReferenceFuncAsync', () => {
|
||||
it('passes when both succeed and actual === expected', async () => {
|
||||
await testWithReferenceFuncAsync(alwaysValueFunc(0.5), divAsync, [1, 2]);
|
||||
});
|
||||
|
||||
it('passes when both fail and actual error contains expected error', async () => {
|
||||
await testWithReferenceFuncAsync(alwaysFailFunc('divide by zero'), divAsync, [1, 0]);
|
||||
});
|
||||
|
||||
it('fails when both succeed and actual !== expected', async () => {
|
||||
expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2])).to.be.rejectedWith(
|
||||
'Test case {"x":1,"y":2}: expected { value: 0.5 } to deeply equal { value: 3 }',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when both fail and actual error does not contain expected error', async () => {
|
||||
expect(
|
||||
testWithReferenceFuncAsync(alwaysFailFunc('Unexpected math error'), divAsync, [1, 0]),
|
||||
).to.be.rejectedWith(
|
||||
'MathError: divide by zero\n\tTest case: {"x":1,"y":0}: expected \'MathError: divide by zero\' to include \'Unexpected math error\'',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when referenceFunc succeeds and testFunc fails', async () => {
|
||||
expect(testWithReferenceFuncAsync(alwaysValueFunc(0), divAsync, [1, 0])).to.be.rejectedWith(
|
||||
'Test case {"x":1,"y":0}: expected { error: \'MathError: divide by zero\' } to deeply equal { value: 0 }',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when referenceFunc fails and testFunc succeeds', async () => {
|
||||
expect(testWithReferenceFuncAsync(alwaysFailFunc('divide by zero'), divAsync, [1, 2])).to.be.rejectedWith(
|
||||
'Expected error containing divide by zero but got no error\n\tTest case: {"x":1,"y":2}',
|
||||
);
|
||||
});
|
||||
});
|
7
contracts/test-utils/tsconfig.json
Normal file
7
contracts/test-utils/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["./src/**/*", "./test/**/*"]
|
||||
}
|
6
contracts/test-utils/tslint.json
Normal file
6
contracts/test-utils/tslint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["@0x/tslint-config"],
|
||||
"rules": {
|
||||
"custom-no-magic-numbers": false
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user