aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-watcher
diff options
context:
space:
mode:
Diffstat (limited to 'packages/order-watcher')
-rw-r--r--packages/order-watcher/.npmignore10
-rw-r--r--packages/order-watcher/CHANGELOG.json11
-rw-r--r--packages/order-watcher/README.md91
-rw-r--r--packages/order-watcher/coverage/.gitkeep0
-rw-r--r--packages/order-watcher/package.json94
-rw-r--r--packages/order-watcher/src/artifacts.ts18
-rw-r--r--packages/order-watcher/src/compact_artifacts/DummyToken.json22
-rw-r--r--packages/order-watcher/src/compact_artifacts/EtherToken.json287
-rw-r--r--packages/order-watcher/src/compact_artifacts/Exchange.json610
-rw-r--r--packages/order-watcher/src/compact_artifacts/Token.json172
-rw-r--r--packages/order-watcher/src/compact_artifacts/TokenRegistry.json547
-rw-r--r--packages/order-watcher/src/compact_artifacts/TokenTransferProxy.json187
-rw-r--r--packages/order-watcher/src/compact_artifacts/ZRX.json20
-rw-r--r--packages/order-watcher/src/globals.d.ts6
-rw-r--r--packages/order-watcher/src/index.ts7
-rw-r--r--packages/order-watcher/src/monorepo_scripts/postpublish.ts8
-rw-r--r--packages/order-watcher/src/order_watcher/event_watcher.ts99
-rw-r--r--packages/order-watcher/src/order_watcher/expiration_watcher.ts87
-rw-r--r--packages/order-watcher/src/order_watcher/order_watcher.ts393
-rw-r--r--packages/order-watcher/src/types.ts46
-rw-r--r--packages/order-watcher/src/utils/assert.ts19
-rw-r--r--packages/order-watcher/src/utils/utils.ts13
-rw-r--r--packages/order-watcher/test/event_watcher_test.ts126
-rw-r--r--packages/order-watcher/test/expiration_watcher_test.ts200
-rw-r--r--packages/order-watcher/test/global_hooks.ts18
-rw-r--r--packages/order-watcher/test/order_watcher_test.ts574
-rw-r--r--packages/order-watcher/test/remaining_fillable_calculator_test.ts234
-rw-r--r--packages/order-watcher/test/utils/chai_setup.ts13
-rw-r--r--packages/order-watcher/test/utils/constants.ts5
-rw-r--r--packages/order-watcher/test/utils/token_utils.ts34
-rw-r--r--packages/order-watcher/test/utils/web3_wrapper.ts9
-rw-r--r--packages/order-watcher/tsconfig.json7
-rw-r--r--packages/order-watcher/tslint.json3
33 files changed, 3970 insertions, 0 deletions
diff --git a/packages/order-watcher/.npmignore b/packages/order-watcher/.npmignore
new file mode 100644
index 000000000..c5be1b302
--- /dev/null
+++ b/packages/order-watcher/.npmignore
@@ -0,0 +1,10 @@
+.*
+tsconfig.json
+webpack.config.js
+yarn-error.log
+test/
+/src/
+/_bundles/
+/generated_docs/
+/scripts/
+/lib/src/monorepo_scripts/
diff --git a/packages/order-watcher/CHANGELOG.json b/packages/order-watcher/CHANGELOG.json
new file mode 100644
index 000000000..87f40bcb7
--- /dev/null
+++ b/packages/order-watcher/CHANGELOG.json
@@ -0,0 +1,11 @@
+[
+ {
+ "version": "0.0.1",
+ "changes": [
+ {
+ "note": "Moved OrderWatcher out of 0x.js package",
+ "pr": 579
+ }
+ ]
+ }
+]
diff --git a/packages/order-watcher/README.md b/packages/order-watcher/README.md
new file mode 100644
index 000000000..d461dca8d
--- /dev/null
+++ b/packages/order-watcher/README.md
@@ -0,0 +1,91 @@
+## OrderWatcher
+
+An order watcher daemon that watches for order validity.
+
+#### Read the wiki [article](https://0xproject.com/wiki#0x-OrderWatcher).
+
+## Installation
+
+**Install**
+
+```bash
+npm install @0xproject/order-watcher --save
+```
+
+**Import**
+
+```javascript
+import { OrderWatcher } from '@0xproject/order-watcher';
+```
+
+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/order-watcher/coverage/.gitkeep b/packages/order-watcher/coverage/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/order-watcher/coverage/.gitkeep
diff --git a/packages/order-watcher/package.json b/packages/order-watcher/package.json
new file mode 100644
index 000000000..39162fd1e
--- /dev/null
+++ b/packages/order-watcher/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "@0xproject/order-watcher",
+ "version": "0.0.1",
+ "description": "An order watcher daemon that watches for order validity",
+ "keywords": [
+ "0x",
+ "0xproject",
+ "ethereum",
+ "exchange",
+ "orderbook"
+ ],
+ "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).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers && prettier --write 'src/generated_contract_wrappers/**.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/generated_contract_wrappers",
+ "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/order-watcher/_bundles/index.js",
+ "packages/order-watcher/_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/tslint-config": "^0.4.17",
+ "@types/bintrees": "^1.0.2",
+ "@types/lodash": "4.14.104",
+ "@types/mocha": "^2.2.42",
+ "@types/node": "^8.0.53",
+ "@types/sinon": "^2.2.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",
+ "json-loader": "^0.5.4",
+ "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"
+ },
+ "dependencies": {
+ "@0xproject/contract-wrappers": "^0.0.1",
+ "@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",
+ "bintrees": "^1.0.2",
+ "ethers": "^3.0.15",
+ "lodash": "^4.17.4"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/order-watcher/src/artifacts.ts b/packages/order-watcher/src/artifacts.ts
new file mode 100644
index 000000000..13587984c
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/DummyToken.json b/packages/order-watcher/src/compact_artifacts/DummyToken.json
new file mode 100644
index 000000000..f64a8cd3d
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/EtherToken.json b/packages/order-watcher/src/compact_artifacts/EtherToken.json
new file mode 100644
index 000000000..26cca57cd
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/Exchange.json b/packages/order-watcher/src/compact_artifacts/Exchange.json
new file mode 100644
index 000000000..af8db7360
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/Token.json b/packages/order-watcher/src/compact_artifacts/Token.json
new file mode 100644
index 000000000..3b5a86ae0
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/TokenRegistry.json b/packages/order-watcher/src/compact_artifacts/TokenRegistry.json
new file mode 100644
index 000000000..0f583628c
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/TokenTransferProxy.json b/packages/order-watcher/src/compact_artifacts/TokenTransferProxy.json
new file mode 100644
index 000000000..8cf551ddb
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/compact_artifacts/ZRX.json b/packages/order-watcher/src/compact_artifacts/ZRX.json
new file mode 100644
index 000000000..e40b8f268
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/globals.d.ts b/packages/order-watcher/src/globals.d.ts
new file mode 100644
index 000000000..94e63a32d
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/index.ts b/packages/order-watcher/src/index.ts
new file mode 100644
index 000000000..390003b1d
--- /dev/null
+++ b/packages/order-watcher/src/index.ts
@@ -0,0 +1,7 @@
+export { OrderWatcher } from './order_watcher/order_watcher';
+
+export { OrderStateValid, OrderStateInvalid, OrderState } from '@0xproject/types';
+
+export { OnOrderStateChangeCallback, OrderWatcherConfig } from './types';
+
+export { BlockParamLiteral, BlockParam, Order, Provider, SignedOrder } from '@0xproject/types';
diff --git a/packages/order-watcher/src/monorepo_scripts/postpublish.ts b/packages/order-watcher/src/monorepo_scripts/postpublish.ts
new file mode 100644
index 000000000..dcb99d0f7
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/src/order_watcher/event_watcher.ts b/packages/order-watcher/src/order_watcher/event_watcher.ts
new file mode 100644
index 000000000..f39d3bf0e
--- /dev/null
+++ b/packages/order-watcher/src/order_watcher/event_watcher.ts
@@ -0,0 +1,99 @@
+import { BlockParamLiteral, LogEntry } from '@0xproject/types';
+import { intervalUtils } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { EventWatcherCallback, OrderWatcherError } from '../types';
+import { assert } from '../utils/assert';
+
+const DEFAULT_EVENT_POLLING_INTERVAL_MS = 200;
+
+enum LogEventState {
+ Removed,
+ Added,
+}
+
+/**
+ * The EventWatcher watches for blockchain events at the specified block confirmation
+ * depth.
+ */
+export class EventWatcher {
+ private _web3Wrapper: Web3Wrapper;
+ private _pollingIntervalMs: number;
+ private _intervalIdIfExists?: NodeJS.Timer;
+ private _lastEvents: LogEntry[] = [];
+ private _stateLayer: BlockParamLiteral;
+ constructor(
+ web3Wrapper: Web3Wrapper,
+ pollingIntervalIfExistsMs: undefined | number,
+ stateLayer: BlockParamLiteral = BlockParamLiteral.Latest,
+ ) {
+ this._web3Wrapper = web3Wrapper;
+ this._stateLayer = stateLayer;
+ this._pollingIntervalMs = _.isUndefined(pollingIntervalIfExistsMs)
+ ? DEFAULT_EVENT_POLLING_INTERVAL_MS
+ : pollingIntervalIfExistsMs;
+ }
+ public subscribe(callback: EventWatcherCallback): void {
+ assert.isFunction('callback', callback);
+ if (!_.isUndefined(this._intervalIdIfExists)) {
+ throw new Error(OrderWatcherError.SubscriptionAlreadyPresent);
+ }
+ this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval(
+ this._pollForBlockchainEventsAsync.bind(this, callback),
+ this._pollingIntervalMs,
+ (err: Error) => {
+ this.unsubscribe();
+ callback(err);
+ },
+ );
+ }
+ public unsubscribe(): void {
+ this._lastEvents = [];
+ if (!_.isUndefined(this._intervalIdIfExists)) {
+ intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists);
+ delete this._intervalIdIfExists;
+ }
+ }
+ private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> {
+ const pendingEvents = await this._getEventsAsync();
+ if (_.isUndefined(pendingEvents)) {
+ // HACK: This should never happen, but happens frequently on CI due to a ganache bug
+ return;
+ }
+ if (pendingEvents.length === 0) {
+ // HACK: Sometimes when node rebuilds the pending block we get back the empty result.
+ // We don't want to emit a lot of removal events and bring them back after a couple of miliseconds,
+ // that's why we just ignore those cases.
+ return;
+ }
+ const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify);
+ const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify);
+ await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback);
+ await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback);
+ this._lastEvents = pendingEvents;
+ }
+ private async _getEventsAsync(): Promise<LogEntry[]> {
+ const eventFilter = {
+ fromBlock: this._stateLayer,
+ toBlock: this._stateLayer,
+ };
+ const events = await this._web3Wrapper.getLogsAsync(eventFilter);
+ return events;
+ }
+ private async _emitDifferencesAsync(
+ logs: LogEntry[],
+ logEventState: LogEventState,
+ callback: EventWatcherCallback,
+ ): Promise<void> {
+ for (const log of logs) {
+ const logEvent = {
+ removed: logEventState === LogEventState.Removed,
+ ...log,
+ };
+ if (!_.isUndefined(this._intervalIdIfExists)) {
+ callback(null, logEvent);
+ }
+ }
+ }
+}
diff --git a/packages/order-watcher/src/order_watcher/expiration_watcher.ts b/packages/order-watcher/src/order_watcher/expiration_watcher.ts
new file mode 100644
index 000000000..ec2c1ec35
--- /dev/null
+++ b/packages/order-watcher/src/order_watcher/expiration_watcher.ts
@@ -0,0 +1,87 @@
+import { BigNumber, intervalUtils } from '@0xproject/utils';
+import { RBTree } from 'bintrees';
+import * as _ from 'lodash';
+
+import { OrderWatcherError } from '../types';
+import { utils } from '../utils/utils';
+
+const DEFAULT_EXPIRATION_MARGIN_MS = 0;
+const DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS = 50;
+
+/**
+ * This class includes the functionality to detect expired orders.
+ * It stores them in a min heap by expiration time and checks for expired ones every `orderExpirationCheckingIntervalMs`
+ */
+export class ExpirationWatcher {
+ private _orderHashByExpirationRBTree: RBTree<string>;
+ private _expiration: { [orderHash: string]: BigNumber } = {};
+ private _orderExpirationCheckingIntervalMs: number;
+ private _expirationMarginMs: number;
+ private _orderExpirationCheckingIntervalIdIfExists?: NodeJS.Timer;
+ constructor(expirationMarginIfExistsMs?: number, orderExpirationCheckingIntervalIfExistsMs?: number) {
+ this._expirationMarginMs = expirationMarginIfExistsMs || DEFAULT_EXPIRATION_MARGIN_MS;
+ this._orderExpirationCheckingIntervalMs =
+ expirationMarginIfExistsMs || DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS;
+ const comparator = (lhsOrderHash: string, rhsOrderHash: string) => {
+ const lhsExpiration = this._expiration[lhsOrderHash].toNumber();
+ const rhsExpiration = this._expiration[rhsOrderHash].toNumber();
+ if (lhsExpiration !== rhsExpiration) {
+ return lhsExpiration - rhsExpiration;
+ } else {
+ // HACK: If two orders have identical expirations, the order in which they are emitted by the
+ // ExpirationWatcher does not matter, so we emit them in alphabetical order by orderHash.
+ return lhsOrderHash.localeCompare(rhsOrderHash);
+ }
+ };
+ this._orderHashByExpirationRBTree = new RBTree(comparator);
+ }
+ public subscribe(callback: (orderHash: string) => void): void {
+ if (!_.isUndefined(this._orderExpirationCheckingIntervalIdIfExists)) {
+ throw new Error(OrderWatcherError.SubscriptionAlreadyPresent);
+ }
+ this._orderExpirationCheckingIntervalIdIfExists = intervalUtils.setInterval(
+ this._pruneExpiredOrders.bind(this, callback),
+ this._orderExpirationCheckingIntervalMs,
+ _.noop, // _pruneExpiredOrders never throws
+ );
+ }
+ public unsubscribe(): void {
+ if (_.isUndefined(this._orderExpirationCheckingIntervalIdIfExists)) {
+ throw new Error(OrderWatcherError.SubscriptionNotFound);
+ }
+ intervalUtils.clearInterval(this._orderExpirationCheckingIntervalIdIfExists);
+ delete this._orderExpirationCheckingIntervalIdIfExists;
+ }
+ public addOrder(orderHash: string, expirationUnixTimestampMs: BigNumber): void {
+ this._expiration[orderHash] = expirationUnixTimestampMs;
+ this._orderHashByExpirationRBTree.insert(orderHash);
+ }
+ public removeOrder(orderHash: string): void {
+ if (_.isUndefined(this._expiration[orderHash])) {
+ return; // noop since order already removed
+ }
+ this._orderHashByExpirationRBTree.remove(orderHash);
+ delete this._expiration[orderHash];
+ }
+ private _pruneExpiredOrders(callback: (orderHash: string) => void): void {
+ const currentUnixTimestampMs = utils.getCurrentUnixTimestampMs();
+ while (true) {
+ const hasTrakedOrders = this._orderHashByExpirationRBTree.size === 0;
+ if (hasTrakedOrders) {
+ break;
+ }
+ const nextOrderHashToExpire = this._orderHashByExpirationRBTree.min();
+ const hasNoExpiredOrders = this._expiration[nextOrderHashToExpire].greaterThan(
+ currentUnixTimestampMs.plus(this._expirationMarginMs),
+ );
+ const isSubscriptionActive = _.isUndefined(this._orderExpirationCheckingIntervalIdIfExists);
+ if (hasNoExpiredOrders || isSubscriptionActive) {
+ break;
+ }
+ const orderHash = this._orderHashByExpirationRBTree.min();
+ this._orderHashByExpirationRBTree.remove(orderHash);
+ delete this._expiration[orderHash];
+ callback(orderHash);
+ }
+ }
+}
diff --git a/packages/order-watcher/src/order_watcher/order_watcher.ts b/packages/order-watcher/src/order_watcher/order_watcher.ts
new file mode 100644
index 000000000..63f87b565
--- /dev/null
+++ b/packages/order-watcher/src/order_watcher/order_watcher.ts
@@ -0,0 +1,393 @@
+import {
+ BalanceAndProxyAllowanceLazyStore,
+ ContractWrappers,
+ OrderFilledCancelledLazyStore,
+} from '@0xproject/contract-wrappers';
+import { schemas } from '@0xproject/json-schemas';
+import { getOrderHashHex, OrderStateUtils } from '@0xproject/order-utils';
+import {
+ BlockParamLiteral,
+ ExchangeContractErrs,
+ LogEntryEvent,
+ LogWithDecodedArgs,
+ OrderState,
+ Provider,
+ SignedOrder,
+} from '@0xproject/types';
+import { AbiDecoder, intervalUtils } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import {
+ DepositContractEventArgs,
+ EtherTokenContractEventArgs,
+ EtherTokenEvents,
+ WithdrawalContractEventArgs,
+} from '../generated_contract_wrappers/ether_token';
+import {
+ ExchangeContractEventArgs,
+ ExchangeEvents,
+ LogCancelContractEventArgs,
+ LogFillContractEventArgs,
+} from '../generated_contract_wrappers/exchange';
+import {
+ ApprovalContractEventArgs,
+ TokenContractEventArgs,
+ TokenEvents,
+ TransferContractEventArgs,
+} from '../generated_contract_wrappers/token';
+import { OnOrderStateChangeCallback, OrderWatcherConfig, OrderWatcherError } from '../types';
+import { assert } from '../utils/assert';
+import { utils } from '../utils/utils';
+
+import { EventWatcher } from './event_watcher';
+import { ExpirationWatcher } from './expiration_watcher';
+
+type ContractEventArgs = EtherTokenContractEventArgs | ExchangeContractEventArgs | TokenContractEventArgs;
+
+interface DependentOrderHashes {
+ [makerAddress: string]: {
+ [makerToken: string]: Set<string>;
+ };
+}
+
+interface OrderByOrderHash {
+ [orderHash: string]: SignedOrder;
+}
+
+interface OrderStateByOrderHash {
+ [orderHash: string]: OrderState;
+}
+
+const DEFAULT_CLEANUP_JOB_INTERVAL_MS = 1000 * 60 * 60; // 1h
+
+/**
+ * This class includes all the functionality related to watching a set of orders
+ * for potential changes in order validity/fillability. The orderWatcher notifies
+ * the subscriber of these changes so that a final decision can be made on whether
+ * the order should be deemed invalid.
+ */
+export class OrderWatcher {
+ private _contractWrappers: ContractWrappers;
+ private _orderStateByOrderHashCache: OrderStateByOrderHash = {};
+ private _orderByOrderHash: OrderByOrderHash = {};
+ private _dependentOrderHashes: DependentOrderHashes = {};
+ private _callbackIfExists?: OnOrderStateChangeCallback;
+ private _eventWatcher: EventWatcher;
+ private _web3Wrapper: Web3Wrapper;
+ private _expirationWatcher: ExpirationWatcher;
+ private _orderStateUtils: OrderStateUtils;
+ private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
+ private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
+ private _cleanupJobInterval: number;
+ private _cleanupJobIntervalIdIfExists?: NodeJS.Timer;
+ constructor(provider: Provider, networkId: number, config?: OrderWatcherConfig) {
+ this._web3Wrapper = new Web3Wrapper(provider);
+ const artifactJSONs = _.values(artifacts);
+ const abiArrays = _.map(artifactJSONs, artifact => artifact.abi);
+ _.forEach(abiArrays, abi => {
+ this._web3Wrapper.abiDecoder.addABI(abi);
+ });
+ this._contractWrappers = new ContractWrappers(provider, { networkId });
+ const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs;
+ const stateLayer =
+ _.isUndefined(config) || _.isUndefined(config.stateLayer) ? BlockParamLiteral.Latest : config.stateLayer;
+ this._eventWatcher = new EventWatcher(this._web3Wrapper, pollingIntervalIfExistsMs, stateLayer);
+ this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(
+ this._contractWrappers.token,
+ stateLayer,
+ );
+ this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(
+ this._contractWrappers.exchange,
+ stateLayer,
+ );
+ this._orderStateUtils = new OrderStateUtils(
+ this._balanceAndProxyAllowanceLazyStore,
+ this._orderFilledCancelledLazyStore,
+ );
+ const orderExpirationCheckingIntervalMsIfExists = _.isUndefined(config)
+ ? undefined
+ : config.orderExpirationCheckingIntervalMs;
+ const expirationMarginIfExistsMs = _.isUndefined(config) ? undefined : config.expirationMarginMs;
+ this._expirationWatcher = new ExpirationWatcher(
+ expirationMarginIfExistsMs,
+ orderExpirationCheckingIntervalMsIfExists,
+ );
+ this._cleanupJobInterval =
+ _.isUndefined(config) || _.isUndefined(config.cleanupJobIntervalMs)
+ ? DEFAULT_CLEANUP_JOB_INTERVAL_MS
+ : config.cleanupJobIntervalMs;
+ }
+ /**
+ * Add an order to the orderWatcher. Before the order is added, it's
+ * signature is verified.
+ * @param signedOrder The order you wish to start watching.
+ */
+ public addOrder(signedOrder: SignedOrder): void {
+ assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
+ const orderHash = getOrderHashHex(signedOrder);
+ assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker);
+ this._orderByOrderHash[orderHash] = signedOrder;
+ this._addToDependentOrderHashes(signedOrder, orderHash);
+ const expirationUnixTimestampMs = signedOrder.expirationUnixTimestampSec.times(1000);
+ this._expirationWatcher.addOrder(orderHash, expirationUnixTimestampMs);
+ }
+ /**
+ * Removes an order from the orderWatcher
+ * @param orderHash The orderHash of the order you wish to stop watching.
+ */
+ public removeOrder(orderHash: string): void {
+ assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
+ const signedOrder = this._orderByOrderHash[orderHash];
+ if (_.isUndefined(signedOrder)) {
+ return; // noop
+ }
+ delete this._orderByOrderHash[orderHash];
+ delete this._orderStateByOrderHashCache[orderHash];
+ const zrxTokenAddress = this._orderFilledCancelledLazyStore.getZRXTokenAddress();
+
+ this._removeFromDependentOrderHashes(signedOrder.maker, zrxTokenAddress, orderHash);
+ if (zrxTokenAddress !== signedOrder.makerTokenAddress) {
+ this._removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash);
+ }
+
+ this._expirationWatcher.removeOrder(orderHash);
+ }
+ /**
+ * Starts an orderWatcher subscription. The callback will be called every time a watched order's
+ * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order.
+ * @param callback Receives the orderHash of the order that should be re-validated, together
+ * with all the order-relevant blockchain state needed to re-validate the order.
+ */
+ public subscribe(callback: OnOrderStateChangeCallback): void {
+ assert.isFunction('callback', callback);
+ if (!_.isUndefined(this._callbackIfExists)) {
+ throw new Error(OrderWatcherError.SubscriptionAlreadyPresent);
+ }
+ this._callbackIfExists = callback;
+ this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this));
+ this._expirationWatcher.subscribe(this._onOrderExpired.bind(this));
+ this._cleanupJobIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval(
+ this._cleanupAsync.bind(this),
+ this._cleanupJobInterval,
+ (err: Error) => {
+ this.unsubscribe();
+ callback(err);
+ },
+ );
+ }
+ /**
+ * Ends an orderWatcher subscription.
+ */
+ public unsubscribe(): void {
+ if (_.isUndefined(this._callbackIfExists) || _.isUndefined(this._cleanupJobIntervalIdIfExists)) {
+ throw new Error(OrderWatcherError.SubscriptionNotFound);
+ }
+ this._balanceAndProxyAllowanceLazyStore.deleteAll();
+ this._orderFilledCancelledLazyStore.deleteAll();
+ delete this._callbackIfExists;
+ this._eventWatcher.unsubscribe();
+ this._expirationWatcher.unsubscribe();
+ intervalUtils.clearAsyncExcludingInterval(this._cleanupJobIntervalIdIfExists);
+ }
+ private async _cleanupAsync(): Promise<void> {
+ for (const orderHash of _.keys(this._orderByOrderHash)) {
+ this._cleanupOrderRelatedState(orderHash);
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ }
+ private _cleanupOrderRelatedState(orderHash: string): void {
+ const signedOrder = this._orderByOrderHash[orderHash];
+
+ this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(orderHash);
+ this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(orderHash);
+
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.makerTokenAddress, signedOrder.maker);
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.makerTokenAddress, signedOrder.maker);
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.takerTokenAddress, signedOrder.taker);
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.takerTokenAddress, signedOrder.taker);
+
+ const zrxTokenAddress = this._getZRXTokenAddress();
+ if (!signedOrder.makerFee.isZero()) {
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.maker);
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.maker);
+ }
+ if (!signedOrder.takerFee.isZero()) {
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.taker);
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.taker);
+ }
+ }
+ private _onOrderExpired(orderHash: string): void {
+ const orderState: OrderState = {
+ isValid: false,
+ orderHash,
+ error: ExchangeContractErrs.OrderFillExpired,
+ };
+ if (!_.isUndefined(this._orderByOrderHash[orderHash])) {
+ this.removeOrder(orderHash);
+ if (!_.isUndefined(this._callbackIfExists)) {
+ this._callbackIfExists(null, orderState);
+ }
+ }
+ }
+ private async _onEventWatcherCallbackAsync(err: Error | null, logIfExists?: LogEntryEvent): Promise<void> {
+ if (!_.isNull(err)) {
+ if (!_.isUndefined(this._callbackIfExists)) {
+ this._callbackIfExists(err);
+ this.unsubscribe();
+ }
+ return;
+ }
+ const log = logIfExists as LogEntryEvent; // At this moment we are sure that no error occured and log is defined.
+ const maybeDecodedLog = this._web3Wrapper.abiDecoder.tryToDecodeLogOrNoop<ContractEventArgs>(log);
+ const isLogDecoded = !_.isUndefined(((maybeDecodedLog as any) as LogWithDecodedArgs<ContractEventArgs>).event);
+ if (!isLogDecoded) {
+ return; // noop
+ }
+ const decodedLog = (maybeDecodedLog as any) as LogWithDecodedArgs<ContractEventArgs>;
+ let makerToken: string;
+ let makerAddress: string;
+ switch (decodedLog.event) {
+ case TokenEvents.Approval: {
+ // Invalidate cache
+ const args = decodedLog.args as ApprovalContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._owner;
+ if (
+ !_.isUndefined(this._dependentOrderHashes[makerAddress]) &&
+ !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken])
+ ) {
+ const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case TokenEvents.Transfer: {
+ // Invalidate cache
+ const args = decodedLog.args as TransferContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from);
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._from;
+ if (
+ !_.isUndefined(this._dependentOrderHashes[makerAddress]) &&
+ !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken])
+ ) {
+ const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case EtherTokenEvents.Deposit: {
+ // Invalidate cache
+ const args = decodedLog.args as DepositContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._owner;
+ if (
+ !_.isUndefined(this._dependentOrderHashes[makerAddress]) &&
+ !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken])
+ ) {
+ const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case EtherTokenEvents.Withdrawal: {
+ // Invalidate cache
+ const args = decodedLog.args as WithdrawalContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._owner;
+ if (
+ !_.isUndefined(this._dependentOrderHashes[makerAddress]) &&
+ !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken])
+ ) {
+ const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case ExchangeEvents.LogFill: {
+ // Invalidate cache
+ const args = decodedLog.args as LogFillContractEventArgs;
+ this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash);
+ // Revalidate orders
+ const orderHash = args.orderHash;
+ const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
+ if (isOrderWatched) {
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ break;
+ }
+ case ExchangeEvents.LogCancel: {
+ // Invalidate cache
+ const args = decodedLog.args as LogCancelContractEventArgs;
+ this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash);
+ // Revalidate orders
+ const orderHash = args.orderHash;
+ const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
+ if (isOrderWatched) {
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ break;
+ }
+ case ExchangeEvents.LogError:
+ return; // noop
+
+ default:
+ throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event);
+ }
+ }
+ private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> {
+ for (const orderHash of orderHashes) {
+ const signedOrder = this._orderByOrderHash[orderHash];
+ // Most of these calls will never reach the network because the data is fetched from stores
+ // and only updated when cache is invalidated
+ const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder);
+ if (_.isUndefined(this._callbackIfExists)) {
+ break; // Unsubscribe was called
+ }
+ if (_.isEqual(orderState, this._orderStateByOrderHashCache[orderHash])) {
+ // Actual order state didn't change
+ continue;
+ } else {
+ this._orderStateByOrderHashCache[orderHash] = orderState;
+ }
+ this._callbackIfExists(null, orderState);
+ }
+ }
+ private _addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string): void {
+ if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker])) {
+ this._dependentOrderHashes[signedOrder.maker] = {};
+ }
+ if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress])) {
+ this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set();
+ }
+ this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash);
+ const zrxTokenAddress = this._getZRXTokenAddress();
+ if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress])) {
+ this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress] = new Set();
+ }
+ this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress].add(orderHash);
+ }
+ private _removeFromDependentOrderHashes(makerAddress: string, tokenAddress: string, orderHash: string) {
+ this._dependentOrderHashes[makerAddress][tokenAddress].delete(orderHash);
+ if (this._dependentOrderHashes[makerAddress][tokenAddress].size === 0) {
+ delete this._dependentOrderHashes[makerAddress][tokenAddress];
+ }
+ if (_.isEmpty(this._dependentOrderHashes[makerAddress])) {
+ delete this._dependentOrderHashes[makerAddress];
+ }
+ }
+ private _getZRXTokenAddress(): string {
+ const zrxTokenAddress = this._orderFilledCancelledLazyStore.getZRXTokenAddress();
+ return zrxTokenAddress;
+ }
+}
diff --git a/packages/order-watcher/src/types.ts b/packages/order-watcher/src/types.ts
new file mode 100644
index 000000000..9125af236
--- /dev/null
+++ b/packages/order-watcher/src/types.ts
@@ -0,0 +1,46 @@
+import { BigNumber } from '@0xproject/utils';
+
+import {
+ BlockParam,
+ BlockParamLiteral,
+ ContractAbi,
+ ContractEventArg,
+ ExchangeContractErrs,
+ FilterObject,
+ LogEntryEvent,
+ LogWithDecodedArgs,
+ Order,
+ OrderState,
+ SignedOrder,
+} from '@0xproject/types';
+
+export enum OrderWatcherError {
+ SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT',
+ SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND',
+}
+
+export type EventWatcherCallback = (err: null | Error, log?: LogEntryEvent) => void;
+
+/**
+ * orderExpirationCheckingIntervalMs: How often to check for expired orders. Default=50.
+ * eventPollingIntervalMs: How often to poll the Ethereum node for new events. Default=200.
+ * expirationMarginMs: Amount of time before order expiry that you'd like to be notified
+ * of an orders expiration. Default=0.
+ * cleanupJobIntervalMs: How often to run a cleanup job which revalidates all the orders. Default=1hr.
+ * stateLayer: Optional blockchain state layer OrderWatcher will monitor for new events. Default=latest.
+ */
+export interface OrderWatcherConfig {
+ orderExpirationCheckingIntervalMs?: number;
+ eventPollingIntervalMs?: number;
+ expirationMarginMs?: number;
+ cleanupJobIntervalMs?: number;
+ stateLayer: BlockParamLiteral;
+}
+
+export type OnOrderStateChangeCallback = (err: Error | null, orderState?: OrderState) => void;
+
+export enum InternalOrderWatcherError {
+ NoAbiDecoder = 'NO_ABI_DECODER',
+ ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
+ WethNotInTokenRegistry = 'WETH_NOT_IN_TOKEN_REGISTRY',
+}
diff --git a/packages/order-watcher/src/utils/assert.ts b/packages/order-watcher/src/utils/assert.ts
new file mode 100644
index 000000000..f96bcebc1
--- /dev/null
+++ b/packages/order-watcher/src/utils/assert.ts
@@ -0,0 +1,19 @@
+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`);
+ },
+};
diff --git a/packages/order-watcher/src/utils/utils.ts b/packages/order-watcher/src/utils/utils.ts
new file mode 100644
index 000000000..af1125632
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/test/event_watcher_test.ts b/packages/order-watcher/test/event_watcher_test.ts
new file mode 100644
index 000000000..b4eca315e
--- /dev/null
+++ b/packages/order-watcher/test/event_watcher_test.ts
@@ -0,0 +1,126 @@
+import { callbackErrorReporter, web3Factory } from '@0xproject/dev-utils';
+import { DoneCallback, LogEntry, LogEntryEvent } from '@0xproject/types';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import 'mocha';
+import * as Sinon from 'sinon';
+
+import { EventWatcher } from '../src/order_watcher/event_watcher';
+
+import { chaiSetup } from './utils/chai_setup';
+import { provider } from './utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('EventWatcher', () => {
+ let stubs: Sinon.SinonStub[] = [];
+ let eventWatcher: EventWatcher;
+ let web3Wrapper: Web3Wrapper;
+ const logA: LogEntry = {
+ address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: [],
+ transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17',
+ transactionIndex: 0,
+ };
+ const logB: LogEntry = {
+ address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: ['0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567'],
+ transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
+ transactionIndex: 0,
+ };
+ const logC: LogEntry = {
+ address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5',
+ blockHash: null,
+ blockNumber: null,
+ data: '',
+ logIndex: null,
+ topics: ['0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567'],
+ transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
+ transactionIndex: 0,
+ };
+ before(async () => {
+ const pollingIntervalMs = 10;
+ web3Wrapper = new Web3Wrapper(provider);
+ eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs);
+ });
+ afterEach(() => {
+ // clean up any stubs after the test has completed
+ _.each(stubs, s => s.restore());
+ stubs = [];
+ eventWatcher.unsubscribe();
+ });
+ it('correctly emits initial log events', (done: DoneCallback) => {
+ const logs: LogEntry[] = [logA, logB];
+ const expectedLogEvents = [
+ {
+ removed: false,
+ ...logA,
+ },
+ {
+ removed: false,
+ ...logB,
+ },
+ ];
+ const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
+ getLogsStub.onCall(0).returns(logs);
+ stubs.push(getLogsStub);
+ const expectedToBeCalledOnce = false;
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done, expectedToBeCalledOnce)(
+ (event: LogEntryEvent) => {
+ const expectedLogEvent = expectedLogEvents.shift();
+ expect(event).to.be.deep.equal(expectedLogEvent);
+ if (_.isEmpty(expectedLogEvents)) {
+ done();
+ }
+ },
+ );
+ eventWatcher.subscribe(callback);
+ });
+ it('correctly computes the difference and emits only changes', (done: DoneCallback) => {
+ const initialLogs: LogEntry[] = [logA, logB];
+ const changedLogs: LogEntry[] = [logA, logC];
+ const expectedLogEvents = [
+ {
+ removed: false,
+ ...logA,
+ },
+ {
+ removed: false,
+ ...logB,
+ },
+ {
+ removed: true,
+ ...logB,
+ },
+ {
+ removed: false,
+ ...logC,
+ },
+ ];
+ const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
+ getLogsStub.onCall(0).returns(initialLogs);
+ getLogsStub.onCall(1).returns(changedLogs);
+ stubs.push(getLogsStub);
+ const expectedToBeCalledOnce = false;
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done, expectedToBeCalledOnce)(
+ (event: LogEntryEvent) => {
+ const expectedLogEvent = expectedLogEvents.shift();
+ expect(event).to.be.deep.equal(expectedLogEvent);
+ if (_.isEmpty(expectedLogEvents)) {
+ done();
+ }
+ },
+ );
+ eventWatcher.subscribe(callback);
+ });
+});
diff --git a/packages/order-watcher/test/expiration_watcher_test.ts b/packages/order-watcher/test/expiration_watcher_test.ts
new file mode 100644
index 000000000..0a2524d78
--- /dev/null
+++ b/packages/order-watcher/test/expiration_watcher_test.ts
@@ -0,0 +1,200 @@
+import { ContractWrappers } from '@0xproject/contract-wrappers';
+import { BlockchainLifecycle, callbackErrorReporter, devConstants } from '@0xproject/dev-utils';
+import { FillScenarios } from '@0xproject/fill-scenarios';
+import { getOrderHashHex } from '@0xproject/order-utils';
+import { DoneCallback, Token } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import 'mocha';
+import * as Sinon from 'sinon';
+
+import { artifacts } from '../src/artifacts';
+import { ExpirationWatcher } from '../src/order_watcher/expiration_watcher';
+import { utils } from '../src/utils/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('ExpirationWatcher', () => {
+ let contractWrappers: ContractWrappers;
+ let tokenUtils: TokenUtils;
+ let tokens: Token[];
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let fillScenarios: FillScenarios;
+ let exchangeContractAddress: string;
+ let makerTokenAddress: string;
+ let takerTokenAddress: string;
+ let coinbase: string;
+ let makerAddress: string;
+ let takerAddress: string;
+ let feeRecipient: string;
+ const fillableAmount = new BigNumber(5);
+ let currentUnixTimestampSec: BigNumber;
+ let timer: Sinon.SinonFakeTimers;
+ let expirationWatcher: ExpirationWatcher;
+ before(async () => {
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ 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);
+ [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ const [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ makerTokenAddress = makerToken.address;
+ takerTokenAddress = takerToken.address;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ const sinonTimerConfig = { shouldAdvanceTime: true } as any;
+ // This constructor has incorrect types
+ timer = Sinon.useFakeTimers(sinonTimerConfig);
+ currentUnixTimestampSec = utils.getCurrentUnixTimestampSec();
+ expirationWatcher = new ExpirationWatcher();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ timer.restore();
+ expirationWatcher.unsubscribe();
+ });
+ it('correctly emits events when order expires', (done: DoneCallback) => {
+ (async () => {
+ const orderLifetimeSec = 60;
+ const expirationUnixTimestampSec = currentUnixTimestampSec.plus(orderLifetimeSec);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ expirationUnixTimestampSec,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ expirationWatcher.addOrder(orderHash, signedOrder.expirationUnixTimestampSec.times(1000));
+ const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done)((hash: string) => {
+ expect(hash).to.be.equal(orderHash);
+ expect(utils.getCurrentUnixTimestampSec()).to.be.bignumber.gte(expirationUnixTimestampSec);
+ });
+ expirationWatcher.subscribe(callbackAsync);
+ timer.tick(orderLifetimeSec * 1000);
+ })().catch(done);
+ });
+ it("doesn't emit events before order expires", (done: DoneCallback) => {
+ (async () => {
+ const orderLifetimeSec = 60;
+ const expirationUnixTimestampSec = currentUnixTimestampSec.plus(orderLifetimeSec);
+ const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ expirationUnixTimestampSec,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ expirationWatcher.addOrder(orderHash, signedOrder.expirationUnixTimestampSec.times(1000));
+ const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done)(async (hash: string) => {
+ done(new Error('Emitted expiration went before the order actually expired'));
+ });
+ expirationWatcher.subscribe(callbackAsync);
+ const notEnoughTime = orderLifetimeSec - 1;
+ timer.tick(notEnoughTime * 1000);
+ done();
+ })().catch(done);
+ });
+ it('emits events in correct order', (done: DoneCallback) => {
+ (async () => {
+ const order1Lifetime = 60;
+ const order2Lifetime = 120;
+ const order1ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order1Lifetime);
+ const order2ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order2Lifetime);
+ const signedOrder1 = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ order1ExpirationUnixTimestampSec,
+ );
+ const signedOrder2 = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ order2ExpirationUnixTimestampSec,
+ );
+ const orderHash1 = getOrderHashHex(signedOrder1);
+ const orderHash2 = getOrderHashHex(signedOrder2);
+ expirationWatcher.addOrder(orderHash2, signedOrder2.expirationUnixTimestampSec.times(1000));
+ expirationWatcher.addOrder(orderHash1, signedOrder1.expirationUnixTimestampSec.times(1000));
+ const expirationOrder = [orderHash1, orderHash2];
+ const expectToBeCalledOnce = false;
+ const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done, expectToBeCalledOnce)(
+ (hash: string) => {
+ const orderHash = expirationOrder.shift();
+ expect(hash).to.be.equal(orderHash);
+ if (_.isEmpty(expirationOrder)) {
+ done();
+ }
+ },
+ );
+ expirationWatcher.subscribe(callbackAsync);
+ timer.tick(order2Lifetime * 1000);
+ })().catch(done);
+ });
+ it('emits events in correct order when expirations are equal', (done: DoneCallback) => {
+ (async () => {
+ const order1Lifetime = 60;
+ const order2Lifetime = 60;
+ const order1ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order1Lifetime);
+ const order2ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order2Lifetime);
+ const signedOrder1 = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ order1ExpirationUnixTimestampSec,
+ );
+ const signedOrder2 = await fillScenarios.createFillableSignedOrderAsync(
+ makerTokenAddress,
+ takerTokenAddress,
+ makerAddress,
+ takerAddress,
+ fillableAmount,
+ order2ExpirationUnixTimestampSec,
+ );
+ const orderHash1 = getOrderHashHex(signedOrder1);
+ const orderHash2 = getOrderHashHex(signedOrder2);
+ expirationWatcher.addOrder(orderHash1, signedOrder1.expirationUnixTimestampSec.times(1000));
+ expirationWatcher.addOrder(orderHash2, signedOrder2.expirationUnixTimestampSec.times(1000));
+ const expirationOrder = orderHash1 < orderHash2 ? [orderHash1, orderHash2] : [orderHash2, orderHash1];
+ const expectToBeCalledOnce = false;
+ const callbackAsync = callbackErrorReporter.reportNoErrorCallbackErrors(done, expectToBeCalledOnce)(
+ (hash: string) => {
+ const orderHash = expirationOrder.shift();
+ expect(hash).to.be.equal(orderHash);
+ if (_.isEmpty(expirationOrder)) {
+ done();
+ }
+ },
+ );
+ expirationWatcher.subscribe(callbackAsync);
+ timer.tick(order2Lifetime * 1000);
+ })().catch(done);
+ });
+});
diff --git a/packages/order-watcher/test/global_hooks.ts b/packages/order-watcher/test/global_hooks.ts
new file mode 100644
index 000000000..88f202761
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/test/order_watcher_test.ts b/packages/order-watcher/test/order_watcher_test.ts
new file mode 100644
index 000000000..8c9249f58
--- /dev/null
+++ b/packages/order-watcher/test/order_watcher_test.ts
@@ -0,0 +1,574 @@
+import { ContractWrappers } from '@0xproject/contract-wrappers';
+import { BlockchainLifecycle, callbackErrorReporter, devConstants } from '@0xproject/dev-utils';
+import { FillScenarios } from '@0xproject/fill-scenarios';
+import { getOrderHashHex } from '@0xproject/order-utils';
+import {
+ DoneCallback,
+ ExchangeContractErrs,
+ OrderState,
+ OrderStateInvalid,
+ OrderStateValid,
+ SignedOrder,
+ Token,
+} 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 { OrderWatcher } from '../src/order_watcher/order_watcher';
+import { OrderWatcherError } from '../src/types';
+
+import { chaiSetup } from './utils/chai_setup';
+import { constants } from './utils/constants';
+import { TokenUtils } from './utils/token_utils';
+import { provider, web3Wrapper } from './utils/web3_wrapper';
+
+const TIMEOUT_MS = 150;
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+
+describe('OrderWatcher', () => {
+ let contractWrappers: ContractWrappers;
+ let tokens: Token[];
+ let tokenUtils: TokenUtils;
+ let fillScenarios: FillScenarios;
+ let userAddresses: string[];
+ let zrxTokenAddress: string;
+ let exchangeContractAddress: string;
+ let makerToken: Token;
+ let takerToken: Token;
+ let maker: string;
+ let taker: string;
+ let signedOrder: SignedOrder;
+ let orderWatcher: OrderWatcher;
+ const config = {
+ networkId: constants.TESTRPC_NETWORK_ID,
+ };
+ const decimals = constants.ZRX_DECIMALS;
+ const fillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals);
+ before(async () => {
+ contractWrappers = new ContractWrappers(provider, config);
+ const networkId = await web3Wrapper.getNetworkIdAsync();
+ orderWatcher = new OrderWatcher(provider, constants.TESTRPC_NETWORK_ID);
+ exchangeContractAddress = contractWrappers.exchange.getContractAddress();
+ userAddresses = await web3Wrapper.getAvailableAddressesAsync();
+ [, maker, taker] = userAddresses;
+ tokens = await contractWrappers.tokenRegistry.getTokensAsync();
+ tokenUtils = new TokenUtils(tokens);
+ zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
+ fillScenarios = new FillScenarios(provider, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
+ await fillScenarios.initTokenBalancesAsync();
+ [makerToken, takerToken] = tokenUtils.getDummyTokens();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#removeOrder', async () => {
+ it('should successfully remove existing order', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+ expect((orderWatcher as any)._orderByOrderHash).to.include({
+ [orderHash]: signedOrder,
+ });
+ let dependentOrderHashes = (orderWatcher as any)._dependentOrderHashes;
+ expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash);
+ orderWatcher.removeOrder(orderHash);
+ expect((orderWatcher as any)._orderByOrderHash).to.not.include({
+ [orderHash]: signedOrder,
+ });
+ dependentOrderHashes = (orderWatcher as any)._dependentOrderHashes;
+ expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined();
+ });
+ it('should no-op when removing a non-existing order', async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ const nonExistentOrderHash = `0x${orderHash
+ .substr(2)
+ .split('')
+ .reverse()
+ .join('')}`;
+ orderWatcher.removeOrder(nonExistentOrderHash);
+ });
+ });
+ describe('#subscribe', async () => {
+ afterEach(async () => {
+ orderWatcher.unsubscribe();
+ });
+ it('should fail when trying to subscribe twice', async () => {
+ orderWatcher.subscribe(_.noop);
+ expect(() => orderWatcher.subscribe(_.noop)).to.throw(OrderWatcherError.SubscriptionAlreadyPresent);
+ });
+ });
+ describe('tests with cleanup', async () => {
+ afterEach(async () => {
+ orderWatcher.unsubscribe();
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.removeOrder(orderHash);
+ });
+ it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance);
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0));
+ })().catch(done);
+ });
+ it('should not emit an orderState event when irrelevant Transfer event received', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ orderWatcher.addOrder(signedOrder);
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ throw new Error('OrderState callback fired for irrelevant order');
+ });
+ orderWatcher.subscribe(callback);
+ const notTheMaker = userAddresses[0];
+ const anyRecipient = taker;
+ const transferAmount = new BigNumber(2);
+ await contractWrappers.token.transferAsync(
+ makerToken.address,
+ notTheMaker,
+ anyRecipient,
+ transferAmount,
+ );
+ setTimeout(() => {
+ done();
+ }, TIMEOUT_MS);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance);
+ });
+ orderWatcher.subscribe(callback);
+ const anyRecipient = taker;
+ const makerBalance = await contractWrappers.token.getBalanceAsync(makerToken.address, maker);
+ await contractWrappers.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when watched order fully filled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ });
+ orderWatcher.subscribe(callback);
+
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ fillableAmount,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ taker,
+ );
+ })().catch(done);
+ });
+ it('should emit orderStateValid when watched order partially filled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+
+ const makerBalance = await contractWrappers.token.getBalanceAsync(makerToken.address, maker);
+ const fillAmountInBaseUnits = new BigNumber(2);
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits);
+ const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits);
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingFillable,
+ );
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ remainingFillable,
+ );
+ expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance);
+ });
+ orderWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ fillAmountInBaseUnits,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ taker,
+ );
+ })().catch(done);
+ });
+ it('should trigger the callback when orders backing ZRX allowance changes', (done: DoneCallback) => {
+ (async () => {
+ const makerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), 18);
+ const takerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 18);
+ signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerToken.address,
+ takerToken.address,
+ makerFee,
+ takerFee,
+ maker,
+ taker,
+ fillableAmount,
+ taker,
+ );
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)();
+ orderWatcher.addOrder(signedOrder);
+ orderWatcher.subscribe(callback);
+ await contractWrappers.token.setProxyAllowanceAsync(zrxTokenAddress, maker, new BigNumber(0));
+ })().catch(done);
+ });
+ describe('remainingFillable(M|T)akerTokenAmount', () => {
+ it('should calculate correct remaining fillable', (done: DoneCallback) => {
+ (async () => {
+ const takerFillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), decimals);
+ const makerFillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(20), decimals);
+ signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ makerFillableAmount,
+ takerFillableAmount,
+ );
+ const fillAmountInBaseUnits = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals);
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(16), decimals),
+ );
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(8), decimals),
+ );
+ });
+ orderWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await contractWrappers.exchange.fillOrderAsync(
+ signedOrder,
+ fillAmountInBaseUnits,
+ shouldThrowOnInsufficientBalanceOrAllowance,
+ taker,
+ );
+ })().catch(done);
+ });
+ it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+
+ const changedMakerApprovalAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(3), decimals);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ changedMakerApprovalAmount,
+ );
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ changedMakerApprovalAmount,
+ );
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.token.setProxyAllowanceAsync(
+ makerToken.address,
+ maker,
+ changedMakerApprovalAmount,
+ );
+ })().catch(done);
+ });
+ it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+
+ const makerBalance = await contractWrappers.token.getBalanceAsync(makerToken.address, maker);
+
+ const remainingAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), decimals);
+ const transferAmount = makerBalance.sub(remainingAmount);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingAmount,
+ );
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ remainingAmount,
+ );
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.token.transferAsync(
+ makerToken.address,
+ maker,
+ constants.NULL_ADDRESS,
+ transferAmount,
+ );
+ })().catch(done);
+ });
+ it('should equal remaining amount when partially cancelled and order has fees', (done: DoneCallback) => {
+ (async () => {
+ const takerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), decimals);
+ const makerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals);
+ const feeRecipient = taker;
+ signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerToken.address,
+ takerToken.address,
+ makerFee,
+ takerFee,
+ maker,
+ taker,
+ fillableAmount,
+ feeRecipient,
+ );
+
+ const remainingTokenAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(4), decimals);
+ const transferTokenAmount = makerFee.sub(remainingTokenAmount);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingTokenAmount,
+ );
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.exchange.cancelOrderAsync(signedOrder, transferTokenAmount);
+ })().catch(done);
+ });
+ it('should equal ratio amount when fee balance is lowered', (done: DoneCallback) => {
+ (async () => {
+ const takerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), decimals);
+ const makerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals);
+ const feeRecipient = taker;
+ signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerToken.address,
+ takerToken.address,
+ makerFee,
+ takerFee,
+ maker,
+ taker,
+ fillableAmount,
+ feeRecipient,
+ );
+
+ const remainingFeeAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(3), decimals);
+
+ const remainingTokenAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(4), decimals);
+ const transferTokenAmount = makerFee.sub(remainingTokenAmount);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingFeeAmount,
+ );
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.token.setProxyAllowanceAsync(zrxTokenAddress, maker, remainingFeeAmount);
+ await contractWrappers.token.transferAsync(
+ makerToken.address,
+ maker,
+ constants.NULL_ADDRESS,
+ transferTokenAmount,
+ );
+ })().catch(done);
+ });
+ it('should calculate full amount when all available and non-divisible', (done: DoneCallback) => {
+ (async () => {
+ const takerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), decimals);
+ const makerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals);
+ const feeRecipient = taker;
+ signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerToken.address,
+ takerToken.address,
+ makerFee,
+ takerFee,
+ maker,
+ taker,
+ fillableAmount,
+ feeRecipient,
+ );
+
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ fillableAmount,
+ );
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.token.setProxyAllowanceAsync(
+ makerToken.address,
+ maker,
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(100), decimals),
+ );
+ })().catch(done);
+ });
+ });
+ it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ });
+ orderWatcher.subscribe(callback);
+
+ await contractWrappers.exchange.cancelOrderAsync(signedOrder, fillableAmount);
+ })().catch(done);
+ });
+ it('should emit orderStateInvalid when within rounding error range', (done: DoneCallback) => {
+ (async () => {
+ const remainingFillableAmountInBaseUnits = new BigNumber(100);
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError);
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.exchange.cancelOrderAsync(
+ signedOrder,
+ fillableAmount.minus(remainingFillableAmountInBaseUnits),
+ );
+ })().catch(done);
+ });
+ it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address,
+ takerToken.address,
+ maker,
+ taker,
+ fillableAmount,
+ );
+
+ const cancelAmountInBaseUnits = new BigNumber(2);
+ const orderHash = getOrderHashHex(signedOrder);
+ orderWatcher.addOrder(signedOrder);
+
+ const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.cancelledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits);
+ });
+ orderWatcher.subscribe(callback);
+ await contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits);
+ })().catch(done);
+ });
+ });
+}); // tslint:disable:max-file-line-count
diff --git a/packages/order-watcher/test/remaining_fillable_calculator_test.ts b/packages/order-watcher/test/remaining_fillable_calculator_test.ts
new file mode 100644
index 000000000..7ec3f1ebc
--- /dev/null
+++ b/packages/order-watcher/test/remaining_fillable_calculator_test.ts
@@ -0,0 +1,234 @@
+import { ECSignature, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import * as chai from 'chai';
+import 'mocha';
+
+import { RemainingFillableCalculator } from '@0xproject/order-utils';
+
+import { chaiSetup } from './utils/chai_setup';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+describe('RemainingFillableCalculator', () => {
+ let calculator: RemainingFillableCalculator;
+ let signedOrder: SignedOrder;
+ let transferrableMakerTokenAmount: BigNumber;
+ let transferrableMakerFeeTokenAmount: BigNumber;
+ let remainingMakerTokenAmount: BigNumber;
+ let makerAmount: BigNumber;
+ let takerAmount: BigNumber;
+ let makerFeeAmount: BigNumber;
+ let isMakerTokenZRX: boolean;
+ const makerToken: string = '0x1';
+ const takerToken: string = '0x2';
+ const decimals: number = 4;
+ const zero: BigNumber = new BigNumber(0);
+ const zeroAddress = '0x0';
+ const signature: ECSignature = { v: 27, r: '', s: '' };
+ beforeEach(async () => {
+ [makerAmount, takerAmount, makerFeeAmount] = [
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(50), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(1), decimals),
+ ];
+ [transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount] = [
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(50), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals),
+ ];
+ });
+ function buildSignedOrder(): SignedOrder {
+ return {
+ ecSignature: signature,
+ exchangeContractAddress: zeroAddress,
+ feeRecipient: zeroAddress,
+ maker: zeroAddress,
+ taker: zeroAddress,
+ makerFee: makerFeeAmount,
+ takerFee: zero,
+ makerTokenAmount: makerAmount,
+ takerTokenAmount: takerAmount,
+ makerTokenAddress: makerToken,
+ takerTokenAddress: takerToken,
+ salt: zero,
+ expirationUnixTimestampSec: zero,
+ };
+ }
+ describe('Maker token is NOT ZRX', () => {
+ before(async () => {
+ isMakerTokenZRX = false;
+ });
+ it('calculates the correct amount when unfilled and funds available', () => {
+ signedOrder = buildSignedOrder();
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount;
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
+ });
+ it('calculates the correct amount when partially filled and funds available', () => {
+ signedOrder = buildSignedOrder();
+ remainingMakerTokenAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), decimals);
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
+ });
+ it('calculates the amount to be 0 when all fee funds are transferred', () => {
+ signedOrder = buildSignedOrder();
+ transferrableMakerFeeTokenAmount = zero;
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount;
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero);
+ });
+ it('calculates the correct amount when balance is less than remaining fillable', () => {
+ signedOrder = buildSignedOrder();
+ const partiallyFilledAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals);
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount);
+ transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount);
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount);
+ });
+ describe('Order to Fee Ratio is < 1', () => {
+ beforeEach(async () => {
+ [makerAmount, takerAmount, makerFeeAmount] = [
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(3), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(6), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(6), decimals),
+ ];
+ });
+ it('calculates the correct amount when funds unavailable', () => {
+ signedOrder = buildSignedOrder();
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount;
+ const transferredAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals);
+ transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount);
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount);
+ });
+ });
+ describe('Ratio is not evenly divisble', () => {
+ beforeEach(async () => {
+ [makerAmount, takerAmount, makerFeeAmount] = [
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(3), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(7), decimals),
+ Web3Wrapper.toBaseUnitAmount(new BigNumber(7), decimals),
+ ];
+ });
+ it('calculates the correct amount when funds unavailable', () => {
+ signedOrder = buildSignedOrder();
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount;
+ const transferredAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals);
+ transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount);
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ const calculatedFillableAmount = calculator.computeRemainingMakerFillable();
+ expect(calculatedFillableAmount.lessThanOrEqualTo(transferrableMakerTokenAmount)).to.be.true();
+ expect(calculatedFillableAmount).to.be.bignumber.greaterThan(new BigNumber(0));
+ const orderToFeeRatio = signedOrder.makerTokenAmount.dividedBy(signedOrder.makerFee);
+ const calculatedFeeAmount = calculatedFillableAmount.dividedBy(orderToFeeRatio);
+ expect(calculatedFeeAmount).to.be.bignumber.lessThan(transferrableMakerFeeTokenAmount);
+ });
+ });
+ });
+ describe('Maker Token is ZRX', () => {
+ before(async () => {
+ isMakerTokenZRX = true;
+ });
+ it('calculates the correct amount when unfilled and funds available', () => {
+ signedOrder = buildSignedOrder();
+ transferrableMakerTokenAmount = makerAmount.plus(makerFeeAmount);
+ transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount;
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount;
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
+ });
+ it('calculates the correct amount when partially filled and funds available', () => {
+ signedOrder = buildSignedOrder();
+ remainingMakerTokenAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), decimals);
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
+ });
+ it('calculates the amount to be 0 when all fee funds are transferred', () => {
+ signedOrder = buildSignedOrder();
+ transferrableMakerTokenAmount = zero;
+ transferrableMakerFeeTokenAmount = zero;
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount;
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero);
+ });
+ it('calculates the correct amount when balance is less than remaining fillable', () => {
+ signedOrder = buildSignedOrder();
+ const partiallyFilledAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals);
+ remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount);
+ transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount);
+ transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount;
+
+ const orderToFeeRatio = signedOrder.makerTokenAmount.dividedToIntegerBy(signedOrder.makerFee);
+ const expectedFillableAmount = new BigNumber(450980);
+ calculator = new RemainingFillableCalculator(
+ signedOrder,
+ isMakerTokenZRX,
+ transferrableMakerTokenAmount,
+ transferrableMakerFeeTokenAmount,
+ remainingMakerTokenAmount,
+ );
+ const calculatedFillableAmount = calculator.computeRemainingMakerFillable();
+ const numberOfFillsInRatio = calculatedFillableAmount.dividedToIntegerBy(orderToFeeRatio);
+ const calculatedFillableAmountPlusFees = calculatedFillableAmount.plus(numberOfFillsInRatio);
+ expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(transferrableMakerTokenAmount);
+ expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(remainingMakerTokenAmount);
+ expect(calculatedFillableAmount).to.be.bignumber.equal(expectedFillableAmount);
+ expect(numberOfFillsInRatio.decimalPlaces()).to.be.equal(0);
+ });
+ });
+});
diff --git a/packages/order-watcher/test/utils/chai_setup.ts b/packages/order-watcher/test/utils/chai_setup.ts
new file mode 100644
index 000000000..078edd309
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/test/utils/constants.ts b/packages/order-watcher/test/utils/constants.ts
new file mode 100644
index 000000000..78037647c
--- /dev/null
+++ b/packages/order-watcher/test/utils/constants.ts
@@ -0,0 +1,5 @@
+export const constants = {
+ NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
+ TESTRPC_NETWORK_ID: 50,
+ ZRX_DECIMALS: 18,
+};
diff --git a/packages/order-watcher/test/utils/token_utils.ts b/packages/order-watcher/test/utils/token_utils.ts
new file mode 100644
index 000000000..e1191b5bb
--- /dev/null
+++ b/packages/order-watcher/test/utils/token_utils.ts
@@ -0,0 +1,34 @@
+import { Token } from '@0xproject/types';
+import * as _ from 'lodash';
+
+import { InternalOrderWatcherError } 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(InternalOrderWatcherError.ZrxNotInTokenRegistry);
+ }
+ return zrxToken;
+ }
+ public getWethTokenOrThrow(): Token {
+ const wethToken = _.find(this._tokens, { symbol: WETH_TOKEN_SYMBOL });
+ if (_.isUndefined(wethToken)) {
+ throw new Error(InternalOrderWatcherError.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/order-watcher/test/utils/web3_wrapper.ts b/packages/order-watcher/test/utils/web3_wrapper.ts
new file mode 100644
index 000000000..b0ccfa546
--- /dev/null
+++ b/packages/order-watcher/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/order-watcher/tsconfig.json b/packages/order-watcher/tsconfig.json
new file mode 100644
index 000000000..e35816553
--- /dev/null
+++ b/packages/order-watcher/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib"
+ },
+ "include": ["./src/**/*", "./test/**/*"]
+}
diff --git a/packages/order-watcher/tslint.json b/packages/order-watcher/tslint.json
new file mode 100644
index 000000000..ffaefe83a
--- /dev/null
+++ b/packages/order-watcher/tslint.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["@0xproject/tslint-config"]
+}