aboutsummaryrefslogtreecommitdiffstats
path: root/packages/contract-wrappers
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2018-05-12 01:11:27 +0800
committerGitHub <noreply@github.com>2018-05-12 01:11:27 +0800
commit6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b (patch)
tree8d374bf8f4f70a40d75c8bf4b35fe9a169250909 /packages/contract-wrappers
parentf42f608f3f97a5244f09f17ae5ee184c0f775de3 (diff)
parentad8e12eeaebd40fdafac7cbef96121d4454909df (diff)
downloaddexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar
dexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.gz
dexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.bz2
dexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.lz
dexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.xz
dexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.zst
dexon-0x-contracts-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.zip
Merge pull request #579 from 0xProject/breakUp0xjs
Split 0x.js into: contract-wrappers & order-watcher
Diffstat (limited to 'packages/contract-wrappers')
-rw-r--r--packages/contract-wrappers/.npmignore11
-rw-r--r--packages/contract-wrappers/CHANGELOG.json11
-rw-r--r--packages/contract-wrappers/CHANGELOG.md6
-rw-r--r--packages/contract-wrappers/README.md91
-rw-r--r--packages/contract-wrappers/coverage/.gitkeep0
-rw-r--r--packages/contract-wrappers/package.json96
-rw-r--r--packages/contract-wrappers/src/artifacts.ts18
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/DummyToken.json22
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/EtherToken.json287
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/Exchange.json610
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/Token.json172
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json547
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json187
-rw-r--r--packages/contract-wrappers/src/compact_artifacts/ZRX.json20
-rw-r--r--packages/contract-wrappers/src/contract_wrappers.ts112
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts208
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts207
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts939
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts131
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts72
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts441
-rw-r--r--packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts28
-rw-r--r--packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts36
-rw-r--r--packages/contract-wrappers/src/globals.d.ts6
-rw-r--r--packages/contract-wrappers/src/index.ts68
-rw-r--r--packages/contract-wrappers/src/monorepo_scripts/postpublish.ts8
-rw-r--r--packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts5
-rw-r--r--packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts35
-rw-r--r--packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts29
-rw-r--r--packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts91
-rw-r--r--packages/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts75
-rw-r--r--packages/contract-wrappers/src/types.ts186
-rw-r--r--packages/contract-wrappers/src/utils/assert.ts31
-rw-r--r--packages/contract-wrappers/src/utils/constants.ts11
-rw-r--r--packages/contract-wrappers/src/utils/decorators.ts91
-rw-r--r--packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts115
-rw-r--r--packages/contract-wrappers/src/utils/filter_utils.ts95
-rw-r--r--packages/contract-wrappers/src/utils/order_validation_utils.ts198
-rw-r--r--packages/contract-wrappers/src/utils/utils.ts13
-rw-r--r--packages/contract-wrappers/test/artifacts_test.ts49
-rw-r--r--packages/contract-wrappers/test/ether_token_wrapper_test.ts417
-rw-r--r--packages/contract-wrappers/test/exchange_transfer_simulator_test.ts119
-rw-r--r--packages/contract-wrappers/test/exchange_wrapper_test.ts1228
-rw-r--r--packages/contract-wrappers/test/global_hooks.ts18
-rw-r--r--packages/contract-wrappers/test/order_validation_test.ts527
-rw-r--r--packages/contract-wrappers/test/subscription_test.ts95
-rw-r--r--packages/contract-wrappers/test/token_registry_wrapper_test.ts136
-rw-r--r--packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts35
-rw-r--r--packages/contract-wrappers/test/token_wrapper_test.ts605
-rw-r--r--packages/contract-wrappers/test/utils/chai_setup.ts13
-rw-r--r--packages/contract-wrappers/test/utils/constants.ts9
-rw-r--r--packages/contract-wrappers/test/utils/token_utils.ts33
-rw-r--r--packages/contract-wrappers/test/utils/web3_wrapper.ts9
-rw-r--r--packages/contract-wrappers/tsconfig.json7
-rw-r--r--packages/contract-wrappers/tslint.json3
55 files changed, 8612 insertions, 0 deletions
diff --git a/packages/contract-wrappers/.npmignore b/packages/contract-wrappers/.npmignore
new file mode 100644
index 000000000..6a3eb57bd
--- /dev/null
+++ b/packages/contract-wrappers/.npmignore
@@ -0,0 +1,11 @@
+.*
+tsconfig.json
+webpack.config.js
+yarn-error.log
+test/
+/src/
+/_bundles/
+/contract_templates/
+/generated_docs/
+/scripts/
+/lib/src/monorepo_scripts/
diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json
new file mode 100644
index 000000000..18ca4143e
--- /dev/null
+++ b/packages/contract-wrappers/CHANGELOG.json
@@ -0,0 +1,11 @@
+[
+ {
+ "version": "0.0.1",
+ "changes": [
+ {
+ "note": "Moved contractWrappers out of 0x.js",
+ "pr": 579
+ }
+ ]
+ }
+]
diff --git a/packages/contract-wrappers/CHANGELOG.md b/packages/contract-wrappers/CHANGELOG.md
new file mode 100644
index 000000000..5565d6210
--- /dev/null
+++ b/packages/contract-wrappers/CHANGELOG.md
@@ -0,0 +1,6 @@
+<!--
+This file is auto-generated using the monorepo-scripts package. Don't edit directly.
+Edit the package's CHANGELOG.json file only.
+-->
+
+CHANGELOG
diff --git a/packages/contract-wrappers/README.md b/packages/contract-wrappers/README.md
new file mode 100644
index 000000000..0fca4c762
--- /dev/null
+++ b/packages/contract-wrappers/README.md
@@ -0,0 +1,91 @@
+## @0xproject/contract-wrappers
+
+Smart TS wrappers for 0x smart contracts. The wrappers have simplified interfaces, perform client-side validation on transactions and throw helpful error messages.
+
+### Read the [Documentation](https://0xproject.com/docs/0x.js).
+
+## Installation
+
+**Install**
+
+```bash
+npm install @0xproject/contract-wrappers --save
+```
+
+**Import**
+
+```javascript
+import { ContractWrappers } from '@0xproject/contract-wrappers';
+```
+
+If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`:
+
+```json
+"compilerOptions": {
+ "typeRoots": ["node_modules/@0xproject/typescript-typings/types", "node_modules/@types"],
+}
+```
+
+## Contributing
+
+We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
+
+Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
+
+### Install dependencies
+
+If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
+
+```bash
+yarn config set workspaces-experimental true
+```
+
+Then install dependencies
+
+```bash
+yarn install
+```
+
+### Build
+
+If this is your **first** time building this package, you must first build **all** packages within the monorepo. This is because packages that depend on other packages located inside this monorepo are symlinked when run from **within** the monorepo. This allows you to make changes across multiple packages without first publishing dependent packages to NPM. To build all packages, run the following from the monorepo root directory:
+
+```bash
+yarn lerna:rebuild
+```
+
+Or continuously rebuild on change:
+
+```bash
+yarn dev
+```
+
+You can also build this specific package by running the following from within its directory:
+
+```bash
+yarn build
+```
+
+or continuously rebuild on change:
+
+```bash
+yarn build:watch
+```
+
+### Clean
+
+```bash
+yarn clean
+```
+
+### Lint
+
+```bash
+yarn lint
+```
+
+### Run Tests
+
+```bash
+yarn test
+```
diff --git a/packages/contract-wrappers/coverage/.gitkeep b/packages/contract-wrappers/coverage/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/contract-wrappers/coverage/.gitkeep
diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json
new file mode 100644
index 000000000..2b53f5c8d
--- /dev/null
+++ b/packages/contract-wrappers/package.json
@@ -0,0 +1,96 @@
+{
+ "name": "@0xproject/contract-wrappers",
+ "version": "0.0.1",
+ "description": "Smart TS wrappers for 0x smart contracts",
+ "keywords": [
+ "0xproject",
+ "ethereum",
+ "tokens",
+ "exchange"
+ ],
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.ts",
+ "scripts": {
+ "build:watch": "tsc -w",
+ "prebuild": "run-s clean generate_contract_wrappers",
+ "generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis 'src/compact_artifacts/@(Exchange|Token|TokenTransferProxy|EtherToken|TokenRegistry|DummyToken).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers && prettier --write 'src/contract_wrappers/generated/**.ts'",
+ "lint": "tslint --project .",
+ "test:circleci": "run-s test:coverage",
+ "test": "run-s clean build run_mocha",
+ "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
+ "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
+ "update_artifacts": "for i in ${npm_package_config_contracts}; do copyfiles -u 4 ../migrations/artifacts/1.0.0/$i.json test/artifacts; done;",
+ "clean": "shx rm -rf _bundles lib test_temp scripts test/artifacts src/contract_wrappers/generated",
+ "build": "tsc && yarn update_artifacts && copyfiles -u 2 './src/compact_artifacts/**/*.json' ./lib/src/compact_artifacts && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",
+ "run_mocha": "mocha lib/test/**/*_test.js lib/test/global_hooks.js --timeout 10000 --bail --exit",
+ "manual:postpublish": "yarn build; node ./scripts/postpublish.js"
+ },
+ "config": {
+ "compact_artifacts": "Exchange DummyToken ZRXToken Token EtherToken TokenTransferProxy TokenRegistry",
+ "contracts": "Exchange DummyToken ZRXToken Token WETH9 TokenTransferProxy MultiSigWallet MultiSigWalletWithTimeLock MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress MaliciousToken TokenRegistry Arbitrage EtherDelta AccountLevels",
+ "postpublish": {
+ "assets": [
+ "packages/contract-wrappers/_bundles/index.js",
+ "packages/contract-wrappers/_bundles/index.min.js"
+ ]
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/0xProject/0x-monorepo"
+ },
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "devDependencies": {
+ "@0xproject/sol-compiler": "^0.4.3",
+ "@0xproject/dev-utils": "^0.4.1",
+ "@0xproject/migrations": "^0.0.5",
+ "@0xproject/monorepo-scripts": "^0.1.19",
+ "@0xproject/subproviders": "^0.10.1",
+ "@0xproject/tslint-config": "^0.4.17",
+ "@types/lodash": "4.14.104",
+ "@types/mocha": "^2.2.42",
+ "@types/node": "^8.0.53",
+ "@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-bignumber": "^2.0.1",
+ "copyfiles": "^1.2.0",
+ "dirty-chai": "^2.0.1",
+ "mocha": "^4.0.1",
+ "npm-run-all": "^4.1.2",
+ "nyc": "^11.0.1",
+ "opn-cli": "^3.1.0",
+ "prettier": "^1.11.1",
+ "shx": "^0.2.2",
+ "sinon": "^4.0.0",
+ "source-map-support": "^0.5.0",
+ "tslint": "5.8.0",
+ "typescript": "2.7.1",
+ "web3-provider-engine": "^14.0.4"
+ },
+ "dependencies": {
+ "@0xproject/assert": "^0.2.9",
+ "@0xproject/base-contract": "^0.3.1",
+ "@0xproject/fill-scenarios": "^0.0.1",
+ "@0xproject/json-schemas": "^0.7.23",
+ "@0xproject/order-utils": "^0.0.4",
+ "@0xproject/types": "^0.6.3",
+ "@0xproject/typescript-typings": "^0.3.1",
+ "@0xproject/utils": "^0.6.1",
+ "@0xproject/web3-wrapper": "^0.6.3",
+ "ethereumjs-blockstream": "^2.0.6",
+ "ethereumjs-util": "^5.1.1",
+ "ethers": "^3.0.15",
+ "js-sha3": "^0.7.0",
+ "lodash": "^4.17.4",
+ "uuid": "^3.1.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/contract-wrappers/src/artifacts.ts b/packages/contract-wrappers/src/artifacts.ts
new file mode 100644
index 000000000..13587984c
--- /dev/null
+++ b/packages/contract-wrappers/src/artifacts.ts
@@ -0,0 +1,18 @@
+import { Artifact } from '@0xproject/types';
+
+import * as DummyToken from './compact_artifacts/DummyToken.json';
+import * as EtherToken from './compact_artifacts/EtherToken.json';
+import * as Exchange from './compact_artifacts/Exchange.json';
+import * as Token from './compact_artifacts/Token.json';
+import * as TokenRegistry from './compact_artifacts/TokenRegistry.json';
+import * as TokenTransferProxy from './compact_artifacts/TokenTransferProxy.json';
+import * as ZRX from './compact_artifacts/ZRX.json';
+export const artifacts = {
+ ZRX: (ZRX as any) as Artifact,
+ DummyToken: (DummyToken as any) as Artifact,
+ Token: (Token as any) as Artifact,
+ Exchange: (Exchange as any) as Artifact,
+ EtherToken: (EtherToken as any) as Artifact,
+ TokenRegistry: (TokenRegistry as any) as Artifact,
+ TokenTransferProxy: (TokenTransferProxy as any) as Artifact,
+};
diff --git a/packages/contract-wrappers/src/compact_artifacts/DummyToken.json b/packages/contract-wrappers/src/compact_artifacts/DummyToken.json
new file mode 100644
index 000000000..f64a8cd3d
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/DummyToken.json
@@ -0,0 +1,22 @@
+{
+ "contract_name": "DummyToken",
+ "abi": [
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_target",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "setBalance",
+ "outputs": [],
+ "payable": false,
+ "type": "function"
+ }
+ ]
+}
diff --git a/packages/contract-wrappers/src/compact_artifacts/EtherToken.json b/packages/contract-wrappers/src/compact_artifacts/EtherToken.json
new file mode 100644
index 000000000..26cca57cd
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/EtherToken.json
@@ -0,0 +1,287 @@
+{
+ "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"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Deposit",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Withdrawal",
+ "type": "event"
+ }
+ ],
+ "networks": {
+ "1": {
+ "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
+ },
+ "3": {
+ "address": "0xc00fd9820cd2898cc4c054b7bf142de637ad129a"
+ },
+ "4": {
+ "address": "0xc778417e063141139fce010982780140aa0cd5ab"
+ },
+ "42": {
+ "address": "0x653e49e301e508a13237c0ddc98ae7d4cd2667a1"
+ },
+ "50": {
+ "address": "0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c"
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/compact_artifacts/Exchange.json b/packages/contract-wrappers/src/compact_artifacts/Exchange.json
new file mode 100644
index 000000000..af8db7360
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/Exchange.json
@@ -0,0 +1,610 @@
+{
+ "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"
+ }
+ ],
+ "networks": {
+ "1": {
+ "address": "0x12459c951127e0c374ff9105dda097662a027093"
+ },
+ "3": {
+ "address": "0x479cc461fecd078f766ecc58533d6f69580cf3ac"
+ },
+ "4": {
+ "address": "0x1d16ef40fac01cec8adac2ac49427b9384192c05"
+ },
+ "42": {
+ "address": "0x90fe2af704b34e0224bf2299c838e04d4dcf1364"
+ },
+ "50": {
+ "address": "0x48bacb9266a570d521063ef5dd96e61686dbe788"
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/compact_artifacts/Token.json b/packages/contract-wrappers/src/compact_artifacts/Token.json
new file mode 100644
index 000000000..3b5a86ae0
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/Token.json
@@ -0,0 +1,172 @@
+{
+ "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"
+ }
+ ]
+}
diff --git a/packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json b/packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json
new file mode 100644
index 000000000..0f583628c
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json
@@ -0,0 +1,547 @@
+{
+ "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"
+ }
+ ],
+ "networks": {
+ "1": {
+ "address": "0x926a74c5c36adf004c87399e65f75628b0f98d2c"
+ },
+ "3": {
+ "address": "0x6b1a50f0bb5a7995444bd3877b22dc89c62843ed"
+ },
+ "4": {
+ "address": "0x4e9aad8184de8833365fea970cd9149372fdf1e6"
+ },
+ "42": {
+ "address": "0xf18e504561f4347bea557f3d4558f559dddbae7f"
+ },
+ "50": {
+ "address": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082"
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json b/packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json
new file mode 100644
index 000000000..8cf551ddb
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json
@@ -0,0 +1,187 @@
+{
+ "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"
+ }
+ ],
+ "networks": {
+ "1": {
+ "address": "0x8da0d80f5007ef1e431dd2127178d224e32c2ef4"
+ },
+ "3": {
+ "address": "0x4e9aad8184de8833365fea970cd9149372fdf1e6"
+ },
+ "4": {
+ "address": "0xa8e9fa8f91e5ae138c74648c9c304f1c75003a8d"
+ },
+ "42": {
+ "address": "0x087eed4bc1ee3de49befbd66c662b434b15d49d4"
+ },
+ "50": {
+ "address": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48"
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/compact_artifacts/ZRX.json b/packages/contract-wrappers/src/compact_artifacts/ZRX.json
new file mode 100644
index 000000000..e40b8f268
--- /dev/null
+++ b/packages/contract-wrappers/src/compact_artifacts/ZRX.json
@@ -0,0 +1,20 @@
+{
+ "contract_name": "ZRX",
+ "networks": {
+ "1": {
+ "address": "0xe41d2489571d322189246dafa5ebde1f4699f498"
+ },
+ "3": {
+ "address": "0xa8e9fa8f91e5ae138c74648c9c304f1c75003a8d"
+ },
+ "4": {
+ "address": "0x00f58d6d585f84b2d7267940cede30ce2fe6eae8"
+ },
+ "42": {
+ "address": "0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570"
+ },
+ "50": {
+ "address": "0x1d7022f5b17d2f8b695918fb48fa1089c9f85401"
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts
new file mode 100644
index 000000000..2ef050f27
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers.ts
@@ -0,0 +1,112 @@
+import { Provider } from '@0xproject/types';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from './artifacts';
+import { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper';
+import { ExchangeWrapper } from './contract_wrappers/exchange_wrapper';
+import { TokenRegistryWrapper } from './contract_wrappers/token_registry_wrapper';
+import { TokenTransferProxyWrapper } from './contract_wrappers/token_transfer_proxy_wrapper';
+import { TokenWrapper } from './contract_wrappers/token_wrapper';
+import { ContractWrappersConfigSchema } from './schemas/contract_wrappers_config_schema';
+import { contractWrappersPrivateNetworkConfigSchema } from './schemas/contract_wrappers_private_network_config_schema';
+import { contractWrappersPublicNetworkConfigSchema } from './schemas/contract_wrappers_public_network_config_schema';
+import { ContractWrappersConfig } from './types';
+import { assert } from './utils/assert';
+/**
+ * The ContractWrappers class contains smart contract wrappers helpful when building on 0x protocol.
+ */
+export class ContractWrappers {
+ /**
+ * 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;
+ private _web3Wrapper: Web3Wrapper;
+ /**
+ * Instantiates a new ContractWrappers instance.
+ * @param provider The 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 ContractWrappers class.
+ */
+ constructor(provider: Provider, config: ContractWrappersConfig) {
+ assert.isWeb3Provider('provider', provider);
+ assert.doesConformToSchema('config', config, ContractWrappersConfigSchema, [
+ contractWrappersPrivateNetworkConfigSchema,
+ contractWrappersPublicNetworkConfigSchema,
+ ]);
+ const artifactJSONs = _.values(artifacts);
+ const abiArrays = _.map(artifactJSONs, artifact => artifact.abi);
+ const txDefaults = {
+ gasPrice: config.gasPrice,
+ };
+ this._web3Wrapper = new Web3Wrapper(provider, txDefaults);
+ _.forEach(abiArrays, abi => {
+ this._web3Wrapper.abiDecoder.addABI(abi);
+ });
+ this.proxy = new TokenTransferProxyWrapper(
+ this._web3Wrapper,
+ config.networkId,
+ config.tokenTransferProxyContractAddress,
+ );
+ this.token = new TokenWrapper(this._web3Wrapper, config.networkId, this.proxy);
+ this.exchange = new ExchangeWrapper(
+ this._web3Wrapper,
+ config.networkId,
+ this.token,
+ config.exchangeContractAddress,
+ config.zrxContractAddress,
+ );
+ this.tokenRegistry = new TokenRegistryWrapper(
+ this._web3Wrapper,
+ config.networkId,
+ config.tokenRegistryContractAddress,
+ );
+ this.etherToken = new EtherTokenWrapper(this._web3Wrapper, config.networkId, this.token);
+ }
+ /**
+ * 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.
+ * @param networkId The id of the network your provider is connected to
+ */
+ public setProvider(provider: Provider, networkId: number): void {
+ this._web3Wrapper.setProvider(provider);
+ (this.exchange as any)._invalidateContractInstances();
+ (this.exchange as any)._setNetworkId(networkId);
+ (this.tokenRegistry as any)._invalidateContractInstance();
+ (this.tokenRegistry as any)._setNetworkId(networkId);
+ (this.token as any)._invalidateContractInstances();
+ (this.token as any)._setNetworkId(networkId);
+ (this.proxy as any)._invalidateContractInstance();
+ (this.proxy as any)._setNetworkId(networkId);
+ (this.etherToken as any)._invalidateContractInstance();
+ (this.etherToken as any)._setNetworkId(networkId);
+ }
+ /**
+ * Get the provider instance currently used by 0x.js
+ * @return Web3 provider instance
+ */
+ public getProvider(): Provider {
+ return this._web3Wrapper.getProvider();
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts
new file mode 100644
index 000000000..f255ced62
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts
@@ -0,0 +1,208 @@
+import {
+ Artifact,
+ BlockParamLiteral,
+ ContractAbi,
+ FilterObject,
+ LogEntry,
+ LogWithDecodedArgs,
+ RawLog,
+} from '@0xproject/types';
+import { AbiDecoder, intervalUtils } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import { Block, BlockAndLogStreamer } from 'ethereumjs-blockstream';
+import * as _ from 'lodash';
+
+import {
+ BlockRange,
+ ContractEventArgs,
+ ContractEvents,
+ ContractWrappersError,
+ EventCallback,
+ IndexedFilterValues,
+ InternalContractWrappersError,
+} from '../types';
+import { constants } from '../utils/constants';
+import { filterUtils } from '../utils/filter_utils';
+
+const CONTRACT_NAME_TO_NOT_FOUND_ERROR: {
+ [contractName: string]: ContractWrappersError;
+} = {
+ ZRX: ContractWrappersError.ZRXContractDoesNotExist,
+ EtherToken: ContractWrappersError.EtherTokenContractDoesNotExist,
+ Token: ContractWrappersError.TokenContractDoesNotExist,
+ TokenRegistry: ContractWrappersError.TokenRegistryContractDoesNotExist,
+ TokenTransferProxy: ContractWrappersError.TokenTransferProxyContractDoesNotExist,
+ Exchange: ContractWrappersError.ExchangeContractDoesNotExist,
+};
+
+export class ContractWrapper {
+ protected _web3Wrapper: Web3Wrapper;
+ protected _networkId: number;
+ private _blockAndLogStreamerIfExists?: BlockAndLogStreamer;
+ private _blockAndLogStreamIntervalIfExists?: NodeJS.Timer;
+ private _filters: { [filterToken: string]: FilterObject };
+ private _filterCallbacks: {
+ [filterToken: string]: EventCallback<ContractEventArgs>;
+ };
+ private _onLogAddedSubscriptionToken: string | undefined;
+ private _onLogRemovedSubscriptionToken: string | undefined;
+ constructor(web3Wrapper: Web3Wrapper, networkId: number) {
+ this._web3Wrapper = web3Wrapper;
+ this._networkId = networkId;
+ this._filters = {};
+ this._filterCallbacks = {};
+ this._blockAndLogStreamerIfExists = undefined;
+ this._onLogAddedSubscriptionToken = undefined;
+ this._onLogRemovedSubscriptionToken = undefined;
+ }
+ protected _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(ContractWrappersError.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: ContractAbi,
+ callback: EventCallback<ArgsType>,
+ ): string {
+ const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi);
+ if (_.isUndefined(this._blockAndLogStreamerIfExists)) {
+ this._startBlockAndLogStream();
+ }
+ const filterToken = filterUtils.generateUUID();
+ this._filters[filterToken] = filter;
+ this._filterCallbacks[filterToken] = callback as EventCallback<ContractEventArgs>;
+ return filterToken;
+ }
+ protected async _getLogsAsync<ArgsType extends ContractEventArgs>(
+ address: string,
+ eventName: ContractEvents,
+ blockRange: BlockRange,
+ indexFilterValues: IndexedFilterValues,
+ abi: ContractAbi,
+ ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi, blockRange);
+ const logs = await this._web3Wrapper.getLogsAsync(filter);
+ const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this));
+ return logsWithDecodedArguments;
+ }
+ protected _tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>(
+ log: LogEntry,
+ ): LogWithDecodedArgs<ArgsType> | RawLog {
+ if (_.isUndefined(this._web3Wrapper.abiDecoder)) {
+ throw new Error(InternalContractWrappersError.NoAbiDecoder);
+ }
+ const logWithDecodedArgs = this._web3Wrapper.abiDecoder.tryToDecodeLogOrNoop(log);
+ return logWithDecodedArgs;
+ }
+ protected async _getContractAbiAndAddressFromArtifactsAsync(
+ artifact: Artifact,
+ addressIfExists?: string,
+ ): Promise<[ContractAbi, string]> {
+ let contractAddress: string;
+ if (_.isUndefined(addressIfExists)) {
+ if (_.isUndefined(artifact.networks[this._networkId])) {
+ throw new Error(ContractWrappersError.ContractNotDeployedOnNetwork);
+ }
+ contractAddress = artifact.networks[this._networkId].address.toLowerCase();
+ } else {
+ contractAddress = addressIfExists;
+ }
+ const doesContractExist = await this._web3Wrapper.doesContractExistAtAddressAsync(contractAddress);
+ if (!doesContractExist) {
+ throw new Error(CONTRACT_NAME_TO_NOT_FOUND_ERROR[artifact.contract_name]);
+ }
+ const abiAndAddress: [ContractAbi, string] = [artifact.abi, contractAddress];
+ return abiAndAddress;
+ }
+ protected _getContractAddress(artifact: Artifact, addressIfExists?: string): string {
+ if (_.isUndefined(addressIfExists)) {
+ const contractAddress = artifact.networks[this._networkId].address;
+ if (_.isUndefined(contractAddress)) {
+ throw new Error(ContractWrappersError.ExchangeContractDoesNotExist);
+ }
+ return contractAddress;
+ } else {
+ return addressIfExists;
+ }
+ }
+ private _onLogStateChanged<ArgsType extends ContractEventArgs>(isRemoved: boolean, log: LogEntry): void {
+ _.forEach(this._filters, (filter: FilterObject, filterToken: string) => {
+ if (filterUtils.matchesFilter(log, filter)) {
+ const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>;
+ const logEvent = {
+ log: decodedLog,
+ isRemoved,
+ };
+ this._filterCallbacks[filterToken](null, logEvent);
+ }
+ });
+ }
+ private _startBlockAndLogStream(): void {
+ if (!_.isUndefined(this._blockAndLogStreamerIfExists)) {
+ throw new Error(ContractWrappersError.SubscriptionAlreadyPresent);
+ }
+ this._blockAndLogStreamerIfExists = new BlockAndLogStreamer(
+ this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper),
+ this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper),
+ );
+ const catchAllLogFilter = {};
+ this._blockAndLogStreamerIfExists.addLogFilter(catchAllLogFilter);
+ this._blockAndLogStreamIntervalIfExists = intervalUtils.setAsyncExcludingInterval(
+ this._reconcileBlockAsync.bind(this),
+ constants.DEFAULT_BLOCK_POLLING_INTERVAL,
+ this._onReconcileBlockError.bind(this),
+ );
+ let isRemoved = false;
+ this._onLogAddedSubscriptionToken = this._blockAndLogStreamerIfExists.subscribeToOnLogAdded(
+ this._onLogStateChanged.bind(this, isRemoved),
+ );
+ isRemoved = true;
+ this._onLogRemovedSubscriptionToken = this._blockAndLogStreamerIfExists.subscribeToOnLogRemoved(
+ this._onLogStateChanged.bind(this, isRemoved),
+ );
+ }
+ private _onReconcileBlockError(err: Error): void {
+ const filterTokens = _.keys(this._filterCallbacks);
+ _.each(filterTokens, filterToken => {
+ this._unsubscribe(filterToken, err);
+ });
+ }
+ private _setNetworkId(networkId: number): void {
+ this._networkId = networkId;
+ }
+ private _stopBlockAndLogStream(): void {
+ if (_.isUndefined(this._blockAndLogStreamerIfExists)) {
+ throw new Error(ContractWrappersError.SubscriptionNotFound);
+ }
+ this._blockAndLogStreamerIfExists.unsubscribeFromOnLogAdded(this._onLogAddedSubscriptionToken as string);
+ this._blockAndLogStreamerIfExists.unsubscribeFromOnLogRemoved(this._onLogRemovedSubscriptionToken as string);
+ intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamIntervalIfExists as NodeJS.Timer);
+ delete this._blockAndLogStreamerIfExists;
+ }
+ private async _reconcileBlockAsync(): Promise<void> {
+ 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._blockAndLogStreamerIfExists)) {
+ // If we clear the interval while fetching the block - this._blockAndLogStreamer will be undefined
+ await this._blockAndLogStreamerIfExists.reconcileNewBlock((latestBlock as any) as Block);
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts
new file mode 100644
index 000000000..1bd65270b
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts
@@ -0,0 +1,207 @@
+import { schemas } from '@0xproject/json-schemas';
+import { LogWithDecodedArgs } from '@0xproject/types';
+import { AbiDecoder, BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import { BlockRange, ContractWrappersError, EventCallback, IndexedFilterValues, TransactionOpts } from '../types';
+import { assert } from '../utils/assert';
+
+import { ContractWrapper } from './contract_wrapper';
+import { EtherTokenContract, EtherTokenContractEventArgs, EtherTokenEvents } from './generated/ether_token';
+import { TokenWrapper } from './token_wrapper';
+
+/**
+ * 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 _etherTokenContractsByAddress: {
+ [address: string]: EtherTokenContract;
+ } = {};
+ private _tokenWrapper: TokenWrapper;
+ constructor(web3Wrapper: Web3Wrapper, networkId: number, tokenWrapper: TokenWrapper) {
+ super(web3Wrapper, networkId);
+ this._tokenWrapper = tokenWrapper;
+ }
+ /**
+ * 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 etherTokenAddress EtherToken address you wish to deposit into.
+ * @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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async depositAsync(
+ etherTokenAddress: string,
+ amountInWei: BigNumber,
+ depositor: string,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('etherTokenAddress', etherTokenAddress);
+ assert.isValidBaseUnitAmount('amountInWei', amountInWei);
+ await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper);
+ const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase();
+ const normalizedDepositorAddress = depositor.toLowerCase();
+
+ const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(normalizedDepositorAddress);
+ assert.assert(ethBalanceInWei.gte(amountInWei), ContractWrappersError.InsufficientEthBalanceForDeposit);
+
+ const wethContract = await this._getEtherTokenContractAsync(normalizedEtherTokenAddress);
+ const txHash = await wethContract.deposit.sendTransactionAsync({
+ from: normalizedDepositorAddress,
+ value: amountInWei,
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ });
+ 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 etherTokenAddress EtherToken address you wish to withdraw from.
+ * @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 withdrawal.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async withdrawAsync(
+ etherTokenAddress: string,
+ amountInWei: BigNumber,
+ withdrawer: string,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isValidBaseUnitAmount('amountInWei', amountInWei);
+ assert.isETHAddressHex('etherTokenAddress', etherTokenAddress);
+ await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper);
+ const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase();
+ const normalizedWithdrawerAddress = withdrawer.toLowerCase();
+
+ const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync(
+ normalizedEtherTokenAddress,
+ normalizedWithdrawerAddress,
+ );
+ assert.assert(
+ WETHBalanceInBaseUnits.gte(amountInWei),
+ ContractWrappersError.InsufficientWEthBalanceForWithdrawal,
+ );
+
+ const wethContract = await this._getEtherTokenContractAsync(normalizedEtherTokenAddress);
+ const txHash = await wethContract.withdraw.sendTransactionAsync(amountInWei, {
+ from: normalizedWithdrawerAddress,
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ });
+ return txHash;
+ }
+ /**
+ * Gets historical logs without creating a subscription
+ * @param etherTokenAddress An address of the ether token that emitted the logs.
+ * @param eventName The ether token contract event you would like to subscribe to.
+ * @param blockRange Block range to get logs from.
+ * @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 `{_owner: aUserAddressHex}`
+ * @return Array of logs that match the parameters
+ */
+ public async getLogsAsync<ArgsType extends EtherTokenContractEventArgs>(
+ etherTokenAddress: string,
+ eventName: EtherTokenEvents,
+ blockRange: BlockRange,
+ indexFilterValues: IndexedFilterValues,
+ ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ assert.isETHAddressHex('etherTokenAddress', etherTokenAddress);
+ const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase();
+ assert.doesBelongToStringEnum('eventName', eventName, EtherTokenEvents);
+ assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ const logs = await this._getLogsAsync<ArgsType>(
+ normalizedEtherTokenAddress,
+ eventName,
+ blockRange,
+ indexFilterValues,
+ artifacts.EtherToken.abi,
+ );
+ return logs;
+ }
+ /**
+ * Subscribe to an event type emitted by the Token contract.
+ * @param etherTokenAddress The hex encoded address where the ether token is deployed.
+ * @param eventName The ether 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 `{_owner: aUserAddressHex}`
+ * @param callback Callback that gets called when a log is added/removed
+ * @return Subscription token used later to unsubscribe
+ */
+ public subscribe<ArgsType extends EtherTokenContractEventArgs>(
+ etherTokenAddress: string,
+ eventName: EtherTokenEvents,
+ indexFilterValues: IndexedFilterValues,
+ callback: EventCallback<ArgsType>,
+ ): string {
+ assert.isETHAddressHex('etherTokenAddress', etherTokenAddress);
+ const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase();
+ assert.doesBelongToStringEnum('eventName', eventName, EtherTokenEvents);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ assert.isFunction('callback', callback);
+ const subscriptionToken = this._subscribe<ArgsType>(
+ normalizedEtherTokenAddress,
+ eventName,
+ indexFilterValues,
+ artifacts.EtherToken.abi,
+ callback,
+ );
+ return subscriptionToken;
+ }
+ /**
+ * Cancel a subscription
+ * @param subscriptionToken Subscription token returned by `subscribe()`
+ */
+ public unsubscribe(subscriptionToken: string): void {
+ this._unsubscribe(subscriptionToken);
+ }
+ /**
+ * Cancels all existing subscriptions
+ */
+ public unsubscribeAll(): void {
+ super._unsubscribeAll();
+ }
+ /**
+ * Retrieves the Ethereum address of the EtherToken contract deployed on the network
+ * that the user-passed web3 provider is connected to. If it's not Kovan, Ropsten, Rinkeby, Mainnet or TestRPC
+ * (networkId: 50), it will return undefined (e.g a private network).
+ * @returns The Ethereum address of the EtherToken contract or undefined.
+ */
+ public getContractAddressIfExists(): string | undefined {
+ const networkSpecificArtifact = artifacts.EtherToken.networks[this._networkId];
+ const contractAddressIfExists = _.isUndefined(networkSpecificArtifact)
+ ? undefined
+ : networkSpecificArtifact.address;
+ return contractAddressIfExists;
+ }
+ private _invalidateContractInstance(): void {
+ this.unsubscribeAll();
+ this._etherTokenContractsByAddress = {};
+ }
+ private async _getEtherTokenContractAsync(etherTokenAddress: string): Promise<EtherTokenContract> {
+ let etherTokenContract = this._etherTokenContractsByAddress[etherTokenAddress];
+ if (!_.isUndefined(etherTokenContract)) {
+ return etherTokenContract;
+ }
+ const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.EtherToken,
+ etherTokenAddress,
+ );
+ const contractInstance = new EtherTokenContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ etherTokenContract = contractInstance;
+ this._etherTokenContractsByAddress[etherTokenAddress] = etherTokenContract;
+ return etherTokenContract;
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts
new file mode 100644
index 000000000..fc177dd42
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts
@@ -0,0 +1,939 @@
+import { schemas } from '@0xproject/json-schemas';
+import { formatters, getOrderHashHex, OrderStateUtils } from '@0xproject/order-utils';
+import {
+ BlockParamLiteral,
+ DecodedLogArgs,
+ ECSignature,
+ ExchangeContractErrs,
+ LogEntry,
+ LogWithDecodedArgs,
+ Order,
+ OrderAddresses,
+ OrderState,
+ OrderValues,
+ SignedOrder,
+} from '@0xproject/types';
+import { AbiDecoder, BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import { SimpleBalanceAndProxyAllowanceFetcher } from '../fetchers/simple_balance_and_proxy_allowance_fetcher';
+import { SimpleOrderFilledCancelledFetcher } from '../fetchers/simple_order_filled_cancelled_fetcher';
+import {
+ BlockRange,
+ EventCallback,
+ ExchangeContractErrCodes,
+ IndexedFilterValues,
+ MethodOpts,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ OrderTransactionOpts,
+ ValidateOrderFillableOpts,
+} from '../types';
+import { assert } from '../utils/assert';
+import { decorators } from '../utils/decorators';
+import { ExchangeTransferSimulator } from '../utils/exchange_transfer_simulator';
+import { OrderValidationUtils } from '../utils/order_validation_utils';
+import { utils } from '../utils/utils';
+
+import { ContractWrapper } from './contract_wrapper';
+import {
+ ExchangeContract,
+ ExchangeContractEventArgs,
+ ExchangeEvents,
+ LogErrorContractEventArgs,
+} from './generated/exchange';
+import { TokenWrapper } from './token_wrapper';
+const SHOULD_VALIDATE_BY_DEFAULT = true;
+
+interface ExchangeContractErrCodesToMsgs {
+ [exchangeContractErrCodes: number]: string;
+}
+
+/**
+ * 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: ExchangeContractErrCodesToMsgs = {
+ [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 _zrxContractAddressIfExists?: string;
+ constructor(
+ web3Wrapper: Web3Wrapper,
+ networkId: number,
+ tokenWrapper: TokenWrapper,
+ contractAddressIfExists?: string,
+ zrxContractAddressIfExists?: string,
+ ) {
+ super(web3Wrapper, networkId);
+ this._tokenWrapper = tokenWrapper;
+ this._orderValidationUtils = new OrderValidationUtils(this);
+ this._contractAddressIfExists = contractAddressIfExists;
+ this._zrxContractAddressIfExists = zrxContractAddressIfExists;
+ }
+ /**
+ * 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 cancelled.
+ */
+ 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;
+ const txData = {};
+ let unavailableTakerTokenAmount = await exchangeContract.getUnavailableTakerTokenAmount.callAsync(
+ orderHash,
+ txData,
+ 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;
+ const txData = {};
+ let fillAmountInBaseUnits = await exchangeContract.filled.callAsync(orderHash, txData, 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 getCancelledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
+ assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
+
+ const exchangeContract = await this._getExchangeContractAsync();
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ let cancelledAmountInBaseUnits = await exchangeContract.cancelled.callAsync(orderHash, txData, 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 Provider
+ * passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.asyncZeroExErrorHandler
+ 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 normalizedTakerAddress = takerAddress.toLowerCase();
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount,
+ normalizedTakerAddress,
+ zrxTokenAddress,
+ );
+ }
+
+ const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(signedOrder);
+
+ const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: normalizedTakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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 Provider
+ * passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.asyncZeroExErrorHandler
+ 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 normalizedTakerAddress = takerAddress.toLowerCase();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ let filledTakerTokenAmount = new BigNumber(0);
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ for (const signedOrder of signedOrders) {
+ const singleFilledTakerTokenAmount = await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount.minus(filledTakerTokenAmount),
+ normalizedTakerAddress,
+ zrxTokenAddress,
+ );
+ filledTakerTokenAmount = filledTakerTokenAmount.plus(singleFilledTakerTokenAmount);
+ if (filledTakerTokenAmount.eq(fillTakerTokenAmount)) {
+ break;
+ }
+ }
+ }
+
+ if (_.isEmpty(signedOrders)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+
+ const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => {
+ return [
+ ...formatters.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 txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync(
+ orderAddressesArray,
+ orderValuesArray,
+ fillTakerTokenAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ vArray,
+ rArray,
+ sArray,
+ {
+ from: normalizedTakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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
+ * Provider passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.asyncZeroExErrorHandler
+ 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 normalizedTakerAddress = takerAddress.toLowerCase();
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ for (const orderFillRequest of orderFillRequests) {
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ orderFillRequest.signedOrder,
+ orderFillRequest.takerTokenFillAmount,
+ normalizedTakerAddress,
+ zrxTokenAddress,
+ );
+ }
+ }
+ if (_.isEmpty(orderFillRequests)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+
+ const orderAddressesValuesAmountsAndSignatureArray = _.map(orderFillRequests, orderFillRequest => {
+ return [
+ ...formatters.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 txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync(
+ orderAddressesArray,
+ orderValuesArray,
+ fillTakerTokenAmounts,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ vArray,
+ rArray,
+ sArray,
+ {
+ from: normalizedTakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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 Provider passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.asyncZeroExErrorHandler
+ 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 normalizedTakerAddress = takerAddress.toLowerCase();
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount,
+ normalizedTakerAddress,
+ zrxTokenAddress,
+ );
+ }
+
+ const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(signedOrder);
+ const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmount,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: normalizedTakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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 Provider passed to 0x.js.
+ * @param orderTransactionOpts Optional arguments this method accepts.
+ * @return Transaction hash.
+ */
+ @decorators.asyncZeroExErrorHandler
+ 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);
+ const normalizedTakerAddress = takerAddress.toLowerCase();
+ if (_.isEmpty(orderFillRequests)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ for (const orderFillRequest of orderFillRequests) {
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ orderFillRequest.signedOrder,
+ orderFillRequest.takerTokenFillAmount,
+ normalizedTakerAddress,
+ zrxTokenAddress,
+ );
+ }
+ }
+
+ const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillRequests, request => {
+ return [
+ ...formatters.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 txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ fillTakerTokenAmounts,
+ vParams,
+ rParams,
+ sParams,
+ {
+ from: normalizedTakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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.asyncZeroExErrorHandler
+ 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 normalizedMakerAddress = order.maker.toLowerCase();
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ const orderHash = getOrderHashHex(order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ OrderValidationUtils.validateCancelOrderThrowIfInvalid(
+ order,
+ cancelTakerTokenAmount,
+ unavailableTakerTokenAmount,
+ );
+ }
+
+ const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(order);
+ const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ cancelTakerTokenAmount,
+ {
+ from: normalizedMakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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.asyncZeroExErrorHandler
+ 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 normalizedMakerAddress = maker.toLowerCase();
+
+ const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate)
+ ? SHOULD_VALIDATE_BY_DEFAULT
+ : orderTransactionOpts.shouldValidate;
+ if (shouldValidate) {
+ for (const orderCancellationRequest of orderCancellationRequests) {
+ const orderHash = getOrderHashHex(orderCancellationRequest.order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ OrderValidationUtils.validateCancelOrderThrowIfInvalid(
+ orderCancellationRequest.order,
+ orderCancellationRequest.takerTokenCancelAmount,
+ unavailableTakerTokenAmount,
+ );
+ }
+ }
+ if (_.isEmpty(orderCancellationRequests)) {
+ throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ }
+ const exchangeInstance = await this._getExchangeContractAsync();
+ const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => {
+ return [
+ ...formatters.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 txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync(
+ orderAddresses,
+ orderValues,
+ cancelTakerTokenAmounts,
+ {
+ from: normalizedMakerAddress,
+ gas: orderTransactionOpts.gasLimit,
+ gasPrice: orderTransactionOpts.gasPrice,
+ },
+ );
+ 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 subscribe<ArgsType extends ExchangeContractEventArgs>(
+ eventName: ExchangeEvents,
+ indexFilterValues: IndexedFilterValues,
+ callback: EventCallback<ArgsType>,
+ ): string {
+ assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ assert.isFunction('callback', callback);
+ const exchangeContractAddress = this.getContractAddress();
+ const subscriptionToken = this._subscribe<ArgsType>(
+ exchangeContractAddress,
+ eventName,
+ indexFilterValues,
+ artifacts.Exchange.abi,
+ callback,
+ );
+ return subscriptionToken;
+ }
+ /**
+ * Cancel a subscription
+ * @param subscriptionToken Subscription token returned by `subscribe()`
+ */
+ public unsubscribe(subscriptionToken: string): void {
+ this._unsubscribe(subscriptionToken);
+ }
+ /**
+ * Cancels all existing subscriptions
+ */
+ public unsubscribeAll(): void {
+ super._unsubscribeAll();
+ }
+ /**
+ * Gets historical logs without creating a subscription
+ * @param eventName The exchange contract event you would like to subscribe to.
+ * @param blockRange Block range to get logs from.
+ * @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,
+ blockRange: BlockRange,
+ indexFilterValues: IndexedFilterValues,
+ ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
+ assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ const exchangeContractAddress = this.getContractAddress();
+ const logs = await this._getLogsAsync<ArgsType>(
+ exchangeContractAddress,
+ eventName,
+ blockRange,
+ indexFilterValues,
+ artifacts.Exchange.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 getContractAddress(): string {
+ const contractAddress = this._getContractAddress(artifacts.Exchange, this._contractAddressIfExists);
+ return contractAddress;
+ }
+ /**
+ * 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 = this.getZRXTokenAddress();
+ const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined;
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ 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 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 normalizedTakerAddress = takerAddress.toLowerCase();
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount,
+ normalizedTakerAddress,
+ 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 = getOrderHashHex(order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ OrderValidationUtils.validateCancelOrderThrowIfInvalid(
+ 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 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 normalizedTakerAddress = takerAddress.toLowerCase();
+ const zrxTokenAddress = this.getZRXTokenAddress();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest);
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount,
+ normalizedTakerAddress,
+ 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 emitted by Exchange contract on transaction failure.
+ * @param logs Transaction logs as returned by `zeroEx.awaitTransactionMinedAsync`
+ */
+ public throwLogErrorsAsErrors(logs: Array<LogWithDecodedArgs<DecodedLogArgs> | LogEntry>): void {
+ const errLog = _.find(logs, {
+ event: ExchangeEvents.LogError,
+ });
+ if (!_.isUndefined(errLog)) {
+ const logArgs = (errLog as LogWithDecodedArgs<LogErrorContractEventArgs>).args;
+ const errCode = logArgs.errorId;
+ const errMessage = this._exchangeContractErrCodesToMsg[errCode];
+ throw new Error(errMessage);
+ }
+ }
+ /**
+ * Gets the latest OrderState of a signedOrder
+ * @param signedOrder The signedOrder
+ * @param stateLayer Optional, desired blockchain state layer (defaults to latest).
+ * @return OrderState of the signedOrder
+ */
+ public async getOrderStateAsync(
+ signedOrder: SignedOrder,
+ stateLayer: BlockParamLiteral = BlockParamLiteral.Latest,
+ ): Promise<OrderState> {
+ const simpleBalanceAndProxyAllowanceFetcher = new SimpleBalanceAndProxyAllowanceFetcher(
+ this._tokenWrapper,
+ stateLayer,
+ );
+ const simpleOrderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(this, stateLayer);
+ const orderStateUtils = new OrderStateUtils(
+ simpleBalanceAndProxyAllowanceFetcher,
+ simpleOrderFilledCancelledFetcher,
+ );
+ const orderState = orderStateUtils.getOrderStateAsync(signedOrder);
+ return orderState;
+ }
+ /**
+ * Returns the ZRX token address used by the exchange contract.
+ * @return Address of ZRX token
+ */
+ public getZRXTokenAddress(): string {
+ const contractAddress = this._getContractAddress(artifacts.ZRX, this._zrxContractAddressIfExists);
+ return contractAddress;
+ }
+ private _invalidateContractInstances(): 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 normalizedSignerAddress = signerAddressHex.toLowerCase();
+
+ const exchangeInstance = await this._getExchangeContractAsync();
+
+ const isValidSignature = await exchangeInstance.isValidSignature.callAsync(
+ normalizedSignerAddress,
+ 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] = formatters.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 [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.Exchange,
+ this._contractAddressIfExists,
+ );
+ const contractInstance = new ExchangeContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ this._exchangeContractIfExists = contractInstance;
+ return this._exchangeContractIfExists;
+ }
+} // tslint:disable:max-file-line-count
diff --git a/packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts
new file mode 100644
index 000000000..e75973658
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts
@@ -0,0 +1,131 @@
+import { Token } from '@0xproject/types';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import { TokenMetadata } from '../types';
+import { assert } from '../utils/assert';
+import { constants } from '../utils/constants';
+
+import { ContractWrapper } from './contract_wrapper';
+import { TokenRegistryContract } from './generated/token_registry';
+
+/**
+ * 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;
+ private static _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],
+ };
+ return token;
+ }
+ constructor(web3Wrapper: Web3Wrapper, networkId: number, contractAddressIfExists?: string) {
+ super(web3Wrapper, networkId);
+ 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 addresses = await this.getTokenAddressesAsync();
+ const tokenPromises: Array<Promise<Token | undefined>> = _.map(addresses, async (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();
+ const lowerCaseAddresses = _.map(addresses, address => address.toLowerCase());
+ return lowerCaseAddresses;
+ }
+ /**
+ * 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 normalizedAddress = address.toLowerCase();
+
+ const tokenRegistryContract = await this._getTokenRegistryContractAsync();
+ const metadata = await tokenRegistryContract.getTokenMetaData.callAsync(normalizedAddress);
+ const token = TokenRegistryWrapper._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 = TokenRegistryWrapper._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 = TokenRegistryWrapper._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 getContractAddress(): string {
+ const contractAddress = this._getContractAddress(artifacts.TokenRegistry, this._contractAddressIfExists);
+ return contractAddress;
+ }
+ private _invalidateContractInstance(): void {
+ delete this._tokenRegistryContractIfExists;
+ }
+ private async _getTokenRegistryContractAsync(): Promise<TokenRegistryContract> {
+ if (!_.isUndefined(this._tokenRegistryContractIfExists)) {
+ return this._tokenRegistryContractIfExists;
+ }
+ const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.TokenRegistry,
+ this._contractAddressIfExists,
+ );
+ const contractInstance = new TokenRegistryContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ this._tokenRegistryContractIfExists = contractInstance;
+ return this._tokenRegistryContractIfExists;
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts
new file mode 100644
index 000000000..02a2e19d0
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts
@@ -0,0 +1,72 @@
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import { assert } from '../utils/assert';
+
+import { ContractWrapper } from './contract_wrapper';
+import { TokenTransferProxyContract } from './generated/token_transfer_proxy';
+
+/**
+ * This class includes the functionality related to interacting with the TokenTransferProxy contract.
+ */
+export class TokenTransferProxyWrapper extends ContractWrapper {
+ private _tokenTransferProxyContractIfExists?: TokenTransferProxyContract;
+ private _contractAddressIfExists?: string;
+ constructor(web3Wrapper: Web3Wrapper, networkId: number, contractAddressIfExists?: string) {
+ super(web3Wrapper, networkId);
+ this._contractAddressIfExists = contractAddressIfExists;
+ }
+ /**
+ * 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> {
+ assert.isETHAddressHex('exchangeContractAddress', exchangeContractAddress);
+ const normalizedExchangeContractAddress = exchangeContractAddress.toLowerCase();
+ const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync();
+ const isAuthorized = await tokenTransferProxyContractInstance.authorized.callAsync(
+ normalizedExchangeContractAddress,
+ );
+ 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 getContractAddress(): string {
+ const contractAddress = this._getContractAddress(artifacts.TokenTransferProxy, this._contractAddressIfExists);
+ return contractAddress;
+ }
+ private _invalidateContractInstance(): void {
+ delete this._tokenTransferProxyContractIfExists;
+ }
+ private async _getTokenTransferProxyContractAsync(): Promise<TokenTransferProxyContract> {
+ if (!_.isUndefined(this._tokenTransferProxyContractIfExists)) {
+ return this._tokenTransferProxyContractIfExists;
+ }
+ const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.TokenTransferProxy,
+ this._contractAddressIfExists,
+ );
+ const contractInstance = new TokenTransferProxyContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ this._tokenTransferProxyContractIfExists = contractInstance;
+ return this._tokenTransferProxyContractIfExists;
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts
new file mode 100644
index 000000000..844318c79
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts
@@ -0,0 +1,441 @@
+import { schemas } from '@0xproject/json-schemas';
+import { LogWithDecodedArgs } from '@0xproject/types';
+import { AbiDecoder, BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import {
+ BlockRange,
+ ContractWrappersError,
+ EventCallback,
+ IndexedFilterValues,
+ MethodOpts,
+ TransactionOpts,
+} from '../types';
+import { assert } from '../utils/assert';
+import { constants } from '../utils/constants';
+
+import { ContractWrapper } from './contract_wrapper';
+import { TokenContract, TokenContractEventArgs, TokenEvents } from './generated/token';
+import { TokenTransferProxyWrapper } from './token_transfer_proxy_wrapper';
+
+/**
+ * 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 _tokenTransferProxyWrapper: TokenTransferProxyWrapper;
+ constructor(web3Wrapper: Web3Wrapper, networkId: number, tokenTransferProxyWrapper: TokenTransferProxyWrapper) {
+ super(web3Wrapper, networkId);
+ this._tokenContractsByAddress = {};
+ this._tokenTransferProxyWrapper = tokenTransferProxyWrapper;
+ }
+ /**
+ * 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 normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ let balance = await tokenContract.balanceOf.callAsync(normalizedOwnerAddress, txData, 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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setAllowanceAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ spenderAddress: string,
+ amountInBaseUnits: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('spenderAddress', spenderAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedSpenderAddress = spenderAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const txHash = await tokenContract.approve.sendTransactionAsync(normalizedSpenderAddress, amountInBaseUnits, {
+ from: normalizedOwnerAddress,
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ });
+ 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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setUnlimitedAllowanceAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ spenderAddress: string,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('spenderAddress', spenderAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ const normalizedSpenderAddress = spenderAddress.toLowerCase();
+ const txHash = await this.setAllowanceAsync(
+ normalizedTokenAddress,
+ normalizedOwnerAddress,
+ normalizedSpenderAddress,
+ this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ txOpts,
+ );
+ 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);
+ assert.isETHAddressHex('spenderAddress', spenderAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ const normalizedSpenderAddress = spenderAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ let allowanceInBaseUnits = await tokenContract.allowance.callAsync(
+ normalizedOwnerAddress,
+ normalizedSpenderAddress,
+ txData,
+ 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 normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+
+ const proxyAddress = this._tokenTransferProxyWrapper.getContractAddress();
+ const allowanceInBaseUnits = await this.getAllowanceAsync(
+ normalizedTokenAddress,
+ normalizedOwnerAddress,
+ 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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setProxyAllowanceAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ amountInBaseUnits: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const proxyAddress = this._tokenTransferProxyWrapper.getContractAddress();
+ const txHash = await this.setAllowanceAsync(
+ normalizedTokenAddress,
+ normalizedOwnerAddress,
+ proxyAddress,
+ amountInBaseUnits,
+ txOpts,
+ );
+ 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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setUnlimitedProxyAllowanceAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ const txHash = await this.setProxyAllowanceAsync(
+ normalizedTokenAddress,
+ normalizedOwnerAddress,
+ this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ txOpts,
+ );
+ 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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async transferAsync(
+ tokenAddress: string,
+ fromAddress: string,
+ toAddress: string,
+ amountInBaseUnits: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('toAddress', toAddress);
+ await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedFromAddress = fromAddress.toLowerCase();
+ const normalizedToAddress = toAddress.toLowerCase();
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+
+ const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress);
+ if (fromAddressBalance.lessThan(amountInBaseUnits)) {
+ throw new Error(ContractWrappersError.InsufficientBalanceForTransfer);
+ }
+
+ const txHash = await tokenContract.transfer.sendTransactionAsync(normalizedToAddress, amountInBaseUnits, {
+ from: normalizedFromAddress,
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ });
+ 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.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async transferFromAsync(
+ tokenAddress: string,
+ fromAddress: string,
+ toAddress: string,
+ senderAddress: string,
+ amountInBaseUnits: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('toAddress', toAddress);
+ assert.isETHAddressHex('fromAddress', fromAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper);
+ const normalizedToAddress = toAddress.toLowerCase();
+ const normalizedFromAddress = fromAddress.toLowerCase();
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedSenderAddress = senderAddress.toLowerCase();
+ assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+
+ const fromAddressAllowance = await this.getAllowanceAsync(
+ normalizedTokenAddress,
+ normalizedFromAddress,
+ normalizedSenderAddress,
+ );
+ if (fromAddressAllowance.lessThan(amountInBaseUnits)) {
+ throw new Error(ContractWrappersError.InsufficientAllowanceForTransfer);
+ }
+
+ const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress);
+ if (fromAddressBalance.lessThan(amountInBaseUnits)) {
+ throw new Error(ContractWrappersError.InsufficientBalanceForTransfer);
+ }
+
+ const txHash = await tokenContract.transferFrom.sendTransactionAsync(
+ normalizedFromAddress,
+ normalizedToAddress,
+ amountInBaseUnits,
+ {
+ from: normalizedSenderAddress,
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ },
+ );
+ 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);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ assert.isFunction('callback', callback);
+ const subscriptionToken = this._subscribe<ArgsType>(
+ normalizedTokenAddress,
+ eventName,
+ indexFilterValues,
+ artifacts.Token.abi,
+ callback,
+ );
+ return subscriptionToken;
+ }
+ /**
+ * Cancel a subscription
+ * @param subscriptionToken Subscription token returned by `subscribe()`
+ */
+ public unsubscribe(subscriptionToken: string): void {
+ this._unsubscribe(subscriptionToken);
+ }
+ /**
+ * Cancels all existing subscriptions
+ */
+ public unsubscribeAll(): void {
+ super._unsubscribeAll();
+ }
+ /**
+ * Gets historical logs without creating a subscription
+ * @param tokenAddress An address of the token that emitted the logs.
+ * @param eventName The token contract event you would like to subscribe to.
+ * @param blockRange Block range to get logs from.
+ * @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,
+ blockRange: BlockRange,
+ indexFilterValues: IndexedFilterValues,
+ ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
+ assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ const logs = await this._getLogsAsync<ArgsType>(
+ normalizedTokenAddress,
+ eventName,
+ blockRange,
+ indexFilterValues,
+ artifacts.Token.abi,
+ );
+ return logs;
+ }
+ private _invalidateContractInstances(): void {
+ this.unsubscribeAll();
+ this._tokenContractsByAddress = {};
+ }
+ private async _getTokenContractAsync(tokenAddress: string): Promise<TokenContract> {
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ let tokenContract = this._tokenContractsByAddress[normalizedTokenAddress];
+ if (!_.isUndefined(tokenContract)) {
+ return tokenContract;
+ }
+ const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.Token,
+ normalizedTokenAddress,
+ );
+ const contractInstance = new TokenContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ tokenContract = contractInstance;
+ this._tokenContractsByAddress[normalizedTokenAddress] = tokenContract;
+ return tokenContract;
+ }
+}
diff --git a/packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts b/packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts
new file mode 100644
index 000000000..5017d879a
--- /dev/null
+++ b/packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts
@@ -0,0 +1,28 @@
+import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils';
+import { BlockParamLiteral } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+
+import { TokenWrapper } from '../contract_wrappers/token_wrapper';
+
+export class SimpleBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
+ private _tokenWrapper: TokenWrapper;
+ private _defaultBlock: BlockParamLiteral;
+ constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) {
+ this._tokenWrapper = token;
+ this._defaultBlock = defaultBlock;
+ }
+ public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
+ const methodOpts = {
+ defaultBlock: this._defaultBlock,
+ };
+ const balance = this._tokenWrapper.getBalanceAsync(tokenAddress, userAddress, methodOpts);
+ return balance;
+ }
+ public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
+ const methodOpts = {
+ defaultBlock: this._defaultBlock,
+ };
+ const proxyAllowance = this._tokenWrapper.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts);
+ return proxyAllowance;
+ }
+}
diff --git a/packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts b/packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts
new file mode 100644
index 000000000..52d0a100c
--- /dev/null
+++ b/packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts
@@ -0,0 +1,36 @@
+import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils';
+import { BlockParamLiteral } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+
+import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper';
+
+export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher {
+ private _exchangeWrapper: ExchangeWrapper;
+ private _defaultBlock: BlockParamLiteral;
+ constructor(exchange: ExchangeWrapper, defaultBlock: BlockParamLiteral) {
+ this._exchangeWrapper = exchange;
+ this._defaultBlock = defaultBlock;
+ }
+ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ const methodOpts = {
+ defaultBlock: this._defaultBlock,
+ };
+ const filledTakerAmount = this._exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts);
+ return filledTakerAmount;
+ }
+ public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ const methodOpts = {
+ defaultBlock: this._defaultBlock,
+ };
+ const cancelledTakerAmount = this._exchangeWrapper.getCancelledTakerAmountAsync(orderHash, methodOpts);
+ return cancelledTakerAmount;
+ }
+ public async getUnavailableTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ const unavailableTakerAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
+ return unavailableTakerAmount;
+ }
+ public getZRXTokenAddress(): string {
+ const zrxToken = this._exchangeWrapper.getZRXTokenAddress();
+ return zrxToken;
+ }
+}
diff --git a/packages/contract-wrappers/src/globals.d.ts b/packages/contract-wrappers/src/globals.d.ts
new file mode 100644
index 000000000..94e63a32d
--- /dev/null
+++ b/packages/contract-wrappers/src/globals.d.ts
@@ -0,0 +1,6 @@
+declare module '*.json' {
+ const json: any;
+ /* tslint:disable */
+ export default json;
+ /* tslint:enable */
+}
diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts
new file mode 100644
index 000000000..45af8a707
--- /dev/null
+++ b/packages/contract-wrappers/src/index.ts
@@ -0,0 +1,68 @@
+export { ContractWrappers } from './contract_wrappers';
+export { ExchangeWrapper } from './contract_wrappers/exchange_wrapper';
+export { TokenWrapper } from './contract_wrappers/token_wrapper';
+export { TokenRegistryWrapper } from './contract_wrappers/token_registry_wrapper';
+export { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper';
+export { TokenTransferProxyWrapper } from './contract_wrappers/token_transfer_proxy_wrapper';
+
+export {
+ ContractWrappersError,
+ EventCallback,
+ ContractEvent,
+ Token,
+ IndexedFilterValues,
+ BlockRange,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ ContractEventArgs,
+ ContractWrappersConfig,
+ MethodOpts,
+ OrderTransactionOpts,
+ TransactionOpts,
+ LogEvent,
+ DecodedLogEvent,
+ OnOrderStateChangeCallback,
+} from './types';
+
+export {
+ BlockParamLiteral,
+ FilterObject,
+ BlockParam,
+ ContractEventArg,
+ ExchangeContractErrs,
+ LogWithDecodedArgs,
+ Order,
+ Provider,
+ SignedOrder,
+ ECSignature,
+ OrderStateValid,
+ OrderStateInvalid,
+ OrderState,
+ TransactionReceipt,
+ TransactionReceiptWithDecodedLogs,
+} from '@0xproject/types';
+
+export {
+ EtherTokenContractEventArgs,
+ WithdrawalContractEventArgs,
+ DepositContractEventArgs,
+ EtherTokenEvents,
+} from './contract_wrappers/generated/ether_token';
+
+export {
+ TransferContractEventArgs,
+ ApprovalContractEventArgs,
+ TokenContractEventArgs,
+ TokenEvents,
+} from './contract_wrappers/generated/token';
+
+export {
+ LogErrorContractEventArgs,
+ LogCancelContractEventArgs,
+ LogFillContractEventArgs,
+ ExchangeContractEventArgs,
+ ExchangeEvents,
+} from './contract_wrappers/generated/exchange';
+
+export { BalanceAndProxyAllowanceLazyStore } from './stores/balance_proxy_allowance_lazy_store';
+export { OrderFilledCancelledLazyStore } from './stores/order_filled_cancelled_lazy_store';
diff --git a/packages/contract-wrappers/src/monorepo_scripts/postpublish.ts b/packages/contract-wrappers/src/monorepo_scripts/postpublish.ts
new file mode 100644
index 000000000..dcb99d0f7
--- /dev/null
+++ b/packages/contract-wrappers/src/monorepo_scripts/postpublish.ts
@@ -0,0 +1,8 @@
+import { postpublishUtils } from '@0xproject/monorepo-scripts';
+
+import * as packageJSON from '../package.json';
+import * as tsConfigJSON from '../tsconfig.json';
+
+const cwd = `${__dirname}/..`;
+// tslint:disable-next-line:no-floating-promises
+postpublishUtils.runAsync(packageJSON, tsConfigJSON, cwd);
diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts
new file mode 100644
index 000000000..ac248b2d4
--- /dev/null
+++ b/packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts
@@ -0,0 +1,5 @@
+export const ContractWrappersConfigSchema = {
+ id: '/ContractWrappersConfig',
+ oneOf: [{ $ref: '/ZeroExContractPrivateNetworkConfig' }, { $ref: '/ZeroExContractPublicNetworkConfig' }],
+ type: 'object',
+};
diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts
new file mode 100644
index 000000000..759078feb
--- /dev/null
+++ b/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts
@@ -0,0 +1,35 @@
+export const contractWrappersPrivateNetworkConfigSchema = {
+ id: '/ZeroExContractPrivateNetworkConfig',
+ properties: {
+ networkId: {
+ type: 'number',
+ minimum: 1,
+ },
+ gasPrice: { $ref: '/Number' },
+ zrxContractAddress: { $ref: '/Address' },
+ exchangeContractAddress: { $ref: '/Address' },
+ tokenRegistryContractAddress: { $ref: '/Address' },
+ tokenTransferProxyContractAddress: { $ref: '/Address' },
+ orderWatcherConfig: {
+ type: 'object',
+ properties: {
+ pollingIntervalMs: {
+ type: 'number',
+ minimum: 0,
+ },
+ numConfirmations: {
+ type: 'number',
+ minimum: 0,
+ },
+ },
+ },
+ },
+ type: 'object',
+ required: [
+ 'networkId',
+ 'zrxContractAddress',
+ 'exchangeContractAddress',
+ 'tokenRegistryContractAddress',
+ 'tokenTransferProxyContractAddress',
+ ],
+};
diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts
new file mode 100644
index 000000000..bf1000afa
--- /dev/null
+++ b/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts
@@ -0,0 +1,29 @@
+export const contractWrappersPublicNetworkConfigSchema = {
+ id: '/ZeroExContractPublicNetworkConfig',
+ properties: {
+ networkId: {
+ type: 'number',
+ enum: [1, 3, 4, 42, 50],
+ },
+ gasPrice: { $ref: '/Number' },
+ zrxContractAddress: { $ref: '/Address' },
+ exchangeContractAddress: { $ref: '/Address' },
+ tokenRegistryContractAddress: { $ref: '/Address' },
+ tokenTransferProxyContractAddress: { $ref: '/Address' },
+ orderWatcherConfig: {
+ type: 'object',
+ properties: {
+ pollingIntervalMs: {
+ type: 'number',
+ minimum: 0,
+ },
+ numConfirmations: {
+ type: 'number',
+ minimum: 0,
+ },
+ },
+ },
+ },
+ type: 'object',
+ required: ['networkId'],
+};
diff --git a/packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts b/packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts
new file mode 100644
index 000000000..614195157
--- /dev/null
+++ b/packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts
@@ -0,0 +1,91 @@
+import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils';
+import { BlockParamLiteral } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { TokenWrapper } from '../contract_wrappers/token_wrapper';
+
+/**
+ * Copy on read store for balances/proxyAllowances of tokens/accounts
+ */
+export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProxyAllowanceFetcher {
+ private _tokenWrapper: TokenWrapper;
+ private _defaultBlock: BlockParamLiteral;
+ private _balance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber;
+ };
+ };
+ private _proxyAllowance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber;
+ };
+ };
+ constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) {
+ this._tokenWrapper = token;
+ this._defaultBlock = defaultBlock;
+ 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: this._defaultBlock,
+ };
+ const balance = await this._tokenWrapper.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: this._defaultBlock,
+ };
+ const proxyAllowance = await this._tokenWrapper.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/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts b/packages/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts
new file mode 100644
index 000000000..3d96a95f3
--- /dev/null
+++ b/packages/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts
@@ -0,0 +1,75 @@
+import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils';
+import { BlockParamLiteral } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper';
+
+/**
+ * Copy on read store for filled/cancelled taker amounts
+ */
+export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancelledFetcher {
+ private _exchangeWrapper: ExchangeWrapper;
+ private _defaultBlock: BlockParamLiteral;
+ private _filledTakerAmount: {
+ [orderHash: string]: BigNumber;
+ };
+ private _cancelledTakerAmount: {
+ [orderHash: string]: BigNumber;
+ };
+ constructor(exchange: ExchangeWrapper, defaultBlock: BlockParamLiteral) {
+ this._exchangeWrapper = exchange;
+ this._defaultBlock = defaultBlock;
+ this._filledTakerAmount = {};
+ this._cancelledTakerAmount = {};
+ }
+ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ if (_.isUndefined(this._filledTakerAmount[orderHash])) {
+ const methodOpts = {
+ defaultBlock: this._defaultBlock,
+ };
+ const filledTakerAmount = await this._exchangeWrapper.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: this._defaultBlock,
+ };
+ const cancelledTakerAmount = await this._exchangeWrapper.getCancelledTakerAmountAsync(
+ 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 = {};
+ }
+ public async getUnavailableTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ const unavailableTakerAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
+ return unavailableTakerAmount;
+ }
+ public getZRXTokenAddress(): string {
+ const zrxToken = this._exchangeWrapper.getZRXTokenAddress();
+ return zrxToken;
+ }
+}
diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts
new file mode 100644
index 000000000..9a463d635
--- /dev/null
+++ b/packages/contract-wrappers/src/types.ts
@@ -0,0 +1,186 @@
+import { BigNumber } from '@0xproject/utils';
+
+import {
+ BlockParam,
+ BlockParamLiteral,
+ ContractAbi,
+ ContractEventArg,
+ ExchangeContractErrs,
+ FilterObject,
+ LogEntryEvent,
+ LogWithDecodedArgs,
+ Order,
+ OrderState,
+ SignedOrder,
+} from '@0xproject/types';
+
+import { EtherTokenContractEventArgs, EtherTokenEvents } from './contract_wrappers/generated/ether_token';
+import { ExchangeContractEventArgs, ExchangeEvents } from './contract_wrappers/generated/exchange';
+import { TokenContractEventArgs, TokenEvents } from './contract_wrappers/generated/token';
+
+export enum ContractWrappersError {
+ ExchangeContractDoesNotExist = 'EXCHANGE_CONTRACT_DOES_NOT_EXIST',
+ ZRXContractDoesNotExist = 'ZRX_CONTRACT_DOES_NOT_EXIST',
+ EtherTokenContractDoesNotExist = 'ETHER_TOKEN_CONTRACT_DOES_NOT_EXIST',
+ TokenTransferProxyContractDoesNotExist = 'TOKEN_TRANSFER_PROXY_CONTRACT_DOES_NOT_EXIST',
+ TokenRegistryContractDoesNotExist = 'TOKEN_REGISTRY_CONTRACT_DOES_NOT_EXIST',
+ TokenContractDoesNotExist = 'TOKEN_CONTRACT_DOES_NOT_EXIST',
+ 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',
+ SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND',
+ SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT',
+}
+
+export enum InternalContractWrappersError {
+ NoAbiDecoder = 'NO_ABI_DECODER',
+ ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
+ WethNotInTokenRegistry = 'WETH_NOT_IN_TOKEN_REGISTRY',
+}
+
+export type LogEvent = LogEntryEvent;
+export interface DecodedLogEvent<ArgsType> {
+ isRemoved: boolean;
+ log: LogWithDecodedArgs<ArgsType>;
+}
+
+export type EventCallback<ArgsType> = (err: null | Error, log?: DecodedLogEvent<ArgsType>) => void;
+
+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 interface ContractEvent {
+ logIndex: number;
+ transactionIndex: number;
+ transactionHash: string;
+ blockHash: string;
+ blockNumber: number;
+ address: string;
+ type: string;
+ event: string;
+ args: ContractEventArgs;
+}
+
+export type ContractEventArgs = ExchangeContractEventArgs | TokenContractEventArgs | EtherTokenContractEventArgs;
+
+// [address, name, symbol, decimals, ipfsHash, swarmHash]
+export type TokenMetadata = [string, string, string, number, string, string];
+
+export interface Token {
+ name: string;
+ address: string;
+ symbol: string;
+ decimals: number;
+}
+
+export interface TxOpts {
+ from: string;
+ gas?: number;
+ value?: BigNumber;
+ gasPrice?: BigNumber;
+}
+
+export interface TokenAddressBySymbol {
+ [symbol: string]: string;
+}
+
+export type ContractEvents = TokenEvents | ExchangeEvents | EtherTokenEvents;
+
+export interface IndexedFilterValues {
+ [index: string]: ContractEventArg;
+}
+
+export interface BlockRange {
+ fromBlock: BlockParam;
+ toBlock: BlockParam;
+}
+
+export interface OrderCancellationRequest {
+ order: Order | SignedOrder;
+ takerTokenCancelAmount: BigNumber;
+}
+
+export interface OrderFillRequest {
+ signedOrder: SignedOrder;
+ takerTokenFillAmount: BigNumber;
+}
+
+export type AsyncMethod = (...args: any[]) => Promise<any>;
+export type SyncMethod = (...args: any[]) => any;
+
+/**
+ * networkId: The id of the underlying ethereum network your provider is connected to. (1-mainnet, 3-ropsten, 4-rinkeby, 42-kovan, 50-testrpc)
+ * gasPrice: Gas price to use with every transaction
+ * exchangeContractAddress: The address of an exchange contract to use
+ * zrxContractAddress: The address of the ZRX contract to use
+ * tokenRegistryContractAddress: The address of a token registry contract to use
+ * tokenTransferProxyContractAddress: The address of the token transfer proxy contract to use
+ * orderWatcherConfig: All the configs related to the orderWatcher
+ */
+export interface ContractWrappersConfig {
+ networkId: number;
+ gasPrice?: BigNumber;
+ exchangeContractAddress?: string;
+ zrxContractAddress?: string;
+ tokenRegistryContractAddress?: string;
+ tokenTransferProxyContractAddress?: 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?: BlockParam;
+}
+
+/**
+ * gasPrice: Gas price in Wei to use for a transaction
+ * gasLimit: The amount of gas to send with a transaction
+ */
+export interface TransactionOpts {
+ gasPrice?: BigNumber;
+ gasLimit?: number;
+}
+
+/**
+ * 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. Default=true.
+ */
+export interface OrderTransactionOpts extends TransactionOpts {
+ shouldValidate?: boolean;
+}
+
+export enum TradeSide {
+ Maker = 'maker',
+ Taker = 'taker',
+}
+
+export enum TransferType {
+ Trade = 'trade',
+ Fee = 'fee',
+}
+
+export type OnOrderStateChangeCallback = (err: Error | null, orderState?: OrderState) => void;
diff --git a/packages/contract-wrappers/src/utils/assert.ts b/packages/contract-wrappers/src/utils/assert.ts
new file mode 100644
index 000000000..2588a4d09
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/assert.ts
@@ -0,0 +1,31 @@
+import { assert as sharedAssert } from '@0xproject/assert';
+// We need those two unused imports because they're actually used by sharedAssert which gets injected here
+// tslint:disable-next-line:no-unused-variable
+import { Schema } from '@0xproject/json-schemas';
+// tslint:disable-next-line:no-unused-variable
+import { ECSignature } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { isValidSignature } from '@0xproject/order-utils';
+
+export const assert = {
+ ...sharedAssert,
+ isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) {
+ const isValid = isValidSignature(orderHash, ecSignature, signerAddress);
+ this.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`);
+ },
+ async isSenderAddressAsync(
+ variableName: string,
+ senderAddressHex: string,
+ web3Wrapper: Web3Wrapper,
+ ): Promise<void> {
+ sharedAssert.isETHAddressHex(variableName, senderAddressHex);
+ const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex);
+ sharedAssert.assert(
+ isSenderAddressAvailable,
+ `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`,
+ );
+ },
+};
diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts
new file mode 100644
index 000000000..07da6745d
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/constants.ts
@@ -0,0 +1,11 @@
+import { BigNumber } from '@0xproject/utils';
+
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ TESTRPC_NETWORK_ID: 50,
+ INVALID_JUMP_PATTERN: 'invalid JUMP at',
+ OUT_OF_GAS_PATTERN: 'out of gas',
+ INVALID_TAKER_FORMAT: 'instance.taker is not of a type(s) string',
+ UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
+ DEFAULT_BLOCK_POLLING_INTERVAL: 1000,
+};
diff --git a/packages/contract-wrappers/src/utils/decorators.ts b/packages/contract-wrappers/src/utils/decorators.ts
new file mode 100644
index 000000000..64123143c
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/decorators.ts
@@ -0,0 +1,91 @@
+import * as _ from 'lodash';
+
+import { AsyncMethod, ContractWrappersError, SyncMethod } from '../types';
+
+import { constants } from './constants';
+
+type ErrorTransformer = (err: Error) => Error;
+
+const contractCallErrorTransformer = (error: Error) => {
+ if (_.includes(error.message, constants.INVALID_JUMP_PATTERN)) {
+ return new Error(ContractWrappersError.InvalidJump);
+ }
+ if (_.includes(error.message, constants.OUT_OF_GAS_PATTERN)) {
+ return new Error(ContractWrappersError.OutOfGas);
+ }
+ return error;
+};
+
+const schemaErrorTransformer = (error: Error) => {
+ if (_.includes(error.message, constants.INVALID_TAKER_FORMAT)) {
+ const errMsg =
+ 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS';
+ return new Error(errMsg);
+ }
+ return error;
+};
+
+/**
+ * Source: https://stackoverflow.com/a/29837695/3546986
+ */
+const asyncErrorHandlerFactory = (errorTransformer: ErrorTransformer) => {
+ const asyncErrorHandlingDecorator = (
+ target: object,
+ key: string | symbol,
+ descriptor: 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) {
+ const transformedError = errorTransformer(error);
+ throw transformedError;
+ }
+ };
+
+ return descriptor;
+ };
+
+ return asyncErrorHandlingDecorator;
+};
+
+const syncErrorHandlerFactory = (errorTransformer: ErrorTransformer) => {
+ const syncErrorHandlingDecorator = (
+ target: object,
+ key: string | symbol,
+ descriptor: TypedPropertyDescriptor<SyncMethod>,
+ ) => {
+ const originalMethod = descriptor.value as SyncMethod;
+
+ // 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 = function(...args: any[]) {
+ try {
+ const result = originalMethod.apply(this, args);
+ return result;
+ } catch (error) {
+ const transformedError = errorTransformer(error);
+ throw transformedError;
+ }
+ };
+
+ return descriptor;
+ };
+
+ return syncErrorHandlingDecorator;
+};
+
+// _.flow(f, g) = f ∘ g
+const zeroExErrorTransformer = _.flow(schemaErrorTransformer, contractCallErrorTransformer);
+
+export const decorators = {
+ asyncZeroExErrorHandler: asyncErrorHandlerFactory(zeroExErrorTransformer),
+ syncZeroExErrorHandler: syncErrorHandlerFactory(zeroExErrorTransformer),
+};
diff --git a/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts b/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts
new file mode 100644
index 000000000..b575285a1
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts
@@ -0,0 +1,115 @@
+import { BlockParamLiteral, ExchangeContractErrs } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { TokenWrapper } from '../contract_wrappers/token_wrapper';
+import { BalanceAndProxyAllowanceLazyStore } from '../stores/balance_proxy_allowance_lazy_store';
+import { TradeSide, TransferType } from '../types';
+import { constants } from '../utils/constants';
+
+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;
+ private static _throwValidationError(
+ failureReason: FailureReason,
+ tradeSide: TradeSide,
+ transferType: TransferType,
+ ): never {
+ const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
+ throw new Error(errMsg);
+ }
+ constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) {
+ this._store = new BalanceAndProxyAllowanceLazyStore(token, defaultBlock);
+ 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> {
+ // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/
+ // allowances for the taker. We do however, want to increase the balance of the maker since the maker
+ // might be relying on those funds to fill subsequent orders or pay the order's fees.
+ if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) {
+ await this._increaseBalanceAsync(tokenAddress, to, amountInBaseUnits);
+ return;
+ }
+ const balance = await this._store.getBalanceAsync(tokenAddress, from);
+ const proxyAllowance = await this._store.getProxyAllowanceAsync(tokenAddress, from);
+ if (proxyAllowance.lessThan(amountInBaseUnits)) {
+ ExchangeTransferSimulator._throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
+ }
+ if (balance.lessThan(amountInBaseUnits)) {
+ ExchangeTransferSimulator._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));
+ }
+}
diff --git a/packages/contract-wrappers/src/utils/filter_utils.ts b/packages/contract-wrappers/src/utils/filter_utils.ts
new file mode 100644
index 000000000..c5df7321e
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/filter_utils.ts
@@ -0,0 +1,95 @@
+import {
+ ConstructorAbi,
+ ContractAbi,
+ EventAbi,
+ FallbackAbi,
+ FilterObject,
+ LogEntry,
+ MethodAbi,
+} from '@0xproject/types';
+import * as ethUtil from 'ethereumjs-util';
+import * as jsSHA3 from 'js-sha3';
+import * as _ from 'lodash';
+import * as uuid from 'uuid/v4';
+
+import { BlockRange, ContractEvents, IndexedFilterValues } from '../types';
+
+const TOPIC_LENGTH = 32;
+
+export const filterUtils = {
+ generateUUID(): string {
+ return uuid();
+ },
+ getFilter(
+ address: string,
+ eventName: ContractEvents,
+ indexFilterValues: IndexedFilterValues,
+ abi: ContractAbi,
+ blockRange?: BlockRange,
+ ): FilterObject {
+ const eventAbi = _.find(abi, { name: eventName }) as 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: FilterObject = {
+ address,
+ topics,
+ };
+ if (!_.isUndefined(blockRange)) {
+ filter = {
+ ...blockRange,
+ ...filter,
+ };
+ }
+ return filter;
+ },
+ getEventSignatureFromAbiByName(eventAbi: EventAbi, eventName: ContractEvents): string {
+ const types = _.map(eventAbi.inputs, 'type');
+ const signature = `${eventAbi.name}(${types.join(',')})`;
+ return signature;
+ },
+ getTopicsForIndexedArgs(abi: 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: LogEntry, filter: 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/contract-wrappers/src/utils/order_validation_utils.ts b/packages/contract-wrappers/src/utils/order_validation_utils.ts
new file mode 100644
index 000000000..36dfbd800
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/order_validation_utils.ts
@@ -0,0 +1,198 @@
+import { getOrderHashHex, isValidSignature, OrderError } from '@0xproject/order-utils';
+import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper';
+import { ContractWrappersError, TradeSide, TransferType } from '../types';
+import { constants } from '../utils/constants';
+import { utils } from '../utils/utils';
+
+import { ExchangeTransferSimulator } from './exchange_transfer_simulator';
+
+export class OrderValidationUtils {
+ private _exchangeWrapper: ExchangeWrapper;
+ public static validateCancelOrderThrowIfInvalid(
+ order: Order,
+ cancelTakerTokenAmount: BigNumber,
+ unavailableTakerTokenAmount: BigNumber,
+ ): void {
+ if (cancelTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderCancelAmountZero);
+ }
+ if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) {
+ throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
+ }
+ const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec();
+ if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
+ throw new Error(ExchangeContractErrs.OrderCancelExpired);
+ }
+ }
+ public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator,
+ signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ senderAddress: string,
+ zrxTokenAddress: string,
+ ): Promise<void> {
+ const fillMakerTokenAmount = OrderValidationUtils._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 = OrderValidationUtils._getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerFee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress,
+ signedOrder.maker,
+ signedOrder.feeRecipient,
+ makerFeeAmount,
+ TradeSide.Maker,
+ TransferType.Fee,
+ );
+ const takerFeeAmount = OrderValidationUtils._getPartialAmount(
+ fillTakerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.takerFee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress,
+ senderAddress,
+ signedOrder.feeRecipient,
+ takerFeeAmount,
+ TradeSide.Taker,
+ TransferType.Fee,
+ );
+ }
+ private static _validateRemainingFillAmountNotZeroOrThrow(
+ takerTokenAmount: BigNumber,
+ unavailableTakerTokenAmount: BigNumber,
+ ) {
+ if (takerTokenAmount.eq(unavailableTakerTokenAmount)) {
+ throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ }
+ }
+ private static _validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) {
+ const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec();
+ if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
+ throw new Error(ExchangeContractErrs.OrderFillExpired);
+ }
+ }
+ private static _getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
+ const fillMakerTokenAmount = numerator
+ .mul(target)
+ .div(denominator)
+ .round(0);
+ return fillMakerTokenAmount;
+ }
+ constructor(exchangeWrapper: ExchangeWrapper) {
+ this._exchangeWrapper = exchangeWrapper;
+ }
+ public async validateOrderFillableOrThrowAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator,
+ signedOrder: SignedOrder,
+ zrxTokenAddress: string,
+ expectedFillTakerTokenAmount?: BigNumber,
+ ): Promise<void> {
+ const orderHash = getOrderHashHex(signedOrder);
+ const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
+ OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow(
+ signedOrder.takerTokenAmount,
+ unavailableTakerTokenAmount,
+ );
+ OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
+ let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ if (!_.isUndefined(expectedFillTakerTokenAmount)) {
+ fillTakerTokenAmount = expectedFillTakerTokenAmount;
+ }
+ await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount,
+ signedOrder.taker,
+ zrxTokenAddress,
+ );
+ }
+ 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 = getOrderHashHex(signedOrder);
+ if (!isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker)) {
+ throw new Error(OrderError.InvalidSignature);
+ }
+ const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
+ OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow(
+ signedOrder.takerTokenAmount,
+ unavailableTakerTokenAmount,
+ );
+ if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) {
+ throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
+ }
+ OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
+ const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount)
+ ? remainingTakerTokenAmount
+ : fillTakerTokenAmount;
+ await OrderValidationUtils.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);
+ }
+ }
+}
diff --git a/packages/contract-wrappers/src/utils/utils.ts b/packages/contract-wrappers/src/utils/utils.ts
new file mode 100644
index 000000000..af1125632
--- /dev/null
+++ b/packages/contract-wrappers/src/utils/utils.ts
@@ -0,0 +1,13 @@
+import { BigNumber } from '@0xproject/utils';
+
+export const utils = {
+ spawnSwitchErr(name: string, value: any): Error {
+ return new Error(`Unexpected switch value: ${value} encountered for ${name}`);
+ },
+ getCurrentUnixTimestampSec(): BigNumber {
+ return new BigNumber(Date.now() / 1000).round();
+ },
+ getCurrentUnixTimestampMs(): BigNumber {
+ return new BigNumber(Date.now());
+ },
+};
diff --git a/packages/contract-wrappers/test/artifacts_test.ts b/packages/contract-wrappers/test/artifacts_test.ts
new file mode 100644
index 000000000..5d7261e09
--- /dev/null
+++ b/packages/contract-wrappers/test/artifacts_test.ts
@@ -0,0 +1,49 @@
+import { web3Factory } from '@0xproject/dev-utils';
+import * as fs from 'fs';
+
+import { ContractWrappers } from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+
+chaiSetup.configure();
+
+// 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 provider = web3Factory.create({ rpcUrl: kovanRpcUrl }).currentProvider;
+ const config = {
+ networkId: constants.KOVAN_NETWORK_ID,
+ };
+ const contractWrappers = new ContractWrappers(provider, config);
+ it('token registry contract is deployed', async () => {
+ await (contractWrappers.tokenRegistry as any)._getTokenRegistryContractAsync();
+ }).timeout(TIMEOUT);
+ it('proxy contract is deployed', async () => {
+ await (contractWrappers.proxy as any)._getTokenTransferProxyContractAsync();
+ }).timeout(TIMEOUT);
+ it('exchange contract is deployed', async () => {
+ await (contractWrappers.exchange as any)._getExchangeContractAsync();
+ }).timeout(TIMEOUT);
+ });
+ describe('contracts are deployed on ropsten', () => {
+ const ropstenRpcUrl = constants.ROPSTEN_RPC_URL;
+ const provider = web3Factory.create({ rpcUrl: ropstenRpcUrl }).currentProvider;
+ const config = {
+ networkId: constants.ROPSTEN_NETWORK_ID,
+ };
+ const contractWrappers = new ContractWrappers(provider, config);
+ it('token registry contract is deployed', async () => {
+ await (contractWrappers.tokenRegistry as any)._getTokenRegistryContractAsync();
+ }).timeout(TIMEOUT);
+ it('proxy contract is deployed', async () => {
+ await (contractWrappers.proxy as any)._getTokenTransferProxyContractAsync();
+ }).timeout(TIMEOUT);
+ it('exchange contract is deployed', async () => {
+ await (contractWrappers.exchange as any)._getExchangeContractAsync();
+ }).timeout(TIMEOUT);
+ });
+});
diff --git a/packages/contract-wrappers/test/ether_token_wrapper_test.ts b/packages/contract-wrappers/test/ether_token_wrapper_test.ts
new file mode 100644
index 000000000..e9a9705b1
--- /dev/null
+++ b/packages/contract-wrappers/test/ether_token_wrapper_test.ts
@@ -0,0 +1,417 @@
+import { BlockchainLifecycle, callbackErrorReporter, devConstants, web3Factory } from '@0xproject/dev-utils';
+import { DoneCallback } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as chai from 'chai';
+import 'mocha';
+
+import {
+ ApprovalContractEventArgs,
+ BlockParamLiteral,
+ BlockRange,
+ ContractWrappers,
+ ContractWrappersError,
+ DecodedLogEvent,
+ DepositContractEventArgs,
+ EtherTokenEvents,
+ Token,
+ TransferContractEventArgs,
+ WithdrawalContractEventArgs,
+} from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { TokenUtils } from './utils/token_utils';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+// 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 = 62517;
+
+describe('EtherTokenWrapper', () => {
+ let contractWrappers: ContractWrappers;
+ let tokens: Token[];
+ let userAddresses: string[];
+ let addressWithETH: string;
+ let wethContractAddress: string;
+ let depositWeiAmount: BigNumber;
+ let decimalPlaces: number;
+ let addressWithoutFunds: string;
+ const gasPrice = new BigNumber(1);
+ const zeroExConfig = {
+ gasPrice,
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ const depositAmount = new BigNumber(42);
+ const withdrawalAmount = new BigNumber(42);
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, zeroExConfig);
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ addressWithETH = userAddresses[0];
+ wethContractAddress = contractWrappers.etherToken.getContractAddressIfExists() as string;
+ depositWeiAmount = Web3Wrapper.toWei(new BigNumber(5));
+ decimalPlaces = 7;
+ addressWithoutFunds = userAddresses[1];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#getContractAddressIfExists', async () => {
+ it('should return contract address if connected to a known network', () => {
+ const contractAddressIfExists = contractWrappers.etherToken.getContractAddressIfExists();
+ expect(contractAddressIfExists).to.not.be.undefined();
+ });
+ it('should throw if connected to a private network and contract addresses are not specified', () => {
+ const UNKNOWN_NETWORK_NETWORK_ID = 10;
+ expect(
+ () =>
+ new ContractWrappers(provider, {
+ networkId: UNKNOWN_NETWORK_NETWORK_ID,
+ } as any),
+ ).to.throw();
+ });
+ });
+ describe('#depositAsync', () => {
+ it('should successfully deposit ETH and issue Wrapped ETH tokens', async () => {
+ const preETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const preWETHBalance = await contractWrappers.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ expect(preETHBalance).to.be.bignumber.gt(0);
+ expect(preWETHBalance).to.be.bignumber.equal(0);
+
+ const txHash = await contractWrappers.etherToken.depositAsync(
+ wethContractAddress,
+ depositWeiAmount,
+ addressWithETH,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+
+ const postETHBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const postWETHBalanceInBaseUnits = await contractWrappers.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 web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+
+ const extraETHBalance = Web3Wrapper.toWei(new BigNumber(5));
+ const overETHBalanceinWei = preETHBalance.add(extraETHBalance);
+
+ return expect(
+ contractWrappers.etherToken.depositAsync(wethContractAddress, overETHBalanceinWei, addressWithETH),
+ ).to.be.rejectedWith(ContractWrappersError.InsufficientEthBalanceForDeposit);
+ });
+ });
+ describe('#withdrawAsync', () => {
+ it('should successfully withdraw ETH in return for Wrapped ETH tokens', async () => {
+ const ETHBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+
+ await contractWrappers.etherToken.depositAsync(wethContractAddress, depositWeiAmount, addressWithETH);
+
+ const expectedPreETHBalance = ETHBalanceInWei.minus(depositWeiAmount);
+ const preETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const preWETHBalance = await contractWrappers.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 contractWrappers.etherToken.withdrawAsync(
+ wethContractAddress,
+ depositWeiAmount,
+ addressWithETH,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+
+ const postETHBalance = await web3Wrapper.getBalanceInWeiAsync(addressWithETH);
+ const postWETHBalanceInBaseUnits = await contractWrappers.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 withdrawal', async () => {
+ const preWETHBalance = await contractWrappers.token.getBalanceAsync(wethContractAddress, addressWithETH);
+ expect(preWETHBalance).to.be.bignumber.equal(0);
+
+ const overWETHBalance = preWETHBalance.add(999999999);
+
+ return expect(
+ contractWrappers.etherToken.withdrawAsync(wethContractAddress, overWETHBalance, addressWithETH),
+ ).to.be.rejectedWith(ContractWrappersError.InsufficientWEthBalanceForWithdrawal);
+ });
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ let etherTokenAddress: string;
+ before(() => {
+ const tokenUtils = new TokenUtils(tokens);
+ const etherToken = tokenUtils.getWethTokenOrThrow();
+ etherTokenAddress = etherToken.address;
+ });
+ afterEach(() => {
+ contractWrappers.etherToken.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 async 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 = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<TransferContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ expect(logEvent.isRemoved).to.be.false();
+ expect(logEvent.log.logIndex).to.be.equal(0);
+ expect(logEvent.log.transactionIndex).to.be.equal(0);
+ expect(logEvent.log.blockNumber).to.be.a('number');
+ const args = logEvent.log.args;
+ expect(args._from).to.be.equal(addressWithETH);
+ expect(args._to).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(transferAmount);
+ },
+ );
+ await contractWrappers.etherToken.depositAsync(etherTokenAddress, transferAmount, addressWithETH);
+ contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Transfer,
+ indexFilterValues,
+ callback,
+ );
+ await contractWrappers.token.transferAsync(
+ etherTokenAddress,
+ addressWithETH,
+ addressWithoutFunds,
+ transferAmount,
+ );
+ })().catch(done);
+ });
+ it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => {
+ (async () => {
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ expect(logEvent.isRemoved).to.be.false();
+ const args = logEvent.log.args;
+ expect(args._owner).to.be.equal(addressWithETH);
+ expect(args._spender).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(allowanceAmount);
+ },
+ );
+ contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Approval,
+ indexFilterValues,
+ callback,
+ );
+ await contractWrappers.token.setAllowanceAsync(
+ etherTokenAddress,
+ addressWithETH,
+ addressWithoutFunds,
+ allowanceAmount,
+ );
+ })().catch(done);
+ });
+ it('Should receive the Deposit event when ether is being deposited', (done: DoneCallback) => {
+ (async () => {
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<DepositContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ expect(logEvent.isRemoved).to.be.false();
+ const args = logEvent.log.args;
+ expect(args._owner).to.be.equal(addressWithETH);
+ expect(args._value).to.be.bignumber.equal(depositAmount);
+ },
+ );
+ contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Deposit,
+ indexFilterValues,
+ callback,
+ );
+ await contractWrappers.etherToken.depositAsync(etherTokenAddress, depositAmount, addressWithETH);
+ })().catch(done);
+ });
+ it('Should receive the Withdrawal event when ether is being withdrawn', (done: DoneCallback) => {
+ (async () => {
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<WithdrawalContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ expect(logEvent.isRemoved).to.be.false();
+ const args = logEvent.log.args;
+ expect(args._owner).to.be.equal(addressWithETH);
+ expect(args._value).to.be.bignumber.equal(depositAmount);
+ },
+ );
+ await contractWrappers.etherToken.depositAsync(etherTokenAddress, depositAmount, addressWithETH);
+ contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Withdrawal,
+ indexFilterValues,
+ callback,
+ );
+ await contractWrappers.etherToken.withdrawAsync(etherTokenAddress, withdrawalAmount, addressWithETH);
+ })().catch(done);
+ });
+ it('should cancel outstanding subscriptions when ZeroEx.setProvider is called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ },
+ );
+ contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Transfer,
+ indexFilterValues,
+ callbackNeverToBeCalled,
+ );
+ const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)();
+ contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID);
+ await contractWrappers.etherToken.depositAsync(etherTokenAddress, transferAmount, addressWithETH);
+ contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Transfer,
+ indexFilterValues,
+ callbackToBeCalled,
+ );
+ await contractWrappers.token.transferAsync(
+ etherTokenAddress,
+ addressWithETH,
+ addressWithoutFunds,
+ transferAmount,
+ );
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ },
+ );
+ await contractWrappers.etherToken.depositAsync(etherTokenAddress, transferAmount, addressWithETH);
+ const subscriptionToken = contractWrappers.etherToken.subscribe(
+ etherTokenAddress,
+ EtherTokenEvents.Transfer,
+ indexFilterValues,
+ callbackNeverToBeCalled,
+ );
+ contractWrappers.etherToken.unsubscribe(subscriptionToken);
+ await contractWrappers.token.transferAsync(
+ etherTokenAddress,
+ addressWithETH,
+ addressWithoutFunds,
+ transferAmount,
+ );
+ done();
+ })().catch(done);
+ });
+ });
+ describe('#getLogsAsync', () => {
+ let etherTokenAddress: string;
+ let tokenTransferProxyAddress: string;
+ const blockRange: BlockRange = {
+ fromBlock: 0,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(() => {
+ addressWithETH = userAddresses[0];
+ const tokenUtils = new TokenUtils(tokens);
+ const etherToken = tokenUtils.getWethTokenOrThrow();
+ etherTokenAddress = etherToken.address;
+ tokenTransferProxyAddress = contractWrappers.proxy.getContractAddress();
+ });
+ it('should get logs with decoded args emitted by Approval', async () => {
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(etherTokenAddress, addressWithETH);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const eventName = EtherTokenEvents.Approval;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.etherToken.getLogsAsync<ApprovalContractEventArgs>(
+ etherTokenAddress,
+ eventName,
+ blockRange,
+ 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(addressWithETH);
+ expect(args._spender).to.be.equal(tokenTransferProxyAddress);
+ expect(args._value).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ it('should get logs with decoded args emitted by Deposit', async () => {
+ await contractWrappers.etherToken.depositAsync(etherTokenAddress, depositAmount, addressWithETH);
+ const eventName = EtherTokenEvents.Deposit;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.etherToken.getLogsAsync<DepositContractEventArgs>(
+ etherTokenAddress,
+ eventName,
+ blockRange,
+ 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(addressWithETH);
+ expect(args._value).to.be.bignumber.equal(depositAmount);
+ });
+ it('should only get the logs with the correct event name', async () => {
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(etherTokenAddress, addressWithETH);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const differentEventName = EtherTokenEvents.Transfer;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.etherToken.getLogsAsync(
+ etherTokenAddress,
+ differentEventName,
+ blockRange,
+ indexFilterValues,
+ );
+ expect(logs).to.have.length(0);
+ });
+ it('should only get the logs with the correct indexed fields', async () => {
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(etherTokenAddress, addressWithETH);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(
+ etherTokenAddress,
+ addressWithoutFunds,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const eventName = EtherTokenEvents.Approval;
+ const indexFilterValues = {
+ _owner: addressWithETH,
+ };
+ const logs = await contractWrappers.etherToken.getLogsAsync<ApprovalContractEventArgs>(
+ etherTokenAddress,
+ eventName,
+ blockRange,
+ indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args._owner).to.be.equal(addressWithETH);
+ });
+ });
+});
diff --git a/packages/contract-wrappers/test/exchange_transfer_simulator_test.ts b/packages/contract-wrappers/test/exchange_transfer_simulator_test.ts
new file mode 100644
index 000000000..b4ea91181
--- /dev/null
+++ b/packages/contract-wrappers/test/exchange_transfer_simulator_test.ts
@@ -0,0 +1,119 @@
+import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils';
+import { BlockParamLiteral, Token } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+
+import { ContractWrappers, ExchangeContractErrs } from '../src';
+import { TradeSide, TransferType } from '../src/types';
+import { ExchangeTransferSimulator } from '../src/utils/exchange_transfer_simulator';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('ExchangeTransferSimulator', () => {
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ const contractWrappers = new ContractWrappers(provider, config);
+ 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 web3Wrapper.getAvailableAddressesAsync();
+ [coinbase, sender, recipient] = userAddresses;
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ exampleTokenAddress = tokens[0].address;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#transferFromAsync', () => {
+ beforeEach(() => {
+ exchangeTransferSimulator = new ExchangeTransferSimulator(contractWrappers.token, BlockParamLiteral.Latest);
+ });
+ 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 contractWrappers.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await web3Wrapper.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 contractWrappers.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ txHash = await contractWrappers.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await web3Wrapper.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 contractWrappers.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(exampleTokenAddress, sender);
+ await web3Wrapper.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(
+ contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ );
+ });
+ });
+});
diff --git a/packages/contract-wrappers/test/exchange_wrapper_test.ts b/packages/contract-wrappers/test/exchange_wrapper_test.ts
new file mode 100644
index 000000000..fc0a23485
--- /dev/null
+++ b/packages/contract-wrappers/test/exchange_wrapper_test.ts
@@ -0,0 +1,1228 @@
+import { BlockchainLifecycle, callbackErrorReporter, devConstants, web3Factory } from '@0xproject/dev-utils';
+import { FillScenarios } from '@0xproject/fill-scenarios';
+import { getOrderHashHex } from '@0xproject/order-utils';
+import { BlockParamLiteral, DoneCallback, OrderState } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import 'mocha';
+
+import {
+ BlockRange,
+ ContractWrappers,
+ DecodedLogEvent,
+ ExchangeContractErrs,
+ ExchangeEvents,
+ LogCancelContractEventArgs,
+ LogFillContractEventArgs,
+ OrderCancellationRequest,
+ OrderFillRequest,
+ SignedOrder,
+ Token,
+} from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { TokenUtils } from './utils/token_utils';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777';
+
+describe('ExchangeWrapper', () => {
+ let contractWrappers: ContractWrappers;
+ let tokenUtils: TokenUtils;
+ let tokens: Token[];
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let fillScenarios: FillScenarios;
+ let exchangeContractAddress: string;
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ exchangeContractAddress = contractWrappers.exchange.getContractAddress();
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(provider, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ await fillScenarios.initTokenBalancesAsync();
+ });
+ 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 contractWrappers.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ 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 contractWrappers.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(
+ contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, {
+ shouldValidate: true,
+ }),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(
+ contractWrappers.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 contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(fillableAmount);
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(0);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(0);
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(fillableAmount);
+ await contractWrappers.exchange.fillOrKillOrderAsync(
+ signedOrder,
+ takerTokenFillAmount,
+ takerAddress,
+ );
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(takerTokenFillAmount);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(takerTokenFillAmount);
+ expect(
+ await contractWrappers.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 contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(partialFillAmount);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(partialFillAmount);
+ expect(
+ await contractWrappers.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(
+ contractWrappers.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.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(
+ contractWrappers.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 contractWrappers.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ 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 contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(fillableAmount);
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(0);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(0);
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(fillableAmount);
+ const txHash = await contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ takerTokenFillAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(takerTokenFillAmount);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(takerTokenFillAmount);
+ expect(
+ await contractWrappers.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 contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ partialFillAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
+ expect(
+ await contractWrappers.token.getBalanceAsync(takerTokenAddress, makerAddress),
+ ).to.be.bignumber.equal(partialFillAmount);
+ expect(
+ await contractWrappers.token.getBalanceAsync(makerTokenAddress, takerAddress),
+ ).to.be.bignumber.equal(partialFillAmount);
+ expect(
+ await contractWrappers.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 contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ takerTokenFillAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ expect(
+ await contractWrappers.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(
+ contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ emptyFillTakerAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.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(
+ contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ emptyFillTakerAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ {
+ shouldValidate: false,
+ },
+ ),
+ ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ describe('negative fill amount', async () => {
+ let signedOrder: SignedOrder;
+ const negativeFillTakerAmount = new BigNumber(-100);
+ beforeEach(async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ });
+ it('should not allow the exchange wrapper to fill if amount is negative', async () => {
+ return expect(
+ contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ negativeFillTakerAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejected();
+ });
+ });
+ });
+ 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 = getOrderHashHex(signedOrder);
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ anotherOrderHashHex = getOrderHashHex(anotherSignedOrder);
+ });
+ describe('successful batch fills', () => {
+ beforeEach(() => {
+ orderFillBatch = [
+ {
+ signedOrder,
+ takerTokenFillAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ takerTokenFillAmount,
+ },
+ ];
+ });
+ it('should throw if a batch is empty', async () => {
+ return expect(
+ contractWrappers.exchange.batchFillOrdersAsync(
+ [],
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ });
+ it('should successfully fill multiple orders', async () => {
+ const txHash = await contractWrappers.exchange.batchFillOrdersAsync(
+ orderFillBatch,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await contractWrappers.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(
+ contractWrappers.exchange.batchFillOrdersAsync(
+ orderFillBatch,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.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(
+ contractWrappers.exchange.batchFillOrdersAsync(
+ orderFillBatch,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ {
+ shouldValidate: false,
+ },
+ ),
+ ).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ });
+ describe('negative batch fill amount', async () => {
+ beforeEach(async () => {
+ const negativeFillTakerAmount = new BigNumber(-100);
+ orderFillBatch = [
+ {
+ signedOrder,
+ takerTokenFillAmount,
+ },
+ {
+ signedOrder: anotherSignedOrder,
+ takerTokenFillAmount: negativeFillTakerAmount,
+ },
+ ];
+ });
+ it('should not allow the exchange wrapper to batch fill if any amount is negative', async () => {
+ return expect(
+ contractWrappers.exchange.batchFillOrdersAsync(
+ orderFillBatch,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejected();
+ });
+ });
+ });
+ 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 = getOrderHashHex(signedOrder);
+ anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ anotherOrderHashHex = getOrderHashHex(anotherSignedOrder);
+ signedOrders = [signedOrder, anotherSignedOrder];
+ });
+ describe('successful batch fills', () => {
+ it('should throw if a batch is empty', async () => {
+ return expect(
+ contractWrappers.exchange.fillOrdersUpToAsync(
+ [],
+ fillUpToAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
+ });
+ it('should successfully fill up to specified amount when all orders are fully funded', async () => {
+ const txHash = await contractWrappers.exchange.fillOrdersUpToAsync(
+ signedOrders,
+ fillUpToAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(
+ anotherOrderHashHex,
+ );
+ expect(filledAmount).to.be.bignumber.equal(fillableAmount);
+ const remainingFillAmount = fillableAmount.minus(1);
+ expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
+ });
+ it('should successfully fill up to specified amount and leave the rest of the orders untouched', async () => {
+ const txHash = await contractWrappers.exchange.fillOrdersUpToAsync(
+ signedOrders,
+ fillableAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const zeroAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(anotherOrderHashHex);
+ expect(filledAmount).to.be.bignumber.equal(fillableAmount);
+ expect(zeroAmount).to.be.bignumber.equal(0);
+ });
+ it('should successfully fill up to specified amount even if filling all orders would fail', async () => {
+ const missingBalance = new BigNumber(1); // User will still have enough balance to fill up to 9,
+ // but won't have 10 to fully fill all orders in a batch.
+ await contractWrappers.token.transferAsync(
+ makerTokenAddress,
+ makerAddress,
+ coinbase,
+ missingBalance,
+ );
+ const txHash = await contractWrappers.exchange.fillOrdersUpToAsync(
+ signedOrders,
+ fillUpToAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const filledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
+ const anotherFilledAmount = await contractWrappers.exchange.getFilledTakerAmountAsync(
+ anotherOrderHashHex,
+ );
+ expect(filledAmount).to.be.bignumber.equal(fillableAmount);
+ const remainingFillAmount = fillableAmount.minus(1);
+ expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
+ });
+ });
+ describe('failed batch fills', () => {
+ it("should fail validation if user doesn't have enough balance without fill up to", async () => {
+ const missingBalance = new BigNumber(2); // User will only have enough balance to fill up to 8
+ await contractWrappers.token.transferAsync(
+ makerTokenAddress,
+ makerAddress,
+ coinbase,
+ missingBalance,
+ );
+ return expect(
+ contractWrappers.exchange.fillOrdersUpToAsync(
+ signedOrders,
+ fillUpToAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
+ });
+ });
+ describe('order transaction options', () => {
+ const emptyFillUpToAmount = new BigNumber(0);
+ it('should validate when orderTransactionOptions are not present', async () => {
+ return expect(
+ contractWrappers.exchange.fillOrdersUpToAsync(
+ signedOrders,
+ emptyFillUpToAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.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(
+ contractWrappers.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.getDummyTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ orderHashHex = getOrderHashHex(signedOrder);
+ });
+ describe('#cancelOrderAsync', () => {
+ describe('successful cancels', () => {
+ it('should cancel an order', async () => {
+ const txHash = await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const cancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(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(
+ contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, {
+ shouldValidate: true,
+ }),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(
+ contractWrappers.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 = 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(
+ contractWrappers.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 contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch);
+ const cancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHashHex);
+ const anotherCancelledAmount = await contractWrappers.exchange.getCancelledTakerAmountAsync(
+ 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(contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch)).to.be.rejectedWith(
+ ExchangeContractErrs.OrderCancelAmountZero,
+ );
+ });
+ it('should validate when orderTransactionOptions specify to validate', async () => {
+ return expect(
+ contractWrappers.exchange.batchCancelOrdersAsync(cancelBatch, {
+ shouldValidate: true,
+ }),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
+ });
+ it('should not validate when orderTransactionOptions specify not to validate', async () => {
+ return expect(
+ contractWrappers.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];
+ tokenUtils = new TokenUtils(tokens);
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ 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 = getOrderHashHex(signedOrder);
+ });
+ describe('#getUnavailableTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(
+ contractWrappers.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex),
+ ).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const unavailableValueT = await contractWrappers.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 contractWrappers.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(
+ contractWrappers.exchange.getFilledTakerAmountAsync(invalidOrderHashHex),
+ ).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const filledValueT = await contractWrappers.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 contractWrappers.exchange.getFilledTakerAmountAsync(orderHash);
+ expect(filledValueT).to.be.bignumber.equal(partialFillAmount);
+ });
+ });
+ describe('#getCancelledTakerAmountAsync', () => {
+ it('should throw if passed an invalid orderHash', async () => {
+ const invalidOrderHashHex = '0x123';
+ return expect(
+ contractWrappers.exchange.getCancelledTakerAmountAsync(invalidOrderHashHex),
+ ).to.be.rejected();
+ });
+ it('should return zero if passed a valid but non-existent orderHash', async () => {
+ const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync(
+ 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 contractWrappers.exchange.getCancelledTakerAmountAsync(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 contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmount);
+ const cancelledValueT = await contractWrappers.exchange.getCancelledTakerAmountAsync(orderHash);
+ expect(cancelledValueT).to.be.bignumber.equal(cancelAmount);
+ });
+ });
+ });
+ describe('#subscribe', () => {
+ 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] = tokenUtils.getDummyTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ beforeEach(async () => {
+ fillableAmount = new BigNumber(5);
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ });
+ afterEach(async () => {
+ contractWrappers.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 = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill);
+ },
+ );
+ contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callback);
+ await contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ takerTokenFillAmountInBaseUnits,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ })().catch(done);
+ });
+ it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => {
+ (async () => {
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => {
+ expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogCancel);
+ },
+ );
+ contractWrappers.exchange.subscribe(ExchangeEvents.LogCancel, indexFilterValues, callback);
+ await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits);
+ })().catch(done);
+ });
+ it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ },
+ );
+ contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled);
+
+ contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ expect(logEvent.log.event).to.be.equal(ExchangeEvents.LogFill);
+ },
+ );
+ contractWrappers.exchange.subscribe(ExchangeEvents.LogFill, indexFilterValues, callback);
+ await contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ takerTokenFillAmountInBaseUnits,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ },
+ );
+ const subscriptionToken = contractWrappers.exchange.subscribe(
+ ExchangeEvents.LogFill,
+ indexFilterValues,
+ callbackNeverToBeCalled,
+ );
+ contractWrappers.exchange.unsubscribe(subscriptionToken);
+ await contractWrappers.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.getDummyTokens();
+ 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 = getOrderHashHex(signedOrder);
+ const orderHashFromContract = await (contractWrappers.exchange as any)._getOrderHashHexUsingContractCallAsync(
+ signedOrder,
+ );
+ expect(orderHash).to.equal(orderHashFromContract);
+ });
+ });
+ describe('#getZRXTokenAddressAsync', () => {
+ it('gets the same token as is in token registry', () => {
+ const zrxAddress = contractWrappers.exchange.getZRXTokenAddress();
+ 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 blockRange: BlockRange = {
+ fromBlock: 0,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(async () => {
+ [, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ 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 contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ fillableAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const eventName = ExchangeEvents.LogFill;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.exchange.getLogsAsync(eventName, blockRange, 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 contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ fillableAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const differentEventName = ExchangeEvents.LogCancel;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.exchange.getLogsAsync(
+ differentEventName,
+ blockRange,
+ 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 contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ fillableAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+
+ const differentMakerAddress = userAddresses[2];
+ const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ differentMakerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ txHash = await contractWrappers.exchange.fillOrderAsync(
+ anotherSignedOrder,
+ fillableAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ takerAddress,
+ );
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+
+ const eventName = ExchangeEvents.LogFill;
+ const indexFilterValues = {
+ maker: differentMakerAddress,
+ };
+ const logs = await contractWrappers.exchange.getLogsAsync<LogFillContractEventArgs>(
+ eventName,
+ blockRange,
+ indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args.maker).to.be.equal(differentMakerAddress);
+ });
+ });
+ describe('#getOrderStateAsync', () => {
+ let maker: string;
+ let taker: string;
+ let makerToken: Token;
+ let takerToken: Token;
+ let signedOrder: SignedOrder;
+ let orderState: OrderState;
+ const fillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), constants.ZRX_DECIMALS);
+ before(async () => {
+ [, maker, taker] = userAddresses;
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ });
+ it('should report orderStateValid when order is fillable', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ orderState = await contractWrappers.exchange.getOrderStateAsync(signedOrder);
+ expect(orderState.isValid).to.be.true();
+ });
+ it('should report orderStateInvalid when maker allowance set to 0', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ await contractWrappers.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0));
+ orderState = await contractWrappers.exchange.getOrderStateAsync(signedOrder);
+ expect(orderState.isValid).to.be.false();
+ });
+ });
+}); // tslint:disable:max-file-line-count
diff --git a/packages/contract-wrappers/test/global_hooks.ts b/packages/contract-wrappers/test/global_hooks.ts
new file mode 100644
index 000000000..88f202761
--- /dev/null
+++ b/packages/contract-wrappers/test/global_hooks.ts
@@ -0,0 +1,18 @@
+import { devConstants } from '@0xproject/dev-utils';
+import { runMigrationsAsync } from '@0xproject/migrations';
+import * as path from 'path';
+
+import { constants } from './utils/constants';
+import { provider } from './utils/web3_wrapper';
+
+before('migrate contracts', async function() {
+ // HACK: Since the migrations take longer then our global mocha timeout limit
+ // we manually increase it for this before hook.
+ this.timeout(20000);
+ const txDefaults = {
+ gas: devConstants.GAS_ESTIMATE,
+ from: devConstants.TESTRPC_FIRST_ADDRESS,
+ };
+ const artifactsDir = `../migrations/artifacts/1.0.0`;
+ await runMigrationsAsync(provider, artifactsDir, txDefaults);
+});
diff --git a/packages/contract-wrappers/test/order_validation_test.ts b/packages/contract-wrappers/test/order_validation_test.ts
new file mode 100644
index 000000000..d28549ba2
--- /dev/null
+++ b/packages/contract-wrappers/test/order_validation_test.ts
@@ -0,0 +1,527 @@
+import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils';
+import { FillScenarios } from '@0xproject/fill-scenarios';
+import { OrderError } from '@0xproject/order-utils';
+import { BlockParamLiteral } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import * as Sinon from 'sinon';
+
+import { ContractWrappers, ContractWrappersError, ExchangeContractErrs, SignedOrder, Token } from '../src';
+import { TradeSide, TransferType } from '../src/types';
+import { ExchangeTransferSimulator } from '../src/utils/exchange_transfer_simulator';
+import { OrderValidationUtils } from '../src/utils/order_validation_utils';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { TokenUtils } from './utils/token_utils';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('OrderValidation', () => {
+ let contractWrappers: ContractWrappers;
+ 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;
+ const fillableAmount = new BigNumber(5);
+ const fillTakerAmount = new BigNumber(5);
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ exchangeContractAddress = contractWrappers.exchange.getContractAddress();
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(provider, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ 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 contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder);
+ });
+ it('should succeed if the maker is buying ZRX and has no ZRX balance', async () => {
+ const makerFee = new BigNumber(2);
+ const takerFee = new BigNumber(2);
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress,
+ zrxTokenAddress,
+ makerFee,
+ takerFee,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ feeRecipient,
+ );
+ const zrxMakerBalance = await contractWrappers.token.getBalanceAsync(zrxTokenAddress, makerAddress);
+ await contractWrappers.token.transferAsync(zrxTokenAddress, makerAddress, takerAddress, zrxMakerBalance);
+ await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder);
+ });
+ it('should succeed if the maker is buying ZRX and has no ZRX balance and there is no specified taker', async () => {
+ const makerFee = new BigNumber(2);
+ const takerFee = new BigNumber(2);
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress,
+ zrxTokenAddress,
+ makerFee,
+ takerFee,
+ makerAddress,
+ constants.NULL_ADDRESS,
+ fillableAmount,
+ feeRecipient,
+ );
+ const zrxMakerBalance = await contractWrappers.token.getBalanceAsync(zrxTokenAddress, makerAddress);
+ await contractWrappers.token.transferAsync(zrxTokenAddress, makerAddress, takerAddress, zrxMakerBalance);
+ await contractWrappers.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 contractWrappers.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 contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(contractWrappers.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(contractWrappers.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(
+ contractWrappers.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 = 28 - signedOrder.ecSignature.v + 27;
+ return expect(
+ contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync(
+ signedOrder,
+ fillableAmount,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(OrderError.InvalidSignature);
+ });
+ it('should throw when the order is fully filled or cancelled', async () => {
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ await contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(
+ contractWrappers.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(
+ contractWrappers.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(
+ contractWrappers.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(
+ contractWrappers.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 contractWrappers.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference);
+ await contractWrappers.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount);
+ await contractWrappers.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference);
+ await contractWrappers.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount);
+
+ return expect(
+ contractWrappers.exchange.validateFillOrKillOrderThrowIfInvalidAsync(
+ signedOrder,
+ tooLargeFillAmount,
+ takerAddress,
+ ),
+ ).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount);
+ });
+ });
+ describe('validateCancelOrderAndThrowIfInvalidAsync', () => {
+ let signedOrder: SignedOrder;
+ const cancelAmount = new BigNumber(3);
+ beforeEach(async () => {
+ [coinbase, makerAddress, takerAddress] = userAddresses;
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ );
+ });
+ it('should throw when cancel amount is zero', async () => {
+ const zeroCancelAmount = new BigNumber(0);
+ return expect(
+ contractWrappers.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,
+ );
+ return expect(
+ contractWrappers.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount),
+ ).to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired);
+ });
+ it('should throw when order is already cancelled or filled', async () => {
+ await contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ return expect(
+ contractWrappers.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(contractWrappers.token, BlockParamLiteral.Latest);
+ 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,
+ constants.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,
+ constants.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);
+ });
+ });
+}); // tslint:disable-line:max-file-line-count
diff --git a/packages/contract-wrappers/test/subscription_test.ts b/packages/contract-wrappers/test/subscription_test.ts
new file mode 100644
index 000000000..64262ad9c
--- /dev/null
+++ b/packages/contract-wrappers/test/subscription_test.ts
@@ -0,0 +1,95 @@
+import { BlockchainLifecycle, callbackErrorReporter, devConstants } from '@0xproject/dev-utils';
+import { DoneCallback } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+import 'mocha';
+import * as Sinon from 'sinon';
+
+import { ApprovalContractEventArgs, ContractWrappers, DecodedLogEvent, Token, TokenEvents } from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('SubscriptionTest', () => {
+ let contractWrappers: ContractWrappers;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let coinbase: string;
+ let addressWithoutFunds: string;
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ coinbase = userAddresses[0];
+ addressWithoutFunds = userAddresses[1];
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ let tokenAddress: string;
+ const allowanceAmount = new BigNumber(42);
+ let stubs: Sinon.SinonStub[] = [];
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(() => {
+ contractWrappers.token.unsubscribeAll();
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ });
+ it('Should receive the Error when an error occurs while fetching the block', (done: DoneCallback) => {
+ (async () => {
+ const errMsg = 'Error fetching block';
+ const callback = callbackErrorReporter.assertNodeCallbackError(done, errMsg);
+ stubs = [Sinon.stub((contractWrappers as any)._web3Wrapper, 'getBlockAsync').throws(new Error(errMsg))];
+ contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await contractWrappers.token.setAllowanceAsync(
+ tokenAddress,
+ coinbase,
+ addressWithoutFunds,
+ allowanceAmount,
+ );
+ })().catch(done);
+ });
+ it('Should receive the Error when an error occurs while reconciling the new block', (done: DoneCallback) => {
+ (async () => {
+ const errMsg = 'Error fetching logs';
+ const callback = callbackErrorReporter.assertNodeCallbackError(done, errMsg);
+ stubs = [Sinon.stub((contractWrappers as any)._web3Wrapper, 'getLogsAsync').throws(new Error(errMsg))];
+ contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await contractWrappers.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 | null, logEvent?: DecodedLogEvent<ApprovalContractEventArgs>) => _.noop;
+ contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ stubs = [
+ Sinon.stub((contractWrappers as any)._web3Wrapper, 'getBlockAsync').throws(
+ new Error('JSON RPC error'),
+ ),
+ ];
+ contractWrappers.token.unsubscribeAll();
+ done();
+ })().catch(done);
+ });
+ });
+});
diff --git a/packages/contract-wrappers/test/token_registry_wrapper_test.ts b/packages/contract-wrappers/test/token_registry_wrapper_test.ts
new file mode 100644
index 000000000..a21efed21
--- /dev/null
+++ b/packages/contract-wrappers/test/token_registry_wrapper_test.ts
@@ -0,0 +1,136 @@
+import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils';
+import { schemas, SchemaValidator } from '@0xproject/json-schemas';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import 'mocha';
+
+import { ContractWrappers, Token } from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+const TOKEN_REGISTRY_SIZE_AFTER_MIGRATION = 7;
+
+describe('TokenRegistryWrapper', () => {
+ let contractWrappers: ContractWrappers;
+ 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';
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ tokens = await contractWrappers.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 contractWrappers.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(constants.NULL_ADDRESS);
+ });
+ });
+ });
+ describe('#getTokenAddressBySymbol', () => {
+ it('should return correct address for a token in the registry', async () => {
+ const tokenAddress = await contractWrappers.tokenRegistry.getTokenAddressBySymbolIfExistsAsync(
+ registeredSymbol,
+ );
+ expect(tokenAddress).to.be.equal(tokenAddressBySymbol[registeredSymbol]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const tokenAddress = await contractWrappers.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 contractWrappers.tokenRegistry.getTokenAddressByNameIfExistsAsync(
+ registeredName,
+ );
+ expect(tokenAddress).to.be.equal(tokenAddressByName[registeredName]);
+ });
+ it('should return undefined for a token out of registry', async () => {
+ const tokenAddress = await contractWrappers.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 contractWrappers.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 contractWrappers.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 contractWrappers.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 contractWrappers.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 contractWrappers.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 contractWrappers.tokenRegistry.getTokenIfExistsAsync(unregisteredTokenAddress);
+ expect(tokenIfExists).to.be.undefined();
+ });
+ });
+});
diff --git a/packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts b/packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts
new file mode 100644
index 000000000..0b66985aa
--- /dev/null
+++ b/packages/contract-wrappers/test/token_transfer_proxy_wrapper_test.ts
@@ -0,0 +1,35 @@
+import * as chai from 'chai';
+
+import { ContractWrappers } from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { provider } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('TokenTransferProxyWrapper', () => {
+ let contractWrappers: ContractWrappers;
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ });
+ describe('#isAuthorizedAsync', () => {
+ it('should return false if the address is not authorized', async () => {
+ const isAuthorized = await contractWrappers.proxy.isAuthorizedAsync(constants.NULL_ADDRESS);
+ expect(isAuthorized).to.be.false();
+ });
+ });
+ describe('#getAuthorizedAddressesAsync', () => {
+ it('should return the list of authorized addresses', async () => {
+ const authorizedAddresses = await contractWrappers.proxy.getAuthorizedAddressesAsync();
+ for (const authorizedAddress of authorizedAddresses) {
+ const isAuthorized = await contractWrappers.proxy.isAuthorizedAsync(authorizedAddress);
+ expect(isAuthorized).to.be.true();
+ }
+ });
+ });
+});
diff --git a/packages/contract-wrappers/test/token_wrapper_test.ts b/packages/contract-wrappers/test/token_wrapper_test.ts
new file mode 100644
index 000000000..053901c85
--- /dev/null
+++ b/packages/contract-wrappers/test/token_wrapper_test.ts
@@ -0,0 +1,605 @@
+import { BlockchainLifecycle, callbackErrorReporter, devConstants } from '@0xproject/dev-utils';
+import { EmptyWalletSubprovider } from '@0xproject/subproviders';
+import { DoneCallback, Provider } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import 'mocha';
+import Web3ProviderEngine = require('web3-provider-engine');
+
+import {
+ ApprovalContractEventArgs,
+ BlockParamLiteral,
+ BlockRange,
+ ContractWrappers,
+ ContractWrappersError,
+ DecodedLogEvent,
+ Token,
+ TokenEvents,
+ TransferContractEventArgs,
+} from '../src';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { TokenUtils } from './utils/token_utils';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('TokenWrapper', () => {
+ let contractWrappers: ContractWrappers;
+ let userAddresses: string[];
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let coinbase: string;
+ let addressWithoutFunds: string;
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ tokens = await contractWrappers.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 contractWrappers.token.getBalanceAsync(token.address, toAddress);
+ expect(preBalance).to.be.bignumber.equal(0);
+ await contractWrappers.token.transferAsync(token.address, fromAddress, toAddress, transferAmount);
+ const postBalance = await contractWrappers.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(
+ contractWrappers.token.transferAsync(token.address, fromAddress, toAddress, transferAmount),
+ ).to.be.rejectedWith(ContractWrappersError.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(
+ contractWrappers.token.transferAsync(nonExistentTokenAddress, fromAddress, toAddress, transferAmount),
+ ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist);
+ });
+ });
+ 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 contractWrappers.token.getBalanceAsync(token.address, fromAddress);
+ expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount);
+
+ const fromAddressAllowance = await contractWrappers.token.getAllowanceAsync(
+ token.address,
+ fromAddress,
+ toAddress,
+ );
+ expect(fromAddressAllowance).to.be.bignumber.equal(0);
+
+ return expect(
+ contractWrappers.token.transferFromAsync(
+ token.address,
+ fromAddress,
+ toAddress,
+ senderAddress,
+ transferAmount,
+ ),
+ ).to.be.rejectedWith(ContractWrappersError.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 contractWrappers.token.setAllowanceAsync(token.address, fromAddress, toAddress, transferAmount);
+
+ return expect(
+ contractWrappers.token.transferFromAsync(
+ token.address,
+ fromAddress,
+ toAddress,
+ senderAddress,
+ transferAmount,
+ ),
+ ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer);
+ });
+ it('should fail to transfer tokens if fromAddress has insufficient balance', async () => {
+ const fromAddress = addressWithoutFunds;
+ const transferAmount = new BigNumber(42);
+
+ const fromAddressBalance = await contractWrappers.token.getBalanceAsync(token.address, fromAddress);
+ expect(fromAddressBalance).to.be.bignumber.equal(0);
+
+ await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
+ const fromAddressAllowance = await contractWrappers.token.getAllowanceAsync(
+ token.address,
+ fromAddress,
+ senderAddress,
+ );
+ expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount);
+
+ return expect(
+ contractWrappers.token.transferFromAsync(
+ token.address,
+ fromAddress,
+ toAddress,
+ senderAddress,
+ transferAmount,
+ ),
+ ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer);
+ });
+ it('should successfully transfer tokens', async () => {
+ const fromAddress = coinbase;
+
+ const preBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress);
+ expect(preBalance).to.be.bignumber.equal(0);
+
+ const transferAmount = new BigNumber(42);
+ await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
+
+ await contractWrappers.token.transferFromAsync(
+ token.address,
+ fromAddress,
+ toAddress,
+ senderAddress,
+ transferAmount,
+ );
+ const postBalance = await contractWrappers.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(
+ contractWrappers.token.transferFromAsync(
+ nonExistentTokenAddress,
+ fromAddress,
+ toAddress,
+ senderAddress,
+ new BigNumber(42),
+ ),
+ ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist);
+ });
+ });
+ describe('#getBalanceAsync', () => {
+ describe('With provider with accounts', () => {
+ it('should return the balance for an existing ERC20 token', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const balance = await contractWrappers.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(
+ contractWrappers.token.getBalanceAsync(nonExistentTokenAddress, ownerAddress),
+ ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist);
+ });
+ it('should return a balance of 0 for a non-existent owner address', async () => {
+ const token = tokens[0];
+ const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593';
+ const balance = await contractWrappers.token.getBalanceAsync(token.address, nonExistentOwner);
+ const expectedBalance = new BigNumber(0);
+ return expect(balance).to.be.bignumber.equal(expectedBalance);
+ });
+ });
+ describe('With provider without accounts', () => {
+ let zeroExContractWithoutAccounts: ContractWrappers;
+ before(async () => {
+ const hasAddresses = false;
+ const emptyWalletProvider = addEmptyWalletSubprovider(provider);
+ zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config);
+ });
+ it('should return balance even when called with provider instance without addresses', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const balance = await zeroExContractWithoutAccounts.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 contractWrappers.token.getAllowanceAsync(
+ token.address,
+ ownerAddress,
+ spenderAddress,
+ );
+ const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
+ expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
+
+ const amountInBaseUnits = new BigNumber(50);
+ await contractWrappers.token.setAllowanceAsync(
+ token.address,
+ ownerAddress,
+ spenderAddress,
+ amountInBaseUnits,
+ );
+
+ const allowanceAfterSet = await contractWrappers.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 contractWrappers.token.setUnlimitedAllowanceAsync(token.address, ownerAddress, spenderAddress);
+ const allowance = await contractWrappers.token.getAllowanceAsync(
+ token.address,
+ ownerAddress,
+ spenderAddress,
+ );
+ return expect(allowance).to.be.bignumber.equal(contractWrappers.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 contractWrappers.token.setAllowanceAsync(
+ zrx.address,
+ coinbase,
+ userWithNormalAllowance,
+ transferAmount,
+ );
+ await contractWrappers.token.setUnlimitedAllowanceAsync(zrx.address, coinbase, userWithUnlimitedAllowance);
+
+ const initBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance);
+ const initBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync(
+ userWithUnlimitedAllowance,
+ );
+
+ await contractWrappers.token.transferFromAsync(
+ zrx.address,
+ coinbase,
+ userWithNormalAllowance,
+ userWithNormalAllowance,
+ transferAmount,
+ );
+ await contractWrappers.token.transferFromAsync(
+ zrx.address,
+ coinbase,
+ userWithUnlimitedAllowance,
+ userWithUnlimitedAllowance,
+ transferAmount,
+ );
+
+ const finalBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance);
+ const finalBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync(
+ 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 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 contractWrappers.token.setAllowanceAsync(
+ token.address,
+ ownerAddress,
+ spenderAddress,
+ amountInBaseUnits,
+ );
+
+ const allowance = await contractWrappers.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 contractWrappers.token.getAllowanceAsync(
+ token.address,
+ ownerAddress,
+ spenderAddress,
+ );
+ const expectedAllowance = new BigNumber(0);
+ return expect(allowance).to.be.bignumber.equal(expectedAllowance);
+ });
+ });
+ describe('With provider without accounts', () => {
+ let zeroExContractWithoutAccounts: ContractWrappers;
+ before(async () => {
+ const hasAddresses = false;
+ const emptyWalletProvider = addEmptyWalletSubprovider(provider);
+ zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config);
+ });
+ it('should get the proxy allowance', async () => {
+ const token = tokens[0];
+ const ownerAddress = coinbase;
+ const spenderAddress = addressWithoutFunds;
+
+ const amountInBaseUnits = new BigNumber(50);
+ await contractWrappers.token.setAllowanceAsync(
+ token.address,
+ ownerAddress,
+ spenderAddress,
+ amountInBaseUnits,
+ );
+
+ const allowance = await zeroExContractWithoutAccounts.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 contractWrappers.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
+
+ const allowance = await contractWrappers.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 contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
+ expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
+
+ const amountInBaseUnits = new BigNumber(50);
+ await contractWrappers.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
+
+ const allowanceAfterSet = await contractWrappers.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 contractWrappers.token.setUnlimitedProxyAllowanceAsync(token.address, ownerAddress);
+ const allowance = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress);
+ return expect(allowance).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+ describe('#subscribe', () => {
+ const indexFilterValues = {};
+ let tokenAddress: string;
+ const transferAmount = new BigNumber(42);
+ const allowanceAmount = new BigNumber(42);
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ });
+ afterEach(() => {
+ contractWrappers.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 = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<TransferContractEventArgs>) => {
+ expect(logEvent.isRemoved).to.be.false();
+ expect(logEvent.log.logIndex).to.be.equal(0);
+ expect(logEvent.log.transactionIndex).to.be.equal(0);
+ expect(logEvent.log.blockNumber).to.be.a('number');
+ const args = logEvent.log.args;
+ expect(args._from).to.be.equal(coinbase);
+ expect(args._to).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(transferAmount);
+ },
+ );
+ contractWrappers.token.subscribe(tokenAddress, TokenEvents.Transfer, indexFilterValues, callback);
+ await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => {
+ (async () => {
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ expect(logEvent).to.not.be.undefined();
+ expect(logEvent.isRemoved).to.be.false();
+ const args = logEvent.log.args;
+ expect(args._owner).to.be.equal(coinbase);
+ expect(args._spender).to.be.equal(addressWithoutFunds);
+ expect(args._value).to.be.bignumber.equal(allowanceAmount);
+ },
+ );
+ contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
+ await contractWrappers.token.setAllowanceAsync(
+ tokenAddress,
+ coinbase,
+ addressWithoutFunds,
+ allowanceAmount,
+ );
+ })().catch(done);
+ });
+ it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ },
+ );
+ contractWrappers.token.subscribe(
+ tokenAddress,
+ TokenEvents.Transfer,
+ indexFilterValues,
+ callbackNeverToBeCalled,
+ );
+ const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)();
+ contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID);
+ contractWrappers.token.subscribe(
+ tokenAddress,
+ TokenEvents.Transfer,
+ indexFilterValues,
+ callbackToBeCalled,
+ );
+ await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ })().catch(done);
+ });
+ it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
+ (async () => {
+ const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(
+ (logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
+ done(new Error('Expected this subscription to have been cancelled'));
+ },
+ );
+ const subscriptionToken = contractWrappers.token.subscribe(
+ tokenAddress,
+ TokenEvents.Transfer,
+ indexFilterValues,
+ callbackNeverToBeCalled,
+ );
+ contractWrappers.token.unsubscribe(subscriptionToken);
+ await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
+ done();
+ })().catch(done);
+ });
+ });
+ describe('#getLogsAsync', () => {
+ let tokenAddress: string;
+ let tokenTransferProxyAddress: string;
+ const blockRange: BlockRange = {
+ fromBlock: 0,
+ toBlock: BlockParamLiteral.Latest,
+ };
+ let txHash: string;
+ before(() => {
+ const token = tokens[0];
+ tokenAddress = token.address;
+ tokenTransferProxyAddress = contractWrappers.proxy.getContractAddress();
+ });
+ it('should get logs with decoded args emitted by Approval', async () => {
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const eventName = TokenEvents.Approval;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.token.getLogsAsync<ApprovalContractEventArgs>(
+ tokenAddress,
+ eventName,
+ blockRange,
+ 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(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ it('should only get the logs with the correct event name', async () => {
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const differentEventName = TokenEvents.Transfer;
+ const indexFilterValues = {};
+ const logs = await contractWrappers.token.getLogsAsync(
+ tokenAddress,
+ differentEventName,
+ blockRange,
+ indexFilterValues,
+ );
+ expect(logs).to.have.length(0);
+ });
+ it('should only get the logs with the correct indexed fields', async () => {
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, addressWithoutFunds);
+ await web3Wrapper.awaitTransactionMinedAsync(txHash);
+ const eventName = TokenEvents.Approval;
+ const indexFilterValues = {
+ _owner: coinbase,
+ };
+ const logs = await contractWrappers.token.getLogsAsync<ApprovalContractEventArgs>(
+ tokenAddress,
+ eventName,
+ blockRange,
+ indexFilterValues,
+ );
+ expect(logs).to.have.length(1);
+ const args = logs[0].args;
+ expect(args._owner).to.be.equal(coinbase);
+ });
+ });
+});
+// tslint:disable:max-file-line-count
+
+function addEmptyWalletSubprovider(p: Provider): Provider {
+ const providerEngine = new Web3ProviderEngine();
+ providerEngine.addProvider(new EmptyWalletSubprovider());
+ const currentSubproviders = (p as any)._providers;
+ for (const subprovider of currentSubproviders) {
+ providerEngine.addProvider(subprovider);
+ }
+ providerEngine.start();
+ return providerEngine;
+}
diff --git a/packages/contract-wrappers/test/utils/chai_setup.ts b/packages/contract-wrappers/test/utils/chai_setup.ts
new file mode 100644
index 000000000..078edd309
--- /dev/null
+++ b/packages/contract-wrappers/test/utils/chai_setup.ts
@@ -0,0 +1,13 @@
+import * as chai from 'chai';
+import chaiAsPromised = require('chai-as-promised');
+import ChaiBigNumber = require('chai-bignumber');
+import * as dirtyChai from 'dirty-chai';
+
+export const chaiSetup = {
+ configure() {
+ chai.config.includeStack = true;
+ chai.use(ChaiBigNumber());
+ chai.use(dirtyChai);
+ chai.use(chaiAsPromised);
+ },
+};
diff --git a/packages/contract-wrappers/test/utils/constants.ts b/packages/contract-wrappers/test/utils/constants.ts
new file mode 100644
index 000000000..cf030259c
--- /dev/null
+++ b/packages/contract-wrappers/test/utils/constants.ts
@@ -0,0 +1,9 @@
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ ROPSTEN_NETWORK_ID: 3,
+ KOVAN_NETWORK_ID: 42,
+ TESTRPC_NETWORK_ID: 50,
+ KOVAN_RPC_URL: 'https://kovan.infura.io/',
+ ROPSTEN_RPC_URL: 'https://ropsten.infura.io/',
+ ZRX_DECIMALS: 18,
+};
diff --git a/packages/contract-wrappers/test/utils/token_utils.ts b/packages/contract-wrappers/test/utils/token_utils.ts
new file mode 100644
index 000000000..fe85de085
--- /dev/null
+++ b/packages/contract-wrappers/test/utils/token_utils.ts
@@ -0,0 +1,33 @@
+import * as _ from 'lodash';
+
+import { InternalContractWrappersError, Token } from '../../src/types';
+
+const PROTOCOL_TOKEN_SYMBOL = 'ZRX';
+const WETH_TOKEN_SYMBOL = 'WETH';
+
+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(InternalContractWrappersError.ZrxNotInTokenRegistry);
+ }
+ return zrxToken;
+ }
+ public getWethTokenOrThrow(): Token {
+ const wethToken = _.find(this._tokens, { symbol: WETH_TOKEN_SYMBOL });
+ if (_.isUndefined(wethToken)) {
+ throw new Error(InternalContractWrappersError.WethNotInTokenRegistry);
+ }
+ return wethToken;
+ }
+ public getDummyTokens(): Token[] {
+ const dummyTokens = _.filter(this._tokens, token => {
+ return !_.includes([PROTOCOL_TOKEN_SYMBOL, WETH_TOKEN_SYMBOL], token.symbol);
+ });
+ return dummyTokens;
+ }
+}
diff --git a/packages/contract-wrappers/test/utils/web3_wrapper.ts b/packages/contract-wrappers/test/utils/web3_wrapper.ts
new file mode 100644
index 000000000..b0ccfa546
--- /dev/null
+++ b/packages/contract-wrappers/test/utils/web3_wrapper.ts
@@ -0,0 +1,9 @@
+import { devConstants, web3Factory } from '@0xproject/dev-utils';
+import { Provider } from '@0xproject/types';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+
+const web3 = web3Factory.create({ shouldUseInProcessGanache: true });
+const provider: Provider = web3.currentProvider;
+const web3Wrapper = new Web3Wrapper(web3.currentProvider);
+
+export { provider, web3Wrapper };
diff --git a/packages/contract-wrappers/tsconfig.json b/packages/contract-wrappers/tsconfig.json
new file mode 100644
index 000000000..e35816553
--- /dev/null
+++ b/packages/contract-wrappers/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib"
+ },
+ "include": ["./src/**/*", "./test/**/*"]
+}
diff --git a/packages/contract-wrappers/tslint.json b/packages/contract-wrappers/tslint.json
new file mode 100644
index 000000000..ffaefe83a
--- /dev/null
+++ b/packages/contract-wrappers/tslint.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["@0xproject/tslint-config"]
+}