aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-11-13 11:17:18 +0800
committerFabio Berger <me@fabioberger.com>2017-11-13 11:17:18 +0800
commitc4ee2d73865a1444c079b9e2836b7630a0adf03e (patch)
treeb9c7794e7022fb189675d914f5fe58dcabd67dec /packages
parenta74ec0effa818a86233fe64cb0dad2c61bbb4bb6 (diff)
downloaddexon-0x-contracts-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.gz
dexon-0x-contracts-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.zst
dexon-0x-contracts-c4ee2d73865a1444c079b9e2836b7630a0adf03e.zip
Switch over to Lerna + Yarn Workspaces setup for a mono-repo approach
Diffstat (limited to 'packages')
-rw-r--r--packages/0x.js/README.md37
-rw-r--r--packages/0x.js/package.json106
-rwxr-xr-xpackages/0x.js/scripts/test_umd.sh7
-rw-r--r--packages/0x.js/src/0x.ts333
-rw-r--r--packages/0x.js/src/artifacts.ts14
-rw-r--r--packages/0x.js/src/artifacts/EtherToken.json445
-rw-r--r--packages/0x.js/src/artifacts/Exchange.json1130
-rw-r--r--packages/0x.js/src/artifacts/Token.json176
-rw-r--r--packages/0x.js/src/artifacts/TokenRegistry.json1211
-rw-r--r--packages/0x.js/src/artifacts/TokenTransferProxy.json174
-rw-r--r--packages/0x.js/src/bignumber_config.ts11
-rw-r--r--packages/0x.js/src/contract.ts80
-rw-r--r--packages/0x.js/src/contract_wrappers/contract_wrapper.ts152
-rw-r--r--packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts87
-rw-r--r--packages/0x.js/src/contract_wrappers/exchange_wrapper.ts866
-rw-r--r--packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts122
-rw-r--r--packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts60
-rw-r--r--packages/0x.js/src/contract_wrappers/token_wrapper.ts313
-rw-r--r--packages/0x.js/src/globals.d.ts80
-rw-r--r--packages/0x.js/src/globalsAugment.d.ts23
-rw-r--r--packages/0x.js/src/index.ts45
-rw-r--r--packages/0x.js/src/order_watcher/event_watcher.ts88
-rw-r--r--packages/0x.js/src/order_watcher/order_state_watcher.ts232
-rw-r--r--packages/0x.js/src/schemas/zero_ex_config_schema.ts23
-rw-r--r--packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts82
-rw-r--r--packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts61
-rw-r--r--packages/0x.js/src/subproviders/empty_wallet_subprovider.ts24
-rw-r--r--packages/0x.js/src/types.ts525
-rw-r--r--packages/0x.js/src/utils/abi_decoder.ts68
-rw-r--r--packages/0x.js/src/utils/assert.ts101
-rw-r--r--packages/0x.js/src/utils/constants.ts11
-rw-r--r--packages/0x.js/src/utils/decorators.ts35
-rw-r--r--packages/0x.js/src/utils/exchange_transfer_simulator.ts88
-rw-r--r--packages/0x.js/src/utils/filter_utils.ts82
-rw-r--r--packages/0x.js/src/utils/interval_utils.ts20
-rw-r--r--packages/0x.js/src/utils/order_state_utils.ts119
-rw-r--r--packages/0x.js/src/utils/order_validation_utils.ts166
-rw-r--r--packages/0x.js/src/utils/signature_utils.ts44
-rw-r--r--packages/0x.js/src/utils/utils.ts55
-rw-r--r--packages/0x.js/src/web3_wrapper.ts172
-rw-r--r--packages/0x.js/test/0x.js_test.ts259
-rw-r--r--packages/0x.js/test/artifacts_test.ts49
-rw-r--r--packages/0x.js/test/assert_test.ts34
-rw-r--r--packages/0x.js/test/ether_token_wrapper_test.ts111
-rw-r--r--packages/0x.js/test/event_watcher_test.ts127
-rw-r--r--packages/0x.js/test/exchange_transfer_simulator_test.ts87
-rw-r--r--packages/0x.js/test/exchange_wrapper_test.ts824
-rw-r--r--packages/0x.js/test/order_state_watcher_test.ts356
-rw-r--r--packages/0x.js/test/order_validation_test.ts327
-rw-r--r--packages/0x.js/test/subscription_test.ts95
-rw-r--r--packages/0x.js/test/token_registry_wrapper_test.ts123
-rw-r--r--packages/0x.js/test/token_transfer_proxy_wrapper_test.ts31
-rw-r--r--packages/0x.js/test/token_wrapper_test.ts477
-rw-r--r--packages/0x.js/test/utils/blockchain_lifecycle.ts26
-rw-r--r--packages/0x.js/test/utils/chai_setup.ts13
-rw-r--r--packages/0x.js/test/utils/constants.ts8
-rw-r--r--packages/0x.js/test/utils/fill_scenarios.ts114
-rw-r--r--packages/0x.js/test/utils/order_factory.ts42
-rw-r--r--packages/0x.js/test/utils/report_callback_errors.ts14
-rw-r--r--packages/0x.js/test/utils/rpc.ts57
-rw-r--r--packages/0x.js/test/utils/token_utils.ts24
-rw-r--r--packages/0x.js/test/utils/web3_factory.ts31
-rw-r--r--packages/0x.js/test/web3_wrapper_test.ts29
-rw-r--r--packages/0x.js/tsconfig.json22
-rw-r--r--packages/0x.js/webpack.config.js56
65 files changed, 10804 insertions, 0 deletions
diff --git a/packages/0x.js/README.md b/packages/0x.js/README.md
new file mode 100644
index 000000000..4b6cc8df4
--- /dev/null
+++ b/packages/0x.js/README.md
@@ -0,0 +1,37 @@
+## Installation
+
+0x.js ships as both a [UMD](https://github.com/umdjs/umd) module and a [CommonJS](https://en.wikipedia.org/wiki/CommonJS) package.
+
+#### CommonJS *(recommended)*:
+
+**Install**
+
+```bash
+npm install 0x.js --save
+```
+
+**Import**
+
+```javascript
+import {ZeroEx} from '0x.js';
+```
+
+#### UMD:
+
+**Install**
+
+Download the UMD module from our [releases page](https://github.com/0xProject/0x.js/releases) and add it to your project.
+
+**Import**
+
+```html
+<script type="text/javascript" src="0x.js"></script>
+```
+
+## Documentation
+
+Extensive documentation of 0x.js can be found on [our website][docs-url].
+
+[website-url]: https://0xproject.com/
+[whitepaper-url]: https://0xproject.com/pdfs/0x_white_paper.pdf
+[docs-url]: https://0xproject.com/docs/0xjs
diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json
new file mode 100644
index 000000000..1d3b0c7d2
--- /dev/null
+++ b/packages/0x.js/package.json
@@ -0,0 +1,106 @@
+{
+ "name": "0x.js",
+ "version": "0.23.0",
+ "description": "A javascript library for interacting with the 0x protocol",
+ "keywords": [
+ "0x.js",
+ "0xproject",
+ "ethereum",
+ "tokens",
+ "exchange"
+ ],
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.ts",
+ "scripts": {
+ "prebuild": "npm run clean",
+ "build": "run-p build:umd:prod build:commonjs",
+ "prepublishOnly": "run-p build",
+ "postpublish": "run-s release docs:json upload_docs_json",
+ "release": "publish-release --assets _bundles/index.js,_bundles/index.min.js --tag $(git describe --tags) --owner 0xProject --repo 0x.js",
+ "upload_docs_json": "aws s3 cp docs/index.json s3://0xjs-docs-jsons/$(git describe --tags).json --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type aplication/json",
+ "lint": "tslint src/**/*.ts test/**/*.ts",
+ "test": "run-s clean test:commonjs",
+ "test:umd": "./scripts/test_umd.sh",
+ "test:coverage": "nyc npm run test --all",
+ "report_test_coverage": "nyc report --reporter=text-lcov | coveralls",
+ "update_contracts": "for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../0x.js/src/artifacts; done;",
+ "testrpc": "testrpc -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"",
+ "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json docs/index.json .",
+ "docs:generate": "typedoc --out docs .",
+ "docs:open": "opn docs/index.html",
+ "clean": "shx rm -rf _bundles lib test_temp",
+ "build:umd:dev": "webpack",
+ "build:umd:prod": "NODE_ENV=production webpack",
+ "build:commonjs": "tsc; copyfiles -u 2 './src/artifacts/**/*.json' ./lib/src/artifacts;",
+ "test:commonjs": "run-s build:commonjs run_mocha",
+ "pretest:umd": "run-s clean build:umd:dev build:commonjs",
+ "substitute_umd_bundle": "npm run remove_src_files_not_used_by_tests; shx mv _bundles/* lib/src",
+ "remove_src_files_not_used_by_tests": "find ./lib/src \\( -path ./lib/src/utils -o -path ./lib/src/subproviders -o -path ./lib/src/schemas -o -path \"./lib/src/types.*\" \\) -prune -o -type f -print | xargs rm",
+ "run_mocha": "mocha lib/test/**/*_test.js --timeout 5000 --bail --exit"
+ },
+ "config": {
+ "artifacts": "TokenTransferProxy Exchange TokenRegistry Token EtherToken",
+ "mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/0xProject/0x.js"
+ },
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "devDependencies": {
+ "@types/jsonschema": "^1.1.1",
+ "@types/lodash": "^4.14.64",
+ "@types/mocha": "^2.2.41",
+ "@types/node": "^8.0.1",
+ "@types/sinon": "^2.2.2",
+ "@types/uuid": "^3.4.2",
+ "awesome-typescript-loader": "^3.1.3",
+ "chai": "^4.0.1",
+ "chai-as-promised": "^7.1.0",
+ "chai-as-promised-typescript-typings": "0.0.3",
+ "chai-bignumber": "^2.0.1",
+ "chai-typescript-typings": "^0.0.1",
+ "copyfiles": "^1.2.0",
+ "coveralls": "^3.0.0",
+ "dirty-chai": "^2.0.1",
+ "ethereumjs-testrpc": "4.0.1",
+ "json-loader": "^0.5.4",
+ "mocha": "^4.0.0",
+ "npm-run-all": "^4.0.2",
+ "nyc": "^11.0.1",
+ "opn-cli": "^3.1.0",
+ "request": "^2.81.0",
+ "request-promise-native": "^1.0.4",
+ "shx": "^0.2.2",
+ "sinon": "^4.0.0",
+ "source-map-support": "^0.5.0",
+ "truffle-hdwallet-provider": "^0.0.3",
+ "tslint": "~5.5.0",
+ "tslint-config-0xproject": "^0.0.2",
+ "typedoc": "~0.8.0",
+ "types-bn": "^0.0.1",
+ "types-ethereumjs-util": "0xProject/types-ethereumjs-util",
+ "typescript": "^2.4.1",
+ "web3-provider-engine": "^13.0.1",
+ "web3-typescript-typings": "^0.7.1",
+ "webpack": "^3.1.0"
+ },
+ "dependencies": {
+ "0x-json-schemas": "^0.6.1",
+ "bignumber.js": "^4.1.0",
+ "compare-versions": "^3.0.1",
+ "es6-promisify": "^5.0.0",
+ "ethereumjs-abi": "^0.6.4",
+ "ethereumjs-blockstream": "^2.0.6",
+ "ethereumjs-util": "^5.1.1",
+ "find-versions": "^2.0.0",
+ "js-sha3": "^0.6.1",
+ "lodash": "^4.17.4",
+ "publish-release": "^1.3.3",
+ "uuid": "^3.1.0",
+ "web3": "^0.20.0"
+ }
+}
diff --git a/packages/0x.js/scripts/test_umd.sh b/packages/0x.js/scripts/test_umd.sh
new file mode 100755
index 000000000..d200c76d0
--- /dev/null
+++ b/packages/0x.js/scripts/test_umd.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# This script runs umd tests and cleans up after them while preserving the `return_code` for CI
+# UMD tests should only be run after building the commonjs because they reuse some of the commonjs build artifacts
+run-s substitute_umd_bundle run_mocha
+return_code=$?
+npm run clean
+exit $return_code
diff --git a/packages/0x.js/src/0x.ts b/packages/0x.js/src/0x.ts
new file mode 100644
index 000000000..fe765bbbe
--- /dev/null
+++ b/packages/0x.js/src/0x.ts
@@ -0,0 +1,333 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {SchemaValidator, schemas} from '0x-json-schemas';
+import {bigNumberConfigs} from './bignumber_config';
+import * as ethUtil from 'ethereumjs-util';
+import {Web3Wrapper} from './web3_wrapper';
+import {constants} from './utils/constants';
+import {utils} from './utils/utils';
+import {signatureUtils} from './utils/signature_utils';
+import {assert} from './utils/assert';
+import {AbiDecoder} from './utils/abi_decoder';
+import {intervalUtils} from './utils/interval_utils';
+import {artifacts} from './artifacts';
+import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper';
+import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper';
+import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper';
+import {TokenWrapper} from './contract_wrappers/token_wrapper';
+import {TokenTransferProxyWrapper} from './contract_wrappers/token_transfer_proxy_wrapper';
+import {OrderStateWatcher} from './order_watcher/order_state_watcher';
+import {OrderStateUtils} from './utils/order_state_utils';
+import {
+ ECSignature,
+ ZeroExError,
+ Order,
+ SignedOrder,
+ Web3Provider,
+ ZeroExConfig,
+ OrderStateWatcherConfig,
+ TransactionReceiptWithDecodedLogs,
+} from './types';
+import {zeroExConfigSchema} from './schemas/zero_ex_config_schema';
+
+// Customize our BigNumber instances
+bigNumberConfigs.configure();
+
+/**
+ * The ZeroEx class is the single entry-point into the 0x.js library. It contains all of the library's functionality
+ * and all calls to the library should be made through a ZeroEx instance.
+ */
+export class ZeroEx {
+ /**
+ * When creating an order without a specified taker or feeRecipient you must supply the Solidity
+ * address null type (as opposed to Javascripts `null`, `undefined` or empty string). We expose
+ * this constant for your convenience.
+ */
+ public static NULL_ADDRESS = constants.NULL_ADDRESS;
+
+ /**
+ * An instance of the ExchangeWrapper class containing methods for interacting with the 0x Exchange smart contract.
+ */
+ public exchange: ExchangeWrapper;
+ /**
+ * An instance of the TokenRegistryWrapper class containing methods for interacting with the 0x
+ * TokenRegistry smart contract.
+ */
+ public tokenRegistry: TokenRegistryWrapper;
+ /**
+ * An instance of the TokenWrapper class containing methods for interacting with any ERC20 token smart contract.
+ */
+ public token: TokenWrapper;
+ /**
+ * An instance of the EtherTokenWrapper class containing methods for interacting with the
+ * wrapped ETH ERC20 token smart contract.
+ */
+ public etherToken: EtherTokenWrapper;
+ /**
+ * An instance of the TokenTransferProxyWrapper class containing methods for interacting with the
+ * tokenTransferProxy smart contract.
+ */
+ public proxy: TokenTransferProxyWrapper;
+ /**
+ * An instance of the OrderStateWatcher class containing methods for watching a set of orders for relevant
+ * blockchain state changes.
+ */
+ public orderStateWatcher: OrderStateWatcher;
+ private _web3Wrapper: Web3Wrapper;
+ private _abiDecoder: AbiDecoder;
+ /**
+ * Verifies that the elliptic curve signature `signature` was generated
+ * by signing `data` with the private key corresponding to the `signerAddress` address.
+ * @param data The hex encoded data signed by the supplied signature.
+ * @param signature An object containing the elliptic curve signature parameters.
+ * @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
+ * @return Whether the signature is valid for the supplied signerAddress and data.
+ */
+ public static isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean {
+ assert.isHexString('data', data);
+ assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema);
+ assert.isETHAddressHex('signerAddress', signerAddress);
+
+ const isValidSignature = signatureUtils.isValidSignature(data, signature, signerAddress);
+ return isValidSignature;
+ }
+ /**
+ * Generates a pseudo-random 256-bit salt.
+ * The salt can be included in an 0x order, ensuring that the order generates a unique orderHash
+ * and will not collide with other outstanding orders that are identical in all other parameters.
+ * @return A pseudo-random 256-bit number that can be used as a salt.
+ */
+ public static generatePseudoRandomSalt(): BigNumber {
+ // BigNumber.random returns a pseudo-random number between 0 & 1 with a passed in number of decimal places.
+ // Source: https://mikemcl.github.io/bignumber.js/#random
+ const randomNumber = BigNumber.random(constants.MAX_DIGITS_IN_UNSIGNED_256_INT);
+ const factor = new BigNumber(10).pow(constants.MAX_DIGITS_IN_UNSIGNED_256_INT - 1);
+ const salt = randomNumber.times(factor).round();
+ return salt;
+ }
+ /**
+ * Checks if the supplied hex encoded order hash is valid.
+ * Note: Valid means it has the expected format, not that an order with the orderHash exists.
+ * Use this method when processing orderHashes submitted as user input.
+ * @param orderHash Hex encoded orderHash.
+ * @return Whether the supplied orderHash has the expected format.
+ */
+ public static isValidOrderHash(orderHash: string): boolean {
+ // Since this method can be called to check if any arbitrary string conforms to an orderHash's
+ // format, we only assert that we were indeed passed a string.
+ assert.isString('orderHash', orderHash);
+ const schemaValidator = new SchemaValidator();
+ const isValidOrderHash = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid;
+ return isValidOrderHash;
+ }
+ /**
+ * A unit amount is defined as the amount of a token above the specified decimal places (integer part).
+ * E.g: If a currency has 18 decimal places, 1e18 or one quintillion of the currency is equivalent
+ * to 1 unit.
+ * @param amount The amount in baseUnits that you would like converted to units.
+ * @param decimals The number of decimal places the unit amount has.
+ * @return The amount in units.
+ */
+ public static toUnitAmount(amount: BigNumber, decimals: number): BigNumber {
+ assert.isBigNumber('amount', amount);
+ assert.isNumber('decimals', decimals);
+
+ const aUnit = new BigNumber(10).pow(decimals);
+ const unit = amount.div(aUnit);
+ return unit;
+ }
+ /**
+ * A baseUnit is defined as the smallest denomination of a token. An amount expressed in baseUnits
+ * is the amount expressed in the smallest denomination.
+ * E.g: 1 unit of a token with 18 decimal places is expressed in baseUnits as 1000000000000000000
+ * @param amount The amount of units that you would like converted to baseUnits.
+ * @param decimals The number of decimal places the unit amount has.
+ * @return The amount in baseUnits.
+ */
+ public static toBaseUnitAmount(amount: BigNumber, decimals: number): BigNumber {
+ assert.isBigNumber('amount', amount);
+ assert.isNumber('decimals', decimals);
+
+ const unit = new BigNumber(10).pow(decimals);
+ const baseUnitAmount = amount.times(unit);
+ return baseUnitAmount;
+ }
+ /**
+ * Computes the orderHash for a supplied order.
+ * @param order An object that conforms to the Order or SignedOrder interface definitions.
+ * @return The resulting orderHash from hashing the supplied order.
+ */
+ public static getOrderHashHex(order: Order|SignedOrder): string {
+ assert.doesConformToSchema('order', order, schemas.orderSchema);
+ const orderHashHex = utils.getOrderHashHex(order);
+ return orderHashHex;
+ }
+ /**
+ * Instantiates a new ZeroEx instance that provides the public interface to the 0x.js library.
+ * @param provider The Web3.js Provider instance you would like the 0x.js library to use for interacting with
+ * the Ethereum network.
+ * @param config The configuration object. Look up the type for the description.
+ * @return An instance of the 0x.js ZeroEx class.
+ */
+ constructor(provider: Web3Provider, config?: ZeroExConfig) {
+ assert.isWeb3Provider('provider', provider);
+ if (!_.isUndefined(config)) {
+ assert.doesConformToSchema('config', config, zeroExConfigSchema);
+ }
+ const artifactJSONs = _.values(artifacts);
+ const abiArrays = _.map(artifactJSONs, artifact => artifact.abi);
+ this._abiDecoder = new AbiDecoder(abiArrays);
+ const gasPrice = _.isUndefined(config) ? undefined : config.gasPrice;
+ const defaults = {
+ gasPrice,
+ };
+ this._web3Wrapper = new Web3Wrapper(provider, defaults);
+ this.token = new TokenWrapper(
+ this._web3Wrapper,
+ this._abiDecoder,
+ this._getTokenTransferProxyAddressAsync.bind(this),
+ );
+ const exchageContractAddressIfExists = _.isUndefined(config) ? undefined : config.exchangeContractAddress;
+ this.exchange = new ExchangeWrapper(
+ this._web3Wrapper,
+ this._abiDecoder,
+ this.token,
+ exchageContractAddressIfExists,
+ );
+ this.proxy = new TokenTransferProxyWrapper(
+ this._web3Wrapper,
+ this._getTokenTransferProxyAddressAsync.bind(this),
+ );
+ const tokenRegistryContractAddressIfExists = _.isUndefined(config) ?
+ undefined :
+ config.tokenRegistryContractAddress;
+ this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper, tokenRegistryContractAddressIfExists);
+ const etherTokenContractAddressIfExists = _.isUndefined(config) ? undefined : config.etherTokenContractAddress;
+ this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, etherTokenContractAddressIfExists);
+ const orderWatcherConfig = _.isUndefined(config) ? undefined : config.orderWatcherConfig;
+ this.orderStateWatcher = new OrderStateWatcher(
+ this._web3Wrapper, this._abiDecoder, this.token, this.exchange, orderWatcherConfig,
+ );
+ }
+ /**
+ * Sets a new web3 provider for 0x.js. Updating the provider will stop all
+ * subscriptions so you will need to re-subscribe to all events relevant to your app after this call.
+ * @param provider The Web3Provider you would like the 0x.js library to use from now on.
+ */
+ public async setProviderAsync(provider: Web3Provider) {
+ this._web3Wrapper.setProvider(provider);
+ await (this.exchange as any)._invalidateContractInstancesAsync();
+ (this.tokenRegistry as any)._invalidateContractInstance();
+ await (this.token as any)._invalidateContractInstancesAsync();
+ (this.proxy as any)._invalidateContractInstance();
+ (this.etherToken as any)._invalidateContractInstance();
+ }
+ /**
+ * Get user Ethereum addresses available through the supplied web3 provider available for sending transactions.
+ * @return An array of available user Ethereum addresses.
+ */
+ public async getAvailableAddressesAsync(): Promise<string[]> {
+ const availableAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
+ return availableAddresses;
+ }
+ /**
+ * Signs an orderHash and returns it's elliptic curve signature.
+ * This method currently supports TestRPC, Geth and Parity above and below V1.6.6
+ * @param orderHash Hex encoded orderHash to sign.
+ * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
+ * must be available via the Web3.Provider supplied to 0x.js.
+ * @return An object containing the Elliptic curve signature parameters generated by signing the orderHash.
+ */
+ public async signOrderHashAsync(orderHash: string, signerAddress: string): Promise<ECSignature> {
+ assert.isHexString('orderHash', orderHash);
+ await assert.isSenderAddressAsync('signerAddress', signerAddress, this._web3Wrapper);
+
+ let msgHashHex;
+ const nodeVersion = await this._web3Wrapper.getNodeVersionAsync();
+ const isParityNode = utils.isParityNode(nodeVersion);
+ const isTestRpc = utils.isTestRpc(nodeVersion);
+ if (isParityNode || isTestRpc) {
+ // Parity and TestRpc nodes add the personalMessage prefix itself
+ msgHashHex = orderHash;
+ } else {
+ const orderHashBuff = ethUtil.toBuffer(orderHash);
+ const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff);
+ msgHashHex = ethUtil.bufferToHex(msgHashBuff);
+ }
+
+ const signature = await this._web3Wrapper.signTransactionAsync(signerAddress, msgHashHex);
+
+ // HACK: There is no consensus on whether the signatureHex string should be formatted as
+ // v + r + s OR r + s + v, and different clients (even different versions of the same client)
+ // return the signature params in different orders. In order to support all client implementations,
+ // we parse the signature in both ways, and evaluate if either one is a valid signature.
+ const validVParamValues = [27, 28];
+ const ecSignatureVRS = signatureUtils.parseSignatureHexAsVRS(signature);
+ if (_.includes(validVParamValues, ecSignatureVRS.v)) {
+ const isValidVRSSignature = ZeroEx.isValidSignature(orderHash, ecSignatureVRS, signerAddress);
+ if (isValidVRSSignature) {
+ return ecSignatureVRS;
+ }
+ }
+
+ const ecSignatureRSV = signatureUtils.parseSignatureHexAsRSV(signature);
+ if (_.includes(validVParamValues, ecSignatureRSV.v)) {
+ const isValidRSVSignature = ZeroEx.isValidSignature(orderHash, ecSignatureRSV, signerAddress);
+ if (isValidRSVSignature) {
+ return ecSignatureRSV;
+ }
+ }
+
+ throw new Error(ZeroExError.InvalidSignature);
+ }
+ /**
+ * Waits for a transaction to be mined and returns the transaction receipt.
+ * @param txHash Transaction hash
+ * @param pollingIntervalMs How often (in ms) should we check if the transaction is mined.
+ * @param timeoutMs How long (in ms) to poll for transaction mined until aborting.
+ * @return Transaction receipt with decoded log args.
+ */
+ public async awaitTransactionMinedAsync(
+ txHash: string, pollingIntervalMs = 1000, timeoutMs?: number): Promise<TransactionReceiptWithDecodedLogs> {
+ let timeoutExceeded = false;
+ if (timeoutMs) {
+ setTimeout(() => timeoutExceeded = true, timeoutMs);
+ }
+
+ const txReceiptPromise = new Promise(
+ (resolve: (receipt: TransactionReceiptWithDecodedLogs) => void, reject) => {
+ const intervalId = intervalUtils.setAsyncExcludingInterval(async () => {
+ if (timeoutExceeded) {
+ intervalUtils.clearAsyncExcludingInterval(intervalId);
+ return reject(ZeroExError.TransactionMiningTimeout);
+ }
+
+ const transactionReceipt = await this._web3Wrapper.getTransactionReceiptAsync(txHash);
+ if (!_.isNull(transactionReceipt)) {
+ intervalUtils.clearAsyncExcludingInterval(intervalId);
+ const logsWithDecodedArgs = _.map(
+ transactionReceipt.logs,
+ this._abiDecoder.tryToDecodeLogOrNoop.bind(this._abiDecoder),
+ );
+ const transactionReceiptWithDecodedLogArgs: TransactionReceiptWithDecodedLogs = {
+ ...transactionReceipt,
+ logs: logsWithDecodedArgs,
+ };
+ resolve(transactionReceiptWithDecodedLogArgs);
+ }
+ }, pollingIntervalMs);
+ });
+
+ return txReceiptPromise;
+ }
+ /*
+ * HACK: `TokenWrapper` needs a token transfer proxy address. `TokenTransferProxy` address is fetched from
+ * an `ExchangeWrapper`. `ExchangeWrapper` needs `TokenWrapper` to validate orders, creating a dependency cycle.
+ * In order to break this - we create this function here and pass it as a parameter to the `TokenWrapper`
+ * and `ProxyWrapper`.
+ */
+ private async _getTokenTransferProxyAddressAsync(): Promise<string> {
+ const tokenTransferProxyAddress = await (this.exchange as any)._getTokenTransferProxyAddressAsync();
+ return tokenTransferProxyAddress;
+ }
+}
diff --git a/packages/0x.js/src/artifacts.ts b/packages/0x.js/src/artifacts.ts
new file mode 100644
index 000000000..447f9880a
--- /dev/null
+++ b/packages/0x.js/src/artifacts.ts
@@ -0,0 +1,14 @@
+import {Artifact} from './types';
+import * as TokenArtifact from './artifacts/Token.json';
+import * as ExchangeArtifact from './artifacts/Exchange.json';
+import * as EtherTokenArtifact from './artifacts/EtherToken.json';
+import * as TokenRegistryArtifact from './artifacts/TokenRegistry.json';
+import * as TokenTransferProxyArtifact from './artifacts/TokenTransferProxy.json';
+
+export const artifacts = {
+ TokenArtifact: TokenArtifact as any as Artifact,
+ ExchangeArtifact: ExchangeArtifact as any as Artifact,
+ EtherTokenArtifact: EtherTokenArtifact as any as Artifact,
+ TokenRegistryArtifact: TokenRegistryArtifact as any as Artifact,
+ TokenTransferProxyArtifact: TokenTransferProxyArtifact as any as Artifact,
+};
diff --git a/packages/0x.js/src/artifacts/EtherToken.json b/packages/0x.js/src/artifacts/EtherToken.json
new file mode 100644
index 000000000..91b23bc94
--- /dev/null
+++ b/packages/0x.js/src/artifacts/EtherToken.json
@@ -0,0 +1,445 @@
+{
+ "contract_name": "EtherToken",
+ "abi": [
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "amount",
+ "type": "uint256"
+ }
+ ],
+ "name": "withdraw",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "decimals",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint8"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transfer",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [],
+ "name": "deposit",
+ "outputs": [],
+ "payable": true,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "name": "_spender",
+ "type": "address"
+ }
+ ],
+ "name": "allowance",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "payable": true,
+ "type": "fallback"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ ],
+ "unlinked_binary": "0x6060604052341561000c57fe5b5b6107598061001c6000396000f300606060405236156100935763ffffffff60e060020a60003504166306fdde0381146100a4578063095ea7b31461013457806318160ddd1461016757806323b872dd146101895780632e1a7d4d146101c2578063313ce567146101d757806370a08231146101fd57806395d89b411461022b578063a9059cbb146102bb578063d0e30db0146102ee578063dd62ed3e146102f8575b6100a25b61009f61032c565b5b565b005b34156100ac57fe5b6100b461037b565b6040805160208082528351818301528351919283929083019185019080838382156100fa575b8051825260208311156100fa57601f1990920191602091820191016100da565b505050905090810190601f1680156101265780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561013c57fe5b610153600160a060020a03600435166024356103a3565b604080519115158252519081900360200190f35b341561016f57fe5b61017761040e565b60408051918252519081900360200190f35b341561019157fe5b610153600160a060020a0360043581169060243516604435610414565b604080519115158252519081900360200190f35b34156101ca57fe5b6100a2600435610537565b005b34156101df57fe5b6101e76105b8565b6040805160ff9092168252519081900360200190f35b341561020557fe5b610177600160a060020a03600435166105bd565b60408051918252519081900360200190f35b341561023357fe5b6100b46105dc565b6040805160208082528351818301528351919283929083019185019080838382156100fa575b8051825260208311156100fa57601f1990920191602091820191016100da565b505050905090810190601f1680156101265780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34156102c357fe5b610153600160a060020a03600435166024356105fd565b604080519115158252519081900360200190f35b6100a261032c565b005b341561030057fe5b610177600160a060020a03600435811690602435166106af565b60408051918252519081900360200190f35b600160a060020a03331660009081526020819052604090205461034f90346106dc565b600160a060020a03331660009081526020819052604090205560025461037590346106dc565b6002555b565b60408051808201909152600b815260a960020a6a22ba3432b9102a37b5b2b702602082015281565b600160a060020a03338116600081815260016020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b60025481565b600160a060020a03808416600081815260016020908152604080832033909516835293815283822054928252819052918220548390108015906104575750828110155b801561047d5750600160a060020a03841660009081526020819052604090205483810110155b1561052957600160a060020a03808516600090815260208190526040808220805487019055918716815220805484900390556000198110156104e757600160a060020a03808616600090815260016020908152604080832033909416835292905220805484900390555b83600160a060020a031685600160a060020a031660008051602061070e833981519152856040518082815260200191505060405180910390a36001915061052e565b600091505b5b509392505050565b600160a060020a03331660009081526020819052604090205461055a90826106f6565b600160a060020a03331660009081526020819052604090205560025461058090826106f6565b600255604051600160a060020a0333169082156108fc029083906000818181858888f1935050505015156105b45760006000fd5b5b50565b601281565b600160a060020a0381166000908152602081905260409020545b919050565b604080518082019091526004815260e360020a630ae8aa8902602082015281565b600160a060020a0333166000908152602081905260408120548290108015906106405750600160a060020a03831660009081526020819052604090205482810110155b156106a057600160a060020a03338116600081815260208181526040808320805488900390559387168083529184902080548701905583518681529351919360008051602061070e833981519152929081900390910190a3506001610408565b506000610408565b5b92915050565b600160a060020a038083166000908152600160209081526040808320938516835292905220545b92915050565b6000828201838110156106eb57fe5b8091505b5092915050565b60008282111561070257fe5b508082035b929150505600ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa165627a7a72305820ec42c469bb8ddd5de28c55b9cc393c812397c063a57fb88926e3f6de246318b70029",
+ "networks": {
+ "1": {
+ "links": {},
+ "events": {
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ },
+ "updated_at": 1502488087000,
+ "address": "0x2956356cd2a2bf3202f771f50d3d14a367b48070"
+ },
+ "3": {
+ "links": {},
+ "events": {
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ },
+ "updated_at": 1506602007000,
+ "address": "0xc00fd9820cd2898cc4c054b7bf142de637ad129a"
+ },
+ "42": {
+ "links": {},
+ "events": {
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ },
+ "updated_at": 1502391794392,
+ "address": "0x05d090b51c40b020eab3bfcb6a2dff130df22e9c"
+ },
+ "50": {
+ "links": {},
+ "events": {
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ },
+ "updated_at": 1503318938233,
+ "address": "0x48bacb9266a570d521063ef5dd96e61686dbe788"
+ }
+ },
+ "schema_version": "0.0.5",
+ "updated_at": 1503318938233
+}
diff --git a/packages/0x.js/src/artifacts/Exchange.json b/packages/0x.js/src/artifacts/Exchange.json
new file mode 100644
index 000000000..734c8f9c7
--- /dev/null
+++ b/packages/0x.js/src/artifacts/Exchange.json
@@ -0,0 +1,1130 @@
+{
+ "contract_name": "Exchange",
+ "abi": [
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "numerator",
+ "type": "uint256"
+ },
+ {
+ "name": "denominator",
+ "type": "uint256"
+ },
+ {
+ "name": "target",
+ "type": "uint256"
+ }
+ ],
+ "name": "isRoundingError",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "",
+ "type": "bytes32"
+ }
+ ],
+ "name": "filled",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "",
+ "type": "bytes32"
+ }
+ ],
+ "name": "cancelled",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5][]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6][]"
+ },
+ {
+ "name": "fillTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "name": "shouldThrowOnInsufficientBalanceOrAllowance",
+ "type": "bool"
+ },
+ {
+ "name": "v",
+ "type": "uint8[]"
+ },
+ {
+ "name": "r",
+ "type": "bytes32[]"
+ },
+ {
+ "name": "s",
+ "type": "bytes32[]"
+ }
+ ],
+ "name": "fillOrdersUpTo",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6]"
+ },
+ {
+ "name": "cancelTakerTokenAmount",
+ "type": "uint256"
+ }
+ ],
+ "name": "cancelOrder",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "ZRX_TOKEN_CONTRACT",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5][]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6][]"
+ },
+ {
+ "name": "fillTakerTokenAmounts",
+ "type": "uint256[]"
+ },
+ {
+ "name": "v",
+ "type": "uint8[]"
+ },
+ {
+ "name": "r",
+ "type": "bytes32[]"
+ },
+ {
+ "name": "s",
+ "type": "bytes32[]"
+ }
+ ],
+ "name": "batchFillOrKillOrders",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6]"
+ },
+ {
+ "name": "fillTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "name": "v",
+ "type": "uint8"
+ },
+ {
+ "name": "r",
+ "type": "bytes32"
+ },
+ {
+ "name": "s",
+ "type": "bytes32"
+ }
+ ],
+ "name": "fillOrKillOrder",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "getUnavailableTakerTokenAmount",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "signer",
+ "type": "address"
+ },
+ {
+ "name": "hash",
+ "type": "bytes32"
+ },
+ {
+ "name": "v",
+ "type": "uint8"
+ },
+ {
+ "name": "r",
+ "type": "bytes32"
+ },
+ {
+ "name": "s",
+ "type": "bytes32"
+ }
+ ],
+ "name": "isValidSignature",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "numerator",
+ "type": "uint256"
+ },
+ {
+ "name": "denominator",
+ "type": "uint256"
+ },
+ {
+ "name": "target",
+ "type": "uint256"
+ }
+ ],
+ "name": "getPartialAmount",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "TOKEN_TRANSFER_PROXY_CONTRACT",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5][]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6][]"
+ },
+ {
+ "name": "fillTakerTokenAmounts",
+ "type": "uint256[]"
+ },
+ {
+ "name": "shouldThrowOnInsufficientBalanceOrAllowance",
+ "type": "bool"
+ },
+ {
+ "name": "v",
+ "type": "uint8[]"
+ },
+ {
+ "name": "r",
+ "type": "bytes32[]"
+ },
+ {
+ "name": "s",
+ "type": "bytes32[]"
+ }
+ ],
+ "name": "batchFillOrders",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5][]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6][]"
+ },
+ {
+ "name": "cancelTakerTokenAmounts",
+ "type": "uint256[]"
+ }
+ ],
+ "name": "batchCancelOrders",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6]"
+ },
+ {
+ "name": "fillTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "name": "shouldThrowOnInsufficientBalanceOrAllowance",
+ "type": "bool"
+ },
+ {
+ "name": "v",
+ "type": "uint8"
+ },
+ {
+ "name": "r",
+ "type": "bytes32"
+ },
+ {
+ "name": "s",
+ "type": "bytes32"
+ }
+ ],
+ "name": "fillOrder",
+ "outputs": [
+ {
+ "name": "filledTakerTokenAmount",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "orderAddresses",
+ "type": "address[5]"
+ },
+ {
+ "name": "orderValues",
+ "type": "uint256[6]"
+ }
+ ],
+ "name": "getOrderHash",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bytes32"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "EXTERNAL_QUERY_GAS_LIMIT",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint16"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "VERSION",
+ "outputs": [
+ {
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "name": "_zrxToken",
+ "type": "address"
+ },
+ {
+ "name": "_tokenTransferProxy",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "constructor"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "taker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "filledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "filledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidMakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidTakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogFill",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogCancel",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "errorId",
+ "type": "uint8"
+ },
+ {
+ "indexed": true,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogError",
+ "type": "event"
+ }
+ ],
+ "unlinked_binary": "0x6060604052341561000c57fe5b604051604080611ed68339810160405280516020909101515b60008054600160a060020a03808516600160a060020a03199283161790925560018054928416929091169190911790555b50505b611e6e806100686000396000f300606060405236156100e05763ffffffff60e060020a60003504166314df96ee81146100e2578063288cdc911461010f5780632ac1262214610134578063363349be14610159578063394c21e71461031b5780633b30ba591461038e5780634f150787146103ba578063741bcc931461059d5780637e9abb50146106135780638163681e1461063857806398024a8b14610677578063add1cbc5146106a2578063b7b2c7d6146106ce578063baa0181d146108b9578063bc61394a146109f4578063cfc4d0ec14610a81578063f06bbf7514610af2578063ffa1ad7414610b19575bfe5b34156100ea57fe5b6100fb600435602435604435610ba9565b604080519115158252519081900360200190f35b341561011757fe5b610122600435610bf7565b60408051918252519081900360200190f35b341561013c57fe5b610122600435610c09565b60408051918252519081900360200190f35b341561016157fe5b61012260048080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b828210156101d3576040805160a08181019092529080840287019060059083908390808284375050509183525050600190910190602001610197565b5050505050919080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b82821015610246576040805160c0818101909252908084028701906006908390839080828437505050918352505060019091019060200161020a565b5050604080516020878301358901803582810280850184019095528084529799893599838101351515999198506060019650929450810192829185019084908082843750506040805187358901803560208181028481018201909552818452989a998901989297509082019550935083925085019084908082843750506040805187358901803560208181028481018201909552818452989a998901989297509082019550935083925085019084908082843750949650610c1b95505050505050565b60408051918252519081900360200190f35b341561032357fe5b6040805160a08181019092526101229160049160a4918390600590839083908082843750506040805160c08181019092529496958181019594509250600691508390839080828437509395505092359250610d44915050565b60408051918252519081900360200190f35b341561039657fe5b61039e611036565b60408051600160a060020a039092168252519081900360200190f35b34156103c257fe5b61059b60048080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b82821015610434576040805160a081810190925290808402870190600590839083908082843750505091835250506001909101906020016103f8565b5050505050919080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b828210156104a7576040805160c0818101909252908084028701906006908390839080828437505050918352505060019091019060200161046b565b50505050509190803590602001908201803590602001908080602002602001604051908101604052809392919081815260200183836020028082843750506040805187358901803560208181028481018201909552818452989a998901989297509082019550935083925085019084908082843750506040805187358901803560208181028481018201909552818452989a998901989297509082019550935083925085019084908082843750506040805187358901803560208181028481018201909552818452989a99890198929750908201955093508392508501908490808284375094965061104595505050505050565b005b34156105a557fe5b6040805160a081810190925261059b9160049160a4918390600590839083908082843750506040805160c08181019092529496958181019594509250600691508390839080828437509395505083359360ff6020820135169350604081013592506060013590506110fc565b005b341561061b57fe5b610122600435611121565b60408051918252519081900360200190f35b341561064057fe5b6100fb600160a060020a036004351660243560ff6044351660643560843561114d565b604080519115158252519081900360200190f35b341561067f57fe5b610122600435602435604435611205565b60408051918252519081900360200190f35b34156106aa57fe5b61039e611224565b60408051600160a060020a039092168252519081900360200190f35b34156106d657fe5b61059b60048080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b82821015610748576040805160a0818101909252908084028701906005908390839080828437505050918352505060019091019060200161070c565b5050505050919080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b828210156107bb576040805160c0818101909252908084028701906006908390839080828437505050918352505060019091019060200161077f565b5050505050919080359060200190820180359060200190808060200260200160405190810160405280939291908181526020018383602002808284375050604080516020808901358a01803580830284810184018652818552999b8b3515159b909a950198509296508101945090925082919085019084908082843750506040805187358901803560208181028481018201909552818452989a998901989297509082019550935083925085019084908082843750506040805187358901803560208181028481018201909552818452989a99890198929750908201955093508392508501908490808284375094965061123395505050505050565b005b34156108c157fe5b61059b60048080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b82821015610933576040805160a081810190925290808402870190600590839083908082843750505091835250506001909101906020016108f7565b5050505050919080359060200190820180359060200190808060200260200160405190810160405280939291908181526020016000905b828210156109a6576040805160c0818101909252908084028701906006908390839080828437505050918352505060019091019060200161096a565b505050505091908035906020019082018035906020019080806020026020016040519081016040528093929190818152602001838360200280828437509496506112ed95505050505050565b005b34156109fc57fe5b6040805160a08181019092526101229160049160a4918390600590839083908082843750506040805160c08181019092529496958181019594509250600691508390839080828437509395505083359360208101351515935060ff6040820135169250606081013591506080013561135a565b60408051918252519081900360200190f35b3415610a8957fe5b6040805160a08181019092526101229160049160a4918390600590839083908082843750506040805160c0818101909252949695818101959450925060069150839083908082843750939550611838945050505050565b60408051918252519081900360200190f35b3415610afa57fe5b610b0261192b565b6040805161ffff9092168252519081900360200190f35b3415610b2157fe5b610b29611931565b604080516020808252835181830152835191928392908301918501908083838215610b6f575b805182526020831115610b6f57601f199092019160209182019101610b4f565b505050905090810190601f168015610b9b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b600060006000848685099150811515610bc55760009250610bee565b610be4610bd583620f4240611953565b610bdf8887611953565b611982565b90506103e8811192505b50509392505050565b60026020526000908152604090205481565b60036020526000908152604090205481565b600080805b8951811015610d3357896000815181101515610c3857fe5b6020908102909101015160035b6020020151600160a060020a03168a82815181101515610c6157fe5b6020908102909101015160035b6020020151600160a060020a031614610c875760006000fd5b610d1b82610d168c84815181101515610c9c57fe5b906020019060200201518c85815181101515610cb457fe5b90602001906020020151610cc88d8861199f565b8c8c88815181101515610cd757fe5b906020019060200201518c89815181101515610cef57fe5b906020019060200201518c8a815181101515610d0757fe5b9060200190602002015161135a565b6119b6565b915087821415610d2a57610d33565b5b600101610c20565b8192505b5050979650505050505050565b6000610d4e611dc6565b60408051610160810190915260009081908088835b60209081029190910151600160a060020a03168252018860015b60209081029190910151600160a060020a03168252018860025b60209081029190910151600160a060020a03168252018860035b60209081029190910151600160a060020a03168252018860045b60209081029190910151600160a060020a03168252018760005b602090810291909101518252018760015b602090810291909101518252018760025b602090810291909101518252018760035b602090810291909101518252018760045b60200201518152602001610e3d8989611838565b9052805190935033600160a060020a03908116911614610e5d5760006000fd5b60008360a00151118015610e75575060008360c00151115b8015610e815750600085115b1515610e8d5760006000fd5b6101208301514210610ec95761014083015160005b60405160ff9190911690600080516020611e2383398151915290600090a36000935061102c565b610ee48360c00151610edf856101400151611121565b61199f565b9150610ef085836119d0565b9050801515610f2d576101408301516001610ea2565b60405160ff9190911690600080516020611e2383398151915290600090a36000935061102c565b610140830151600090815260036020526040902054610f4c90826119b6565b610140840151600090815260036020526040908190209190915580840180516060860180518451606060020a600160a060020a03948516810282529184169091026014820152935193849003602801909320608087015187519351945160c089015160a08a0151939692851695909416937f67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b0458713193610fea918991611205565b6101408a015160408051600160a060020a0395861681529390941660208401528284019190915260608201889052608082015290519081900360a00190a48093505b5050509392505050565b600054600160a060020a031681565b60005b86518110156110f2576110e9878281518110151561106257fe5b90602001906020020151878381518110151561107a57fe5b90602001906020020151878481518110151561109257fe5b9060200190602002015187858151811015156110aa57fe5b9060200190602002015187868151811015156110c257fe5b9060200190602002015187878151811015156110da57fe5b906020019060200201516110fc565b5b600101611048565b5b50505050505050565b8361110d878787600088888861135a565b146111185760006000fd5b5b505050505050565b600081815260026020908152604080832054600390925282205461114591906119b6565b90505b919050565b604080517f19457468657265756d205369676e6564204d6573736167653a0a3332000000008152601c8101869052815190819003603c018120600082815260208381018552928401819052835191825260ff8716828401528184018690526060820185905292516001926080808401939192601f1981019281900390910190868661646e5a03f115156111dc57fe5b505060206040510351600160a060020a031686600160a060020a03161490505b95945050505050565b600061121a6112148584611953565b84611982565b90505b9392505050565b600154600160a060020a031681565b60005b87518110156112e2576112d8888281518110151561125057fe5b90602001906020020151888381518110151561126857fe5b90602001906020020151888481518110151561128057fe5b9060200190602002015188888681518110151561129957fe5b9060200190602002015188878151811015156112b157fe5b906020019060200201518888815181101515610d0757fe5b9060200190602002015161135a565b505b600101611236565b5b5050505050505050565b60005b835181101561135357611349848281518110151561130a57fe5b90602001906020020151848381518110151561132257fe5b90602001906020020151848481518110151561133a57fe5b90602001906020020151610d44565b505b6001016112f0565b5b50505050565b6000611364611dc6565b6000600060006000610160604051908101604052808e600060058110151561138857fe5b60209081029190910151600160a060020a03168252018e60015b60209081029190910151600160a060020a03168252018e60025b60209081029190910151600160a060020a03168252018e60035b60209081029190910151600160a060020a03168252018e60045b60209081029190910151600160a060020a03168252018d60005b602090810291909101518252018d60015b602090810291909101518252018d60025b602090810291909101518252018d60035b602090810291909101518252018d60045b602002015181526020016114628f8f611838565b90526020810151909550600160a060020a03161580611496575033600160a060020a03168560200151600160a060020a0316145b15156114a25760006000fd5b60008560a001511180156114ba575060008560c00151115b80156114c6575060008b115b15156114d25760006000fd5b6114e885600001518661014001518b8b8b61114d565b15156114f45760006000fd5b61012085015142106115305761014085015160005b60405160ff9190911690600080516020611e2383398151915290600090a360009550611828565b61154b8560c00151610edf876101400151611121565b61199f565b93506115578b856119d0565b9550851515611594576101408501516001611509565b60405160ff9190911690600080516020611e2383398151915290600090a360009550611828565b6115a7868660c001518760a00151610ba9565b156115e0576101408501516002611509565b60405160ff9190911690600080516020611e2383398151915290600090a360009550611828565b891580156115f557506115f385876119ea565b155b1561162e576101408501516003611509565b60405160ff9190911690600080516020611e2383398151915290600090a360009550611828565b611641868660c001518760a00151611205565b61014086015160009081526002602052604090205490935061166390876119b6565b6101408601516000908152600260205260409081902091909155850151855161168e91903386611c46565b151561169a5760006000fd5b6116ae856060015133876000015189611c46565b15156116ba5760006000fd5b6080850151600160a060020a03161561176e5760008560e00151111561171c576116ed868660c001518760e00151611205565b6000548651608088015192945061171092600160a060020a039092169185611c46565b151561171c5760006000fd5b5b6000856101000151111561176e5761173f868660c00151876101000151611205565b600054608087015191925061176291600160a060020a0390911690339084611c46565b151561176e5760006000fd5b5b5b60408086018051606080890180518551606060020a600160a060020a0395861681028252918516909102601482015285519081900360280181206080808d01518d51975194516101408f0151338916865295881660208601528716848a01529483018b905282018d905260a0820189905260c0820188905260e08201929092529451909491831693909216917f0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3918190036101000190a45b5050505050979650505050505050565b60003083825b60200201518460015b60200201518560025b60200201518660035b60200201518760045b60200201518760005b60200201518860015b60200201518960025b60200201518a60035b60200201518b60045b60200201518c60055b602002015160408051606060020a600160a060020a039e8f16810282529c8e168d0260148201529a8d168c0260288c0152988c168b02603c8b0152968b168a0260508a01529490991690970260648701526078860191909152609885015260b884019490945260d883019490945260f8820192909252610118810192909252519081900361013801902090505b92915050565b61138781565b604080518082019091526005815260dc60020a640312e302e302602082015281565b600082820283158061196f575082848281151561196c57fe5b04145b151561197757fe5b8091505b5092915050565b60006000828481151561199157fe5b0490508091505b5092915050565b6000828211156119ab57fe5b508082035b92915050565b60008282018381101561197757fe5b8091505b5092915050565b60008183106119df57816119e1565b825b90505b92915050565b600060006000600060006000600060006000339750611a128a8c60c001518d60a00151611205565b60808c0151909750600160a060020a031615611bc75760005460408c015160608d015160c08e015160e08f0151600160a060020a0394851693851684149a50939091169091149650611a66918c9190611205565b9350611a7c8a8c60c001518d6101000151611205565b925085611a895783611a93565b611a9387856119b6565b5b915084611aa15782611aab565b611aab8a846119b6565b5b6000548c519192508391611ac991600160a060020a031690611cd5565b1080611aee57506000548b518391611aec91600160a060020a0390911690611d50565b105b80611b0e57506000548190611b0c90600160a060020a03168a611cd5565b105b80611b2e57506000548190611b2c90600160a060020a03168a611d50565b105b15611b3c5760009850611c38565b85158015611b74575086611b588c604001518d60000151611cd5565b1080611b74575086611b728c604001518d60000151611d50565b105b5b15611b835760009850611c38565b84158015611bb3575089611b9b8c606001518a611cd5565b1080611bb3575089611bb18c606001518a611d50565b105b5b15611bc25760009850611c38565b611c32565b86611bda8c604001518d60000151611cd5565b1080611bf6575086611bf48c604001518d60000151611d50565b105b80611c0d575089611c0b8c606001518a611cd5565b105b80611c24575089611c228c606001518a611d50565b105b15611c325760009850611c38565b5b600198505b505050505050505092915050565b6001546040805160006020918201819052825160e160020a630aed65f5028152600160a060020a03898116600483015288811660248301528781166044830152606482018790529351919493909316926315dacbea92608480830193919282900301818787803b1515611cb557fe5b6102c65a03f11515611cc357fe5b5050604051519150505b949350505050565b600082600160a060020a03166370a0823161138761ffff16846040518363ffffffff1660e060020a0281526004018082600160a060020a0316600160a060020a03168152602001915050602060405180830381600088803b1515611d3557fe5b87f11515611d3f57fe5b505060405151925050505b92915050565b6001546040805160e160020a636eb1769f028152600160a060020a0384811660048301529283166024820152905160009285169163dd62ed3e916113879160448082019260209290919082900301818888803b1515611d3557fe5b87f11515611d3f57fe5b505060405151925050505b92915050565b6040805161016081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e081018290526101008101829052610120810182905261014081019190915290560036d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90a165627a7a72305820ee5481a0a94774cee2beaad49e754979e4db70aed7ee4a00103d60def15aa8da0029",
+ "networks": {
+ "1": {
+ "links": {},
+ "events": {
+ "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "taker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "filledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "filledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidMakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidTakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogFill",
+ "type": "event"
+ },
+ "0x67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogCancel",
+ "type": "event"
+ },
+ "0x36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "errorId",
+ "type": "uint8"
+ },
+ {
+ "indexed": true,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogError",
+ "type": "event"
+ }
+ },
+ "updated_at": 1502480340000,
+ "address": "0x12459c951127e0c374ff9105dda097662a027093"
+ },
+ "3": {
+ "links": {},
+ "events": {
+ "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "taker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "filledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "filledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidMakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidTakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogFill",
+ "type": "event"
+ },
+ "0x67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogCancel",
+ "type": "event"
+ },
+ "0x36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "errorId",
+ "type": "uint8"
+ },
+ {
+ "indexed": true,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogError",
+ "type": "event"
+ }
+ },
+ "updated_at": 1506602007000,
+ "address": "0x479cc461fecd078f766ecc58533d6f69580cf3ac"
+ },
+ "42": {
+ "links": {},
+ "events": {
+ "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "taker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "filledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "filledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidMakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidTakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogFill",
+ "type": "event"
+ },
+ "0x67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogCancel",
+ "type": "event"
+ },
+ "0x36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "errorId",
+ "type": "uint8"
+ },
+ {
+ "indexed": true,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogError",
+ "type": "event"
+ }
+ },
+ "updated_at": 1502391794390,
+ "address": "0x90fe2af704b34e0224bf2299c838e04d4dcf1364"
+ },
+ "50": {
+ "links": {},
+ "events": {
+ "0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "taker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "filledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "filledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidMakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "paidTakerFee",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogFill",
+ "type": "event"
+ },
+ "0x67d66f160bc93d925d05dae1794c90d2d6d6688b29b84ff069398a9b04587131": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "maker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "feeRecipient",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "makerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "takerToken",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledMakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "cancelledTakerTokenAmount",
+ "type": "uint256"
+ },
+ {
+ "indexed": true,
+ "name": "tokens",
+ "type": "bytes32"
+ },
+ {
+ "indexed": false,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogCancel",
+ "type": "event"
+ },
+ "0x36d86c59e00bd73dc19ba3adfe068e4b64ac7e92be35546adeddf1b956a87e90": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "errorId",
+ "type": "uint8"
+ },
+ {
+ "indexed": true,
+ "name": "orderHash",
+ "type": "bytes32"
+ }
+ ],
+ "name": "LogError",
+ "type": "event"
+ }
+ },
+ "updated_at": 1503318938231,
+ "address": "0xb69e673309512a9d726f87304c6984054f87a93b"
+ }
+ },
+ "schema_version": "0.0.5",
+ "updated_at": 1503318938231
+}
diff --git a/packages/0x.js/src/artifacts/Token.json b/packages/0x.js/src/artifacts/Token.json
new file mode 100644
index 000000000..e922ff66d
--- /dev/null
+++ b/packages/0x.js/src/artifacts/Token.json
@@ -0,0 +1,176 @@
+{
+ "contract_name": "Token",
+ "abi": [
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [
+ {
+ "name": "success",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "name": "supply",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [
+ {
+ "name": "success",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "name": "balance",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transfer",
+ "outputs": [
+ {
+ "name": "success",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "name": "_spender",
+ "type": "address"
+ }
+ ],
+ "name": "allowance",
+ "outputs": [
+ {
+ "name": "remaining",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ ],
+ "unlinked_binary": "0x6060604052341561000c57fe5b5b6101e08061001c6000396000f3006060604052361561005c5763ffffffff60e060020a600035041663095ea7b3811461005e57806318160ddd1461009157806323b872dd146100b357806370a08231146100ec578063a9059cbb1461005e578063dd62ed3e1461014d575bfe5b341561006657fe5b61007d600160a060020a0360043516602435610181565b604080519115158252519081900360200190f35b341561009957fe5b6100a161018a565b60408051918252519081900360200190f35b34156100bb57fe5b61007d600160a060020a0360043581169060243516604435610190565b604080519115158252519081900360200190f35b34156100f457fe5b6100a1600160a060020a036004351661019a565b60408051918252519081900360200190f35b341561006657fe5b61007d600160a060020a0360043516602435610181565b604080519115158252519081900360200190f35b341561015557fe5b6100a1600160a060020a0360043581169060243516610181565b60408051918252519081900360200190f35b60005b92915050565b60005b90565b60005b9392505050565b60005b919050565b60005b92915050565b60005b929150505600a165627a7a723058202e3f7ac17048343c0d0ea24fccb64620577374eeeed61539e543df4025d7d0db0029",
+ "networks": {},
+ "schema_version": "0.0.5",
+ "updated_at": 1503317882695
+} \ No newline at end of file
diff --git a/packages/0x.js/src/artifacts/TokenRegistry.json b/packages/0x.js/src/artifacts/TokenRegistry.json
new file mode 100644
index 000000000..5a0564a69
--- /dev/null
+++ b/packages/0x.js/src/artifacts/TokenRegistry.json
@@ -0,0 +1,1211 @@
+{
+ "contract_name": "TokenRegistry",
+ "abi": [
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ },
+ {
+ "name": "_index",
+ "type": "uint256"
+ }
+ ],
+ "name": "removeToken",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_name",
+ "type": "string"
+ }
+ ],
+ "name": "getTokenAddressByName",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_symbol",
+ "type": "string"
+ }
+ ],
+ "name": "getTokenAddressBySymbol",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ },
+ {
+ "name": "_swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "setTokenSwarmHash",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ }
+ ],
+ "name": "getTokenMetaData",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ },
+ {
+ "name": "",
+ "type": "string"
+ },
+ {
+ "name": "",
+ "type": "string"
+ },
+ {
+ "name": "",
+ "type": "uint8"
+ },
+ {
+ "name": "",
+ "type": "bytes"
+ },
+ {
+ "name": "",
+ "type": "bytes"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "owner",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ },
+ {
+ "name": "_name",
+ "type": "string"
+ },
+ {
+ "name": "_symbol",
+ "type": "string"
+ },
+ {
+ "name": "_decimals",
+ "type": "uint8"
+ },
+ {
+ "name": "_ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "name": "_swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "addToken",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ },
+ {
+ "name": "_name",
+ "type": "string"
+ }
+ ],
+ "name": "setTokenName",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "name": "tokens",
+ "outputs": [
+ {
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "name": "tokenAddresses",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_name",
+ "type": "string"
+ }
+ ],
+ "name": "getTokenByName",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ },
+ {
+ "name": "",
+ "type": "string"
+ },
+ {
+ "name": "",
+ "type": "string"
+ },
+ {
+ "name": "",
+ "type": "uint8"
+ },
+ {
+ "name": "",
+ "type": "bytes"
+ },
+ {
+ "name": "",
+ "type": "bytes"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "getTokenAddresses",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address[]"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ },
+ {
+ "name": "_ipfsHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "setTokenIpfsHash",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_symbol",
+ "type": "string"
+ }
+ ],
+ "name": "getTokenBySymbol",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ },
+ {
+ "name": "",
+ "type": "string"
+ },
+ {
+ "name": "",
+ "type": "string"
+ },
+ {
+ "name": "",
+ "type": "uint8"
+ },
+ {
+ "name": "",
+ "type": "bytes"
+ },
+ {
+ "name": "",
+ "type": "bytes"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_token",
+ "type": "address"
+ },
+ {
+ "name": "_symbol",
+ "type": "string"
+ }
+ ],
+ "name": "setTokenSymbol",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "newOwner",
+ "type": "address"
+ }
+ ],
+ "name": "transferOwnership",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogAddToken",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogRemoveToken",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldName",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newName",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenNameChange",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSymbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newSymbol",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenSymbolChange",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldIpfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newIpfsHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenIpfsHashChange",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSwarmHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newSwarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenSwarmHashChange",
+ "type": "event"
+ }
+ ],
+ "unlinked_binary": "0x60606040525b60008054600160a060020a03191633600160a060020a03161790555b5b612cc5806100316000396000f300606060405236156100ca5763ffffffff60e060020a60003504166313baf1e681146100cc5780632fbfeba9146100ed5780633550b6d91461015f57806356318820146101d15780637abccac9146102335780638da5cb5b1461044d578063a880319d14610479578063c370c86d1461059a578063e4860339146105fc578063e5df8b841461082b578063e73fc0c31461085a578063ee8c24b814610aae578063eef05f6514610b19578063efa74f1f14610b7b578063f036417f14610dcf578063f2fde38b14610e31575bfe5b34156100d457fe5b6100eb600160a060020a0360043516602435610e4f565b005b34156100f557fe5b610143600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284375094965061134895505050505050565b60408051600160a060020a039092168252519081900360200190f35b341561016757fe5b610143600480803590602001908201803590602001908080601f016020809104026020016040519081016040528093929190818152602001838380828437509496506113bb95505050505050565b60408051600160a060020a039092168252519081900360200190f35b34156101d957fe5b60408051602060046024803582810135601f81018590048502860185019096528585526100eb958335600160a060020a0316959394604494939290920191819084018382808284375094965061142e95505050505050565b005b341561023b57fe5b61024f600160a060020a03600435166115cf565b6040518087600160a060020a0316600160a060020a0316815260200180602001806020018660ff1660ff168152602001806020018060200185810385528a8181518152602001915080519060200190808383600083146102ca575b8051825260208311156102ca57601f1990920191602091820191016102aa565b505050905090810190601f1680156102f65780820380516001836020036101000a031916815260200191505b5085810384528951815289516020918201918b01908083838215610335575b80518252602083111561033557601f199092019160209182019101610315565b505050905090810190601f1680156103615780820380516001836020036101000a031916815260200191505b50858103835287518152875160209182019189019080838382156103a0575b8051825260208311156103a057601f199092019160209182019101610380565b505050905090810190601f1680156103cc5780820380516001836020036101000a031916815260200191505b508581038252865181528651602091820191880190808383821561040b575b80518252602083111561040b57601f1990920191602091820191016103eb565b505050905090810190601f1680156104375780820380516001836020036101000a031916815260200191505b509a505050505050505050505060405180910390f35b341561045557fe5b6101436118ba565b60408051600160a060020a039092168252519081900360200190f35b341561048157fe5b60408051602060046024803582810135601f81018590048502860185019096528585526100eb958335600160a060020a0316959394604494939290920191819084018382808284375050604080516020601f89358b0180359182018390048302840183019094528083529799988101979196509182019450925082915084018382808284375050604080516020601f818a01358b0180359182018390048302840183018552818452989a60ff8b35169a90999401975091955091820193509150819084018382808284375050604080516020601f89358b018035918201839004830284018301909452808352979998810197919650918201945092508291508401838280828437509496506118c995505050505050565b005b34156105a257fe5b60408051602060046024803582810135601f81018590048502860185019096528585526100eb958335600160a060020a03169593946044949392909201918190840183828082843750949650611e7a95505050505050565b005b341561060457fe5b610618600160a060020a03600435166121a1565b60408051600160a060020a038816815260ff8516606082015260c0602082018181528854600260001961010060018416150201909116049183018290529192830190608084019060a085019060e08601908b9080156106b85780601f1061068d576101008083540402835291602001916106b8565b820191906000526020600020905b81548152906001019060200180831161069b57829003601f168201915b505085810384528954600260001961010060018416150201909116048082526020909101908a90801561072c5780601f106107015761010080835404028352916020019161072c565b820191906000526020600020905b81548152906001019060200180831161070f57829003601f168201915b50508581038352875460026000196101006001841615020190911604808252602090910190889080156107a05780601f10610775576101008083540402835291602001916107a0565b820191906000526020600020905b81548152906001019060200180831161078357829003601f168201915b50508581038252865460026000196101006001841615020190911604808252602090910190879080156108145780601f106107e957610100808354040283529160200191610814565b820191906000526020600020905b8154815290600101906020018083116107f757829003601f168201915b50509a505050505050505050505060405180910390f35b341561083357fe5b6101436004356121dc565b60408051600160a060020a039092168252519081900360200190f35b341561086257fe5b61024f600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284375094965061220e95505050505050565b6040518087600160a060020a0316600160a060020a0316815260200180602001806020018660ff1660ff168152602001806020018060200185810385528a8181518152602001915080519060200190808383600083146102ca575b8051825260208311156102ca57601f1990920191602091820191016102aa565b505050905090810190601f1680156102f65780820380516001836020036101000a031916815260200191505b5085810384528951815289516020918201918b01908083838215610335575b80518252602083111561033557601f199092019160209182019101610315565b505050905090810190601f1680156103615780820380516001836020036101000a031916815260200191505b50858103835287518152875160209182019189019080838382156103a0575b8051825260208311156103a057601f199092019160209182019101610380565b505050905090810190601f1680156103cc5780820380516001836020036101000a031916815260200191505b508581038252865181528651602091820191880190808383821561040b575b80518252602083111561040b57601f1990920191602091820191016103eb565b505050905090810190601f1680156104375780820380516001836020036101000a031916815260200191505b509a505050505050505050505060405180910390f35b3415610ab657fe5b610abe6122c1565b6040805160208082528351818301528351919283929083019185810191028083838215610b06575b805182526020831115610b0657601f199092019160209182019101610ae6565b5050509050019250505060405180910390f35b3415610b2157fe5b60408051602060046024803582810135601f81018590048502860185019096528585526100eb958335600160a060020a0316959394604494939290920191819084018382808284375094965061232a95505050505050565b005b3415610b8357fe5b61024f600480803590602001908201803590602001908080601f016020809104026020016040519081016040528093929190818152602001838380828437509496506124cb95505050505050565b6040518087600160a060020a0316600160a060020a0316815260200180602001806020018660ff1660ff168152602001806020018060200185810385528a8181518152602001915080519060200190808383600083146102ca575b8051825260208311156102ca57601f1990920191602091820191016102aa565b505050905090810190601f1680156102f65780820380516001836020036101000a031916815260200191505b5085810384528951815289516020918201918b01908083838215610335575b80518252602083111561033557601f199092019160209182019101610315565b505050905090810190601f1680156103615780820380516001836020036101000a031916815260200191505b50858103835287518152875160209182019189019080838382156103a0575b8051825260208311156103a057601f199092019160209182019101610380565b505050905090810190601f1680156103cc5780820380516001836020036101000a031916815260200191505b508581038252865181528651602091820191880190808383821561040b575b80518252602083111561040b57601f1990920191602091820191016103eb565b505050905090810190601f1680156104375780820380516001836020036101000a031916815260200191505b509a505050505050505050505060405180910390f35b3415610dd757fe5b60408051602060046024803582810135601f81018590048502860185019096528585526100eb958335600160a060020a0316959394604494939290920191819084018382808284375094965061257e95505050505050565b005b3415610e3957fe5b6100eb600160a060020a03600435166128a4565b005b6000805433600160a060020a03908116911614610e6c5760006000fd5b600160a060020a038084166000908152600160205260409020548491161515610e955760006000fd5b83600160a060020a0316600484815481101515610eae57fe5b906000526020600020900160005b9054600160a060020a036101009290920a90041614610edb5760006000fd5b600480546000198101908110610eed57fe5b906000526020600020900160005b9054906101000a9004600160a060020a0316600484815481101515610f1c57fe5b906000526020600020900160005b6101000a815481600160a060020a030219169083600160a060020a031602179055506001600481818054905003915081610f6491906128f0565b50600160a060020a0380851660009081526001602081815260409283902080546003820154855160ff90911695810186905260a0808252838601805460026000199882161561010002989098011687900491830182905293995091909616957f32c54f1e2ea75844ded7517e7dbcd3895da7cd0c28f9ab9f9cf6ecf5f83762c695929489019360048a019260058b0192918291908201906060830190608084019060c08501908b9080156110595780601f1061102e57610100808354040283529160200191611059565b820191906000526020600020905b81548152906001019060200180831161103c57829003601f168201915b505085810384528954600260001961010060018416150201909116048082526020909101908a9080156110cd5780601f106110a2576101008083540402835291602001916110cd565b820191906000526020600020905b8154815290600101906020018083116110b057829003601f168201915b50508581038352875460026000196101006001841615020190911604808252602090910190889080156111415780601f1061111657610100808354040283529160200191611141565b820191906000526020600020905b81548152906001019060200180831161112457829003601f168201915b50508581038252865460026000196101006001841615020190911604808252602090910190879080156111b55780601f1061118a576101008083540402835291602001916111b5565b820191906000526020600020905b81548152906001019060200180831161119857829003601f168201915b5050995050505050505050505060405180910390a2600282600201604051808280546001816001161561010002031660029004801561122b5780601f1061120957610100808354040283529182019161122b565b820191906000526020600020905b815481529060010190602001808311611217575b5050915050908152602001604051809103902060006101000a815490600160a060020a03021916905560038260010160405180828054600181600116156101000203166002900480156112b55780601f106112935761010080835404028352918201916112b5565b820191906000526020600020905b8154815290600101906020018083116112a1575b5050928352505060408051602092819003830190208054600160a060020a0319908116909155600160a060020a038716600090815260019384905291822080549091168155916113079083018261291a565b61131560028301600061291a565b60038201805460ff1916905561132f60048301600061291a565b61133d60058301600061291a565b50505b5b505b505050565b60006003826040518082805190602001908083835b6020831061137c5780518252601f19909201916020918201910161135d565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a0316925050505b919050565b60006002826040518082805190602001908083835b6020831061137c5780518252601f19909201916020918201910161135d565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a0316925050505b919050565b6000805433600160a060020a0390811691161461144b5760006000fd5b600160a060020a0380841660009081526001602052604090205484911615156114745760006000fd5b600160a060020a0384166000818152600160208181526040928390208351848152600582018054600295811615610100026000190116949094049481018590529096507fc3168fdc13112e44a031057dbf6c609b33353addb4d8037d24543e22cbfe2acd9388928291908201906060830190869080156115355780601f1061150a57610100808354040283529160200191611535565b820191906000526020600020905b81548152906001019060200180831161151857829003601f168201915b505083810382528451815284516020918201918601908083838215611575575b80518252602083111561157557601f199092019160209182019101611555565b505050905090810190601f1680156115a15780820380516001836020036101000a031916815260200191505b5094505050505060405180910390a282516115c590600584019060208601906129aa565b505b5b505b505050565b60006115d9612a29565b6115e1612a29565b60006115eb612a29565b6115f3612a29565b6115fb612a4d565b600160a060020a03888116600090815260016020818152604092839020835160c0810185528154909516855280830180548551600261010096831615969096026000190190911694909404601f8101849004840285018401909552848452909385830193928301828280156116b15780601f10611686576101008083540402835291602001916116b1565b820191906000526020600020905b81548152906001019060200180831161169457829003601f168201915b5050509183525050600282810180546040805160206001841615610100026000190190931694909404601f810183900483028501830190915280845293810193908301828280156117435780601f1061171857610100808354040283529160200191611743565b820191906000526020600020905b81548152906001019060200180831161172657829003601f168201915b5050509183525050600382015460ff1660208083019190915260048301805460408051601f600260001960018616156101000201909416939093049283018590048502810185018252828152940193928301828280156117e45780601f106117b9576101008083540402835291602001916117e4565b820191906000526020600020905b8154815290600101906020018083116117c757829003601f168201915b505050918352505060058201805460408051602060026001851615610100026000190190941693909304601f81018490048402820184019092528181529382019392918301828280156118785780601f1061184d57610100808354040283529160200191611878565b820191906000526020600020905b81548152906001019060200180831161185b57829003601f168201915b5050509190925250508151602083015160408401516060850151608086015160a0870151949d50929b50909950975095509350909150505b5091939550919395565b600054600160a060020a031681565b60005433600160a060020a039081169116146118e55760006000fd5b600160a060020a038087166000908152600160205260409020548791161561190d5760006000fd5b86600160a060020a03811615156119245760006000fd5b856000600160a060020a03166002826040518082805190602001908083835b602083106119625780518252601f199092019160209182019101611943565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a03169290921491506119a990505760006000fd5b876000600160a060020a03166003826040518082805190602001908083835b602083106119e75780518252601f1990920191602091820191016119c8565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a0316929092149150611a2e90505760006000fd5b6040805160c081018252600160a060020a038c811680835260208084018e81528486018e905260ff8d166060860152608085018c905260a085018b9052600092835260018083529590922084518154600160a060020a03191694169390931783559051805193949293611aa89385019291909101906129aa565b5060408201518051611ac49160028401916020909101906129aa565b50606082015160038201805460ff191660ff90921691909117905560808201518051611afa9160048401916020909101906129aa565b5060a08201518051611b169160058401916020909101906129aa565b50506004805490915060018101611b2d83826128f0565b916000526020600020900160005b8c909190916101000a815481600160a060020a030219169083600160a060020a0316021790555050896002896040518082805190602001908083835b60208310611b965780518252601f199092019160209182019101611b77565b51815160209384036101000a600019018019909216911617905292019485525060405193849003810184208054600160a060020a031916600160a060020a03969096169590951790945550508a518c926003928d9290918291908401908083835b60208310611c165780518252601f199092019160209182019101611bf7565b51815160209384036101000a60001901801990921691161790529201948552506040805194859003820185208054600160a060020a031916600160a060020a0397881617905560ff8d169085015260a08085528e51908501528d51948f16947fd8d928b0b50ca11d9dc273236b46f3526515b03602f71f3a6af4f45bd9fa9144948f94508e93508d928d928d92918291828201916060840191608085019160c0860191908c01908083838215611ce7575b805182526020831115611ce757601f199092019160209182019101611cc7565b505050905090810190601f168015611d135780820380516001836020036101000a031916815260200191505b5085810384528951815289516020918201918b01908083838215611d52575b805182526020831115611d5257601f199092019160209182019101611d32565b505050905090810190601f168015611d7e5780820380516001836020036101000a031916815260200191505b5085810383528751815287516020918201918901908083838215611dbd575b805182526020831115611dbd57601f199092019160209182019101611d9d565b505050905090810190601f168015611de95780820380516001836020036101000a031916815260200191505b5085810382528651815286516020918201918801908083838215611e28575b805182526020831115611e2857601f199092019160209182019101611e08565b505050905090810190601f168015611e545780820380516001836020036101000a031916815260200191505b50995050505050505050505060405180910390a25b5b505b505b505b505b505050505050565b6000805433600160a060020a03908116911614611e975760006000fd5b600160a060020a038084166000908152600160205260409020548491161515611ec05760006000fd5b826000600160a060020a03166003826040518082805190602001908083835b60208310611efe5780518252601f199092019160209182019101611edf565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a0316929092149150611f4590505760006000fd5b600160a060020a03851660008181526001602081815260409283902083518481528184018054600295811615610100026000190116949094049481018590529097507f4a6dbfc867b179991dec22ff19960f0a94d8d9d891fc556f547764670340e8ae9389928291908201906060830190869080156120055780601f10611fda57610100808354040283529160200191612005565b820191906000526020600020905b815481529060010190602001808311611fe857829003601f168201915b505083810382528451815284516020918201918601908083838215612045575b80518252602083111561204557601f199092019160209182019101612025565b505050905090810190601f1680156120715780820380516001836020036101000a031916815260200191505b5094505050505060405180910390a260038360010160405180828054600181600116156101000203166002900480156120e15780601f106120bf5761010080835404028352918201916120e1565b820191906000526020600020905b8154815290600101906020018083116120cd575b5050928352505060405190819003602090810182208054600160a060020a031916905585518792600392889282918401908083835b602083106121355780518252601f199092019160209182019101612116565b51815160209384036101000a60001901801990921691161790529201948552506040519384900381019093208054600160a060020a031916600160a060020a03959095169490941790935550855161133d925060018601918701906129aa565b505b5b505b505b505050565b600160208190526000918252604090912080546003820154600160a060020a0390911692820191600281019160ff1690600481019060050186565b60048054829081106121ea57fe5b906000526020600020900160005b915054906101000a9004600160a060020a031681565b6000612218612a29565b612220612a29565b600061222a612a29565b612232612a29565b60006003886040518082805190602001908083835b602083106122665780518252601f199092019160209182019101612247565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a031692506122aa91508290506115cf565b9650965096509650965096505b5091939550919395565b6122c9612a29565b600480548060200260200160405190810160405280929190818152602001828054801561231f57602002820191906000526020600020905b8154600160a060020a03168152600190910190602001808311612301575b505050505090505b90565b6000805433600160a060020a039081169116146123475760006000fd5b600160a060020a0380841660009081526001602052604090205484911615156123705760006000fd5b600160a060020a0384166000818152600160208181526040928390208351848152600482018054600295811615610100026000190116949094049481018590529096507f5b19f79ac4e8cfa820815502e11615f1a449e28155dc289ec5cac1a11f9086949388928291908201906060830190869080156124315780601f1061240657610100808354040283529160200191612431565b820191906000526020600020905b81548152906001019060200180831161241457829003601f168201915b505083810382528451815284516020918201918601908083838215612471575b80518252602083111561247157601f199092019160209182019101612451565b505050905090810190601f16801561249d5780820380516001836020036101000a031916815260200191505b5094505050505060405180910390a282516115c590600484019060208601906129aa565b505b5b505b505050565b60006124d5612a29565b6124dd612a29565b60006124e7612a29565b6124ef612a29565b60006002886040518082805190602001908083835b602083106122665780518252601f199092019160209182019101612247565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a031692506122aa91508290506115cf565b9650965096509650965096505b5091939550919395565b6000805433600160a060020a0390811691161461259b5760006000fd5b600160a060020a0380841660009081526001602052604090205484911615156125c45760006000fd5b826000600160a060020a03166002826040518082805190602001908083835b602083106126025780518252601f1990920191602091820191016125e3565b51815160209384036101000a6000190180199092169116179052920194855250604051938490030190922054600160a060020a031692909214915061264990505760006000fd5b600160a060020a038516600081815260016020818152604092839020835184815260028083018054958616156101000260001901909516049481018590529097507f53d878a6530e56c9bc96548fa0a8cae4f1d1f49c86b0e934c086b992ebb6998f9389928291908201906060830190869080156127085780601f106126dd57610100808354040283529160200191612708565b820191906000526020600020905b8154815290600101906020018083116126eb57829003601f168201915b505083810382528451815284516020918201918601908083838215612748575b80518252602083111561274857601f199092019160209182019101612728565b505050905090810190601f1680156127745780820380516001836020036101000a031916815260200191505b5094505050505060405180910390a260028360020160405180828054600181600116156101000203166002900480156127e45780601f106127c25761010080835404028352918201916127e4565b820191906000526020600020905b8154815290600101906020018083116127d0575b5050928352505060405190819003602090810182208054600160a060020a031916905585518792600292889282918401908083835b602083106128385780518252601f199092019160209182019101612819565b51815160209384036101000a60001901801990921691161790529201948552506040519384900381019093208054600160a060020a031916600160a060020a03959095169490941790935550855161133d925060028601918701906129aa565b505b5b505b505b505050565b60005433600160a060020a039081169116146128c05760006000fd5b600160a060020a038116156128eb5760008054600160a060020a031916600160a060020a0383161790555b5b5b50565b81548183558181151161134357600083815260209020611343918101908301612c54565b5b505050565b50805460018160011615610100020316600290046000825580601f1061294057506128eb565b601f0160209004906000526020600020908101906128eb9190612c54565b5b50565b50805460018160011615610100020316600290046000825580601f1061294057506128eb565b601f0160209004906000526020600020908101906128eb9190612c54565b5b50565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106129eb57805160ff1916838001178555612a18565b82800160010185558215612a18579182015b82811115612a185782518255916020019190600101906129fd565b5b50612a25929150612c54565b5090565b60408051602081019091526000815290565b60408051602081019091526000815290565b6040805160c081019091526000815260208101612a68612a29565b8152602001612a75612a29565b815260006020820152604001612a89612a29565b8152602001612a96612a29565b905290565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106129eb57805160ff1916838001178555612a18565b82800160010185558215612a18579182015b82811115612a185782518255916020019190600101906129fd565b5b50612a25929150612c54565b5090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106129eb57805160ff1916838001178555612a18565b82800160010185558215612a18579182015b82811115612a185782518255916020019190600101906129fd565b5b50612a25929150612c54565b5090565b81548183558181151161134357600083815260209020611343918101908301612c54565b5b505050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106129eb57805160ff1916838001178555612a18565b82800160010185558215612a18579182015b82811115612a185782518255916020019190600101906129fd565b5b50612a25929150612c54565b5090565b60408051602081019091526000815290565b61232791905b80821115612a255760008155600101612c5a565b5090565b90565b60408051602081019091526000815290565b604080516020810190915260008152905600a165627a7a72305820f2f49870071c0109a35c5649b17fcc2e8194d1f11d27fa5772c57ddacf6975090029",
+ "networks": {
+ "1": {
+ "links": {},
+ "events": {
+ "0xd8d928b0b50ca11d9dc273236b46f3526515b03602f71f3a6af4f45bd9fa9144": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogAddToken",
+ "type": "event"
+ },
+ "0x32c54f1e2ea75844ded7517e7dbcd3895da7cd0c28f9ab9f9cf6ecf5f83762c6": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogRemoveToken",
+ "type": "event"
+ },
+ "0x4a6dbfc867b179991dec22ff19960f0a94d8d9d891fc556f547764670340e8ae": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldName",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newName",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenNameChange",
+ "type": "event"
+ },
+ "0x53d878a6530e56c9bc96548fa0a8cae4f1d1f49c86b0e934c086b992ebb6998f": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSymbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newSymbol",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenSymbolChange",
+ "type": "event"
+ },
+ "0x5b19f79ac4e8cfa820815502e11615f1a449e28155dc289ec5cac1a11f908694": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldIpfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newIpfsHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenIpfsHashChange",
+ "type": "event"
+ },
+ "0xc3168fdc13112e44a031057dbf6c609b33353addb4d8037d24543e22cbfe2acd": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSwarmHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newSwarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenSwarmHashChange",
+ "type": "event"
+ }
+ },
+ "updated_at": 1502488442000,
+ "address": "0x926a74c5c36adf004c87399e65f75628b0f98d2c"
+ },
+ "3": {
+ "links": {},
+ "events": {
+ "0xd8d928b0b50ca11d9dc273236b46f3526515b03602f71f3a6af4f45bd9fa9144": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogAddToken",
+ "type": "event"
+ },
+ "0x32c54f1e2ea75844ded7517e7dbcd3895da7cd0c28f9ab9f9cf6ecf5f83762c6": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogRemoveToken",
+ "type": "event"
+ },
+ "0x4a6dbfc867b179991dec22ff19960f0a94d8d9d891fc556f547764670340e8ae": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldName",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newName",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenNameChange",
+ "type": "event"
+ },
+ "0x53d878a6530e56c9bc96548fa0a8cae4f1d1f49c86b0e934c086b992ebb6998f": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSymbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newSymbol",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenSymbolChange",
+ "type": "event"
+ },
+ "0x5b19f79ac4e8cfa820815502e11615f1a449e28155dc289ec5cac1a11f908694": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldIpfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newIpfsHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenIpfsHashChange",
+ "type": "event"
+ },
+ "0xc3168fdc13112e44a031057dbf6c609b33353addb4d8037d24543e22cbfe2acd": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSwarmHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newSwarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenSwarmHashChange",
+ "type": "event"
+ }
+ },
+ "updated_at": 1506602007000,
+ "address": "0x6b1a50f0bb5a7995444bd3877b22dc89c62843ed"
+ },
+ "42": {
+ "links": {},
+ "events": {
+ "0xd8d928b0b50ca11d9dc273236b46f3526515b03602f71f3a6af4f45bd9fa9144": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogAddToken",
+ "type": "event"
+ },
+ "0x32c54f1e2ea75844ded7517e7dbcd3895da7cd0c28f9ab9f9cf6ecf5f83762c6": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogRemoveToken",
+ "type": "event"
+ },
+ "0x4a6dbfc867b179991dec22ff19960f0a94d8d9d891fc556f547764670340e8ae": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldName",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newName",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenNameChange",
+ "type": "event"
+ },
+ "0x53d878a6530e56c9bc96548fa0a8cae4f1d1f49c86b0e934c086b992ebb6998f": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSymbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newSymbol",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenSymbolChange",
+ "type": "event"
+ },
+ "0x5b19f79ac4e8cfa820815502e11615f1a449e28155dc289ec5cac1a11f908694": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldIpfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newIpfsHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenIpfsHashChange",
+ "type": "event"
+ },
+ "0xc3168fdc13112e44a031057dbf6c609b33353addb4d8037d24543e22cbfe2acd": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSwarmHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newSwarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenSwarmHashChange",
+ "type": "event"
+ }
+ },
+ "updated_at": 1502391794385,
+ "address": "0xf18e504561f4347bea557f3d4558f559dddbae7f"
+ },
+ "50": {
+ "links": {},
+ "events": {
+ "0xd8d928b0b50ca11d9dc273236b46f3526515b03602f71f3a6af4f45bd9fa9144": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogAddToken",
+ "type": "event"
+ },
+ "0x32c54f1e2ea75844ded7517e7dbcd3895da7cd0c28f9ab9f9cf6ecf5f83762c6": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "decimals",
+ "type": "uint8"
+ },
+ {
+ "indexed": false,
+ "name": "ipfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "swarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogRemoveToken",
+ "type": "event"
+ },
+ "0x4a6dbfc867b179991dec22ff19960f0a94d8d9d891fc556f547764670340e8ae": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldName",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newName",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenNameChange",
+ "type": "event"
+ },
+ "0x53d878a6530e56c9bc96548fa0a8cae4f1d1f49c86b0e934c086b992ebb6998f": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSymbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "name": "newSymbol",
+ "type": "string"
+ }
+ ],
+ "name": "LogTokenSymbolChange",
+ "type": "event"
+ },
+ "0x5b19f79ac4e8cfa820815502e11615f1a449e28155dc289ec5cac1a11f908694": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldIpfsHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newIpfsHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenIpfsHashChange",
+ "type": "event"
+ },
+ "0xc3168fdc13112e44a031057dbf6c609b33353addb4d8037d24543e22cbfe2acd": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "oldSwarmHash",
+ "type": "bytes"
+ },
+ {
+ "indexed": false,
+ "name": "newSwarmHash",
+ "type": "bytes"
+ }
+ ],
+ "name": "LogTokenSwarmHashChange",
+ "type": "event"
+ }
+ },
+ "updated_at": 1503318938228,
+ "address": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082"
+ }
+ },
+ "schema_version": "0.0.5",
+ "updated_at": 1503318938228
+}
diff --git a/packages/0x.js/src/artifacts/TokenTransferProxy.json b/packages/0x.js/src/artifacts/TokenTransferProxy.json
new file mode 100644
index 000000000..beeb16cfe
--- /dev/null
+++ b/packages/0x.js/src/artifacts/TokenTransferProxy.json
@@ -0,0 +1,174 @@
+{
+ "contract_name": "TokenTransferProxy",
+ "abi": [
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "token",
+ "type": "address"
+ },
+ {
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "name": "value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "target",
+ "type": "address"
+ }
+ ],
+ "name": "addAuthorizedAddress",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "name": "authorities",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "target",
+ "type": "address"
+ }
+ ],
+ "name": "removeAuthorizedAddress",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "owner",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "name": "authorized",
+ "outputs": [
+ {
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "getAuthorizedAddresses",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address[]"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "newOwner",
+ "type": "address"
+ }
+ ],
+ "name": "transferOwnership",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "target",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "caller",
+ "type": "address"
+ }
+ ],
+ "name": "LogAuthorizedAddressAdded",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "target",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "caller",
+ "type": "address"
+ }
+ ],
+ "name": "LogAuthorizedAddressRemoved",
+ "type": "event"
+ }
+ ],
+ "unlinked_binary": "0x60606040525b60008054600160a060020a03191633600160a060020a03161790555b5b6106e6806100316000396000f300606060405236156100725763ffffffff60e060020a60003504166315dacbea811461007457806342f1181e146100b3578063494503d4146100d157806370712939146101005780638da5cb5b1461011e578063b91816111461014a578063d39de6e91461017a578063f2fde38b146101e5575bfe5b341561007c57fe5b61009f600160a060020a0360043581169060243581169060443516606435610203565b604080519115158252519081900360200190f35b34156100bb57fe5b6100cf600160a060020a03600435166102ae565b005b34156100d957fe5b6100e4600435610390565b60408051600160a060020a039092168252519081900360200190f35b341561010857fe5b6100cf600160a060020a03600435166103c2565b005b341561012657fe5b6100e461055a565b60408051600160a060020a039092168252519081900360200190f35b341561015257fe5b61009f600160a060020a0360043516610569565b604080519115158252519081900360200190f35b341561018257fe5b61018a61057e565b60408051602080825283518183015283519192839290830191858101910280838382156101d2575b8051825260208311156101d257601f1990920191602091820191016101b2565b5050509050019250505060405180910390f35b34156101ed57fe5b6100cf600160a060020a03600435166105e7565b005b600160a060020a03331660009081526001602052604081205460ff16151561022b5760006000fd5b6040805160006020918201819052825160e060020a6323b872dd028152600160a060020a0388811660048301528781166024830152604482018790529351938916936323b872dd9360648084019491938390030190829087803b151561028d57fe5b6102c65a03f1151561029b57fe5b5050604051519150505b5b949350505050565b60005433600160a060020a039081169116146102ca5760006000fd5b600160a060020a038116600090815260016020526040902054819060ff16156102f35760006000fd5b600160a060020a0382166000908152600160208190526040909120805460ff191682179055600280549091810161032a8382610633565b916000526020600020900160005b81546101009190910a600160a060020a0381810219909216868316918202179092556040513390911692507f94bb87f4c15c4587ff559a7584006fa01ddf9299359be6b512b94527aa961aca90600090a35b5b505b50565b600280548290811061039e57fe5b906000526020600020900160005b915054906101000a9004600160a060020a031681565b6000805433600160a060020a039081169116146103df5760006000fd5b600160a060020a038216600090815260016020526040902054829060ff1615156104095760006000fd5b600160a060020a0383166000908152600160205260408120805460ff1916905591505b6002548210156105195782600160a060020a031660028381548110151561044f57fe5b906000526020600020900160005b9054906101000a9004600160a060020a0316600160a060020a0316141561050d5760028054600019810190811061049057fe5b906000526020600020900160005b9054906101000a9004600160a060020a03166002838154811015156104bf57fe5b906000526020600020900160005b6101000a815481600160a060020a030219169083600160a060020a0316021790555060016002818180549050039150816105079190610633565b50610519565b5b60019091019061042c565b604051600160a060020a0333811691908516907ff5b347a1e40749dd050f5f07fbdbeb7e3efa9756903044dd29401fd1d4bb4a1c90600090a35b5b505b5050565b600054600160a060020a031681565b60016020526000908152604090205460ff1681565b610586610687565b60028054806020026020016040519081016040528092919081815260200182805480156105dc57602002820191906000526020600020905b8154600160a060020a031681526001909101906020018083116105be575b505050505090505b90565b60005433600160a060020a039081169116146106035760006000fd5b600160a060020a0381161561038d5760008054600160a060020a031916600160a060020a0383161790555b5b5b50565b81548183558181151161055357600083815260209020610553918101908301610699565b5b505050565b81548183558181151161055357600083815260209020610553918101908301610699565b5b505050565b60408051602081019091526000815290565b6105e491905b808211156106b3576000815560010161069f565b5090565b905600a165627a7a72305820d2924957bb88a128789172e164d874fe5445218fc2dde2f5eb265839a1f341a20029",
+ "networks": {},
+ "schema_version": "0.0.5",
+ "updated_at": 1503318938227
+}
diff --git a/packages/0x.js/src/bignumber_config.ts b/packages/0x.js/src/bignumber_config.ts
new file mode 100644
index 000000000..2d5214e6f
--- /dev/null
+++ b/packages/0x.js/src/bignumber_config.ts
@@ -0,0 +1,11 @@
+import BigNumber from 'bignumber.js';
+
+export const bigNumberConfigs = {
+ configure() {
+ // By default BigNumber's `toString` method converts to exponential notation if the value has
+ // more then 20 digits. We want to avoid this behavior, so we set EXPONENTIAL_AT to a high number
+ BigNumber.config({
+ EXPONENTIAL_AT: 1000,
+ });
+ },
+};
diff --git a/packages/0x.js/src/contract.ts b/packages/0x.js/src/contract.ts
new file mode 100644
index 000000000..1aacc65dc
--- /dev/null
+++ b/packages/0x.js/src/contract.ts
@@ -0,0 +1,80 @@
+import * as Web3 from 'web3';
+import * as _ from 'lodash';
+import promisify = require('es6-promisify');
+import {SchemaValidator, schemas} from '0x-json-schemas';
+import {AbiType} from './types';
+
+export class Contract implements Web3.ContractInstance {
+ public address: string;
+ public abi: Web3.ContractAbi;
+ private contract: Web3.ContractInstance;
+ private defaults: Partial<Web3.TxData>;
+ private validator: SchemaValidator;
+ // This class instance is going to be populated with functions and events depending on the ABI
+ // and we don't know their types in advance
+ [name: string]: any;
+ constructor(web3ContractInstance: Web3.ContractInstance, defaults: Partial<Web3.TxData>) {
+ this.contract = web3ContractInstance;
+ this.address = web3ContractInstance.address;
+ this.abi = web3ContractInstance.abi;
+ this.defaults = defaults;
+ this.populateEvents();
+ this.populateFunctions();
+ this.validator = new SchemaValidator();
+ }
+ private populateFunctions(): void {
+ const functionsAbi = _.filter(this.abi, abiPart => abiPart.type === AbiType.Function);
+ _.forEach(functionsAbi, (functionAbi: Web3.MethodAbi) => {
+ if (functionAbi.constant) {
+ const cbStyleCallFunction = this.contract[functionAbi.name].call;
+ this[functionAbi.name] = {
+ callAsync: promisify(cbStyleCallFunction, this.contract),
+ };
+ } else {
+ const cbStyleFunction = this.contract[functionAbi.name];
+ const cbStyleEstimateGasFunction = this.contract[functionAbi.name].estimateGas;
+ this[functionAbi.name] = {
+ estimateGasAsync: promisify(cbStyleEstimateGasFunction, this.contract),
+ sendTransactionAsync: this.promisifyWithDefaultParams(cbStyleFunction),
+ };
+ }
+ });
+ }
+ private populateEvents(): void {
+ const eventsAbi = _.filter(this.abi, abiPart => abiPart.type === AbiType.Event);
+ _.forEach(eventsAbi, (eventAbi: Web3.EventAbi) => {
+ this[eventAbi.name] = this.contract[eventAbi.name];
+ });
+ }
+ private promisifyWithDefaultParams(fn: (...args: any[]) => void): (...args: any[]) => Promise<any> {
+ const promisifiedWithDefaultParams = (...args: any[]) => {
+ const promise = new Promise((resolve, reject) => {
+ const lastArg = args[args.length - 1];
+ let txData: Partial<Web3.TxData> = {};
+ if (this.isTxData(lastArg)) {
+ txData = args.pop();
+ }
+ txData = {
+ ...this.defaults,
+ ...txData,
+ };
+ const callback = (err: Error, data: any) => {
+ if (_.isNull(err)) {
+ resolve(data);
+ } else {
+ reject(err);
+ }
+ };
+ args.push(txData);
+ args.push(callback);
+ fn.apply(this.contract, args);
+ });
+ return promise;
+ };
+ return promisifiedWithDefaultParams;
+ }
+ private isTxData(lastArg: any): boolean {
+ const isValid = this.validator.isValid(lastArg, schemas.txDataSchema);
+ return isValid;
+ }
+}
diff --git a/packages/0x.js/src/contract_wrappers/contract_wrapper.ts b/packages/0x.js/src/contract_wrappers/contract_wrapper.ts
new file mode 100644
index 000000000..7997b1647
--- /dev/null
+++ b/packages/0x.js/src/contract_wrappers/contract_wrapper.ts
@@ -0,0 +1,152 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import {BlockAndLogStreamer, Block} from 'ethereumjs-blockstream';
+import {Web3Wrapper} from '../web3_wrapper';
+import {AbiDecoder} from '../utils/abi_decoder';
+import {
+ ZeroExError,
+ InternalZeroExError,
+ Artifact,
+ LogWithDecodedArgs,
+ RawLog,
+ ContractEvents,
+ SubscriptionOpts,
+ IndexedFilterValues,
+ EventCallback,
+ BlockParamLiteral,
+ ContractEventArgs,
+} from '../types';
+import {constants} from '../utils/constants';
+import {intervalUtils} from '../utils/interval_utils';
+import {filterUtils} from '../utils/filter_utils';
+
+export class ContractWrapper {
+ protected _web3Wrapper: Web3Wrapper;
+ private _abiDecoder?: AbiDecoder;
+ private _blockAndLogStreamer: BlockAndLogStreamer|undefined;
+ private _blockAndLogStreamInterval: NodeJS.Timer;
+ private _filters: {[filterToken: string]: Web3.FilterObject};
+ private _filterCallbacks: {[filterToken: string]: EventCallback<ContractEventArgs>};
+ private _onLogAddedSubscriptionToken: string|undefined;
+ private _onLogRemovedSubscriptionToken: string|undefined;
+ constructor(web3Wrapper: Web3Wrapper, abiDecoder?: AbiDecoder) {
+ this._web3Wrapper = web3Wrapper;
+ this._abiDecoder = abiDecoder;
+ this._filters = {};
+ this._filterCallbacks = {};
+ this._blockAndLogStreamer = undefined;
+ this._onLogAddedSubscriptionToken = undefined;
+ this._onLogRemovedSubscriptionToken = undefined;
+ }
+ /**
+ * Cancels all existing subscriptions
+ */
+ public unsubscribeAll(): void {
+ const filterTokens = _.keys(this._filterCallbacks);
+ _.each(filterTokens, filterToken => {
+ this._unsubscribe(filterToken);
+ });
+ }
+ protected _unsubscribe(filterToken: string, err?: Error): void {
+ if (_.isUndefined(this._filters[filterToken])) {
+ throw new Error(ZeroExError.SubscriptionNotFound);
+ }
+ if (!_.isUndefined(err)) {
+ const callback = this._filterCallbacks[filterToken];
+ callback(err, undefined);
+ }
+ delete this._filters[filterToken];
+ delete this._filterCallbacks[filterToken];
+ if (_.isEmpty(this._filters)) {
+ this._stopBlockAndLogStream();
+ }
+ }
+ protected _subscribe<ArgsType extends ContractEventArgs>(
+ address: string, eventName: ContractEvents, indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi,
+ callback: EventCallback<ArgsType>): string {
+ const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi);
+ if (_.isUndefined(this._blockAndLogStreamer)) {
+ this._startBlockAndLogStream();
+ }
+ const filterToken = filterUtils.generateUUID();
+ this._filters[filterToken] = filter;
+ this._filterCallbacks[filterToken] = callback;
+ return filterToken;
+ }
+ protected async _getLogsAsync<ArgsType extends ContractEventArgs>(
+ address: string, eventName: ContractEvents, subscriptionOpts: SubscriptionOpts,
+ indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi, subscriptionOpts);
+ const logs = await this._web3Wrapper.getLogsAsync(filter);
+ const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this));
+ return logsWithDecodedArguments;
+ }
+ protected _tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>(
+ log: Web3.LogEntry): LogWithDecodedArgs<ArgsType>|RawLog {
+ if (_.isUndefined(this._abiDecoder)) {
+ throw new Error(InternalZeroExError.NoAbiDecoder);
+ }
+ const logWithDecodedArgs = this._abiDecoder.tryToDecodeLogOrNoop(log);
+ return logWithDecodedArgs;
+ }
+ protected async _instantiateContractIfExistsAsync<ContractType extends Web3.ContractInstance>(
+ artifact: Artifact, addressIfExists?: string): Promise<ContractType> {
+ const contractInstance =
+ await this._web3Wrapper.getContractInstanceFromArtifactAsync<ContractType>(artifact, addressIfExists);
+ return contractInstance;
+ }
+ private _onLogStateChanged<ArgsType extends ContractEventArgs>(removed: boolean, log: Web3.LogEntry): void {
+ _.forEach(this._filters, (filter: Web3.FilterObject, filterToken: string) => {
+ if (filterUtils.matchesFilter(log, filter)) {
+ const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>;
+ const logEvent = {
+ ...decodedLog,
+ removed,
+ };
+ this._filterCallbacks[filterToken](null, logEvent);
+ }
+ });
+ }
+ private _startBlockAndLogStream(): void {
+ this._blockAndLogStreamer = new BlockAndLogStreamer(
+ this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper),
+ this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper),
+ );
+ const catchAllLogFilter = {};
+ this._blockAndLogStreamer.addLogFilter(catchAllLogFilter);
+ this._blockAndLogStreamInterval = intervalUtils.setAsyncExcludingInterval(
+ this._reconcileBlockAsync.bind(this), constants.DEFAULT_BLOCK_POLLING_INTERVAL,
+ );
+ let removed = false;
+ this._onLogAddedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogAdded(
+ this._onLogStateChanged.bind(this, removed),
+ );
+ removed = true;
+ this._onLogRemovedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogRemoved(
+ this._onLogStateChanged.bind(this, removed),
+ );
+ }
+ private _stopBlockAndLogStream(): void {
+ (this._blockAndLogStreamer as BlockAndLogStreamer).unsubscribeFromOnLogAdded(
+ this._onLogAddedSubscriptionToken as string);
+ (this._blockAndLogStreamer as BlockAndLogStreamer).unsubscribeFromOnLogRemoved(
+ this._onLogRemovedSubscriptionToken as string);
+ intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamInterval);
+ delete this._blockAndLogStreamer;
+ }
+ private async _reconcileBlockAsync(): Promise<void> {
+ try {
+ const latestBlock = await this._web3Wrapper.getBlockAsync(BlockParamLiteral.Latest);
+ // We need to coerce to Block type cause Web3.Block includes types for mempool blocks
+ if (!_.isUndefined(this._blockAndLogStreamer)) {
+ // If we clear the interval while fetching the block - this._blockAndLogStreamer will be undefined
+ this._blockAndLogStreamer.reconcileNewBlock(latestBlock as any as Block);
+ }
+ } catch (err) {
+ const filterTokens = _.keys(this._filterCallbacks);
+ _.each(filterTokens, filterToken => {
+ this._unsubscribe(filterToken, err);
+ });
+ }
+ }
+}
diff --git a/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts b/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts
new file mode 100644
index 000000000..3cd2f0224
--- /dev/null
+++ b/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts
@@ -0,0 +1,87 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {Web3Wrapper} from '../web3_wrapper';
+import {ContractWrapper} from './contract_wrapper';
+import {TokenWrapper} from './token_wrapper';
+import {EtherTokenContract, ZeroExError} from '../types';
+import {assert} from '../utils/assert';
+import {artifacts} from '../artifacts';
+
+/**
+ * This class includes all the functionality related to interacting with a wrapped Ether ERC20 token contract.
+ * The caller can convert ETH into the equivalent number of wrapped ETH ERC20 tokens and back.
+ */
+export class EtherTokenWrapper extends ContractWrapper {
+ private _etherTokenContractIfExists?: EtherTokenContract;
+ private _tokenWrapper: TokenWrapper;
+ private _contractAddressIfExists?: string;
+ constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper, contractAddressIfExists?: string) {
+ super(web3Wrapper);
+ this._tokenWrapper = tokenWrapper;
+ this._contractAddressIfExists = contractAddressIfExists;
+ }
+ /**
+ * Deposit ETH into the Wrapped ETH smart contract and issues the equivalent number of wrapped ETH tokens
+ * to the depositor address. These wrapped ETH tokens can be used in 0x trades and are redeemable for 1-to-1
+ * for ETH.
+ * @param amountInWei Amount of ETH in Wei the caller wishes to deposit.
+ * @param depositor The hex encoded user Ethereum address that would like to make the deposit.
+ * @return Transaction hash.
+ */
+ public async depositAsync(amountInWei: BigNumber, depositor: string): Promise<string> {
+ assert.isValidBaseUnitAmount('amountInWei', amountInWei);
+ await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper);
+
+ const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(depositor);
+ assert.assert(ethBalanceInWei.gte(amountInWei), ZeroExError.InsufficientEthBalanceForDeposit);
+
+ const wethContract = await this._getEtherTokenContractAsync();
+ const txHash = await wethContract.deposit.sendTransactionAsync({
+ from: depositor,
+ value: amountInWei,
+ });
+ return txHash;
+ }
+ /**
+ * Withdraw ETH to the withdrawer's address from the wrapped ETH smart contract in exchange for the
+ * equivalent number of wrapped ETH tokens.
+ * @param amountInWei Amount of ETH in Wei the caller wishes to withdraw.
+ * @param withdrawer The hex encoded user Ethereum address that would like to make the withdrawl.
+ * @return Transaction hash.
+ */
+ public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise<string> {
+ assert.isValidBaseUnitAmount('amountInWei', amountInWei);
+ await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper);
+
+ const wethContractAddress = await this.getContractAddressAsync();
+ const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync(wethContractAddress, withdrawer);
+ assert.assert(WETHBalanceInBaseUnits.gte(amountInWei), ZeroExError.InsufficientWEthBalanceForWithdrawal);
+
+ const wethContract = await this._getEtherTokenContractAsync();
+ const txHash = await wethContract.withdraw.sendTransactionAsync(amountInWei, {
+ from: withdrawer,
+ });
+ return txHash;
+ }
+ /**
+ * Retrieves the Wrapped Ether token contract address
+ * @return The Wrapped Ether token contract address
+ */
+ public async getContractAddressAsync(): Promise<string> {
+ const wethContract = await this._getEtherTokenContractAsync();
+ return wethContract.address;
+ }
+ private _invalidateContractInstance(): void {
+ delete this._etherTokenContractIfExists;
+ }
+ private async _getEtherTokenContractAsync(): Promise<EtherTokenContract> {
+ if (!_.isUndefined(this._etherTokenContractIfExists)) {
+ return this._etherTokenContractIfExists;
+ }
+ const contractInstance = await this._instantiateContractIfExistsAsync<EtherTokenContract>(
+ artifacts.EtherTokenArtifact, this._contractAddressIfExists,
+ );
+ this._etherTokenContractIfExists = contractInstance as EtherTokenContract;
+ return this._etherTokenContractIfExists;
+ }
+}
diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
new file mode 100644
index 000000000..fe0c5bc00
--- /dev/null
+++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
@@ -0,0 +1,866 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {schemas} from '0x-json-schemas';
+import {Web3Wrapper} from '../web3_wrapper';
+import {
+ ECSignature,
+ ExchangeContract,
+ ExchangeContractErrCodes,
+ ExchangeContractErrs,
+ ZeroExError,
+ OrderValues,
+ OrderAddresses,
+ Order,
+ SignedOrder,
+ ExchangeEvents,
+ SubscriptionOpts,
+ IndexedFilterValues,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ LogErrorContractEventArgs,
+ LogFillContractEventArgs,
+ LogCancelContractEventArgs,
+ LogWithDecodedArgs,
+ MethodOpts,
+ ValidateOrderFillableOpts,
+ OrderTransactionOpts,
+ RawLog,
+ EventCallback,
+ ExchangeContractEventArgs,
+ DecodedLogArgs,
+} from '../types';
+import {assert} from '../utils/assert';
+import {utils} from '../utils/utils';
+import {OrderValidationUtils} from '../utils/order_validation_utils';
+import {ContractWrapper} from './contract_wrapper';
+import {TokenWrapper} from './token_wrapper';
+import {decorators} from '../utils/decorators';
+import {AbiDecoder} from '../utils/abi_decoder';
+import {ExchangeTransferSimulator} from '../utils/exchange_transfer_simulator';
+import {artifacts} from '../artifacts';
+
+const SHOULD_VALIDATE_BY_DEFAULT = true;
+
+/**
+ * This class includes all the functionality related to calling methods and subscribing to
+ * events of the 0x Exchange smart contract.
+ */
+export class ExchangeWrapper extends ContractWrapper {
+ private _exchangeContractIfExists?: ExchangeContract;
+ private _orderValidationUtils: OrderValidationUtils;
+ private _tokenWrapper: TokenWrapper;
+ private _exchangeContractErrCodesToMsg = {
+ [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.OrderFillExpired,
+ [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.OrderFillExpired,
+ [ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero,
+ [ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero,
+ [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.OrderFillRoundingError,
+ [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FillBalanceAllowanceError,
+ };
+ private _contractAddressIfExists?: string;
+ private static _getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] {
+ const orderAddresses: OrderAddresses = [
+ order.maker,
+ order.taker,
+ order.makerTokenAddress,
+ order.takerTokenAddress,
+ order.feeRecipient,
+ ];
+ const orderValues: OrderValues = [
+ order.makerTokenAmount,
+ order.takerTokenAmount,
+ order.makerFee,
+ order.takerFee,
+ order.expirationUnixTimestampSec,
+ order.salt,
+ ];
+ return [orderAddresses, orderValues];
+ }
+ constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder,
+ tokenWrapper: TokenWrapper, contractAddressIfExists?: string) {
+ super(web3Wrapper, abiDecoder);
+ this._tokenWrapper = tokenWrapper;
+ this._orderValidationUtils = new OrderValidationUtils(tokenWrapper, this);
+ this._contractAddressIfExists = contractAddressIfExists;
+ }
+ /**
+ * Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total
+ * amount that has been filled or cancelled. The remaining takerAmount can be calculated by
+ * subtracting the unavailable amount from the total order takerAmount.
+ * @param orderHash The hex encoded orderHash for which you would like to retrieve the
+ * unavailable takerAmount.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The amount of the order (in taker tokens) that has either been filled or canceled.
+ */
+ public async getUnavailableTakerAmountAsync(orderHash: string,
+ methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
+
+ const exchangeContract = await this._getExchangeContractAsync();
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ let unavailableTakerTokenAmount = await exchangeContract.getUnavailableTakerTokenAmount.callAsync(
+ orderHash, defaultBlock,
+ );
+ // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
+ unavailableTakerTokenAmount = new BigNumber(unavailableTakerTokenAmount);
+ return unavailableTakerTokenAmount;
+ }
+ /**
+ * Retrieve the takerAmount of an order that has already been filled.
+ * @param orderHash The hex encoded orderHash for which you would like to retrieve the filled takerAmount.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The amount of the order (in taker tokens) that has already been filled.
+ */
+ public async getFilledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
+
+ const exchangeContract = await this._getExchangeContractAsync();
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ let fillAmountInBaseUnits = await exchangeContract.filled.callAsync(orderHash, defaultBlock);
+ // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
+ fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits);
+ return fillAmountInBaseUnits;
+ }
+ /**
+ * Retrieve the takerAmount of an order that has been cancelled.
+ * @param orderHash The hex encoded orderHash for which you would like to retrieve the
+ * cancelled takerAmount.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The amount of the order (in taker tokens) that has been cancelled.
+ */
+ public async getCanceledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
+
+ const exchangeContract = await this._getExchangeContractAsync();
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ let cancelledAmountInBaseUnits = await exchangeContract.cancelled.callAsync(orderHash, defaultBlock);
+ // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
+ cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits);
+ return cancelledAmountInBaseUnits;
+ }
+ /**
+ * Fills a signed order with an amount denominated in baseUnits of the taker token.
+ * Since the order in which transactions are included in the next block is indeterminate, race-conditions
+ * could arise where a users balance or allowance changes before the fillOrder executes. Because of this,
+ * we allow you to specify `shouldThrowOnInsufficientBalanceOrAllowance`.
+ * If false, the smart contract will not throw if the parties
+ * do not have sufficient balances/allowances, preserving gas costs. Setting it to true forgoes this check
+ * and causes the smart contract to throw (using all the gas supplied) instead.
+ * @param signedOrder An object that conforms to the SignedOrder interface.
+ * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that
+ * you wish to fill.
+ * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw
+ * if upon execution the tokens cannot be transferred.
+ * @param takerAddress The user Ethereum address who would like to fill this order.
+ * Must be available via the supplied Web3.Provider
+ * passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber,
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ takerAddress: string,
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
+ assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ }
+
+ const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
+
+ const gas = await exchangeInstance.fillOrder.estimateGasAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ },
+ );
+ const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Sequentially and atomically fills signedOrders up to the specified takerTokenFillAmount.
+ * If the fill amount is reached - it succeeds and does not fill the rest of the orders.
+ * If fill amount is not reached - it fills as much of the fill amount as possible and succeeds.
+ * @param signedOrders The array of signedOrders that you would like to fill until
+ * takerTokenFillAmount is reached.
+ * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
+ * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw if
+ * upon execution any of the tokens cannot be transferred.
+ * If set to false, the call will continue to fill subsequent
+ * signedOrders even when some cannot be filled.
+ * @param takerAddress The user Ethereum address who would like to fill these
+ * orders. Must be available via the supplied Web3.Provider
+ * passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async fillOrdersUpToAsync(signedOrders: SignedOrder[], fillTakerTokenAmount: BigNumber,
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ takerAddress: string,
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
+ const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress);
+ assert.hasAtMostOneUniqueValue(takerTokenAddresses,
+ ExchangeContractErrs.MultipleTakerTokensInFillUpToDisallowed);
+ const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress);
+ assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
+ ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
+ assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
+ assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ for (const signedOrder of signedOrders) {
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ }
+ }
+
+ if (_.isEmpty(signedOrders)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+
+ const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => {
+ return [
+ ...ExchangeWrapper._getOrderAddressesAndValues(signedOrder),
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ ];
+ });
+ // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
+ const [orderAddressesArray, orderValuesArray, vArray, rArray, sArray] = _.unzip<any>(
+ orderAddressesValuesAndSignatureArray,
+ );
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const gas = await exchangeInstance.fillOrdersUpTo.estimateGasAsync(
+ orderAddressesArray,
+ orderValuesArray,
+ fillTakerTokenAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ vArray,
+ rArray,
+ sArray,
+ {
+ from: takerAddress,
+ },
+ );
+ const txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync(
+ orderAddressesArray,
+ orderValuesArray,
+ fillTakerTokenAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ vArray,
+ rArray,
+ sArray,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Batch version of fillOrderAsync.
+ * Executes multiple fills atomically in a single transaction.
+ * If shouldThrowOnInsufficientBalanceOrAllowance is set to false, it will continue filling subsequent orders even
+ * when earlier ones fail.
+ * When shouldThrowOnInsufficientBalanceOrAllowance is set to true, if any fill fails, the entire batch fails.
+ * @param orderFillRequests An array of objects that conform to the
+ * OrderFillRequest interface.
+ * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw
+ * if upon execution any of the tokens cannot be
+ * transferred. If set to false, the call will continue to
+ * fill subsequent signedOrders even when some
+ * cannot be filled.
+ * @param takerAddress The user Ethereum address who would like to fill
+ * these orders. Must be available via the supplied
+ * Web3.Provider passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async batchFillOrdersAsync(orderFillRequests: OrderFillRequest[],
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ takerAddress: string,
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema);
+ const exchangeContractAddresses = _.map(
+ orderFillRequests,
+ orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress,
+ );
+ assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
+ ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
+ assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ for (const orderFillRequest of orderFillRequests) {
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount,
+ takerAddress, zrxTokenAddress,
+ );
+ }
+ }
+ if (_.isEmpty(orderFillRequests)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+
+ const orderAddressesValuesAmountsAndSignatureArray = _.map(orderFillRequests, orderFillRequest => {
+ return [
+ ...ExchangeWrapper._getOrderAddressesAndValues(orderFillRequest.signedOrder),
+ orderFillRequest.takerTokenFillAmount,
+ orderFillRequest.signedOrder.ecSignature.v,
+ orderFillRequest.signedOrder.ecSignature.r,
+ orderFillRequest.signedOrder.ecSignature.s,
+ ];
+ });
+ // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
+ const [orderAddressesArray, orderValuesArray, fillTakerTokenAmounts, vArray, rArray, sArray] = _.unzip<any>(
+ orderAddressesValuesAmountsAndSignatureArray,
+ );
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const gas = await exchangeInstance.batchFillOrders.estimateGasAsync(
+ orderAddressesArray,
+ orderValuesArray,
+ fillTakerTokenAmounts,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ vArray,
+ rArray,
+ sArray,
+ {
+ from: takerAddress,
+ },
+ );
+ const txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync(
+ orderAddressesArray,
+ orderValuesArray,
+ fillTakerTokenAmounts,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ vArray,
+ rArray,
+ sArray,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled,
+ * the fill order is abandoned.
+ * @param signedOrder An object that conforms to the SignedOrder interface. The
+ * signedOrder you wish to fill.
+ * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
+ * @param takerAddress The user Ethereum address who would like to fill this order.
+ * Must be available via the supplied Web3.Provider passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber,
+ takerAddress: string,
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ }
+
+ const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
+
+ const gas = await exchangeInstance.fillOrKillOrder.estimateGasAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ },
+ );
+ const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically
+ * filled (each to the specified fillAmount) or aborted.
+ * @param orderFillRequests An array of objects that conform to the OrderFillRequest interface.
+ * @param takerAddress The user Ethereum address who would like to fill there orders.
+ * Must be available via the supplied Web3.Provider passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async batchFillOrKillAsync(orderFillRequests: OrderFillRequest[],
+ takerAddress: string,
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('orderFillRequests', orderFillRequests,
+ schemas.orderFillRequestsSchema);
+ const exchangeContractAddresses = _.map(
+ orderFillRequests,
+ orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress,
+ );
+ assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
+ ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+ if (_.isEmpty(orderFillRequests)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ for (const orderFillRequest of orderFillRequests) {
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount,
+ takerAddress, zrxTokenAddress,
+ );
+ }
+ }
+
+ const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillRequests, request => {
+ return [
+ ...ExchangeWrapper._getOrderAddressesAndValues(request.signedOrder),
+ request.takerTokenFillAmount,
+ request.signedOrder.ecSignature.v,
+ request.signedOrder.ecSignature.r,
+ request.signedOrder.ecSignature.s,
+ ];
+ });
+
+ // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
+ const [orderAddresses, orderValues, fillTakerTokenAmounts, vParams, rParams, sParams] =
+ _.unzip<any>(orderAddressesValuesAndTakerTokenFillAmounts);
+
+ const gas = await exchangeInstance.batchFillOrKillOrders.estimateGasAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmounts,
+ vParams,
+ rParams,
+ sParams,
+ {
+ from: takerAddress,
+ },
+ );
+ const txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmounts,
+ vParams,
+ rParams,
+ sParams,
+ {
+ from: takerAddress,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Cancel a given fill amount of an order. Cancellations are cumulative.
+ * @param order An object that conforms to the Order or SignedOrder interface.
+ * The order you would like to cancel.
+ * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel.
+ * @param transactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async cancelOrderAsync(order: Order|SignedOrder,
+ cancelTakerTokenAmount: BigNumber,
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('order', order, schemas.orderSchema);
+ assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount);
+ await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper);
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const orderHash = utils.getOrderHashHex(order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
+ order, cancelTakerTokenAmount, unavailableTakerTokenAmount);
+ }
+
+ const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
+ const gas = await exchangeInstance.cancelOrder.estimateGasAsync(
+ orderAddresses,
+ orderValues,
+ cancelTakerTokenAmount,
+ {
+ from: order.maker,
+ },
+ );
+ const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ cancelTakerTokenAmount,
+ {
+ from: order.maker,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Batch version of cancelOrderAsync. Atomically cancels multiple orders in a single transaction.
+ * All orders must be from the same maker.
+ * @param orderCancellationRequests An array of objects that conform to the OrderCancellationRequest
+ * interface.
+ * @param transactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.contractCallErrorHandler
+ public async batchCancelOrdersAsync(orderCancellationRequests: OrderCancellationRequest[],
+ orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
+ assert.doesConformToSchema('orderCancellationRequests', orderCancellationRequests,
+ schemas.orderCancellationRequestsSchema);
+ const exchangeContractAddresses = _.map(
+ orderCancellationRequests,
+ orderCancellationRequest => orderCancellationRequest.order.exchangeContractAddress,
+ );
+ assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
+ ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
+ const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker);
+ assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed);
+ const maker = makers[0];
+ await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper);
+ const shouldValidate = _.isUndefined(orderTransactionOpts) ?
+ SHOULD_VALIDATE_BY_DEFAULT :
+ orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ for (const orderCancellationRequest of orderCancellationRequests) {
+ const orderHash = utils.getOrderHashHex(orderCancellationRequest.order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
+ orderCancellationRequest.order, orderCancellationRequest.takerTokenCancelAmount,
+ unavailableTakerTokenAmount,
+ );
+ }
+
+ }
+ if (_.isEmpty(orderCancellationRequests)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => {
+ return [
+ ...ExchangeWrapper._getOrderAddressesAndValues(cancellationRequest.order),
+ cancellationRequest.takerTokenCancelAmount,
+ ];
+ });
+ // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
+ const [orderAddresses, orderValues, cancelTakerTokenAmounts] =
+ _.unzip<any>(orderAddressesValuesAndTakerTokenCancelAmounts);
+ const gas = await exchangeInstance.batchCancelOrders.estimateGasAsync(
+ orderAddresses,
+ orderValues,
+ cancelTakerTokenAmounts,
+ {
+ from: maker,
+ },
+ );
+ const txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ cancelTakerTokenAmounts,
+ {
+ from: maker,
+ gas,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Subscribe to an event type emitted by the Exchange contract.
+ * @param eventName The exchange contract event you would like to subscribe to.
+ * @param indexFilterValues An object where the keys are indexed args returned by the event and
+ * the value is the value you are interested in. E.g `{maker: aUserAddressHex}`
+ * @param callback Callback that gets called when a log is added/removed
+ * @return Subscription token used later to unsubscribe
+ */
+ public async subscribeAsync<ArgsType extends ExchangeContractEventArgs>(
+ eventName: ExchangeEvents, indexFilterValues: IndexedFilterValues,
+ callback: EventCallback<ArgsType>): Promise<string> {
+ assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ assert.isFunction('callback', callback);
+ const exchangeContractAddress = await this.getContractAddressAsync();
+ const subscriptionToken = this._subscribe<ArgsType>(
+ exchangeContractAddress, eventName, indexFilterValues, artifacts.ExchangeArtifact.abi, callback,
+ );
+ return subscriptionToken;
+ }
+ /**
+ * Cancel a subscription
+ * @param subscriptionToken Subscription token returned by `subscribe()`
+ */
+ public unsubscribe(subscriptionToken: string): void {
+ this._unsubscribe(subscriptionToken);
+ }
+ /**
+ * Gets historical logs without creating a subscription
+ * @param eventName The exchange contract event you would like to subscribe to.
+ * @param subscriptionOpts Subscriptions options that let you configure the subscription.
+ * @param indexFilterValues An object where the keys are indexed args returned by the event and
+ * the value is the value you are interested in. E.g `{_from: aUserAddressHex}`
+ * @return Array of logs that match the parameters
+ */
+ public async getLogsAsync<ArgsType extends ExchangeContractEventArgs>(
+ eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, indexFilterValues: IndexedFilterValues,
+ ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
+ assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ const exchangeContractAddress = await this.getContractAddressAsync();
+ const logs = await this._getLogsAsync<ArgsType>(
+ exchangeContractAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.ExchangeArtifact.abi,
+ );
+ return logs;
+ }
+ /**
+ * Retrieves the Ethereum address of the Exchange contract deployed on the network
+ * that the user-passed web3 provider is connected to.
+ * @returns The Ethereum address of the Exchange contract being used.
+ */
+ public async getContractAddressAsync(): Promise<string> {
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const exchangeAddress = exchangeInstance.address;
+ return exchangeAddress;
+ }
+ /**
+ * Checks if order is still fillable and throws an error otherwise. Useful for orderbook
+ * pruning where you want to remove stale orders without knowing who the taker will be.
+ * @param signedOrder An object that conforms to the SignedOrder interface. The
+ * signedOrder you wish to validate.
+ * @param opts An object that conforms to the ValidateOrderFillableOpts
+ * interface. Allows specifying a specific fillTakerTokenAmount
+ * to validate for.
+ */
+ public async validateOrderFillableOrThrowAsync(
+ signedOrder: SignedOrder, opts?: ValidateOrderFillableOpts,
+ ): Promise<void> {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined;
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateOrderFillableOrThrowAsync(
+ exchangeTradeEmulator, signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount,
+ );
+ }
+ /**
+ * Checks if order fill will succeed and throws an error otherwise.
+ * @param signedOrder An object that conforms to the SignedOrder interface. The
+ * signedOrder you wish to fill.
+ * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
+ * @param takerAddress The user Ethereum address who would like to fill this order.
+ * Must be available via the supplied Web3.Provider passed to 0x.js.
+ */
+ public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ takerAddress: string): Promise<void> {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ }
+ /**
+ * Checks if cancelling a given order will succeed and throws an informative error if it won't.
+ * @param order An object that conforms to the Order or SignedOrder interface.
+ * The order you would like to cancel.
+ * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel.
+ */
+ public async validateCancelOrderThrowIfInvalidAsync(
+ order: Order, cancelTakerTokenAmount: BigNumber): Promise<void> {
+ assert.doesConformToSchema('order', order, schemas.orderSchema);
+ assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount);
+ const orderHash = utils.getOrderHashHex(order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
+ order, cancelTakerTokenAmount, unavailableTakerTokenAmount);
+ }
+ /**
+ * Checks if calling fillOrKill on a given order will succeed and throws an informative error if it won't.
+ * @param signedOrder An object that conforms to the SignedOrder interface. The
+ * signedOrder you wish to fill.
+ * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
+ * @param takerAddress The user Ethereum address who would like to fill this order.
+ * Must be available via the supplied Web3.Provider passed to 0x.js.
+ */
+ public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ takerAddress: string): Promise<void> {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
+ await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ }
+ /**
+ * Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing:
+ * `(fillTakerTokenAmount * makerTokenAmount) / takerTokenAmount`.
+ * 0x Protocol does not accept any trades that result in large rounding errors. This means that tokens with few or
+ * no decimals can only be filled in quantities and ratios that avoid large rounding errors.
+ * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that you wish to fill.
+ * @param takerTokenAmount The order size on the taker side
+ * @param makerTokenAmount The order size on the maker side
+ */
+ public async isRoundingErrorAsync(fillTakerTokenAmount: BigNumber,
+ takerTokenAmount: BigNumber,
+ makerTokenAmount: BigNumber): Promise<boolean> {
+ assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
+ assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount);
+ assert.isValidBaseUnitAmount('makerTokenAmount', makerTokenAmount);
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const isRoundingError = await exchangeInstance.isRoundingError.callAsync(
+ fillTakerTokenAmount, takerTokenAmount, makerTokenAmount,
+ );
+ return isRoundingError;
+ }
+ /**
+ * Checks if logs contain LogError, which is emmited by Exchange contract on transaction failure.
+ * @param logs Transaction logs as returned by `zeroEx.awaitTransactionMinedAsync`
+ */
+ public throwLogErrorsAsErrors(logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>): void {
+ const errLog = _.find(logs, {
+ event: ExchangeEvents.LogError,
+ }) as LogWithDecodedArgs<LogErrorContractEventArgs>|undefined;
+ if (!_.isUndefined(errLog)) {
+ const logArgs = errLog.args;
+ const errCode = logArgs.errorId.toNumber();
+ const errMessage = this._exchangeContractErrCodesToMsg[errCode];
+ throw new Error(errMessage);
+ }
+ }
+ /**
+ * Returns the ZRX token address used by the exchange contract.
+ * @return Address of ZRX token
+ */
+ public async getZRXTokenAddressAsync(): Promise<string> {
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.callAsync();
+ return ZRXtokenAddress;
+ }
+ private async _invalidateContractInstancesAsync(): Promise<void> {
+ this.unsubscribeAll();
+ delete this._exchangeContractIfExists;
+ }
+ private async _isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature,
+ signerAddressHex: string): Promise<boolean> {
+ assert.isHexString('dataHex', dataHex);
+ assert.doesConformToSchema('ecSignature', ecSignature, schemas.ecSignatureSchema);
+ assert.isETHAddressHex('signerAddressHex', signerAddressHex);
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const isValidSignature = await exchangeInstance.isValidSignature.callAsync(
+ signerAddressHex,
+ dataHex,
+ ecSignature.v,
+ ecSignature.r,
+ ecSignature.s,
+ );
+ return isValidSignature;
+ }
+ private async _getOrderHashHexUsingContractCallAsync(order: Order|SignedOrder): Promise<string> {
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
+ const orderHashHex = await exchangeInstance.getOrderHash.callAsync(orderAddresses, orderValues);
+ return orderHashHex;
+ }
+ private async _getExchangeContractAsync(): Promise<ExchangeContract> {
+ if (!_.isUndefined(this._exchangeContractIfExists)) {
+ return this._exchangeContractIfExists;
+ }
+ const contractInstance = await this._instantiateContractIfExistsAsync<ExchangeContract>(
+ artifacts.ExchangeArtifact, this._contractAddressIfExists,
+ );
+ this._exchangeContractIfExists = contractInstance as ExchangeContract;
+ return this._exchangeContractIfExists;
+ }
+ private async _getTokenTransferProxyAddressAsync(): Promise<string> {
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const tokenTransferProxyAddress = await exchangeInstance.TOKEN_TRANSFER_PROXY_CONTRACT.callAsync();
+ const tokenTransferProxyAddressLowerCase = tokenTransferProxyAddress.toLowerCase();
+ return tokenTransferProxyAddressLowerCase;
+ }
+}
diff --git a/packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts
new file mode 100644
index 000000000..2cc5a9aa0
--- /dev/null
+++ b/packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts
@@ -0,0 +1,122 @@
+import * as _ from 'lodash';
+import {Web3Wrapper} from '../web3_wrapper';
+import {assert} from '../utils/assert';
+import {Token, TokenRegistryContract, TokenMetadata} from '../types';
+import {constants} from '../utils/constants';
+import {ContractWrapper} from './contract_wrapper';
+import {artifacts} from '../artifacts';
+
+/**
+ * This class includes all the functionality related to interacting with the 0x Token Registry smart contract.
+ */
+export class TokenRegistryWrapper extends ContractWrapper {
+ private _tokenRegistryContractIfExists?: TokenRegistryContract;
+ private _contractAddressIfExists?: string;
+ constructor(web3Wrapper: Web3Wrapper, contractAddressIfExists?: string) {
+ super(web3Wrapper);
+ this._contractAddressIfExists = contractAddressIfExists;
+ }
+ /**
+ * Retrieves all the tokens currently listed in the Token Registry smart contract
+ * @return An array of objects that conform to the Token interface.
+ */
+ public async getTokensAsync(): Promise<Token[]> {
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+
+ const addresses = await this.getTokenAddressesAsync();
+ const tokenPromises: Array<Promise<Token|undefined>> = _.map(
+ addresses,
+ (address: string) => (this.getTokenIfExistsAsync(address)),
+ );
+ const tokens = await Promise.all(tokenPromises);
+ return tokens as Token[];
+ }
+ /**
+ * Retrieves all the addresses of the tokens currently listed in the Token Registry smart contract
+ * @return An array of token addresses.
+ */
+ public async getTokenAddressesAsync(): Promise<string[]> {
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const addresses = await tokenRegistryContract.getTokenAddresses.callAsync();
+ return addresses;
+ }
+ /**
+ * Retrieves a token by address currently listed in the Token Registry smart contract
+ * @return An object that conforms to the Token interface or undefined if token not found.
+ */
+ public async getTokenIfExistsAsync(address: string): Promise<Token|undefined> {
+ assert.isETHAddressHex('address', address);
+
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const metadata = await tokenRegistryContract.getTokenMetaData.callAsync(address);
+ const token = this._createTokenFromMetadata(metadata);
+ return token;
+ }
+ public async getTokenAddressBySymbolIfExistsAsync(symbol: string): Promise<string|undefined> {
+ assert.isString('symbol', symbol);
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const addressIfExists = await tokenRegistryContract.getTokenAddressBySymbol.callAsync(symbol);
+ if (addressIfExists === constants.NULL_ADDRESS) {
+ return undefined;
+ }
+ return addressIfExists;
+ }
+ public async getTokenAddressByNameIfExistsAsync(name: string): Promise<string|undefined> {
+ assert.isString('name', name);
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const addressIfExists = await tokenRegistryContract.getTokenAddressByName.callAsync(name);
+ if (addressIfExists === constants.NULL_ADDRESS) {
+ return undefined;
+ }
+ return addressIfExists;
+ }
+ public async getTokenBySymbolIfExistsAsync(symbol: string): Promise<Token|undefined> {
+ assert.isString('symbol', symbol);
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const metadata = await tokenRegistryContract.getTokenBySymbol.callAsync(symbol);
+ const token = this._createTokenFromMetadata(metadata);
+ return token;
+ }
+ public async getTokenByNameIfExistsAsync(name: string): Promise<Token|undefined> {
+ assert.isString('name', name);
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const metadata = await tokenRegistryContract.getTokenByName.callAsync(name);
+ const token = this._createTokenFromMetadata(metadata);
+ return token;
+ }
+ /**
+ * Retrieves the Ethereum address of the TokenRegistry contract deployed on the network
+ * that the user-passed web3 provider is connected to.
+ * @returns The Ethereum address of the TokenRegistry contract being used.
+ */
+ public async getContractAddressAsync(): Promise<string> {
+ const tokenRegistryInstance = await this._getTokenRegistryContractAsync();
+ const tokenRegistryAddress = tokenRegistryInstance.address;
+ return tokenRegistryAddress;
+ }
+ private _createTokenFromMetadata(metadata: TokenMetadata): Token|undefined {
+ if (metadata[0] === constants.NULL_ADDRESS) {
+ return undefined;
+ }
+ const token = {
+ address: metadata[0],
+ name: metadata[1],
+ symbol: metadata[2],
+ decimals: metadata[3].toNumber(),
+ };
+ return token;
+ }
+ private _invalidateContractInstance(): void {
+ delete this._tokenRegistryContractIfExists;
+ }
+ private async _getTokenRegistryContractAsync(): Promise<TokenRegistryContract> {
+ if (!_.isUndefined(this._tokenRegistryContractIfExists)) {
+ return this._tokenRegistryContractIfExists;
+ }
+ const contractInstance = await this._instantiateContractIfExistsAsync<TokenRegistryContract>(
+ artifacts.TokenRegistryArtifact, this._contractAddressIfExists,
+ );
+ this._tokenRegistryContractIfExists = contractInstance as TokenRegistryContract;
+ return this._tokenRegistryContractIfExists;
+ }
+}
diff --git a/packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts
new file mode 100644
index 000000000..f81845af9
--- /dev/null
+++ b/packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts
@@ -0,0 +1,60 @@
+import * as _ from 'lodash';
+import {Web3Wrapper} from '../web3_wrapper';
+import {ContractWrapper} from './contract_wrapper';
+import {artifacts} from '../artifacts';
+import {TokenTransferProxyContract} from '../types';
+
+/**
+ * This class includes the functionality related to interacting with the TokenTransferProxy contract.
+ */
+export class TokenTransferProxyWrapper extends ContractWrapper {
+ private _tokenTransferProxyContractIfExists?: TokenTransferProxyContract;
+ private _tokenTransferProxyContractAddressFetcher: () => Promise<string>;
+ constructor(web3Wrapper: Web3Wrapper, tokenTransferProxyContractAddressFetcher: () => Promise<string>) {
+ super(web3Wrapper);
+ this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher;
+ }
+ /**
+ * Check if the Exchange contract address is authorized by the TokenTransferProxy contract.
+ * @param exchangeContractAddress The hex encoded address of the Exchange contract to call.
+ * @return Whether the exchangeContractAddress is authorized.
+ */
+ public async isAuthorizedAsync(exchangeContractAddress: string): Promise<boolean> {
+ const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync();
+ const isAuthorized = await tokenTransferProxyContractInstance.authorized.callAsync(exchangeContractAddress);
+ return isAuthorized;
+ }
+ /**
+ * Get the list of all Exchange contract addresses authorized by the TokenTransferProxy contract.
+ * @return The list of authorized addresses.
+ */
+ public async getAuthorizedAddressesAsync(): Promise<string[]> {
+ const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync();
+ const authorizedAddresses = await tokenTransferProxyContractInstance.getAuthorizedAddresses.callAsync();
+ return authorizedAddresses;
+ }
+ /**
+ * Retrieves the Ethereum address of the TokenTransferProxy contract deployed on the network
+ * that the user-passed web3 provider is connected to.
+ * @returns The Ethereum address of the TokenTransferProxy contract being used.
+ */
+ public async getContractAddressAsync(): Promise<string> {
+ const proxyInstance = await this._getTokenTransferProxyContractAsync();
+ const proxyAddress = proxyInstance.address;
+ return proxyAddress;
+ }
+ private _invalidateContractInstance(): void {
+ delete this._tokenTransferProxyContractIfExists;
+ }
+ private async _getTokenTransferProxyContractAsync(): Promise<TokenTransferProxyContract> {
+ if (!_.isUndefined(this._tokenTransferProxyContractIfExists)) {
+ return this._tokenTransferProxyContractIfExists;
+ }
+ const contractAddress = await this._tokenTransferProxyContractAddressFetcher();
+ const contractInstance = await this._instantiateContractIfExistsAsync<TokenTransferProxyContract>(
+ artifacts.TokenTransferProxyArtifact, contractAddress,
+ );
+ this._tokenTransferProxyContractIfExists = contractInstance as TokenTransferProxyContract;
+ return this._tokenTransferProxyContractIfExists;
+ }
+}
diff --git a/packages/0x.js/src/contract_wrappers/token_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_wrapper.ts
new file mode 100644
index 000000000..614ac19d4
--- /dev/null
+++ b/packages/0x.js/src/contract_wrappers/token_wrapper.ts
@@ -0,0 +1,313 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {schemas} from '0x-json-schemas';
+import {Web3Wrapper} from '../web3_wrapper';
+import {assert} from '../utils/assert';
+import {constants} from '../utils/constants';
+import {ContractWrapper} from './contract_wrapper';
+import {AbiDecoder} from '../utils/abi_decoder';
+import {artifacts} from '../artifacts';
+import {
+ TokenContract,
+ ZeroExError,
+ TokenEvents,
+ IndexedFilterValues,
+ SubscriptionOpts,
+ MethodOpts,
+ LogWithDecodedArgs,
+ EventCallback,
+ TokenContractEventArgs,
+} from '../types';
+
+const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 47275;
+
+/**
+ * This class includes all the functionality related to interacting with ERC20 token contracts.
+ * All ERC20 method calls are supported, along with some convenience methods for getting/setting allowances
+ * to the 0x Proxy smart contract.
+ */
+export class TokenWrapper extends ContractWrapper {
+ public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
+ private _tokenContractsByAddress: {[address: string]: TokenContract};
+ private _tokenTransferProxyContractAddressFetcher: () => Promise<string>;
+ constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder,
+ tokenTransferProxyContractAddressFetcher: () => Promise<string>) {
+ super(web3Wrapper, abiDecoder);
+ this._tokenContractsByAddress = {};
+ this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher;
+ }
+ /**
+ * Retrieves an owner's ERC20 token balance.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address whose balance you would like to check.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The owner's ERC20 token balance in base units.
+ */
+ public async getBalanceAsync(tokenAddress: string, ownerAddress: string,
+ methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+
+ const tokenContract = await this._getTokenContractAsync(tokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ let balance = await tokenContract.balanceOf.callAsync(ownerAddress, defaultBlock);
+ // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
+ balance = new BigNumber(balance);
+ return balance;
+ }
+ /**
+ * Sets the spender's allowance to a specified number of baseUnits on behalf of the owner address.
+ * Equivalent to the ERC20 spec method `approve`.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance
+ * for spenderAddress.
+ * @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance.
+ * @param amountInBaseUnits The allowance amount you would like to set.
+ * @return Transaction hash.
+ */
+ public async setAllowanceAsync(tokenAddress: string, ownerAddress: string, spenderAddress: string,
+ amountInBaseUnits: BigNumber): Promise<string> {
+ await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper);
+ assert.isETHAddressHex('spenderAddress', spenderAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const tokenContract = await this._getTokenContractAsync(tokenAddress);
+ // Hack: for some reason default estimated gas amount causes `base fee exceeds gas limit` exception
+ // on testrpc. Probably related to https://github.com/ethereumjs/testrpc/issues/294
+ // TODO: Debug issue in testrpc and submit a PR, then remove this hack
+ const networkIdIfExists = await this._web3Wrapper.getNetworkIdIfExistsAsync();
+ const gas = networkIdIfExists === constants.TESTRPC_NETWORK_ID ? ALLOWANCE_TO_ZERO_GAS_AMOUNT : undefined;
+ const txHash = await tokenContract.approve.sendTransactionAsync(spenderAddress, amountInBaseUnits, {
+ from: ownerAddress,
+ gas,
+ });
+ return txHash;
+ }
+ /**
+ * Sets the spender's allowance to an unlimited number of baseUnits on behalf of the owner address.
+ * Equivalent to the ERC20 spec method `approve`.
+ * Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating
+ * allowances set to the max amount (e.g ZRX, WETH)
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance
+ * for spenderAddress.
+ * @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance.
+ * @return Transaction hash.
+ */
+ public async setUnlimitedAllowanceAsync(tokenAddress: string, ownerAddress: string,
+ spenderAddress: string): Promise<string> {
+ const txHash = await this.setAllowanceAsync(
+ tokenAddress, ownerAddress, spenderAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ return txHash;
+ }
+ /**
+ * Retrieves the owners allowance in baseUnits set to the spender's address.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address whose allowance to spenderAddress
+ * you would like to retrieve.
+ * @param spenderAddress The hex encoded user Ethereum address who can spend the allowance you are fetching.
+ * @param methodOpts Optional arguments this method accepts.
+ */
+ public async getAllowanceAsync(tokenAddress: string, ownerAddress: string,
+ spenderAddress: string, methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+
+ const tokenContract = await this._getTokenContractAsync(tokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ let allowanceInBaseUnits = await tokenContract.allowance.callAsync(ownerAddress, spenderAddress, defaultBlock);
+ // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
+ allowanceInBaseUnits = new BigNumber(allowanceInBaseUnits);
+ return allowanceInBaseUnits;
+ }
+ /**
+ * Retrieves the owner's allowance in baseUnits set to the 0x proxy contract.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address whose proxy contract allowance we are retrieving.
+ * @param methodOpts Optional arguments this method accepts.
+ */
+ public async getProxyAllowanceAsync(tokenAddress: string, ownerAddress: string,
+ methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+
+ const proxyAddress = await this._getTokenTransferProxyAddressAsync();
+ const allowanceInBaseUnits = await this.getAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, methodOpts);
+ return allowanceInBaseUnits;
+ }
+ /**
+ * Sets the 0x proxy contract's allowance to a specified number of a tokens' baseUnits on behalf
+ * of an owner address.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address who is setting an allowance
+ * for the Proxy contract.
+ * @param amountInBaseUnits The allowance amount specified in baseUnits.
+ * @return Transaction hash.
+ */
+ public async setProxyAllowanceAsync(tokenAddress: string, ownerAddress: string,
+ amountInBaseUnits: BigNumber): Promise<string> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const proxyAddress = await this._getTokenTransferProxyAddressAsync();
+ const txHash = await this.setAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, amountInBaseUnits);
+ return txHash;
+ }
+ /**
+ * Sets the 0x proxy contract's allowance to a unlimited number of a tokens' baseUnits on behalf
+ * of an owner address.
+ * Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating
+ * allowances set to the max amount (e.g ZRX, WETH)
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address who is setting an allowance
+ * for the Proxy contract.
+ * @return Transaction hash.
+ */
+ public async setUnlimitedProxyAllowanceAsync(tokenAddress: string, ownerAddress: string): Promise<string> {
+ const txHash = await this.setProxyAllowanceAsync(
+ tokenAddress, ownerAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ return txHash;
+ }
+ /**
+ * Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param fromAddress The hex encoded user Ethereum address that will send the funds.
+ * @param toAddress The hex encoded user Ethereum address that will receive the funds.
+ * @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer.
+ * @return Transaction hash.
+ */
+ public async transferAsync(tokenAddress: string, fromAddress: string, toAddress: string,
+ amountInBaseUnits: BigNumber): Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper);
+ assert.isETHAddressHex('toAddress', toAddress);
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const tokenContract = await this._getTokenContractAsync(tokenAddress);
+
+ const fromAddressBalance = await this.getBalanceAsync(tokenAddress, fromAddress);
+ if (fromAddressBalance.lessThan(amountInBaseUnits)) {
+ throw new Error(ZeroExError.InsufficientBalanceForTransfer);
+ }
+
+ const txHash = await tokenContract.transfer.sendTransactionAsync(toAddress, amountInBaseUnits, {
+ from: fromAddress,
+ });
+ return txHash;
+ }
+ /**
+ * Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`.
+ * Requires the fromAddress to have sufficient funds and to have approved an allowance of
+ * `amountInBaseUnits` to `senderAddress`.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
+ * @param fromAddress The hex encoded user Ethereum address whose funds are being sent.
+ * @param toAddress The hex encoded user Ethereum address that will receive the funds.
+ * @param senderAddress The hex encoded user Ethereum address whose initiates the fund transfer. The
+ * `fromAddress` must have set an allowance to the `senderAddress`
+ * before this call.
+ * @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer.
+ * @return Transaction hash.
+ */
+ public async transferFromAsync(tokenAddress: string, fromAddress: string, toAddress: string,
+ senderAddress: string, amountInBaseUnits: BigNumber):
+ Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('fromAddress', fromAddress);
+ assert.isETHAddressHex('toAddress', toAddress);
+ await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper);
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const tokenContract = await this._getTokenContractAsync(tokenAddress);
+
+ const fromAddressAllowance = await this.getAllowanceAsync(tokenAddress, fromAddress, senderAddress);
+ if (fromAddressAllowance.lessThan(amountInBaseUnits)) {
+ throw new Error(ZeroExError.InsufficientAllowanceForTransfer);
+ }
+
+ const fromAddressBalance = await this.getBalanceAsync(tokenAddress, fromAddress);
+ if (fromAddressBalance.lessThan(amountInBaseUnits)) {
+ throw new Error(ZeroExError.InsufficientBalanceForTransfer);
+ }
+
+ const txHash = await tokenContract.transferFrom.sendTransactionAsync(
+ fromAddress, toAddress, amountInBaseUnits,
+ {
+ from: senderAddress,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Subscribe to an event type emitted by the Token contract.
+ * @param tokenAddress The hex encoded address where the ERC20 token is deployed.
+ * @param eventName The token contract event you would like to subscribe to.
+ * @param indexFilterValues An object where the keys are indexed args returned by the event and
+ * the value is the value you are interested in. E.g `{maker: aUserAddressHex}`
+ * @param callback Callback that gets called when a log is added/removed
+ * @return Subscription token used later to unsubscribe
+ */
+ public subscribe<ArgsType extends TokenContractEventArgs>(
+ tokenAddress: string, eventName: TokenEvents, indexFilterValues: IndexedFilterValues,
+ callback: EventCallback<ArgsType>): string {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ assert.isFunction('callback', callback);
+ const subscriptionToken = this._subscribe<ArgsType>(
+ tokenAddress, eventName, indexFilterValues, artifacts.TokenArtifact.abi, callback,
+ );
+ return subscriptionToken;
+ }
+ /**
+ * Cancel a subscription
+ * @param subscriptionToken Subscription token returned by `subscribe()`
+ */
+ public unsubscribe(subscriptionToken: string): void {
+ this._unsubscribe(subscriptionToken);
+ }
+ /**
+ * Gets historical logs without creating a subscription
+ * @param tokenAddress An address of the token that emmited the logs.
+ * @param eventName The token contract event you would like to subscribe to.
+ * @param subscriptionOpts Subscriptions options that let you configure the subscription.
+ * @param indexFilterValues An object where the keys are indexed args returned by the event and
+ * the value is the value you are interested in. E.g `{_from: aUserAddressHex}`
+ * @return Array of logs that match the parameters
+ */
+ public async getLogsAsync<ArgsType extends TokenContractEventArgs>(
+ tokenAddress: string, eventName: TokenEvents, subscriptionOpts: SubscriptionOpts,
+ indexFilterValues: IndexedFilterValues): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
+ assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ const logs = await this._getLogsAsync<ArgsType>(
+ tokenAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.TokenArtifact.abi,
+ );
+ return logs;
+ }
+ private _invalidateContractInstancesAsync(): void {
+ this.unsubscribeAll();
+ this._tokenContractsByAddress = {};
+ }
+ private async _getTokenContractAsync(tokenAddress: string): Promise<TokenContract> {
+ let tokenContract = this._tokenContractsByAddress[tokenAddress];
+ if (!_.isUndefined(tokenContract)) {
+ return tokenContract;
+ }
+ const contractInstance = await this._instantiateContractIfExistsAsync<TokenContract>(
+ artifacts.TokenArtifact, tokenAddress,
+ );
+ tokenContract = contractInstance as TokenContract;
+ this._tokenContractsByAddress[tokenAddress] = tokenContract;
+ return tokenContract;
+ }
+ private async _getTokenTransferProxyAddressAsync(): Promise<string> {
+ const tokenTransferProxyContractAddress = await this._tokenTransferProxyContractAddressFetcher();
+ return tokenTransferProxyContractAddress;
+ }
+}
diff --git a/packages/0x.js/src/globals.d.ts b/packages/0x.js/src/globals.d.ts
new file mode 100644
index 000000000..cb3800056
--- /dev/null
+++ b/packages/0x.js/src/globals.d.ts
@@ -0,0 +1,80 @@
+/// <reference types='chai-typescript-typings' />
+/// <reference types='chai-as-promised-typescript-typings' />
+declare module 'web3_beta';
+declare module 'chai-bignumber';
+declare module 'dirty-chai';
+declare module 'request-promise-native';
+declare module 'web3-provider-engine';
+declare module 'web3-provider-engine/subproviders/rpc';
+
+// HACK: In order to merge the bignumber declaration added by chai-bignumber to the chai Assertion
+// interface we must use `namespace` as the Chai definitelyTyped definition does. Since we otherwise
+// disallow `namespace`, we disable tslint for the following.
+/* tslint:disable */
+declare namespace Chai {
+ interface Assertion {
+ bignumber: Assertion;
+ // HACK: In order to comply with chai-as-promised we make eventually a `PromisedAssertion` not an `Assertion`
+ eventually: PromisedAssertion;
+ }
+}
+/* tslint:enable */
+
+declare module '*.json' {
+ const json: any;
+ /* tslint:disable */
+ export default json;
+ /* tslint:enable */
+}
+
+// find-version declarations
+declare function findVersions(version: string): string[];
+declare module 'find-versions' {
+ export = findVersions;
+}
+
+// compare-version declarations
+declare function compareVersions(firstVersion: string, secondVersion: string): number;
+declare module 'compare-versions' {
+ export = compareVersions;
+}
+
+// es6-promisify declarations
+declare function promisify(original: any, settings?: any): ((...arg: any[]) => Promise<any>);
+declare module 'es6-promisify' {
+ export = promisify;
+}
+
+declare module 'ethereumjs-abi' {
+ const soliditySHA3: (argTypes: string[], args: any[]) => Buffer;
+}
+
+// truffle-hdwallet-provider declarations
+declare module 'truffle-hdwallet-provider' {
+ import * as Web3 from 'web3';
+ class HDWalletProvider implements Web3.Provider {
+ constructor(mnemonic: string, rpcUrl: string);
+ public sendAsync(
+ payload: Web3.JSONRPCRequestPayload,
+ callback: (err: Error, result: Web3.JSONRPCResponsePayload) => void,
+ ): void;
+ }
+ export = HDWalletProvider;
+}
+
+// abi-decoder declarations
+interface DecodedLogArg {
+}
+interface DecodedLog {
+ name: string;
+ events: DecodedLogArg[];
+}
+declare module 'abi-decoder' {
+ import * as Web3 from 'web3';
+ const addABI: (abi: Web3.AbiDefinition) => void;
+ const decodeLogs: (logs: Web3.LogEntry[]) => DecodedLog[];
+}
+
+declare module 'web3/lib/solidity/coder' {
+ const decodeParams: (types: string[], data: string) => any[];
+}
diff --git a/packages/0x.js/src/globalsAugment.d.ts b/packages/0x.js/src/globalsAugment.d.ts
new file mode 100644
index 000000000..60e2312a3
--- /dev/null
+++ b/packages/0x.js/src/globalsAugment.d.ts
@@ -0,0 +1,23 @@
+import BigNumber from 'bignumber.js';
+
+// HACK: This module overrides the Chai namespace so that we can use BigNumber types inside.
+// Source: https://github.com/Microsoft/TypeScript/issues/7352#issuecomment-191547232
+declare global {
+ // HACK: In order to merge the bignumber declaration added by chai-bignumber to the chai Assertion
+ // interface we must use `namespace` as the Chai definitelyTyped definition does. Since we otherwise
+ // disallow `namespace`, we disable tslint for the following.
+ /* tslint:disable */
+ namespace Chai {
+ interface NumberComparer {
+ (value: number|BigNumber, message?: string): Assertion;
+ }
+ interface NumericComparison {
+ greaterThan: NumberComparer;
+ }
+ }
+ /* tslint:enable */
+ interface DecodedLogArg {
+ name: string;
+ value: string|BigNumber;
+ }
+}
diff --git a/packages/0x.js/src/index.ts b/packages/0x.js/src/index.ts
new file mode 100644
index 000000000..1b3e893ba
--- /dev/null
+++ b/packages/0x.js/src/index.ts
@@ -0,0 +1,45 @@
+export {ZeroEx} from './0x';
+
+export {
+ Order,
+ SignedOrder,
+ ECSignature,
+ ZeroExError,
+ EventCallback,
+ EventCallbackAsync,
+ EventCallbackSync,
+ ExchangeContractErrs,
+ ContractEvent,
+ Token,
+ ExchangeEvents,
+ TokenEvents,
+ IndexedFilterValues,
+ SubscriptionOpts,
+ BlockParam,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ LogErrorContractEventArgs,
+ LogCancelContractEventArgs,
+ LogFillContractEventArgs,
+ ExchangeContractEventArgs,
+ TransferContractEventArgs,
+ ApprovalContractEventArgs,
+ TokenContractEventArgs,
+ ContractEventArgs,
+ ContractEventArg,
+ Web3Provider,
+ ZeroExConfig,
+ TransactionReceipt,
+ TransactionReceiptWithDecodedLogs,
+ LogWithDecodedArgs,
+ MethodOpts,
+ OrderTransactionOpts,
+ FilterObject,
+ LogEvent,
+ DecodedLogEvent,
+ EventWatcherCallback,
+ OnOrderStateChangeCallback,
+ OrderStateValid,
+ OrderStateInvalid,
+ OrderState,
+} from './types';
diff --git a/packages/0x.js/src/order_watcher/event_watcher.ts b/packages/0x.js/src/order_watcher/event_watcher.ts
new file mode 100644
index 000000000..81529a98c
--- /dev/null
+++ b/packages/0x.js/src/order_watcher/event_watcher.ts
@@ -0,0 +1,88 @@
+import * as Web3 from 'web3';
+import * as _ from 'lodash';
+import {Web3Wrapper} from '../web3_wrapper';
+import {
+ BlockParamLiteral,
+ EventCallback,
+ EventWatcherCallback,
+ ZeroExError,
+} from '../types';
+import {AbiDecoder} from '../utils/abi_decoder';
+import {intervalUtils} from '../utils/interval_utils';
+import {assert} from '../utils/assert';
+import {utils} from '../utils/utils';
+
+const DEFAULT_EVENT_POLLING_INTERVAL = 200;
+
+enum LogEventState {
+ Removed,
+ Added,
+}
+
+/*
+ * The EventWatcher watches for blockchain events at the specified block confirmation
+ * depth.
+ */
+export class EventWatcher {
+ private _web3Wrapper: Web3Wrapper;
+ private _pollingIntervalMs: number;
+ private _intervalIdIfExists?: NodeJS.Timer;
+ private _lastEvents: Web3.LogEntry[] = [];
+ constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) {
+ this._web3Wrapper = web3Wrapper;
+ this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ?
+ DEFAULT_EVENT_POLLING_INTERVAL :
+ pollingIntervalMs;
+ }
+ public subscribe(callback: EventWatcherCallback): void {
+ assert.isFunction('callback', callback);
+ if (!_.isUndefined(this._intervalIdIfExists)) {
+ throw new Error(ZeroExError.SubscriptionAlreadyPresent);
+ }
+ this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval(
+ this._pollForBlockchainEventsAsync.bind(this, callback), this._pollingIntervalMs,
+ );
+ }
+ public unsubscribe(): void {
+ this._lastEvents = [];
+ if (!_.isUndefined(this._intervalIdIfExists)) {
+ intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists);
+ delete this._intervalIdIfExists;
+ }
+ }
+ private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> {
+ const pendingEvents = await this._getEventsAsync();
+ if (pendingEvents.length === 0) {
+ // HACK: Sometimes when node rebuilds the pending block we get back the empty result.
+ // We don't want to emit a lot of removal events and bring them back after a couple of miliseconds,
+ // that's why we just ignore those cases.
+ return;
+ }
+ const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify);
+ const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify);
+ await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback);
+ await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback);
+ this._lastEvents = pendingEvents;
+ }
+ private async _getEventsAsync(): Promise<Web3.LogEntry[]> {
+ const eventFilter = {
+ fromBlock: BlockParamLiteral.Pending,
+ toBlock: BlockParamLiteral.Pending,
+ };
+ const events = await this._web3Wrapper.getLogsAsync(eventFilter);
+ return events;
+ }
+ private async _emitDifferencesAsync(
+ logs: Web3.LogEntry[], logEventState: LogEventState, callback: EventWatcherCallback,
+ ): Promise<void> {
+ for (const log of logs) {
+ const logEvent = {
+ removed: logEventState === LogEventState.Removed,
+ ...log,
+ };
+ if (!_.isUndefined(this._intervalIdIfExists)) {
+ await callback(logEvent);
+ }
+ }
+ }
+}
diff --git a/packages/0x.js/src/order_watcher/order_state_watcher.ts b/packages/0x.js/src/order_watcher/order_state_watcher.ts
new file mode 100644
index 000000000..139f13fdf
--- /dev/null
+++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts
@@ -0,0 +1,232 @@
+import * as _ from 'lodash';
+import {schemas} from '0x-json-schemas';
+import {ZeroEx} from '../0x';
+import {EventWatcher} from './event_watcher';
+import {assert} from '../utils/assert';
+import {utils} from '../utils/utils';
+import {artifacts} from '../artifacts';
+import {AbiDecoder} from '../utils/abi_decoder';
+import {OrderStateUtils} from '../utils/order_state_utils';
+import {
+ LogEvent,
+ OrderState,
+ SignedOrder,
+ Web3Provider,
+ BlockParamLiteral,
+ LogWithDecodedArgs,
+ ContractEventArgs,
+ OnOrderStateChangeCallback,
+ OrderStateWatcherConfig,
+ ApprovalContractEventArgs,
+ TransferContractEventArgs,
+ LogFillContractEventArgs,
+ LogCancelContractEventArgs,
+ ExchangeEvents,
+ TokenEvents,
+ ZeroExError,
+} from '../types';
+import {Web3Wrapper} from '../web3_wrapper';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
+import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
+
+const DEFAULT_NUM_CONFIRMATIONS = 0;
+
+interface DependentOrderHashes {
+ [makerAddress: string]: {
+ [makerToken: string]: Set<string>,
+ };
+}
+
+interface OrderByOrderHash {
+ [orderHash: string]: SignedOrder;
+}
+
+/**
+ * This class includes all the functionality related to watching a set of orders
+ * for potential changes in order validity/fillability. The orderWatcher notifies
+ * the subscriber of these changes so that a final decison can be made on whether
+ * the order should be deemed invalid.
+ */
+export class OrderStateWatcher {
+ private _orderByOrderHash: OrderByOrderHash = {};
+ private _dependentOrderHashes: DependentOrderHashes = {};
+ private _callbackIfExistsAsync?: OnOrderStateChangeCallback;
+ private _eventWatcher: EventWatcher;
+ private _web3Wrapper: Web3Wrapper;
+ private _abiDecoder: AbiDecoder;
+ private _orderStateUtils: OrderStateUtils;
+ private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
+ private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
+ constructor(
+ web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper,
+ config?: OrderStateWatcherConfig,
+ ) {
+ this._abiDecoder = abiDecoder;
+ this._web3Wrapper = web3Wrapper;
+ const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs;
+ this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs);
+ this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token);
+ this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange);
+ this._orderStateUtils = new OrderStateUtils(
+ this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore,
+ );
+ }
+ /**
+ * Add an order to the orderStateWatcher. Before the order is added, it's
+ * signature is verified.
+ * @param signedOrder The order you wish to start watching.
+ */
+ public addOrder(signedOrder: SignedOrder): void {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker);
+ this._orderByOrderHash[orderHash] = signedOrder;
+ this.addToDependentOrderHashes(signedOrder, orderHash);
+ }
+ /**
+ * Removes an order from the orderStateWatcher
+ * @param orderHash The orderHash of the order you wish to stop watching.
+ */
+ public removeOrder(orderHash: string): void {
+ assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
+ const signedOrder = this._orderByOrderHash[orderHash];
+ if (_.isUndefined(signedOrder)) {
+ return; // noop
+ }
+ delete this._orderByOrderHash[orderHash];
+ this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash);
+ }
+ /**
+ * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's
+ * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order.
+ * @param callback Receives the orderHash of the order that should be re-validated, together
+ * with all the order-relevant blockchain state needed to re-validate the order.
+ */
+ public subscribe(callback: OnOrderStateChangeCallback): void {
+ assert.isFunction('callback', callback);
+ if (!_.isUndefined(this._callbackIfExistsAsync)) {
+ throw new Error(ZeroExError.SubscriptionAlreadyPresent);
+ }
+ this._callbackIfExistsAsync = callback;
+ this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this));
+ }
+ /**
+ * Ends an orderStateWatcher subscription.
+ */
+ public unsubscribe(): void {
+ if (_.isUndefined(this._callbackIfExistsAsync)) {
+ throw new Error(ZeroExError.SubscriptionNotFound);
+ }
+ this._balanceAndProxyAllowanceLazyStore.deleteAll();
+ this._orderFilledCancelledLazyStore.deleteAll();
+ delete this._callbackIfExistsAsync;
+ this._eventWatcher.unsubscribe();
+ }
+ private async _onEventWatcherCallbackAsync(log: LogEvent): Promise<void> {
+ const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log);
+ const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event);
+ if (!isLogDecoded) {
+ return; // noop
+ }
+ const decodedLog = maybeDecodedLog as LogWithDecodedArgs<ContractEventArgs>;
+ let makerToken: string;
+ let makerAddress: string;
+ let orderHashesSet: Set<string>;
+ switch (decodedLog.event) {
+ case TokenEvents.Approval:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as ApprovalContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._owner;
+ orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
+ if (!_.isUndefined(orderHashesSet)) {
+ const orderHashes = Array.from(orderHashesSet);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case TokenEvents.Transfer:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as TransferContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from);
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._from;
+ orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
+ if (!_.isUndefined(orderHashesSet)) {
+ const orderHashes = Array.from(orderHashesSet);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case ExchangeEvents.LogFill:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as LogFillContractEventArgs;
+ this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash);
+ // Revalidate orders
+ const orderHash = args.orderHash;
+ const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
+ if (isOrderWatched) {
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ break;
+ }
+ case ExchangeEvents.LogCancel:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as LogCancelContractEventArgs;
+ this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash);
+ // Revalidate orders
+ const orderHash = args.orderHash;
+ const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
+ if (isOrderWatched) {
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ break;
+ }
+ case ExchangeEvents.LogError:
+ return; // noop
+
+ default:
+ throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event);
+ }
+ }
+ private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> {
+ for (const orderHash of orderHashes) {
+ const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder;
+ // Most of these calls will never reach the network because the data is fetched from stores
+ // and only updated when cache is invalidated
+ const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder);
+ if (_.isUndefined(this._callbackIfExistsAsync)) {
+ break; // Unsubscribe was called
+ }
+ await this._callbackIfExistsAsync(orderState);
+ }
+ }
+ private addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string) {
+ if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker])) {
+ this._dependentOrderHashes[signedOrder.maker] = {};
+ }
+ if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress])) {
+ this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set();
+ }
+ this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash);
+ }
+ private removeFromDependentOrderHashes(makerAddress: string, makerTokenAddress: string, orderHash: string) {
+ this._dependentOrderHashes[makerAddress][makerTokenAddress].delete(orderHash);
+ if (this._dependentOrderHashes[makerAddress][makerTokenAddress].size === 0) {
+ delete this._dependentOrderHashes[makerAddress][makerTokenAddress];
+ }
+ if (_.isEmpty(this._dependentOrderHashes[makerAddress])) {
+ delete this._dependentOrderHashes[makerAddress];
+ }
+ }
+}
diff --git a/packages/0x.js/src/schemas/zero_ex_config_schema.ts b/packages/0x.js/src/schemas/zero_ex_config_schema.ts
new file mode 100644
index 000000000..6d4b3ed27
--- /dev/null
+++ b/packages/0x.js/src/schemas/zero_ex_config_schema.ts
@@ -0,0 +1,23 @@
+export const zeroExConfigSchema = {
+ id: '/ZeroExConfig',
+ properties: {
+ gasPrice: {$ref: '/Number'},
+ exchangeContractAddress: {$ref: '/Address'},
+ tokenRegistryContractAddress: {$ref: '/Address'},
+ etherTokenContractAddress: {$ref: '/Address'},
+ orderWatcherConfig: {
+ type: 'object',
+ properties: {
+ pollingIntervalMs: {
+ type: 'number',
+ minimum: 0,
+ },
+ numConfirmations: {
+ type: 'number',
+ minimum: 0,
+ },
+ },
+ },
+ },
+ type: 'object',
+};
diff --git a/packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts b/packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts
new file mode 100644
index 000000000..c83e61606
--- /dev/null
+++ b/packages/0x.js/src/stores/balance_proxy_allowance_lazy_store.ts
@@ -0,0 +1,82 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import {BigNumber} from 'bignumber.js';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {BlockParamLiteral} from '../types';
+
+/**
+ * Copy on read store for balances/proxyAllowances of tokens/accounts
+ */
+export class BalanceAndProxyAllowanceLazyStore {
+ private token: TokenWrapper;
+ private balance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber,
+ },
+ };
+ private proxyAllowance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber,
+ },
+ };
+ constructor(token: TokenWrapper) {
+ this.token = token;
+ this.balance = {};
+ this.proxyAllowance = {};
+ }
+ public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
+ if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts);
+ this.setBalance(tokenAddress, userAddress, balance);
+ }
+ const cachedBalance = this.balance[tokenAddress][userAddress];
+ return cachedBalance;
+ }
+ public setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
+ if (_.isUndefined(this.balance[tokenAddress])) {
+ this.balance[tokenAddress] = {};
+ }
+ this.balance[tokenAddress][userAddress] = balance;
+ }
+ public deleteBalance(tokenAddress: string, userAddress: string): void {
+ if (!_.isUndefined(this.balance[tokenAddress])) {
+ delete this.balance[tokenAddress][userAddress];
+ if (_.isEmpty(this.balance[tokenAddress])) {
+ delete this.balance[tokenAddress];
+ }
+ }
+ }
+ public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
+ if (_.isUndefined(this.proxyAllowance[tokenAddress]) ||
+ _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts);
+ this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
+ }
+ const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress];
+ return cachedProxyAllowance;
+ }
+ public setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
+ if (_.isUndefined(this.proxyAllowance[tokenAddress])) {
+ this.proxyAllowance[tokenAddress] = {};
+ }
+ this.proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
+ }
+ public deleteProxyAllowance(tokenAddress: string, userAddress: string): void {
+ if (!_.isUndefined(this.proxyAllowance[tokenAddress])) {
+ delete this.proxyAllowance[tokenAddress][userAddress];
+ if (_.isEmpty(this.proxyAllowance[tokenAddress])) {
+ delete this.proxyAllowance[tokenAddress];
+ }
+ }
+ }
+ public deleteAll(): void {
+ this.balance = {};
+ this.proxyAllowance = {};
+ }
+}
diff --git a/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts b/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts
new file mode 100644
index 000000000..9d74da096
--- /dev/null
+++ b/packages/0x.js/src/stores/order_filled_cancelled_lazy_store.ts
@@ -0,0 +1,61 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import {BigNumber} from 'bignumber.js';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {BlockParamLiteral} from '../types';
+
+/**
+ * Copy on read store for filled/cancelled taker amounts
+ */
+export class OrderFilledCancelledLazyStore {
+ private exchange: ExchangeWrapper;
+ private filledTakerAmount: {
+ [orderHash: string]: BigNumber,
+ };
+ private cancelledTakerAmount: {
+ [orderHash: string]: BigNumber,
+ };
+ constructor(exchange: ExchangeWrapper) {
+ this.exchange = exchange;
+ this.filledTakerAmount = {};
+ this.cancelledTakerAmount = {};
+ }
+ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ if (_.isUndefined(this.filledTakerAmount[orderHash])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts);
+ this.setFilledTakerAmount(orderHash, filledTakerAmount);
+ }
+ const cachedFilled = this.filledTakerAmount[orderHash];
+ return cachedFilled;
+ }
+ public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void {
+ this.filledTakerAmount[orderHash] = filledTakerAmount;
+ }
+ public deleteFilledTakerAmount(orderHash: string): void {
+ delete this.filledTakerAmount[orderHash];
+ }
+ public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ if (_.isUndefined(this.cancelledTakerAmount[orderHash])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts);
+ this.setCancelledTakerAmount(orderHash, cancelledTakerAmount);
+ }
+ const cachedCancelled = this.cancelledTakerAmount[orderHash];
+ return cachedCancelled;
+ }
+ public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void {
+ this.cancelledTakerAmount[orderHash] = cancelledTakerAmount;
+ }
+ public deleteCancelledTakerAmount(orderHash: string): void {
+ delete this.cancelledTakerAmount[orderHash];
+ }
+ public deleteAll(): void {
+ this.filledTakerAmount = {};
+ this.cancelledTakerAmount = {};
+ }
+}
diff --git a/packages/0x.js/src/subproviders/empty_wallet_subprovider.ts b/packages/0x.js/src/subproviders/empty_wallet_subprovider.ts
new file mode 100644
index 000000000..2f260217c
--- /dev/null
+++ b/packages/0x.js/src/subproviders/empty_wallet_subprovider.ts
@@ -0,0 +1,24 @@
+import {JSONRPCPayload} from '../types';
+
+/*
+ * 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 {
+ public handleRequest(payload: JSONRPCPayload, next: () => void, end: (err: Error|null, result: any) => void) {
+ switch (payload.method) {
+ case 'eth_accounts':
+ end(null, []);
+ return;
+
+ default:
+ next();
+ return;
+ }
+ }
+ // Required to implement this method despite not needing it for this subprovider
+ public setEngine(engine: any) {
+ // noop
+ }
+}
diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts
new file mode 100644
index 000000000..11683378f
--- /dev/null
+++ b/packages/0x.js/src/types.ts
@@ -0,0 +1,525 @@
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+
+export enum ZeroExError {
+ ContractDoesNotExist = 'CONTRACT_DOES_NOT_EXIST',
+ ExchangeContractDoesNotExist = 'EXCHANGE_CONTRACT_DOES_NOT_EXIST',
+ UnhandledError = 'UNHANDLED_ERROR',
+ UserHasNoAssociatedAddress = 'USER_HAS_NO_ASSOCIATED_ADDRESSES',
+ InvalidSignature = 'INVALID_SIGNATURE',
+ ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK',
+ InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER',
+ InsufficientBalanceForTransfer = 'INSUFFICIENT_BALANCE_FOR_TRANSFER',
+ InsufficientEthBalanceForDeposit = 'INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT',
+ InsufficientWEthBalanceForWithdrawal = 'INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL',
+ InvalidJump = 'INVALID_JUMP',
+ OutOfGas = 'OUT_OF_GAS',
+ NoNetworkId = 'NO_NETWORK_ID',
+ SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND',
+ SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT',
+ TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT',
+}
+
+export enum InternalZeroExError {
+ NoAbiDecoder = 'NO_ABI_DECODER',
+ ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
+}
+
+/**
+ * Elliptic Curve signature
+ */
+export interface ECSignature {
+ v: number;
+ r: string;
+ s: string;
+}
+
+export type OrderAddresses = [string, string, string, string, string];
+
+export type OrderValues = [BigNumber, BigNumber, BigNumber,
+ BigNumber, BigNumber, BigNumber];
+
+export type LogEvent = Web3.LogEntryEvent;
+export type DecodedLogEvent<ArgsType> = Web3.DecodedLogEntryEvent<ArgsType>;
+
+export type EventCallbackAsync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => Promise<void>;
+export type EventCallbackSync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => void;
+export type EventCallback<ArgsType> = EventCallbackSync<ArgsType>|EventCallbackAsync<ArgsType>;
+
+export type EventWatcherCallbackSync = (log: LogEvent) => void;
+export type EventWatcherCallbackAsync = (log: LogEvent) => Promise<void>;
+export type EventWatcherCallback = EventWatcherCallbackSync|EventWatcherCallbackAsync;
+
+export interface ExchangeContract extends Web3.ContractInstance {
+ isValidSignature: {
+ callAsync: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string,
+ txOpts?: TxOpts) => Promise<boolean>;
+ };
+ ZRX_TOKEN_CONTRACT: {
+ callAsync: () => Promise<string>;
+ };
+ TOKEN_TRANSFER_PROXY_CONTRACT: {
+ callAsync: () => Promise<string>;
+ };
+ getUnavailableTakerTokenAmount: {
+ callAsync: (orderHash: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
+ };
+ isRoundingError: {
+ callAsync: (takerTokenFillAmount: BigNumber, takerTokenAmount: BigNumber,
+ makerTokenAmount: BigNumber, txOpts?: TxOpts) => Promise<boolean>;
+ };
+ fillOrder: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
+ fillTakerTokenAmount: BigNumber,
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ v: number, r: string, s: string, txOpts?: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
+ fillTakerTokenAmount: BigNumber,
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ v: number, r: string, s: string, txOpts?: TxOpts) => Promise<number>;
+ };
+ batchFillOrders: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ fillTakerTokenAmounts: BigNumber[],
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ fillTakerTokenAmounts: BigNumber[],
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<number>;
+ };
+ fillOrdersUpTo: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ fillTakerTokenAmount: BigNumber,
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ fillTakerTokenAmount: BigNumber,
+ shouldThrowOnInsufficientBalanceOrAllowance: boolean,
+ v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<number>;
+ };
+ cancelOrder: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
+ cancelTakerTokenAmount: BigNumber, txOpts?: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
+ cancelTakerTokenAmount: BigNumber,
+ txOpts?: TxOpts) => Promise<number>;
+ };
+ batchCancelOrders: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ cancelTakerTokenAmounts: BigNumber[], txOpts?: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ cancelTakerTokenAmounts: BigNumber[],
+ txOpts?: TxOpts) => Promise<number>;
+ };
+ fillOrKillOrder: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
+ fillTakerTokenAmount: BigNumber,
+ v: number, r: string, s: string, txOpts?: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
+ fillTakerTokenAmount: BigNumber,
+ v: number, r: string, s: string, txOpts?: TxOpts) => Promise<number>;
+ };
+ batchFillOrKillOrders: {
+ sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ fillTakerTokenAmounts: BigNumber[],
+ v: number[], r: string[], s: string[], txOpts: TxOpts) => Promise<string>;
+ estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
+ fillTakerTokenAmounts: BigNumber[],
+ v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<number>;
+ };
+ filled: {
+ callAsync: (orderHash: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
+ };
+ cancelled: {
+ callAsync: (orderHash: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
+ };
+ getOrderHash: {
+ callAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues) => Promise<string>;
+ };
+}
+
+export interface TokenContract extends Web3.ContractInstance {
+ balanceOf: {
+ callAsync: (address: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
+ };
+ allowance: {
+ callAsync: (ownerAddress: string, allowedAddress: string,
+ defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
+ };
+ transfer: {
+ sendTransactionAsync: (toAddress: string, amountInBaseUnits: BigNumber,
+ txOpts?: TxOpts) => Promise<string>;
+ };
+ transferFrom: {
+ sendTransactionAsync: (fromAddress: string, toAddress: string, amountInBaseUnits: BigNumber,
+ txOpts?: TxOpts) => Promise<string>;
+ };
+ approve: {
+ sendTransactionAsync: (proxyAddress: string, amountInBaseUnits: BigNumber,
+ txOpts?: TxOpts) => Promise<string>;
+ };
+}
+
+export interface TokenRegistryContract extends Web3.ContractInstance {
+ getTokenMetaData: {
+ callAsync: (address: string) => Promise<TokenMetadata>;
+ };
+ getTokenAddresses: {
+ callAsync: () => Promise<string[]>;
+ };
+ getTokenAddressBySymbol: {
+ callAsync: (symbol: string) => Promise<string>;
+ };
+ getTokenAddressByName: {
+ callAsync: (name: string) => Promise<string>;
+ };
+ getTokenBySymbol: {
+ callAsync: (symbol: string) => Promise<TokenMetadata>;
+ };
+ getTokenByName: {
+ callAsync: (name: string) => Promise<TokenMetadata>;
+ };
+}
+
+export interface EtherTokenContract extends Web3.ContractInstance {
+ deposit: {
+ sendTransactionAsync: (txOpts: TxOpts) => Promise<string>;
+ };
+ withdraw: {
+ sendTransactionAsync: (amount: BigNumber, txOpts: TxOpts) => Promise<string>;
+ };
+}
+
+export interface TokenTransferProxyContract extends Web3.ContractInstance {
+ getAuthorizedAddresses: {
+ callAsync: () => Promise<string[]>;
+ };
+ authorized: {
+ callAsync: (address: string) => Promise<boolean>;
+ };
+}
+
+export enum SolidityTypes {
+ Address = 'address',
+ Uint256 = 'uint256',
+ Uint8 = 'uint8',
+ Uint = 'uint',
+}
+
+export enum ExchangeContractErrCodes {
+ ERROR_FILL_EXPIRED, // Order has already expired
+ ERROR_FILL_NO_VALUE, // Order has already been fully filled or cancelled
+ ERROR_FILL_TRUNCATION, // Rounding error too large
+ ERROR_FILL_BALANCE_ALLOWANCE, // Insufficient balance or allowance for token transfer
+ ERROR_CANCEL_EXPIRED, // Order has already expired
+ ERROR_CANCEL_NO_VALUE, // Order has already been fully filled or cancelled
+}
+
+export enum ExchangeContractErrs {
+ OrderFillExpired = 'ORDER_FILL_EXPIRED',
+ OrderCancelExpired = 'ORDER_CANCEL_EXPIRED',
+ OrderCancelAmountZero = 'ORDER_CANCEL_AMOUNT_ZERO',
+ OrderAlreadyCancelledOrFilled = 'ORDER_ALREADY_CANCELLED_OR_FILLED',
+ OrderFillAmountZero = 'ORDER_FILL_AMOUNT_ZERO',
+ OrderRemainingFillAmountZero = 'ORDER_REMAINING_FILL_AMOUNT_ZERO',
+ OrderFillRoundingError = 'ORDER_FILL_ROUNDING_ERROR',
+ FillBalanceAllowanceError = 'FILL_BALANCE_ALLOWANCE_ERROR',
+ InsufficientTakerBalance = 'INSUFFICIENT_TAKER_BALANCE',
+ InsufficientTakerAllowance = 'INSUFFICIENT_TAKER_ALLOWANCE',
+ InsufficientMakerBalance = 'INSUFFICIENT_MAKER_BALANCE',
+ InsufficientMakerAllowance = 'INSUFFICIENT_MAKER_ALLOWANCE',
+ InsufficientTakerFeeBalance = 'INSUFFICIENT_TAKER_FEE_BALANCE',
+ InsufficientTakerFeeAllowance = 'INSUFFICIENT_TAKER_FEE_ALLOWANCE',
+ InsufficientMakerFeeBalance = 'INSUFFICIENT_MAKER_FEE_BALANCE',
+ InsufficientMakerFeeAllowance = 'INSUFFICIENT_MAKER_FEE_ALLOWANCE',
+ TransactionSenderIsNotFillOrderTaker = 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER',
+ MultipleMakersInSingleCancelBatchDisallowed = 'MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH_DISALLOWED',
+ InsufficientRemainingFillAmount = 'INSUFFICIENT_REMAINING_FILL_AMOUNT',
+ MultipleTakerTokensInFillUpToDisallowed = 'MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO_DISALLOWED',
+ BatchOrdersMustHaveSameExchangeAddress = 'BATCH_ORDERS_MUST_HAVE_SAME_EXCHANGE_ADDRESS',
+ BatchOrdersMustHaveAtLeastOneItem = 'BATCH_ORDERS_MUST_HAVE_AT_LEAST_ONE_ITEM',
+}
+
+export type RawLog = Web3.LogEntry;
+
+export interface ContractEvent {
+ logIndex: number;
+ transactionIndex: number;
+ transactionHash: string;
+ blockHash: string;
+ blockNumber: number;
+ address: string;
+ type: string;
+ event: string;
+ args: ContractEventArgs;
+}
+
+export interface LogFillContractEventArgs {
+ maker: string;
+ taker: string;
+ feeRecipient: string;
+ makerToken: string;
+ takerToken: string;
+ filledMakerTokenAmount: BigNumber;
+ filledTakerTokenAmount: BigNumber;
+ paidMakerFee: BigNumber;
+ paidTakerFee: BigNumber;
+ tokens: string;
+ orderHash: string;
+}
+export interface LogCancelContractEventArgs {
+ maker: string;
+ feeRecipient: string;
+ makerToken: string;
+ takerToken: string;
+ cancelledMakerTokenAmount: BigNumber;
+ cancelledTakerTokenAmount: BigNumber;
+ tokens: string;
+ orderHash: string;
+}
+export interface LogErrorContractEventArgs {
+ errorId: BigNumber;
+ orderHash: string;
+}
+export type ExchangeContractEventArgs = LogFillContractEventArgs|LogCancelContractEventArgs|LogErrorContractEventArgs;
+export interface TransferContractEventArgs {
+ _from: string;
+ _to: string;
+ _value: BigNumber;
+}
+export interface ApprovalContractEventArgs {
+ _owner: string;
+ _spender: string;
+ _value: BigNumber;
+}
+export type TokenContractEventArgs = TransferContractEventArgs|ApprovalContractEventArgs;
+export type ContractEventArgs = ExchangeContractEventArgs|TokenContractEventArgs;
+export type ContractEventArg = string|BigNumber;
+
+export interface Order {
+ maker: string;
+ taker: string;
+ makerFee: BigNumber;
+ takerFee: BigNumber;
+ makerTokenAmount: BigNumber;
+ takerTokenAmount: BigNumber;
+ makerTokenAddress: string;
+ takerTokenAddress: string;
+ salt: BigNumber;
+ exchangeContractAddress: string;
+ feeRecipient: string;
+ expirationUnixTimestampSec: BigNumber;
+}
+
+export interface SignedOrder extends Order {
+ ecSignature: ECSignature;
+}
+
+// [address, name, symbol, decimals, ipfsHash, swarmHash]
+export type TokenMetadata = [string, string, string, BigNumber, string, string];
+
+export interface Token {
+ name: string;
+ address: string;
+ symbol: string;
+ decimals: number;
+}
+
+export interface TxOpts {
+ from: string;
+ gas?: number;
+ value?: BigNumber;
+}
+
+export interface TokenAddressBySymbol {
+ [symbol: string]: string;
+}
+
+export enum ExchangeEvents {
+ LogFill = 'LogFill',
+ LogCancel = 'LogCancel',
+ LogError = 'LogError',
+}
+
+export enum TokenEvents {
+ Transfer = 'Transfer',
+ Approval = 'Approval',
+}
+
+export type ContractEvents = TokenEvents|ExchangeEvents;
+
+export interface IndexedFilterValues {
+ [index: string]: ContractEventArg;
+}
+
+export enum BlockParamLiteral {
+ Latest = 'latest',
+ Earliest = 'earliest',
+ Pending = 'pending',
+}
+
+export type BlockParam = BlockParamLiteral|number;
+
+export interface SubscriptionOpts {
+ fromBlock: BlockParam;
+ toBlock: BlockParam;
+}
+
+export type DoneCallback = (err?: Error) => void;
+
+export interface OrderCancellationRequest {
+ order: Order|SignedOrder;
+ takerTokenCancelAmount: BigNumber;
+}
+
+export interface OrderFillRequest {
+ signedOrder: SignedOrder;
+ takerTokenFillAmount: BigNumber;
+}
+
+export type AsyncMethod = (...args: any[]) => Promise<any>;
+
+/**
+ * We re-export the `Web3.Provider` type specified in the Web3 Typescript typings
+ * since it is the type of the `provider` argument to the `ZeroEx` constructor.
+ * It is however a `Web3` library type, not a native `0x.js` type.
+ */
+export type Web3Provider = Web3.Provider;
+
+export interface ExchangeContractByAddress {
+ [address: string]: ExchangeContract;
+}
+
+export interface JSONRPCPayload {
+ params: any[];
+ method: string;
+}
+
+/*
+ * eventPollingIntervalMs: How often to poll the Ethereum node for new events
+ */
+export interface OrderStateWatcherConfig {
+ eventPollingIntervalMs?: number;
+}
+
+/*
+ * gasPrice: Gas price to use with every transaction
+ * exchangeContractAddress: The address of an exchange contract to use
+ * tokenRegistryContractAddress: The address of a token registry contract to use
+ * etherTokenContractAddress: The address of an ether token contract to use
+ * orderWatcherConfig: All the configs related to the orderWatcher
+ */
+export interface ZeroExConfig {
+ gasPrice?: BigNumber; // Gas price to use with every transaction
+ exchangeContractAddress?: string;
+ tokenRegistryContractAddress?: string;
+ etherTokenContractAddress?: string;
+ orderWatcherConfig?: OrderStateWatcherConfig;
+}
+
+export enum AbiType {
+ Function = 'function',
+ Constructor = 'constructor',
+ Event = 'event',
+ Fallback = 'fallback',
+}
+
+export interface DecodedLogArgs {
+ [argName: string]: ContractEventArg;
+}
+
+export interface LogWithDecodedArgs<ArgsType> extends Web3.DecodedLogEntry<ArgsType> {}
+
+export interface TransactionReceiptWithDecodedLogs extends TransactionReceipt {
+ logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>;
+}
+
+export interface Artifact {
+ abi: any;
+ networks: {[networkId: number]: {
+ address: string;
+ }};
+}
+
+/*
+ * expectedFillTakerTokenAmount: If specified, the validation method will ensure that the
+ * supplied order maker has a sufficient allowance/balance to fill this amount of the order's
+ * takerTokenAmount. If not specified, the validation method ensures that the maker has a sufficient
+ * allowance/balance to fill the entire remaining order amount.
+ */
+export interface ValidateOrderFillableOpts {
+ expectedFillTakerTokenAmount?: BigNumber;
+}
+
+/*
+ * defaultBlock: The block up to which to query the blockchain state. Setting this to a historical block number
+ * let's the user query the blockchain's state at an arbitrary point in time. In order for this to work, the
+ * backing Ethereum node must keep the entire historical state of the chain (e.g setting `--pruning=archive`
+ * flag when running Parity).
+ */
+export interface MethodOpts {
+ defaultBlock?: Web3.BlockParam;
+}
+
+/*
+ * shouldValidate: Flag indicating whether the library should make attempts to validate a transaction before
+ * broadcasting it. For example, order has a valid signature, maker has sufficient funds, etc.
+ */
+export interface OrderTransactionOpts {
+ shouldValidate: boolean;
+}
+
+export type FilterObject = Web3.FilterObject;
+
+export enum TradeSide {
+ Maker = 'maker',
+ Taker = 'taker',
+}
+
+export enum TransferType {
+ Trade = 'trade',
+ Fee = 'fee',
+}
+
+export interface OrderRelevantState {
+ makerBalance: BigNumber;
+ makerProxyAllowance: BigNumber;
+ makerFeeBalance: BigNumber;
+ makerFeeProxyAllowance: BigNumber;
+ filledTakerTokenAmount: BigNumber;
+ canceledTakerTokenAmount: BigNumber;
+ remainingFillableMakerTokenAmount: BigNumber;
+}
+
+export interface OrderStateValid {
+ isValid: true;
+ orderHash: string;
+ orderRelevantState: OrderRelevantState;
+}
+
+export interface OrderStateInvalid {
+ isValid: false;
+ orderHash: string;
+ error: ExchangeContractErrs;
+}
+
+export type OrderState = OrderStateValid|OrderStateInvalid;
+
+export type OnOrderStateChangeCallbackSync = (orderState: OrderState) => void;
+export type OnOrderStateChangeCallbackAsync = (orderState: OrderState) => Promise<void>;
+export type OnOrderStateChangeCallback = OnOrderStateChangeCallbackAsync|OnOrderStateChangeCallbackSync;
+
+export interface TransactionReceipt {
+ blockHash: string;
+ blockNumber: number;
+ transactionHash: string;
+ transactionIndex: number;
+ from: string;
+ to: string;
+ status: null|0|1;
+ cumulativeGasUsed: number;
+ gasUsed: number;
+ contractAddress: string|null;
+ logs: Web3.LogEntry[];
+}
diff --git a/packages/0x.js/src/utils/abi_decoder.ts b/packages/0x.js/src/utils/abi_decoder.ts
new file mode 100644
index 000000000..840ad9be0
--- /dev/null
+++ b/packages/0x.js/src/utils/abi_decoder.ts
@@ -0,0 +1,68 @@
+import * as Web3 from 'web3';
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {AbiType, DecodedLogArgs, LogWithDecodedArgs, RawLog, SolidityTypes, ContractEventArgs} from '../types';
+import * as SolidityCoder from 'web3/lib/solidity/coder';
+
+export class AbiDecoder {
+ private savedABIs: Web3.AbiDefinition[] = [];
+ private methodIds: {[signatureHash: string]: Web3.EventAbi} = {};
+ constructor(abiArrays: Web3.AbiDefinition[][]) {
+ _.map(abiArrays, this.addABI.bind(this));
+ }
+ // This method can only decode logs from the 0x & ERC20 smart contracts
+ public tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>(
+ log: Web3.LogEntry): LogWithDecodedArgs<ArgsType>|RawLog {
+ const methodId = log.topics[0];
+ const event = this.methodIds[methodId];
+ if (_.isUndefined(event)) {
+ return log;
+ }
+ const logData = log.data;
+ const decodedParams: DecodedLogArgs = {};
+ let dataIndex = 0;
+ let topicsIndex = 1;
+
+ const nonIndexedInputs = _.filter(event.inputs, input => !input.indexed);
+ const dataTypes = _.map(nonIndexedInputs, input => input.type);
+ const decodedData = SolidityCoder.decodeParams(dataTypes, logData.slice('0x'.length));
+
+ _.map(event.inputs, (param: Web3.EventParameter) => {
+ // Indexed parameters are stored in topics. Non-indexed ones in decodedData
+ let value = param.indexed ? log.topics[topicsIndex++] : decodedData[dataIndex++];
+ if (param.type === SolidityTypes.Address) {
+ value = this.padZeros(new BigNumber(value).toString(16));
+ } else if (param.type === SolidityTypes.Uint256 ||
+ param.type === SolidityTypes.Uint8 ||
+ param.type === SolidityTypes.Uint ) {
+ value = new BigNumber(value);
+ }
+ decodedParams[param.name] = value;
+ });
+
+ return {
+ ...log,
+ event: event.name,
+ args: decodedParams,
+ };
+ }
+ private addABI(abiArray: Web3.AbiDefinition[]): void {
+ _.map(abiArray, (abi: Web3.AbiDefinition) => {
+ if (abi.type === AbiType.Event) {
+ const signature = `${abi.name}(${_.map(abi.inputs, input => input.type).join(',')})`;
+ const signatureHash = new Web3().sha3(signature);
+ this.methodIds[signatureHash] = abi;
+ }
+ });
+ this.savedABIs = this.savedABIs.concat(abiArray);
+ }
+ private padZeros(address: string) {
+ let formatted = address;
+ if (_.startsWith(formatted, '0x')) {
+ formatted = formatted.slice(2);
+ }
+
+ formatted = _.padStart(formatted, 40, '0');
+ return `0x${formatted}`;
+ }
+}
diff --git a/packages/0x.js/src/utils/assert.ts b/packages/0x.js/src/utils/assert.ts
new file mode 100644
index 000000000..e5c9439f3
--- /dev/null
+++ b/packages/0x.js/src/utils/assert.ts
@@ -0,0 +1,101 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {SchemaValidator, Schema} from '0x-json-schemas';
+import {Web3Wrapper} from '../web3_wrapper';
+import {signatureUtils} from '../utils/signature_utils';
+import {ECSignature} from '../types';
+
+const HEX_REGEX = /^0x[0-9A-F]*$/i;
+
+export const assert = {
+ isBigNumber(variableName: string, value: BigNumber): void {
+ const isBigNumber = _.isObject(value) && (value as any).isBigNumber;
+ this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value));
+ },
+ isValidBaseUnitAmount(variableName: string, value: BigNumber) {
+ assert.isBigNumber(variableName, value);
+ const hasDecimals = value.decimalPlaces() !== 0;
+ this.assert(
+ !hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`,
+ );
+ },
+ isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) {
+ const isValidSignature = signatureUtils.isValidSignature(orderHash, ecSignature, signerAddress);
+ this.assert(isValidSignature, `Expected order with hash '${orderHash}' to have a valid signature`);
+ },
+ isUndefined(value: any, variableName?: string): void {
+ this.assert(_.isUndefined(value), this.typeAssertionMessage(variableName, 'undefined', value));
+ },
+ isString(variableName: string, value: string): void {
+ this.assert(_.isString(value), this.typeAssertionMessage(variableName, 'string', value));
+ },
+ isFunction(variableName: string, value: any): void {
+ this.assert(_.isFunction(value), this.typeAssertionMessage(variableName, 'function', value));
+ },
+ isHexString(variableName: string, value: string): void {
+ this.assert(_.isString(value) && HEX_REGEX.test(value),
+ this.typeAssertionMessage(variableName, 'HexString', value));
+ },
+ isETHAddressHex(variableName: string, value: string): void {
+ const web3 = new Web3();
+ this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value));
+ this.assert(
+ web3.isAddress(value) && value.toLowerCase() === value,
+ `Checksummed addresses are not supported. Convert ${variableName} to lower case before passing`,
+ );
+ },
+ doesBelongToStringEnum(variableName: string, value: string,
+ stringEnum: any /* There is no base type for every string enum */): void {
+ const doesBelongToStringEnum = !_.isUndefined(stringEnum[value]);
+ const enumValues = _.keys(stringEnum);
+ const enumValuesAsStrings = _.map(enumValues, enumValue => `'${enumValue}'`);
+ const enumValuesAsString = enumValuesAsStrings.join(', ');
+ assert.assert(
+ doesBelongToStringEnum,
+ `Expected ${variableName} to be one of: ${enumValuesAsString}, encountered: ${value}`,
+ );
+ },
+ async isSenderAddressAsync(variableName: string, senderAddressHex: string,
+ web3Wrapper: Web3Wrapper): Promise<void> {
+ assert.isETHAddressHex(variableName, senderAddressHex);
+ const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex);
+ assert.assert(isSenderAddressAvailable,
+ `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`,
+ );
+ },
+ async isUserAddressAvailableAsync(web3Wrapper: Web3Wrapper): Promise<void> {
+ const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ this.assert(!_.isEmpty(availableAddresses), 'No addresses were available on the provided web3 provider');
+ },
+ hasAtMostOneUniqueValue(value: any[], errMsg: string): void {
+ this.assert(_.uniq(value).length <= 1, errMsg);
+ },
+ isNumber(variableName: string, value: number): void {
+ this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value));
+ },
+ isBoolean(variableName: string, value: boolean): void {
+ this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value));
+ },
+ isWeb3Provider(variableName: string, value: Web3.Provider): void {
+ const isWeb3Provider = _.isFunction((value as any).send) || _.isFunction((value as any).sendAsync);
+ this.assert(isWeb3Provider, this.typeAssertionMessage(variableName, 'Web3.Provider', value));
+ },
+ doesConformToSchema(variableName: string, value: any, schema: Schema): void {
+ const schemaValidator = new SchemaValidator();
+ const validationResult = schemaValidator.validate(value, schema);
+ const hasValidationErrors = validationResult.errors.length > 0;
+ const msg = `Expected ${variableName} to conform to schema ${schema.id}
+Encountered: ${JSON.stringify(value, null, '\t')}
+Validation errors: ${validationResult.errors.join(', ')}`;
+ this.assert(!hasValidationErrors, msg);
+ },
+ assert(condition: boolean, message: string): void {
+ if (!condition) {
+ throw new Error(message);
+ }
+ },
+ typeAssertionMessage(variableName: string, type: string, value: any): string {
+ return `Expected ${variableName} to be of type ${type}, encountered: ${value}`;
+ },
+};
diff --git a/packages/0x.js/src/utils/constants.ts b/packages/0x.js/src/utils/constants.ts
new file mode 100644
index 000000000..3de3f5bc1
--- /dev/null
+++ b/packages/0x.js/src/utils/constants.ts
@@ -0,0 +1,11 @@
+import BigNumber from 'bignumber.js';
+
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ TESTRPC_NETWORK_ID: 50,
+ MAX_DIGITS_IN_UNSIGNED_256_INT: 78,
+ INVALID_JUMP_PATTERN: 'invalid JUMP at',
+ OUT_OF_GAS_PATTERN: 'out of gas',
+ UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
+ DEFAULT_BLOCK_POLLING_INTERVAL: 1000,
+};
diff --git a/packages/0x.js/src/utils/decorators.ts b/packages/0x.js/src/utils/decorators.ts
new file mode 100644
index 000000000..ec750b891
--- /dev/null
+++ b/packages/0x.js/src/utils/decorators.ts
@@ -0,0 +1,35 @@
+import * as _ from 'lodash';
+import {constants} from './constants';
+import {AsyncMethod, ZeroExError} from '../types';
+
+export const decorators = {
+ /**
+ * Source: https://stackoverflow.com/a/29837695/3546986
+ */
+ contractCallErrorHandler(target: object,
+ key: string|symbol,
+ descriptor: TypedPropertyDescriptor<AsyncMethod>,
+ ): TypedPropertyDescriptor<AsyncMethod> {
+ const originalMethod = (descriptor.value as AsyncMethod);
+
+ // Do not use arrow syntax here. Use a function expression in
+ // order to use the correct value of `this` in this method
+ // tslint:disable-next-line:only-arrow-functions
+ descriptor.value = async function(...args: any[]) {
+ try {
+ const result = await originalMethod.apply(this, args);
+ return result;
+ } catch (error) {
+ if (_.includes(error.message, constants.INVALID_JUMP_PATTERN)) {
+ throw new Error(ZeroExError.InvalidJump);
+ }
+ if (_.includes(error.message, constants.OUT_OF_GAS_PATTERN)) {
+ throw new Error(ZeroExError.OutOfGas);
+ }
+ throw error;
+ }
+ };
+
+ return descriptor;
+ },
+};
diff --git a/packages/0x.js/src/utils/exchange_transfer_simulator.ts b/packages/0x.js/src/utils/exchange_transfer_simulator.ts
new file mode 100644
index 000000000..308ef06db
--- /dev/null
+++ b/packages/0x.js/src/utils/exchange_transfer_simulator.ts
@@ -0,0 +1,88 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
+
+enum FailureReason {
+ Balance = 'balance',
+ ProxyAllowance = 'proxyAllowance',
+}
+
+const ERR_MSG_MAPPING = {
+ [FailureReason.Balance]: {
+ [TradeSide.Maker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance,
+ },
+ [TradeSide.Taker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance,
+ },
+ },
+ [FailureReason.ProxyAllowance]: {
+ [TradeSide.Maker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance,
+ },
+ [TradeSide.Taker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance,
+ },
+ },
+};
+
+export class ExchangeTransferSimulator {
+ private store: BalanceAndProxyAllowanceLazyStore;
+ private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber;
+ constructor(token: TokenWrapper) {
+ this.store = new BalanceAndProxyAllowanceLazyStore(token);
+ this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
+ }
+ /**
+ * Simulates transferFrom call performed by a proxy
+ * @param tokenAddress Address of the token to be transferred
+ * @param from Owner of the transferred tokens
+ * @param to Recipient of the transferred tokens
+ * @param amountInBaseUnits The amount of tokens being transferred
+ * @param tradeSide Is Maker/Taker transferring
+ * @param transferType Is it a fee payment or a value transfer
+ */
+ public async transferFromAsync(tokenAddress: string, from: string, to: string,
+ amountInBaseUnits: BigNumber, tradeSide: TradeSide,
+ transferType: TransferType): Promise<void> {
+ const balance = await this.store.getBalanceAsync(tokenAddress, from);
+ const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, from);
+ if (proxyAllowance.lessThan(amountInBaseUnits)) {
+ this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
+ }
+ if (balance.lessThan(amountInBaseUnits)) {
+ this.throwValidationError(FailureReason.Balance, tradeSide, transferType);
+ }
+ await this.decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits);
+ await this.decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits);
+ await this.increaseBalanceAsync(tokenAddress, to, amountInBaseUnits);
+ }
+ private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber): Promise<void> {
+ const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, userAddress);
+ if (!proxyAllowance.eq(this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ this.store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
+ }
+ }
+ private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber): Promise<void> {
+ const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
+ this.store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
+ }
+ private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber): Promise<void> {
+ const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
+ this.store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
+ }
+ private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
+ transferType: TransferType): Promise<never> {
+ const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
+ throw new Error(errMsg);
+ }
+}
diff --git a/packages/0x.js/src/utils/filter_utils.ts b/packages/0x.js/src/utils/filter_utils.ts
new file mode 100644
index 000000000..e09a95a6e
--- /dev/null
+++ b/packages/0x.js/src/utils/filter_utils.ts
@@ -0,0 +1,82 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import * as uuid from 'uuid/v4';
+import * as ethUtil from 'ethereumjs-util';
+import * as jsSHA3 from 'js-sha3';
+import {ContractEvents, IndexedFilterValues, SubscriptionOpts} from '../types';
+
+const TOPIC_LENGTH = 32;
+
+export const filterUtils = {
+ generateUUID(): string {
+ return uuid();
+ },
+ getFilter(address: string, eventName: ContractEvents,
+ indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi,
+ subscriptionOpts?: SubscriptionOpts): Web3.FilterObject {
+ const eventAbi = _.find(abi, {name: eventName}) as Web3.EventAbi;
+ const eventSignature = filterUtils.getEventSignatureFromAbiByName(eventAbi, eventName);
+ const topicForEventSignature = ethUtil.addHexPrefix(jsSHA3.keccak256(eventSignature));
+ const topicsForIndexedArgs = filterUtils.getTopicsForIndexedArgs(eventAbi, indexFilterValues);
+ const topics = [topicForEventSignature, ...topicsForIndexedArgs];
+ let filter: Web3.FilterObject = {
+ address,
+ topics,
+ };
+ if (!_.isUndefined(subscriptionOpts)) {
+ filter = {
+ ...subscriptionOpts,
+ ...filter,
+ };
+ }
+ return filter;
+ },
+ getEventSignatureFromAbiByName(eventAbi: Web3.EventAbi, eventName: ContractEvents): string {
+ const types = _.map(eventAbi.inputs, 'type');
+ const signature = `${eventAbi.name}(${types.join(',')})`;
+ return signature;
+ },
+ getTopicsForIndexedArgs(abi: Web3.EventAbi, indexFilterValues: IndexedFilterValues): Array<string|null> {
+ const topics: Array<string|null> = [];
+ for (const eventInput of abi.inputs) {
+ if (!eventInput.indexed) {
+ continue;
+ }
+ if (_.isUndefined(indexFilterValues[eventInput.name])) {
+ // Null is a wildcard topic in a JSON-RPC call
+ topics.push(null);
+ } else {
+ const value = indexFilterValues[eventInput.name] as string;
+ const buffer = ethUtil.toBuffer(value);
+ const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH);
+ const topic = ethUtil.bufferToHex(paddedBuffer);
+ topics.push(topic);
+ }
+ }
+ return topics;
+ },
+ matchesFilter(log: Web3.LogEntry, filter: Web3.FilterObject): boolean {
+ if (!_.isUndefined(filter.address) && log.address !== filter.address) {
+ return false;
+ }
+ if (!_.isUndefined(filter.topics)) {
+ return filterUtils.matchesTopics(log.topics, filter.topics);
+ }
+ return true;
+ },
+ matchesTopics(logTopics: string[], filterTopics: Array<string[]|string|null>): boolean {
+ const matchesTopic = _.zipWith(logTopics, filterTopics, filterUtils.matchesTopic.bind(filterUtils));
+ const matchesTopics = _.every(matchesTopic);
+ return matchesTopics;
+ },
+ matchesTopic(logTopic: string, filterTopic: string[]|string|null): boolean {
+ if (_.isArray(filterTopic)) {
+ return _.includes(filterTopic, logTopic);
+ }
+ if (_.isString(filterTopic)) {
+ return filterTopic === logTopic;
+ }
+ // null topic is a wildcard
+ return true;
+ },
+};
diff --git a/packages/0x.js/src/utils/interval_utils.ts b/packages/0x.js/src/utils/interval_utils.ts
new file mode 100644
index 000000000..62b79f2f5
--- /dev/null
+++ b/packages/0x.js/src/utils/interval_utils.ts
@@ -0,0 +1,20 @@
+import * as _ from 'lodash';
+
+export const intervalUtils = {
+ setAsyncExcludingInterval(fn: () => Promise<void>, intervalMs: number) {
+ let locked = false;
+ const intervalId = setInterval(async () => {
+ if (locked) {
+ return;
+ } else {
+ locked = true;
+ await fn();
+ locked = false;
+ }
+ }, intervalMs);
+ return intervalId;
+ },
+ clearAsyncExcludingInterval(intervalId: NodeJS.Timer): void {
+ clearInterval(intervalId);
+ },
+};
diff --git a/packages/0x.js/src/utils/order_state_utils.ts b/packages/0x.js/src/utils/order_state_utils.ts
new file mode 100644
index 000000000..f82601cae
--- /dev/null
+++ b/packages/0x.js/src/utils/order_state_utils.ts
@@ -0,0 +1,119 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {
+ ExchangeContractErrs,
+ SignedOrder,
+ OrderRelevantState,
+ MethodOpts,
+ OrderState,
+ OrderStateValid,
+ OrderStateInvalid,
+} from '../types';
+import {ZeroEx} from '../0x';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {utils} from '../utils/utils';
+import {constants} from '../utils/constants';
+import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
+import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
+
+export class OrderStateUtils {
+ private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
+ private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
+ constructor(balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore,
+ orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore) {
+ this.balanceAndProxyAllowanceLazyStore = balanceAndProxyAllowanceLazyStore;
+ this.orderFilledCancelledLazyStore = orderFilledCancelledLazyStore;
+ }
+ public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
+ const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ try {
+ this.validateIfOrderIsValid(signedOrder, orderRelevantState);
+ const orderState: OrderStateValid = {
+ isValid: true,
+ orderHash,
+ orderRelevantState,
+ };
+ return orderState;
+ } catch (err) {
+ const orderState: OrderStateInvalid = {
+ isValid: false,
+ orderHash,
+ error: err.message,
+ };
+ return orderState;
+ }
+ }
+ public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> {
+ // HACK: We access the private property here but otherwise the interface will be less nice.
+ // If we pass it from the instantiator - there is no opportunity to get it there
+ // because JS doesn't support async constructors.
+ // Moreover - it's cached under the hood so it's equivalent to an async constructor.
+ const exchange = (this.orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper;
+ const zrxTokenAddress = await exchange.getZRXTokenAddressAsync();
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ const makerBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker,
+ );
+ const makerProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker,
+ );
+ const makerFeeBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
+ zrxTokenAddress, signedOrder.maker,
+ );
+ const makerFeeProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
+ zrxTokenAddress, signedOrder.maker,
+ );
+ const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash);
+ const canceledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync(
+ orderHash,
+ );
+ const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash);
+ const totalMakerTokenAmount = signedOrder.makerTokenAmount;
+ const totalTakerTokenAmount = signedOrder.takerTokenAmount;
+ const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount);
+ const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount)
+ .dividedToIntegerBy(totalTakerTokenAmount);
+ const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
+ const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount);
+ // TODO: Handle edge case where maker token is ZRX with fee
+ const orderRelevantState = {
+ makerBalance,
+ makerProxyAllowance,
+ makerFeeBalance,
+ makerFeeProxyAllowance,
+ filledTakerTokenAmount,
+ canceledTakerTokenAmount,
+ remainingFillableMakerTokenAmount,
+ };
+ return orderRelevantState;
+ }
+ private validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void {
+ const unavailableTakerTokenAmount = orderRelevantState.canceledTakerTokenAmount.add(
+ orderRelevantState.filledTakerTokenAmount,
+ );
+ const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ if (availableTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ }
+
+ if (orderRelevantState.makerBalance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerBalance);
+ }
+ if (orderRelevantState.makerProxyAllowance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerAllowance);
+ }
+ if (!signedOrder.makerFee.eq(0)) {
+ if (orderRelevantState.makerFeeBalance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance);
+ }
+ if (orderRelevantState.makerFeeProxyAllowance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
+ }
+ }
+ // TODO Add linear function solver when maker token is ZRX #badass
+ // Return the max amount that's fillable
+ }
+}
diff --git a/packages/0x.js/src/utils/order_validation_utils.ts b/packages/0x.js/src/utils/order_validation_utils.ts
new file mode 100644
index 000000000..f03703c4e
--- /dev/null
+++ b/packages/0x.js/src/utils/order_validation_utils.ts
@@ -0,0 +1,166 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {ExchangeContractErrs, SignedOrder, Order, ZeroExError, TradeSide, TransferType} from '../types';
+import {ZeroEx} from '../0x';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {utils} from '../utils/utils';
+import {constants} from '../utils/constants';
+import {ExchangeTransferSimulator} from './exchange_transfer_simulator';
+
+export class OrderValidationUtils {
+ private tokenWrapper: TokenWrapper;
+ private exchangeWrapper: ExchangeWrapper;
+ constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) {
+ this.tokenWrapper = tokenWrapper;
+ this.exchangeWrapper = exchangeWrapper;
+ }
+ public async validateOrderFillableOrThrowAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, zrxTokenAddress: string,
+ expectedFillTakerTokenAmount?: BigNumber): Promise<void> {
+ const orderHash = utils.getOrderHashHex(signedOrder);
+ const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
+ this.validateRemainingFillAmountNotZeroOrThrow(
+ signedOrder.takerTokenAmount, unavailableTakerTokenAmount,
+ );
+ this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
+ let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ if (!_.isUndefined(expectedFillTakerTokenAmount)) {
+ fillTakerTokenAmount = expectedFillTakerTokenAmount;
+ }
+ const fillMakerTokenAmount = this.getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerTokenAmount,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker, signedOrder.taker, fillMakerTokenAmount,
+ TradeSide.Maker, TransferType.Trade,
+ );
+ const makerFeeAmount = this.getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerFee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, makerFeeAmount,
+ TradeSide.Maker, TransferType.Fee,
+ );
+ }
+ public async validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber, takerAddress: string,
+ zrxTokenAddress: string): Promise<BigNumber> {
+ if (fillTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderFillAmountZero);
+ }
+ const orderHash = utils.getOrderHashHex(signedOrder);
+ if (!ZeroEx.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker)) {
+ throw new Error(ZeroExError.InvalidSignature);
+ }
+ const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
+ this.validateRemainingFillAmountNotZeroOrThrow(
+ signedOrder.takerTokenAmount, unavailableTakerTokenAmount,
+ );
+ if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) {
+ throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
+ }
+ this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
+ const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) ?
+ remainingTakerTokenAmount :
+ fillTakerTokenAmount;
+ await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, filledTakerTokenAmount, takerAddress, zrxTokenAddress,
+ );
+
+ const wouldRoundingErrorOccur = await this.exchangeWrapper.isRoundingErrorAsync(
+ filledTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount,
+ );
+ if (wouldRoundingErrorOccur) {
+ throw new Error(ExchangeContractErrs.OrderFillRoundingError);
+ }
+ return filledTakerTokenAmount;
+ }
+ public async validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber, takerAddress: string, zrxTokenAddress: string): Promise<void> {
+ const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
+ );
+ if (filledTakerTokenAmount !== fillTakerTokenAmount) {
+ throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount);
+ }
+ }
+ public async validateCancelOrderThrowIfInvalidAsync(order: Order,
+ cancelTakerTokenAmount: BigNumber,
+ unavailableTakerTokenAmount: BigNumber,
+ ): Promise<void> {
+ if (cancelTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderCancelAmountZero);
+ }
+ if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) {
+ throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
+ }
+ const currentUnixTimestampSec = utils.getCurrentUnixTimestamp();
+ if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
+ throw new Error(ExchangeContractErrs.OrderCancelExpired);
+ }
+ }
+ public async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber, senderAddress: string, zrxTokenAddress: string): Promise<void> {
+ const fillMakerTokenAmount = this.getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerTokenAmount,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker, senderAddress, fillMakerTokenAmount,
+ TradeSide.Maker, TransferType.Trade,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.takerTokenAddress, senderAddress, signedOrder.maker, fillTakerTokenAmount,
+ TradeSide.Taker, TransferType.Trade,
+ );
+ const makerFeeAmount = this.getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerFee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, makerFeeAmount, TradeSide.Maker,
+ TransferType.Fee,
+ );
+ const takerFeeAmount = this.getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.takerFee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, senderAddress, signedOrder.feeRecipient, takerFeeAmount, TradeSide.Taker,
+ TransferType.Fee,
+ );
+ }
+ private validateRemainingFillAmountNotZeroOrThrow(
+ takerTokenAmount: BigNumber, unavailableTakerTokenAmount: BigNumber,
+ ) {
+ if (takerTokenAmount.eq(unavailableTakerTokenAmount)) {
+ throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ }
+ }
+ private validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) {
+ const currentUnixTimestampSec = utils.getCurrentUnixTimestamp();
+ if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
+ throw new Error(ExchangeContractErrs.OrderFillExpired);
+ }
+ }
+ private getPartialAmount(numerator: BigNumber, denominator: BigNumber,
+ target: BigNumber): BigNumber {
+ const fillMakerTokenAmount = numerator
+ .mul(target)
+ .div(denominator)
+ .round(0);
+ return fillMakerTokenAmount;
+ }
+}
diff --git a/packages/0x.js/src/utils/signature_utils.ts b/packages/0x.js/src/utils/signature_utils.ts
new file mode 100644
index 000000000..d066f8bf0
--- /dev/null
+++ b/packages/0x.js/src/utils/signature_utils.ts
@@ -0,0 +1,44 @@
+import * as ethUtil from 'ethereumjs-util';
+import {ECSignature} from '../types';
+
+export const signatureUtils = {
+ isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean {
+ const dataBuff = ethUtil.toBuffer(data);
+ const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff);
+ try {
+ const pubKey = ethUtil.ecrecover(
+ msgHashBuff,
+ signature.v,
+ ethUtil.toBuffer(signature.r),
+ ethUtil.toBuffer(signature.s));
+ const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey));
+ return retrievedAddress === signerAddress;
+ } catch (err) {
+ return false;
+ }
+ },
+ parseSignatureHexAsVRS(signatureHex: string): ECSignature {
+ const signatureBuffer = ethUtil.toBuffer(signatureHex);
+ let v = signatureBuffer[0];
+ if (v < 27) {
+ v += 27;
+ }
+ const r = signatureBuffer.slice(1, 33);
+ const s = signatureBuffer.slice(33, 65);
+ const ecSignature: ECSignature = {
+ v,
+ r: ethUtil.bufferToHex(r),
+ s: ethUtil.bufferToHex(s),
+ };
+ return ecSignature;
+ },
+ parseSignatureHexAsRSV(signatureHex: string): ECSignature {
+ const {v, r, s} = ethUtil.fromRpcSig(signatureHex);
+ const ecSignature: ECSignature = {
+ v,
+ r: ethUtil.bufferToHex(r),
+ s: ethUtil.bufferToHex(s),
+ };
+ return ecSignature;
+ },
+};
diff --git a/packages/0x.js/src/utils/utils.ts b/packages/0x.js/src/utils/utils.ts
new file mode 100644
index 000000000..280f3e979
--- /dev/null
+++ b/packages/0x.js/src/utils/utils.ts
@@ -0,0 +1,55 @@
+import * as _ from 'lodash';
+import * as ethABI from 'ethereumjs-abi';
+import * as ethUtil from 'ethereumjs-util';
+import {Order, SignedOrder, SolidityTypes} from '../types';
+import BigNumber from 'bignumber.js';
+import BN = require('bn.js');
+
+export const utils = {
+ /**
+ * Converts BigNumber instance to BN
+ * The only reason we convert to BN is to remain compatible with `ethABI. soliditySHA3` that
+ * expects values of Solidity type `uint` to be passed as type `BN`.
+ * We do not use BN anywhere else in the codebase.
+ */
+ bigNumberToBN(value: BigNumber) {
+ return new BN(value.toString(), 10);
+ },
+ consoleLog(message: string): void {
+ // tslint:disable-next-line: no-console
+ console.log(message);
+ },
+ isParityNode(nodeVersion: string): boolean {
+ return _.includes(nodeVersion, 'Parity');
+ },
+ isTestRpc(nodeVersion: string): boolean {
+ return _.includes(nodeVersion, 'TestRPC');
+ },
+ spawnSwitchErr(name: string, value: any): Error {
+ return new Error(`Unexpected switch value: ${value} encountered for ${name}`);
+ },
+ getOrderHashHex(order: Order|SignedOrder): string {
+ const orderParts = [
+ {value: order.exchangeContractAddress, type: SolidityTypes.Address},
+ {value: order.maker, type: SolidityTypes.Address},
+ {value: order.taker, type: SolidityTypes.Address},
+ {value: order.makerTokenAddress, type: SolidityTypes.Address},
+ {value: order.takerTokenAddress, type: SolidityTypes.Address},
+ {value: order.feeRecipient, type: SolidityTypes.Address},
+ {value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.Uint256},
+ {value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.Uint256},
+ {value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.Uint256},
+ {value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.Uint256},
+ {value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.Uint256},
+ {value: utils.bigNumberToBN(order.salt), type: SolidityTypes.Uint256},
+ ];
+ const types = _.map(orderParts, o => o.type);
+ const values = _.map(orderParts, o => o.value);
+ const hashBuff = ethABI.soliditySHA3(types, values);
+ const hashHex = ethUtil.bufferToHex(hashBuff);
+ return hashHex;
+ },
+ getCurrentUnixTimestamp(): BigNumber {
+ return new BigNumber(Date.now() / 1000);
+ },
+};
diff --git a/packages/0x.js/src/web3_wrapper.ts b/packages/0x.js/src/web3_wrapper.ts
new file mode 100644
index 000000000..c937f9288
--- /dev/null
+++ b/packages/0x.js/src/web3_wrapper.ts
@@ -0,0 +1,172 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+import {ZeroExError, Artifact, TransactionReceipt} from './types';
+import {Contract} from './contract';
+
+export class Web3Wrapper {
+ private web3: Web3;
+ private defaults: Partial<Web3.TxData>;
+ private networkIdIfExists?: number;
+ private jsonRpcRequestId: number;
+ constructor(provider: Web3.Provider, defaults?: Partial<Web3.TxData>) {
+ if (_.isUndefined((provider as any).sendAsync)) {
+ // Web3@1.0 provider doesn't support synchronous http requests,
+ // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x`
+ // We re-assign the send method so that Web3@1.0 providers work with 0x.js
+ (provider as any).sendAsync = (provider as any).send;
+ }
+ this.web3 = new Web3();
+ this.web3.setProvider(provider);
+ this.defaults = defaults || {};
+ this.jsonRpcRequestId = 0;
+ }
+ public setProvider(provider: Web3.Provider) {
+ delete this.networkIdIfExists;
+ this.web3.setProvider(provider);
+ }
+ public isAddress(address: string): boolean {
+ return this.web3.isAddress(address);
+ }
+ public async isSenderAddressAvailableAsync(senderAddress: string): Promise<boolean> {
+ const addresses = await this.getAvailableAddressesAsync();
+ return _.includes(addresses, senderAddress);
+ }
+ public async getNodeVersionAsync(): Promise<string> {
+ const nodeVersion = await promisify(this.web3.version.getNode)();
+ return nodeVersion;
+ }
+ public async getTransactionReceiptAsync(txHash: string): Promise<TransactionReceipt> {
+ const transactionReceipt = await promisify(this.web3.eth.getTransactionReceipt)(txHash);
+ transactionReceipt.status = this.normalizeTxReceiptStatus(transactionReceipt.status);
+ return transactionReceipt;
+ }
+ public getCurrentProvider(): Web3.Provider {
+ return this.web3.currentProvider;
+ }
+ public async getNetworkIdIfExistsAsync(): Promise<number|undefined> {
+ if (!_.isUndefined(this.networkIdIfExists)) {
+ return this.networkIdIfExists;
+ }
+
+ try {
+ const networkId = await this.getNetworkAsync();
+ this.networkIdIfExists = Number(networkId);
+ return this.networkIdIfExists;
+ } catch (err) {
+ return undefined;
+ }
+ }
+ public async getContractInstanceFromArtifactAsync<A extends Web3.ContractInstance>(artifact: Artifact,
+ address?: string): Promise<A> {
+ let contractAddress: string;
+ if (_.isUndefined(address)) {
+ const networkIdIfExists = await this.getNetworkIdIfExistsAsync();
+ if (_.isUndefined(networkIdIfExists)) {
+ throw new Error(ZeroExError.NoNetworkId);
+ }
+ if (_.isUndefined(artifact.networks[networkIdIfExists])) {
+ throw new Error(ZeroExError.ContractNotDeployedOnNetwork);
+ }
+ contractAddress = artifact.networks[networkIdIfExists].address.toLowerCase();
+ } else {
+ contractAddress = address;
+ }
+ const doesContractExist = await this.doesContractExistAtAddressAsync(contractAddress);
+ if (!doesContractExist) {
+ throw new Error(ZeroExError.ContractDoesNotExist);
+ }
+ const contractInstance = this.getContractInstance<A>(
+ artifact.abi, contractAddress,
+ );
+ return contractInstance;
+ }
+ public toWei(ethAmount: BigNumber): BigNumber {
+ const balanceWei = this.web3.toWei(ethAmount, 'ether');
+ return balanceWei;
+ }
+ public async getBalanceInWeiAsync(owner: string): Promise<BigNumber> {
+ let balanceInWei = await promisify(this.web3.eth.getBalance)(owner);
+ balanceInWei = new BigNumber(balanceInWei);
+ return balanceInWei;
+ }
+ public async doesContractExistAtAddressAsync(address: string): Promise<boolean> {
+ const code = await promisify(this.web3.eth.getCode)(address);
+ // Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients
+ const codeIsEmpty = /^0x0{0,40}$/i.test(code);
+ return !codeIsEmpty;
+ }
+ public async signTransactionAsync(address: string, message: string): Promise<string> {
+ const signData = await promisify(this.web3.eth.sign)(address, message);
+ return signData;
+ }
+ public async getBlockNumberAsync(): Promise<number> {
+ const blockNumber = await promisify(this.web3.eth.getBlockNumber)();
+ return blockNumber;
+ }
+ public async getBlockAsync(blockParam: string|Web3.BlockParam): Promise<Web3.BlockWithoutTransactionData> {
+ const block = await promisify(this.web3.eth.getBlock)(blockParam);
+ return block;
+ }
+ public async getBlockTimestampAsync(blockParam: string|Web3.BlockParam): Promise<number> {
+ const {timestamp} = await this.getBlockAsync(blockParam);
+ return timestamp;
+ }
+ public async getAvailableAddressesAsync(): Promise<string[]> {
+ const addresses: string[] = await promisify(this.web3.eth.getAccounts)();
+ return addresses;
+ }
+ public async getLogsAsync(filter: Web3.FilterObject): Promise<Web3.LogEntry[]> {
+ let fromBlock = filter.fromBlock;
+ if (_.isNumber(fromBlock)) {
+ fromBlock = this.web3.toHex(fromBlock);
+ }
+ let toBlock = filter.toBlock;
+ if (_.isNumber(toBlock)) {
+ toBlock = this.web3.toHex(toBlock);
+ }
+ const serializedFilter = {
+ ...filter,
+ fromBlock,
+ toBlock,
+ };
+ const payload = {
+ jsonrpc: '2.0',
+ id: this.jsonRpcRequestId++,
+ method: 'eth_getLogs',
+ params: [serializedFilter],
+ };
+ const logs = await this.sendRawPayloadAsync(payload);
+ return logs;
+ }
+ private getContractInstance<A extends Web3.ContractInstance>(abi: Web3.ContractAbi, address: string): A {
+ const web3ContractInstance = this.web3.eth.contract(abi).at(address);
+ const contractInstance = new Contract(web3ContractInstance, this.defaults) as any as A;
+ return contractInstance;
+ }
+ private async getNetworkAsync(): Promise<number> {
+ const networkId = await promisify(this.web3.version.getNetwork)();
+ return networkId;
+ }
+ private async sendRawPayloadAsync(payload: Web3.JSONRPCRequestPayload): Promise<any> {
+ const sendAsync = this.web3.currentProvider.sendAsync.bind(this.web3.currentProvider);
+ const response = await promisify(sendAsync)(payload);
+ const result = response.result;
+ return result;
+ }
+ private normalizeTxReceiptStatus(status: undefined|null|string|0|1): null|0|1 {
+ // Transaction status might have four values
+ // undefined - Testrpc and other old clients
+ // null - New clients on old transactions
+ // number - Parity
+ // hex - Geth
+ if (_.isString(status)) {
+ return this.web3.toDecimal(status) as 0|1;
+ } else if (_.isUndefined(status)) {
+ return null;
+ } else {
+ return status;
+ }
+ }
+}
diff --git a/packages/0x.js/test/0x.js_test.ts b/packages/0x.js/test/0x.js_test.ts
new file mode 100644
index 000000000..d56acc38b
--- /dev/null
+++ b/packages/0x.js/test/0x.js_test.ts
@@ -0,0 +1,259 @@
+import * as _ from 'lodash';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import 'mocha';
+import BigNumber from 'bignumber.js';
+import * as Sinon from 'sinon';
+import {ZeroEx, Order, ZeroExError, LogWithDecodedArgs, ApprovalContractEventArgs, TokenEvents} from '../src';
+import {constants} from './utils/constants';
+import {TokenUtils} from './utils/token_utils';
+import {web3Factory} from './utils/web3_factory';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+
+const blockchainLifecycle = new BlockchainLifecycle();
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('ZeroEx library', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ describe('#setProvider', () => {
+ it('overrides provider in nested web3s and invalidates contractInstances', async () => {
+ // Instantiate the contract instances with the current provider
+ await (zeroEx.exchange as any)._getExchangeContractAsync();
+ await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
+ expect((zeroEx.exchange as any)._exchangeContractIfExists).to.not.be.undefined();
+ expect((zeroEx.tokenRegistry as any)._tokenRegistryContractIfExists).to.not.be.undefined();
+
+ const newProvider = web3Factory.getRpcProvider();
+ // Add property to newProvider so that we can differentiate it from old provider
+ (newProvider as any).zeroExTestId = 1;
+ await zeroEx.setProviderAsync(newProvider);
+
+ // Check that contractInstances with old provider are removed after provider update
+ expect((zeroEx.exchange as any)._exchangeContractIfExists).to.be.undefined();
+ expect((zeroEx.tokenRegistry as any)._tokenRegistryContractIfExists).to.be.undefined();
+
+ // Check that all nested web3 wrapper instances return the updated provider
+ const nestedWeb3WrapperProvider = (zeroEx as any)._web3Wrapper.getCurrentProvider();
+ expect((nestedWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
+ const exchangeWeb3WrapperProvider = (zeroEx.exchange as any)._web3Wrapper.getCurrentProvider();
+ expect((exchangeWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
+ const tokenRegistryWeb3WrapperProvider = (zeroEx.tokenRegistry as any)._web3Wrapper.getCurrentProvider();
+ expect((tokenRegistryWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
+ });
+ });
+ describe('#isValidSignature', () => {
+ // The Exchange smart contract `isValidSignature` method only validates orderHashes and assumes
+ // the length of the data is exactly 32 bytes. Thus for these tests, we use data of this size.
+ const dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
+ const signature = {
+ v: 27,
+ r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ };
+ const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
+ it('should return false if the data doesn\'t pertain to the signature & address', async () => {
+ expect(ZeroEx.isValidSignature('0x0', signature, address)).to.be.false();
+ return expect(
+ (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync('0x0', signature, address),
+ ).to.become(false);
+ });
+ it('should return false if the address doesn\'t pertain to the signature & data', async () => {
+ const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42';
+ expect(ZeroEx.isValidSignature(dataHex, signature, validUnrelatedAddress)).to.be.false();
+ return expect(
+ (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, signature,
+ validUnrelatedAddress),
+ ).to.become(false);
+ });
+ it('should return false if the signature doesn\'t pertain to the dataHex & address', async () => {
+ const wrongSignature = _.assign({}, signature, {v: 28});
+ expect(ZeroEx.isValidSignature(dataHex, wrongSignature, address)).to.be.false();
+ return expect(
+ (zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, wrongSignature, address),
+ ).to.become(false);
+ });
+ it('should return true if the signature does pertain to the dataHex & address', async () => {
+ const isValidSignatureLocal = ZeroEx.isValidSignature(dataHex, signature, address);
+ expect(isValidSignatureLocal).to.be.true();
+ const isValidSignatureOnContract = await (zeroEx.exchange as any)
+ ._isValidSignatureUsingContractCallAsync(dataHex, signature, address);
+ return expect(isValidSignatureOnContract).to.be.true();
+ });
+ });
+ describe('#generateSalt', () => {
+ it('generates different salts', () => {
+ const equal = ZeroEx.generatePseudoRandomSalt().eq(ZeroEx.generatePseudoRandomSalt());
+ expect(equal).to.be.false();
+ });
+ it('generates salt in range [0..2^256)', () => {
+ const salt = ZeroEx.generatePseudoRandomSalt();
+ expect(salt.greaterThanOrEqualTo(0)).to.be.true();
+ const twoPow256 = new BigNumber(2).pow(256);
+ expect(salt.lessThan(twoPow256)).to.be.true();
+ });
+ });
+ describe('#isValidOrderHash', () => {
+ it('returns false if the value is not a hex string', () => {
+ const isValid = ZeroEx.isValidOrderHash('not a hex');
+ expect(isValid).to.be.false();
+ });
+ it('returns false if the length is wrong', () => {
+ const isValid = ZeroEx.isValidOrderHash('0xdeadbeef');
+ expect(isValid).to.be.false();
+ });
+ it('returns true if order hash is correct', () => {
+ const isValid = ZeroEx.isValidOrderHash('0x' + Array(65).join('0'));
+ expect(isValid).to.be.true();
+ });
+ });
+ describe('#toUnitAmount', () => {
+ it('Should return the expected unit amount for the decimals passed in', () => {
+ const baseUnitAmount = new BigNumber(1000000000);
+ const decimals = 6;
+ const unitAmount = ZeroEx.toUnitAmount(baseUnitAmount, decimals);
+ const expectedUnitAmount = new BigNumber(1000);
+ expect(unitAmount).to.be.bignumber.equal(expectedUnitAmount);
+ });
+ });
+ describe('#toBaseUnitAmount', () => {
+ it('Should return the expected base unit amount for the decimals passed in', () => {
+ const unitAmount = new BigNumber(1000);
+ const decimals = 6;
+ const baseUnitAmount = ZeroEx.toBaseUnitAmount(unitAmount, decimals);
+ const expectedUnitAmount = new BigNumber(1000000000);
+ expect(baseUnitAmount).to.be.bignumber.equal(expectedUnitAmount);
+ });
+ });
+ describe('#getOrderHashHex', () => {
+ const expectedOrderHash = '0x39da987067a3c9e5f1617694f1301326ba8c8b0498ebef5df4863bed394e3c83';
+ const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b';
+ const order: Order = {
+ maker: constants.NULL_ADDRESS,
+ taker: constants.NULL_ADDRESS,
+ feeRecipient: constants.NULL_ADDRESS,
+ makerTokenAddress: constants.NULL_ADDRESS,
+ takerTokenAddress: constants.NULL_ADDRESS,
+ exchangeContractAddress: fakeExchangeContractAddress,
+ salt: new BigNumber(0),
+ makerFee: new BigNumber(0),
+ takerFee: new BigNumber(0),
+ makerTokenAmount: new BigNumber(0),
+ takerTokenAmount: new BigNumber(0),
+ expirationUnixTimestampSec: new BigNumber(0),
+ };
+ it('calculates the order hash', async () => {
+ const orderHash = ZeroEx.getOrderHashHex(order);
+ expect(orderHash).to.be.equal(expectedOrderHash);
+ });
+ });
+ describe('#signOrderHashAsync', () => {
+ let stubs: Sinon.SinonStub[] = [];
+ let makerAddress: string;
+ before(async () => {
+ const availableAddreses = await zeroEx.getAvailableAddressesAsync();
+ makerAddress = availableAddreses[0];
+ });
+ afterEach(() => {
+ // clean up any stubs after the test has completed
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ });
+ it('Should return the correct ECSignature', async () => {
+ const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
+ const expectedECSignature = {
+ v: 27,
+ r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ };
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
+ expect(ecSignature).to.deep.equal(expectedECSignature);
+ });
+ it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => {
+ const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
+ // tslint:disable-next-line: max-line-length
+ const signature = '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb021b';
+ const expectedECSignature = {
+ v: 27,
+ r: '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3',
+ s: '0x050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb02',
+ };
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'signTransactionAsync')
+ .returns(Promise.resolve(signature)),
+ Sinon.stub(ZeroEx, 'isValidSignature').returns(true),
+ ];
+
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
+ expect(ecSignature).to.deep.equal(expectedECSignature);
+ });
+ it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => {
+ const orderHash = '0xc793e33ffded933b76f2f48d9aa3339fc090399d5e7f5dec8d3660f5480793f7';
+ // tslint:disable-next-line: max-line-length
+ const signature = '0x1bc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee02dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960';
+ const expectedECSignature = {
+ v: 27,
+ r: '0xc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee0',
+ s: '0x2dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960',
+ };
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'signTransactionAsync')
+ .returns(Promise.resolve(signature)),
+ Sinon.stub(ZeroEx, 'isValidSignature').returns(true),
+ ];
+
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
+ expect(ecSignature).to.deep.equal(expectedECSignature);
+ });
+ });
+ describe('#awaitTransactionMinedAsync', () => {
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ it('returns transaction receipt with decoded logs', async () => {
+ const availableAddresses = await zeroEx.getAvailableAddressesAsync();
+ const coinbase = availableAddresses[0];
+ const tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ const tokenUtils = new TokenUtils(tokens);
+ const zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ const proxyAddress = await zeroEx.proxy.getContractAddressAsync();
+ const txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(zrxTokenAddress, coinbase);
+ const txReceiptWithDecodedLogs = await zeroEx.awaitTransactionMinedAsync(txHash);
+ const log = txReceiptWithDecodedLogs.logs[0] as LogWithDecodedArgs<ApprovalContractEventArgs>;
+ expect(log.event).to.be.equal(TokenEvents.Approval);
+ expect(log.args._owner).to.be.equal(coinbase);
+ expect(log.args._spender).to.be.equal(proxyAddress);
+ expect(log.args._value).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+ describe('#config', () => {
+ it('allows to specify exchange contract address', async () => {
+ const config = {
+ exchangeContractAddress: ZeroEx.NULL_ADDRESS,
+ };
+ const zeroExWithWrongExchangeAddress = new ZeroEx(web3.currentProvider, config);
+ return expect(zeroExWithWrongExchangeAddress.exchange.getContractAddressAsync())
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ it('allows to specify ether token contract address', async () => {
+ const config = {
+ etherTokenContractAddress: ZeroEx.NULL_ADDRESS,
+ };
+ const zeroExWithWrongEtherTokenAddress = new ZeroEx(web3.currentProvider, config);
+ return expect(zeroExWithWrongEtherTokenAddress.etherToken.getContractAddressAsync())
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ it('allows to specify token registry token contract address', async () => {
+ const config = {
+ tokenRegistryContractAddress: ZeroEx.NULL_ADDRESS,
+ };
+ const zeroExWithWrongTokenRegistryAddress = new ZeroEx(web3.currentProvider, config);
+ return expect(zeroExWithWrongTokenRegistryAddress.tokenRegistry.getContractAddressAsync())
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ });
+});
diff --git a/packages/0x.js/test/artifacts_test.ts b/packages/0x.js/test/artifacts_test.ts
new file mode 100644
index 000000000..b2866a1d6
--- /dev/null
+++ b/packages/0x.js/test/artifacts_test.ts
@@ -0,0 +1,49 @@
+import * as fs from 'fs';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import HDWalletProvider = require('truffle-hdwallet-provider');
+import {ZeroEx} from '../src';
+import {constants} from './utils/constants';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+// Those tests are slower cause they're talking to a remote node
+const TIMEOUT = 10000;
+
+describe('Artifacts', () => {
+ describe('contracts are deployed on kovan', () => {
+ const kovanRpcUrl = constants.KOVAN_RPC_URL;
+ const packageJSONContent = fs.readFileSync('package.json', 'utf-8');
+ const packageJSON = JSON.parse(packageJSONContent);
+ const mnemonic = packageJSON.config.mnemonic;
+ const web3Provider = new HDWalletProvider(mnemonic, kovanRpcUrl);
+ const zeroEx = new ZeroEx(web3Provider);
+ it('token registry contract is deployed', async () => {
+ await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
+ }).timeout(TIMEOUT);
+ it('proxy contract is deployed', async () => {
+ await (zeroEx.token as any)._getTokenTransferProxyAddressAsync();
+ }).timeout(TIMEOUT);
+ it('exchange contract is deployed', async () => {
+ await zeroEx.exchange.getContractAddressAsync();
+ }).timeout(TIMEOUT);
+ });
+ describe('contracts are deployed on ropsten', () => {
+ const ropstenRpcUrl = constants.ROPSTEN_RPC_URL;
+ const packageJSONContent = fs.readFileSync('package.json', 'utf-8');
+ const packageJSON = JSON.parse(packageJSONContent);
+ const mnemonic = packageJSON.config.mnemonic;
+ const web3Provider = new HDWalletProvider(mnemonic, ropstenRpcUrl);
+ const zeroEx = new ZeroEx(web3Provider);
+ it('token registry contract is deployed', async () => {
+ await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
+ }).timeout(TIMEOUT);
+ it('proxy contract is deployed', async () => {
+ await (zeroEx.token as any)._getTokenTransferProxyAddressAsync();
+ }).timeout(TIMEOUT);
+ it('exchange contract is deployed', async () => {
+ await zeroEx.exchange.getContractAddressAsync();
+ }).timeout(TIMEOUT);
+ });
+});
diff --git a/packages/0x.js/test/assert_test.ts b/packages/0x.js/test/assert_test.ts
new file mode 100644
index 000000000..bfca95d9c
--- /dev/null
+++ b/packages/0x.js/test/assert_test.ts
@@ -0,0 +1,34 @@
+import * as chai from 'chai';
+import 'mocha';
+import {ZeroEx} from '../src';
+import {assert} from '../src/utils/assert';
+import {web3Factory} from './utils/web3_factory';
+
+const expect = chai.expect;
+
+describe('Assertion library', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ describe('#isSenderAddressHexAsync', () => {
+ it('throws when address is invalid', async () => {
+ const address = '0xdeadbeef';
+ const varName = 'address';
+ return expect(assert.isSenderAddressAsync(varName, address, (zeroEx as any)._web3Wrapper))
+ .to.be.rejectedWith(`Expected ${varName} to be of type ETHAddressHex, encountered: ${address}`);
+ });
+ it('throws when address is unavailable', async () => {
+ const validUnrelatedAddress = '0x8b0292b11a196601eddce54b665cafeca0347d42';
+ const varName = 'address';
+ return expect(assert.isSenderAddressAsync(varName, validUnrelatedAddress, (zeroEx as any)._web3Wrapper))
+ .to.be.rejectedWith(
+ `Specified ${varName} ${validUnrelatedAddress} isn't available through the supplied web3 provider`,
+ );
+ });
+ it('doesn\'t throw if address is available', async () => {
+ const availableAddress = (await zeroEx.getAvailableAddressesAsync())[0];
+ const varName = 'address';
+ return expect(assert.isSenderAddressAsync(varName, availableAddress, (zeroEx as any)._web3Wrapper))
+ .to.become(undefined);
+ });
+ });
+});
diff --git a/packages/0x.js/test/ether_token_wrapper_test.ts b/packages/0x.js/test/ether_token_wrapper_test.ts
new file mode 100644
index 000000000..ba679d1a1
--- /dev/null
+++ b/packages/0x.js/test/ether_token_wrapper_test.ts
@@ -0,0 +1,111 @@
+import 'mocha';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, ZeroExError} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+// 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
+// the expected balance and actual balance (given the amount of ETH deposited), only deviates by the amount
+// required to pay gas costs.
+const MAX_REASONABLE_GAS_COST_IN_WEI = 62237;
+
+describe('EtherTokenWrapper', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let addressWithETH: string;
+ let wethContractAddress: string;
+ let depositWeiAmount: BigNumber;
+ let decimalPlaces: number;
+ const gasPrice = new BigNumber(1);
+ const zeroExConfig = {
+ gasPrice,
+ };
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider, zeroExConfig);
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ addressWithETH = userAddresses[0];
+ wethContractAddress = await zeroEx.etherToken.getContractAddressAsync();
+ depositWeiAmount = (zeroEx as any)._web3Wrapper.toWei(new BigNumber(5));
+ decimalPlaces = 7;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#depositAsync', () => {
+ it('should successfully deposit ETH and issue Wrapped ETH tokens', async () => {
+ const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ expect(preETHBalance).to.be.bignumber.gt(0);
+ expect(preWETHBalance).to.be.bignumber.equal(0);
+
+ const txHash = await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const postETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const postWETHBalanceInBaseUnits = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+
+ expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(depositWeiAmount);
+ const remainingETHInWei = preETHBalance.minus(depositWeiAmount);
+ const gasCost = remainingETHInWei.minus(postETHBalanceInWei);
+ expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI);
+ });
+ it('should throw if user has insufficient ETH balance for deposit', async () => {
+ const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+
+ const extraETHBalance = (zeroEx as any)._web3Wrapper.toWei(5, 'ether');
+ const overETHBalanceinWei = preETHBalance.add(extraETHBalance);
+
+ return expect(
+ zeroEx.etherToken.depositAsync(overETHBalanceinWei, addressWithETH),
+ ).to.be.rejectedWith(ZeroExError.InsufficientEthBalanceForDeposit);
+ });
+ });
+ describe('#withdrawAsync', () => {
+ it('should successfully withdraw ETH in return for Wrapped ETH tokens', async () => {
+ const ETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+
+ await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH);
+
+ const expectedPreETHBalance = ETHBalanceInWei.minus(depositWeiAmount);
+ const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ let gasCost = expectedPreETHBalance.minus(preETHBalance);
+ expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI);
+ expect(preWETHBalance).to.be.bignumber.equal(depositWeiAmount);
+
+ const txHash = await zeroEx.etherToken.withdrawAsync(depositWeiAmount, addressWithETH);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const postETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const postWETHBalanceInBaseUnits = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+
+ expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(0);
+ const expectedETHBalance = preETHBalance.add(depositWeiAmount).round(decimalPlaces);
+ gasCost = expectedETHBalance.minus(postETHBalance);
+ expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI);
+ });
+ it('should throw if user has insufficient WETH balance for withdrawl', async () => {
+ const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ expect(preWETHBalance).to.be.bignumber.equal(0);
+
+ const overWETHBalance = preWETHBalance.add(999999999);
+
+ return expect(
+ zeroEx.etherToken.withdrawAsync(overWETHBalance, addressWithETH),
+ ).to.be.rejectedWith(ZeroExError.InsufficientWEthBalanceForWithdrawal);
+ });
+ });
+});
diff --git a/packages/0x.js/test/event_watcher_test.ts b/packages/0x.js/test/event_watcher_test.ts
new file mode 100644
index 000000000..b4164fe63
--- /dev/null
+++ b/packages/0x.js/test/event_watcher_test.ts
@@ -0,0 +1,127 @@
+import 'mocha';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import * as Sinon from 'sinon';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {Web3Wrapper} from '../src/web3_wrapper';
+import {EventWatcher} from '../src/order_watcher/event_watcher';
+import {
+ ZeroEx,
+ LogEvent,
+ DecodedLogEvent,
+} from '../src';
+import {DoneCallback} from '../src/types';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('EventWatcher', () => {
+ let web3: Web3;
+ let stubs: Sinon.SinonStub[] = [];
+ let eventWatcher: EventWatcher;
+ let web3Wrapper: Web3Wrapper;
+ const numConfirmations = 0;
+ const logA: Web3.LogEntry = {
+ address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [],
+ transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17',
+ transactionIndex: 0,
+ };
+ const logB: Web3.LogEntry = {
+ address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ],
+ transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
+ transactionIndex: 0,
+ };
+ const logC: Web3.LogEntry = {
+ address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ],
+ transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
+ transactionIndex: 0,
+ };
+ before(async () => {
+ web3 = web3Factory.create();
+ const pollingIntervalMs = 10;
+ web3Wrapper = new Web3Wrapper(web3.currentProvider);
+ eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs);
+ });
+ afterEach(() => {
+ // clean up any stubs after the test has completed
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ eventWatcher.unsubscribe();
+ });
+ it('correctly emits initial log events', (done: DoneCallback) => {
+ const logs: Web3.LogEntry[] = [logA, logB];
+ const expectedLogEvents = [
+ {
+ removed: false,
+ ...logA,
+ },
+ {
+ removed: false,
+ ...logB,
+ },
+ ];
+ const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
+ getLogsStub.onCall(0).returns(logs);
+ stubs.push(getLogsStub);
+ const callback = (event: LogEvent) => {
+ const expectedLogEvent = expectedLogEvents.shift();
+ expect(event).to.be.deep.equal(expectedLogEvent);
+ if (_.isEmpty(expectedLogEvents)) {
+ done();
+ }
+ };
+ eventWatcher.subscribe(callback);
+ });
+ it('correctly computes the difference and emits only changes', (done: DoneCallback) => {
+ const initialLogs: Web3.LogEntry[] = [logA, logB];
+ const changedLogs: Web3.LogEntry[] = [logA, logC];
+ const expectedLogEvents = [
+ {
+ removed: false,
+ ...logA,
+ },
+ {
+ removed: false,
+ ...logB,
+ },
+ {
+ removed: true,
+ ...logB,
+ },
+ {
+ removed: false,
+ ...logC,
+ },
+ ];
+ const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
+ getLogsStub.onCall(0).returns(initialLogs);
+ getLogsStub.onCall(1).returns(changedLogs);
+ stubs.push(getLogsStub);
+ const callback = (event: LogEvent) => {
+ const expectedLogEvent = expectedLogEvents.shift();
+ expect(event).to.be.deep.equal(expectedLogEvent);
+ if (_.isEmpty(expectedLogEvents)) {
+ done();
+ }
+ };
+ eventWatcher.subscribe(callback);
+ });
+});
diff --git a/packages/0x.js/test/exchange_transfer_simulator_test.ts b/packages/0x.js/test/exchange_transfer_simulator_test.ts
new file mode 100644
index 000000000..99cb7fb4f
--- /dev/null
+++ b/packages/0x.js/test/exchange_transfer_simulator_test.ts
@@ -0,0 +1,87 @@
+import * as chai from 'chai';
+import BigNumber from 'bignumber.js';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, ExchangeContractErrs, Token} from '../src';
+import {TradeSide, TransferType} from '../src/types';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('ExchangeTransferSimulator', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ const transferAmount = new BigNumber(5);
+ let userAddresses: string[];
+ let tokens: Token[];
+ let coinbase: string;
+ let sender: string;
+ let recipient: string;
+ let exampleTokenAddress: string;
+ let exchangeTransferSimulator: ExchangeTransferSimulator;
+ let txHash: string;
+ before(async () => {
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ [coinbase, sender, recipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ exampleTokenAddress = tokens[0].address;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#transferFromAsync', () => {
+ beforeEach(() => {
+ exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
+ });
+ it('throws if the user doesn\'t have enough allowance', async () => {
+ return expect(exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
+ )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
+ });
+ it('throws if the user doesn\'t have enough balance', async () => {
+ txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ return expect(exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Maker, TransferType.Trade,
+ )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
+ });
+ it('updates balances and proxyAllowance after transfer', async () => {
+ txHash = await zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ await exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
+ );
+ const store = (exchangeTransferSimulator as any).store;
+ const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
+ const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
+ const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
+ expect(senderBalance).to.be.bignumber.equal(0);
+ expect(recipientBalance).to.be.bignumber.equal(transferAmount);
+ expect(senderProxyAllowance).to.be.bignumber.equal(0);
+ });
+ it('doesn\'t update proxyAllowance after transfer if unlimited', async () => {
+ txHash = await zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(exampleTokenAddress, sender);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ await exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
+ );
+ const store = (exchangeTransferSimulator as any).store;
+ const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
+ const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
+ const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
+ expect(senderBalance).to.be.bignumber.equal(0);
+ expect(recipientBalance).to.be.bignumber.equal(transferAmount);
+ expect(senderProxyAllowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+});
diff --git a/packages/0x.js/test/exchange_wrapper_test.ts b/packages/0x.js/test/exchange_wrapper_test.ts
new file mode 100644
index 000000000..26b8c1e0e
--- /dev/null
+++ b/packages/0x.js/test/exchange_wrapper_test.ts
@@ -0,0 +1,824 @@
+import 'mocha';
+import * as chai from 'chai';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {
+ ZeroEx,
+ Token,
+ SignedOrder,
+ SubscriptionOpts,
+ ExchangeEvents,
+ ExchangeContractErrs,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ LogFillContractEventArgs,
+ LogCancelContractEventArgs,
+ LogEvent,
+ DecodedLogEvent,
+} from '../src';
+import {DoneCallback, BlockParamLiteral} from '../src/types';
+import {FillScenarios} from './utils/fill_scenarios';
+import {TokenUtils} from './utils/token_utils';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777';
+
+describe('ExchangeWrapper', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let tokenUtils: TokenUtils;
+ let tokens: Token[];
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let fillScenarios: FillScenarios;
+ let exchangeContractAddress: string;
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('fillOrKill order(s)', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipient: string;
+ const takerTokenFillAmount = new BigNumber(5);
+ before(async () => {
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ describe('#batchFillOrKillAsync', () => {
+ it('successfully batch fillOrKill', async () => {
+ const fillableAmount = new BigNumber(5);
+ const partialFillTakerAmount = new BigNumber(2);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const orderFillRequests = [
+ {
+ signedOrder,
+ takerTokenFillAmount: partialFillTakerAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ takerTokenFillAmount: partialFillTakerAmount,
+ },
+ ];
+ await zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress);
+ });
+ describe('order transaction options', () => {
+ let signedOrder: SignedOrder;
+ let orderFillRequests: OrderFillRequest[];
+ const fillableAmount = new BigNumber(5);
+ beforeEach(async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ orderFillRequests = [
+ {
+ signedOrder,
+ takerTokenFillAmount: new BigNumber(0),
+ },
+ ];
+ });
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ describe('#fillOrKillOrderAsync', () => {
+ let signedOrder: SignedOrder;
+ const fillableAmount = new BigNumber(5);
+ beforeEach(async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ });
+ describe('successful fills', () => {
+ it('should fill a valid order', async () => {
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ });
+ it('should partially fill a valid order', async () => {
+ const partialFillAmount = new BigNumber(3);
+ await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ });
+ });
+ describe('order transaction options', () => {
+ const emptyFillableAmount = new BigNumber(0);
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ });
+ describe('fill order(s)', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipient: string;
+ const fillableAmount = new BigNumber(5);
+ const takerTokenFillAmount = new BigNumber(5);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ before(async () => {
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ describe('#fillOrderAsync', () => {
+ describe('successful fills', () => {
+ it('should fill a valid order', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(0);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount);
+ const txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(takerTokenFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ });
+ it('should partially fill the valid order', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const partialFillAmount = new BigNumber(3);
+ const txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(partialFillAmount);
+ expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
+ .to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ });
+ it('should fill the valid orders with fees', async () => {
+ const makerFee = new BigNumber(1);
+ const takerFee = new BigNumber(2);
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee,
+ makerAddress, takerAddress, fillableAmount, feeRecipient,
+ );
+ const txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ expect(await zeroEx.token.getBalanceAsync(zrxTokenAddress, feeRecipient))
+ .to.be.bignumber.equal(makerFee.plus(takerFee));
+ });
+ });
+ describe('order transaction options', () => {
+ let signedOrder: SignedOrder;
+ const emptyFillTakerAmount = new BigNumber(0);
+ beforeEach(async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ });
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.fillOrderAsync(
+ signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.fillOrderAsync(
+ signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.fillOrderAsync(
+ signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ describe('#batchFillOrdersAsync', () => {
+ let signedOrder: SignedOrder;
+ let signedOrderHashHex: string;
+ let anotherSignedOrder: SignedOrder;
+ let anotherOrderHashHex: string;
+ let orderFillBatch: OrderFillRequest[];
+ beforeEach(async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ signedOrderHashHex = ZeroEx.getOrderHashHex(signedOrder);
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
+ });
+ describe('successful batch fills', () => {
+ beforeEach(() => {
+ orderFillBatch = [
+ {
+ signedOrder,
+ takerTokenFillAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ takerTokenFillAmount,
+ },
+ ];
+ });
+ it('should throw if a batch is empty', async () => {
+ return expect(zeroEx.exchange.batchFillOrdersAsync(
+ [], shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ });
+ it('should successfully fill multiple orders', async () => {
+ const txHash = await zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex);
+ expect(filledAmount).to.be.bignumber.equal(takerTokenFillAmount);
+ expect(anotherFilledAmount).to.be.bignumber.equal(takerTokenFillAmount);
+ });
+ });
+ describe('order transaction options', () => {
+ beforeEach(async () => {
+ const emptyFillTakerAmount = new BigNumber(0);
+ orderFillBatch = [
+ {
+ signedOrder,
+ takerTokenFillAmount: emptyFillTakerAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ takerTokenFillAmount: emptyFillTakerAmount,
+ },
+ ];
+ });
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.batchFillOrdersAsync(
+ orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ describe('#fillOrdersUpTo', () => {
+ let signedOrder: SignedOrder;
+ let signedOrderHashHex: string;
+ let anotherSignedOrder: SignedOrder;
+ let anotherOrderHashHex: string;
+ let signedOrders: SignedOrder[];
+ const fillUpToAmount = fillableAmount.plus(fillableAmount).minus(1);
+ beforeEach(async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ signedOrderHashHex = ZeroEx.getOrderHashHex(signedOrder);
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
+ signedOrders = [signedOrder, anotherSignedOrder];
+ });
+ describe('successful batch fills', () => {
+ it('should throw if a batch is empty', async () => {
+ return expect(zeroEx.exchange.fillOrdersUpToAsync(
+ [], fillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ });
+ it('should successfully fill up to specified amount', async () => {
+ const txHash = await zeroEx.exchange.fillOrdersUpToAsync(
+ signedOrders, fillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex);
+ expect(filledAmount).to.be.bignumber.equal(fillableAmount);
+ const remainingFillAmount = fillableAmount.minus(1);
+ expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
+ });
+ });
+ describe('order transaction options', () => {
+ const emptyFillUpToAmount = new BigNumber(0);
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.fillOrdersUpToAsync(
+ signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.fillOrdersUpToAsync(
+ signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.fillOrdersUpToAsync(
+ signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ });
+ });
+ describe('cancel order(s)', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ const fillableAmount = new BigNumber(5);
+ let signedOrder: SignedOrder;
+ let orderHashHex: string;
+ const cancelAmount = new BigNumber(3);
+ beforeEach(async () => {
+ [coinbase, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ orderHashHex = ZeroEx.getOrderHashHex(signedOrder);
+ });
+ describe('#cancelOrderAsync', () => {
+ describe('successful cancels', () => {
+ it('should cancel an order', async () => {
+ const txHash = await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
+ expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
+ });
+ });
+ describe('order transaction options', () => {
+ const emptyCancelTakerTokenAmount = new BigNumber(0);
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ });
+ });
+ describe('#batchCancelOrdersAsync', () => {
+ let anotherSignedOrder: SignedOrder;
+ let anotherOrderHashHex: string;
+ let cancelBatch: OrderCancellationRequest[];
+ beforeEach(async () => {
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
+ cancelBatch = [
+ {
+ order: signedOrder,
+ takerTokenCancelAmount: cancelAmount,
+ },
+ {
+ order: anotherSignedOrder,
+ takerTokenCancelAmount: cancelAmount,
+ },
+ ];
+ });
+ describe('failed batch cancels', () => {
+ it('should throw when orders have different makers', async () => {
+ const signedOrderWithDifferentMaker = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, takerAddress, takerAddress, fillableAmount,
+ );
+ return expect(zeroEx.exchange.batchCancelOrdersAsync([
+ cancelBatch[0],
+ {
+ order: signedOrderWithDifferentMaker,
+ takerTokenCancelAmount: cancelAmount,
+ },
+ ])).to.be.rejectedWith(ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed);
+ });
+ });
+ describe('successful batch cancels', () => {
+ it('should cancel a batch of orders', async () => {
+ await zeroEx.exchange.batchCancelOrdersAsync(cancelBatch);
+ const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
+ const anotherCancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(
+ anotherOrderHashHex,
+ );
+ expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
+ expect(anotherCancelledAmount).to.be.bignumber.equal(cancelAmount);
+ });
+ });
+ describe('order transaction options', () => {
+ beforeEach(async () => {
+ const emptyTakerTokenCancelAmount = new BigNumber(0);
+ cancelBatch = [
+ {
+ order: signedOrder,
+ takerTokenCancelAmount: emptyTakerTokenCancelAmount,
+ },
+ {
+ order: anotherSignedOrder,
+ takerTokenCancelAmount: emptyTakerTokenCancelAmount,
+ },
+ ];
+ });
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, {
+ shouldValidate: true,
+ })).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, {
+ shouldValidate: false,
+ })).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ });
+ });
+ });
+ describe('tests that require partially filled order', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let takerAddress: string;
+ let fillableAmount: BigNumber;
+ let partialFillAmount: BigNumber;
+ let signedOrder: SignedOrder;
+ let orderHash: string;
+ before(() => {
+ takerAddress = userAddresses[1];
+ const [makerToken, takerToken] = tokens;
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ beforeEach(async () => {
+ fillableAmount = new BigNumber(5);
+ partialFillAmount = new BigNumber(2);
+ signedOrder = await fillScenarios.createPartiallyFilledSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, takerAddress, fillableAmount, partialFillAmount,
+ );
+ orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ });
+ describe('#getUnavailableTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(zeroEx.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const unavailableValueT = await zeroEx.exchange.getUnavailableTakerAmountAsync(NON_EXISTENT_ORDER_HASH);
+ expect(unavailableValueT).to.be.bignumber.equal(0);
+ });
+ it('should return the unavailableValueT for a valid and partially filled orderHash', async () => {
+ const unavailableValueT = await zeroEx.exchange.getUnavailableTakerAmountAsync(orderHash);
+ expect(unavailableValueT).to.be.bignumber.equal(partialFillAmount);
+ });
+ });
+ describe('#getFilledTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(zeroEx.exchange.getFilledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const filledValueT = await zeroEx.exchange.getFilledTakerAmountAsync(NON_EXISTENT_ORDER_HASH,
+ );
+ expect(filledValueT).to.be.bignumber.equal(0);
+ });
+ it('should return the filledValueT for a valid and partially filled orderHash', async () => {
+ const filledValueT = await zeroEx.exchange.getFilledTakerAmountAsync(orderHash);
+ expect(filledValueT).to.be.bignumber.equal(partialFillAmount);
+ });
+ });
+ describe('#getCanceledTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(zeroEx.exchange.getCanceledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(NON_EXISTENT_ORDER_HASH);
+ expect(cancelledValueT).to.be.bignumber.equal(0);
+ });
+ it('should return the cancelledValueT for a valid and partially filled orderHash', async () => {
+ const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash);
+ expect(cancelledValueT).to.be.bignumber.equal(0);
+ });
+ it('should return the cancelledValueT for a valid and cancelled orderHash', async () => {
+ const cancelAmount = fillableAmount.minus(partialFillAmount);
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash);
+ expect(cancelledValueT).to.be.bignumber.equal(cancelAmount);
+ });
+ });
+ });
+ describe('#subscribeAsync', () => {
+ const indexFilterValues = {};
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let takerAddress: string;
+ let makerAddress: string;
+ let fillableAmount: BigNumber;
+ let signedOrder: SignedOrder;
+ const takerTokenFillAmountInBaseUnits = new BigNumber(1);
+ const cancelTakerAmountInBaseUnits = new BigNumber(1);
+ before(() => {
+ [coinbase, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokens;
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ beforeEach(async () => {
+ fillableAmount = new BigNumber(5);
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ });
+ afterEach(async () => {
+ zeroEx.exchange.unsubscribeAll();
+ });
+ // Hack: Mocha does not allow a test to be both async and have a `done` callback
+ // Since we need to await the receipt of the event in the `subscribe` callback,
+ // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then
+ // wrap the rest of the test in an async block
+ // Source: https://github.com/mochajs/mocha/issues/2407
+ it('Should receive the LogFill event when an order is filled', (done: DoneCallback) => {
+ (async () => {
+
+ const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill);
+ done();
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callback,
+ );
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ })().catch(done);
+ });
+ it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => {
+ (async () => {
+
+ const callback = (err: Error, logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => {
+ expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel);
+ done();
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogCancel, indexFilterValues, callback,
+ );
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits);
+ })().catch(done);
+ });
+ it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
+ (async () => {
+
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled,
+ );
+
+ const newProvider = web3Factory.getRpcProvider();
+ await zeroEx.setProviderAsync(newProvider);
+
+ const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill);
+ done();
+ };
+ await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callback,
+ );
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ const subscriptionToken = await zeroEx.exchange.subscribeAsync(
+ ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled,
+ );
+ zeroEx.exchange.unsubscribe(subscriptionToken);
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ done();
+ })().catch(done);
+ });
+ });
+ describe('#getOrderHashHexUsingContractCallAsync', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ const fillableAmount = new BigNumber(5);
+ before(async () => {
+ [, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ it('get\'s the same hash as the local function', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ const orderHashFromContract = await (zeroEx.exchange as any)
+ ._getOrderHashHexUsingContractCallAsync(signedOrder);
+ expect(orderHash).to.equal(orderHashFromContract);
+ });
+ });
+ describe('#getZRXTokenAddressAsync', () => {
+ it('gets the same token as is in token registry', async () => {
+ const zrxAddress = await zeroEx.exchange.getZRXTokenAddressAsync();
+ const zrxToken = tokenUtils.getProtocolTokenOrThrow();
+ expect(zrxAddress).to.equal(zrxToken.address);
+ });
+ });
+ describe('#getLogsAsync', () => {
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ const fillableAmount = new BigNumber(5);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ const subscriptionOpts: SubscriptionOpts = {
+ fromBlock: BlockParamLiteral.Earliest,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(async () => {
+ [, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ it('should get logs with decoded args emitted by LogFill', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const eventName = ExchangeEvents.LogFill;
+ const indexFilterValues = {};
+ const logs = await zeroEx.exchange.getLogsAsync(eventName, subscriptionOpts, indexFilterValues);
+ expect(logs).to.have.length(1);
+ expect(logs[0].event).to.be.equal(eventName);
+ });
+ it('should only get the logs with the correct event name', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const differentEventName = ExchangeEvents.LogCancel;
+ const indexFilterValues = {};
+ const logs = await zeroEx.exchange.getLogsAsync(differentEventName, subscriptionOpts, indexFilterValues);
+ expect(logs).to.have.length(0);
+ });
+ it('should only get the logs with the correct indexed fields', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ txHash = await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const differentMakerAddress = userAddresses[2];
+ const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, differentMakerAddress, takerAddress, fillableAmount,
+ );
+ txHash = await zeroEx.exchange.fillOrderAsync(
+ anotherSignedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+
+ const eventName = ExchangeEvents.LogFill;
+ const indexFilterValues = {
+ maker: differentMakerAddress,
+ };
+ const logs = await zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>(
+ eventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args.maker).to.be.equal(differentMakerAddress);
+ });
+ });
+});
diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts
new file mode 100644
index 000000000..c8a4a8064
--- /dev/null
+++ b/packages/0x.js/test/order_state_watcher_test.ts
@@ -0,0 +1,356 @@
+import 'mocha';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import { chaiSetup } from './utils/chai_setup';
+import { web3Factory } from './utils/web3_factory';
+import { Web3Wrapper } from '../src/web3_wrapper';
+import { OrderStateWatcher } from '../src/order_watcher/order_state_watcher';
+import {
+ Token,
+ ZeroEx,
+ LogEvent,
+ DecodedLogEvent,
+ ZeroExConfig,
+ OrderState,
+ SignedOrder,
+ ZeroExError,
+ OrderStateValid,
+ OrderStateInvalid,
+ ExchangeContractErrs,
+} from '../src';
+import { TokenUtils } from './utils/token_utils';
+import { FillScenarios } from './utils/fill_scenarios';
+import { DoneCallback } from '../src/types';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {reportCallbackErrors} from './utils/report_callback_errors';
+
+const TIMEOUT_MS = 150;
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('OrderStateWatcher', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let fillScenarios: FillScenarios;
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let exchangeContractAddress: string;
+ let makerToken: Token;
+ let takerToken: Token;
+ let maker: string;
+ let taker: string;
+ let web3Wrapper: Web3Wrapper;
+ let signedOrder: SignedOrder;
+ const fillableAmount = new BigNumber(5);
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ [, maker, taker] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ web3Wrapper = (zeroEx as any)._web3Wrapper;
+ });
+ describe('#removeOrder', async () => {
+ it('should successfully remove existing order', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.include({
+ [orderHash]: signedOrder,
+ });
+ let dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes;
+ expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash);
+ zeroEx.orderStateWatcher.removeOrder(orderHash);
+ expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.not.include({
+ [orderHash]: signedOrder,
+ });
+ dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes;
+ expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined();
+ });
+ it('should no-op when removing a non-existing order', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ const nonExistentOrderHash = `0x${orderHash.substr(2).split('').reverse().join('')}`;
+ zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash);
+ });
+ });
+ describe('#subscribe', async () => {
+ afterEach(async () => {
+ zeroEx.orderStateWatcher.unsubscribe();
+ });
+ it('should fail when trying to subscribe twice', async () => {
+ zeroEx.orderStateWatcher.subscribe(_.noop);
+ expect(() => zeroEx.orderStateWatcher.subscribe(_.noop))
+ .to.throw(ZeroExError.SubscriptionAlreadyPresent);
+ });
+ });
+ describe('tests with cleanup', async () => {
+ afterEach(async () => {
+ zeroEx.orderStateWatcher.unsubscribe();
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.removeOrder(orderHash);
+ });
+ it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0));
+ })().catch(done);
+ });
+ it('should not emit an orderState event when irrelevant Transfer event received', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ throw new Error('OrderState callback fired for irrelevant order');
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const notTheMaker = userAddresses[0];
+ const anyRecipient = taker;
+ const transferAmount = new BigNumber(2);
+ const notTheMakerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, notTheMaker);
+ await zeroEx.token.transferAsync(makerToken.address, notTheMaker, anyRecipient, transferAmount);
+ setTimeout(() => {
+ done();
+ }, TIMEOUT_MS);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const anyRecipient = taker;
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when watched order fully filled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ it('should emit orderStateValid when watched order partially filled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+
+ const fillAmountInBaseUnits = new BigNumber(2);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits);
+ const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits);
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingFillable);
+ expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance);
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ describe('remainingFillableMakerTokenAmount', () => {
+ it('should calculate correct remaining fillable', (done: DoneCallback) => {
+ (async () => {
+ const takerFillableAmount = new BigNumber(10);
+ const makerFillableAmount = new BigNumber(20);
+ signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount);
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+ const fillAmountInBaseUnits = new BigNumber(2);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ new BigNumber(16));
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+
+ const changedMakerApprovalAmount = new BigNumber(3);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ changedMakerApprovalAmount);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
+ })().catch(done);
+ });
+ it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+
+ const remainingAmount = new BigNumber(1);
+ const transferAmount = makerBalance.sub(remainingAmount);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingAmount);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.transferAsync(
+ makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
+ })().catch(done);
+ });
+ });
+ it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ })().catch(done);
+ });
+ it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+
+ const cancelAmountInBaseUnits = new BigNumber(2);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits);
+ })().catch(done);
+ });
+ });
+});
diff --git a/packages/0x.js/test/order_validation_test.ts b/packages/0x.js/test/order_validation_test.ts
new file mode 100644
index 000000000..4f18742d3
--- /dev/null
+++ b/packages/0x.js/test/order_validation_test.ts
@@ -0,0 +1,327 @@
+import * as chai from 'chai';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import * as Sinon from 'sinon';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src';
+import {TradeSide, TransferType} from '../src/types';
+import {TokenUtils} from './utils/token_utils';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {FillScenarios} from './utils/fill_scenarios';
+import {OrderValidationUtils} from '../src/utils/order_validation_utils';
+import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('OrderValidation', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let exchangeContractAddress: string;
+ let zrxTokenAddress: string;
+ let fillScenarios: FillScenarios;
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipient: string;
+ let orderValidationUtils: OrderValidationUtils;
+ const fillableAmount = new BigNumber(5);
+ const fillTakerAmount = new BigNumber(5);
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ orderValidationUtils = new OrderValidationUtils(zeroEx.token, zeroEx.exchange);
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('validateOrderFillableOrThrowAsync', () => {
+ it('should succeed if the order is fillable', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ await zeroEx.exchange.validateOrderFillableOrThrowAsync(
+ signedOrder,
+ );
+ });
+ it('should succeed if the order is asymmetric and fillable', async () => {
+ const makerFillableAmount = fillableAmount;
+ const takerFillableAmount = fillableAmount.minus(4);
+ const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ makerFillableAmount, takerFillableAmount,
+ );
+ await zeroEx.exchange.validateOrderFillableOrThrowAsync(
+ signedOrder,
+ );
+ });
+ it('should throw when the order is fully filled or cancelled', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync(
+ signedOrder,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ });
+ it('should throw when order is expired', async () => {
+ const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, expirationInPast,
+ );
+ return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync(
+ signedOrder,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired);
+ });
+ });
+ describe('validateFillOrderAndThrowIfInvalidAsync', () => {
+ it('should throw when the fill amount is zero', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const zeroFillAmount = new BigNumber(0);
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, zeroFillAmount, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should throw when the signature is invalid', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ // 27 <--> 28
+ signedOrder.ecSignature.v = 27 + (28 - signedOrder.ecSignature.v);
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, fillableAmount, takerAddress,
+ )).to.be.rejectedWith(ZeroExError.InvalidSignature);
+ });
+ it('should throw when the order is fully filled or cancelled', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, fillableAmount, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ });
+ it('should throw when sender is not a taker', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const nonTakerAddress = userAddresses[6];
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, fillTakerAmount, nonTakerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
+ });
+ it('should throw when order is expired', async () => {
+ const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, expirationInPast,
+ );
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, fillTakerAmount, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired);
+ });
+ it('should throw when there a rounding error would have occurred', async () => {
+ const makerAmount = new BigNumber(3);
+ const takerAmount = new BigNumber(5);
+ const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ makerAmount, takerAmount,
+ );
+ const fillTakerAmountThatCausesRoundingError = new BigNumber(3);
+ return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder, fillTakerAmountThatCausesRoundingError, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.OrderFillRoundingError);
+ });
+ });
+ describe('#validateFillOrKillOrderAndThrowIfInvalidAsync', () => {
+ it('should throw if remaining fillAmount is less then the desired fillAmount', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ const tooLargeFillAmount = new BigNumber(7);
+ const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount);
+ await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount);
+ await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference);
+ await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount);
+
+ return expect(zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync(
+ signedOrder, tooLargeFillAmount, takerAddress,
+ )).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount);
+ });
+ });
+ describe('validateCancelOrderAndThrowIfInvalidAsync', () => {
+ let signedOrder: SignedOrder;
+ let orderHashHex: string;
+ const cancelAmount = new BigNumber(3);
+ beforeEach(async () => {
+ [coinbase, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
+ );
+ orderHashHex = ZeroEx.getOrderHashHex(signedOrder);
+ });
+ it('should throw when cancel amount is zero', async () => {
+ const zeroCancelAmount = new BigNumber(0);
+ return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, zeroCancelAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should throw when order is expired', async () => {
+ const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
+ const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, expirationInPast,
+ );
+ orderHashHex = ZeroEx.getOrderHashHex(expiredSignedOrder);
+ return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired);
+ });
+ it('should throw when order is already cancelled or filled', async () => {
+ await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, fillableAmount))
+ .to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
+ });
+ });
+ describe('#validateFillOrderBalancesAllowancesThrowIfInvalidAsync', () => {
+ let exchangeTransferSimulator: ExchangeTransferSimulator;
+ let transferFromAsync: Sinon.SinonSpy;
+ const bigNumberMatch = (expected: BigNumber) => {
+ return Sinon.match((value: BigNumber) => value.eq(expected));
+ };
+ beforeEach('create exchangeTransferSimulator', async () => {
+ exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
+ transferFromAsync = Sinon.spy();
+ exchangeTransferSimulator.transferFromAsync = transferFromAsync as any;
+ });
+ it('should call exchangeTransferSimulator.transferFrom in a correct order', async () => {
+ const makerFee = new BigNumber(2);
+ const takerFee = new BigNumber(2);
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee,
+ makerAddress, takerAddress, fillableAmount, feeRecipient,
+ );
+ await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress,
+ );
+ expect(transferFromAsync.callCount).to.be.equal(4);
+ expect(
+ transferFromAsync.getCall(0).calledWith(
+ makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount),
+ TradeSide.Maker, TransferType.Trade,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(1).calledWith(
+ takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount),
+ TradeSide.Taker, TransferType.Trade,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(2).calledWith(
+ zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee),
+ TradeSide.Maker, TransferType.Fee,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(3).calledWith(
+ zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee),
+ TradeSide.Taker, TransferType.Fee,
+ ),
+ ).to.be.true();
+ });
+ it('should call exchangeTransferSimulator.transferFrom with correct values for an open order', async () => {
+ const makerFee = new BigNumber(2);
+ const takerFee = new BigNumber(2);
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee,
+ makerAddress, ZeroEx.NULL_ADDRESS, fillableAmount, feeRecipient,
+ );
+ await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress,
+ );
+ expect(transferFromAsync.callCount).to.be.equal(4);
+ expect(
+ transferFromAsync.getCall(0).calledWith(
+ makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount),
+ TradeSide.Maker, TransferType.Trade,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(1).calledWith(
+ takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount),
+ TradeSide.Taker, TransferType.Trade,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(2).calledWith(
+ zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee),
+ TradeSide.Maker, TransferType.Fee,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(3).calledWith(
+ zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee),
+ TradeSide.Taker, TransferType.Fee,
+ ),
+ ).to.be.true();
+ });
+ it('should correctly round the fillMakerTokenAmount', async () => {
+ const makerTokenAmount = new BigNumber(3);
+ const takerTokenAmount = new BigNumber(1);
+ const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, makerTokenAmount, takerTokenAmount,
+ );
+ await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTransferSimulator, signedOrder, takerTokenAmount, takerAddress, zrxTokenAddress,
+ );
+ expect(transferFromAsync.callCount).to.be.equal(4);
+ const makerFillAmount = transferFromAsync.getCall(0).args[3];
+ expect(makerFillAmount).to.be.bignumber.equal(makerTokenAmount);
+ });
+ it('should correctly round the makerFeeAmount', async () => {
+ const makerFee = new BigNumber(2);
+ const takerFee = new BigNumber(4);
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
+ fillableAmount, ZeroEx.NULL_ADDRESS,
+ );
+ const fillTakerTokenAmount = fillableAmount.div(2).round(0);
+ await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTransferSimulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
+ );
+ const makerPartialFee = makerFee.div(2);
+ const takerPartialFee = takerFee.div(2);
+ expect(transferFromAsync.callCount).to.be.equal(4);
+ const partialMakerFee = transferFromAsync.getCall(2).args[3];
+ expect(partialMakerFee).to.be.bignumber.equal(makerPartialFee);
+ const partialTakerFee = transferFromAsync.getCall(3).args[3];
+ expect(partialTakerFee).to.be.bignumber.equal(takerPartialFee);
+ });
+ });
+});
diff --git a/packages/0x.js/test/subscription_test.ts b/packages/0x.js/test/subscription_test.ts
new file mode 100644
index 000000000..985fdc1d6
--- /dev/null
+++ b/packages/0x.js/test/subscription_test.ts
@@ -0,0 +1,95 @@
+import 'mocha';
+import * as _ from 'lodash';
+import * as chai from 'chai';
+import * as Sinon from 'sinon';
+import {chaiSetup} from './utils/chai_setup';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+import {web3Factory} from './utils/web3_factory';
+import {
+ ZeroEx,
+ ZeroExError,
+ Token,
+ ApprovalContractEventArgs,
+ TokenEvents,
+ LogEvent,
+} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {TokenUtils} from './utils/token_utils';
+import {DoneCallback, BlockParamLiteral} from '../src/types';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('SubscriptionTest', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let coinbase: string;
+ let addressWithoutFunds: string;
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ coinbase = userAddresses[0];
+ addressWithoutFunds = userAddresses[1];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ let tokenAddress: string;
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ let stubs: Sinon.SinonStub[] = [];
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(() => {
+ zeroEx.token.unsubscribeAll();
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ });
+ it('Should receive the Error when an error occurs', (done: DoneCallback) => {
+ (async () => {
+ const callback = (err: Error, logEvent: LogEvent<ApprovalContractEventArgs>) => {
+ expect(err).to.not.be.null();
+ expect(logEvent).to.be.undefined();
+ done();
+ };
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'getBlockAsync')
+ .throws("JSON RPC error")
+ ]
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
+ })().catch(done);
+ });
+ it('Should allow unsubscribeAll to be called successfully after an error', (done: DoneCallback) => {
+ (async () => {
+ const callback = (err: Error, logEvent: LogEvent<ApprovalContractEventArgs>) => { };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ stubs = [
+ Sinon.stub((zeroEx as any)._web3Wrapper, 'getBlockAsync')
+ .throws("JSON RPC error")
+ ]
+ zeroEx.token.unsubscribeAll();
+ done();
+ })().catch(done);
+ });
+ })
+ }) \ No newline at end of file
diff --git a/packages/0x.js/test/token_registry_wrapper_test.ts b/packages/0x.js/test/token_registry_wrapper_test.ts
new file mode 100644
index 000000000..6b5dd517e
--- /dev/null
+++ b/packages/0x.js/test/token_registry_wrapper_test.ts
@@ -0,0 +1,123 @@
+import * as _ from 'lodash';
+import 'mocha';
+import * as chai from 'chai';
+import {SchemaValidator, schemas} from '0x-json-schemas';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, Token} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+const TOKEN_REGISTRY_SIZE_AFTER_MIGRATION = 7;
+
+describe('TokenRegistryWrapper', () => {
+ let zeroEx: ZeroEx;
+ let tokens: Token[];
+ const tokenAddressBySymbol: {[symbol: string]: string} = {};
+ const tokenAddressByName: {[symbol: string]: string} = {};
+ const tokenBySymbol: {[symbol: string]: Token} = {};
+ const tokenByName: {[symbol: string]: Token} = {};
+ const registeredSymbol = 'ZRX';
+ const registeredName = '0x Protocol Token';
+ const unregisteredSymbol = 'MAL';
+ const unregisteredName = 'Malicious Token';
+ before(async () => {
+ const web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ _.map(tokens, token => {
+ tokenAddressBySymbol[token.symbol] = token.address;
+ tokenAddressByName[token.name] = token.address;
+ tokenBySymbol[token.symbol] = token;
+ tokenByName[token.name] = token;
+ });
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#getTokensAsync', () => {
+ it('should return all the tokens added to the tokenRegistry during the migration', async () => {
+ expect(tokens).to.have.lengthOf(TOKEN_REGISTRY_SIZE_AFTER_MIGRATION);
+
+ const schemaValidator = new SchemaValidator();
+ _.each(tokens, token => {
+ const validationResult = schemaValidator.validate(token, schemas.tokenSchema);
+ expect(validationResult.errors).to.have.lengthOf(0);
+ });
+ });
+ });
+ describe('#getTokenAddressesAsync', () => {
+ it('should return all the token addresses added to the tokenRegistry during the migration', async () => {
+ const tokenAddresses = await zeroEx.tokenRegistry.getTokenAddressesAsync();
+ expect(tokenAddresses).to.have.lengthOf(TOKEN_REGISTRY_SIZE_AFTER_MIGRATION);
+
+ const schemaValidator = new SchemaValidator();
+ _.each(tokenAddresses, tokenAddress => {
+ const validationResult = schemaValidator.validate(tokenAddress, schemas.addressSchema);
+ expect(validationResult.errors).to.have.lengthOf(0);
+ expect(tokenAddress).to.not.be.equal(ZeroEx.NULL_ADDRESS);
+ });
+ });
+ });
+ describe('#getTokenAddressBySymbol', () => {
+ it('should return correct address for a token in the registry', async () => {
+ const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressBySymbolIfExistsAsync(registeredSymbol);
+ expect(tokenAddress).to.be.equal(tokenAddressBySymbol[registeredSymbol]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressBySymbolIfExistsAsync(unregisteredSymbol);
+ expect(tokenAddress).to.be.undefined();
+ });
+ });
+ describe('#getTokenAddressByName', () => {
+ it('should return correct address for a token in the registry', async () => {
+ const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressByNameIfExistsAsync(registeredName);
+ expect(tokenAddress).to.be.equal(tokenAddressByName[registeredName]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressByNameIfExistsAsync(unregisteredName);
+ expect(tokenAddress).to.be.undefined();
+ });
+ });
+ describe('#getTokenBySymbol', () => {
+ it('should return correct token for a token in the registry', async () => {
+ const token = await zeroEx.tokenRegistry.getTokenBySymbolIfExistsAsync(registeredSymbol);
+ expect(token).to.be.deep.equal(tokenBySymbol[registeredSymbol]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const token = await zeroEx.tokenRegistry.getTokenBySymbolIfExistsAsync(unregisteredSymbol);
+ expect(token).to.be.undefined();
+ });
+ });
+ describe('#getTokenByName', () => {
+ it('should return correct token for a token in the registry', async () => {
+ const token = await zeroEx.tokenRegistry.getTokenByNameIfExistsAsync(registeredName);
+ expect(token).to.be.deep.equal(tokenByName[registeredName]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const token = await zeroEx.tokenRegistry.getTokenByNameIfExistsAsync(unregisteredName);
+ expect(token).to.be.undefined();
+ });
+ });
+ describe('#getTokenIfExistsAsync', () => {
+ it('should return the token added to the tokenRegistry during the migration', async () => {
+ const aToken = tokens[0];
+
+ const token = await zeroEx.tokenRegistry.getTokenIfExistsAsync(aToken.address);
+ const schemaValidator = new SchemaValidator();
+ const validationResult = schemaValidator.validate(token, schemas.tokenSchema);
+ expect(validationResult.errors).to.have.lengthOf(0);
+ });
+ it('should return return undefined when passed a token address not in the tokenRegistry', async () => {
+ const unregisteredTokenAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
+ const tokenIfExists = await zeroEx.tokenRegistry.getTokenIfExistsAsync(unregisteredTokenAddress);
+ expect(tokenIfExists).to.be.undefined();
+ });
+ });
+});
diff --git a/packages/0x.js/test/token_transfer_proxy_wrapper_test.ts b/packages/0x.js/test/token_transfer_proxy_wrapper_test.ts
new file mode 100644
index 000000000..8faef0b30
--- /dev/null
+++ b/packages/0x.js/test/token_transfer_proxy_wrapper_test.ts
@@ -0,0 +1,31 @@
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx} from '../src';
+import {TokenTransferProxyWrapper} from '../src/contract_wrappers/token_transfer_proxy_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('TokenTransferProxyWrapper', () => {
+ let zeroEx: ZeroEx;
+ before(async () => {
+ const web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ });
+ describe('#isAuthorizedAsync', () => {
+ it('should return false if the address is not authorized', async () => {
+ const isAuthorized = await zeroEx.proxy.isAuthorizedAsync(ZeroEx.NULL_ADDRESS);
+ expect(isAuthorized).to.be.false();
+ });
+ });
+ describe('#getAuthorizedAddressesAsync', () => {
+ it('should return the list of authorized addresses', async () => {
+ const authorizedAddresses = await zeroEx.proxy.getAuthorizedAddressesAsync();
+ for (const authorizedAddress of authorizedAddresses) {
+ const isAuthorized = await zeroEx.proxy.isAuthorizedAsync(authorizedAddress);
+ expect(isAuthorized).to.be.true();
+ }
+ });
+ });
+});
diff --git a/packages/0x.js/test/token_wrapper_test.ts b/packages/0x.js/test/token_wrapper_test.ts
new file mode 100644
index 000000000..b30762e8c
--- /dev/null
+++ b/packages/0x.js/test/token_wrapper_test.ts
@@ -0,0 +1,477 @@
+import 'mocha';
+import * as chai from 'chai';
+import {chaiSetup} from './utils/chai_setup';
+import * as Web3 from 'web3';
+import BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+import {web3Factory} from './utils/web3_factory';
+import {
+ ZeroEx,
+ ZeroExError,
+ Token,
+ SubscriptionOpts,
+ TokenEvents,
+ ContractEvent,
+ TransferContractEventArgs,
+ ApprovalContractEventArgs,
+ TokenContractEventArgs,
+ LogWithDecodedArgs,
+ LogEvent,
+ DecodedLogEvent,
+} from '../src';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {TokenUtils} from './utils/token_utils';
+import {DoneCallback, BlockParamLiteral} from '../src/types';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('TokenWrapper', () => {
+ let web3: Web3;
+ let zeroEx: ZeroEx;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let coinbase: string;
+ let addressWithoutFunds: string;
+ before(async () => {
+ web3 = web3Factory.create();
+ zeroEx = new ZeroEx(web3.currentProvider);
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ coinbase = userAddresses[0];
+ addressWithoutFunds = userAddresses[1];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#transferAsync', () => {
+ let token: Token;
+ let transferAmount: BigNumber;
+ before(() => {
+ token = tokens[0];
+ transferAmount = new BigNumber(42);
+ });
+ it('should successfully transfer tokens', async () => {
+ const fromAddress = coinbase;
+ const toAddress = addressWithoutFunds;
+ const preBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
+ expect(preBalance).to.be.bignumber.equal(0);
+ const txHash = await zeroEx.token.transferAsync(token.address, fromAddress, toAddress, transferAmount);
+ const receipt = await zeroEx.awaitTransactionMinedAsync(txHash);
+ const postBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
+ return expect(postBalance).to.be.bignumber.equal(transferAmount);
+ });
+ it('should fail to transfer tokens if fromAddress has an insufficient balance', async () => {
+ const fromAddress = addressWithoutFunds;
+ const toAddress = coinbase;
+ return expect(zeroEx.token.transferAsync(
+ token.address, fromAddress, toAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.InsufficientBalanceForTransfer);
+ });
+ it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => {
+ const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065';
+ const fromAddress = coinbase;
+ const toAddress = coinbase;
+ return expect(zeroEx.token.transferAsync(
+ nonExistentTokenAddress, fromAddress, toAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ });
+ describe('#transferFromAsync', () => {
+ let token: Token;
+ let toAddress: string;
+ let senderAddress: string;
+ before(async () => {
+ token = tokens[0];
+ toAddress = addressWithoutFunds;
+ senderAddress = userAddresses[2];
+ });
+ it('should fail to transfer tokens if fromAddress has insufficient allowance set', async () => {
+ const fromAddress = coinbase;
+ const transferAmount = new BigNumber(42);
+
+ const fromAddressBalance = await zeroEx.token.getBalanceAsync(token.address, fromAddress);
+ expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount);
+
+ const fromAddressAllowance = await zeroEx.token.getAllowanceAsync(token.address, fromAddress,
+ toAddress);
+ expect(fromAddressAllowance).to.be.bignumber.equal(0);
+
+ return expect(zeroEx.token.transferFromAsync(
+ token.address, fromAddress, toAddress, senderAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.InsufficientAllowanceForTransfer);
+ });
+ it('[regression] should fail to transfer tokens if set allowance for toAddress instead of senderAddress',
+ async () => {
+ const fromAddress = coinbase;
+ const transferAmount = new BigNumber(42);
+
+ await zeroEx.token.setAllowanceAsync(token.address, fromAddress, toAddress, transferAmount);
+
+ return expect(zeroEx.token.transferFromAsync(
+ token.address, fromAddress, toAddress, senderAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.InsufficientAllowanceForTransfer);
+ });
+ it('should fail to transfer tokens if fromAddress has insufficient balance', async () => {
+ const fromAddress = addressWithoutFunds;
+ const transferAmount = new BigNumber(42);
+
+ const fromAddressBalance = await zeroEx.token.getBalanceAsync(token.address, fromAddress);
+ expect(fromAddressBalance).to.be.bignumber.equal(0);
+
+ await zeroEx.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
+ const fromAddressAllowance = await zeroEx.token.getAllowanceAsync(token.address, fromAddress,
+ senderAddress);
+ expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount);
+
+ return expect(zeroEx.token.transferFromAsync(
+ token.address, fromAddress, toAddress, senderAddress, transferAmount,
+ )).to.be.rejectedWith(ZeroExError.InsufficientBalanceForTransfer);
+ });
+ it('should successfully transfer tokens', async () => {
+ const fromAddress = coinbase;
+
+ const preBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
+ expect(preBalance).to.be.bignumber.equal(0);
+
+ const transferAmount = new BigNumber(42);
+ await zeroEx.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
+
+ await zeroEx.token.transferFromAsync(token.address, fromAddress, toAddress, senderAddress,
+ transferAmount);
+ const postBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
+ return expect(postBalance).to.be.bignumber.equal(transferAmount);
+ });
+ it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => {
+ const fromAddress = coinbase;
+ const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065';
+ return expect(zeroEx.token.transferFromAsync(
+ nonExistentTokenAddress, fromAddress, toAddress, senderAddress, new BigNumber(42),
+ )).to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ });
+ describe('#getBalanceAsync', () => {
+ describe('With web3 provider with accounts', () => {
+ it('should return the balance for an existing ERC20 token', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const balance = await zeroEx.token.getBalanceAsync(token.address, ownerAddress);
+ const expectedBalance = new BigNumber('1000000000000000000000000000');
+ return expect(balance).to.be.bignumber.equal(expectedBalance);
+ });
+ it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => {
+ const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065';
+ const ownerAddress = coinbase;
+ return expect(zeroEx.token.getBalanceAsync(nonExistentTokenAddress, ownerAddress))
+ .to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
+ });
+ it('should return a balance of 0 for a non-existent owner address', async () => {
+ const token = tokens[0];
+ const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593';
+ const balance = await zeroEx.token.getBalanceAsync(token.address, nonExistentOwner);
+ const expectedBalance = new BigNumber(0);
+ return expect(balance).to.be.bignumber.equal(expectedBalance);
+ });
+ });
+ describe('With web3 provider without accounts', () => {
+ let zeroExWithoutAccounts: ZeroEx;
+ before(async () => {
+ const hasAddresses = false;
+ const web3WithoutAccounts = web3Factory.create(hasAddresses);
+ zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider);
+ });
+ it('should return balance even when called with Web3 provider instance without addresses', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const balance = await zeroExWithoutAccounts.token.getBalanceAsync(token.address, ownerAddress);
+ const expectedBalance = new BigNumber('1000000000000000000000000000');
+ return expect(balance).to.be.bignumber.equal(expectedBalance);
+ });
+ });
+ });
+ describe('#setAllowanceAsync', () => {
+ it('should set the spender\'s allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+
+ const allowanceBeforeSet = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress,
+ spenderAddress);
+ const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
+ expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
+
+ const allowanceAfterSet = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const expectedAllowanceAfterAllowanceSet = amountInBaseUnits;
+ return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet);
+ });
+ });
+ describe('#setUnlimitedAllowanceAsync', () => {
+ it('should set the unlimited spender\'s allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+
+ await zeroEx.token.setUnlimitedAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ return expect(allowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ it('should reduce the gas cost for transfers including tokens with unlimited allowance support', async () => {
+ const transferAmount = new BigNumber(5);
+ const zrx = tokenUtils.getProtocolTokenOrThrow();
+ const [, userWithNormalAllowance, userWithUnlimitedAllowance] = userAddresses;
+ await zeroEx.token.setAllowanceAsync(zrx.address, coinbase, userWithNormalAllowance, transferAmount);
+ await zeroEx.token.setUnlimitedAllowanceAsync(zrx.address, coinbase, userWithUnlimitedAllowance);
+
+ const initBalanceWithNormalAllowance = await promisify(web3.eth.getBalance)(userWithNormalAllowance);
+ const initBalanceWithUnlimitedAllowance = await promisify(web3.eth.getBalance)(userWithUnlimitedAllowance);
+
+ await zeroEx.token.transferFromAsync(
+ zrx.address, coinbase, userWithNormalAllowance, userWithNormalAllowance, transferAmount,
+ );
+ await zeroEx.token.transferFromAsync(
+ zrx.address, coinbase, userWithUnlimitedAllowance, userWithUnlimitedAllowance, transferAmount,
+ );
+
+ const finalBalanceWithNormalAllowance = await promisify(web3.eth.getBalance)(userWithNormalAllowance);
+ const finalBalanceWithUnlimitedAllowance = await promisify(web3.eth.getBalance)(userWithUnlimitedAllowance);
+
+ const normalGasCost = initBalanceWithNormalAllowance.minus(finalBalanceWithNormalAllowance);
+ const unlimitedGasCost = initBalanceWithUnlimitedAllowance.minus(finalBalanceWithUnlimitedAllowance);
+
+ // In theory the gas cost with unlimited allowance should be smaller, but with testrpc it's actually bigger.
+ // This needs to be investigated in ethereumjs-vm. This test is essentially a repro.
+ // TODO: Make this test pass with inverted assertion.
+ expect(unlimitedGasCost.toNumber()).to.be.gt(normalGasCost.toNumber());
+ });
+ });
+ describe('#getAllowanceAsync', () => {
+ describe('With web3 provider with accounts', () => {
+ it('should get the proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
+
+ const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const expectedAllowance = amountInBaseUnits;
+ return expect(allowance).to.be.bignumber.equal(expectedAllowance);
+ });
+ it('should return 0 if no allowance set yet', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+ const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const expectedAllowance = new BigNumber(0);
+ return expect(allowance).to.be.bignumber.equal(expectedAllowance);
+ });
+ });
+ describe('With web3 provider without accounts', () => {
+ let zeroExWithoutAccounts: ZeroEx;
+ before(async () => {
+ const hasAddresses = false;
+ const web3WithoutAccounts = web3Factory.create(hasAddresses);
+ zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider);
+ });
+ it('should get the proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
+
+ const allowance = await zeroExWithoutAccounts.token.getAllowanceAsync(
+ token.address, ownerAddress, spenderAddress,
+ );
+ const expectedAllowance = amountInBaseUnits;
+ return expect(allowance).to.be.bignumber.equal(expectedAllowance);
+ });
+ });
+ });
+ describe('#getProxyAllowanceAsync', () => {
+ it('should get the proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
+
+ const allowance = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ const expectedAllowance = amountInBaseUnits;
+ return expect(allowance).to.be.bignumber.equal(expectedAllowance);
+ });
+ });
+ describe('#setProxyAllowanceAsync', () => {
+ it('should set the proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+
+ const allowanceBeforeSet = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
+ expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
+
+ const amountInBaseUnits = new BigNumber(50);
+ await zeroEx.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
+
+ const allowanceAfterSet = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ const expectedAllowanceAfterAllowanceSet = amountInBaseUnits;
+ return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet);
+ });
+ });
+ describe('#setUnlimitedProxyAllowanceAsync', () => {
+ it('should set the unlimited proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+
+ await zeroEx.token.setUnlimitedProxyAllowanceAsync(token.address, ownerAddress);
+ const allowance = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ return expect(allowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ let tokenAddress: string;
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(() => {
+ zeroEx.token.unsubscribeAll();
+ });
+ // Hack: Mocha does not allow a test to be both async and have a `done` callback
+ // Since we need to await the receipt of the event in the `subscribe` callback,
+ // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then
+ // wrap the rest of the test in an async block
+ // Source: https://github.com/mochajs/mocha/issues/2407
+ it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => {
+ (async () => {
+ const callback = (err: Error, logEvent: DecodedLogEvent<TransferContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ const args = logEvent.args;
+ expect(args._from).to.be.equal(coinbase);
+ expect(args._to).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(transferAmount);
+ done();
+ };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callback);
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => {
+ (async () => {
+ const callback = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ const args = logEvent.args;
+ expect(args._owner).to.be.equal(coinbase);
+ expect(args._spender).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(allowanceAmount);
+ done();
+ };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
+ })().catch(done);
+ });
+ it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled,
+ );
+ const callbackToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done();
+ };
+ const newProvider = web3Factory.getRpcProvider();
+ await zeroEx.setProviderAsync(newProvider);
+ zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackToBeCalled,
+ );
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ };
+ const subscriptionToken = zeroEx.token.subscribe(
+ tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled);
+ zeroEx.token.unsubscribe(subscriptionToken);
+ await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ done();
+ })().catch(done);
+ });
+ });
+ describe('#getLogsAsync', () => {
+ let tokenAddress: string;
+ let tokenTransferProxyAddress: string;
+ const subscriptionOpts: SubscriptionOpts = {
+ fromBlock: BlockParamLiteral.Earliest,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(async () => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ tokenTransferProxyAddress = await zeroEx.proxy.getContractAddressAsync();
+ });
+ it('should get logs with decoded args emitted by Approval', async () => {
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const eventName = TokenEvents.Approval;
+ const indexFilterValues = {};
+ const logs = await zeroEx.token.getLogsAsync<ApprovalContractEventArgs>(
+ tokenAddress, eventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(logs[0].event).to.be.equal(eventName);
+ expect(args._owner).to.be.equal(coinbase);
+ expect(args._spender).to.be.equal(tokenTransferProxyAddress);
+ expect(args._value).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ it('should only get the logs with the correct event name', async () => {
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const differentEventName = TokenEvents.Transfer;
+ const indexFilterValues = {};
+ const logs = await zeroEx.token.getLogsAsync(
+ tokenAddress, differentEventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(0);
+ });
+ it('should only get the logs with the correct indexed fields', async () => {
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, addressWithoutFunds);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ const eventName = TokenEvents.Approval;
+ const indexFilterValues = {
+ _owner: coinbase,
+ };
+ const logs = await zeroEx.token.getLogsAsync<ApprovalContractEventArgs>(
+ tokenAddress, eventName, subscriptionOpts, indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args._owner).to.be.equal(coinbase);
+ });
+ });
+});
diff --git a/packages/0x.js/test/utils/blockchain_lifecycle.ts b/packages/0x.js/test/utils/blockchain_lifecycle.ts
new file mode 100644
index 000000000..9a44ccd6f
--- /dev/null
+++ b/packages/0x.js/test/utils/blockchain_lifecycle.ts
@@ -0,0 +1,26 @@
+import {RPC} from './rpc';
+
+export class BlockchainLifecycle {
+ private rpc: RPC;
+ private snapshotIdsStack: number[];
+ constructor() {
+ this.rpc = new RPC();
+ 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();
+ this.snapshotIdsStack.push(snapshotId);
+ }
+ public async revertAsync(): Promise<void> {
+ const snapshotId = this.snapshotIdsStack.pop() as number;
+ const didRevert = await this.rpc.revertSnapshotAsync(snapshotId);
+ if (!didRevert) {
+ throw new Error(`Snapshot with id #${snapshotId} failed to revert`);
+ }
+ }
+ public async mineABlock(): Promise<void> {
+ await this.rpc.mineBlockAsync();
+ }
+}
diff --git a/packages/0x.js/test/utils/chai_setup.ts b/packages/0x.js/test/utils/chai_setup.ts
new file mode 100644
index 000000000..c18988106
--- /dev/null
+++ b/packages/0x.js/test/utils/chai_setup.ts
@@ -0,0 +1,13 @@
+import * as chai from 'chai';
+import * as dirtyChai from 'dirty-chai';
+import ChaiBigNumber = require('chai-bignumber');
+import chaiAsPromised = require('chai-as-promised');
+
+export const chaiSetup = {
+ configure() {
+ chai.config.includeStack = true;
+ chai.use(ChaiBigNumber());
+ chai.use(dirtyChai);
+ chai.use(chaiAsPromised);
+ },
+};
diff --git a/packages/0x.js/test/utils/constants.ts b/packages/0x.js/test/utils/constants.ts
new file mode 100644
index 000000000..c7d3aebca
--- /dev/null
+++ b/packages/0x.js/test/utils/constants.ts
@@ -0,0 +1,8 @@
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ RPC_HOST: 'localhost',
+ RPC_PORT: 8545,
+ TESTRPC_NETWORK_ID: 50,
+ KOVAN_RPC_URL: 'https://kovan.infura.io',
+ ROPSTEN_RPC_URL: 'https://ropsten.infura.io',
+};
diff --git a/packages/0x.js/test/utils/fill_scenarios.ts b/packages/0x.js/test/utils/fill_scenarios.ts
new file mode 100644
index 000000000..a0632b12c
--- /dev/null
+++ b/packages/0x.js/test/utils/fill_scenarios.ts
@@ -0,0 +1,114 @@
+import BigNumber from 'bignumber.js';
+import {ZeroEx, Token, SignedOrder} from '../../src';
+import {orderFactory} from '../utils/order_factory';
+import {constants} from './constants';
+
+export class FillScenarios {
+ private zeroEx: ZeroEx;
+ private userAddresses: string[];
+ private tokens: Token[];
+ private coinbase: string;
+ private zrxTokenAddress: string;
+ private exchangeContractAddress: string;
+ constructor(zeroEx: ZeroEx, userAddresses: string[],
+ tokens: Token[], zrxTokenAddress: string, exchangeContractAddress: string) {
+ this.zeroEx = zeroEx;
+ this.userAddresses = userAddresses;
+ this.tokens = tokens;
+ this.coinbase = userAddresses[0];
+ this.zrxTokenAddress = zrxTokenAddress;
+ this.exchangeContractAddress = exchangeContractAddress;
+ }
+ public async createFillableSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string,
+ makerAddress: string, takerAddress: string,
+ fillableAmount: BigNumber,
+ expirationUnixTimestampSec?: BigNumber):
+ Promise<SignedOrder> {
+ return this.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, fillableAmount, expirationUnixTimestampSec,
+ );
+ }
+ public async createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress: string, takerTokenAddress: string,
+ makerFee: BigNumber, takerFee: BigNumber,
+ makerAddress: string, takerAddress: string,
+ fillableAmount: BigNumber,
+ feeRecepient: string, expirationUnixTimestampSec?: BigNumber,
+ ): Promise<SignedOrder> {
+ return this.createAsymmetricFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
+ fillableAmount, fillableAmount, feeRecepient, expirationUnixTimestampSec,
+ );
+ }
+ public async createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress: string, takerTokenAddress: string, makerAddress: string, takerAddress: string,
+ makerFillableAmount: BigNumber, takerFillableAmount: BigNumber,
+ expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
+ const makerFee = new BigNumber(0);
+ const takerFee = new BigNumber(0);
+ const feeRecepient = constants.NULL_ADDRESS;
+ return this.createAsymmetricFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
+ makerFillableAmount, takerFillableAmount, feeRecepient, expirationUnixTimestampSec,
+ );
+ }
+ public async createPartiallyFilledSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string,
+ takerAddress: string, fillableAmount: BigNumber,
+ partialFillAmount: BigNumber) {
+ const [makerAddress] = this.userAddresses;
+ const signedOrder = await this.createAsymmetricFillableSignedOrderAsync(
+ makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
+ fillableAmount, fillableAmount,
+ );
+ const shouldThrowOnInsufficientBalanceOrAllowance = false;
+ await this.zeroEx.exchange.fillOrderAsync(
+ signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
+ );
+ return signedOrder;
+ }
+ private async createAsymmetricFillableSignedOrderWithFeesAsync(
+ makerTokenAddress: string, takerTokenAddress: string,
+ makerFee: BigNumber, takerFee: BigNumber,
+ makerAddress: string, takerAddress: string,
+ makerFillableAmount: BigNumber, takerFillableAmount: BigNumber,
+ feeRecepient: string, expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
+
+ await Promise.all([
+ this.increaseBalanceAndAllowanceAsync(makerTokenAddress, makerAddress, makerFillableAmount),
+ this.increaseBalanceAndAllowanceAsync(takerTokenAddress, takerAddress, takerFillableAmount),
+ ]);
+ await Promise.all([
+ this.increaseBalanceAndAllowanceAsync(this.zrxTokenAddress, makerAddress, makerFee),
+ this.increaseBalanceAndAllowanceAsync(this.zrxTokenAddress, takerAddress, takerFee),
+ ]);
+
+ const signedOrder = await orderFactory.createSignedOrderAsync(this.zeroEx,
+ makerAddress, takerAddress, makerFee, takerFee,
+ makerFillableAmount, makerTokenAddress, takerFillableAmount, takerTokenAddress,
+ this.exchangeContractAddress, feeRecepient, expirationUnixTimestampSec);
+ return signedOrder;
+ }
+ private async increaseBalanceAndAllowanceAsync(
+ tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
+ if (amount.isZero() || address === ZeroEx.NULL_ADDRESS) {
+ return; // noop
+ }
+ await Promise.all([
+ this.increaseBalanceAsync(tokenAddress, address, amount),
+ this.increaseAllowanceAsync(tokenAddress, address, amount),
+ ]);
+ }
+ private async increaseBalanceAsync(
+ tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
+ await this.zeroEx.token.transferAsync(tokenAddress, this.coinbase, address, amount);
+ }
+ private async increaseAllowanceAsync(
+ tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
+ const oldMakerAllowance = await this.zeroEx.token.getProxyAllowanceAsync(tokenAddress, address);
+ const newMakerAllowance = oldMakerAllowance.plus(amount);
+ await this.zeroEx.token.setProxyAllowanceAsync(
+ tokenAddress, address, newMakerAllowance,
+ );
+ }
+}
diff --git a/packages/0x.js/test/utils/order_factory.ts b/packages/0x.js/test/utils/order_factory.ts
new file mode 100644
index 000000000..6086e09f7
--- /dev/null
+++ b/packages/0x.js/test/utils/order_factory.ts
@@ -0,0 +1,42 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {ZeroEx, SignedOrder} from '../../src';
+
+export const orderFactory = {
+ async createSignedOrderAsync(
+ zeroEx: ZeroEx,
+ maker: string,
+ taker: string,
+ makerFee: BigNumber,
+ takerFee: BigNumber,
+ makerTokenAmount: BigNumber,
+ makerTokenAddress: string,
+ takerTokenAmount: BigNumber,
+ takerTokenAddress: string,
+ exchangeContractAddress: string,
+ feeRecipient: string,
+ expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
+ const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite
+ expirationUnixTimestampSec = _.isUndefined(expirationUnixTimestampSec) ?
+ defaultExpirationUnixTimestampSec :
+ expirationUnixTimestampSec;
+ const order = {
+ maker,
+ taker,
+ makerFee,
+ takerFee,
+ makerTokenAmount,
+ takerTokenAmount,
+ makerTokenAddress,
+ takerTokenAddress,
+ salt: ZeroEx.generatePseudoRandomSalt(),
+ exchangeContractAddress,
+ feeRecipient,
+ expirationUnixTimestampSec,
+ };
+ const orderHash = ZeroEx.getOrderHashHex(order);
+ const ecSignature = await zeroEx.signOrderHashAsync(orderHash, maker);
+ const signedOrder: SignedOrder = _.assign(order, {ecSignature});
+ return signedOrder;
+ },
+};
diff --git a/packages/0x.js/test/utils/report_callback_errors.ts b/packages/0x.js/test/utils/report_callback_errors.ts
new file mode 100644
index 000000000..d471b2af2
--- /dev/null
+++ b/packages/0x.js/test/utils/report_callback_errors.ts
@@ -0,0 +1,14 @@
+import { DoneCallback } from '../../src/types';
+
+export const reportCallbackErrors = (done: DoneCallback) => {
+ return (f: (...args: any[]) => void) => {
+ const wrapped = (...args: any[]) => {
+ try {
+ f(...args);
+ } catch (err) {
+ done(err);
+ }
+ };
+ return wrapped;
+ };
+};
diff --git a/packages/0x.js/test/utils/rpc.ts b/packages/0x.js/test/utils/rpc.ts
new file mode 100644
index 000000000..299e72e79
--- /dev/null
+++ b/packages/0x.js/test/utils/rpc.ts
@@ -0,0 +1,57 @@
+import * as ethUtil from 'ethereumjs-util';
+import * as request from 'request-promise-native';
+import {constants} from './constants';
+
+export class RPC {
+ private host: string;
+ private port: number;
+ private id: number;
+ constructor() {
+ this.host = constants.RPC_HOST;
+ this.port = constants.RPC_PORT;
+ 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 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: `http://${this.host}:${this.port}`,
+ body: payload,
+ headers: {
+ 'content-type': 'application/json',
+ },
+ };
+ const bodyString = await request(opts);
+ const body = JSON.parse(bodyString);
+ return body.result;
+ }
+}
diff --git a/packages/0x.js/test/utils/token_utils.ts b/packages/0x.js/test/utils/token_utils.ts
new file mode 100644
index 000000000..51cb9411c
--- /dev/null
+++ b/packages/0x.js/test/utils/token_utils.ts
@@ -0,0 +1,24 @@
+import * as _ from 'lodash';
+import {Token, InternalZeroExError} from '../../src/types';
+
+const PROTOCOL_TOKEN_SYMBOL = 'ZRX';
+
+export class TokenUtils {
+ private tokens: Token[];
+ constructor(tokens: Token[]) {
+ this.tokens = tokens;
+ }
+ public getProtocolTokenOrThrow(): Token {
+ const zrxToken = _.find(this.tokens, {symbol: PROTOCOL_TOKEN_SYMBOL});
+ if (_.isUndefined(zrxToken)) {
+ throw new Error(InternalZeroExError.ZrxNotInTokenRegistry);
+ }
+ return zrxToken;
+ }
+ public getNonProtocolTokens(): Token[] {
+ const nonProtocolTokens = _.filter(this.tokens, token => {
+ return token.symbol !== PROTOCOL_TOKEN_SYMBOL;
+ });
+ return nonProtocolTokens;
+ }
+}
diff --git a/packages/0x.js/test/utils/web3_factory.ts b/packages/0x.js/test/utils/web3_factory.ts
new file mode 100644
index 000000000..b20070c74
--- /dev/null
+++ b/packages/0x.js/test/utils/web3_factory.ts
@@ -0,0 +1,31 @@
+// HACK: web3 injects XMLHttpRequest into the global scope and ProviderEngine checks XMLHttpRequest
+// to know whether it is running in a browser or node environment. We need it to be undefined since
+// we are not running in a browser env.
+// Filed issue: https://github.com/ethereum/web3.js/issues/844
+(global as any).XMLHttpRequest = undefined;
+import ProviderEngine = require('web3-provider-engine');
+import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
+import * as Web3 from 'web3';
+import {constants} from './constants';
+import {EmptyWalletSubProvider} from '../../src/subproviders/empty_wallet_subprovider';
+
+export const web3Factory = {
+ create(hasAddresses: boolean = true): Web3 {
+ const provider = this.getRpcProvider(hasAddresses);
+ const web3 = new Web3();
+ web3.setProvider(provider);
+ return web3;
+ },
+ getRpcProvider(hasAddresses: boolean = true): Web3.Provider {
+ const provider = new ProviderEngine();
+ const rpcUrl = `http://${constants.RPC_HOST}:${constants.RPC_PORT}`;
+ if (!hasAddresses) {
+ provider.addProvider(new EmptyWalletSubProvider());
+ }
+ provider.addProvider(new RpcSubprovider({
+ rpcUrl,
+ }));
+ provider.start();
+ return provider;
+ },
+};
diff --git a/packages/0x.js/test/web3_wrapper_test.ts b/packages/0x.js/test/web3_wrapper_test.ts
new file mode 100644
index 000000000..d1c2e8e89
--- /dev/null
+++ b/packages/0x.js/test/web3_wrapper_test.ts
@@ -0,0 +1,29 @@
+import * as chai from 'chai';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx} from '../src/';
+import {Web3Wrapper} from '../src/web3_wrapper';
+import {constants} from './utils/constants';
+
+chai.config.includeStack = true;
+const expect = chai.expect;
+
+describe('Web3Wrapper', () => {
+ const web3Provider = web3Factory.create().currentProvider;
+ describe('#getNetworkIdIfExistsAsync', () => {
+ it('caches network id requests', async () => {
+ const web3Wrapper = (new ZeroEx(web3Provider) as any)._web3Wrapper as Web3Wrapper;
+ expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
+ const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync();
+ expect((web3Wrapper as any).networkIdIfExists).to.be.equal(constants.TESTRPC_NETWORK_ID);
+ });
+ it('invalidates network id cache on setProvider call', async () => {
+ const web3Wrapper = (new ZeroEx(web3Provider) as any)._web3Wrapper as Web3Wrapper;
+ expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
+ const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync();
+ expect((web3Wrapper as any).networkIdIfExists).to.be.equal(constants.TESTRPC_NETWORK_ID);
+ const newProvider = web3Factory.create().currentProvider;
+ web3Wrapper.setProvider(newProvider);
+ expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
+ });
+ });
+});
diff --git a/packages/0x.js/tsconfig.json b/packages/0x.js/tsconfig.json
new file mode 100644
index 000000000..0684d4f1b
--- /dev/null
+++ b/packages/0x.js/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es5",
+ "lib": [ "es2015", "dom" ],
+ "outDir": "lib",
+ "sourceMap": true,
+ "declaration": true,
+ "noImplicitAny": true,
+ "experimentalDecorators": true,
+ "strictNullChecks": true
+ },
+ "include": [
+ "./src/**/*",
+ "./test/**/*",
+ "./node_modules/types-bn/index.d.ts",
+ "./node_modules/types-ethereumjs-util/index.d.ts",
+ "./node_modules/web3-typescript-typings/index.d.ts",
+ "./node_modules/chai-typescript-typings/index.d.ts",
+ "./node_modules/chai-as-promised-typescript-typings/index.d.ts"
+ ]
+}
diff --git a/packages/0x.js/webpack.config.js b/packages/0x.js/webpack.config.js
new file mode 100644
index 000000000..61a7e4196
--- /dev/null
+++ b/packages/0x.js/webpack.config.js
@@ -0,0 +1,56 @@
+/**
+ * This is to generate the umd bundle only
+ */
+const _ = require('lodash');
+const webpack = require('webpack');
+const path = require('path');
+const production = process.env.NODE_ENV === 'production';
+
+let entry = {
+ 'index': './src/index.ts',
+};
+if (production) {
+ entry = _.assign({}, entry, {'index.min': './src/index.ts'});
+}
+
+module.exports = {
+ entry,
+ output: {
+ path: path.resolve(__dirname, '_bundles'),
+ filename: '[name].js',
+ libraryTarget: 'umd',
+ library: 'ZeroEx',
+ umdNamedDefine: true,
+ },
+ resolve: {
+ extensions: ['.ts', '.js', '.json'],
+ },
+ devtool: 'source-map',
+ plugins: [
+ new webpack.optimize.UglifyJsPlugin({
+ minimize: true,
+ sourceMap: true,
+ include: /\.min\.js$/,
+ }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ use: [
+ {
+ loader: 'awesome-typescript-loader',
+ query: {
+ declaration: false,
+ },
+ },
+ ],
+ exclude: /node_modules/,
+ },
+ {
+ test: /\.json$/,
+ loader: 'json-loader',
+ },
+ ],
+ },
+};