├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .env ├── .gitattributes ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── db └── .gitkeep ├── js ├── config.js ├── constants.js ├── db_connection.js ├── entity │ └── SignedOrderEntity.js ├── errors.js ├── fee_strategy.js ├── handlers.js ├── index.js ├── mesh_utils.js ├── middleware │ ├── error_handling.js │ └── url_params_parsing.js ├── models │ └── SignedOrderModel.js ├── paginator.js ├── runners │ ├── http_service_runner.js │ ├── order_watcher_service_runner.js │ └── websocket_service_runner.js ├── services │ ├── http_service.js │ ├── order_watcher_service.js │ ├── orderbook_service.js │ ├── orderbook_utils.js │ └── websocket_service.js ├── types.js └── utils.js ├── launch_kit_banner.png ├── metadata.json ├── ormconfig.json ├── package.json ├── ts ├── src │ ├── config.ts │ ├── constants.ts │ ├── db_connection.ts │ ├── entity │ │ └── SignedOrderEntity.ts │ ├── errors.ts │ ├── fee_strategy.ts │ ├── globals.d.ts │ ├── handlers.ts │ ├── index.ts │ ├── mesh_utils.ts │ ├── middleware │ │ ├── error_handling.ts │ │ └── url_params_parsing.ts │ ├── models │ │ └── SignedOrderModel.ts │ ├── paginator.ts │ ├── runners │ │ ├── http_service_runner.ts │ │ ├── order_watcher_service_runner.ts │ │ └── websocket_service_runner.ts │ ├── services │ │ ├── http_service.ts │ │ ├── order_watcher_service.ts │ │ ├── orderbook_service.ts │ │ ├── orderbook_utils.ts │ │ └── websocket_service.ts │ ├── types.ts │ └── utils.ts ├── tsconfig.json └── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:8.12.0-browsers 10 | 11 | working_directory: ~/repo 12 | 13 | steps: 14 | - checkout 15 | # Download and cache dependencies 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "yarn.lock" }} 19 | # fallback to using the latest cache if no exact match is found 20 | - v1-dependencies- 21 | 22 | - run: yarn install 23 | 24 | - save_cache: 25 | paths: 26 | - node_modules 27 | key: v1-dependencies-{{ checksum "yarn.lock" }} 28 | - run: yarn build 29 | - run: yarn lint:ts 30 | - run: yarn prettier 31 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | HTTP_PORT= 2 | NETWORK_ID= 3 | FEE_RECIPIENT= 4 | MAKER_FEE_ZRX_UNIT_AMOUNT= 5 | TAKER_FEE_ZRX_UNIT_AMOUNT= 6 | RPC_URL= 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-generated 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Webstorm config folder 2 | .idea 3 | 4 | # VSCode config folder 5 | .vscode 6 | 7 | # dotenv environment variables file 8 | .env 9 | 10 | # Generated metadata file 11 | metadata.json 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # next.js build output 70 | .next 71 | 72 | # compiled ts sources 73 | ts/lib 74 | 75 | # sqlite DB 76 | db/database.sqlite 77 | 78 | TODO.txt 79 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | 3 | ## v0.1.1 - _March 29, 2019_ 4 | 5 | * Add support for environment variable WHITELIST_ALL_TOKENS. If set (to any value), the whitelist will be set to '*'. 6 | 7 | ## v0.1.0 - _March 27, 2019_ 8 | 9 | * Add the sorting of bids/asks for the GET /v2/orderbook endpoint as stated in the SRA v2: 10 | https://github.com/0xProject/standard-relayer-api/blob/master/http/v2.md#get-v2orderbook 11 | 12 | ## v0.0.2 - _February 5, 2019_ 13 | 14 | * Fix bug where orders in DB weren't being added to OrderWatcher on re-start 15 | * Fix bug where order fillability wasn't being checked before being added to OrderWatcher & the database 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | FROM node:11-alpine as yarn-install 3 | WORKDIR /usr/src/app 4 | # Install app dependencies 5 | COPY package.json yarn.lock ./ 6 | RUN apk update && \ 7 | apk upgrade && \ 8 | apk add --no-cache --virtual build-dependencies bash git openssh python make g++ && \ 9 | yarn --frozen-lockfile --no-cache && \ 10 | apk del build-dependencies && \ 11 | yarn cache clean 12 | 13 | # Runtime container with minimal dependencies 14 | FROM node:11-alpine 15 | WORKDIR /usr/src/app 16 | COPY --from=yarn-install /usr/src/app/node_modules /usr/src/app/node_modules 17 | # Bundle app source 18 | COPY . . 19 | 20 | RUN yarn build 21 | 22 | EXPOSE 3000 23 | CMD [ "./node_modules/.bin/forever", "ts/lib/index.js" ] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2018 ZeroEx Intl. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## ⚠️ Deprecation Warning ️️⚠️ 4 | 5 | This project is no longer being actively maintained. 6 | To fork or run a [Standard Relayer API](https://0x.org/docs/api#sra) instance, you can use the [0x API](https://github.com/0xProject/0x-api) instead. 7 | 8 | ## Table of contents 9 | 10 | - [Introduction](#introduction) 11 | - [Language choice](#language-choice) 12 | - [Getting started](#getting-started) 13 | - [Client for your relayers API](#client-for-your-relayers-api) 14 | - [Commands](#commands) 15 | - [Database](#database) 16 | - [Deployment](#deployment) 17 | - [Legal Disclaimer](#legal-disclaimer) 18 | 19 | ## Introduction 20 | 21 | Launch a 0x relayer in under a minute with Launch Kit. This repository contains an open-source, free-to-use 0x relayer template that you can use as a starting point for your own project. 22 | 23 | - Quickly launch a market for your community token 24 | - Seemlessly create an in-game marketplace for digital items and collectibles 25 | - Enable trading of any ERC-20 or ERC-721 asset 26 | 27 | Fork this repository to get started! 28 | 29 | ### [See website](https://0xproject.com/launch-kit) 30 | 31 | ## Language choice 32 | 33 | `0x-launch-kit-backend` ships with 2 codebases, one in Typescript and another in Javascript. Although the Javascript is auto-generated from the Typescript, we made sure the Javascript generated is readable. 34 | 35 | Before you start using `0x-launch-kit-backend`, choose whether you want your codebase to be in Typescript or Javascript. 36 | 37 | **If you want to work in Javascript:** 38 | 39 | - delete the `ts` directory 40 | - delete all scripts from `package.json` that end with `:ts` 41 | 42 | Note: If you also wish to build and use the Docker image, please update the command in the Dockerfile to `CMD [ "forever", "js/index.js" ]` 43 | 44 | **If you want to work in Typescript:** 45 | 46 | - delete the `js` directory 47 | - delete all scripts from `package.json` that end with `:js` 48 | 49 | ## Getting started 50 | 51 | #### Pre-requirements 52 | 53 | - [Node.js](https://nodejs.org/en/download/) > v8.x 54 | - [Yarn](https://yarnpkg.com/en/) > v1.x 55 | - [0x Mesh](https://github.com/0xProject/0x-mesh) > v5.0.1 for v3. Docker image `0xorg/mesh:5.0.1-beta-0xv3` or greater 56 | 57 | To develop ontop of `0x-launch-kit`, follow the following instructions: 58 | 59 | 1. Fork this repository 60 | 61 | 2. Clone your fork of this repository 62 | 63 | 3. Open the `config.ts`/`config.js` file (depending on the language you've chosen above) and edit the whitelisted tokens: 64 | 65 | - `WHITELISTED_TOKENS` -- Which tokens you would like to host orderbooks for. 66 | 67 | 4. Open the `.env` file and edit the following fields. Defaults are defined in `config.ts`/`config.js`. The bash environment takes precedence over the `.env` file. If you run `source .env`, changes to the `.env` file will have no effect until you unset the colliding variables. 68 | 69 | - `CHAIN_ID` -- the chain you'd like your relayer to run on (e.g: `1` -> mainnet, `42` -> Kovan, 3 -> Ropsten, etc...). Defaults to `42` 70 | - `MESH_ENDPOINT` -- the url pointing to the 0x Mesh node. Defaults to `ws://localhost:60557` 71 | - `FEE_RECIPIENT` -- The Ethereum address which should be specified as the fee recipient in orders your relayer accepts. Defaults to a fake address that helps the 0x core team use anonymous, already public data to understand Launch Kit developer usage. Defaults to an auto-generated address 72 | - `MAKER_FEE_ASSET_DATA` -- The maker fee token asset data. Defaults to `0x`, i.e no fee 73 | - `MAKER_FEE_UNIT_AMOUNT` -- The flat maker fee amount you'd like to receive for filled orders hosted by you. Defaults to `0` 74 | - `MAKER_FEE_ASSET_DATA` -- The taker fee token asset data. Defaults to `0x`, i.e no fee 75 | - `TAKER_FEE_UNIT_AMOUNT` -- The flat taker fee you'd like to receive for filled orders hosted by you. Defaults to `0` 76 | 77 | [Instructions for using Launch Kit with Ganache](https://hackmd.io/-rC79gYWRyG7h6M9jUf5qA) 78 | 79 | 5. Make sure you have [Yarn](https://yarnpkg.com/en/) installed. 80 | 81 | 6. Install the dependencies: 82 | 83 | ```sh 84 | yarn 85 | ``` 86 | 87 | 7. Build the project [This step is for Typescript users only] 88 | 89 | ```sh 90 | yarn build:ts 91 | ``` 92 | 93 | or build & watch: 94 | 95 | ```sh 96 | yarn watch:ts 97 | ``` 98 | 99 | **Note:** There isn't currently a build step when working on the Javascript codebase because we assume `0x-launch-kit` will be running on Node.js > v8.0. If you want this project to work in an environment that doesn't support many of the latest Javascript features, you will need to add a transpiler (e.g [Babel](https://babeljs.io/)) to this project. 100 | 101 | 8. Start the relayer 102 | 103 | ```sh 104 | yarn start:ts 105 | ``` 106 | 107 | OR 108 | 109 | ```sh 110 | yarn start:js 111 | ``` 112 | 113 | ## Client for your relayer's API 114 | 115 | Since the `0x-launch-kit-backend` relayer adheres to V3 of the [Standard Relayer API Specification](https://github.com/0xProject/standard-relayer-api/), you can use [0x Connect](https://0xproject.com/docs/connect) (an HTTP/Websocket client) to make calls to your relayer (e.g submit an order, get all orders, etc...) 116 | 117 | Learn how to use 0x Connect to interact with your `0x-launch-kit` relayer in [this tutorial](https://0xproject.com/wiki#Find,-Submit,-Fill-Order-From-Relayer). 118 | 119 | To quickly check if your relayer is up-and-running, send it this CURL request from the command-line: 120 | 121 | ```sh 122 | curl http://localhost:3000/v3/orders 123 | ``` 124 | 125 | If everything is working as expected, you should see this response: 126 | 127 | ``` 128 | { 129 | "total": 0, 130 | "page": 0, 131 | "perPage": 20, 132 | "records": [] 133 | } 134 | ``` 135 | 136 | Since no orders have been added to your relayer yet, the `records` array is empty. 137 | 138 | ## Commands 139 | 140 | Typescript project commands: 141 | 142 | - `yarn build:ts` - Build the code 143 | - `yarn lint:ts` - Lint the code 144 | - `yarn start:ts` - Starts the relayer 145 | - `yarn watch:ts` - Watch the source code and rebuild on change 146 | - `yarn prettier:ts` - Auto-format the code 147 | 148 | Javascript project commands: 149 | 150 | - `yarn start:js` - Start the relayer 151 | - `yarn prettier:js` - Auto-format the code 152 | 153 | ## Database 154 | 155 | This project uses [TypeORM](https://github.com/typeorm/typeorm). It makes it easier for anyone to switch out the backing database used by this project. By default, this project uses an [SQLite](https://sqlite.org/docs.html) database. 156 | 157 | Because we want to support both Javascript and Typescript codebases, we don't use `TypeORM`'s [decorators](https://github.com/typeorm/typeorm/blob/master/docs/decorator-reference.md) (since they don't transpile nicely into readable Javascript). TypeORM shines with decorators however, so you might want to use them if you're going to be working in Typescript. 158 | 159 | ## Deployment 160 | 161 | `0x-launch-kit` ships as a docker container. First, install Docker ([mac](https://docs.docker.com/docker-for-mac/install/), [windows](https://docs.docker.com/docker-for-windows/install/)). To build the image run: 162 | 163 | ```sh 164 | docker build -t 0x-launch-kit-backend . 165 | ``` 166 | 167 | You can check that the image was built by running: 168 | 169 | ```sh 170 | docker images 171 | ``` 172 | 173 | And launch it with 174 | 175 | ```sh 176 | docker run -p 3000:3000 -d 0x-launch-kit-backend 177 | ``` 178 | 179 | Check that it's working by running 180 | 181 | ``` 182 | curl http://localhost:3000/v3/asset_pairs 183 | ``` 184 | 185 | ## Legal Disclaimer 186 | 187 | The laws and regulations applicable to the use and exchange of digital assets and blockchain-native tokens, including through any software developed using the licensed work created by ZeroEx Intl. as described here (the “Work”), vary by jurisdiction. As set forth in the Apache License, Version 2.0 applicable to the Work, developers are “solely responsible for determining the appropriateness of using or redistributing the Work,” which includes responsibility for ensuring compliance with any such applicable laws and regulations. 188 | See the Apache License, Version 2.0 for the specific language governing all applicable permissions and limitations: http://www.apache.org/licenses/LICENSE-2.0 189 | -------------------------------------------------------------------------------- /db/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xProject/0x-launch-kit-backend/b87060f41a475a014f19e0869f175ea8dd06a903/db/.gitkeep -------------------------------------------------------------------------------- /js/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | // tslint:disable:custom-no-magic-numbers 4 | var assert_1 = require('@0x/assert'); 5 | var utils_1 = require('@0x/utils'); 6 | var crypto = require('crypto'); 7 | var fs = require('fs'); 8 | var _ = require('lodash'); 9 | var path = require('path'); 10 | var constants_1 = require('./constants'); 11 | var metadataPath = path.join(__dirname, '../../metadata.json'); 12 | var EnvVarType; 13 | (function(EnvVarType) { 14 | EnvVarType[(EnvVarType['Port'] = 0)] = 'Port'; 15 | EnvVarType[(EnvVarType['ChainId'] = 1)] = 'ChainId'; 16 | EnvVarType[(EnvVarType['FeeRecipient'] = 2)] = 'FeeRecipient'; 17 | EnvVarType[(EnvVarType['UnitAmount'] = 3)] = 'UnitAmount'; 18 | EnvVarType[(EnvVarType['Url'] = 4)] = 'Url'; 19 | EnvVarType[(EnvVarType['WhitelistAllTokens'] = 5)] = 'WhitelistAllTokens'; 20 | EnvVarType[(EnvVarType['Boolean'] = 6)] = 'Boolean'; 21 | EnvVarType[(EnvVarType['FeeAssetData'] = 7)] = 'FeeAssetData'; 22 | })(EnvVarType || (EnvVarType = {})); 23 | // Whitelisted token addresses. Set to a '*' instead of an array to allow all tokens. 24 | exports.WHITELISTED_TOKENS = _.isEmpty(process.env.WHITELIST_ALL_TOKENS) 25 | ? ['0x2002d3812f58e35f0ea1ffbf80a75a38c32175fa', '0xd0a1e359811322d97991e03f863a0c30c2cf029c'] 26 | : assertEnvVarType('WHITELIST_ALL_TOKENS', process.env.WHITELIST_ALL_TOKENS, EnvVarType.WhitelistAllTokens); 27 | // Network port to listen on 28 | exports.HTTP_PORT = _.isEmpty(process.env.HTTP_PORT) 29 | ? 3000 30 | : assertEnvVarType('HTTP_PORT', process.env.HTTP_PORT, EnvVarType.Port); 31 | // Default chain id to use when not specified 32 | exports.CHAIN_ID = _.isEmpty(process.env.CHAIN_ID) 33 | ? 42 34 | : assertEnvVarType('CHAIN_ID', process.env.CHAIN_ID, EnvVarType.ChainId); 35 | // Mesh Endpoint 36 | exports.MESH_ENDPOINT = _.isEmpty(process.env.MESH_ENDPOINT) 37 | ? 'ws://localhost:60557' 38 | : assertEnvVarType('MESH_ENDPOINT', process.env.MESH_ENDPOINT, EnvVarType.Url); 39 | // The fee recipient for orders 40 | exports.FEE_RECIPIENT = _.isEmpty(process.env.FEE_RECIPIENT) 41 | ? getDefaultFeeRecipient() 42 | : assertEnvVarType('FEE_RECIPIENT', process.env.FEE_RECIPIENT, EnvVarType.FeeRecipient); 43 | // A flat fee that should be charged to the order maker 44 | exports.MAKER_FEE_UNIT_AMOUNT = _.isEmpty(process.env.MAKER_FEE_UNIT_AMOUNT) 45 | ? new utils_1.BigNumber(0) 46 | : assertEnvVarType('MAKER_FEE_UNIT_AMOUNT', process.env.MAKER_FEE_UNIT_AMOUNT, EnvVarType.UnitAmount); 47 | // A flat fee that should be charged to the order taker 48 | exports.TAKER_FEE_UNIT_AMOUNT = _.isEmpty(process.env.TAKER_FEE_UNIT_AMOUNT) 49 | ? new utils_1.BigNumber(0) 50 | : assertEnvVarType('TAKER_FEE_UNIT_AMOUNT', process.env.TAKER_FEE_UNIT_AMOUNT, EnvVarType.UnitAmount); 51 | // The maker fee token encoded as asset data 52 | exports.MAKER_FEE_ASSET_DATA = _.isEmpty(process.env.MAKER_FEE_ASSET_DATA) 53 | ? constants_1.NULL_BYTES 54 | : assertEnvVarType('MAKER_FEE_ASSET_DATA', process.env.MAKER_FEE_ASSET_DATA, EnvVarType.FeeAssetData); 55 | // The taker fee token encoded as asset data 56 | exports.TAKER_FEE_ASSET_DATA = _.isEmpty(process.env.TAKER_FEE_ASSET_DATA) 57 | ? constants_1.NULL_BYTES 58 | : assertEnvVarType('TAKER_FEE_ASSET_DATA', process.env.TAKER_FEE_ASSET_DATA, EnvVarType.FeeAssetData); 59 | // Max number of entities per page 60 | exports.MAX_PER_PAGE = 1000; 61 | // Default ERC20 token precision 62 | exports.DEFAULT_ERC20_TOKEN_PRECISION = 18; 63 | // Address used when simulating transfers from the maker as part of 0x order validation 64 | exports.DEFAULT_TAKER_SIMULATION_ADDRESS = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; 65 | function assertEnvVarType(name, value, expectedType) { 66 | var returnValue; 67 | switch (expectedType) { 68 | case EnvVarType.Port: 69 | try { 70 | returnValue = parseInt(value, 10); 71 | var isWithinRange = returnValue >= 0 && returnValue <= 65535; 72 | if (!isWithinRange) { 73 | throw new Error(); 74 | } 75 | } catch (err) { 76 | throw new Error(name + ' must be between 0 to 65535, found ' + value + '.'); 77 | } 78 | return returnValue; 79 | case EnvVarType.ChainId: 80 | try { 81 | returnValue = parseInt(value, 10); 82 | } catch (err) { 83 | throw new Error(name + ' must be a valid integer, found ' + value + '.'); 84 | } 85 | return returnValue; 86 | case EnvVarType.FeeRecipient: 87 | assert_1.assert.isETHAddressHex(name, value); 88 | return value; 89 | case EnvVarType.Url: 90 | assert_1.assert.isUri(name, value); 91 | return value; 92 | case EnvVarType.Boolean: 93 | return value === 'true'; 94 | case EnvVarType.UnitAmount: 95 | try { 96 | returnValue = new utils_1.BigNumber(parseFloat(value)); 97 | if (returnValue.isNegative()) { 98 | throw new Error(); 99 | } 100 | } catch (err) { 101 | throw new Error(name + ' must be valid number greater than 0.'); 102 | } 103 | return returnValue; 104 | case EnvVarType.WhitelistAllTokens: 105 | return '*'; 106 | case EnvVarType.FeeAssetData: 107 | assert_1.assert.isString(name, value); 108 | return value; 109 | default: 110 | throw new Error('Unrecognised EnvVarType: ' + expectedType + ' encountered for variable ' + name + '.'); 111 | } 112 | } 113 | function getDefaultFeeRecipient() { 114 | var metadata = JSON.parse(fs.readFileSync(metadataPath).toString()); 115 | var existingDefault = metadata.DEFAULT_FEE_RECIPIENT; 116 | var newDefault = existingDefault || '0xabcabc' + crypto.randomBytes(17).toString('hex'); 117 | if (_.isEmpty(existingDefault)) { 118 | var metadataCopy = JSON.parse(JSON.stringify(metadata)); 119 | metadataCopy.DEFAULT_FEE_RECIPIENT = newDefault; 120 | fs.writeFileSync(metadataPath, JSON.stringify(metadataCopy)); 121 | } 122 | return newDefault; 123 | } 124 | -------------------------------------------------------------------------------- /js/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var utils_1 = require('@0x/utils'); 4 | exports.NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; 5 | exports.NULL_BYTES = '0x'; 6 | exports.ZRX_DECIMALS = 18; 7 | exports.DEFAULT_PAGE = 1; 8 | exports.DEFAULT_PER_PAGE = 20; 9 | exports.ZERO = new utils_1.BigNumber(0); 10 | exports.MAX_TOKEN_SUPPLY_POSSIBLE = new utils_1.BigNumber(2).pow(256); // tslint:disable-line custom-no-magic-numbers 11 | -------------------------------------------------------------------------------- /js/db_connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | Object.defineProperty(exports, '__esModule', { value: true }); 131 | var typeorm_1 = require('typeorm'); 132 | var connectionIfExists; 133 | /** 134 | * Returns the DB connnection 135 | */ 136 | function getDBConnection() { 137 | if (connectionIfExists === undefined) { 138 | throw new Error('DB connection not initialized'); 139 | } 140 | return connectionIfExists; 141 | } 142 | exports.getDBConnection = getDBConnection; 143 | /** 144 | * Creates the DB connnection to use in an app 145 | */ 146 | function initDBConnectionAsync() { 147 | return __awaiter(this, void 0, void 0, function() { 148 | return __generator(this, function(_a) { 149 | switch (_a.label) { 150 | case 0: 151 | if (connectionIfExists !== undefined) { 152 | throw new Error('DB connection already exists'); 153 | } 154 | return [4 /*yield*/, typeorm_1.createConnection()]; 155 | case 1: 156 | connectionIfExists = _a.sent(); 157 | return [2 /*return*/]; 158 | } 159 | }); 160 | }); 161 | } 162 | exports.initDBConnectionAsync = initDBConnectionAsync; 163 | -------------------------------------------------------------------------------- /js/entity/SignedOrderEntity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var typeorm_1 = require('typeorm'); 4 | var SignedOrderModel_1 = require('../models/SignedOrderModel'); 5 | exports.signedOrderEntity = new typeorm_1.EntitySchema({ 6 | name: 'SignedOrder', 7 | target: SignedOrderModel_1.SignedOrderModel, 8 | columns: { 9 | hash: { 10 | primary: true, 11 | type: 'varchar', 12 | }, 13 | senderAddress: { 14 | type: 'varchar', 15 | }, 16 | makerAddress: { 17 | type: 'varchar', 18 | }, 19 | takerAddress: { 20 | type: 'varchar', 21 | }, 22 | makerAssetData: { 23 | type: 'varchar', 24 | }, 25 | takerAssetData: { 26 | type: 'varchar', 27 | }, 28 | exchangeAddress: { 29 | type: 'varchar', 30 | }, 31 | feeRecipientAddress: { 32 | type: 'varchar', 33 | }, 34 | expirationTimeSeconds: { 35 | type: 'varchar', 36 | }, 37 | makerFee: { 38 | type: 'varchar', 39 | }, 40 | takerFee: { 41 | type: 'varchar', 42 | }, 43 | makerFeeAssetData: { 44 | type: 'varchar', 45 | }, 46 | takerFeeAssetData: { 47 | type: 'varchar', 48 | }, 49 | makerAssetAmount: { 50 | type: 'varchar', 51 | }, 52 | takerAssetAmount: { 53 | type: 'varchar', 54 | }, 55 | salt: { 56 | type: 'varchar', 57 | }, 58 | signature: { 59 | type: 'varchar', 60 | }, 61 | remainingFillableTakerAssetAmount: { 62 | type: 'varchar', 63 | }, 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /js/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __extends = 3 | (this && this.__extends) || 4 | (function() { 5 | var extendStatics = function(d, b) { 6 | extendStatics = 7 | Object.setPrototypeOf || 8 | ({ __proto__: [] } instanceof Array && 9 | function(d, b) { 10 | d.__proto__ = b; 11 | }) || 12 | function(d, b) { 13 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 14 | }; 15 | return extendStatics(d, b); 16 | }; 17 | return function(d, b) { 18 | extendStatics(d, b); 19 | function __() { 20 | this.constructor = d; 21 | } 22 | d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); 23 | }; 24 | })(); 25 | Object.defineProperty(exports, '__esModule', { value: true }); 26 | var _a; 27 | // tslint:disable:max-classes-per-file 28 | var RelayerBaseError = /** @class */ (function(_super) { 29 | __extends(RelayerBaseError, _super); 30 | function RelayerBaseError() { 31 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 32 | _this.isRelayerError = true; 33 | return _this; 34 | } 35 | return RelayerBaseError; 36 | })(Error); 37 | exports.RelayerBaseError = RelayerBaseError; 38 | var BadRequestError = /** @class */ (function(_super) { 39 | __extends(BadRequestError, _super); 40 | function BadRequestError() { 41 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 42 | _this.statusCode = 400; 43 | return _this; 44 | } 45 | return BadRequestError; 46 | })(RelayerBaseError); 47 | exports.BadRequestError = BadRequestError; 48 | var ValidationError = /** @class */ (function(_super) { 49 | __extends(ValidationError, _super); 50 | function ValidationError(validationErrors) { 51 | var _this = _super.call(this) || this; 52 | _this.generalErrorCode = GeneralErrorCodes.ValidationError; 53 | _this.validationErrors = validationErrors; 54 | return _this; 55 | } 56 | return ValidationError; 57 | })(BadRequestError); 58 | exports.ValidationError = ValidationError; 59 | var MalformedJSONError = /** @class */ (function(_super) { 60 | __extends(MalformedJSONError, _super); 61 | function MalformedJSONError() { 62 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 63 | _this.generalErrorCode = GeneralErrorCodes.MalformedJson; 64 | return _this; 65 | } 66 | return MalformedJSONError; 67 | })(BadRequestError); 68 | exports.MalformedJSONError = MalformedJSONError; 69 | var TooManyRequestsError = /** @class */ (function(_super) { 70 | __extends(TooManyRequestsError, _super); 71 | function TooManyRequestsError() { 72 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 73 | _this.statusCode = 429; 74 | _this.generalErrorCode = GeneralErrorCodes.Throttled; 75 | return _this; 76 | } 77 | return TooManyRequestsError; 78 | })(BadRequestError); 79 | exports.TooManyRequestsError = TooManyRequestsError; 80 | var NotImplementedError = /** @class */ (function(_super) { 81 | __extends(NotImplementedError, _super); 82 | function NotImplementedError() { 83 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 84 | _this.statusCode = 501; 85 | _this.generalErrorCode = GeneralErrorCodes.NotImplemented; 86 | return _this; 87 | } 88 | return NotImplementedError; 89 | })(BadRequestError); 90 | exports.NotImplementedError = NotImplementedError; 91 | var NotFoundError = /** @class */ (function(_super) { 92 | __extends(NotFoundError, _super); 93 | function NotFoundError() { 94 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 95 | _this.statusCode = 404; 96 | return _this; 97 | } 98 | return NotFoundError; 99 | })(RelayerBaseError); 100 | exports.NotFoundError = NotFoundError; 101 | var InternalServerError = /** @class */ (function(_super) { 102 | __extends(InternalServerError, _super); 103 | function InternalServerError() { 104 | var _this = (_super !== null && _super.apply(this, arguments)) || this; 105 | _this.statusCode = 500; 106 | return _this; 107 | } 108 | return InternalServerError; 109 | })(RelayerBaseError); 110 | exports.InternalServerError = InternalServerError; 111 | var GeneralErrorCodes; 112 | (function(GeneralErrorCodes) { 113 | GeneralErrorCodes[(GeneralErrorCodes['ValidationError'] = 100)] = 'ValidationError'; 114 | GeneralErrorCodes[(GeneralErrorCodes['MalformedJson'] = 101)] = 'MalformedJson'; 115 | GeneralErrorCodes[(GeneralErrorCodes['OrderSubmissionDisabled'] = 102)] = 'OrderSubmissionDisabled'; 116 | GeneralErrorCodes[(GeneralErrorCodes['Throttled'] = 103)] = 'Throttled'; 117 | GeneralErrorCodes[(GeneralErrorCodes['NotImplemented'] = 104)] = 'NotImplemented'; 118 | })((GeneralErrorCodes = exports.GeneralErrorCodes || (exports.GeneralErrorCodes = {}))); 119 | exports.generalErrorCodeToReason = 120 | ((_a = {}), 121 | (_a[GeneralErrorCodes.ValidationError] = 'Validation Failed'), 122 | (_a[GeneralErrorCodes.MalformedJson] = 'Malformed JSON'), 123 | (_a[GeneralErrorCodes.OrderSubmissionDisabled] = 'Order submission disabled'), 124 | (_a[GeneralErrorCodes.Throttled] = 'Throttled'), 125 | (_a[GeneralErrorCodes.NotImplemented] = 'Not Implemented'), 126 | _a); 127 | var ValidationErrorCodes; 128 | (function(ValidationErrorCodes) { 129 | ValidationErrorCodes[(ValidationErrorCodes['RequiredField'] = 1000)] = 'RequiredField'; 130 | ValidationErrorCodes[(ValidationErrorCodes['IncorrectFormat'] = 1001)] = 'IncorrectFormat'; 131 | ValidationErrorCodes[(ValidationErrorCodes['InvalidAddress'] = 1002)] = 'InvalidAddress'; 132 | ValidationErrorCodes[(ValidationErrorCodes['AddressNotSupported'] = 1003)] = 'AddressNotSupported'; 133 | ValidationErrorCodes[(ValidationErrorCodes['ValueOutOfRange'] = 1004)] = 'ValueOutOfRange'; 134 | ValidationErrorCodes[(ValidationErrorCodes['InvalidSignatureOrHash'] = 1005)] = 'InvalidSignatureOrHash'; 135 | ValidationErrorCodes[(ValidationErrorCodes['UnsupportedOption'] = 1006)] = 'UnsupportedOption'; 136 | ValidationErrorCodes[(ValidationErrorCodes['InvalidOrder'] = 1007)] = 'InvalidOrder'; 137 | ValidationErrorCodes[(ValidationErrorCodes['InternalError'] = 1008)] = 'InternalError'; 138 | })((ValidationErrorCodes = exports.ValidationErrorCodes || (exports.ValidationErrorCodes = {}))); 139 | -------------------------------------------------------------------------------- /js/fee_strategy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var config_1 = require('./config'); 4 | var constants_1 = require('./constants'); 5 | exports.fixedFeeStrategy = { 6 | getOrderConfig: function(_order) { 7 | var normalizedFeeRecipient = config_1.FEE_RECIPIENT.toLowerCase(); 8 | var orderConfigResponse = { 9 | senderAddress: constants_1.NULL_ADDRESS, 10 | feeRecipientAddress: normalizedFeeRecipient, 11 | makerFee: config_1.MAKER_FEE_UNIT_AMOUNT, 12 | takerFee: config_1.TAKER_FEE_UNIT_AMOUNT, 13 | makerFeeAssetData: config_1.MAKER_FEE_ASSET_DATA, 14 | takerFeeAssetData: config_1.TAKER_FEE_ASSET_DATA, 15 | }; 16 | return orderConfigResponse; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var _this = this; 131 | Object.defineProperty(exports, '__esModule', { value: true }); 132 | var mesh_rpc_client_1 = require('@0x/mesh-rpc-client'); 133 | var express = require('express'); 134 | require('reflect-metadata'); 135 | var config = require('./config'); 136 | var db_connection_1 = require('./db_connection'); 137 | var http_service_1 = require('./services/http_service'); 138 | var order_watcher_service_1 = require('./services/order_watcher_service'); 139 | var orderbook_service_1 = require('./services/orderbook_service'); 140 | var websocket_service_1 = require('./services/websocket_service'); 141 | var utils_1 = require('./utils'); 142 | (function() { 143 | return __awaiter(_this, void 0, void 0, function() { 144 | var app, server, meshClient, orderWatcherService, orderBookService; 145 | var _this = this; 146 | return __generator(this, function(_a) { 147 | switch (_a.label) { 148 | case 0: 149 | return [4 /*yield*/, db_connection_1.initDBConnectionAsync()]; 150 | case 1: 151 | _a.sent(); 152 | app = express(); 153 | server = app.listen(config.HTTP_PORT, function() { 154 | utils_1.utils.log( 155 | 'Standard relayer API (HTTP) listening on port ' + 156 | config.HTTP_PORT + 157 | '!\nConfig: ' + 158 | JSON.stringify(config, null, 2), 159 | ); 160 | }); 161 | return [ 162 | 4 /*yield*/, 163 | utils_1.utils.attemptAsync( 164 | function() { 165 | return __awaiter(_this, void 0, void 0, function() { 166 | return __generator(this, function(_a) { 167 | switch (_a.label) { 168 | case 0: 169 | meshClient = new mesh_rpc_client_1.WSClient(config.MESH_ENDPOINT); 170 | return [4 /*yield*/, meshClient.getStatsAsync()]; 171 | case 1: 172 | _a.sent(); 173 | return [2 /*return*/]; 174 | } 175 | }); 176 | }); 177 | }, 178 | { interval: 3000, maxRetries: 10 }, 179 | ), 180 | ]; 181 | case 2: 182 | _a.sent(); 183 | if (!meshClient) { 184 | throw new Error('Unable to establish connection to Mesh'); 185 | } 186 | utils_1.utils.log('Connected to Mesh'); 187 | orderWatcherService = new order_watcher_service_1.OrderWatcherService(meshClient); 188 | return [4 /*yield*/, orderWatcherService.syncOrderbookAsync()]; 189 | case 3: 190 | _a.sent(); 191 | // tslint:disable-next-line:no-unused-expression 192 | new websocket_service_1.WebsocketService(server, meshClient); 193 | orderBookService = new orderbook_service_1.OrderBookService(meshClient); 194 | // tslint:disable-next-line:no-unused-expression 195 | new http_service_1.HttpService(app, orderBookService); 196 | return [2 /*return*/]; 197 | } 198 | }); 199 | }); 200 | })().catch(utils_1.utils.log); 201 | -------------------------------------------------------------------------------- /js/mesh_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var __values = 131 | (this && this.__values) || 132 | function(o) { 133 | var m = typeof Symbol === 'function' && o[Symbol.iterator], 134 | i = 0; 135 | if (m) return m.call(o); 136 | return { 137 | next: function() { 138 | if (o && i >= o.length) o = void 0; 139 | return { value: o && o[i++], done: !o }; 140 | }, 141 | }; 142 | }; 143 | var __read = 144 | (this && this.__read) || 145 | function(o, n) { 146 | var m = typeof Symbol === 'function' && o[Symbol.iterator]; 147 | if (!m) return o; 148 | var i = m.call(o), 149 | r, 150 | ar = [], 151 | e; 152 | try { 153 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 154 | } catch (error) { 155 | e = { error: error }; 156 | } finally { 157 | try { 158 | if (r && !r.done && (m = i['return'])) m.call(i); 159 | } finally { 160 | if (e) throw e.error; 161 | } 162 | } 163 | return ar; 164 | }; 165 | var __spread = 166 | (this && this.__spread) || 167 | function() { 168 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 169 | return ar; 170 | }; 171 | Object.defineProperty(exports, '__esModule', { value: true }); 172 | var mesh_rpc_client_1 = require('@0x/mesh-rpc-client'); 173 | var _ = require('lodash'); 174 | var constants_1 = require('./constants'); 175 | var errors_1 = require('./errors'); 176 | // tslint:disable-next-line:no-var-requires 177 | var d = require('debug')('MESH'); 178 | // tslint:disable-next-line:no-unnecessary-class 179 | var MeshUtils = /** @class */ (function() { 180 | function MeshUtils() {} 181 | MeshUtils.addOrdersToMeshAsync = function(meshClient, orders, batchSize) { 182 | if (batchSize === void 0) { 183 | batchSize = 100; 184 | } 185 | return __awaiter(this, void 0, void 0, function() { 186 | var e_1, _a, validationResults, chunks, chunks_1, chunks_1_1, chunk, results, e_1_1; 187 | return __generator(this, function(_b) { 188 | switch (_b.label) { 189 | case 0: 190 | validationResults = { accepted: [], rejected: [] }; 191 | chunks = _.chunk(orders, batchSize); 192 | _b.label = 1; 193 | case 1: 194 | _b.trys.push([1, 6, 7, 8]); 195 | (chunks_1 = __values(chunks)), (chunks_1_1 = chunks_1.next()); 196 | _b.label = 2; 197 | case 2: 198 | if (!!chunks_1_1.done) return [3 /*break*/, 5]; 199 | chunk = chunks_1_1.value; 200 | return [4 /*yield*/, meshClient.addOrdersAsync(chunk, true)]; 201 | case 3: 202 | results = _b.sent(); 203 | validationResults.accepted = __spread(validationResults.accepted, results.accepted); 204 | validationResults.rejected = __spread(validationResults.rejected, results.rejected); 205 | _b.label = 4; 206 | case 4: 207 | chunks_1_1 = chunks_1.next(); 208 | return [3 /*break*/, 2]; 209 | case 5: 210 | return [3 /*break*/, 8]; 211 | case 6: 212 | e_1_1 = _b.sent(); 213 | e_1 = { error: e_1_1 }; 214 | return [3 /*break*/, 8]; 215 | case 7: 216 | try { 217 | if (chunks_1_1 && !chunks_1_1.done && (_a = chunks_1.return)) _a.call(chunks_1); 218 | } finally { 219 | if (e_1) throw e_1.error; 220 | } 221 | return [7 /*endfinally*/]; 222 | case 8: 223 | return [2 /*return*/, validationResults]; 224 | } 225 | }); 226 | }); 227 | }; 228 | MeshUtils.orderInfosToApiOrders = function(orderEvent) { 229 | return orderEvent.map(function(e) { 230 | return MeshUtils.orderInfoToAPIOrder(e); 231 | }); 232 | }; 233 | MeshUtils.orderInfoToAPIOrder = function(orderEvent) { 234 | var remainingFillableTakerAssetAmount = orderEvent.fillableTakerAssetAmount 235 | ? orderEvent.fillableTakerAssetAmount 236 | : constants_1.ZERO; 237 | return { 238 | order: orderEvent.signedOrder, 239 | metaData: { 240 | orderHash: orderEvent.orderHash, 241 | remainingFillableTakerAssetAmount: remainingFillableTakerAssetAmount, 242 | }, 243 | }; 244 | }; 245 | MeshUtils.rejectedCodeToSRACode = function(code) { 246 | switch (code) { 247 | case mesh_rpc_client_1.RejectedCode.OrderCancelled: 248 | case mesh_rpc_client_1.RejectedCode.OrderExpired: 249 | case mesh_rpc_client_1.RejectedCode.OrderUnfunded: 250 | case mesh_rpc_client_1.RejectedCode.OrderHasInvalidMakerAssetAmount: 251 | case mesh_rpc_client_1.RejectedCode.OrderHasInvalidMakerAssetData: 252 | case mesh_rpc_client_1.RejectedCode.OrderHasInvalidTakerAssetAmount: 253 | case mesh_rpc_client_1.RejectedCode.OrderHasInvalidTakerAssetData: 254 | case mesh_rpc_client_1.RejectedCode.OrderFullyFilled: { 255 | return errors_1.ValidationErrorCodes.InvalidOrder; 256 | } 257 | case mesh_rpc_client_1.RejectedCode.OrderHasInvalidSignature: { 258 | return errors_1.ValidationErrorCodes.InvalidSignatureOrHash; 259 | } 260 | case mesh_rpc_client_1.RejectedCode.OrderForIncorrectChain: { 261 | return errors_1.ValidationErrorCodes.InvalidAddress; 262 | } 263 | default: 264 | return errors_1.ValidationErrorCodes.InternalError; 265 | } 266 | }; 267 | MeshUtils.calculateAddedRemovedUpdated = function(orderEvents) { 268 | var e_2, _a; 269 | var added = []; 270 | var removed = []; 271 | var updated = []; 272 | try { 273 | for ( 274 | var orderEvents_1 = __values(orderEvents), orderEvents_1_1 = orderEvents_1.next(); 275 | !orderEvents_1_1.done; 276 | orderEvents_1_1 = orderEvents_1.next() 277 | ) { 278 | var event_1 = orderEvents_1_1.value; 279 | var apiOrder = MeshUtils.orderInfoToAPIOrder(event_1); 280 | switch (event_1.endState) { 281 | case mesh_rpc_client_1.OrderEventEndState.Added: { 282 | added.push(apiOrder); 283 | break; 284 | } 285 | case mesh_rpc_client_1.OrderEventEndState.Cancelled: 286 | case mesh_rpc_client_1.OrderEventEndState.Expired: 287 | case mesh_rpc_client_1.OrderEventEndState.FullyFilled: 288 | case mesh_rpc_client_1.OrderEventEndState.Unfunded: { 289 | removed.push(apiOrder); 290 | break; 291 | } 292 | case mesh_rpc_client_1.OrderEventEndState.FillabilityIncreased: 293 | case mesh_rpc_client_1.OrderEventEndState.Filled: { 294 | updated.push(apiOrder); 295 | break; 296 | } 297 | default: 298 | d('Unknown Event', event_1.endState, event_1); 299 | break; 300 | } 301 | } 302 | } catch (e_2_1) { 303 | e_2 = { error: e_2_1 }; 304 | } finally { 305 | try { 306 | if (orderEvents_1_1 && !orderEvents_1_1.done && (_a = orderEvents_1.return)) _a.call(orderEvents_1); 307 | } finally { 308 | if (e_2) throw e_2.error; 309 | } 310 | } 311 | return { added: added, removed: removed, updated: updated }; 312 | }; 313 | return MeshUtils; 314 | })(); 315 | exports.MeshUtils = MeshUtils; 316 | -------------------------------------------------------------------------------- /js/middleware/error_handling.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var HttpStatus = require('http-status-codes'); 4 | var errors_1 = require('../errors'); 5 | /** 6 | * Wraps an Error with a JSON human readable reason and status code. 7 | */ 8 | function generateError(err) { 9 | if (err.isRelayerError) { 10 | var relayerError = err; 11 | var statusCode = relayerError.statusCode; 12 | if (relayerError.statusCode === HttpStatus.BAD_REQUEST) { 13 | var badRequestError = relayerError; 14 | if (badRequestError.generalErrorCode === errors_1.GeneralErrorCodes.ValidationError) { 15 | var validationError = badRequestError; 16 | return { 17 | statusCode: statusCode, 18 | errorBody: { 19 | code: badRequestError.generalErrorCode, 20 | reason: errors_1.generalErrorCodeToReason[badRequestError.generalErrorCode], 21 | validationErrors: validationError.validationErrors, 22 | }, 23 | }; 24 | } else { 25 | return { 26 | statusCode: statusCode, 27 | errorBody: { 28 | code: badRequestError.generalErrorCode, 29 | reason: errors_1.generalErrorCodeToReason[badRequestError.generalErrorCode], 30 | }, 31 | }; 32 | } 33 | } else { 34 | return { 35 | statusCode: statusCode, 36 | errorBody: { 37 | reason: HttpStatus.getStatusText(relayerError.statusCode), 38 | }, 39 | }; 40 | } 41 | } 42 | return { 43 | statusCode: HttpStatus.BAD_REQUEST, 44 | errorBody: { 45 | reason: err.message, 46 | }, 47 | }; 48 | } 49 | exports.generateError = generateError; 50 | /** 51 | * Catches errors thrown by our code and serialies them 52 | */ 53 | function errorHandler(err, _req, res, next) { 54 | // If you call next() with an error after you have started writing the response 55 | // (for example, if you encounter an error while streaming the response to the client) 56 | // the Express default error handler closes the connection and fails the request. 57 | if (res.headersSent) { 58 | return next(err); 59 | } 60 | if (err.isRelayerError || err.statusCode) { 61 | var _a = generateError(err), 62 | statusCode = _a.statusCode, 63 | errorBody = _a.errorBody; 64 | res.status(statusCode).send(errorBody); 65 | return; 66 | } else { 67 | return next(err); 68 | } 69 | } 70 | exports.errorHandler = errorHandler; 71 | -------------------------------------------------------------------------------- /js/middleware/url_params_parsing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var _ = require('lodash'); 4 | var config_1 = require('../config'); 5 | var errors_1 = require('../errors'); 6 | /** 7 | * Parses URL params and stores them on the request object 8 | */ 9 | function urlParamsParsing(req, _res, next) { 10 | var chainId = parseChainId(req.query.chainId); 11 | // HACK: This is the recommended way to pass data from middlewares on. It's not beautiful nor fully type-safe. 12 | req.chainId = chainId; 13 | next(); 14 | } 15 | exports.urlParamsParsing = urlParamsParsing; 16 | function parseChainId(chainIdStrIfExists) { 17 | if (chainIdStrIfExists === undefined) { 18 | return config_1.CHAIN_ID; 19 | } else { 20 | var chainId = _.parseInt(chainIdStrIfExists); 21 | if (chainId !== config_1.CHAIN_ID) { 22 | var validationErrorItem = { 23 | field: 'chainId', 24 | code: 1004, 25 | reason: 'Incorrect Chain ID: ' + chainIdStrIfExists, 26 | }; 27 | throw new errors_1.ValidationError([validationErrorItem]); 28 | } 29 | return chainId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /js/models/SignedOrderModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var SignedOrderModel = /** @class */ (function() { 4 | function SignedOrderModel(opts) { 5 | if (opts === void 0) { 6 | opts = {}; 7 | } 8 | this.hash = opts.hash; 9 | this.senderAddress = opts.senderAddress; 10 | this.makerAddress = opts.makerAddress; 11 | this.takerAddress = opts.takerAddress; 12 | this.makerAssetData = opts.makerAssetData; 13 | this.takerAssetData = opts.takerAssetData; 14 | this.exchangeAddress = opts.exchangeAddress; 15 | this.feeRecipientAddress = opts.feeRecipientAddress; 16 | this.expirationTimeSeconds = opts.expirationTimeSeconds; 17 | this.makerFee = opts.makerFee; 18 | this.takerFee = opts.takerFee; 19 | this.makerFeeAssetData = opts.makerFeeAssetData; 20 | this.takerFeeAssetData = opts.takerFeeAssetData; 21 | this.makerAssetAmount = opts.makerAssetAmount; 22 | this.takerAssetAmount = opts.takerAssetAmount; 23 | this.salt = opts.salt; 24 | this.signature = opts.signature; 25 | this.remainingFillableTakerAssetAmount = opts.remainingFillableTakerAssetAmount; 26 | } 27 | return SignedOrderModel; 28 | })(); 29 | exports.SignedOrderModel = SignedOrderModel; 30 | -------------------------------------------------------------------------------- /js/paginator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | exports.paginate = function(collection, page, perPage) { 4 | var paginatedCollection = { 5 | total: collection.length, 6 | page: page, 7 | perPage: perPage, 8 | records: collection.slice((page - 1) * perPage, page * perPage), 9 | }; 10 | return paginatedCollection; 11 | }; 12 | -------------------------------------------------------------------------------- /js/runners/http_service_runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var _this = this; 131 | Object.defineProperty(exports, '__esModule', { value: true }); 132 | var mesh_rpc_client_1 = require('@0x/mesh-rpc-client'); 133 | var express = require('express'); 134 | require('reflect-metadata'); 135 | var config = require('../config'); 136 | var db_connection_1 = require('../db_connection'); 137 | var http_service_1 = require('../services/http_service'); 138 | var orderbook_service_1 = require('../services/orderbook_service'); 139 | var utils_1 = require('../utils'); 140 | /** 141 | * This service handles the HTTP requests. This involves fetching from the database 142 | * as well as adding orders to mesh. 143 | */ 144 | (function() { 145 | return __awaiter(_this, void 0, void 0, function() { 146 | var app, meshClient, orderBookService; 147 | return __generator(this, function(_a) { 148 | switch (_a.label) { 149 | case 0: 150 | return [4 /*yield*/, db_connection_1.initDBConnectionAsync()]; 151 | case 1: 152 | _a.sent(); 153 | app = express(); 154 | app.listen(config.HTTP_PORT, function() { 155 | utils_1.utils.log( 156 | 'Standard relayer API (HTTP) listening on port ' + 157 | config.HTTP_PORT + 158 | '!\nConfig: ' + 159 | JSON.stringify(config, null, 2), 160 | ); 161 | }); 162 | meshClient = new mesh_rpc_client_1.WSClient(config.MESH_ENDPOINT); 163 | orderBookService = new orderbook_service_1.OrderBookService(meshClient); 164 | // tslint:disable-next-line:no-unused-expression 165 | new http_service_1.HttpService(app, orderBookService); 166 | return [2 /*return*/]; 167 | } 168 | }); 169 | }); 170 | })().catch(utils_1.utils.log); 171 | -------------------------------------------------------------------------------- /js/runners/order_watcher_service_runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var _this = this; 131 | Object.defineProperty(exports, '__esModule', { value: true }); 132 | var mesh_rpc_client_1 = require('@0x/mesh-rpc-client'); 133 | require('reflect-metadata'); 134 | var config = require('../config'); 135 | var db_connection_1 = require('../db_connection'); 136 | var order_watcher_service_1 = require('../services/order_watcher_service'); 137 | var utils_1 = require('../utils'); 138 | /** 139 | * This service is a simple writer from the Mesh events. On order discovery 140 | * or an order update it will be persisted to the database. It also is responsible 141 | * for syncing the database with Mesh on start or after a disconnect. 142 | */ 143 | (function() { 144 | return __awaiter(_this, void 0, void 0, function() { 145 | var meshClient, orderWatcherService; 146 | return __generator(this, function(_a) { 147 | switch (_a.label) { 148 | case 0: 149 | return [4 /*yield*/, db_connection_1.initDBConnectionAsync()]; 150 | case 1: 151 | _a.sent(); 152 | utils_1.utils.log('Order Watching Service started!\nConfig: ' + JSON.stringify(config, null, 2)); 153 | meshClient = new mesh_rpc_client_1.WSClient(config.MESH_ENDPOINT); 154 | orderWatcherService = new order_watcher_service_1.OrderWatcherService(meshClient); 155 | return [4 /*yield*/, orderWatcherService.syncOrderbookAsync()]; 156 | case 2: 157 | _a.sent(); 158 | return [2 /*return*/]; 159 | } 160 | }); 161 | }); 162 | })().catch(utils_1.utils.log); 163 | -------------------------------------------------------------------------------- /js/runners/websocket_service_runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var _this = this; 131 | Object.defineProperty(exports, '__esModule', { value: true }); 132 | var mesh_rpc_client_1 = require('@0x/mesh-rpc-client'); 133 | var express = require('express'); 134 | require('reflect-metadata'); 135 | var config = require('../config'); 136 | var db_connection_1 = require('../db_connection'); 137 | var websocket_service_1 = require('../services/websocket_service'); 138 | var utils_1 = require('../utils'); 139 | /** 140 | * This service handles websocket updates using a subscription from Mesh. 141 | */ 142 | (function() { 143 | return __awaiter(_this, void 0, void 0, function() { 144 | var app, server, meshClient; 145 | return __generator(this, function(_a) { 146 | switch (_a.label) { 147 | case 0: 148 | return [4 /*yield*/, db_connection_1.initDBConnectionAsync()]; 149 | case 1: 150 | _a.sent(); 151 | app = express(); 152 | server = app.listen(config.HTTP_PORT, function() { 153 | utils_1.utils.log( 154 | 'Standard relayer API (WS) listening on port ' + 155 | config.HTTP_PORT + 156 | '!\nConfig: ' + 157 | JSON.stringify(config, null, 2), 158 | ); 159 | }); 160 | meshClient = new mesh_rpc_client_1.WSClient(config.MESH_ENDPOINT); 161 | // tslint:disable-next-line:no-unused-expression 162 | new websocket_service_1.WebsocketService(server, meshClient); 163 | return [2 /*return*/]; 164 | } 165 | }); 166 | }); 167 | })().catch(utils_1.utils.log); 168 | -------------------------------------------------------------------------------- /js/services/http_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var bodyParser = require('body-parser'); 4 | var cors = require('cors'); 5 | var asyncHandler = require('express-async-handler'); 6 | require('reflect-metadata'); 7 | var handlers_1 = require('../handlers'); 8 | var error_handling_1 = require('../middleware/error_handling'); 9 | var url_params_parsing_1 = require('../middleware/url_params_parsing'); 10 | // tslint:disable-next-line:no-unnecessary-class 11 | var HttpService = /** @class */ (function() { 12 | function HttpService(app, orderBook) { 13 | var handlers = new handlers_1.Handlers(orderBook); 14 | app.use(cors()); 15 | app.use(bodyParser.json()); 16 | app.use(url_params_parsing_1.urlParamsParsing); 17 | /** 18 | * GET AssetPairs endpoint retrieves a list of available asset pairs and the information required to trade them. 19 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getAssetPairs 20 | */ 21 | app.get('/v3/asset_pairs', asyncHandler(handlers_1.Handlers.assetPairsAsync.bind(handlers_1.Handlers))); 22 | /** 23 | * GET Orders endpoint retrieves a list of orders given query parameters. 24 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrders 25 | */ 26 | app.get('/v3/orders', asyncHandler(handlers.ordersAsync.bind(handlers))); 27 | /** 28 | * GET Orderbook endpoint retrieves the orderbook for a given asset pair. 29 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrderbook 30 | */ 31 | app.get('/v3/orderbook', asyncHandler(handlers.orderbookAsync.bind(handlers))); 32 | /** 33 | * GET FeeRecepients endpoint retrieves a collection of all fee recipient addresses for a relayer. 34 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/v3/fee_recipients 35 | */ 36 | app.get('/v3/fee_recipients', handlers_1.Handlers.feeRecipients.bind(handlers_1.Handlers)); 37 | /** 38 | * POST Order config endpoint retrives the values for order fields that the relayer requires. 39 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrderConfig 40 | */ 41 | app.post('/v3/order_config', handlers_1.Handlers.orderConfig.bind(handlers_1.Handlers)); 42 | /** 43 | * POST Order endpoint submits an order to the Relayer. 44 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/postOrder 45 | */ 46 | app.post('/v3/order', asyncHandler(handlers.postOrderAsync.bind(handlers))); 47 | /** 48 | * GET Order endpoint retrieves the order by order hash. 49 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrder 50 | */ 51 | app.get( 52 | '/v3/order/:orderHash', 53 | asyncHandler(handlers_1.Handlers.getOrderByHashAsync.bind(handlers_1.Handlers)), 54 | ); 55 | app.use(error_handling_1.errorHandler); 56 | } 57 | return HttpService; 58 | })(); 59 | exports.HttpService = HttpService; 60 | -------------------------------------------------------------------------------- /js/services/order_watcher_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var __values = 131 | (this && this.__values) || 132 | function(o) { 133 | var m = typeof Symbol === 'function' && o[Symbol.iterator], 134 | i = 0; 135 | if (m) return m.call(o); 136 | return { 137 | next: function() { 138 | if (o && i >= o.length) o = void 0; 139 | return { value: o && o[i++], done: !o }; 140 | }, 141 | }; 142 | }; 143 | Object.defineProperty(exports, '__esModule', { value: true }); 144 | var _ = require('lodash'); 145 | var db_connection_1 = require('../db_connection'); 146 | var mesh_utils_1 = require('../mesh_utils'); 147 | var SignedOrderModel_1 = require('../models/SignedOrderModel'); 148 | var types_1 = require('../types'); 149 | var orderbook_utils_1 = require('./orderbook_utils'); 150 | // tslint:disable-next-line:no-var-requires 151 | var d = require('debug')('orderbook'); 152 | var OrderWatcherService = /** @class */ (function() { 153 | function OrderWatcherService(meshClient) { 154 | var _this = this; 155 | this._meshClient = meshClient; 156 | void this._meshClient.subscribeToOrdersAsync(function(orders) { 157 | return __awaiter(_this, void 0, void 0, function() { 158 | var _a, added, removed, updated; 159 | return __generator(this, function(_b) { 160 | switch (_b.label) { 161 | case 0: 162 | (_a = mesh_utils_1.MeshUtils.calculateAddedRemovedUpdated(orders)), 163 | (added = _a.added), 164 | (removed = _a.removed), 165 | (updated = _a.updated); 166 | return [ 167 | 4 /*yield*/, 168 | OrderWatcherService._onOrderLifeCycleEventAsync( 169 | types_1.OrderWatcherLifeCycleEvents.Removed, 170 | removed, 171 | ), 172 | ]; 173 | case 1: 174 | _b.sent(); 175 | return [ 176 | 4 /*yield*/, 177 | OrderWatcherService._onOrderLifeCycleEventAsync( 178 | types_1.OrderWatcherLifeCycleEvents.Updated, 179 | updated, 180 | ), 181 | ]; 182 | case 2: 183 | _b.sent(); 184 | return [ 185 | 4 /*yield*/, 186 | OrderWatcherService._onOrderLifeCycleEventAsync( 187 | types_1.OrderWatcherLifeCycleEvents.Added, 188 | added, 189 | ), 190 | ]; 191 | case 3: 192 | _b.sent(); 193 | return [2 /*return*/]; 194 | } 195 | }); 196 | }); 197 | }); 198 | this._meshClient.onReconnected(function() { 199 | return __awaiter(_this, void 0, void 0, function() { 200 | return __generator(this, function(_a) { 201 | switch (_a.label) { 202 | case 0: 203 | d('Reconnecting to Mesh'); 204 | return [4 /*yield*/, this.syncOrderbookAsync()]; 205 | case 1: 206 | _a.sent(); 207 | return [2 /*return*/]; 208 | } 209 | }); 210 | }); 211 | }); 212 | } 213 | OrderWatcherService._onOrderLifeCycleEventAsync = function(lifecycleEvent, orders) { 214 | return __awaiter(this, void 0, void 0, function() { 215 | var e_1, _a, connection, _b, signedOrdersModel, orderHashes, chunks, chunks_1, chunks_1_1, chunk, e_1_1; 216 | return __generator(this, function(_c) { 217 | switch (_c.label) { 218 | case 0: 219 | if (orders.length <= 0) { 220 | return [2 /*return*/]; 221 | } 222 | connection = db_connection_1.getDBConnection(); 223 | _b = lifecycleEvent; 224 | switch (_b) { 225 | case types_1.OrderWatcherLifeCycleEvents.Updated: 226 | return [3 /*break*/, 1]; 227 | case types_1.OrderWatcherLifeCycleEvents.Added: 228 | return [3 /*break*/, 1]; 229 | case types_1.OrderWatcherLifeCycleEvents.Removed: 230 | return [3 /*break*/, 3]; 231 | } 232 | return [3 /*break*/, 12]; 233 | case 1: 234 | signedOrdersModel = orders.map(function(o) { 235 | return orderbook_utils_1.serializeOrder(o); 236 | }); 237 | // MAX SQL variable size is 999. This limit is imposed via Sqlite. 238 | // The SELECT query is not entirely effecient and pulls in all attributes 239 | // so we need to leave space for the attributes on the model represented 240 | // as SQL variables in the "AS" syntax. We leave 99 free for the 241 | // signedOrders model 242 | return [4 /*yield*/, connection.manager.save(signedOrdersModel, { chunk: 900 })]; 243 | case 2: 244 | // MAX SQL variable size is 999. This limit is imposed via Sqlite. 245 | // The SELECT query is not entirely effecient and pulls in all attributes 246 | // so we need to leave space for the attributes on the model represented 247 | // as SQL variables in the "AS" syntax. We leave 99 free for the 248 | // signedOrders model 249 | _c.sent(); 250 | return [3 /*break*/, 12]; 251 | case 3: 252 | orderHashes = orders.map(function(o) { 253 | return o.metaData.orderHash; 254 | }); 255 | chunks = _.chunk(orderHashes, 999); 256 | _c.label = 4; 257 | case 4: 258 | _c.trys.push([4, 9, 10, 11]); 259 | (chunks_1 = __values(chunks)), (chunks_1_1 = chunks_1.next()); 260 | _c.label = 5; 261 | case 5: 262 | if (!!chunks_1_1.done) return [3 /*break*/, 8]; 263 | chunk = chunks_1_1.value; 264 | return [4 /*yield*/, connection.manager.delete(SignedOrderModel_1.SignedOrderModel, chunk)]; 265 | case 6: 266 | _c.sent(); 267 | _c.label = 7; 268 | case 7: 269 | chunks_1_1 = chunks_1.next(); 270 | return [3 /*break*/, 5]; 271 | case 8: 272 | return [3 /*break*/, 11]; 273 | case 9: 274 | e_1_1 = _c.sent(); 275 | e_1 = { error: e_1_1 }; 276 | return [3 /*break*/, 11]; 277 | case 10: 278 | try { 279 | if (chunks_1_1 && !chunks_1_1.done && (_a = chunks_1.return)) _a.call(chunks_1); 280 | } finally { 281 | if (e_1) throw e_1.error; 282 | } 283 | return [7 /*endfinally*/]; 284 | case 11: 285 | return [3 /*break*/, 12]; 286 | case 12: 287 | return [2 /*return*/]; 288 | } 289 | }); 290 | }); 291 | }; 292 | OrderWatcherService.prototype.syncOrderbookAsync = function() { 293 | return __awaiter(this, void 0, void 0, function() { 294 | var connection, signedOrderModels, signedOrders, orders, _a, accepted, rejected; 295 | return __generator(this, function(_b) { 296 | switch (_b.label) { 297 | case 0: 298 | d('SYNC orderbook with Mesh'); 299 | connection = db_connection_1.getDBConnection(); 300 | return [4 /*yield*/, connection.manager.find(SignedOrderModel_1.SignedOrderModel)]; 301 | case 1: 302 | signedOrderModels = _b.sent(); 303 | signedOrders = signedOrderModels.map(orderbook_utils_1.deserializeOrder); 304 | return [4 /*yield*/, this._meshClient.getOrdersAsync()]; 305 | case 2: 306 | orders = _b.sent(); 307 | return [ 308 | 4 /*yield*/, 309 | mesh_utils_1.MeshUtils.addOrdersToMeshAsync(this._meshClient, signedOrders), 310 | ]; 311 | case 3: 312 | (_a = _b.sent()), (accepted = _a.accepted), (rejected = _a.rejected); 313 | d( 314 | 'SYNC ' + 315 | rejected.length + 316 | ' rejected ' + 317 | accepted.length + 318 | ' accepted ' + 319 | signedOrders.length + 320 | ' sent', 321 | ); 322 | if (!(rejected.length > 0)) return [3 /*break*/, 5]; 323 | return [ 324 | 4 /*yield*/, 325 | OrderWatcherService._onOrderLifeCycleEventAsync( 326 | types_1.OrderWatcherLifeCycleEvents.Removed, 327 | mesh_utils_1.MeshUtils.orderInfosToApiOrders(rejected), 328 | ), 329 | ]; 330 | case 4: 331 | _b.sent(); 332 | _b.label = 5; 333 | case 5: 334 | if (!(orders.length > 0)) return [3 /*break*/, 7]; 335 | return [ 336 | 4 /*yield*/, 337 | OrderWatcherService._onOrderLifeCycleEventAsync( 338 | types_1.OrderWatcherLifeCycleEvents.Added, 339 | mesh_utils_1.MeshUtils.orderInfosToApiOrders(orders), 340 | ), 341 | ]; 342 | case 6: 343 | _b.sent(); 344 | _b.label = 7; 345 | case 7: 346 | d('SYNC complete'); 347 | return [2 /*return*/]; 348 | } 349 | }); 350 | }); 351 | }; 352 | return OrderWatcherService; 353 | })(); 354 | exports.OrderWatcherService = OrderWatcherService; 355 | -------------------------------------------------------------------------------- /js/services/orderbook_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __assign = 3 | (this && this.__assign) || 4 | function() { 5 | __assign = 6 | Object.assign || 7 | function(t) { 8 | for (var s, i = 1, n = arguments.length; i < n; i++) { 9 | s = arguments[i]; 10 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 11 | } 12 | return t; 13 | }; 14 | return __assign.apply(this, arguments); 15 | }; 16 | var __values = 17 | (this && this.__values) || 18 | function(o) { 19 | var m = typeof Symbol === 'function' && o[Symbol.iterator], 20 | i = 0; 21 | if (m) return m.call(o); 22 | return { 23 | next: function() { 24 | if (o && i >= o.length) o = void 0; 25 | return { value: o && o[i++], done: !o }; 26 | }, 27 | }; 28 | }; 29 | var __read = 30 | (this && this.__read) || 31 | function(o, n) { 32 | var m = typeof Symbol === 'function' && o[Symbol.iterator]; 33 | if (!m) return o; 34 | var i = m.call(o), 35 | r, 36 | ar = [], 37 | e; 38 | try { 39 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 40 | } catch (error) { 41 | e = { error: error }; 42 | } finally { 43 | try { 44 | if (r && !r.done && (m = i['return'])) m.call(i); 45 | } finally { 46 | if (e) throw e.error; 47 | } 48 | } 49 | return ar; 50 | }; 51 | Object.defineProperty(exports, '__esModule', { value: true }); 52 | var order_utils_1 = require('@0x/order-utils'); 53 | var types_1 = require('@0x/types'); 54 | var utils_1 = require('@0x/utils'); 55 | var config_1 = require('../config'); 56 | var constants_1 = require('../constants'); 57 | var SignedOrderModel_1 = require('../models/SignedOrderModel'); 58 | var DEFAULT_ERC721_ASSET = { 59 | minAmount: new utils_1.BigNumber(0), 60 | maxAmount: new utils_1.BigNumber(1), 61 | precision: 0, 62 | }; 63 | var DEFAULT_ERC20_ASSET = { 64 | minAmount: new utils_1.BigNumber(0), 65 | maxAmount: constants_1.MAX_TOKEN_SUPPLY_POSSIBLE, 66 | precision: config_1.DEFAULT_ERC20_TOKEN_PRECISION, 67 | }; 68 | exports.compareAskOrder = function(orderA, orderB) { 69 | var orderAPrice = orderA.takerAssetAmount.div(orderA.makerAssetAmount); 70 | var orderBPrice = orderB.takerAssetAmount.div(orderB.makerAssetAmount); 71 | if (!orderAPrice.isEqualTo(orderBPrice)) { 72 | return orderAPrice.comparedTo(orderBPrice); 73 | } 74 | return exports.compareOrderByFeeRatio(orderA, orderB); 75 | }; 76 | exports.compareBidOrder = function(orderA, orderB) { 77 | var orderAPrice = orderA.makerAssetAmount.div(orderA.takerAssetAmount); 78 | var orderBPrice = orderB.makerAssetAmount.div(orderB.takerAssetAmount); 79 | if (!orderAPrice.isEqualTo(orderBPrice)) { 80 | return orderBPrice.comparedTo(orderAPrice); 81 | } 82 | return exports.compareOrderByFeeRatio(orderA, orderB); 83 | }; 84 | exports.compareOrderByFeeRatio = function(orderA, orderB) { 85 | var orderAFeePrice = orderA.takerFee.div(orderA.takerAssetAmount); 86 | var orderBFeePrice = orderB.takerFee.div(orderB.takerAssetAmount); 87 | if (!orderAFeePrice.isEqualTo(orderBFeePrice)) { 88 | return orderBFeePrice.comparedTo(orderAFeePrice); 89 | } 90 | return orderA.expirationTimeSeconds.comparedTo(orderB.expirationTimeSeconds); 91 | }; 92 | exports.includesTokenAddress = function(assetData, tokenAddress) { 93 | var e_1, _a; 94 | var decodedAssetData = order_utils_1.assetDataUtils.decodeAssetDataOrThrow(assetData); 95 | if (order_utils_1.assetDataUtils.isMultiAssetData(decodedAssetData)) { 96 | try { 97 | for ( 98 | var _b = __values(decodedAssetData.nestedAssetData.entries()), _c = _b.next(); 99 | !_c.done; 100 | _c = _b.next() 101 | ) { 102 | var _d = __read(_c.value, 2), 103 | nestedAssetDataElement = _d[1]; 104 | if (exports.includesTokenAddress(nestedAssetDataElement, tokenAddress)) { 105 | return true; 106 | } 107 | } 108 | } catch (e_1_1) { 109 | e_1 = { error: e_1_1 }; 110 | } finally { 111 | try { 112 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b); 113 | } finally { 114 | if (e_1) throw e_1.error; 115 | } 116 | } 117 | return false; 118 | } else if (!order_utils_1.assetDataUtils.isStaticCallAssetData(decodedAssetData)) { 119 | return decodedAssetData.tokenAddress === tokenAddress; 120 | } 121 | return false; 122 | }; 123 | exports.deserializeOrder = function(signedOrderModel) { 124 | var signedOrder = { 125 | signature: signedOrderModel.signature, 126 | senderAddress: signedOrderModel.senderAddress, 127 | makerAddress: signedOrderModel.makerAddress, 128 | takerAddress: signedOrderModel.takerAddress, 129 | makerFee: new utils_1.BigNumber(signedOrderModel.makerFee), 130 | takerFee: new utils_1.BigNumber(signedOrderModel.takerFee), 131 | makerAssetAmount: new utils_1.BigNumber(signedOrderModel.makerAssetAmount), 132 | takerAssetAmount: new utils_1.BigNumber(signedOrderModel.takerAssetAmount), 133 | makerAssetData: signedOrderModel.makerAssetData, 134 | takerAssetData: signedOrderModel.takerAssetData, 135 | salt: new utils_1.BigNumber(signedOrderModel.salt), 136 | exchangeAddress: signedOrderModel.exchangeAddress, 137 | feeRecipientAddress: signedOrderModel.feeRecipientAddress, 138 | expirationTimeSeconds: new utils_1.BigNumber(signedOrderModel.expirationTimeSeconds), 139 | makerFeeAssetData: signedOrderModel.makerFeeAssetData, 140 | takerFeeAssetData: signedOrderModel.takerFeeAssetData, 141 | chainId: config_1.CHAIN_ID, 142 | }; 143 | return signedOrder; 144 | }; 145 | exports.deserializeOrderToAPIOrder = function(signedOrderModel) { 146 | var order = exports.deserializeOrder(signedOrderModel); 147 | var apiOrder = { 148 | order: order, 149 | metaData: { 150 | orderHash: signedOrderModel.hash, 151 | remainingFillableTakerAssetAmount: signedOrderModel.remainingFillableTakerAssetAmount, 152 | }, 153 | }; 154 | return apiOrder; 155 | }; 156 | exports.serializeOrder = function(apiOrder) { 157 | var signedOrder = apiOrder.order; 158 | var signedOrderModel = new SignedOrderModel_1.SignedOrderModel({ 159 | signature: signedOrder.signature, 160 | senderAddress: signedOrder.senderAddress, 161 | makerAddress: signedOrder.makerAddress, 162 | takerAddress: signedOrder.takerAddress, 163 | makerAssetAmount: signedOrder.makerAssetAmount.toString(), 164 | takerAssetAmount: signedOrder.takerAssetAmount.toString(), 165 | makerAssetData: signedOrder.makerAssetData, 166 | takerAssetData: signedOrder.takerAssetData, 167 | makerFee: signedOrder.makerFee.toString(), 168 | takerFee: signedOrder.takerFee.toString(), 169 | makerFeeAssetData: signedOrder.makerFeeAssetData.toString(), 170 | takerFeeAssetData: signedOrder.takerFeeAssetData.toString(), 171 | salt: signedOrder.salt.toString(), 172 | exchangeAddress: signedOrder.exchangeAddress, 173 | feeRecipientAddress: signedOrder.feeRecipientAddress, 174 | expirationTimeSeconds: signedOrder.expirationTimeSeconds.toString(), 175 | hash: apiOrder.metaData.orderHash, 176 | remainingFillableTakerAssetAmount: apiOrder.metaData.remainingFillableTakerAssetAmount.toString(), 177 | }); 178 | return signedOrderModel; 179 | }; 180 | var erc721AssetDataToAsset = function(assetData) { 181 | return __assign({}, DEFAULT_ERC721_ASSET, { assetData: assetData }); 182 | }; 183 | var erc20AssetDataToAsset = function(assetData) { 184 | return __assign({}, DEFAULT_ERC20_ASSET, { assetData: assetData }); 185 | }; 186 | var assetDataToAsset = function(assetData) { 187 | var assetProxyId = order_utils_1.assetDataUtils.decodeAssetProxyId(assetData); 188 | var asset; 189 | switch (assetProxyId) { 190 | case types_1.AssetProxyId.ERC20: 191 | asset = erc20AssetDataToAsset(assetData); 192 | break; 193 | case types_1.AssetProxyId.ERC721: 194 | asset = erc721AssetDataToAsset(assetData); 195 | break; 196 | default: 197 | throw utils_1.errorUtils.spawnSwitchErr('assetProxyId', assetProxyId); 198 | } 199 | return asset; 200 | }; 201 | exports.signedOrderToAssetPair = function(signedOrder) { 202 | return { 203 | assetDataA: assetDataToAsset(signedOrder.makerAssetData), 204 | assetDataB: assetDataToAsset(signedOrder.takerAssetData), 205 | }; 206 | }; 207 | -------------------------------------------------------------------------------- /js/services/websocket_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __assign = 3 | (this && this.__assign) || 4 | function() { 5 | __assign = 6 | Object.assign || 7 | function(t) { 8 | for (var s, i = 1, n = arguments.length; i < n; i++) { 9 | s = arguments[i]; 10 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 11 | } 12 | return t; 13 | }; 14 | return __assign.apply(this, arguments); 15 | }; 16 | var __values = 17 | (this && this.__values) || 18 | function(o) { 19 | var m = typeof Symbol === 'function' && o[Symbol.iterator], 20 | i = 0; 21 | if (m) return m.call(o); 22 | return { 23 | next: function() { 24 | if (o && i >= o.length) o = void 0; 25 | return { value: o && o[i++], done: !o }; 26 | }, 27 | }; 28 | }; 29 | var __read = 30 | (this && this.__read) || 31 | function(o, n) { 32 | var m = typeof Symbol === 'function' && o[Symbol.iterator]; 33 | if (!m) return o; 34 | var i = m.call(o), 35 | r, 36 | ar = [], 37 | e; 38 | try { 39 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 40 | } catch (error) { 41 | e = { error: error }; 42 | } finally { 43 | try { 44 | if (r && !r.done && (m = i['return'])) m.call(i); 45 | } finally { 46 | if (e) throw e.error; 47 | } 48 | } 49 | return ar; 50 | }; 51 | var __spread = 52 | (this && this.__spread) || 53 | function() { 54 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 55 | return ar; 56 | }; 57 | Object.defineProperty(exports, '__esModule', { value: true }); 58 | var json_schemas_1 = require('@0x/json-schemas'); 59 | var order_utils_1 = require('@0x/order-utils'); 60 | var types_1 = require('@0x/types'); 61 | var WebSocket = require('ws'); 62 | var errors_1 = require('../errors'); 63 | var mesh_utils_1 = require('../mesh_utils'); 64 | var error_handling_1 = require('../middleware/error_handling'); 65 | var types_2 = require('../types'); 66 | var utils_1 = require('../utils'); 67 | var DEFAULT_OPTS = { 68 | pongInterval: 5000, 69 | }; 70 | var WebsocketService = /** @class */ (function() { 71 | function WebsocketService(server, meshClient, opts) { 72 | var _this = this; 73 | this._requestIdToSocket = new Map(); // requestId to WebSocket mapping 74 | this._requestIdToSubscriptionOpts = new Map(); // requestId -> { base, quote } 75 | var wsOpts = __assign({}, DEFAULT_OPTS, opts); 76 | this._server = new WebSocket.Server({ server: server }); 77 | this._server.on('connection', this._processConnection.bind(this)); 78 | this._pongIntervalId = setInterval(this._cleanupConnections.bind(this), wsOpts.pongInterval); 79 | this._meshClient = meshClient; 80 | this._meshClient 81 | .subscribeToOrdersAsync(function(e) { 82 | return _this.orderUpdate(mesh_utils_1.MeshUtils.orderInfosToApiOrders(e)); 83 | }) 84 | .then(function(subscriptionId) { 85 | return (_this._meshSubscriptionId = subscriptionId); 86 | }); 87 | } 88 | WebsocketService._decodedContractAndAssetData = function(assetData) { 89 | var e_1, _a; 90 | var data = [assetData]; 91 | var decodedAssetData = order_utils_1.assetDataUtils.decodeAssetDataOrThrow(assetData); 92 | if (order_utils_1.assetDataUtils.isMultiAssetData(decodedAssetData)) { 93 | try { 94 | for (var _b = __values(decodedAssetData.nestedAssetData), _c = _b.next(); !_c.done; _c = _b.next()) { 95 | var nested = _c.value; 96 | data = __spread(data, WebsocketService._decodedContractAndAssetData(nested).data); 97 | } 98 | } catch (e_1_1) { 99 | e_1 = { error: e_1_1 }; 100 | } finally { 101 | try { 102 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b); 103 | } finally { 104 | if (e_1) throw e_1.error; 105 | } 106 | } 107 | } else if (order_utils_1.assetDataUtils.isStaticCallAssetData(decodedAssetData)) { 108 | // do nothing 109 | } else { 110 | data = __spread(data, [decodedAssetData.tokenAddress]); 111 | } 112 | return { data: data, assetProxyId: decodedAssetData.assetProxyId }; 113 | }; 114 | WebsocketService._matchesOrdersChannelSubscription = function(order, opts) { 115 | if (opts === 'ALL_SUBSCRIPTION_OPTS') { 116 | return true; 117 | } 118 | var makerAssetData = order.makerAssetData, 119 | takerAssetData = order.takerAssetData; 120 | var makerAssetDataTakerAssetData = [makerAssetData, takerAssetData]; 121 | // Handle the specific, unambiguous asset datas 122 | // traderAssetData?: string; 123 | if (opts.traderAssetData && makerAssetDataTakerAssetData.includes(opts.traderAssetData)) { 124 | return true; 125 | } 126 | // makerAssetData?: string; 127 | // takerAssetData?: string; 128 | if ( 129 | opts.makerAssetData && 130 | opts.takerAssetData && 131 | makerAssetDataTakerAssetData.includes(opts.makerAssetData) && 132 | makerAssetDataTakerAssetData.includes(opts.takerAssetData) 133 | ) { 134 | return true; 135 | } 136 | // makerAssetAddress?: string; 137 | // takerAssetAddress?: string; 138 | var makerContractAndAssetData = WebsocketService._decodedContractAndAssetData(makerAssetData); 139 | var takerContractAndAssetData = WebsocketService._decodedContractAndAssetData(takerAssetData); 140 | if ( 141 | opts.makerAssetAddress && 142 | opts.takerAssetAddress && 143 | makerContractAndAssetData.assetProxyId !== types_1.AssetProxyId.MultiAsset && 144 | makerContractAndAssetData.assetProxyId !== types_1.AssetProxyId.StaticCall && 145 | takerContractAndAssetData.assetProxyId !== types_1.AssetProxyId.MultiAsset && 146 | takerContractAndAssetData.assetProxyId !== types_1.AssetProxyId.StaticCall && 147 | makerContractAndAssetData.data.includes(opts.makerAssetAddress) && 148 | takerContractAndAssetData.data.includes(opts.takerAssetAddress) 149 | ) { 150 | return true; 151 | } 152 | // TODO (dekz)handle MAP 153 | // makerAssetProxyId?: string; 154 | // takerAssetProxyId?: string; 155 | return false; 156 | }; 157 | WebsocketService.prototype.destroy = function() { 158 | var e_2, _a; 159 | clearInterval(this._pongIntervalId); 160 | try { 161 | for (var _b = __values(this._server.clients), _c = _b.next(); !_c.done; _c = _b.next()) { 162 | var ws = _c.value; 163 | ws.terminate(); 164 | } 165 | } catch (e_2_1) { 166 | e_2 = { error: e_2_1 }; 167 | } finally { 168 | try { 169 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b); 170 | } finally { 171 | if (e_2) throw e_2.error; 172 | } 173 | } 174 | this._requestIdToSocket.clear(); 175 | this._requestIdToSubscriptionOpts.clear(); 176 | this._server.close(); 177 | if (this._meshSubscriptionId) { 178 | void this._meshClient.unsubscribeAsync(this._meshSubscriptionId); 179 | } 180 | }; 181 | WebsocketService.prototype.orderUpdate = function(apiOrders) { 182 | var e_3, _a, e_4, _b, e_5, _c; 183 | if (this._server.clients.size === 0) { 184 | return; 185 | } 186 | var response = { 187 | type: types_1.OrdersChannelMessageTypes.Update, 188 | channel: types_2.MessageChannels.Orders, 189 | payload: apiOrders, 190 | }; 191 | try { 192 | for ( 193 | var apiOrders_1 = __values(apiOrders), apiOrders_1_1 = apiOrders_1.next(); 194 | !apiOrders_1_1.done; 195 | apiOrders_1_1 = apiOrders_1.next() 196 | ) { 197 | var order = apiOrders_1_1.value; 198 | // Future optimisation is to invert this structure so the order isn't duplicated over many request ids 199 | // order->requestIds it is less likely to get multiple order updates and more likely 200 | // to have many subscribers and a single order 201 | var requestIdToOrders = {}; 202 | try { 203 | for ( 204 | var _d = __values(this._requestIdToSubscriptionOpts), _e = _d.next(); 205 | !_e.done; 206 | _e = _d.next() 207 | ) { 208 | var _f = __read(_e.value, 2), 209 | requestId = _f[0], 210 | subscriptionOpts = _f[1]; 211 | if (WebsocketService._matchesOrdersChannelSubscription(order.order, subscriptionOpts)) { 212 | if (requestIdToOrders[requestId]) { 213 | var orderSet = requestIdToOrders[requestId]; 214 | orderSet.add(order); 215 | } else { 216 | var orderSet = new Set(); 217 | orderSet.add(order); 218 | requestIdToOrders[requestId] = orderSet; 219 | } 220 | } 221 | } 222 | } catch (e_4_1) { 223 | e_4 = { error: e_4_1 }; 224 | } finally { 225 | try { 226 | if (_e && !_e.done && (_b = _d.return)) _b.call(_d); 227 | } finally { 228 | if (e_4) throw e_4.error; 229 | } 230 | } 231 | try { 232 | for ( 233 | var _g = __values(Object.entries(requestIdToOrders)), _h = _g.next(); 234 | !_h.done; 235 | _h = _g.next() 236 | ) { 237 | var _j = __read(_h.value, 2), 238 | requestId = _j[0], 239 | orders = _j[1]; 240 | var ws = this._requestIdToSocket.get(requestId); 241 | if (ws) { 242 | ws.send( 243 | JSON.stringify( 244 | __assign({}, response, { payload: Array.from(orders), requestId: requestId }), 245 | ), 246 | ); 247 | } 248 | } 249 | } catch (e_5_1) { 250 | e_5 = { error: e_5_1 }; 251 | } finally { 252 | try { 253 | if (_h && !_h.done && (_c = _g.return)) _c.call(_g); 254 | } finally { 255 | if (e_5) throw e_5.error; 256 | } 257 | } 258 | } 259 | } catch (e_3_1) { 260 | e_3 = { error: e_3_1 }; 261 | } finally { 262 | try { 263 | if (apiOrders_1_1 && !apiOrders_1_1.done && (_a = apiOrders_1.return)) _a.call(apiOrders_1); 264 | } finally { 265 | if (e_3) throw e_3.error; 266 | } 267 | } 268 | }; 269 | WebsocketService.prototype._processConnection = function(ws, _req) { 270 | ws.on('pong', this._pongHandler(ws).bind(this)); 271 | ws.on(types_1.WebsocketConnectionEventType.Message, this._messageHandler(ws).bind(this)); 272 | ws.on(types_1.WebsocketConnectionEventType.Close, this._closeHandler(ws).bind(this)); 273 | ws.isAlive = true; 274 | ws.requestIds = new Set(); 275 | }; 276 | WebsocketService.prototype._processMessage = function(ws, data) { 277 | var message; 278 | try { 279 | message = JSON.parse(data.toString()); 280 | } catch (e) { 281 | throw new errors_1.MalformedJSONError(); 282 | } 283 | utils_1.utils.validateSchema(message, json_schemas_1.schemas.relayerApiOrdersChannelSubscribeSchema); 284 | var requestId = message.requestId, 285 | payload = message.payload, 286 | type = message.type; 287 | switch (type) { 288 | case types_2.MessageTypes.Subscribe: 289 | ws.requestIds.add(requestId); 290 | var subscriptionOpts = payload || 'ALL_SUBSCRIPTION_OPTS'; 291 | this._requestIdToSubscriptionOpts.set(requestId, subscriptionOpts); 292 | this._requestIdToSocket.set(requestId, ws); 293 | break; 294 | default: 295 | throw new errors_1.NotImplementedError(message.type); 296 | } 297 | }; 298 | WebsocketService.prototype._cleanupConnections = function() { 299 | var e_6, _a; 300 | try { 301 | // Ping every connection and if it is unresponsive 302 | // terminate it during the next check 303 | for (var _b = __values(this._server.clients), _c = _b.next(); !_c.done; _c = _b.next()) { 304 | var ws = _c.value; 305 | if (!ws.isAlive) { 306 | ws.terminate(); 307 | } else { 308 | ws.isAlive = false; 309 | ws.ping(); 310 | } 311 | } 312 | } catch (e_6_1) { 313 | e_6 = { error: e_6_1 }; 314 | } finally { 315 | try { 316 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b); 317 | } finally { 318 | if (e_6) throw e_6.error; 319 | } 320 | } 321 | }; 322 | WebsocketService.prototype._messageHandler = function(ws) { 323 | var _this = this; 324 | return function(data) { 325 | try { 326 | _this._processMessage(ws, data); 327 | } catch (err) { 328 | _this._processError(ws, err); 329 | } 330 | }; 331 | }; 332 | // tslint:disable-next-line:prefer-function-over-method 333 | WebsocketService.prototype._processError = function(ws, err) { 334 | var errorBody = error_handling_1.generateError(err).errorBody; 335 | ws.send(JSON.stringify(errorBody)); 336 | ws.terminate(); 337 | }; 338 | // tslint:disable-next-line:prefer-function-over-method 339 | WebsocketService.prototype._pongHandler = function(ws) { 340 | return function() { 341 | ws.isAlive = true; 342 | }; 343 | }; 344 | WebsocketService.prototype._closeHandler = function(ws) { 345 | var _this = this; 346 | return function() { 347 | var e_7, _a; 348 | try { 349 | for (var _b = __values(ws.requestIds), _c = _b.next(); !_c.done; _c = _b.next()) { 350 | var requestId = _c.value; 351 | _this._requestIdToSocket.delete(requestId); 352 | _this._requestIdToSubscriptionOpts.delete(requestId); 353 | } 354 | } catch (e_7_1) { 355 | e_7 = { error: e_7_1 }; 356 | } finally { 357 | try { 358 | if (_c && !_c.done && (_a = _b.return)) _a.call(_b); 359 | } finally { 360 | if (e_7) throw e_7.error; 361 | } 362 | } 363 | }; 364 | }; 365 | return WebsocketService; 366 | })(); 367 | exports.WebsocketService = WebsocketService; 368 | -------------------------------------------------------------------------------- /js/types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | var OrderWatcherLifeCycleEvents; 4 | (function(OrderWatcherLifeCycleEvents) { 5 | OrderWatcherLifeCycleEvents[(OrderWatcherLifeCycleEvents['Added'] = 0)] = 'Added'; 6 | OrderWatcherLifeCycleEvents[(OrderWatcherLifeCycleEvents['Removed'] = 1)] = 'Removed'; 7 | OrderWatcherLifeCycleEvents[(OrderWatcherLifeCycleEvents['Updated'] = 2)] = 'Updated'; 8 | })((OrderWatcherLifeCycleEvents = exports.OrderWatcherLifeCycleEvents || (exports.OrderWatcherLifeCycleEvents = {}))); 9 | var MessageTypes; 10 | (function(MessageTypes) { 11 | MessageTypes['Subscribe'] = 'subscribe'; 12 | })((MessageTypes = exports.MessageTypes || (exports.MessageTypes = {}))); 13 | var MessageChannels; 14 | (function(MessageChannels) { 15 | MessageChannels['Orders'] = 'orders'; 16 | })((MessageChannels = exports.MessageChannels || (exports.MessageChannels = {}))); 17 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __awaiter = 3 | (this && this.__awaiter) || 4 | function(thisArg, _arguments, P, generator) { 5 | return new (P || (P = Promise))(function(resolve, reject) { 6 | function fulfilled(value) { 7 | try { 8 | step(generator.next(value)); 9 | } catch (e) { 10 | reject(e); 11 | } 12 | } 13 | function rejected(value) { 14 | try { 15 | step(generator['throw'](value)); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | } 20 | function step(result) { 21 | result.done 22 | ? resolve(result.value) 23 | : new P(function(resolve) { 24 | resolve(result.value); 25 | }).then(fulfilled, rejected); 26 | } 27 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 28 | }); 29 | }; 30 | var __generator = 31 | (this && this.__generator) || 32 | function(thisArg, body) { 33 | var _ = { 34 | label: 0, 35 | sent: function() { 36 | if (t[0] & 1) throw t[1]; 37 | return t[1]; 38 | }, 39 | trys: [], 40 | ops: [], 41 | }, 42 | f, 43 | y, 44 | t, 45 | g; 46 | return ( 47 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 48 | typeof Symbol === 'function' && 49 | (g[Symbol.iterator] = function() { 50 | return this; 51 | }), 52 | g 53 | ); 54 | function verb(n) { 55 | return function(v) { 56 | return step([n, v]); 57 | }; 58 | } 59 | function step(op) { 60 | if (f) throw new TypeError('Generator is already executing.'); 61 | while (_) 62 | try { 63 | if ( 64 | ((f = 1), 65 | y && 66 | (t = 67 | op[0] & 2 68 | ? y['return'] 69 | : op[0] 70 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 71 | : y.next) && 72 | !(t = t.call(y, op[1])).done) 73 | ) 74 | return t; 75 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 76 | switch (op[0]) { 77 | case 0: 78 | case 1: 79 | t = op; 80 | break; 81 | case 4: 82 | _.label++; 83 | return { value: op[1], done: false }; 84 | case 5: 85 | _.label++; 86 | y = op[1]; 87 | op = [0]; 88 | continue; 89 | case 7: 90 | op = _.ops.pop(); 91 | _.trys.pop(); 92 | continue; 93 | default: 94 | if ( 95 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 96 | (op[0] === 6 || op[0] === 2) 97 | ) { 98 | _ = 0; 99 | continue; 100 | } 101 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 102 | _.label = op[1]; 103 | break; 104 | } 105 | if (op[0] === 6 && _.label < t[1]) { 106 | _.label = t[1]; 107 | t = op; 108 | break; 109 | } 110 | if (t && _.label < t[2]) { 111 | _.label = t[2]; 112 | _.ops.push(op); 113 | break; 114 | } 115 | if (t[2]) _.ops.pop(); 116 | _.trys.pop(); 117 | continue; 118 | } 119 | op = body.call(thisArg, _); 120 | } catch (e) { 121 | op = [6, e]; 122 | y = 0; 123 | } finally { 124 | f = t = 0; 125 | } 126 | if (op[0] & 5) throw op[1]; 127 | return { value: op[0] ? op[1] : void 0, done: true }; 128 | } 129 | }; 130 | var __read = 131 | (this && this.__read) || 132 | function(o, n) { 133 | var m = typeof Symbol === 'function' && o[Symbol.iterator]; 134 | if (!m) return o; 135 | var i = m.call(o), 136 | r, 137 | ar = [], 138 | e; 139 | try { 140 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 141 | } catch (error) { 142 | e = { error: error }; 143 | } finally { 144 | try { 145 | if (r && !r.done && (m = i['return'])) m.call(i); 146 | } finally { 147 | if (e) throw e.error; 148 | } 149 | } 150 | return ar; 151 | }; 152 | var __spread = 153 | (this && this.__spread) || 154 | function() { 155 | for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); 156 | return ar; 157 | }; 158 | Object.defineProperty(exports, '__esModule', { value: true }); 159 | var json_schemas_1 = require('@0x/json-schemas'); 160 | var _ = require('lodash'); 161 | var errors_1 = require('./errors'); 162 | var schemaValidator = new json_schemas_1.SchemaValidator(); 163 | exports.utils = { 164 | log: function() { 165 | var args = []; 166 | for (var _i = 0; _i < arguments.length; _i++) { 167 | args[_i] = arguments[_i]; 168 | } 169 | // tslint:disable-next-line:no-console 170 | console.log.apply(console, __spread(args)); 171 | }, 172 | validateSchema: function(instance, schema) { 173 | var validationResult = schemaValidator.validate(instance, schema); 174 | if (_.isEmpty(validationResult.errors)) { 175 | return; 176 | } else { 177 | var validationErrorItems = _.map(validationResult.errors, function(schemaValidationError) { 178 | return schemaValidationErrorToValidationErrorItem(schemaValidationError); 179 | }); 180 | throw new errors_1.ValidationError(validationErrorItems); 181 | } 182 | }, 183 | delayAsync: function(ms) { 184 | return __awaiter(this, void 0, void 0, function() { 185 | return __generator(this, function(_a) { 186 | // tslint:disable:no-inferred-empty-object-type 187 | return [ 188 | 2 /*return*/, 189 | new Promise(function(resolve) { 190 | return setTimeout(resolve, ms); 191 | }), 192 | ]; 193 | }); 194 | }); 195 | }, 196 | attemptAsync: function(fn, opts) { 197 | if (opts === void 0) { 198 | opts = { interval: 1000, maxRetries: 10 }; 199 | } 200 | return __awaiter(this, void 0, void 0, function() { 201 | var result, attempt, error, isSuccess, err_1; 202 | return __generator(this, function(_a) { 203 | switch (_a.label) { 204 | case 0: 205 | attempt = 0; 206 | isSuccess = false; 207 | _a.label = 1; 208 | case 1: 209 | if (!(!result && attempt < opts.maxRetries)) return [3 /*break*/, 7]; 210 | attempt++; 211 | _a.label = 2; 212 | case 2: 213 | _a.trys.push([2, 4, , 6]); 214 | return [4 /*yield*/, fn()]; 215 | case 3: 216 | result = _a.sent(); 217 | isSuccess = true; 218 | error = undefined; 219 | return [3 /*break*/, 6]; 220 | case 4: 221 | err_1 = _a.sent(); 222 | error = err_1; 223 | return [4 /*yield*/, exports.utils.delayAsync(opts.interval)]; 224 | case 5: 225 | _a.sent(); 226 | return [3 /*break*/, 6]; 227 | case 6: 228 | return [3 /*break*/, 1]; 229 | case 7: 230 | if (!isSuccess) { 231 | throw error; 232 | } 233 | return [2 /*return*/, result]; 234 | } 235 | }); 236 | }); 237 | }, 238 | }; 239 | function schemaValidationErrorToValidationErrorItem(schemaValidationError) { 240 | if ( 241 | _.includes( 242 | [ 243 | 'type', 244 | 'anyOf', 245 | 'allOf', 246 | 'oneOf', 247 | 'additionalProperties', 248 | 'minProperties', 249 | 'maxProperties', 250 | 'pattern', 251 | 'format', 252 | 'uniqueItems', 253 | 'items', 254 | 'dependencies', 255 | ], 256 | schemaValidationError.name, 257 | ) 258 | ) { 259 | return { 260 | field: schemaValidationError.property, 261 | code: errors_1.ValidationErrorCodes.IncorrectFormat, 262 | reason: schemaValidationError.message, 263 | }; 264 | } else if ( 265 | _.includes( 266 | ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'enum', 'const'], 267 | schemaValidationError.name, 268 | ) 269 | ) { 270 | return { 271 | field: schemaValidationError.property, 272 | code: errors_1.ValidationErrorCodes.ValueOutOfRange, 273 | reason: schemaValidationError.message, 274 | }; 275 | } else if (schemaValidationError.name === 'required') { 276 | return { 277 | field: schemaValidationError.argument, 278 | code: errors_1.ValidationErrorCodes.RequiredField, 279 | reason: schemaValidationError.message, 280 | }; 281 | } else if (schemaValidationError.name === 'not') { 282 | return { 283 | field: schemaValidationError.property, 284 | code: errors_1.ValidationErrorCodes.UnsupportedOption, 285 | reason: schemaValidationError.message, 286 | }; 287 | } else { 288 | throw new Error('Unknnown schema validation error name: ' + schemaValidationError.name); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /launch_kit_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xProject/0x-launch-kit-backend/b87060f41a475a014f19e0869f175ea8dd06a903/launch_kit_banner.png -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | {"DEFAULT_FEE_RECIPIENT":"0xabcabc71026c35ed33c87a006d68a54ef1a122fa"} -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sqlite", 3 | "database": "db/database.sqlite", 4 | "synchronize": true, 5 | "logging": true, 6 | "logger": "debug", 7 | "entities": ["ts/lib/entity/**/*.js", "js/entity/**/*.js"], 8 | "cli": { 9 | "entitiesDir": "ts/lib/entity" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "0x-launch-kit", 3 | "version": "0.3.1", 4 | "description": "0x Relayer", 5 | "repository": "git@github.com:0xProject/0x-launch-kit-backend.git", 6 | "author": "Leonid Logvinov ", 7 | "license": "Apache-2.0", 8 | "scripts": { 9 | "clean": "shx rm -rf ts/lib", 10 | "prettier": "yarn prettier:js && yarn prettier:ts", 11 | "build": "yarn build:ts && shx rm -rf js && shx cp -r ts/lib/ js", 12 | "prettier:js": "prettier --write 'js/**/*.js' --config .prettierrc", 13 | "start:js": "node -r dotenv/config js/index.js", 14 | "build:ts": "tsc -p ts/tsconfig.json", 15 | "watch:ts": "tsc -w -p ts/tsconfig.json", 16 | "prettier:ts": "prettier --write 'ts/**/*.ts' --config .prettierrc", 17 | "start:ts": "node -r dotenv/config ts/lib/index.js", 18 | "start:service:http": "node -r dotenv/config ts/lib/runners/http_service_runner.js", 19 | "start:service:ws": "node -r dotenv/config ts/lib/runners/websocket_service_runner.js", 20 | "start:service:order_watcher": "node -r dotenv/config ts/lib/runners/order_watcher_service_runner.js", 21 | "lint:ts": "tslint --project ts --format stylish" 22 | }, 23 | "devDependencies": { 24 | "@0x/tslint-config": "^4.0.0", 25 | "@types/cors": "^2.8.6", 26 | "@types/dotenv": "^6.1.1", 27 | "@types/express": "^4.17.1", 28 | "@types/lodash": "^4.14.137", 29 | "@types/web3": "^1.0.19", 30 | "@types/web3-provider-engine": "^14.0.0", 31 | "@0x/typescript-typings": "^5.0.0", 32 | "@0x/types": "^3.0.0", 33 | "@types/ws": "^6.0.2", 34 | "ethereum-types": "^2.1.4", 35 | "prettier": "^1.18.2", 36 | "shx": "^0.3.2", 37 | "tslint": "^5.19.0", 38 | "typescript": "3.0.1" 39 | }, 40 | "dependencies": { 41 | "@0x/assert": "^3.0.0", 42 | "@0x/connect": "^6.0.0", 43 | "@0x/json-schemas": "^5.0.0", 44 | "@0x/mesh-rpc-client": "^7.1.0-beta-0xv3", 45 | "@0x/order-utils": "^8.5.0-beta.3", 46 | "@0x/utils": "^5.0.0", 47 | "body-parser": "^1.19.0", 48 | "cors": "^2.8.5", 49 | "debug": "^4.1.1", 50 | "dotenv": "^8.1.0", 51 | "express": "^4.17.1", 52 | "express-async-handler": "^1.1.4", 53 | "forever": "^1.0.0", 54 | "http-status-codes": "^1.3.2", 55 | "jsonschema": "^1.2.4", 56 | "lodash": "^4.17.15", 57 | "reflect-metadata": "^0.1.13", 58 | "sqlite3": "^4.1.0", 59 | "typeorm": "0.2.18", 60 | "ws": "^7.1.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ts/src/config.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:custom-no-magic-numbers 2 | import { assert } from '@0x/assert'; 3 | import { BigNumber } from '@0x/utils'; 4 | import * as crypto from 'crypto'; 5 | import * as fs from 'fs'; 6 | import * as _ from 'lodash'; 7 | import * as path from 'path'; 8 | 9 | import { NULL_BYTES } from './constants'; 10 | 11 | const metadataPath = path.join(__dirname, '../../metadata.json'); 12 | enum EnvVarType { 13 | Port, 14 | ChainId, 15 | FeeRecipient, 16 | UnitAmount, 17 | Url, 18 | WhitelistAllTokens, 19 | Boolean, 20 | FeeAssetData, 21 | } 22 | // Whitelisted token addresses. Set to a '*' instead of an array to allow all tokens. 23 | export const WHITELISTED_TOKENS: string[] | '*' = _.isEmpty(process.env.WHITELIST_ALL_TOKENS) 24 | ? [ 25 | '0x2002d3812f58e35f0ea1ffbf80a75a38c32175fa', // ZRX on Kovan 26 | '0xd0a1e359811322d97991e03f863a0c30c2cf029c', // WETH on Kovan 27 | ] 28 | : assertEnvVarType('WHITELIST_ALL_TOKENS', process.env.WHITELIST_ALL_TOKENS, EnvVarType.WhitelistAllTokens); 29 | 30 | // Network port to listen on 31 | export const HTTP_PORT = _.isEmpty(process.env.HTTP_PORT) 32 | ? 3000 33 | : assertEnvVarType('HTTP_PORT', process.env.HTTP_PORT, EnvVarType.Port); 34 | // Default chain id to use when not specified 35 | export const CHAIN_ID = _.isEmpty(process.env.CHAIN_ID) 36 | ? 42 37 | : assertEnvVarType('CHAIN_ID', process.env.CHAIN_ID, EnvVarType.ChainId); 38 | 39 | // Mesh Endpoint 40 | export const MESH_ENDPOINT = _.isEmpty(process.env.MESH_ENDPOINT) 41 | ? 'ws://localhost:60557' 42 | : assertEnvVarType('MESH_ENDPOINT', process.env.MESH_ENDPOINT, EnvVarType.Url); 43 | // The fee recipient for orders 44 | export const FEE_RECIPIENT = _.isEmpty(process.env.FEE_RECIPIENT) 45 | ? getDefaultFeeRecipient() 46 | : assertEnvVarType('FEE_RECIPIENT', process.env.FEE_RECIPIENT, EnvVarType.FeeRecipient); 47 | // A flat fee that should be charged to the order maker 48 | export const MAKER_FEE_UNIT_AMOUNT = _.isEmpty(process.env.MAKER_FEE_UNIT_AMOUNT) 49 | ? new BigNumber(0) 50 | : assertEnvVarType('MAKER_FEE_UNIT_AMOUNT', process.env.MAKER_FEE_UNIT_AMOUNT, EnvVarType.UnitAmount); 51 | // A flat fee that should be charged to the order taker 52 | export const TAKER_FEE_UNIT_AMOUNT = _.isEmpty(process.env.TAKER_FEE_UNIT_AMOUNT) 53 | ? new BigNumber(0) 54 | : assertEnvVarType('TAKER_FEE_UNIT_AMOUNT', process.env.TAKER_FEE_UNIT_AMOUNT, EnvVarType.UnitAmount); 55 | // The maker fee token encoded as asset data 56 | export const MAKER_FEE_ASSET_DATA = _.isEmpty(process.env.MAKER_FEE_ASSET_DATA) 57 | ? NULL_BYTES 58 | : assertEnvVarType('MAKER_FEE_ASSET_DATA', process.env.MAKER_FEE_ASSET_DATA, EnvVarType.FeeAssetData); 59 | // The taker fee token encoded as asset data 60 | export const TAKER_FEE_ASSET_DATA = _.isEmpty(process.env.TAKER_FEE_ASSET_DATA) 61 | ? NULL_BYTES 62 | : assertEnvVarType('TAKER_FEE_ASSET_DATA', process.env.TAKER_FEE_ASSET_DATA, EnvVarType.FeeAssetData); 63 | 64 | // Max number of entities per page 65 | export const MAX_PER_PAGE = 1000; 66 | // Default ERC20 token precision 67 | export const DEFAULT_ERC20_TOKEN_PRECISION = 18; 68 | // Address used when simulating transfers from the maker as part of 0x order validation 69 | export const DEFAULT_TAKER_SIMULATION_ADDRESS = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; 70 | 71 | function assertEnvVarType(name: string, value: any, expectedType: EnvVarType): any { 72 | let returnValue; 73 | switch (expectedType) { 74 | case EnvVarType.Port: 75 | try { 76 | returnValue = parseInt(value, 10); 77 | const isWithinRange = returnValue >= 0 && returnValue <= 65535; 78 | if (!isWithinRange) { 79 | throw new Error(); 80 | } 81 | } catch (err) { 82 | throw new Error(`${name} must be between 0 to 65535, found ${value}.`); 83 | } 84 | return returnValue; 85 | case EnvVarType.ChainId: 86 | try { 87 | returnValue = parseInt(value, 10); 88 | } catch (err) { 89 | throw new Error(`${name} must be a valid integer, found ${value}.`); 90 | } 91 | return returnValue; 92 | case EnvVarType.FeeRecipient: 93 | assert.isETHAddressHex(name, value); 94 | return value; 95 | case EnvVarType.Url: 96 | assert.isUri(name, value); 97 | return value; 98 | case EnvVarType.Boolean: 99 | return value === 'true'; 100 | case EnvVarType.UnitAmount: 101 | try { 102 | returnValue = new BigNumber(parseFloat(value)); 103 | if (returnValue.isNegative()) { 104 | throw new Error(); 105 | } 106 | } catch (err) { 107 | throw new Error(`${name} must be valid number greater than 0.`); 108 | } 109 | return returnValue; 110 | case EnvVarType.WhitelistAllTokens: 111 | return '*'; 112 | case EnvVarType.FeeAssetData: 113 | assert.isString(name, value); 114 | return value; 115 | default: 116 | throw new Error(`Unrecognised EnvVarType: ${expectedType} encountered for variable ${name}.`); 117 | } 118 | } 119 | function getDefaultFeeRecipient(): string { 120 | const metadata = JSON.parse(fs.readFileSync(metadataPath).toString()); 121 | const existingDefault: string = metadata.DEFAULT_FEE_RECIPIENT; 122 | const newDefault: string = existingDefault || `0xabcabc${crypto.randomBytes(17).toString('hex')}`; 123 | if (_.isEmpty(existingDefault)) { 124 | const metadataCopy = JSON.parse(JSON.stringify(metadata)); 125 | metadataCopy.DEFAULT_FEE_RECIPIENT = newDefault; 126 | fs.writeFileSync(metadataPath, JSON.stringify(metadataCopy)); 127 | } 128 | return newDefault; 129 | } 130 | -------------------------------------------------------------------------------- /ts/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0x/utils'; 2 | 3 | export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; 4 | export const NULL_BYTES = '0x'; 5 | export const ZRX_DECIMALS = 18; 6 | export const DEFAULT_PAGE = 1; 7 | export const DEFAULT_PER_PAGE = 20; 8 | export const ZERO = new BigNumber(0); 9 | export const MAX_TOKEN_SUPPLY_POSSIBLE = new BigNumber(2).pow(256); // tslint:disable-line custom-no-magic-numbers 10 | -------------------------------------------------------------------------------- /ts/src/db_connection.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { Connection, createConnection } from 'typeorm'; 3 | 4 | let connectionIfExists: Connection | undefined; 5 | 6 | /** 7 | * Returns the DB connnection 8 | */ 9 | export function getDBConnection(): Connection { 10 | if (connectionIfExists === undefined) { 11 | throw new Error('DB connection not initialized'); 12 | } 13 | return connectionIfExists; 14 | } 15 | 16 | /** 17 | * Creates the DB connnection to use in an app 18 | */ 19 | export async function initDBConnectionAsync(): Promise { 20 | if (connectionIfExists !== undefined) { 21 | throw new Error('DB connection already exists'); 22 | } 23 | connectionIfExists = await createConnection(); 24 | } 25 | -------------------------------------------------------------------------------- /ts/src/entity/SignedOrderEntity.ts: -------------------------------------------------------------------------------- 1 | import { EntitySchema } from 'typeorm'; 2 | 3 | import { SignedOrderModel } from '../models/SignedOrderModel'; 4 | 5 | export const signedOrderEntity = new EntitySchema({ 6 | name: 'SignedOrder', 7 | target: SignedOrderModel, 8 | columns: { 9 | hash: { 10 | primary: true, 11 | type: 'varchar', 12 | }, 13 | senderAddress: { 14 | type: 'varchar', 15 | }, 16 | makerAddress: { 17 | type: 'varchar', 18 | }, 19 | takerAddress: { 20 | type: 'varchar', 21 | }, 22 | makerAssetData: { 23 | type: 'varchar', 24 | }, 25 | takerAssetData: { 26 | type: 'varchar', 27 | }, 28 | exchangeAddress: { 29 | type: 'varchar', 30 | }, 31 | feeRecipientAddress: { 32 | type: 'varchar', 33 | }, 34 | expirationTimeSeconds: { 35 | type: 'varchar', 36 | }, 37 | makerFee: { 38 | type: 'varchar', 39 | }, 40 | takerFee: { 41 | type: 'varchar', 42 | }, 43 | makerFeeAssetData: { 44 | type: 'varchar', 45 | }, 46 | takerFeeAssetData: { 47 | type: 'varchar', 48 | }, 49 | makerAssetAmount: { 50 | type: 'varchar', 51 | }, 52 | takerAssetAmount: { 53 | type: 'varchar', 54 | }, 55 | salt: { 56 | type: 'varchar', 57 | }, 58 | signature: { 59 | type: 'varchar', 60 | }, 61 | remainingFillableTakerAssetAmount: { 62 | type: 'varchar', 63 | }, 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /ts/src/errors.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | export abstract class RelayerBaseError extends Error { 3 | public abstract statusCode: number; 4 | public isRelayerError = true; 5 | } 6 | 7 | export abstract class BadRequestError extends RelayerBaseError { 8 | public statusCode = 400; 9 | public abstract generalErrorCode: GeneralErrorCodes; 10 | } 11 | 12 | export interface ValidationErrorItem { 13 | field: string; 14 | code: ValidationErrorCodes; 15 | reason: string; 16 | } 17 | 18 | export interface ErrorBodyWithHTTPStatusCode { 19 | statusCode: number; 20 | errorBody: ErrorBody; 21 | } 22 | 23 | export interface ErrorBody { 24 | reason: string; 25 | code?: number; 26 | validationErrors?: ValidationErrorItem[]; 27 | } 28 | 29 | export class ValidationError extends BadRequestError { 30 | public generalErrorCode = GeneralErrorCodes.ValidationError; 31 | public validationErrors: ValidationErrorItem[]; 32 | constructor(validationErrors: ValidationErrorItem[]) { 33 | super(); 34 | this.validationErrors = validationErrors; 35 | } 36 | } 37 | 38 | export class MalformedJSONError extends BadRequestError { 39 | public generalErrorCode = GeneralErrorCodes.MalformedJson; 40 | } 41 | 42 | export class TooManyRequestsError extends BadRequestError { 43 | public statusCode = 429; 44 | public generalErrorCode = GeneralErrorCodes.Throttled; 45 | } 46 | 47 | export class NotImplementedError extends BadRequestError { 48 | public statusCode = 501; 49 | public generalErrorCode = GeneralErrorCodes.NotImplemented; 50 | } 51 | 52 | export class NotFoundError extends RelayerBaseError { 53 | public statusCode = 404; 54 | } 55 | 56 | export class InternalServerError extends RelayerBaseError { 57 | public statusCode = 500; 58 | } 59 | 60 | export enum GeneralErrorCodes { 61 | ValidationError = 100, 62 | MalformedJson = 101, 63 | OrderSubmissionDisabled = 102, 64 | Throttled = 103, 65 | NotImplemented = 104, 66 | } 67 | 68 | export const generalErrorCodeToReason: { [key in GeneralErrorCodes]: string } = { 69 | [GeneralErrorCodes.ValidationError]: 'Validation Failed', 70 | [GeneralErrorCodes.MalformedJson]: 'Malformed JSON', 71 | [GeneralErrorCodes.OrderSubmissionDisabled]: 'Order submission disabled', 72 | [GeneralErrorCodes.Throttled]: 'Throttled', 73 | [GeneralErrorCodes.NotImplemented]: 'Not Implemented', 74 | }; 75 | 76 | export enum ValidationErrorCodes { 77 | RequiredField = 1000, 78 | IncorrectFormat = 1001, 79 | InvalidAddress = 1002, 80 | AddressNotSupported = 1003, 81 | ValueOutOfRange = 1004, 82 | InvalidSignatureOrHash = 1005, 83 | UnsupportedOption = 1006, 84 | InvalidOrder = 1007, 85 | InternalError = 1008, 86 | } 87 | -------------------------------------------------------------------------------- /ts/src/fee_strategy.ts: -------------------------------------------------------------------------------- 1 | import { OrderConfigRequest, OrderConfigResponse } from '@0x/connect'; 2 | 3 | import { 4 | FEE_RECIPIENT, 5 | MAKER_FEE_ASSET_DATA, 6 | MAKER_FEE_UNIT_AMOUNT, 7 | TAKER_FEE_ASSET_DATA, 8 | TAKER_FEE_UNIT_AMOUNT, 9 | } from './config'; 10 | import { NULL_ADDRESS } from './constants'; 11 | 12 | export const fixedFeeStrategy = { 13 | getOrderConfig: (_order: Partial): OrderConfigResponse => { 14 | const normalizedFeeRecipient = FEE_RECIPIENT.toLowerCase(); 15 | const orderConfigResponse: OrderConfigResponse = { 16 | senderAddress: NULL_ADDRESS, 17 | feeRecipientAddress: normalizedFeeRecipient, 18 | makerFee: MAKER_FEE_UNIT_AMOUNT, 19 | takerFee: TAKER_FEE_UNIT_AMOUNT, 20 | makerFeeAssetData: MAKER_FEE_ASSET_DATA, 21 | takerFeeAssetData: TAKER_FEE_ASSET_DATA, 22 | }; 23 | return orderConfigResponse; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /ts/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | networkId: number; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ts/src/handlers.ts: -------------------------------------------------------------------------------- 1 | import { schemas } from '@0x/json-schemas'; 2 | import { assetDataUtils, SignedOrder } from '@0x/order-utils'; 3 | import { BigNumber } from '@0x/utils'; 4 | import * as express from 'express'; 5 | import * as HttpStatus from 'http-status-codes'; 6 | import * as _ from 'lodash'; 7 | 8 | import { FEE_RECIPIENT, MAX_PER_PAGE, WHITELISTED_TOKENS } from './config'; 9 | import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from './constants'; 10 | import { NotFoundError, ValidationError, ValidationErrorCodes } from './errors'; 11 | import { fixedFeeStrategy } from './fee_strategy'; 12 | import { paginate } from './paginator'; 13 | import { OrderBookService } from './services/orderbook_service'; 14 | import { utils } from './utils'; 15 | 16 | const parsePaginationConfig = (req: express.Request): { page: number; perPage: number } => { 17 | const page = req.query.page === undefined ? DEFAULT_PAGE : Number(req.query.page); 18 | const perPage = req.query.perPage === undefined ? DEFAULT_PER_PAGE : Number(req.query.perPage); 19 | if (perPage > MAX_PER_PAGE) { 20 | throw new ValidationError([ 21 | { 22 | field: 'perPage', 23 | code: ValidationErrorCodes.ValueOutOfRange, 24 | reason: `perPage should be less or equal to ${MAX_PER_PAGE}`, 25 | }, 26 | ]); 27 | } 28 | return { page, perPage }; 29 | }; 30 | 31 | export class Handlers { 32 | private readonly _orderBook: OrderBookService; 33 | public static feeRecipients(req: express.Request, res: express.Response): void { 34 | const { page, perPage } = parsePaginationConfig(req); 35 | const normalizedFeeRecipient = FEE_RECIPIENT.toLowerCase(); 36 | const feeRecipients = [normalizedFeeRecipient]; 37 | const paginatedFeeRecipients = paginate(feeRecipients, page, perPage); 38 | res.status(HttpStatus.OK).send(paginatedFeeRecipients); 39 | } 40 | public static orderConfig(req: express.Request, res: express.Response): void { 41 | utils.validateSchema(req.body, schemas.orderConfigRequestSchema); 42 | const orderConfigResponse = fixedFeeStrategy.getOrderConfig(req.body); 43 | res.status(HttpStatus.OK).send(orderConfigResponse); 44 | } 45 | public static async assetPairsAsync(req: express.Request, res: express.Response): Promise { 46 | utils.validateSchema(req.query, schemas.assetPairsRequestOptsSchema); 47 | const { page, perPage } = parsePaginationConfig(req); 48 | const assetPairs = await OrderBookService.getAssetPairsAsync( 49 | page, 50 | perPage, 51 | req.query.assetDataA, 52 | req.query.assetDataB, 53 | ); 54 | res.status(HttpStatus.OK).send(assetPairs); 55 | } 56 | public static async getOrderByHashAsync(req: express.Request, res: express.Response): Promise { 57 | const orderIfExists = await OrderBookService.getOrderByHashIfExistsAsync(req.params.orderHash); 58 | if (orderIfExists === undefined) { 59 | throw new NotFoundError(); 60 | } else { 61 | res.status(HttpStatus.OK).send(orderIfExists); 62 | } 63 | } 64 | constructor(orderBook: OrderBookService) { 65 | this._orderBook = orderBook; 66 | } 67 | public async ordersAsync(req: express.Request, res: express.Response): Promise { 68 | utils.validateSchema(req.query, schemas.ordersRequestOptsSchema); 69 | const { page, perPage } = parsePaginationConfig(req); 70 | const paginatedOrders = await this._orderBook.getOrdersAsync(page, perPage, req.query); 71 | res.status(HttpStatus.OK).send(paginatedOrders); 72 | } 73 | public async orderbookAsync(req: express.Request, res: express.Response): Promise { 74 | utils.validateSchema(req.query, schemas.orderBookRequestSchema); 75 | const { page, perPage } = parsePaginationConfig(req); 76 | const baseAssetData = req.query.baseAssetData; 77 | const quoteAssetData = req.query.quoteAssetData; 78 | const orderbookResponse = await this._orderBook.getOrderBookAsync(page, perPage, baseAssetData, quoteAssetData); 79 | res.status(HttpStatus.OK).send(orderbookResponse); 80 | } 81 | public async postOrderAsync(req: express.Request, res: express.Response): Promise { 82 | utils.validateSchema(req.body, schemas.signedOrderSchema); 83 | const signedOrder = unmarshallOrder(req.body); 84 | if (WHITELISTED_TOKENS !== '*') { 85 | const allowedTokens: string[] = WHITELISTED_TOKENS; 86 | validateAssetDataIsWhitelistedOrThrow(allowedTokens, signedOrder.makerAssetData, 'makerAssetData'); 87 | validateAssetDataIsWhitelistedOrThrow(allowedTokens, signedOrder.takerAssetData, 'takerAssetData'); 88 | } 89 | await this._orderBook.addOrderAsync(signedOrder); 90 | res.status(HttpStatus.OK).send(); 91 | } 92 | } 93 | 94 | function validateAssetDataIsWhitelistedOrThrow(allowedTokens: string[], assetData: string, field: string): void { 95 | const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); 96 | if (assetDataUtils.isMultiAssetData(decodedAssetData)) { 97 | for (const [, nestedAssetDataElement] of decodedAssetData.nestedAssetData.entries()) { 98 | validateAssetDataIsWhitelistedOrThrow(allowedTokens, nestedAssetDataElement, field); 99 | } 100 | } else if (!assetDataUtils.isStaticCallAssetData(decodedAssetData)) { 101 | if (!_.includes(allowedTokens, decodedAssetData.tokenAddress)) { 102 | throw new ValidationError([ 103 | { 104 | field, 105 | code: ValidationErrorCodes.ValueOutOfRange, 106 | reason: `Token ${decodedAssetData.tokenAddress} not supported`, 107 | }, 108 | ]); 109 | } 110 | } 111 | } 112 | 113 | // As the orders come in as JSON they need to be turned into the correct types such as BigNumber 114 | function unmarshallOrder(signedOrderRaw: any): SignedOrder { 115 | const signedOrder = { 116 | ...signedOrderRaw, 117 | salt: new BigNumber(signedOrderRaw.salt), 118 | makerAssetAmount: new BigNumber(signedOrderRaw.makerAssetAmount), 119 | takerAssetAmount: new BigNumber(signedOrderRaw.takerAssetAmount), 120 | makerFee: new BigNumber(signedOrderRaw.makerFee), 121 | takerFee: new BigNumber(signedOrderRaw.takerFee), 122 | expirationTimeSeconds: new BigNumber(signedOrderRaw.expirationTimeSeconds), 123 | }; 124 | return signedOrder; 125 | } 126 | -------------------------------------------------------------------------------- /ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import { WSClient } from '@0x/mesh-rpc-client'; 2 | import * as express from 'express'; 3 | import 'reflect-metadata'; 4 | 5 | import * as config from './config'; 6 | import { initDBConnectionAsync } from './db_connection'; 7 | import { HttpService } from './services/http_service'; 8 | import { OrderWatcherService } from './services/order_watcher_service'; 9 | import { OrderBookService } from './services/orderbook_service'; 10 | import { WebsocketService } from './services/websocket_service'; 11 | import { utils } from './utils'; 12 | 13 | (async () => { 14 | await initDBConnectionAsync(); 15 | const app = express(); 16 | const server = app.listen(config.HTTP_PORT, () => { 17 | utils.log( 18 | `Standard relayer API (HTTP) listening on port ${config.HTTP_PORT}!\nConfig: ${JSON.stringify( 19 | config, 20 | null, 21 | 2, 22 | )}`, 23 | ); 24 | }); 25 | let meshClient; 26 | await utils.attemptAsync( 27 | async () => { 28 | meshClient = new WSClient(config.MESH_ENDPOINT); 29 | await meshClient.getStatsAsync(); 30 | }, 31 | { interval: 3000, maxRetries: 10 }, 32 | ); 33 | if (!meshClient) { 34 | throw new Error('Unable to establish connection to Mesh'); 35 | } 36 | utils.log('Connected to Mesh'); 37 | const orderWatcherService = new OrderWatcherService(meshClient); 38 | await orderWatcherService.syncOrderbookAsync(); 39 | // tslint:disable-next-line:no-unused-expression 40 | new WebsocketService(server, meshClient); 41 | const orderBookService = new OrderBookService(meshClient); 42 | // tslint:disable-next-line:no-unused-expression 43 | new HttpService(app, orderBookService); 44 | })().catch(utils.log); 45 | -------------------------------------------------------------------------------- /ts/src/mesh_utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AcceptedOrderInfo, 3 | OrderEvent, 4 | OrderEventEndState, 5 | OrderInfo, 6 | RejectedCode, 7 | RejectedOrderInfo, 8 | SignedOrder, 9 | ValidationResults, 10 | WSClient, 11 | } from '@0x/mesh-rpc-client'; 12 | import * as _ from 'lodash'; 13 | 14 | import { ZERO } from './constants'; 15 | import { ValidationErrorCodes } from './errors'; 16 | import { APIOrderWithMetaData } from './types'; 17 | 18 | // tslint:disable-next-line:no-var-requires 19 | const d = require('debug')('MESH'); 20 | 21 | // tslint:disable-next-line:no-unnecessary-class 22 | export class MeshUtils { 23 | public static async addOrdersToMeshAsync( 24 | meshClient: WSClient, 25 | orders: SignedOrder[], 26 | batchSize: number = 100, 27 | ): Promise { 28 | // Mesh rpc client can't handle a large amount of orders. This results in a fragmented 29 | // send which Mesh cannot accept. 30 | const validationResults: ValidationResults = { accepted: [], rejected: [] }; 31 | const chunks = _.chunk(orders, batchSize); 32 | for (const chunk of chunks) { 33 | const results = await meshClient.addOrdersAsync(chunk, true); 34 | validationResults.accepted = [...validationResults.accepted, ...results.accepted]; 35 | validationResults.rejected = [...validationResults.rejected, ...results.rejected]; 36 | } 37 | return validationResults; 38 | } 39 | public static orderInfosToApiOrders( 40 | orderEvent: Array, 41 | ): APIOrderWithMetaData[] { 42 | return orderEvent.map(e => MeshUtils.orderInfoToAPIOrder(e)); 43 | } 44 | public static orderInfoToAPIOrder( 45 | orderEvent: OrderEvent | AcceptedOrderInfo | RejectedOrderInfo | OrderInfo, 46 | ): APIOrderWithMetaData { 47 | const remainingFillableTakerAssetAmount = (orderEvent as OrderEvent).fillableTakerAssetAmount 48 | ? (orderEvent as OrderEvent).fillableTakerAssetAmount 49 | : ZERO; 50 | return { 51 | order: orderEvent.signedOrder, 52 | metaData: { 53 | orderHash: orderEvent.orderHash, 54 | remainingFillableTakerAssetAmount, 55 | }, 56 | }; 57 | } 58 | public static rejectedCodeToSRACode(code: RejectedCode): ValidationErrorCodes { 59 | switch (code) { 60 | case RejectedCode.OrderCancelled: 61 | case RejectedCode.OrderExpired: 62 | case RejectedCode.OrderUnfunded: 63 | case RejectedCode.OrderHasInvalidMakerAssetAmount: 64 | case RejectedCode.OrderHasInvalidMakerAssetData: 65 | case RejectedCode.OrderHasInvalidTakerAssetAmount: 66 | case RejectedCode.OrderHasInvalidTakerAssetData: 67 | case RejectedCode.OrderFullyFilled: { 68 | return ValidationErrorCodes.InvalidOrder; 69 | } 70 | case RejectedCode.OrderHasInvalidSignature: { 71 | return ValidationErrorCodes.InvalidSignatureOrHash; 72 | } 73 | case RejectedCode.OrderForIncorrectChain: { 74 | return ValidationErrorCodes.InvalidAddress; 75 | } 76 | default: 77 | return ValidationErrorCodes.InternalError; 78 | } 79 | } 80 | public static calculateAddedRemovedUpdated( 81 | orderEvents: OrderEvent[], 82 | ): { added: APIOrderWithMetaData[]; removed: APIOrderWithMetaData[]; updated: APIOrderWithMetaData[] } { 83 | const added = []; 84 | const removed = []; 85 | const updated = []; 86 | for (const event of orderEvents) { 87 | const apiOrder = MeshUtils.orderInfoToAPIOrder(event); 88 | switch (event.endState) { 89 | case OrderEventEndState.Added: { 90 | added.push(apiOrder); 91 | break; 92 | } 93 | case OrderEventEndState.Cancelled: 94 | case OrderEventEndState.Expired: 95 | case OrderEventEndState.FullyFilled: 96 | case OrderEventEndState.Unfunded: { 97 | removed.push(apiOrder); 98 | break; 99 | } 100 | case OrderEventEndState.FillabilityIncreased: 101 | case OrderEventEndState.Filled: { 102 | updated.push(apiOrder); 103 | break; 104 | } 105 | default: 106 | d('Unknown Event', event.endState, event); 107 | break; 108 | } 109 | } 110 | return { added, removed, updated }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ts/src/middleware/error_handling.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as HttpStatus from 'http-status-codes'; 3 | 4 | import { 5 | BadRequestError, 6 | ErrorBodyWithHTTPStatusCode, 7 | GeneralErrorCodes, 8 | generalErrorCodeToReason, 9 | RelayerBaseError, 10 | ValidationError, 11 | } from '../errors'; 12 | 13 | /** 14 | * Wraps an Error with a JSON human readable reason and status code. 15 | */ 16 | export function generateError(err: Error): ErrorBodyWithHTTPStatusCode { 17 | if ((err as any).isRelayerError) { 18 | const relayerError = err as RelayerBaseError; 19 | const statusCode = relayerError.statusCode; 20 | if (relayerError.statusCode === HttpStatus.BAD_REQUEST) { 21 | const badRequestError = relayerError as BadRequestError; 22 | if (badRequestError.generalErrorCode === GeneralErrorCodes.ValidationError) { 23 | const validationError = badRequestError as ValidationError; 24 | return { 25 | statusCode, 26 | errorBody: { 27 | code: badRequestError.generalErrorCode, 28 | reason: generalErrorCodeToReason[badRequestError.generalErrorCode], 29 | validationErrors: validationError.validationErrors, 30 | }, 31 | }; 32 | } else { 33 | return { 34 | statusCode, 35 | errorBody: { 36 | code: badRequestError.generalErrorCode, 37 | reason: generalErrorCodeToReason[badRequestError.generalErrorCode], 38 | }, 39 | }; 40 | } 41 | } else { 42 | return { 43 | statusCode, 44 | errorBody: { 45 | reason: HttpStatus.getStatusText(relayerError.statusCode), 46 | }, 47 | }; 48 | } 49 | } 50 | return { 51 | statusCode: HttpStatus.BAD_REQUEST, 52 | errorBody: { 53 | reason: err.message, 54 | }, 55 | }; 56 | } 57 | 58 | /** 59 | * Catches errors thrown by our code and serialies them 60 | */ 61 | export function errorHandler( 62 | err: Error, 63 | _req: express.Request, 64 | res: express.Response, 65 | next: express.NextFunction, 66 | ): void { 67 | // If you call next() with an error after you have started writing the response 68 | // (for example, if you encounter an error while streaming the response to the client) 69 | // the Express default error handler closes the connection and fails the request. 70 | if (res.headersSent) { 71 | return next(err); 72 | } 73 | if ((err as any).isRelayerError || (err as any).statusCode) { 74 | const { statusCode, errorBody } = generateError(err); 75 | res.status(statusCode).send(errorBody); 76 | return; 77 | } else { 78 | return next(err); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ts/src/middleware/url_params_parsing.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as _ from 'lodash'; 3 | 4 | import { CHAIN_ID } from '../config'; 5 | import { ValidationError } from '../errors'; 6 | 7 | /** 8 | * Parses URL params and stores them on the request object 9 | */ 10 | export function urlParamsParsing(req: express.Request, _res: express.Response, next: express.NextFunction): void { 11 | const chainId = parseChainId(req.query.chainId); 12 | // HACK: This is the recommended way to pass data from middlewares on. It's not beautiful nor fully type-safe. 13 | (req as any).chainId = chainId; 14 | next(); 15 | } 16 | 17 | function parseChainId(chainIdStrIfExists?: string): number { 18 | if (chainIdStrIfExists === undefined) { 19 | return CHAIN_ID; 20 | } else { 21 | const chainId = _.parseInt(chainIdStrIfExists); 22 | if (chainId !== CHAIN_ID) { 23 | const validationErrorItem = { 24 | field: 'chainId', 25 | code: 1004, 26 | reason: `Incorrect Chain ID: ${chainIdStrIfExists}`, 27 | }; 28 | throw new ValidationError([validationErrorItem]); 29 | } 30 | return chainId; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ts/src/models/SignedOrderModel.ts: -------------------------------------------------------------------------------- 1 | export class SignedOrderModel { 2 | public hash?: string; 3 | public senderAddress?: string; 4 | public makerAddress?: string; 5 | public takerAddress?: string; 6 | public makerAssetData?: string; 7 | public takerAssetData?: string; 8 | public exchangeAddress?: string; 9 | public feeRecipientAddress?: string; 10 | public expirationTimeSeconds?: string; 11 | public makerFee?: string; 12 | public takerFee?: string; 13 | public makerAssetAmount?: string; 14 | public takerAssetAmount?: string; 15 | public salt?: string; 16 | public signature?: string; 17 | public remainingFillableTakerAssetAmount?: string; 18 | public makerFeeAssetData?: string; 19 | public takerFeeAssetData?: string; 20 | constructor( 21 | opts: { 22 | hash?: string; 23 | senderAddress?: string; 24 | makerAddress?: string; 25 | takerAddress?: string; 26 | makerAssetData?: string; 27 | takerAssetData?: string; 28 | exchangeAddress?: string; 29 | feeRecipientAddress?: string; 30 | expirationTimeSeconds?: string; 31 | makerFee?: string; 32 | takerFee?: string; 33 | makerFeeAssetData?: string; 34 | takerFeeAssetData?: string; 35 | makerAssetAmount?: string; 36 | takerAssetAmount?: string; 37 | salt?: string; 38 | signature?: string; 39 | remainingFillableTakerAssetAmount?: string; 40 | } = {}, 41 | ) { 42 | this.hash = opts.hash; 43 | this.senderAddress = opts.senderAddress; 44 | this.makerAddress = opts.makerAddress; 45 | this.takerAddress = opts.takerAddress; 46 | this.makerAssetData = opts.makerAssetData; 47 | this.takerAssetData = opts.takerAssetData; 48 | this.exchangeAddress = opts.exchangeAddress; 49 | this.feeRecipientAddress = opts.feeRecipientAddress; 50 | this.expirationTimeSeconds = opts.expirationTimeSeconds; 51 | this.makerFee = opts.makerFee; 52 | this.takerFee = opts.takerFee; 53 | this.makerFeeAssetData = opts.makerFeeAssetData; 54 | this.takerFeeAssetData = opts.takerFeeAssetData; 55 | this.makerAssetAmount = opts.makerAssetAmount; 56 | this.takerAssetAmount = opts.takerAssetAmount; 57 | this.salt = opts.salt; 58 | this.signature = opts.signature; 59 | this.remainingFillableTakerAssetAmount = opts.remainingFillableTakerAssetAmount; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ts/src/paginator.ts: -------------------------------------------------------------------------------- 1 | export const paginate = (collection: T[], page: number, perPage: number) => { 2 | const paginatedCollection = { 3 | total: collection.length, 4 | page, 5 | perPage, 6 | records: collection.slice((page - 1) * perPage, page * perPage), 7 | }; 8 | return paginatedCollection; 9 | }; 10 | -------------------------------------------------------------------------------- /ts/src/runners/http_service_runner.ts: -------------------------------------------------------------------------------- 1 | import { WSClient } from '@0x/mesh-rpc-client'; 2 | import * as express from 'express'; 3 | import 'reflect-metadata'; 4 | 5 | import * as config from '../config'; 6 | import { initDBConnectionAsync } from '../db_connection'; 7 | import { HttpService } from '../services/http_service'; 8 | import { OrderBookService } from '../services/orderbook_service'; 9 | import { utils } from '../utils'; 10 | 11 | /** 12 | * This service handles the HTTP requests. This involves fetching from the database 13 | * as well as adding orders to mesh. 14 | */ 15 | (async () => { 16 | await initDBConnectionAsync(); 17 | const app = express(); 18 | app.listen(config.HTTP_PORT, () => { 19 | utils.log( 20 | `Standard relayer API (HTTP) listening on port ${config.HTTP_PORT}!\nConfig: ${JSON.stringify( 21 | config, 22 | null, 23 | 2, 24 | )}`, 25 | ); 26 | }); 27 | const meshClient = new WSClient(config.MESH_ENDPOINT); 28 | const orderBookService = new OrderBookService(meshClient); 29 | // tslint:disable-next-line:no-unused-expression 30 | new HttpService(app, orderBookService); 31 | })().catch(utils.log); 32 | -------------------------------------------------------------------------------- /ts/src/runners/order_watcher_service_runner.ts: -------------------------------------------------------------------------------- 1 | import { WSClient } from '@0x/mesh-rpc-client'; 2 | import 'reflect-metadata'; 3 | 4 | import * as config from '../config'; 5 | import { initDBConnectionAsync } from '../db_connection'; 6 | import { OrderWatcherService } from '../services/order_watcher_service'; 7 | import { utils } from '../utils'; 8 | 9 | /** 10 | * This service is a simple writer from the Mesh events. On order discovery 11 | * or an order update it will be persisted to the database. It also is responsible 12 | * for syncing the database with Mesh on start or after a disconnect. 13 | */ 14 | (async () => { 15 | await initDBConnectionAsync(); 16 | utils.log(`Order Watching Service started!\nConfig: ${JSON.stringify(config, null, 2)}`); 17 | const meshClient = new WSClient(config.MESH_ENDPOINT); 18 | const orderWatcherService = new OrderWatcherService(meshClient); 19 | await orderWatcherService.syncOrderbookAsync(); 20 | })().catch(utils.log); 21 | -------------------------------------------------------------------------------- /ts/src/runners/websocket_service_runner.ts: -------------------------------------------------------------------------------- 1 | import { WSClient } from '@0x/mesh-rpc-client'; 2 | import * as express from 'express'; 3 | import 'reflect-metadata'; 4 | 5 | import * as config from '../config'; 6 | import { initDBConnectionAsync } from '../db_connection'; 7 | import { WebsocketService } from '../services/websocket_service'; 8 | import { utils } from '../utils'; 9 | 10 | /** 11 | * This service handles websocket updates using a subscription from Mesh. 12 | */ 13 | (async () => { 14 | await initDBConnectionAsync(); 15 | const app = express(); 16 | const server = app.listen(config.HTTP_PORT, () => { 17 | utils.log( 18 | `Standard relayer API (WS) listening on port ${config.HTTP_PORT}!\nConfig: ${JSON.stringify( 19 | config, 20 | null, 21 | 2, 22 | )}`, 23 | ); 24 | }); 25 | const meshClient = new WSClient(config.MESH_ENDPOINT); 26 | // tslint:disable-next-line:no-unused-expression 27 | new WebsocketService(server, meshClient); 28 | })().catch(utils.log); 29 | -------------------------------------------------------------------------------- /ts/src/services/http_service.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as cors from 'cors'; 3 | import * as asyncHandler from 'express-async-handler'; 4 | // tslint:disable-next-line:no-implicit-dependencies 5 | import * as core from 'express-serve-static-core'; 6 | import 'reflect-metadata'; 7 | 8 | import { Handlers } from '../handlers'; 9 | import { errorHandler } from '../middleware/error_handling'; 10 | import { urlParamsParsing } from '../middleware/url_params_parsing'; 11 | import { OrderBookService } from '../services/orderbook_service'; 12 | 13 | // tslint:disable-next-line:no-unnecessary-class 14 | export class HttpService { 15 | constructor(app: core.Express, orderBook: OrderBookService) { 16 | const handlers = new Handlers(orderBook); 17 | app.use(cors()); 18 | app.use(bodyParser.json()); 19 | app.use(urlParamsParsing); 20 | 21 | /** 22 | * GET AssetPairs endpoint retrieves a list of available asset pairs and the information required to trade them. 23 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getAssetPairs 24 | */ 25 | app.get('/v3/asset_pairs', asyncHandler(Handlers.assetPairsAsync.bind(Handlers))); 26 | /** 27 | * GET Orders endpoint retrieves a list of orders given query parameters. 28 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrders 29 | */ 30 | app.get('/v3/orders', asyncHandler(handlers.ordersAsync.bind(handlers))); 31 | /** 32 | * GET Orderbook endpoint retrieves the orderbook for a given asset pair. 33 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrderbook 34 | */ 35 | app.get('/v3/orderbook', asyncHandler(handlers.orderbookAsync.bind(handlers))); 36 | /** 37 | * GET FeeRecepients endpoint retrieves a collection of all fee recipient addresses for a relayer. 38 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/v3/fee_recipients 39 | */ 40 | app.get('/v3/fee_recipients', Handlers.feeRecipients.bind(Handlers)); 41 | /** 42 | * POST Order config endpoint retrives the values for order fields that the relayer requires. 43 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrderConfig 44 | */ 45 | app.post('/v3/order_config', Handlers.orderConfig.bind(Handlers)); 46 | /** 47 | * POST Order endpoint submits an order to the Relayer. 48 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/postOrder 49 | */ 50 | app.post('/v3/order', asyncHandler(handlers.postOrderAsync.bind(handlers))); 51 | /** 52 | * GET Order endpoint retrieves the order by order hash. 53 | * http://sra-spec.s3-website-us-east-1.amazonaws.com/#operation/getOrder 54 | */ 55 | app.get('/v3/order/:orderHash', asyncHandler(Handlers.getOrderByHashAsync.bind(Handlers))); 56 | 57 | app.use(errorHandler); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ts/src/services/order_watcher_service.ts: -------------------------------------------------------------------------------- 1 | import { WSClient } from '@0x/mesh-rpc-client'; 2 | import * as _ from 'lodash'; 3 | 4 | import { getDBConnection } from '../db_connection'; 5 | import { MeshUtils } from '../mesh_utils'; 6 | import { SignedOrderModel } from '../models/SignedOrderModel'; 7 | import { APIOrderWithMetaData, OrderWatcherLifeCycleEvents } from '../types'; 8 | 9 | import { deserializeOrder, serializeOrder } from './orderbook_utils'; 10 | 11 | // tslint:disable-next-line:no-var-requires 12 | const d = require('debug')('orderbook'); 13 | 14 | export class OrderWatcherService { 15 | private readonly _meshClient: WSClient; 16 | private static async _onOrderLifeCycleEventAsync( 17 | lifecycleEvent: OrderWatcherLifeCycleEvents, 18 | orders: APIOrderWithMetaData[], 19 | ): Promise { 20 | if (orders.length <= 0) { 21 | return; 22 | } 23 | const connection = getDBConnection(); 24 | switch (lifecycleEvent) { 25 | case OrderWatcherLifeCycleEvents.Updated: 26 | case OrderWatcherLifeCycleEvents.Added: { 27 | const signedOrdersModel = orders.map(o => serializeOrder(o)); 28 | // MAX SQL variable size is 999. This limit is imposed via Sqlite. 29 | // The SELECT query is not entirely effecient and pulls in all attributes 30 | // so we need to leave space for the attributes on the model represented 31 | // as SQL variables in the "AS" syntax. We leave 99 free for the 32 | // signedOrders model 33 | await connection.manager.save(signedOrdersModel, { chunk: 900 }); 34 | break; 35 | } 36 | case OrderWatcherLifeCycleEvents.Removed: { 37 | const orderHashes = orders.map(o => o.metaData.orderHash); 38 | // MAX SQL variable size is 999. This limit is imposed via Sqlite 39 | // and other databases have higher limits (or no limits at all, eg postgresql) 40 | // tslint:disable-next-line:custom-no-magic-numbers 41 | const chunks = _.chunk(orderHashes, 999); 42 | for (const chunk of chunks) { 43 | await connection.manager.delete(SignedOrderModel, chunk); 44 | } 45 | break; 46 | } 47 | default: 48 | // Do Nothing 49 | } 50 | } 51 | constructor(meshClient: WSClient) { 52 | this._meshClient = meshClient; 53 | void this._meshClient.subscribeToOrdersAsync(async orders => { 54 | const { added, removed, updated } = MeshUtils.calculateAddedRemovedUpdated(orders); 55 | await OrderWatcherService._onOrderLifeCycleEventAsync(OrderWatcherLifeCycleEvents.Removed, removed); 56 | await OrderWatcherService._onOrderLifeCycleEventAsync(OrderWatcherLifeCycleEvents.Updated, updated); 57 | await OrderWatcherService._onOrderLifeCycleEventAsync(OrderWatcherLifeCycleEvents.Added, added); 58 | }); 59 | this._meshClient.onReconnected(async () => { 60 | d('Reconnecting to Mesh'); 61 | await this.syncOrderbookAsync(); 62 | }); 63 | } 64 | public async syncOrderbookAsync(): Promise { 65 | d('SYNC orderbook with Mesh'); 66 | const connection = getDBConnection(); 67 | const signedOrderModels = (await connection.manager.find(SignedOrderModel)) as Array< 68 | Required 69 | >; 70 | const signedOrders = signedOrderModels.map(deserializeOrder); 71 | // Sync the order watching service state locally 72 | const orders = await this._meshClient.getOrdersAsync(); 73 | // TODO(dekz): Mesh can reject due to InternalError or EthRPCRequestFailed. 74 | // in the future we can attempt to retry these a few times. Ultimately if we 75 | // cannot validate the order we cannot keep the order around 76 | // Validate the local state and notify the order watcher of any missed orders 77 | const { accepted, rejected } = await MeshUtils.addOrdersToMeshAsync(this._meshClient, signedOrders); 78 | d(`SYNC ${rejected.length} rejected ${accepted.length} accepted ${signedOrders.length} sent`); 79 | // Remove all of the rejected orders 80 | if (rejected.length > 0) { 81 | await OrderWatcherService._onOrderLifeCycleEventAsync( 82 | OrderWatcherLifeCycleEvents.Removed, 83 | MeshUtils.orderInfosToApiOrders(rejected), 84 | ); 85 | } 86 | // Sync the order watching service state locally 87 | if (orders.length > 0) { 88 | await OrderWatcherService._onOrderLifeCycleEventAsync( 89 | OrderWatcherLifeCycleEvents.Added, 90 | MeshUtils.orderInfosToApiOrders(orders), 91 | ); 92 | } 93 | d('SYNC complete'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ts/src/services/orderbook_service.ts: -------------------------------------------------------------------------------- 1 | import { APIOrder, OrderbookResponse, PaginatedCollection } from '@0x/connect'; 2 | import { WSClient } from '@0x/mesh-rpc-client'; 3 | import { assetDataUtils } from '@0x/order-utils'; 4 | import { AssetPairsItem, OrdersRequestOpts, SignedOrder } from '@0x/types'; 5 | import * as _ from 'lodash'; 6 | 7 | import { getDBConnection } from '../db_connection'; 8 | import { ValidationError } from '../errors'; 9 | import { MeshUtils } from '../mesh_utils'; 10 | import { SignedOrderModel } from '../models/SignedOrderModel'; 11 | import { paginate } from '../paginator'; 12 | 13 | import { 14 | compareAskOrder, 15 | compareBidOrder, 16 | deserializeOrder, 17 | deserializeOrderToAPIOrder, 18 | includesTokenAddress, 19 | signedOrderToAssetPair, 20 | } from './orderbook_utils'; 21 | 22 | export class OrderBookService { 23 | private readonly _meshClient: WSClient; 24 | public static async getOrderByHashIfExistsAsync(orderHash: string): Promise { 25 | const connection = getDBConnection(); 26 | const signedOrderModelIfExists = await connection.manager.findOne(SignedOrderModel, orderHash); 27 | if (signedOrderModelIfExists === undefined) { 28 | return undefined; 29 | } else { 30 | const deserializedOrder = deserializeOrderToAPIOrder(signedOrderModelIfExists as Required< 31 | SignedOrderModel 32 | >); 33 | return deserializedOrder; 34 | } 35 | } 36 | public static async getAssetPairsAsync( 37 | page: number, 38 | perPage: number, 39 | assetDataA: string, 40 | assetDataB: string, 41 | ): Promise> { 42 | const connection = getDBConnection(); 43 | const signedOrderModels = (await connection.manager.find(SignedOrderModel)) as Array< 44 | Required 45 | >; 46 | 47 | const assetPairsItems: AssetPairsItem[] = signedOrderModels.map(deserializeOrder).map(signedOrderToAssetPair); 48 | let nonPaginatedFilteredAssetPairs: AssetPairsItem[]; 49 | if (assetDataA === undefined && assetDataB === undefined) { 50 | nonPaginatedFilteredAssetPairs = assetPairsItems; 51 | } else if (assetDataA !== undefined && assetDataB !== undefined) { 52 | const containsAssetDataAAndAssetDataB = (assetPair: AssetPairsItem) => 53 | (assetPair.assetDataA.assetData === assetDataA && assetPair.assetDataB.assetData === assetDataB) || 54 | (assetPair.assetDataA.assetData === assetDataB && assetPair.assetDataB.assetData === assetDataA); 55 | nonPaginatedFilteredAssetPairs = assetPairsItems.filter(containsAssetDataAAndAssetDataB); 56 | } else { 57 | const assetData = assetDataA || assetDataB; 58 | const containsAssetData = (assetPair: AssetPairsItem) => 59 | assetPair.assetDataA.assetData === assetData || assetPair.assetDataB.assetData === assetData; 60 | nonPaginatedFilteredAssetPairs = assetPairsItems.filter(containsAssetData); 61 | } 62 | const uniqueNonPaginatedFilteredAssetPairs = _.uniqWith(nonPaginatedFilteredAssetPairs, _.isEqual.bind(_)); 63 | const paginatedFilteredAssetPairs = paginate(uniqueNonPaginatedFilteredAssetPairs, page, perPage); 64 | return paginatedFilteredAssetPairs; 65 | } 66 | // tslint:disable-next-line:prefer-function-over-method 67 | public async getOrderBookAsync( 68 | page: number, 69 | perPage: number, 70 | baseAssetData: string, 71 | quoteAssetData: string, 72 | ): Promise { 73 | const connection = getDBConnection(); 74 | const bidSignedOrderModels = (await connection.manager.find(SignedOrderModel, { 75 | where: { takerAssetData: baseAssetData, makerAssetData: quoteAssetData }, 76 | })) as Array>; 77 | const askSignedOrderModels = (await connection.manager.find(SignedOrderModel, { 78 | where: { takerAssetData: quoteAssetData, makerAssetData: baseAssetData }, 79 | })) as Array>; 80 | const bidApiOrders: APIOrder[] = bidSignedOrderModels 81 | .map(deserializeOrderToAPIOrder) 82 | .sort((orderA, orderB) => compareBidOrder(orderA.order, orderB.order)); 83 | const askApiOrders: APIOrder[] = askSignedOrderModels 84 | .map(deserializeOrderToAPIOrder) 85 | .sort((orderA, orderB) => compareAskOrder(orderA.order, orderB.order)); 86 | const paginatedBidApiOrders = paginate(bidApiOrders, page, perPage); 87 | const paginatedAskApiOrders = paginate(askApiOrders, page, perPage); 88 | return { 89 | bids: paginatedBidApiOrders, 90 | asks: paginatedAskApiOrders, 91 | }; 92 | } 93 | // TODO:(leo) Do all filtering and pagination in a DB (requires stored procedures or redundant fields) 94 | // tslint:disable-next-line:prefer-function-over-method 95 | public async getOrdersAsync( 96 | page: number, 97 | perPage: number, 98 | ordersFilterParams: OrdersRequestOpts, 99 | ): Promise> { 100 | const connection = getDBConnection(); 101 | // Pre-filters 102 | const filterObjectWithValuesIfExist: Partial = { 103 | exchangeAddress: ordersFilterParams.exchangeAddress, 104 | senderAddress: ordersFilterParams.senderAddress, 105 | makerAssetData: ordersFilterParams.makerAssetData, 106 | takerAssetData: ordersFilterParams.takerAssetData, 107 | makerAddress: ordersFilterParams.makerAddress, 108 | takerAddress: ordersFilterParams.takerAddress, 109 | feeRecipientAddress: ordersFilterParams.feeRecipientAddress, 110 | makerFeeAssetData: ordersFilterParams.makerFeeAssetData, 111 | takerFeeAssetData: ordersFilterParams.takerFeeAssetData, 112 | }; 113 | const filterObject = _.pickBy(filterObjectWithValuesIfExist, _.identity.bind(_)); 114 | const signedOrderModels = (await connection.manager.find(SignedOrderModel, { where: filterObject })) as Array< 115 | Required 116 | >; 117 | let apiOrders = _.map(signedOrderModels, deserializeOrderToAPIOrder); 118 | // Post-filters 119 | apiOrders = apiOrders 120 | .filter( 121 | // traderAddress 122 | apiOrder => 123 | ordersFilterParams.traderAddress === undefined || 124 | apiOrder.order.makerAddress === ordersFilterParams.traderAddress || 125 | apiOrder.order.takerAddress === ordersFilterParams.traderAddress, 126 | ) 127 | .filter( 128 | // makerAssetAddress 129 | apiOrder => 130 | ordersFilterParams.makerAssetAddress === undefined || 131 | includesTokenAddress(apiOrder.order.makerAssetData, ordersFilterParams.makerAssetAddress), 132 | ) 133 | .filter( 134 | // takerAssetAddress 135 | apiOrder => 136 | ordersFilterParams.takerAssetAddress === undefined || 137 | includesTokenAddress(apiOrder.order.takerAssetData, ordersFilterParams.takerAssetAddress), 138 | ) 139 | .filter( 140 | // makerAssetProxyId 141 | apiOrder => 142 | ordersFilterParams.makerAssetProxyId === undefined || 143 | assetDataUtils.decodeAssetDataOrThrow(apiOrder.order.makerAssetData).assetProxyId === 144 | ordersFilterParams.makerAssetProxyId, 145 | ) 146 | .filter( 147 | // takerAssetProxyId 148 | apiOrder => 149 | ordersFilterParams.takerAssetProxyId === undefined || 150 | assetDataUtils.decodeAssetDataOrThrow(apiOrder.order.takerAssetData).assetProxyId === 151 | ordersFilterParams.takerAssetProxyId, 152 | ); 153 | const paginatedApiOrders = paginate(apiOrders, page, perPage); 154 | return paginatedApiOrders; 155 | } 156 | constructor(meshClient: WSClient) { 157 | this._meshClient = meshClient; 158 | } 159 | public async addOrderAsync(signedOrder: SignedOrder): Promise { 160 | const { rejected } = await this._meshClient.addOrdersAsync([signedOrder], true); 161 | if (rejected.length !== 0) { 162 | throw new ValidationError([ 163 | { 164 | field: 'signedOrder', 165 | code: MeshUtils.rejectedCodeToSRACode(rejected[0].status.code), 166 | reason: `${rejected[0].status.code}: ${rejected[0].status.message}`, 167 | }, 168 | ]); 169 | } 170 | // Order Watcher Service will handle persistence 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /ts/src/services/orderbook_utils.ts: -------------------------------------------------------------------------------- 1 | import { APIOrder } from '@0x/connect'; 2 | import { assetDataUtils } from '@0x/order-utils'; 3 | import { Asset, AssetPairsItem, AssetProxyId, SignedOrder } from '@0x/types'; 4 | import { BigNumber, errorUtils } from '@0x/utils'; 5 | 6 | import { CHAIN_ID, DEFAULT_ERC20_TOKEN_PRECISION } from '../config'; 7 | import { MAX_TOKEN_SUPPLY_POSSIBLE } from '../constants'; 8 | import { SignedOrderModel } from '../models/SignedOrderModel'; 9 | import { APIOrderWithMetaData } from '../types'; 10 | 11 | const DEFAULT_ERC721_ASSET = { 12 | minAmount: new BigNumber(0), 13 | maxAmount: new BigNumber(1), 14 | precision: 0, 15 | }; 16 | const DEFAULT_ERC20_ASSET = { 17 | minAmount: new BigNumber(0), 18 | maxAmount: MAX_TOKEN_SUPPLY_POSSIBLE, 19 | precision: DEFAULT_ERC20_TOKEN_PRECISION, 20 | }; 21 | 22 | export const compareAskOrder = (orderA: SignedOrder, orderB: SignedOrder): number => { 23 | const orderAPrice = orderA.takerAssetAmount.div(orderA.makerAssetAmount); 24 | const orderBPrice = orderB.takerAssetAmount.div(orderB.makerAssetAmount); 25 | if (!orderAPrice.isEqualTo(orderBPrice)) { 26 | return orderAPrice.comparedTo(orderBPrice); 27 | } 28 | 29 | return compareOrderByFeeRatio(orderA, orderB); 30 | }; 31 | 32 | export const compareBidOrder = (orderA: SignedOrder, orderB: SignedOrder): number => { 33 | const orderAPrice = orderA.makerAssetAmount.div(orderA.takerAssetAmount); 34 | const orderBPrice = orderB.makerAssetAmount.div(orderB.takerAssetAmount); 35 | if (!orderAPrice.isEqualTo(orderBPrice)) { 36 | return orderBPrice.comparedTo(orderAPrice); 37 | } 38 | 39 | return compareOrderByFeeRatio(orderA, orderB); 40 | }; 41 | 42 | export const compareOrderByFeeRatio = (orderA: SignedOrder, orderB: SignedOrder): number => { 43 | const orderAFeePrice = orderA.takerFee.div(orderA.takerAssetAmount); 44 | const orderBFeePrice = orderB.takerFee.div(orderB.takerAssetAmount); 45 | if (!orderAFeePrice.isEqualTo(orderBFeePrice)) { 46 | return orderBFeePrice.comparedTo(orderAFeePrice); 47 | } 48 | 49 | return orderA.expirationTimeSeconds.comparedTo(orderB.expirationTimeSeconds); 50 | }; 51 | 52 | export const includesTokenAddress = (assetData: string, tokenAddress: string): boolean => { 53 | const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); 54 | if (assetDataUtils.isMultiAssetData(decodedAssetData)) { 55 | for (const [, nestedAssetDataElement] of decodedAssetData.nestedAssetData.entries()) { 56 | if (includesTokenAddress(nestedAssetDataElement, tokenAddress)) { 57 | return true; 58 | } 59 | } 60 | return false; 61 | } else if (!assetDataUtils.isStaticCallAssetData(decodedAssetData)) { 62 | return decodedAssetData.tokenAddress === tokenAddress; 63 | } 64 | return false; 65 | }; 66 | 67 | export const deserializeOrder = (signedOrderModel: Required): SignedOrder => { 68 | const signedOrder: SignedOrder = { 69 | signature: signedOrderModel.signature, 70 | senderAddress: signedOrderModel.senderAddress, 71 | makerAddress: signedOrderModel.makerAddress, 72 | takerAddress: signedOrderModel.takerAddress, 73 | makerFee: new BigNumber(signedOrderModel.makerFee), 74 | takerFee: new BigNumber(signedOrderModel.takerFee), 75 | makerAssetAmount: new BigNumber(signedOrderModel.makerAssetAmount), 76 | takerAssetAmount: new BigNumber(signedOrderModel.takerAssetAmount), 77 | makerAssetData: signedOrderModel.makerAssetData, 78 | takerAssetData: signedOrderModel.takerAssetData, 79 | salt: new BigNumber(signedOrderModel.salt), 80 | exchangeAddress: signedOrderModel.exchangeAddress, 81 | feeRecipientAddress: signedOrderModel.feeRecipientAddress, 82 | expirationTimeSeconds: new BigNumber(signedOrderModel.expirationTimeSeconds), 83 | makerFeeAssetData: signedOrderModel.makerFeeAssetData, 84 | takerFeeAssetData: signedOrderModel.takerFeeAssetData, 85 | chainId: CHAIN_ID, 86 | }; 87 | return signedOrder; 88 | }; 89 | 90 | export const deserializeOrderToAPIOrder = (signedOrderModel: Required): APIOrder => { 91 | const order = deserializeOrder(signedOrderModel); 92 | const apiOrder: APIOrder = { 93 | order, 94 | metaData: { 95 | orderHash: signedOrderModel.hash, 96 | remainingFillableTakerAssetAmount: signedOrderModel.remainingFillableTakerAssetAmount, 97 | }, 98 | }; 99 | return apiOrder; 100 | }; 101 | 102 | export const serializeOrder = (apiOrder: APIOrderWithMetaData): SignedOrderModel => { 103 | const signedOrder = apiOrder.order; 104 | const signedOrderModel = new SignedOrderModel({ 105 | signature: signedOrder.signature, 106 | senderAddress: signedOrder.senderAddress, 107 | makerAddress: signedOrder.makerAddress, 108 | takerAddress: signedOrder.takerAddress, 109 | makerAssetAmount: signedOrder.makerAssetAmount.toString(), 110 | takerAssetAmount: signedOrder.takerAssetAmount.toString(), 111 | makerAssetData: signedOrder.makerAssetData, 112 | takerAssetData: signedOrder.takerAssetData, 113 | makerFee: signedOrder.makerFee.toString(), 114 | takerFee: signedOrder.takerFee.toString(), 115 | makerFeeAssetData: signedOrder.makerFeeAssetData.toString(), 116 | takerFeeAssetData: signedOrder.takerFeeAssetData.toString(), 117 | salt: signedOrder.salt.toString(), 118 | exchangeAddress: signedOrder.exchangeAddress, 119 | feeRecipientAddress: signedOrder.feeRecipientAddress, 120 | expirationTimeSeconds: signedOrder.expirationTimeSeconds.toString(), 121 | hash: apiOrder.metaData.orderHash, 122 | remainingFillableTakerAssetAmount: apiOrder.metaData.remainingFillableTakerAssetAmount.toString(), 123 | }); 124 | return signedOrderModel; 125 | }; 126 | 127 | const erc721AssetDataToAsset = (assetData: string): Asset => ({ 128 | ...DEFAULT_ERC721_ASSET, 129 | assetData, 130 | }); 131 | 132 | const erc20AssetDataToAsset = (assetData: string): Asset => ({ 133 | ...DEFAULT_ERC20_ASSET, 134 | assetData, 135 | }); 136 | 137 | const assetDataToAsset = (assetData: string): Asset => { 138 | const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData); 139 | let asset: Asset; 140 | switch (assetProxyId) { 141 | case AssetProxyId.ERC20: 142 | asset = erc20AssetDataToAsset(assetData); 143 | break; 144 | case AssetProxyId.ERC721: 145 | asset = erc721AssetDataToAsset(assetData); 146 | break; 147 | default: 148 | throw errorUtils.spawnSwitchErr('assetProxyId', assetProxyId); 149 | } 150 | return asset; 151 | }; 152 | 153 | export const signedOrderToAssetPair = (signedOrder: SignedOrder): AssetPairsItem => { 154 | return { 155 | assetDataA: assetDataToAsset(signedOrder.makerAssetData), 156 | assetDataB: assetDataToAsset(signedOrder.takerAssetData), 157 | }; 158 | }; 159 | -------------------------------------------------------------------------------- /ts/src/services/websocket_service.ts: -------------------------------------------------------------------------------- 1 | import { schemas } from '@0x/json-schemas'; 2 | import { WSClient } from '@0x/mesh-rpc-client'; 3 | import { assetDataUtils } from '@0x/order-utils'; 4 | import { 5 | APIOrder, 6 | AssetProxyId, 7 | OrdersChannelMessageTypes, 8 | OrdersChannelSubscriptionOpts, 9 | SignedOrder, 10 | WebsocketConnectionEventType, 11 | } from '@0x/types'; 12 | import * as http from 'http'; 13 | import * as WebSocket from 'ws'; 14 | 15 | import { MalformedJSONError, NotImplementedError } from '../errors'; 16 | import { MeshUtils } from '../mesh_utils'; 17 | import { generateError } from '../middleware/error_handling'; 18 | import { 19 | MessageChannels, 20 | MessageTypes, 21 | OrderChannelRequest, 22 | UpdateOrdersChannelMessageWithChannel, 23 | WebsocketSRAOpts, 24 | } from '../types'; 25 | import { utils } from '../utils'; 26 | 27 | interface WrappedWebSocket extends WebSocket { 28 | isAlive: boolean; 29 | requestIds: Set; 30 | } 31 | 32 | const DEFAULT_OPTS: WebsocketSRAOpts = { 33 | pongInterval: 5000, 34 | }; 35 | 36 | type ALL_SUBSCRIPTION_OPTS = 'ALL_SUBSCRIPTION_OPTS'; 37 | 38 | export class WebsocketService { 39 | private readonly _server: WebSocket.Server; 40 | private readonly _meshClient: WSClient; 41 | private readonly _pongIntervalId: number; 42 | private readonly _requestIdToSocket: Map = new Map(); // requestId to WebSocket mapping 43 | private readonly _requestIdToSubscriptionOpts: Map< 44 | string, 45 | OrdersChannelSubscriptionOpts | ALL_SUBSCRIPTION_OPTS 46 | > = new Map(); // requestId -> { base, quote } 47 | private _meshSubscriptionId?: string; 48 | private static _decodedContractAndAssetData(assetData: string): { assetProxyId: string; data: string[] } { 49 | let data: string[] = [assetData]; 50 | const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); 51 | if (assetDataUtils.isMultiAssetData(decodedAssetData)) { 52 | for (const nested of decodedAssetData.nestedAssetData) { 53 | data = [...data, ...WebsocketService._decodedContractAndAssetData(nested).data]; 54 | } 55 | } else if (assetDataUtils.isStaticCallAssetData(decodedAssetData)) { 56 | // do nothing 57 | } else { 58 | data = [...data, decodedAssetData.tokenAddress]; 59 | } 60 | return { data, assetProxyId: decodedAssetData.assetProxyId }; 61 | } 62 | private static _matchesOrdersChannelSubscription( 63 | order: SignedOrder, 64 | opts: OrdersChannelSubscriptionOpts | ALL_SUBSCRIPTION_OPTS, 65 | ): boolean { 66 | if (opts === 'ALL_SUBSCRIPTION_OPTS') { 67 | return true; 68 | } 69 | const { makerAssetData, takerAssetData } = order; 70 | const makerAssetDataTakerAssetData = [makerAssetData, takerAssetData]; 71 | // Handle the specific, unambiguous asset datas 72 | // traderAssetData?: string; 73 | if (opts.traderAssetData && makerAssetDataTakerAssetData.includes(opts.traderAssetData)) { 74 | return true; 75 | } 76 | // makerAssetData?: string; 77 | // takerAssetData?: string; 78 | if ( 79 | opts.makerAssetData && 80 | opts.takerAssetData && 81 | makerAssetDataTakerAssetData.includes(opts.makerAssetData) && 82 | makerAssetDataTakerAssetData.includes(opts.takerAssetData) 83 | ) { 84 | return true; 85 | } 86 | // makerAssetAddress?: string; 87 | // takerAssetAddress?: string; 88 | const makerContractAndAssetData = WebsocketService._decodedContractAndAssetData(makerAssetData); 89 | const takerContractAndAssetData = WebsocketService._decodedContractAndAssetData(takerAssetData); 90 | if ( 91 | opts.makerAssetAddress && 92 | opts.takerAssetAddress && 93 | makerContractAndAssetData.assetProxyId !== AssetProxyId.MultiAsset && 94 | makerContractAndAssetData.assetProxyId !== AssetProxyId.StaticCall && 95 | takerContractAndAssetData.assetProxyId !== AssetProxyId.MultiAsset && 96 | takerContractAndAssetData.assetProxyId !== AssetProxyId.StaticCall && 97 | makerContractAndAssetData.data.includes(opts.makerAssetAddress) && 98 | takerContractAndAssetData.data.includes(opts.takerAssetAddress) 99 | ) { 100 | return true; 101 | } 102 | 103 | // TODO (dekz)handle MAP 104 | // makerAssetProxyId?: string; 105 | // takerAssetProxyId?: string; 106 | return false; 107 | } 108 | constructor(server: http.Server, meshClient: WSClient, opts?: WebsocketSRAOpts) { 109 | const wsOpts = { 110 | ...DEFAULT_OPTS, 111 | ...opts, 112 | }; 113 | this._server = new WebSocket.Server({ server }); 114 | this._server.on('connection', this._processConnection.bind(this)); 115 | this._pongIntervalId = setInterval(this._cleanupConnections.bind(this), wsOpts.pongInterval); 116 | this._meshClient = meshClient; 117 | this._meshClient 118 | .subscribeToOrdersAsync(e => this.orderUpdate(MeshUtils.orderInfosToApiOrders(e))) 119 | .then(subscriptionId => (this._meshSubscriptionId = subscriptionId)); 120 | } 121 | public destroy(): void { 122 | clearInterval(this._pongIntervalId); 123 | for (const ws of this._server.clients) { 124 | ws.terminate(); 125 | } 126 | this._requestIdToSocket.clear(); 127 | this._requestIdToSubscriptionOpts.clear(); 128 | this._server.close(); 129 | if (this._meshSubscriptionId) { 130 | void this._meshClient.unsubscribeAsync(this._meshSubscriptionId); 131 | } 132 | } 133 | public orderUpdate(apiOrders: APIOrder[]): void { 134 | if (this._server.clients.size === 0) { 135 | return; 136 | } 137 | const response: Partial = { 138 | type: OrdersChannelMessageTypes.Update, 139 | channel: MessageChannels.Orders, 140 | payload: apiOrders, 141 | }; 142 | for (const order of apiOrders) { 143 | // Future optimisation is to invert this structure so the order isn't duplicated over many request ids 144 | // order->requestIds it is less likely to get multiple order updates and more likely 145 | // to have many subscribers and a single order 146 | const requestIdToOrders: { [requestId: string]: Set } = {}; 147 | for (const [requestId, subscriptionOpts] of this._requestIdToSubscriptionOpts) { 148 | if (WebsocketService._matchesOrdersChannelSubscription(order.order, subscriptionOpts)) { 149 | if (requestIdToOrders[requestId]) { 150 | const orderSet = requestIdToOrders[requestId]; 151 | orderSet.add(order); 152 | } else { 153 | const orderSet = new Set(); 154 | orderSet.add(order); 155 | requestIdToOrders[requestId] = orderSet; 156 | } 157 | } 158 | } 159 | for (const [requestId, orders] of Object.entries(requestIdToOrders)) { 160 | const ws = this._requestIdToSocket.get(requestId); 161 | if (ws) { 162 | ws.send(JSON.stringify({ ...response, payload: Array.from(orders), requestId })); 163 | } 164 | } 165 | } 166 | } 167 | private _processConnection(ws: WrappedWebSocket, _req: http.IncomingMessage): void { 168 | ws.on('pong', this._pongHandler(ws).bind(this)); 169 | ws.on(WebsocketConnectionEventType.Message, this._messageHandler(ws).bind(this)); 170 | ws.on(WebsocketConnectionEventType.Close, this._closeHandler(ws).bind(this)); 171 | ws.isAlive = true; 172 | ws.requestIds = new Set(); 173 | } 174 | private _processMessage(ws: WrappedWebSocket, data: WebSocket.Data): void { 175 | let message: OrderChannelRequest; 176 | try { 177 | message = JSON.parse(data.toString()); 178 | } catch (e) { 179 | throw new MalformedJSONError(); 180 | } 181 | 182 | utils.validateSchema(message, schemas.relayerApiOrdersChannelSubscribeSchema); 183 | const { requestId, payload, type } = message; 184 | switch (type) { 185 | case MessageTypes.Subscribe: 186 | ws.requestIds.add(requestId); 187 | const subscriptionOpts = payload || 'ALL_SUBSCRIPTION_OPTS'; 188 | this._requestIdToSubscriptionOpts.set(requestId, subscriptionOpts); 189 | this._requestIdToSocket.set(requestId, ws); 190 | break; 191 | default: 192 | throw new NotImplementedError(message.type); 193 | } 194 | } 195 | private _cleanupConnections(): void { 196 | // Ping every connection and if it is unresponsive 197 | // terminate it during the next check 198 | for (const ws of this._server.clients) { 199 | if (!(ws as WrappedWebSocket).isAlive) { 200 | ws.terminate(); 201 | } else { 202 | (ws as WrappedWebSocket).isAlive = false; 203 | ws.ping(); 204 | } 205 | } 206 | } 207 | private _messageHandler(ws: WrappedWebSocket): (data: WebSocket.Data) => void { 208 | return (data: WebSocket.Data) => { 209 | try { 210 | this._processMessage(ws, data); 211 | } catch (err) { 212 | this._processError(ws, err); 213 | } 214 | }; 215 | } 216 | // tslint:disable-next-line:prefer-function-over-method 217 | private _processError(ws: WrappedWebSocket, err: Error): void { 218 | const { errorBody } = generateError(err); 219 | ws.send(JSON.stringify(errorBody)); 220 | ws.terminate(); 221 | } 222 | // tslint:disable-next-line:prefer-function-over-method 223 | private _pongHandler(ws: WrappedWebSocket): () => void { 224 | return () => { 225 | ws.isAlive = true; 226 | }; 227 | } 228 | private _closeHandler(ws: WrappedWebSocket): () => void { 229 | return () => { 230 | for (const requestId of ws.requestIds) { 231 | this._requestIdToSocket.delete(requestId); 232 | this._requestIdToSubscriptionOpts.delete(requestId); 233 | } 234 | }; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /ts/src/types.ts: -------------------------------------------------------------------------------- 1 | import { AcceptedOrderInfo, RejectedOrderInfo } from '@0x/mesh-rpc-client'; 2 | import { APIOrder, OrdersChannelSubscriptionOpts, UpdateOrdersChannelMessage } from '@0x/types'; 3 | import { BigNumber } from '@0x/utils'; 4 | 5 | export enum OrderWatcherLifeCycleEvents { 6 | Added, 7 | Removed, 8 | Updated, 9 | } 10 | 11 | export type onOrdersUpdateCallback = (orders: APIOrderWithMetaData[]) => void; 12 | 13 | export interface AcceptedRejectedResults { 14 | accepted: AcceptedOrderInfo[]; 15 | rejected: RejectedOrderInfo[]; 16 | } 17 | 18 | export interface APIOrderMetaData { 19 | orderHash: string; 20 | remainingFillableTakerAssetAmount: BigNumber; 21 | } 22 | 23 | export interface APIOrderWithMetaData extends APIOrder { 24 | metaData: APIOrderMetaData; 25 | } 26 | 27 | export interface WebsocketSRAOpts { 28 | pongInterval?: number; 29 | } 30 | 31 | export interface OrderChannelRequest { 32 | type: string; 33 | channel: MessageChannels; 34 | requestId: string; 35 | payload?: OrdersChannelSubscriptionOpts; 36 | } 37 | 38 | export enum MessageTypes { 39 | Subscribe = 'subscribe', 40 | } 41 | 42 | export enum MessageChannels { 43 | Orders = 'orders', 44 | } 45 | export interface UpdateOrdersChannelMessageWithChannel extends UpdateOrdersChannelMessage { 46 | channel: MessageChannels; 47 | } 48 | -------------------------------------------------------------------------------- /ts/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaValidator } from '@0x/json-schemas'; 2 | import { ValidationError as SchemaValidationError } from 'jsonschema'; 3 | import * as _ from 'lodash'; 4 | 5 | import { ValidationError, ValidationErrorCodes, ValidationErrorItem } from './errors'; 6 | 7 | const schemaValidator = new SchemaValidator(); 8 | 9 | export const utils = { 10 | log: (...args: any[]) => { 11 | // tslint:disable-next-line:no-console 12 | console.log(...args); 13 | }, 14 | validateSchema(instance: any, schema: Schema): void { 15 | const validationResult = schemaValidator.validate(instance, schema); 16 | if (_.isEmpty(validationResult.errors)) { 17 | return; 18 | } else { 19 | const validationErrorItems = _.map( 20 | validationResult.errors, 21 | (schemaValidationError: SchemaValidationError) => 22 | schemaValidationErrorToValidationErrorItem(schemaValidationError), 23 | ); 24 | throw new ValidationError(validationErrorItems); 25 | } 26 | }, 27 | async delayAsync(ms: number): Promise { 28 | // tslint:disable:no-inferred-empty-object-type 29 | return new Promise(resolve => setTimeout(resolve, ms)); 30 | }, 31 | async attemptAsync( 32 | fn: () => Promise, 33 | opts: { interval: number; maxRetries: number } = { interval: 1000, maxRetries: 10 }, 34 | ): Promise { 35 | let result: T | undefined; 36 | let attempt = 0; 37 | let error; 38 | let isSuccess = false; 39 | while (!result && attempt < opts.maxRetries) { 40 | attempt++; 41 | try { 42 | result = await fn(); 43 | isSuccess = true; 44 | error = undefined; 45 | } catch (err) { 46 | error = err; 47 | await utils.delayAsync(opts.interval); 48 | } 49 | } 50 | if (!isSuccess) { 51 | throw error; 52 | } 53 | return result as T; 54 | }, 55 | }; 56 | 57 | function schemaValidationErrorToValidationErrorItem(schemaValidationError: SchemaValidationError): ValidationErrorItem { 58 | if ( 59 | _.includes( 60 | [ 61 | 'type', 62 | 'anyOf', 63 | 'allOf', 64 | 'oneOf', 65 | 'additionalProperties', 66 | 'minProperties', 67 | 'maxProperties', 68 | 'pattern', 69 | 'format', 70 | 'uniqueItems', 71 | 'items', 72 | 'dependencies', 73 | ], 74 | schemaValidationError.name, 75 | ) 76 | ) { 77 | return { 78 | field: schemaValidationError.property, 79 | code: ValidationErrorCodes.IncorrectFormat, 80 | reason: schemaValidationError.message, 81 | }; 82 | } else if ( 83 | _.includes( 84 | ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'enum', 'const'], 85 | schemaValidationError.name, 86 | ) 87 | ) { 88 | return { 89 | field: schemaValidationError.property, 90 | code: ValidationErrorCodes.ValueOutOfRange, 91 | reason: schemaValidationError.message, 92 | }; 93 | } else if (schemaValidationError.name === 'required') { 94 | return { 95 | field: schemaValidationError.argument, 96 | code: ValidationErrorCodes.RequiredField, 97 | reason: schemaValidationError.message, 98 | }; 99 | } else if (schemaValidationError.name === 'not') { 100 | return { 101 | field: schemaValidationError.property, 102 | code: ValidationErrorCodes.UnsupportedOption, 103 | reason: schemaValidationError.message, 104 | }; 105 | } else { 106 | throw new Error(`Unknnown schema validation error name: ${schemaValidationError.name}`); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017", "dom"], 5 | "target": "es5", 6 | "outDir": "lib", 7 | "strict": true, 8 | "downlevelIteration": true, 9 | "noUnusedLocals": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": false, 13 | "typeRoots": ["../node_modules/@0x/typescript-typings/types", "../node_modules/@types"], 14 | "noUnusedParameters": true 15 | }, 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /ts/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@0x/tslint-config"] 3 | } 4 | --------------------------------------------------------------------------------