diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 00000000..dc4cbeb2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,13 @@ +#### Issue type +- [ ] bug +- [ ] missing functionality +- [ ] performance +- [ ] feature request + +#### Brief description + +#### Steps to reproduce +- + +##### Additional Notes: +- diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..cf62d771 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,16 @@ +### Description: +... + +### Breaking changes: +- [ ] + +### New features: +- [ ] + +### Fixes: +- [ ] + +### PR status: +- [ ] Version bumped +- [ ] Change-log updated +- [ ] Documentation updated diff --git a/.gitignore b/.gitignore index d6d29376..c0a7bdb0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,14 @@ node_modules /build npm-debug.log source/images +.DS_Store +todo +package-lock.json +.env +dist/ +.nyc_output +coverage/ +.vim +tags +.undodir +Session.vim diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..eed9c684 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# only run on added/modified files +git diff --name-only --cached --diff-filter=AM -- "*.js" | \ +while read -r file +do + if [ -f "$file" ]; then # don't run on deleted files + npx standard "$file" + fi +done diff --git a/.jsdoc.json b/.jsdoc.json new file mode 100644 index 00000000..44805c70 --- /dev/null +++ b/.jsdoc.json @@ -0,0 +1,27 @@ +{ + "tags": { + "allowUnknownTags": false, + "dictionaries": ["jsdoc"] + }, + "source": { + "include": ["lib", "LICENSE.md", "README.md"], + "includePattern": ".js$", + "excludePattern": "(node_modules/|docs)" + }, + "plugins": [ + "plugins/markdown" + ], + "templates": { + "cleverLinks": false, + "monospaceLinks": true, + "useLongnameInNav": false, + "showInheritedInNav": true + }, + "opts": { + "destination": "./docs/", + "encoding": "utf8", + "recurse": true, + "template": "./node_modules/docdash" + }, + "package": "" +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..70bd27bb --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +test +docs +coverage +examples +.github +.nyc_output +.travis.yml +.istanbul.yml +.jsdoc.json diff --git a/.travis.yml b/.travis.yml index 6998e324..3ba4c550 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,12 @@ +sudo: false + language: node_js node_js: - - "stable" \ No newline at end of file + - "stable" + +install: + - npm install + +script: + - npm run lint + - npm run unit diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 00000000..bf5810d0 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,266 @@ +8.0.0 +- upgrade: upgraded bfx-api-node-rest to 7.0.0, breaks previous versions compatibility +- upgrade: upgraded bfx-api-mock-srv to 2.0.0, breaks previous versions compatibility + +7.0.0 +- chore: remove pulse deps + +6.0.0 +- removed unused deps: chai, request-promise, request, crc-32 +- removed bluebird Promise +- update params of submitOrder and cancelOrders methods of WS transport to support new rest api signature +- bumped bfx-api-node-rest version up to 5.1.1, breaks previous versions compatibility +- bumped mocha, jsdoc-to-markdown, docdash, blessed-contrib, ws versions to fix vulnerabilities +- moved dev deps readline-promise, blessed, blessed-contrib, cli-table3, p-iteration into corresponding section + +5.0.4 +- fix: public funding trade parsing +- styling: fix code formatting + +5.0.0 +- upgrade: upgraded bfx-api-node-rest to 4.0.0, breaks previous versions compatibility +- fix: jsdocs + +4.0.17 +- added pulse examples + +4.0.16 +- fix: unsubscribe fails depending on channel id type + +4.0.15 +- fix 2 high vulnerabilities, switch from cli-table2 to cli-table3 dependency + +4.0.14 +- fix: README docs reference + +4.0.13 +- meta: mv several moduels to deps from dev-deps for bfx-cli + +4.0.12 +- meta: mv readline-promise to deps from dev-deps + +4.0.11 +- meta: added nyc for coverage file gen +- meta: added husky npm test pre-commit hook +- meta: refactored to async/await instead of Promises where possible +- meta: refactored examples to reduce boilerplate and normalize output +- meta: removed example script runners from package.json +- examples: removed CLI scripts in favour of a dedicated bfx-cli module +- examples: renamed 'orders' to 'list-open-orders' +- examples: positions now always includes P/L +- WSv2: support '*' filter value to match all +- WSv2: added sequencingEnabled() method +- WSv2: added usesAgent() method +- WSv2: added getURL() method +- WSv2: fix in cancelOrders() to prevent context clobber for this.cancelOrder() call +- WSv2: default connection url now exposed on WSv2.url +- WSv2: removed unused prec/len params from unsubscribeOrderBook() +- WSv2: removed unused rawMsg param from _handleEventMessage() +- WSv2: fix getDataChannelId() not filtering by channel type +- WS2Manager: reconnect() and close() now return promises +- WS2Manager: added getAuthArgs() +- WS2Manager: added missing tests + +4.0.10 +- fix: refactor tests so they can run alongside all other HF/API library tests + +4.0.9 +- WS2Manager: respect internal auth arg settings + +4.0.8 +- WSv2: fix on trade message handler by prioritising channel data symbol over pair symbol + +4.0.7 +- WSv2: refactor to use async/await style where possible +- WSv2: reconnect() now always resolves on completion + +4.0.6 +- WSv2: fix internal flag persistence #521 + +4.0.5 +- WSv2: add auth arg management (pre-set dms and calc) +- WSv2: add updateAthArgs() +- WSv2: add getAuthArgs() +- WS2Manager: fix auth args handling with new methods +- WS2Manager: rename setAuthCredentials -> setAPICredentials + +4.0.4 +- Orderbook: generate and update book using lossless string format + in order to prevent floating point precision checksum errors: + https://github.com/bitfinexcom/bitfinex-api-node/issues/511 + +4.0.3 +- WS2Manager: add setAuthArgs method + +4.0.2 +- WS2Manager: add reconnect method + +4.0.1 +- WSv2: fix fte and ftu event routing + +4.0.0 +- include bfx-api-node-rest takeNotification hotfix. + This pull request changed the schema of data returned from the v2 + REST functions. See pr https://github.com/bitfinexcom/bfx-api-node-rest/pull/42 + for more info + +3.0.2 +- WSv2: affCode support + +3.0.1 +- docs: update + +3.0.0 +- Updates function rest2.withdraw to v2 functionality +- Updates function rest2.transfer to v2 functionality +- adds function rest2.getDepositAddress +- adds function rest2.submitAutoFunding +- adds function rest2.closeFunding +- adds function rest2.cancelFundingOffer +- adds function rest2.submitFundingOffer +- adds function rest2.claimPosition +- adds function rest2.cancelOrder +- adds function rest2.updateOrder +- adds function rest2.submitOrder + +2.0.10 +- WSv2: ignore notification auth seq numbers (no longer provided by API) + +2.0.9 + +- WS2Manager: add managedUnsubscribe() +- WS2Manager: add close() +- WS2Manager: add getAuthenticatedSocket() +- WSv2: add suppport for liquidations feed (status methods) +- WSv2: add reconnect throttler in case of connection reset w/ many open sockets + +2.0.8 + +- Bump dependency versions + +2.0.7 + +- WSv2: increase data chan limit to 30 (732499b) + +2.0.6 + +- WSv2: decrease data chan limit to 25 (6816992) +- add close-positions script (face1fc) +- add symbol-details script (708849e) +- add currencies script (cff1647) +- add funding info fetch example (337f202) +- standard --fix (5e6f786, fb5e319, b56b157) +- fix lastMidPrice in example courtesy of MowgliB (004f904) + +2.0.5 + +- WSv2: improve reconnect functionality courtesy of cwolters (950105d) +- WSv2: add funding info example (b597c4d) +- WSv2: add order creation w/ TIF example (f25df58) +- bump dep versions (5e4d439, d72d56f) +- mv babel deps to dev-deps (a576c57) + +2.0.4 + +- add symbols back into ws2 ticker messages [models updated] (1f4a7eb) + +2.0.3 + +- add browser builds (e651496) +- add errors in case of missing chan sub filters (4607154) +- remove symbols from ws2 ticker messages (06b0e13) + +2.0.2 + +- improve logging (ceddd87, 404bd7a) +- export WS2Manager class (afcdefe) + +2.0.1 + +- extract most logic into external libraries (13edff8) +- add support for all currencies in funding offer/loan/credit history (e39f360) +- add automatic re-subscribe on reconnect (e4f65ec) +- add withAllSockets method to manager (90c7fd5) +- split trades listeners between public and private trades (3a428a6) +- allow multiple nonce consumers (2a51dcd) +- REST API v2: add currencies method (122648a) +- OrderBook: add funding support (d8572a6) +- LedgerEntry: add wallet (e5b91c5) +- and more! + +2.0.0 + +- added CLI commands (971e8bf) +- added TradingTicker model (1099273) +- added model class transform support to RESTv2 (1099273) +- added ability to unserialize objects in Model.unserialize() (b23a576) +- added ledgers & movements examples (176d5a9) +- filled in FundingInfo model (268ecc9) +- updated MarginInfo model indices (268ecc9) +- increased max WSv2 listener limit to 1k (5ade818) +- REST API v2: fix calc balances API path (5e2f834) +- WS API v2: added notifyUI helper to generate broadcasts (22cb5bc) +- WS API v2: added support for DMS flag in auth (11e57b1) +- WS API v2: added socket manager for auto multiplexing (f693bb9) +- WS API v2: fixed error notification seq # tracking (1b1c1f3) +- WS API v2: fixed trades event name resolution w/ seq numbers (46af211) +- REST API v2: added ability to auth via token (07f8756) +- REST API v2: added ability to fetch order history for all symbols (57f8c7b) +- REST API v2: added ability to fetch account trades for all symbols (14b13c1) +- REST API v2: added user info endpoint & associated model (36c0079) +- OrderBook: fixed unserialization for raw books (01b5ce4) +- OrderBook: removal of unknown entries no longer raises an error (7bd5bc2) +- OrderBook: array sort is maintained on update (520a9a0) +- OrderBook: converts exp notation numbers to fixed for checksum (2c8487c) +- and more! + +2.0.0-beta.1 + +- refactored general model handling (broke out field indexes) (c616696) +- REST API v1: add support for close position endpoint (14db6fe) +- REST API v2: added query param support to the candles() handler (be779c3) +- REST API v2: added platform status endpoint (5e3fe56) +- WS API v2: clean up channel subscriptions on open/close (7c17b96, 92ce89d) +- WS API v2: now passes update packet & order to Order model events (c616696) +- WS API v2: added support for new order flags (79c4a40, 3406ac3) +- WS API v2: added support for filtering by id to order event listeners (be779c3) +- WS API v2: added support for managed order book checksum verification (cab9635) +- WS API v2: added support for atomic order updates (36d10c4) +- OrderBook: added arrayOBMidPrice helper (f0e3074) +- OrderBook: added checksum helpers (cab9635) +- refactored general model handling (broke out field indexes) (c616696) +- and many small fixes & tweaks + +2.0.0-beta + +- WS API v2: added optional auto-reconnect +- WS API v2: added optional packet watchdog +- WS API v2: added optional order packet buffering via multi-op endpoint +- WS API v2: added optional order book & candle managment/persistence +- WS API v2: added optional seq number verification/audit +- WS API v2: added many extra callback/listener funcs (i.e. onMaintenanceStart) +- WS API v2: added ability to mass-unsubscribe listeners by group ID +- WS API v2: most callback methods now support message filtering +- WS API v2: replaced transform logic w/ model classes (i.e. Order) +- WS API v2: many methods now return promises, such as submitOrder() +- REST API v2: transform method updated to match WSv2 class +- REST API v1: minor refactor, methods unchanged +- REST API v2: minor refactor, methods unchanged +- WS API v1: minor refactor, methods unchanged +- BFX constructor exposes & caches clients on `.rest()` and `.ws()` methods +- Updated ws2 examples +- Added model classes (OrderBook, Order, Trade, etc) + +1.2.1 + +- REST API v2: use /candles/ endpoint for candles data +- WS API v2: Candles event provides key +- Improve error message for nonce errors +- Examples: added example for WS2 orders + +1.2.0 + +- REST API v1: Added support for `/orders/hist` endpoint +- REST API v2: Added support for `auth/r/trades/{symbol}/hist` endpoint +- WS API v2: Candles supports now `key` to identify subscription +- REST API v1: Fix `claim_position` argument handling diff --git a/DOCS.md b/DOCS.md deleted file mode 100644 index 19a62594..00000000 --- a/DOCS.md +++ /dev/null @@ -1,254 +0,0 @@ - -## BitfinexWS -**Kind**: global class - -* [BitfinexWS](#BitfinexWS) - * [new BitfinexWS(APIKey, APISecret)](#new_BitfinexWS_new) - * _instance_ - * [.subscribeOrderBook([pair], [precision], [length])](#BitfinexWS+subscribeOrderBook) - * [.subscribeTrades([pair])](#BitfinexWS+subscribeTrades) - * [.subscribeTicker([pair])](#BitfinexWS+subscribeTicker) - * [.unsubscribe(chanId)](#BitfinexWS+unsubscribe) - * [.auth()](#BitfinexWS+auth) - * ["message"](#BitfinexWS+event_message) - * ["open"](#BitfinexWS+event_open) - * ["error"](#BitfinexWS+event_error) - * ["close"](#BitfinexWS+event_close) - * ["subscribed"](#BitfinexWS+event_subscribed) - * ["auth"](#BitfinexWS+event_auth) - * ["ps"](#BitfinexWS+event_ps) - * ["pn"](#BitfinexWS+event_pn) - * ["pu"](#BitfinexWS+event_pu) - * ["pc"](#BitfinexWS+event_pc) - * ["ws"](#BitfinexWS+event_ws) - * ["ws"](#BitfinexWS+event_ws) - * ["os"](#BitfinexWS+event_os) - * ["on"](#BitfinexWS+event_on) - * ["ou"](#BitfinexWS+event_ou) - * ["oc"](#BitfinexWS+event_oc) - * ["te"](#BitfinexWS+event_te) - * ["tu"](#BitfinexWS+event_tu) - * ["ticker"](#BitfinexWS+event_ticker) - * ["trade"](#BitfinexWS+event_trade) - * ["trade"](#BitfinexWS+event_trade) - * ["orderbook"](#BitfinexWS+event_orderbook) - * _static_ - * [.WebSocketURI](#BitfinexWS.WebSocketURI) : String - - -### new BitfinexWS(APIKey, APISecret) -Handles communitaction with Bitfinex WebSocket API. - - -| Param | Type | -| --- | --- | -| APIKey | sting | -| APISecret | string | - - -### bitfinexWS.subscribeOrderBook([pair], [precision], [length]) -Subscribe to Order book updates. Snapshot will be sended as multiple updates. -Event will be emited as `PAIRNAME_book`. - -**Kind**: instance method of [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#order-books - -| Param | Type | Description | -| --- | --- | --- | -| [pair] | string | BTCUSD, LTCUSD or LTCBTC. Default BTCUSD | -| [precision] | string | Level of price aggregation (P0, P1, P2, P3). The default is P0. | -| [length] | string | Number of price points. 25 (default) or 100. | - - -### bitfinexWS.subscribeTrades([pair]) -Subscribe to trades. Snapshot will be sended as multiple updates. -Event will be emited as `PAIRNAME_trades`. - -**Kind**: instance method of [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#trades75 - -| Param | Type | Description | -| --- | --- | --- | -| [pair] | string | BTCUSD, LTCUSD or LTCBTC. Default BTCUSD | - - -### bitfinexWS.subscribeTicker([pair]) -Subscribe to ticker updates. The ticker is a high level overview of the state -of the market. It shows you the current best bid and ask, as well as the last -trade price. - -Event will be emited as `PAIRNAME_ticker`. - -**Kind**: instance method of [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#ticker76 - -| Param | Type | Description | -| --- | --- | --- | -| [pair] | string | BTCUSD, LTCUSD or LTCBTC. Default BTCUSD | - - -### bitfinexWS.unsubscribe(chanId) -Unsubscribe to a channel. - -**Kind**: instance method of [BitfinexWS](#BitfinexWS) - -| Param | Type | Description | -| --- | --- | --- | -| chanId | number | ID of the channel received on `subscribed` event. | - - -### bitfinexWS.auth() -Autenticate the user. Will receive executed traded updates. - -**Kind**: instance method of [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#wallet-updates - -### "message" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "open" -WebSocket connection is open. Ready to send. - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "error" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "close" -WebSocket connection is closed. - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "subscribed" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) -**Properties** - -| Name | Type | Description | -| --- | --- | --- | -| channel | string | Channel type | -| pair | string | Currency pair. | -| chanId | number | Channel ID sended by Bitfinex | - - -### "auth" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "ps" -position snapshot - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "pn" -new position - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "pu" -position update - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "pc" -position close - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "ws" -wallet snapshot - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "ws" -wallet snapshot - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "os" -order snapshot - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "on" -new order - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "ou" -order update - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "oc" -order cancel - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "te" -trade executed - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "tu" -trade execution update - -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) - -### "ticker" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) -**Properties** - -| Name | Type | -| --- | --- | -| bid | number | -| bidSize | number | -| ask | number | -| askSize | number | -| dailyChange | number | -| dailyChangePerc | number | -| lastPrice | number | -| volume | number | -| high | number | -| low | number | - - -### "trade" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#trades75 -**Properties** - -| Name | Type | -| --- | --- | -| seq | string | -| timestamp | number | -| price | number | -| amount | number | - - -### "trade" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#trades75 -**Properties** - -| Name | Type | -| --- | --- | -| seq | string | -| id | number | -| timestamp | number | -| price | number | -| amount | number | - - -### "orderbook" -**Kind**: event emitted by [BitfinexWS](#BitfinexWS) -**See**: http://docs.bitfinex.com/#order-books -**Properties** - -| Name | Type | -| --- | --- | -| price | string | -| count | number | -| amount | number | - - -### BitfinexWS.WebSocketURI : String -**Kind**: static constant of [BitfinexWS](#BitfinexWS) diff --git a/README.md b/README.md index c890c653..8ba72aac 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,220 @@ -# Bitfinex Trading API for Node.JS. Bitcoin, Ether and Litecoin trading -========= +# Bitfinex WSv2 Trading API for Node.JS - Bitcoin, Ethereum, Ripple and more [![Build Status](https://travis-ci.org/bitfinexcom/bitfinex-api-node.svg?branch=master)](https://travis-ci.org/bitfinexcom/bitfinex-api-node) -A Node.JS reference implementation of the Bitfinex API. See the full docs at +A Node.JS reference implementation of the Bitfinex API + +## Features * Official implementation -* REST API -* WebSockets API +* REST v2 API +* WebSockets v2 API +* WebSockets v1 API ## Installation + ```bash - npm install bitfinex-api-node + npm i --save bitfinex-api-node ``` -## Usage +### Quickstart + ```js -var bitfinexApiNode = require('bitfinex-api-node') - bitfinexWebsocket = bitfinexApiNode.websocket, - bitfinexRest = bitfinexApiNode.rest; +const { WSv2 } = require('bitfinex-api-node') +const ws = new WSv2({ transform: true }) -console.log('bitfinexApiNode', bitfinexApiNode, 'bitfinexWebsocket', - bitfinexWebsocket, 'bitfinexRest', bitfinexRest); +// do something with ws client ``` -## Tests +### Docs -```bash -npm test +Refer to the [`docs/`](https://cdn.statically.io/gh/bitfinexcom/bitfinex-api-node/master/docs/index.html) +folder for JSDoc-generated HTML documentation, and the [`examples/`](/examples) +folder for executable examples covering common use cases. + +Official API documentation at [https://docs.bitfinex.com/v2/reference](https://docs.bitfinex.com/v2/reference) + +### Examples + +Sending an order & tracking status: + +```js +const ws = bfx.ws() + +ws.on('error', (err) => console.log(err)) +ws.on('open', ws.auth.bind(ws)) + +ws.once('auth', () => { + const o = new Order({ + cid: Date.now(), + symbol: 'tETHUSD', + amount: 0.1, + type: Order.type.MARKET + }, ws) + + // Enable automatic updates + o.registerListeners() + + o.on('update', () => { + console.log(`order updated: ${o.serialize()}`) + }) + + o.on('close', () => { + console.log(`order closed: ${o.status}`) + ws.close() + }) + + o.submit().then(() => { + console.log(`submitted order ${o.id}`) + }).catch((err) => { + console.error(err) + ws.close() + }) +}) + +ws.open() ``` -## Contributing +Cancel all open orders +```js +const ws = bfx.ws(2) + +ws.on('error', (err) => console.log(err)) +ws.on('open', ws.auth.bind(ws)) + +ws.onOrderSnapshot({}, (orders) => { + if (orders.length === 0) { + console.log('no open orders') + return + } + + console.log(`recv ${orders.length} open orders`) + + ws.cancelOrders(orders).then(() => { + console.log('cancelled orders') + }) +}) + +ws.open() +``` + +Subscribe to trades by pair + +```js +const ws = bfx.ws(2) + +ws.on('error', (err) => console.log(err)) +ws.on('open', () => { + ws.subscribeTrades('BTCUSD') +}) + +ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => { + console.log(`trades: ${JSON.stringify(trades)}`) +}) +ws.onTradeEntry({ symbol: 'tBTCUSD' }, (trades) => { + console.log(`te: ${JSON.stringify(trades)}`) +}) + +ws.open() ``` -We are following the [standard JavaScript Style Guide](https://github.com/feross/standard). -Add unit tests for any new or changed functionality. Lint and test your code. + +## Version 2.0.0 Breaking changes + +### constructor takes only an options object now, including the API keys + +Old: + +```js +new BFX(API_KEY, API_SECRET, { version: 2 }) ``` -## Release History +since 2.0.0: + +```js +new BFX({ apiKey: '', apiSecret: '' }) ``` -* 0.0.1 Initial release -* 0.3.0 Added support for v2 API + +### `trade` and `orderbook` snapshots are emitted as nested lists + +To make dealing with snapshots better predictable, snapshots are emitted as an array. + +### normalized orderbooks for R0 + +Lists of raw orderbooks (`R0`) are ordered in the same order as `P0`, `P1`, +`P2`, `P3` + +## Testing + +```bash +npm test ``` + +## FAQ + +### Order Creation Limits + +The base limit per-user is 1,000 orders per 5 minute interval, and is shared +between all account API connections. It increases proportionally to your trade +volume based on the following formula: + +`1000 + (TOTAL_PAIRS_PLATFORM * 60 * 5) / (250000000 / USER_VOL_LAST_30d)` + +Where `TOTAL_PAIRS_PLATFORM` is the number of pairs shared between +Ethfinex/Bitfinex (currently ~101) and `USER_VOL_LAST_30d` is in USD. + +### 'on' Packet Guarantees + +No; if your order fills immediately, the first packet referencing the order +will be an `oc` signaling the order has closed. If the order fills partially +immediately after creation, an `on` packet will arrive with a status of +`PARTIALLY FILLED...` + +For example, if you submit a `LIMIT` buy for 0.2 BTC and it is added to the +order book, an `on` packet will arrive via ws2. After a partial fill of 0.1 +BTC, an `ou` packet will arrive, followed by a final `oc` after the remaining +0.1 BTC fills. + +On the other hand, if the order fills immediately for 0.2 BTC, you will only +receive an `oc` packet. + +### Nonce too small + +I make multiple parallel request and I receive an error that the nonce is too +small. What does it mean? + +Nonces are used to guard against replay attacks. When multiple HTTP requests +arrive at the API with the wrong nonce, e.g. because of an async timing issue, +the API will reject the request. + +If you need to go parallel, you have to use multiple API keys right now. + +### `te` vs `tu` Messages + +A `te` packet is sent first to the client immediately after a trade has been +matched & executed, followed by a `tu` message once it has completed processing. +During times of high load, the `tu` message may be noticably delayed, and as +such only the `te` message should be used for a realtime feed. + +### Sequencing + +If you enable sequencing on v2 of the WS API, each incoming packet will have a +public sequence number at the end, along with an auth sequence number in the +case of channel `0` packets. The public seq numbers increment on each packet, +and the auth seq numbers increment on each authenticated action (new orders, +etc). These values allow you to verify that no packets have been missed/dropped, +since they always increase monotonically. + +### Differences Between R* and P* Order Books + +Order books with precision `R0` are considered 'raw' and contain entries for +each order submitted to the book, whereas `P*` books contain entries for each +price level (which aggregate orders). + +### Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/docs/WS2Manager.html b/docs/WS2Manager.html new file mode 100644 index 00000000..55f21a1a --- /dev/null +++ b/docs/WS2Manager.html @@ -0,0 +1,5124 @@ + + + + + + WS2Manager - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

WS2Manager

+ + + + + + + +
+ +
+ +

+ + WS2Manager + +

+ +

Provides a wrapper around the WSv2 class, opening new sockets when a +subscription would push a single socket over the data channel limit.

+

For more complex operations, grab a socket reference with getSocket() or +getFreeDataSocket(), or create a new WSv2 instance manually

+ + +
+ +
+ +
+ + + + +

Constructor

+ + +

new WS2Manager(socketArgs, authArgsopt)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const rest = new RESTv2({ transform: true })
+const details = await rest.symbolDetails()
+const symbols = details.map(d => `t${d.pair.toUpperCase()}`)
+const timeFrames = ['1m', '5m', '30m', '1h', '6h']
+const keys = _flatten(symbols.map(s => {
+  return timeFrames.map(tf => `trade:${tf}:${s}`)
+}))
+
+const m = new Manager()
+
+m.on('error', (err) => {
+  debug('error: %s', err)
+})
+
+m.once('open', () => {
+  debug('open')
+
+  keys.forEach(key => {
+    m.subscribeCandles(key)
+    m.onCandle({ key }, (candles) => {
+      debug('recv %d candles on channel %s', candles.length, key)
+    })
+  })
+
+  symbols.forEach(symbol => {
+    m.subscribeTrades(symbol)
+    m.onTrades({ symbol }, (trades) => {
+      debug('recv %d trades on channel %s', trades.length, symbol)
+    })
+  })
+
+  symbols.forEach(symbol => {
+    m.subscribeTicker(symbol)
+    m.onTicker({ symbol }, (ticker) => {
+      debug('recv ticker on channel %s: %j', symbol, ticker)
+    })
+  })
+
+  symbols.forEach(symbol => {
+    m.subscribeOrderBook(symbol)
+    m.onOrderBook({ symbol }, (update) => {
+      debug('recv book update on channel %s: %j', symbol, update)
+    })
+  })
+
+  setInterval(() => {
+    debug('num keys: %d', keys.length)
+    debug('num sockets: %d', m.getNumSockets())
+    debug('socket info: %j', m.getSocketInfo())
+  }, 5000)
+})
+
+m.openSocket()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
socketArgs + + +object + + + + + + + + + + +

passed to WSv2 constructors

authArgs + + +object + + + + + + + <optional>
+ + + + + +

cached for all internal socket auth() calls

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
calc + + +number + + + + + + + <optional>
+ + + + + +

default 0

dms + + +number + + + + + + + <optional>
+ + + + + +

default 0

+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +

Methods

+ + + + + + +

auth(args)

+ + + + + + +
+ +
Description:
+
  • Authenticates all existing & future sockets with the provided credentials. +Does nothing if an apiKey/apiSecret pair are already known.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args + + +object + + + + +

arguments

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
apiKey + + +string + + + + + + + + + + +

saved if not already provided

apiSecret + + +string + + + + + + + + + + +

saved if not already provided

calc + + +number + + + + + + + <optional>
+ + + + + +

default 0

dms + + +number + + + + + + + <optional>
+ + + + + +

dead man switch, active 4

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) close() → {Promise}

+ + + + + + +
+ +
Description:
+
  • Closes all open sockets

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

getAuthArgs() → {object}

+ + + + + + +
+ +
Description:
+
  • Retrieve internal authentication arguments

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

args

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getAuthenticatedSocket() → {object}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

state

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getFreeDataSocket() → {object}

+ + + + + + +
+ +
Description:
+
  • Returns the first socket that has less active/pending channels than the +DATA_CHANNEL_LIMIT

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

state - undefined if none found

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getNumSockets() → {number}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

n

+
+ + + +
+
+ Type +
+
+ +number + + + +
+
+ + + + + + + + + + +

getSocket(i) → {object}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
i + + +number + + + + +

index into pool

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

state

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getSocketInfo() → {Array.<object>}

+ + + + + + +
+ +
Description:
+
  • Returns an object which can be logged to inspect the socket pool

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

socketInfo

+
+ + + +
+
+ Type +
+
+ +Array.<object> + + + +
+
+ + + + + + + + + + +

getSocketWithChannel(chanId) → {object}

+ + + + + + +
+ +
Description:
+
  • NOTE: Cannot filter against pending subscriptions, due to unknown chanId

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanId + + +number + + + + +

channel ID

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

wsState - undefined if not found

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getSocketWithDataChannel(type, filter) → {object}

+ + + + + + +
+ +
Description:
+
  • Returns the first socket that is subscribed/pending sub to the specified +channel.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
type + + +string + + + + +

i.e. 'book'

filter + + +object + + + + +

i.e. { symbol: 'tBTCUSD', prec: 'R0' }

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

wsState - undefined if not found

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getSocketWithSubRef(channel, identifier) → {object}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel type

identifier + + +string + + + + +

unique channel identifier

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

wsState - undefined if not found

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

managedUnsubscribe(channel, identifier)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel type

identifier + + +string + + + + +

unique channel identifier

+ + + + + + + + + + + + + + + + + + + + + + + + +

onCandle(opts, cb)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
key + + +string + + + + + + + + + + +

candle set key, i.e. trade:30m:tBTCUSD

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + +
Throws:
+ + + +
+ +

an error if no data socket is available

+ +
+ + + + + + + + + + + + + +

onOrderBook(opts, cb)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

order book symbol

prec + + +string + + + + + + + <optional>
+ + + + + +

precision, i.e. 'R0', default 'P0'

len + + +string + + + + + + + <optional>
+ + + + + +

length, default '25'

freq + + +string + + + + + + + <optional>
+ + + + + +

default 'F0'

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + +
Throws:
+ + + +
+ +

an error if no data socket is available

+ +
+ + + + + + + + + + + + + +

onTicker(opts, cb)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol for ticker

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + +
Throws:
+ + + +
+ +

an error if no data socket is available

+ +
+ + + + + + + + + + + + + +

onTrades(opts, cb)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol for trades

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + +
Throws:
+ + + +
+ +

an error if no data socket is available

+ +
+ + + + + + + + + + + + + +

openSocket() → {object}

+ + + + + + +
+ +
Description:
+
  • Creates a new socket/state instance and adds it to the internal pool. Binds +event listeners to forward via our own event emitter, and to manage pending +subs/unsubs.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

state

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

(async) reconnect() → {Promise}

+ + + + + + +
+ +
Description:
+
  • Reconnects all open sockets

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

setAuthArgs(args)

+ + + + + + +
+ +
Description:
+
  • Update authentication arguments on all sockets

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args + + +object + + + + +

arguments

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
calc + + +number + + + + + + + <optional>
+ + + + + +

calc value

dms + + +number + + + + + + + <optional>
+ + + + + +

active 4

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +

subscribe(type, ident, filter)

+ + + + + + +
+ +
Description:
+
  • Subscribes a free data socket if available to the specified channel, or +opens a new socket & subs if needed.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
type + + +string + + + + +

i.e. 'book'

ident + + +string + + + + +

i.e. 'tBTCUSD'

filter + + +object + + + + +

i.e. { symbol: 'tBTCUSD', prec: 'R0' }

+ + + + + + + + + + + + + + + + + + + + + + + + +

subscribeCandles(key)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

candle channel key

+ + + + + + + + + + + + + + + + + + + + + + + + +

subscribeOrderBook(symbol, precopt, lenopt, freqopt)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
symbol + + +string + + + + + + + + + + + + +

symbol for order book

prec + + +string + + + + + + + <optional>
+ + + + + +
+ + P0 + +

precision, i.e. 'R0', default 'P0'

len + + +string + + + + + + + <optional>
+ + + + + +
+ + 25 + +

length, default '25'

freq + + +string + + + + + + + <optional>
+ + + + + +
+ + F0 + +

default 'F0'

+ + + + + + + + + + + + + + + + + + + + + + + + +

subscribeTicker(symbol)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol for ticker

+ + + + + + + + + + + + + + + + + + + + + + + + +

subscribeTrades(symbol)

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol for trades

+ + + + + + + + + + + + + + + + + + + + + + + + +

unsubscribe(chanId)

+ + + + + + +
+ +
Description:
+
  • Unsubscribes the first socket w/ the specified channel. Does nothing if no +such socket is found.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanId + + +number + + + + +

channel ID

+ + + + + + + + + + + + + + + + + + + + + + + + +

withAllSockets(cb)

+ + + + + + +
+ +
Description:
+
  • Calls the provided cb with all internal socket instances

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

(static) getDataChannelCount(s) → {number}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
s + + +object + + + + +

socket state

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

count - # of subscribed/pending data channels

+
+ + + +
+
+ Type +
+
+ +number + + + +
+
+ + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/docs/WSv2.html b/docs/WSv2.html new file mode 100644 index 00000000..50697a74 --- /dev/null +++ b/docs/WSv2.html @@ -0,0 +1,40362 @@ + + + + + + WSv2 - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

WSv2

+ + + + + + + +
+ +
+ +

+ + WSv2 + +

+ + +
+ +
+ +
+ + + + + +

new WSv2(optsopt)

+ + + + + + +
+ +
Description:
+
  • Instantiate a new ws2 transport. Does not auto-open

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+  ws.on('open', async () => {
+    ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+      console.log('recv trades: %j', trades)
+    })
+
+    await ws.subscribeTrades('tBTCUSD')
+  })
+
+  await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
opts + + +object + + + + + + + <optional>
+ + + + + +

instance options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
affCode + + +string + + + + + + + <optional>
+ + + + + +

affiliate code to be applied to all orders

apiKey + + +string + + + + + + + <optional>
+ + + + + +

API key

apiSecret + + +string + + + + + + + <optional>
+ + + + + +

API secret

url + + +string + + + + + + + <optional>
+ + + + + +

ws connection url, defaults to WSv2#url

orderOpBufferDelay + + +number + + + + + + + <optional>
+ + + + + +

multi-order op batching timeout

transform + + +boolean + + + + + + + <optional>
+ + + + + +

if true, packets are converted to models

agent + + +object + + + + + + + <optional>
+ + + + + +

optional node agent for ws connection (proxy)

manageOrderBooks + + +boolean + + + + + + + <optional>
+ + + + + +

enable local OB persistence

manageCandles + + +boolean + + + + + + + <optional>
+ + + + + +

enable local candle persistence

seqAudit + + +boolean + + + + + + + <optional>
+ + + + + +

enable sequence numbers & verification

autoReconnect + + +boolean + + + + + + + <optional>
+ + + + + +

if true, we will reconnect on close

reconnectDelay + + +number + + + + + + + <optional>
+ + + + + +

optional, defaults to 1000 (ms)

reconnectThrottler + + +PromiseThrottle + + + + + + + <optional>
+ + + + + +

optional pt to limit reconnect freq

packetWDDelay + + +number + + + + + + + <optional>
+ + + + + +

watch-dog forced reconnection delay

+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +

Members

+ + + +

(constant) WSv2.flags

+ + + + + +
+ +
Description:
+
  • Map of flag names and numeric values for usage with the API

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
DEC_S + + +number + + + + +

flag to request all decimals as strings

TIME_S + + +number + + + + +

flag to request all timestamps as strings

TIMESTAMP + + +number + + + + +

flag to request timestamp in milliseconds

SEQ_ALL + + +number + + + + +

flag to enable sequence numbers & verification

CHECKSUM + + +number + + + + +

flag to enable order book checksums

+ + + + + + +
+

Map of flag names and numeric values for usage with the API

+
+ + + + + + + + + + +

(constant) WSv2.info

+ + + + + +
+ +
Description:
+
  • Map of info event names and their respective codes for usage with the API

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
SERVER_RESTART + + +number + + + + +

server restart event

MAINTENANCE_START + + +number + + + + +

maintenance start event

MAINTENANCE_END + + +number + + + + +

maintenance end event

+ + + + + + +
+

Map of info event names and their respective codes for usage with the API

+
+ + + + + + + + + + +

(constant) WSv2.url :string

+ + + + + +
+ +
Description:
+
  • Default connection URL

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Default Value:
+
    +
  • wss://api.bitfinex.com/ws/2
  • +
+ + + + + + + +
+ + + + + +
+

Default connection URL

+
+ + + +
Type:
+
    +
  • + +string + + + +
  • +
+ + + + + + + + + + +

Methods

+ + + + + + +

(async) auth(calcnullable, dmsnullable) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Generates & sends an authentication packet to the server; if already +authenticated, rejects with an error, resolves on success.

    +

    If a DMS flag of 4 is provided, all open orders are cancelled when the +connection terminates.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
calc + + +number + + + + + + + + + <nullable>
+ + + +

optional, default is 0

dms + + +number + + + + + + + + + <nullable>
+ + + +

optional dead man switch flag, active 4

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) cancelOrder(order) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Cancels an order by ID and resolves the returned promise once the cancel is +confirmed. Emits an error if not authenticated. The ID can be passed as a +number, or taken from an order array/object.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
order + + +object +| + +Array +| + +number + + + + +

order model, array, or ID to be cancelled

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) cancelOrders(params) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Cancels multiple orders, returns a promise that resolves once all +operations are confirmed.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params + + +object +| + +Array.<object> +| + +Array.<Array> +| + +Array.<number> + + + + +

parameters or array of order models, arrays

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
ids + + +Array.<object> +| + +Array.<Array> +| + +Array.<number> + + + + +

array of order models, arrays +or IDs to be cancelled

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) close(code, reason) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Closes the active connection. If there is none, rejects with a promise. +Resolves on success

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
code + + +number + + + + +

passed to ws

reason + + +string + + + + +

passed to ws

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) enableFlag(flag) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Enables a configuration flag.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+
    +
  • WSv2#flags
  • +
+
+ + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+ws.on('open', async () => {
+  await ws.enableFlag(WSv2.flags.CHECKSUM)
+  console.log('ob checkums enabled')
+})
+
+await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
flag + + +number + + + + +

flag to update, as numeric value

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) enableSequencing(args) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Configures the seq flag to enable sequencing (packet number) for this +connection. When enabled, the seq number will be the last value of +channel packet arrays.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+
    +
  • WSv2#flags
  • +
+
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args + + +object + + + + +

params

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
audit + + +boolean + + + + + + + <optional>
+ + + + + +

if true, an error is emitted on invalid seq

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

getAuthArgs() → {object}

+ + + + + + +
+ +
Description:
+
  • Fetch the current default auth parameters

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

authArgs

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getCandles(key) → {Array}

+ + + + + + +
+ +
Description:
+
  • Fetch a reference to the full set of synced candles for the specified key. +Set managedCandles: true in the constructor to use.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2({ managedCandles: true })
+
+  ws.on('open', async () => {
+      ws.onCandles({ key: 'trade:1m:tBTCUSD' }, () => {
+        const candles = ws.getCandles('trade:1m:tBTCUSD')
+
+        if (!candles) return
+
+        console.log('%d candles in dataset', candles.length)
+      })
+
+      ws.subscribeCandles({ key: 'trade:1m:tBTCUSD' })
+  })
+
+  await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

key for candle set

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

candles - empty array if none exist

+
+ + + +
+
+ Type +
+
+ +Array + + + +
+
+ + + + + + + + + + +

getChannelData(opts) → {object}

+ + + + + + +
+ +
Description:
+
  • Fetch a channel definition

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
chanId + + +number + + + + + + + + + + +

channel ID

channel + + +string + + + + + + + + + + +

channel name

symbol + + +string + + + + + + + <optional>
+ + + + + +

match by symbol

key + + +string + + + + + + + <optional>
+ + + + + +

match by key (for candle channels)

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

chanData - null if not found

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getDataChannelCount() → {number}

+ + + + + + +
+ +
Description:
+
  • Get the total number of data channels this instance is currently +subscribed too.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

count

+
+ + + +
+
+ Type +
+
+ +number + + + +
+
+ + + + + + + + + + +

getDataChannelId(type, filter) → {number}

+ + + + + + +
+ +
Description:
+
  • Fetch the ID of a channel matched by type and channel data filter

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
type + + +string + + + + +

channel type

filter + + +object + + + + +

to be matched against channel data

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

channelID

+
+ + + +
+
+ Type +
+
+ +number + + + +
+
+ + + + + + + + + + +

getLosslessOB(symbol) → {OrderBook}

+ + + + + + +
+ +
Description:
+
  • Returns an up-to-date lossless copy of the order book for the specified symbol, or +null if no OB is managed for that symbol. All amounts and prices are in original +string format.

    +

    Set manageOrderBooks: true in the constructor to use.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol for order book

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

ob - null if not found

+
+ + + +
+
+ Type +
+
+ +OrderBook + + + +
+
+ + + + + + + + + + +

getOB(symbol) → {OrderBook}

+ + + + + + +
+ +
Description:
+
  • Returns an up-to-date copy of the order book for the specified symbol, or +null if no OB is managed for that symbol.

    +

    Set managedOrderBooks: true in the constructor to use.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2({ managedOrderBooks: true })
+
+  ws.on('open', async () => {
+      ws.onOrderBook({ symbol: 'tBTCUSD' }, () => {
+        const book = ws.getOB('tBTCUSD')
+
+        if (!book) return
+
+        const spread = book.midPrice()
+        console.log('spread for tBTCUSD: %f', spread)
+      })
+
+      ws.subscribeOrderBook({ symbol: 'tBTCUSD' })
+  })
+
+  await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol for order book

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

ob - null if not found

+
+ + + +
+
+ Type +
+
+ +OrderBook + + + +
+
+ + + + + + + + + + +

getURL() → {string}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

url

+
+ + + +
+
+ Type +
+
+ +string + + + +
+
+ + + + + + + + + + +

hasChannel(chanId) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Check if the instance is subscribed to the specified channel ID

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanId + + +number + + + + +

ID of channel to query

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

isSubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

hasDataChannel(type, filter) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Check if the instance is subscribed to a data channel matching the +specified type and filter.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
type + + +string + + + + +

channel type

filter + + +object + + + + +

to be matched against channel data

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

hasChannel

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

hasSubscriptionRef(channel, identifier) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Check if a channel/identifier pair has been subscribed too

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel type

identifier + + +string + + + + +

unique identifier for the reference

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

hasRef

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isAuthenticated() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

authenticated

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isFlagEnabled(flag) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Checks local state, relies on successful server config responses

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+
    +
  • enableFlag
  • +
+
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
flag + + +number + + + + +

flag to check for

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

enabled

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isOpen() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

open

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isReconnecting() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

reconnecting

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

managedSubscribe(channel, identifier, payload) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribes and tracks subscriptions per channel/identifier pair. If +already subscribed to the specified pair, nothing happens.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
To Do:
+
+
    +
  • will be refactored to return promise from subscribe() call instead + of sub action taken flag
  • +
+
+ +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+ws.on('open', async () => {
+  ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+    console.log('recv trades: %j', trades)
+ })
+
+  ws.managedSubscribe('trades', 'tBTCUSD', { symbol: 'tBTCUSD' })
+})
+
+await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel name

identifier + + +string + + + + +

for uniquely identifying the ref count

payload + + +object + + + + +

merged with sub packet

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subSent

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

managedUnsubscribe(channel, identifier) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Decreases the subscription ref count for the channel/identifier pair, and +unsubscribes from the channel if it reaches 0.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel name

identifier + + +string + + + + +

for uniquely identifying the ref count

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubSent

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

notifyUI(opts)

+ + + + + + +
+ +
Description:
+
  • Sends a broadcast notification, which will be received by any active UI +websocket connections (at bitfinex.com), triggering a desktop notification.

    +

    In the future our mobile app will also support spawning native push +notifications in response to incoming ucm-notify-ui packets.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
message + + +string + + + + + + + <optional>
+ + + + + +

message to display

type + + +string + + + + + + + <optional>
+ + + + + +

notification type, 'ucm-*' for broadcasts

level + + +string + + + + + + + <optional>
+ + + + + +

'info', 'error', or 'success'

image + + +string + + + + + + + <optional>
+ + + + + +

link to an image to be shown

link + + +string + + + + + + + <optional>
+ + + + + +

URL the notification should forward too

sound + + +string + + + + + + + <optional>
+ + + + + +

URL of sound to play

+ +
+ + + + + + + + + + + + + + +
Throws:
+ + + +
+ +

an error if given no type or message, or the instance is not open +and authenticated

+ +
+ + + + + + + + + + + + + +

onAccountTradeEntry(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each personal trade 'te' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onAccountTradeUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each personal trade 'tu' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onBalanceInfoUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each balance info update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onCandle(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received candle

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
key + + +string + + + + + + + + + + +

candle set key, i.e. trade:30m:tBTCUSD

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding credit is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding credit is created

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding credit snapshot (sent on +auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding credit is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingInfoUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding info update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding loan is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding loan is created

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding loan snapshot (sent on +auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding loan is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding offer is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding offer is created

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each fundign offer snapshot (sent on +auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding offer is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingTradeEntry(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding trade 'te' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingTradeUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding trade 'tu' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onInfoMessage(code, cb)

+ + + + + + +
+ +
Description:
+
  • Registers a new callback to be called when a matching info message is +received.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
code + + +number + + + + +

from #WSv2.info

cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMaintenanceEnd(cb)

+ + + + + + +
+ +
Description:
+
  • Register a callback to be notified of a maintenance period ending

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

called on event trigger

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMaintenanceStart(cb)

+ + + + + + +
+ +
Description:
+
  • Register a callback in case of a 'maintenance started' message from the +server. This is a good time to pause server packets until maintenance ends

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

called on event trigger

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMarginInfoUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each margin info update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMessage(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a generic handler to be called with each received message

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onNotification(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each notification

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
type + + +string + + + + + + + <optional>
+ + + + + +

type to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderBook(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received candle

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

book symbol

prec + + +string + + + + + + + + + + +

book precision, i.e. 'R0'

len + + +string + + + + + + + + + + +

book length, i.e. '25'

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderBookChecksum(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received order book checksum

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

book symbol

prec + + +string + + + + + + + + + + +

book precision, i.e. 'R0'

len + + +string + + + + + + + + + + +

book length, i.e. '25'

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each order close packet

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each new order packet

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each full order snapshot (sent on auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each order update packet

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a position is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a position is opened

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each position snapshot (sent on auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a position is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onServerRestart(cb)

+ + + + + + +
+ +
Description:
+
  • Register a callback in case of a ws server restart message; Use this to +call reconnect() if needed. (code 20051)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

called on event trigger

+ + + + + + + + + + + + + + + + + + + + + + + + +

onStatus(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each message for the desired status +feed.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
key + + +string + + + + + + + + + + +

key of feed to listen on

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group ID

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onTicker(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each received ticker

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

symbol for tickers

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onTradeEntry(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each trade 'te' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onTrades(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received trade (pair or symbol +required)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onWalletSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each wallet snapshot (sent on auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onWalletUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each wallet update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) open() → {Promise}

+ + + + + + +
+ +
Description:
+
  • Opens a connection to the API server. Rejects with an error if a +connection is already open. Resolves on success.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) reconnect() → {Promise}

+ + + + + + +
+ +
Description:
+
  • Utility method to close & re-open the ws connection. Re-authenticates if +previously authenticated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - resolves on completion

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

removeListeners(cbGID)

+ + + + + + +
+ +
Description:
+
  • Remove all listeners by callback group ID

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeTrades({ symbol: 'tBTCUSD', cbGID: 42 })
+await ws.subscribeTrades({ symbol: 'tLEOUSD', cbGID: 42 })
+await ws.subscribeTrades({ symbol: 'tETHUSD', cbGID: 42 })
+
+// ...
+
+ws.removeListeners(42)
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cbGID + + +string + + + + +

callback group to remove

+ + + + + + + + + + + + + + + + + + + + + + + + +

requestCalc(prefixes)

+ + + + + + +
+ +
Description:
+
  • Request a calc operation to be performed on the specified indexes

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
prefixes + + +Array.<string> + + + + +

desired prefixes to be calculated

+ + + + + + + + + + + + + + + + + + + + + + + + +

send(msg)

+ + + + + + +
+ +
Description:
+
  • Send a packet to the WS server

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +* + + + + +

packet, gets stringified

+ + + + + + + + + + + + + + + + + + + + + + + + +

sequencingEnabled() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

sequencingEnabled

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) submitOrder(params) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Sends a new order to the server and resolves the returned promise once the +order submit is confirmed. Emits an error if not authenticated. The order +can be either an array, key/value map, or Order object instance.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
const o = new Order({
+  type: Order.type.EXCHANGE_LIMIT,
+  amount: 18,
+  price: 0.75,
+  symbol: 'tBTCUSD',
+  hidden: true
+}, ws)
+
+await ws.submitOrder(o)
+
+console.log('order confirmed! status: %s', o.status)
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params + + +object +| + +Array + + + + +

parameters or order object model or array

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
order + + +object +| + +Array + + + + +

order object model or array

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - resolves on submit notification

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) submitOrderMultiOp(opPayloads) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Sends the op payloads to the server as an 'ox_multi' command. A promise is +returned and resolves immediately if authenticated, as no confirmation is +available for this message type.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opPayloads + + +Array.<object> + + + + +

order operations

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - rejects if not authenticated

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

subscribe(channel, payload)

+ + + + + + +
+ +
Description:
+
  • Subscribe to a channel with the given filter payload

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+ws.on('open', () => {
+  ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+    // ...
+  })
+
+  ws.subscribe('trades', { symbol: 'tBTCUSD' })
+})
+
+await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel payload/data

payload + + +object + + + + +

optional extra packet data

+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) subscribeCandles(key) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a candle data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeCandles('trade:5m:tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

'trade:5m:tBTCUSD'

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeOrderBook(symbol, prec, len) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to an order book data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeOrderBook('tBTCUSD', 'R0', '25')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
symbol + + +string + + + + + + +

symbol of order book

prec + + +string + + + + + + + P0 + +

P0, P1, P2, or P3 (default P0)

len + + +string + + + + + + + 25 + +

25 or 100 (default 25)

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeStatus(key) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a status data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeStatus('liq:global')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

i.e. 'liq:global'

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeTicker(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a ticker data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeTicker('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of ticker

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeTrades(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a trades data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeTrades('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of market to monitor

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

unsubscribe(chanId)

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from a channel by ID

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const id = ws.getDataChannelId('ticker', { symbol: 'tBTCUSD' })
+
+if (id) {
+  ws.unsubscribe(id)
+}
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanId + + +number + + + + +

ID of channel to unsubscribe from

+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) unsubscribeCandles(symbol, frame) → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubscribeCandles('tBTCUSD', '1m')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of candles

frame + + +string + + + + +

time frame

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeOrderBook(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from an order book data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubcribeOrderBook('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of order book

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeStatus(key) → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

key that was used in initial WSv2#subscribeStatus call

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeTicker(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from a ticker data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubscribeTicker('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of ticker

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeTrades(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from a trades data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubcribeTrades('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of market to unsubscribe from

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

updateAuthArgs(args)

+ + + + + + +
+ +
Description:
+
  • Set calc and dms values to be used on the next WSv2#auth call

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args + + +object + + + + +

arguments

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
calc + + +number + + + + + + + <optional>
+ + + + + +

calc value

dms + + +number + + + + + + + <optional>
+ + + + + +

dms value, active 4

apiKey + + +number + + + + + + + <optional>
+ + + + + +

API key

apiSecret + + +number + + + + + + + <optional>
+ + + + + +

API secret

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) updateOrder(changes) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Send a changeset to update an order in-place while maintaining position in +the price queue. The changeset must contain the order ID, and supports a +'delta' key to increase/decrease the total amount.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
changes + + +object + + + + +

requires at least an 'id'

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - resolves on receiving an confirmation notification

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

usesAgent() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

usesAgent

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ + WSv2 + +

+ +

Communicates with v2 of the Bitfinex WebSocket API

+ + +
+ +
+ +
+ + + + +

Constructor

+ + +

new WSv2()

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +

Members

+ + + +

(constant) WSv2.flags

+ + + + + +
+ +
Description:
+
  • Map of flag names and numeric values for usage with the API

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
DEC_S + + +number + + + + +

flag to request all decimals as strings

TIME_S + + +number + + + + +

flag to request all timestamps as strings

TIMESTAMP + + +number + + + + +

flag to request timestamp in milliseconds

SEQ_ALL + + +number + + + + +

flag to enable sequence numbers & verification

CHECKSUM + + +number + + + + +

flag to enable order book checksums

+ + + + + + +
+

Map of flag names and numeric values for usage with the API

+
+ + + + + + + + + + +

(constant) WSv2.info

+ + + + + +
+ +
Description:
+
  • Map of info event names and their respective codes for usage with the API

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
SERVER_RESTART + + +number + + + + +

server restart event

MAINTENANCE_START + + +number + + + + +

maintenance start event

MAINTENANCE_END + + +number + + + + +

maintenance end event

+ + + + + + +
+

Map of info event names and their respective codes for usage with the API

+
+ + + + + + + + + + +

(constant) WSv2.url :string

+ + + + + +
+ +
Description:
+
  • Default connection URL

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Default Value:
+
    +
  • wss://api.bitfinex.com/ws/2
  • +
+ + + + + + + +
+ + + + + +
+

Default connection URL

+
+ + + +
Type:
+
    +
  • + +string + + + +
  • +
+ + + + + + + + + + +

Methods

+ + + + + + +

(async) auth(calcnullable, dmsnullable) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Generates & sends an authentication packet to the server; if already +authenticated, rejects with an error, resolves on success.

    +

    If a DMS flag of 4 is provided, all open orders are cancelled when the +connection terminates.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
calc + + +number + + + + + + + + + <nullable>
+ + + +

optional, default is 0

dms + + +number + + + + + + + + + <nullable>
+ + + +

optional dead man switch flag, active 4

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) cancelOrder(order) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Cancels an order by ID and resolves the returned promise once the cancel is +confirmed. Emits an error if not authenticated. The ID can be passed as a +number, or taken from an order array/object.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
order + + +object +| + +Array +| + +number + + + + +

order model, array, or ID to be cancelled

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) cancelOrders(params) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Cancels multiple orders, returns a promise that resolves once all +operations are confirmed.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params + + +object +| + +Array.<object> +| + +Array.<Array> +| + +Array.<number> + + + + +

parameters or array of order models, arrays

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
ids + + +Array.<object> +| + +Array.<Array> +| + +Array.<number> + + + + +

array of order models, arrays +or IDs to be cancelled

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) close(code, reason) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Closes the active connection. If there is none, rejects with a promise. +Resolves on success

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
code + + +number + + + + +

passed to ws

reason + + +string + + + + +

passed to ws

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) enableFlag(flag) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Enables a configuration flag.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+
    +
  • WSv2#flags
  • +
+
+ + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+ws.on('open', async () => {
+  await ws.enableFlag(WSv2.flags.CHECKSUM)
+  console.log('ob checkums enabled')
+})
+
+await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
flag + + +number + + + + +

flag to update, as numeric value

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) enableSequencing(args) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Configures the seq flag to enable sequencing (packet number) for this +connection. When enabled, the seq number will be the last value of +channel packet arrays.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+
    +
  • WSv2#flags
  • +
+
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args + + +object + + + + +

params

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
audit + + +boolean + + + + + + + <optional>
+ + + + + +

if true, an error is emitted on invalid seq

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

getAuthArgs() → {object}

+ + + + + + +
+ +
Description:
+
  • Fetch the current default auth parameters

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

authArgs

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getCandles(key) → {Array}

+ + + + + + +
+ +
Description:
+
  • Fetch a reference to the full set of synced candles for the specified key. +Set managedCandles: true in the constructor to use.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2({ managedCandles: true })
+
+  ws.on('open', async () => {
+      ws.onCandles({ key: 'trade:1m:tBTCUSD' }, () => {
+        const candles = ws.getCandles('trade:1m:tBTCUSD')
+
+        if (!candles) return
+
+        console.log('%d candles in dataset', candles.length)
+      })
+
+      ws.subscribeCandles({ key: 'trade:1m:tBTCUSD' })
+  })
+
+  await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

key for candle set

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

candles - empty array if none exist

+
+ + + +
+
+ Type +
+
+ +Array + + + +
+
+ + + + + + + + + + +

getChannelData(opts) → {object}

+ + + + + + +
+ +
Description:
+
  • Fetch a channel definition

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
chanId + + +number + + + + + + + + + + +

channel ID

channel + + +string + + + + + + + + + + +

channel name

symbol + + +string + + + + + + + <optional>
+ + + + + +

match by symbol

key + + +string + + + + + + + <optional>
+ + + + + +

match by key (for candle channels)

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

chanData - null if not found

+
+ + + +
+
+ Type +
+
+ +object + + + +
+
+ + + + + + + + + + +

getDataChannelCount() → {number}

+ + + + + + +
+ +
Description:
+
  • Get the total number of data channels this instance is currently +subscribed too.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

count

+
+ + + +
+
+ Type +
+
+ +number + + + +
+
+ + + + + + + + + + +

getDataChannelId(type, filter) → {number}

+ + + + + + +
+ +
Description:
+
  • Fetch the ID of a channel matched by type and channel data filter

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
type + + +string + + + + +

channel type

filter + + +object + + + + +

to be matched against channel data

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

channelID

+
+ + + +
+
+ Type +
+
+ +number + + + +
+
+ + + + + + + + + + +

getLosslessOB(symbol) → {OrderBook}

+ + + + + + +
+ +
Description:
+
  • Returns an up-to-date lossless copy of the order book for the specified symbol, or +null if no OB is managed for that symbol. All amounts and prices are in original +string format.

    +

    Set manageOrderBooks: true in the constructor to use.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol for order book

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

ob - null if not found

+
+ + + +
+
+ Type +
+
+ +OrderBook + + + +
+
+ + + + + + + + + + +

getOB(symbol) → {OrderBook}

+ + + + + + +
+ +
Description:
+
  • Returns an up-to-date copy of the order book for the specified symbol, or +null if no OB is managed for that symbol.

    +

    Set managedOrderBooks: true in the constructor to use.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2({ managedOrderBooks: true })
+
+  ws.on('open', async () => {
+      ws.onOrderBook({ symbol: 'tBTCUSD' }, () => {
+        const book = ws.getOB('tBTCUSD')
+
+        if (!book) return
+
+        const spread = book.midPrice()
+        console.log('spread for tBTCUSD: %f', spread)
+      })
+
+      ws.subscribeOrderBook({ symbol: 'tBTCUSD' })
+  })
+
+  await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol for order book

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

ob - null if not found

+
+ + + +
+
+ Type +
+
+ +OrderBook + + + +
+
+ + + + + + + + + + +

getURL() → {string}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

url

+
+ + + +
+
+ Type +
+
+ +string + + + +
+
+ + + + + + + + + + +

hasChannel(chanId) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Check if the instance is subscribed to the specified channel ID

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanId + + +number + + + + +

ID of channel to query

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

isSubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

hasDataChannel(type, filter) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Check if the instance is subscribed to a data channel matching the +specified type and filter.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
type + + +string + + + + +

channel type

filter + + +object + + + + +

to be matched against channel data

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

hasChannel

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

hasSubscriptionRef(channel, identifier) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Check if a channel/identifier pair has been subscribed too

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel type

identifier + + +string + + + + +

unique identifier for the reference

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

hasRef

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isAuthenticated() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

authenticated

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isFlagEnabled(flag) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Checks local state, relies on successful server config responses

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+
    +
  • enableFlag
  • +
+
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
flag + + +number + + + + +

flag to check for

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

enabled

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isOpen() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

open

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

isReconnecting() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

reconnecting

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

managedSubscribe(channel, identifier, payload) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribes and tracks subscriptions per channel/identifier pair. If +already subscribed to the specified pair, nothing happens.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
To Do:
+
+
    +
  • will be refactored to return promise from subscribe() call instead + of sub action taken flag
  • +
+
+ +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+ws.on('open', async () => {
+  ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+    console.log('recv trades: %j', trades)
+ })
+
+  ws.managedSubscribe('trades', 'tBTCUSD', { symbol: 'tBTCUSD' })
+})
+
+await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel name

identifier + + +string + + + + +

for uniquely identifying the ref count

payload + + +object + + + + +

merged with sub packet

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subSent

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

managedUnsubscribe(channel, identifier) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Decreases the subscription ref count for the channel/identifier pair, and +unsubscribes from the channel if it reaches 0.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel name

identifier + + +string + + + + +

for uniquely identifying the ref count

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubSent

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

notifyUI(opts)

+ + + + + + +
+ +
Description:
+
  • Sends a broadcast notification, which will be received by any active UI +websocket connections (at bitfinex.com), triggering a desktop notification.

    +

    In the future our mobile app will also support spawning native push +notifications in response to incoming ucm-notify-ui packets.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
message + + +string + + + + + + + <optional>
+ + + + + +

message to display

type + + +string + + + + + + + <optional>
+ + + + + +

notification type, 'ucm-*' for broadcasts

level + + +string + + + + + + + <optional>
+ + + + + +

'info', 'error', or 'success'

image + + +string + + + + + + + <optional>
+ + + + + +

link to an image to be shown

link + + +string + + + + + + + <optional>
+ + + + + +

URL the notification should forward too

sound + + +string + + + + + + + <optional>
+ + + + + +

URL of sound to play

+ +
+ + + + + + + + + + + + + + +
Throws:
+ + + +
+ +

an error if given no type or message, or the instance is not open +and authenticated

+ +
+ + + + + + + + + + + + + +

onAccountTradeEntry(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each personal trade 'te' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onAccountTradeUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each personal trade 'tu' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onBalanceInfoUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each balance info update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onCandle(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received candle

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
key + + +string + + + + + + + + + + +

candle set key, i.e. trade:30m:tBTCUSD

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding credit is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding credit is created

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding credit snapshot (sent on +auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingCreditUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding credit is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingInfoUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding info update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding loan is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding loan is created

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding loan snapshot (sent on +auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingLoanUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding loan is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding offer is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding offer is created

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each fundign offer snapshot (sent on +auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingOfferUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a funding offer is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingTradeEntry(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding trade 'te' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onFundingTradeUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each funding trade 'tu' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onInfoMessage(code, cb)

+ + + + + + +
+ +
Description:
+
  • Registers a new callback to be called when a matching info message is +received.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
code + + +number + + + + +

from #WSv2.info

cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMaintenanceEnd(cb)

+ + + + + + +
+ +
Description:
+
  • Register a callback to be notified of a maintenance period ending

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

called on event trigger

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMaintenanceStart(cb)

+ + + + + + +
+ +
Description:
+
  • Register a callback in case of a 'maintenance started' message from the +server. This is a good time to pause server packets until maintenance ends

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

called on event trigger

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMarginInfoUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each margin info update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onMessage(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a generic handler to be called with each received message

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onNotification(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each notification

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
type + + +string + + + + + + + <optional>
+ + + + + +

type to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderBook(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received candle

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

book symbol

prec + + +string + + + + + + + + + + +

book precision, i.e. 'R0'

len + + +string + + + + + + + + + + +

book length, i.e. '25'

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderBookChecksum(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received order book checksum

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

book symbol

prec + + +string + + + + + + + + + + +

book precision, i.e. 'R0'

len + + +string + + + + + + + + + + +

book length, i.e. '25'

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each order close packet

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each new order packet

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each full order snapshot (sent on auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onOrderUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each order update packet

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

id + + +number + + + + + + + <optional>
+ + + + + +

order ID to match

cid + + +number + + + + + + + <optional>
+ + + + + +

order client ID to match

gid + + +number + + + + + + + <optional>
+ + + + + +

order group ID to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionClose(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a position is closed

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionNew(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a position is opened

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each position snapshot (sent on auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onPositionUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called when a position is updated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + <optional>
+ + + + + +

symbol to match

cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onServerRestart(cb)

+ + + + + + +
+ +
Description:
+
  • Register a callback in case of a ws server restart message; Use this to +call reconnect() if needed. (code 20051)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cb + + +function + + + + +

called on event trigger

+ + + + + + + + + + + + + + + + + + + + + + + + +

onStatus(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each message for the desired status +feed.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
key + + +string + + + + + + + + + + +

key of feed to listen on

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group ID

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onTicker(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each received ticker

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
symbol + + +string + + + + + + + + + + +

symbol for tickers

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onTradeEntry(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each trade 'te' event

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onTrades(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called with each received trade (pair or symbol +required)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
pair + + +string + + + + + + + <optional>
+ + + + + +

required if no symbol specified

symbol + + +string + + + + + + + <optional>
+ + + + + +

required if no pair specified

cbGID + + +string +| + +number + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onWalletSnapshot(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each wallet snapshot (sent on auth)

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

onWalletUpdate(opts, cb)

+ + + + + + +
+ +
Description:
+
  • Register a handler to be called on each wallet update

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opts + + +object + + + + +

options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
cbGID + + +string + + + + + + + <optional>
+ + + + + +

callback group id

+ +
cb + + +function + + + + +

callback

+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) open() → {Promise}

+ + + + + + +
+ +
Description:
+
  • Opens a connection to the API server. Rejects with an error if a +connection is already open. Resolves on success.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) reconnect() → {Promise}

+ + + + + + +
+ +
Description:
+
  • Utility method to close & re-open the ws connection. Re-authenticates if +previously authenticated

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - resolves on completion

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

removeListeners(cbGID)

+ + + + + + +
+ +
Description:
+
  • Remove all listeners by callback group ID

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeTrades({ symbol: 'tBTCUSD', cbGID: 42 })
+await ws.subscribeTrades({ symbol: 'tLEOUSD', cbGID: 42 })
+await ws.subscribeTrades({ symbol: 'tETHUSD', cbGID: 42 })
+
+// ...
+
+ws.removeListeners(42)
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cbGID + + +string + + + + +

callback group to remove

+ + + + + + + + + + + + + + + + + + + + + + + + +

requestCalc(prefixes)

+ + + + + + +
+ +
Description:
+
  • Request a calc operation to be performed on the specified indexes

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
prefixes + + +Array.<string> + + + + +

desired prefixes to be calculated

+ + + + + + + + + + + + + + + + + + + + + + + + +

send(msg)

+ + + + + + +
+ +
Description:
+
  • Send a packet to the WS server

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +* + + + + +

packet, gets stringified

+ + + + + + + + + + + + + + + + + + + + + + + + +

sequencingEnabled() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

sequencingEnabled

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) submitOrder(params) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Sends a new order to the server and resolves the returned promise once the +order submit is confirmed. Emits an error if not authenticated. The order +can be either an array, key/value map, or Order object instance.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
const o = new Order({
+  type: Order.type.EXCHANGE_LIMIT,
+  amount: 18,
+  price: 0.75,
+  symbol: 'tBTCUSD',
+  hidden: true
+}, ws)
+
+await ws.submitOrder(o)
+
+console.log('order confirmed! status: %s', o.status)
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
params + + +object +| + +Array + + + + +

parameters or order object model or array

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
order + + +object +| + +Array + + + + +

order object model or array

+ +
+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - resolves on submit notification

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

(async) submitOrderMultiOp(opPayloads) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Sends the op payloads to the server as an 'ox_multi' command. A promise is +returned and resolves immediately if authenticated, as no confirmation is +available for this message type.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
opPayloads + + +Array.<object> + + + + +

order operations

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - rejects if not authenticated

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

subscribe(channel, payload)

+ + + + + + +
+ +
Description:
+
  • Subscribe to a channel with the given filter payload

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const ws = new WSv2()
+
+ws.on('open', () => {
+  ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+    // ...
+  })
+
+  ws.subscribe('trades', { symbol: 'tBTCUSD' })
+})
+
+await ws.open()
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
channel + + +string + + + + +

channel payload/data

payload + + +object + + + + +

optional extra packet data

+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) subscribeCandles(key) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a candle data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeCandles('trade:5m:tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

'trade:5m:tBTCUSD'

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeOrderBook(symbol, prec, len) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to an order book data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeOrderBook('tBTCUSD', 'R0', '25')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
symbol + + +string + + + + + + +

symbol of order book

prec + + +string + + + + + + + P0 + +

P0, P1, P2, or P3 (default P0)

len + + +string + + + + + + + 25 + +

25 or 100 (default 25)

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeStatus(key) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a status data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeStatus('liq:global')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

i.e. 'liq:global'

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeTicker(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a ticker data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeTicker('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of ticker

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) subscribeTrades(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Subscribe to a trades data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.subscribeTrades('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of market to monitor

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

subscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

unsubscribe(chanId)

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from a channel by ID

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Example
+ +
const id = ws.getDataChannelId('ticker', { symbol: 'tBTCUSD' })
+
+if (id) {
+  ws.unsubscribe(id)
+}
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
chanId + + +number + + + + +

ID of channel to unsubscribe from

+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) unsubscribeCandles(symbol, frame) → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubscribeCandles('tBTCUSD', '1m')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of candles

frame + + +string + + + + +

time frame

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeOrderBook(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from an order book data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubcribeOrderBook('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of order book

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeStatus(key) → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
key + + +string + + + + +

key that was used in initial WSv2#subscribeStatus call

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeTicker(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from a ticker data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubscribeTicker('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of ticker

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

(async) unsubscribeTrades(symbol) → {boolean}

+ + + + + + +
+ +
Description:
+
  • Unsubscribe from a trades data channel

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + +
Example
+ +
await ws.unsubcribeTrades('tBTCUSD')
+ + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
symbol + + +string + + + + +

symbol of market to unsubscribe from

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

unsubscribed

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + +

updateAuthArgs(args)

+ + + + + + +
+ +
Description:
+
  • Set calc and dms values to be used on the next WSv2#auth call

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args + + +object + + + + +

arguments

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
calc + + +number + + + + + + + <optional>
+ + + + + +

calc value

dms + + +number + + + + + + + <optional>
+ + + + + +

dms value, active 4

apiKey + + +number + + + + + + + <optional>
+ + + + + +

API key

apiSecret + + +number + + + + + + + <optional>
+ + + + + +

API secret

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +

(async) updateOrder(changes) → {Promise}

+ + + + + + +
+ +
Description:
+
  • Send a changeset to update an order in-place while maintaining position in +the price queue. The changeset must contain the order ID, and supports a +'delta' key to increase/decrease the total amount.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
changes + + +object + + + + +

requires at least an 'id'

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

p - resolves on receiving an confirmation notification

+
+ + + +
+
+ Type +
+
+ +Promise + + + +
+
+ + + + + + + + + + +

usesAgent() → {boolean}

+ + + + + + +
+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Returns:
+ + +
+

usesAgent

+
+ + + +
+
+ Type +
+
+ +boolean + + + +
+
+ + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/Montserrat/Montserrat-Bold.eot b/docs/fonts/Montserrat/Montserrat-Bold.eot new file mode 100644 index 00000000..f2970bbd Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Bold.eot differ diff --git a/docs/fonts/Montserrat/Montserrat-Bold.ttf b/docs/fonts/Montserrat/Montserrat-Bold.ttf new file mode 100644 index 00000000..3bfd79b6 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Bold.ttf differ diff --git a/docs/fonts/Montserrat/Montserrat-Bold.woff b/docs/fonts/Montserrat/Montserrat-Bold.woff new file mode 100644 index 00000000..92607654 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Bold.woff differ diff --git a/docs/fonts/Montserrat/Montserrat-Bold.woff2 b/docs/fonts/Montserrat/Montserrat-Bold.woff2 new file mode 100644 index 00000000..d9940cd1 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Bold.woff2 differ diff --git a/docs/fonts/Montserrat/Montserrat-Regular.eot b/docs/fonts/Montserrat/Montserrat-Regular.eot new file mode 100644 index 00000000..735d12b5 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Regular.eot differ diff --git a/docs/fonts/Montserrat/Montserrat-Regular.ttf b/docs/fonts/Montserrat/Montserrat-Regular.ttf new file mode 100644 index 00000000..5da852a3 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Regular.ttf differ diff --git a/docs/fonts/Montserrat/Montserrat-Regular.woff b/docs/fonts/Montserrat/Montserrat-Regular.woff new file mode 100644 index 00000000..bf918327 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Regular.woff differ diff --git a/docs/fonts/Montserrat/Montserrat-Regular.woff2 b/docs/fonts/Montserrat/Montserrat-Regular.woff2 new file mode 100644 index 00000000..72d13c60 Binary files /dev/null and b/docs/fonts/Montserrat/Montserrat-Regular.woff2 differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot new file mode 100644 index 00000000..0f24510b Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg new file mode 100644 index 00000000..5384f985 --- /dev/null +++ b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg @@ -0,0 +1,978 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf new file mode 100644 index 00000000..e6c158c2 Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff new file mode 100644 index 00000000..d0a1c292 Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 new file mode 100644 index 00000000..d2869749 Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot new file mode 100644 index 00000000..b4204488 Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg new file mode 100644 index 00000000..dee0949f --- /dev/null +++ b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg @@ -0,0 +1,1049 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf new file mode 100644 index 00000000..4d56c337 Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff new file mode 100644 index 00000000..4681019d Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff differ diff --git a/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 new file mode 100644 index 00000000..8ddcae37 Binary files /dev/null and b/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 differ diff --git a/docs/global.html b/docs/global.html new file mode 100644 index 00000000..0e275bef --- /dev/null +++ b/docs/global.html @@ -0,0 +1,510 @@ + + + + + + Global - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

Global

+ + + + + + + +
+ +
+ +

+ + + +

+ + +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + +

Methods

+ + + + + + +

setSigFig(number, maxSigsopt) → {string}

+ + + + + + +
+ +
Description:
+
  • Smartly set the precision (decimal) on a value based off of the significant +digit maximum. For example, calling with 3.34 when the max sig figs allowed +is 5 would return '3.3400', the representation number of decimals IF they +weren't zeros.

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
number + + +number + + + + + + + + + + + + + 0 + +

number to manipulate

maxSigs + + +number + + + + + + + <optional>
+ + + + + +
+ +

default 5

+ + + + + + + + + + + + + + + + +
Returns:
+ + +
+

str

+
+ + + +
+
+ Type +
+
+ +string + + + +
+
+ + + + + + + + + +

Type Definitions

+ + + +

PromiseThrottle

+ + + + + +
+ +
Description:
+
  • A Promise Throttle instance

+ + + +
Source:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
add + + +function + + + + +

add a promise to be throttled

+ + + + + + +
+

A Promise Throttle instance

+
+ + + +
Type:
+
    +
  • + +object + + + +
  • +
+ + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..8c795438 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,252 @@ + + + + + + Home - Documentation + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+

+
+ + + + + + + + + + + + + +
+

Bitfinex WSv2 Trading API for Node.JS - Bitcoin, Ethereum, Ripple and more

+

Build Status

+

A Node.JS reference implementation of the Bitfinex API

+

Features

+
    +
  • Official implementation
  • +
  • REST v2 API
  • +
  • WebSockets v2 API
  • +
  • WebSockets v1 API
  • +
+

Installation

+
  npm i --save bitfinex-api-node
+
+

Quickstart

+
const { WSv2 } = require('bitfinex-api-node')
+const ws = new WSv2({ transform: true })
+
+// do something with ws client
+
+

Docs

+

Refer to the docs/ +folder for JSDoc-generated HTML documentation, and the examples/ +folder for executable examples covering common use cases.

+

Official API documentation at https://docs.bitfinex.com/v2/reference

+

Examples

+

Sending an order & tracking status:

+
const ws = bfx.ws()
+
+ws.on('error', (err) => console.log(err))
+ws.on('open', ws.auth.bind(ws))
+
+ws.once('auth', () => {
+  const o = new Order({
+    cid: Date.now(),
+    symbol: 'tETHUSD',
+    amount: 0.1,
+    type: Order.type.MARKET
+  }, ws)
+
+  // Enable automatic updates
+  o.registerListeners()
+
+  o.on('update', () => {
+    console.log(`order updated: ${o.serialize()}`)
+  })
+
+  o.on('close', () => {
+    console.log(`order closed: ${o.status}`)
+    ws.close()
+  })
+
+  o.submit().then(() => {
+    console.log(`submitted order ${o.id}`)
+  }).catch((err) => {
+    console.error(err)
+    ws.close()
+  })
+})
+
+ws.open()
+
+

Cancel all open orders

+
const ws = bfx.ws(2)
+
+ws.on('error', (err) => console.log(err))
+ws.on('open', ws.auth.bind(ws))
+
+ws.onOrderSnapshot({}, (orders) => {
+  if (orders.length === 0) {
+    console.log('no open orders')
+    return
+  }
+
+  console.log(`recv ${orders.length} open orders`)
+
+  ws.cancelOrders(orders).then(() => {
+    console.log('cancelled orders')
+  })
+})
+
+ws.open()
+
+

Subscribe to trades by pair

+
const ws = bfx.ws(2)
+
+ws.on('error', (err) => console.log(err))
+ws.on('open', () => {
+  ws.subscribeTrades('BTCUSD')
+})
+
+ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+  console.log(`trades: ${JSON.stringify(trades)}`)
+})
+ws.onTradeEntry({ symbol: 'tBTCUSD' }, (trades) => {
+  console.log(`te: ${JSON.stringify(trades)}`)
+})
+
+ws.open()
+
+

Version 2.0.0 Breaking changes

+

constructor takes only an options object now, including the API keys

+

Old:

+
new BFX(API_KEY, API_SECRET, { version: 2 })
+
+

since 2.0.0:

+
new BFX({ apiKey: '', apiSecret: '' })
+
+

trade and orderbook snapshots are emitted as nested lists

+

To make dealing with snapshots better predictable, snapshots are emitted as an array.

+

normalized orderbooks for R0

+

Lists of raw orderbooks (R0) are ordered in the same order as P0, P1, +P2, P3

+

Testing

+
npm test
+
+

FAQ

+

Order Creation Limits

+

The base limit per-user is 1,000 orders per 5 minute interval, and is shared +between all account API connections. It increases proportionally to your trade +volume based on the following formula:

+

1000 + (TOTAL_PAIRS_PLATFORM * 60 * 5) / (250000000 / USER_VOL_LAST_30d)

+

Where TOTAL_PAIRS_PLATFORM is the number of pairs shared between +Ethfinex/Bitfinex (currently ~101) and USER_VOL_LAST_30d is in USD.

+

'on' Packet Guarantees

+

No; if your order fills immediately, the first packet referencing the order +will be an oc signaling the order has closed. If the order fills partially +immediately after creation, an on packet will arrive with a status of +PARTIALLY FILLED...

+

For example, if you submit a LIMIT buy for 0.2 BTC and it is added to the +order book, an on packet will arrive via ws2. After a partial fill of 0.1 +BTC, an ou packet will arrive, followed by a final oc after the remaining +0.1 BTC fills.

+

On the other hand, if the order fills immediately for 0.2 BTC, you will only +receive an oc packet.

+

Nonce too small

+

I make multiple parallel request and I receive an error that the nonce is too +small. What does it mean?

+

Nonces are used to guard against replay attacks. When multiple HTTP requests +arrive at the API with the wrong nonce, e.g. because of an async timing issue, +the API will reject the request.

+

If you need to go parallel, you have to use multiple API keys right now.

+

te vs tu Messages

+

A te packet is sent first to the client immediately after a trade has been +matched & executed, followed by a tu message once it has completed processing. +During times of high load, the tu message may be noticably delayed, and as +such only the te message should be used for a realtime feed.

+

Sequencing

+

If you enable sequencing on v2 of the WS API, each incoming packet will have a +public sequence number at the end, along with an auth sequence number in the +case of channel 0 packets. The public seq numbers increment on each packet, +and the auth seq numbers increment on each authenticated action (new orders, +etc). These values allow you to verify that no packets have been missed/dropped, +since they always increase monotonically.

+

Differences Between R* and P* Order Books

+

Order books with precision R0 are considered 'raw' and contain entries for +each order submitted to the book, whereas P* books contain entries for each +price level (which aggregate orders).

+

Contributing

+
    +
  1. Fork it
  2. +
  3. Create your feature branch (git checkout -b my-new-feature)
  4. +
  5. Commit your changes (git commit -am 'Add some feature')
  6. +
  7. Push to the branch (git push origin my-new-feature)
  8. +
  9. Create a new Pull Request
  10. +
+
+ + + + + + + + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/docs/scripts/collapse.js b/docs/scripts/collapse.js new file mode 100644 index 00000000..4e63926d --- /dev/null +++ b/docs/scripts/collapse.js @@ -0,0 +1,39 @@ +function hideAllButCurrent(){ + //by default all submenut items are hidden + //but we need to rehide them for search + document.querySelectorAll("nav > ul").forEach(function(parent) { + if (parent.className.indexOf("collapse_top") !== -1) { + parent.style.display = "none"; + } + }); + document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { + parent.style.display = "none"; + }); + document.querySelectorAll("nav > h3").forEach(function(section) { + if (section.className.indexOf("collapsed_header") !== -1) { + section.addEventListener("click", function(){ + if (section.nextSibling.style.display === "none") { + section.nextSibling.style.display = "block"; + } else { + section.nextSibling.style.display = "none"; + } + }); + } + }); + + //only current page (if it exists) should be opened + var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); + document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { + var href = parent.attributes.href.value.replace(/\.html/, ''); + if (file === href) { + if (parent.parentNode.parentNode.className.indexOf("collapse_top") !== -1) { + parent.parentNode.parentNode.style.display = "block"; + } + parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { + elem.style.display = "block"; + }); + } + }); +} + +hideAllButCurrent(); \ No newline at end of file diff --git a/docs/scripts/commonNav.js b/docs/scripts/commonNav.js new file mode 100644 index 00000000..03e82028 --- /dev/null +++ b/docs/scripts/commonNav.js @@ -0,0 +1,28 @@ +if (typeof fetch === 'function') { + const init = () => { + if (typeof scrollToNavItem !== 'function') return false + scrollToNavItem() + // hideAllButCurrent not always loaded + if (typeof hideAllButCurrent === 'function') hideAllButCurrent() + return true + } + fetch('./nav.inc.html') + .then(response => response.ok ? response.text() : `${response.url} => ${response.status} ${response.statusText}`) + .then(body => { + document.querySelector('nav').innerHTML += body + // nav.js should be quicker to load than nav.inc.html, a fallback just in case + return init() + }) + .then(done => { + if (done) return + let i = 0 + ;(function waitUntilNavJs () { + if (init()) return + if (i++ < 100) return setTimeout(waitUntilNavJs, 300) + console.error(Error('nav.js not loaded after 30s waiting for it')) + })() + }) + .catch(error => console.error(error)) +} else { + console.error(Error('Browser too old to display commonNav (remove commonNav docdash option)')) +} diff --git a/docs/scripts/linenumber.js b/docs/scripts/linenumber.js new file mode 100644 index 00000000..8d52f7ea --- /dev/null +++ b/docs/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(function() { + var source = document.getElementsByClassName('prettyprint source linenums'); + var i = 0; + var lineNumber = 0; + var lineId; + var lines; + var totalLines; + var anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = 'line' + lineNumber; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/docs/scripts/nav.js b/docs/scripts/nav.js new file mode 100644 index 00000000..6dd83134 --- /dev/null +++ b/docs/scripts/nav.js @@ -0,0 +1,12 @@ +function scrollToNavItem() { + var path = window.location.href.split('/').pop().replace(/\.html/, ''); + document.querySelectorAll('nav a').forEach(function(link) { + var href = link.attributes.href.value.replace(/\.html/, ''); + if (path === href) { + link.scrollIntoView({block: 'center'}); + return; + } + }) + } + + scrollToNavItem(); diff --git a/docs/scripts/polyfill.js b/docs/scripts/polyfill.js new file mode 100644 index 00000000..44b4c92d --- /dev/null +++ b/docs/scripts/polyfill.js @@ -0,0 +1,4 @@ +//IE Fix, src: https://www.reddit.com/r/programminghorror/comments/6abmcr/nodelist_lacks_foreach_in_internet_explorer/ +if (typeof(NodeList.prototype.forEach)!==typeof(alert)){ + NodeList.prototype.forEach=Array.prototype.forEach; +} \ No newline at end of file diff --git a/docs/scripts/prettify/Apache-License-2.0.txt b/docs/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/docs/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/scripts/prettify/lang-css.js b/docs/scripts/prettify/lang-css.js new file mode 100644 index 00000000..041e1f59 --- /dev/null +++ b/docs/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/docs/scripts/prettify/prettify.js b/docs/scripts/prettify/prettify.js new file mode 100644 index 00000000..eef5ad7e --- /dev/null +++ b/docs/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c< +f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p ul > li:not(.level-hide)").forEach(function(elem) { + elem.style.display = "block"; + }); + + if (typeof hideAllButCurrent === "function"){ + //let's do what ever collapse wants to do + hideAllButCurrent(); + } else { + //menu by default should be opened + document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { + elem.style.display = "block"; + }); + } + } else { + //we are searching + document.documentElement.setAttribute(searchAttr, ''); + + //show all parents + document.querySelectorAll("nav > ul > li").forEach(function(elem) { + elem.style.display = "block"; + }); + document.querySelectorAll("nav > ul").forEach(function(elem) { + elem.style.display = "block"; + }); + //hide all results + document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { + elem.style.display = "none"; + }); + //show results matching filter + document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { + if (!contains(elem.parentNode, search)) { + return; + } + elem.parentNode.style.display = "block"; + }); + //hide parents without children + document.querySelectorAll("nav > ul > li").forEach(function(parent) { + var countSearchA = 0; + parent.querySelectorAll("a").forEach(function(elem) { + if (contains(elem, search)) { + countSearchA++; + } + }); + + var countUl = 0; + var countUlVisible = 0; + parent.querySelectorAll("ul").forEach(function(ulP) { + // count all elements that match the search + if (contains(ulP, search)) { + countUl++; + } + + // count all visible elements + var children = ulP.children + for (i=0; i ul.collapse_top").forEach(function(parent) { + var countVisible = 0; + parent.querySelectorAll("li").forEach(function(elem) { + if (elem.style.display !== "none") { + countVisible++; + } + }); + + if (countVisible == 0) { + //has no child at all and does not contain text + parent.style.display = "none"; + } + }); + } +}); \ No newline at end of file diff --git a/docs/styles/jsdoc.css b/docs/styles/jsdoc.css new file mode 100644 index 00000000..0fe6d3cd --- /dev/null +++ b/docs/styles/jsdoc.css @@ -0,0 +1,776 @@ +* { + box-sizing: border-box +} + +html, body { + height: 100%; + width: 100%; +} + +body { + color: #4d4e53; + background-color: white; + margin: 0 auto; + padding: 0 20px; + font-family: 'Helvetica Neue', Helvetica, sans-serif; + font-size: 16px; +} + +img { + max-width: 100%; +} + +a, +a:active { + color: #606; + text-decoration: none; +} + +a:hover { + text-decoration: none; +} + +article a { + border-bottom: 1px solid #ddd; +} + +article a:hover, article a:active { + border-bottom-color: #222; +} + +article .description a { + word-break: break-word; +} + +p, ul, ol, blockquote { + margin-bottom: 1em; + line-height: 160%; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; +} + +h1, h2, h3, h4, h5, h6 { + color: #000; + font-weight: 400; + margin: 0; +} + +h1 { + font-weight: 300; + font-size: 48px; + margin: 1em 0 .5em; +} + +h1.page-title { + font-size: 48px; + margin: 1em 30px; + line-height: 100%; + word-wrap: break-word; +} + +h2 { + font-size: 24px; + margin: 1.5em 0 .3em; +} + +h3 { + font-size: 24px; + margin: 1.2em 0 .3em; +} + +h4 { + font-size: 18px; + margin: 1em 0 .2em; + color: #4d4e53; +} + +h4.name { + color: #fff; + background: #6d426d; + box-shadow: 0 .25em .5em #d3d3d3; + border-top: 1px solid #d3d3d3; + border-bottom: 1px solid #d3d3d3; + margin: 1.5em 0 0.5em; + padding: .75em 0 .75em 10px; +} + +h4.name a { + color: #fc83ff; +} + +h4.name a:hover { + border-bottom-color: #fc83ff; +} + +h5, .container-overview .subsection-title { + font-size: 120%; + letter-spacing: -0.01em; + margin: 8px 0 3px 0; +} + +h6 { + font-size: 100%; + letter-spacing: -0.01em; + margin: 6px 0 3px 0; + font-style: italic; +} + +.usertext h1 { + font-family: "Source Sans Pro"; + font-size: 24px; + margin: 2.5em 0 1em; + font-weight: 400; +} + +.usertext h2 { + font-family: "Source Sans Pro"; + font-size: 18px; + margin: 2em 0 0.5em; + font-weight: 400; + +} + +.usertext h3 { + font-family: "Source Sans Pro"; + font-size: 15px; + margin: 1.5em 0 0; + font-weight: 400; +} + +.usertext h4 { + font-family: "Source Sans Pro"; + font-size: 14px; + margin: 0 0 0; + font-weight: 400; +} + +.usertext h5 { + font-size: 12px; + margin: 1em 0 0; + font-weight: normal; + color: #666; +} + +.usertext h6 { + font-size: 11px; + margin: 1em 0 0; + font-weight: normal; + font-style: normal; + color: #666; +} + + +tt, code, kbd, samp, pre { + font-family: Consolas, Monaco, 'Andale Mono', monospace; + background: #f4f4f4; +} + +tt, code, kbd, samp{ + padding: 1px 5px; +} + +pre { + padding-bottom: 1em; +} + +.class-description { + font-size: 130%; + line-height: 140%; + margin-bottom: 1em; + margin-top: 1em; +} + +.class-description:empty { + margin: 0 +} + +#main { + float: right; + width: calc(100% - 240px); +} + +header { + display: block +} + +section { + display: block; + background-color: #fff; + padding: 0 0 0 30px; +} + +.variation { + display: none +} + +.signature-attributes { + font-size: 60%; + color: #eee; + font-style: italic; + font-weight: lighter; +} + +nav { + float: left; + display: block; + width: 250px; + background: #fff; + overflow: auto; + position: fixed; + height: 100%; +} + +nav #nav-search{ + width: 210px; + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; + margin-right: 20px; + margin-top: 20px; +} + +nav.wrap a{ + word-wrap: break-word; +} + +nav h3 { + margin-top: 12px; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 700; + line-height: 24px; + margin: 15px 0 10px; + padding: 0; + color: #000; +} + +nav h3.collapsed_header { + cursor: pointer; +} + +nav ul { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; + font-size: 100%; + line-height: 17px; + padding: 0; + margin: 0; + list-style-type: none; +} + +nav ul a, +nav ul a:active { + font-family: 'Montserrat', sans-serif; + line-height: 18px; + padding: 0; + display: block; + font-size: 12px; +} + +nav a:hover, +nav a:active { + color: #606; +} + +nav > ul { + padding: 0 10px; +} + +nav > ul > li > a { + color: #606; + margin-top: 10px; +} + +nav ul ul a { + color: hsl(207, 1%, 60%); + border-left: 1px solid hsl(207, 10%, 86%); +} + +nav ul ul a, +nav ul ul a:active { + padding-left: 20px +} + +nav h2 { + font-size: 13px; + margin: 10px 0 0 0; + padding: 0; +} + +nav > h2 > a { + margin: 10px 0 -10px; + color: #606 !important; +} + +footer { + color: hsl(0, 0%, 28%); + margin-left: 250px; + display: block; + padding: 15px; + font-style: italic; + font-size: 90%; +} + +.ancestors { + color: #999 +} + +.ancestors a { + color: #999 !important; +} + +.clear { + clear: both +} + +.important { + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px +} + +.type-signature { + color: #CA79CA +} + +.type-signature:last-child { + color: #eee; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace +} + +.signature { + color: #fc83ff; +} + +.details { + margin-top: 6px; + border-left: 2px solid #DDD; + line-height: 20px; + font-size: 14px; +} + +.details dt { + width: auto; + float: left; + padding-left: 10px; +} + +.details dd { + margin-left: 70px; + margin-top: 6px; + margin-bottom: 6px; +} + +.details ul { + margin: 0 +} + +.details ul { + list-style-type: none +} + +.details pre.prettyprint { + margin: 0 +} + +.details .object-value { + padding-top: 0 +} + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption { + font-style: italic; + font-size: 107%; + margin: 0; +} + +.prettyprint { + font-size: 14px; + overflow: auto; +} + +.prettyprint.source { + width: inherit; + line-height: 18px; + display: block; + background-color: #0d152a; + color: #aeaeae; +} + +.prettyprint code { + line-height: 18px; + display: block; + background-color: #0d152a; + color: #4D4E53; +} + +.prettyprint > code { + padding: 15px; +} + +.prettyprint .linenums code { + padding: 0 15px +} + +.prettyprint .linenums li:first-of-type code { + padding-top: 15px +} + +.prettyprint code span.line { + display: inline-block +} + +.prettyprint.linenums { + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol { + padding-left: 0 +} + +.prettyprint.linenums li { + border-left: 3px #34446B solid; +} + +.prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { + background-color: #34446B; +} + +.prettyprint.linenums li * { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.prettyprint.linenums li code:empty:after { + content:""; + display:inline-block; + width:0px; +} + +table { + border-spacing: 0; + border: 1px solid #ddd; + border-collapse: collapse; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + width: 100%; + font-size: 14px; + margin: 1em 0; +} + +td, th { + margin: 0px; + text-align: left; + vertical-align: top; + padding: 10px; + display: table-cell; +} + +thead tr, thead tr { + background-color: #fff; + font-weight: bold; + border-bottom: 1px solid #ddd; +} + +.params .type { + white-space: nowrap; +} + +.params code { + white-space: pre; +} + +.params td, .params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td { + border-top: 1px solid #eee +} + +.params td.description > p:first-child, .props td.description > p:first-child { + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, .props td.description > p:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +span.param-type, .params td .param-type, .param-type dd { + color: #606; + font-family: Consolas, Monaco, 'Andale Mono', monospace +} + +.param-type dt, .param-type dd { + display: inline-block +} + +.param-type { + margin: 14px 0; +} + +.disabled { + color: #454545 +} + +/* navicon button */ +.navicon-button { + display: none; + position: relative; + padding: 2.0625rem 1.5rem; + transition: 0.25s; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: .8; +} +.navicon-button .navicon:before, .navicon-button .navicon:after { + transition: 0.25s; +} +.navicon-button:hover { + transition: 0.5s; + opacity: 1; +} +.navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after { + transition: 0.25s; +} +.navicon-button:hover .navicon:before { + top: .825rem; +} +.navicon-button:hover .navicon:after { + top: -.825rem; +} + +/* navicon */ +.navicon { + position: relative; + width: 2.5em; + height: .3125rem; + background: #000; + transition: 0.3s; + border-radius: 2.5rem; +} +.navicon:before, .navicon:after { + display: block; + content: ""; + height: .3125rem; + width: 2.5rem; + background: #000; + position: absolute; + z-index: -1; + transition: 0.3s 0.25s; + border-radius: 1rem; +} +.navicon:before { + top: .625rem; +} +.navicon:after { + top: -.625rem; +} + +/* open */ +.nav-trigger:checked + label:not(.steps) .navicon:before, +.nav-trigger:checked + label:not(.steps) .navicon:after { + top: 0 !important; +} + +.nav-trigger:checked + label .navicon:before, +.nav-trigger:checked + label .navicon:after { + transition: 0.5s; +} + +/* Minus */ +.nav-trigger:checked + label { + -webkit-transform: scale(0.75); + transform: scale(0.75); +} + +/* × and + */ +.nav-trigger:checked + label.plus .navicon, +.nav-trigger:checked + label.x .navicon { + background: transparent; +} + +.nav-trigger:checked + label.plus .navicon:before, +.nav-trigger:checked + label.x .navicon:before { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + background: #FFF; +} + +.nav-trigger:checked + label.plus .navicon:after, +.nav-trigger:checked + label.x .navicon:after { + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + background: #FFF; +} + +.nav-trigger:checked + label.plus { + -webkit-transform: scale(0.75) rotate(45deg); + transform: scale(0.75) rotate(45deg); +} + +.nav-trigger:checked ~ nav { + left: 0 !important; +} + +.nav-trigger:checked ~ .overlay { + display: block; +} + +.nav-trigger { + position: fixed; + top: 0; + clip: rect(0, 0, 0, 0); +} + +.overlay { + display: none; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + background: hsla(0, 0%, 0%, 0.5); + z-index: 1; +} + +/* nav level */ +.level-hide { + display: none; +} +html[data-search-mode] .level-hide { + display: block; +} + + +@media only screen and (max-width: 680px) { + body { + overflow-x: hidden; + } + + nav { + background: #FFF; + width: 250px; + height: 100%; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: -250px; + z-index: 3; + padding: 0 10px; + transition: left 0.2s; + } + + .navicon-button { + display: inline-block; + position: fixed; + top: 1.5em; + right: 0; + z-index: 2; + } + + #main { + width: 100%; + } + + #main h1.page-title { + margin: 1em 0; + } + + #main section { + padding: 0; + } + + footer { + margin-left: 0; + } +} + +/** Add a '#' to static members */ +[data-type="member"] a::before { + content: '#'; + display: inline-block; + margin-left: -14px; + margin-right: 5px; +} + +#disqus_thread{ + margin-left: 30px; +} + +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + src: url('../fonts/Montserrat/Montserrat-Regular.eot'); /* IE9 Compat Modes */ + src: url('../fonts/Montserrat/Montserrat-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/Montserrat/Montserrat-Regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/Montserrat/Montserrat-Regular.woff') format('woff'), /* Pretty Modern Browsers */ + url('../fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); /* Safari, Android, iOS */ +} + +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 700; + src: url('../fonts/Montserrat/Montserrat-Bold.eot'); /* IE9 Compat Modes */ + src: url('../fonts/Montserrat/Montserrat-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/Montserrat/Montserrat-Bold.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/Montserrat/Montserrat-Bold.woff') format('woff'), /* Pretty Modern Browsers */ + url('../fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); /* Safari, Android, iOS */ +} + +@font-face { + font-family: 'Source Sans Pro'; + src: url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot'); + src: url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2') format('woff2'), + url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff') format('woff'), + url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf') format('truetype'), + url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg#source_sans_proregular') format('svg'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Source Sans Pro'; + src: url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot'); + src: url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2') format('woff2'), + url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff') format('woff'), + url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf') format('truetype'), + url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg#source_sans_prolight') format('svg'); + font-weight: 300; + font-style: normal; + +} diff --git a/docs/styles/prettify.css b/docs/styles/prettify.css new file mode 100644 index 00000000..6f4d2eea --- /dev/null +++ b/docs/styles/prettify.css @@ -0,0 +1,80 @@ +.pln { + color: #ddd; +} + +/* string content */ +.str { + color: #61ce3c; +} + +/* a keyword */ +.kwd { + color: #fbde2d; +} + +/* a comment */ +.com { + color: #aeaeae; +} + +/* a type name */ +.typ { + color: #8da6ce; +} + +/* a literal value */ +.lit { + color: #fbde2d; +} + +/* punctuation */ +.pun { + color: #ddd; +} + +/* lisp open bracket */ +.opn { + color: #000000; +} + +/* lisp close bracket */ +.clo { + color: #000000; +} + +/* a markup tag name */ +.tag { + color: #8da6ce; +} + +/* a markup attribute name */ +.atn { + color: #fbde2d; +} + +/* a markup attribute value */ +.atv { + color: #ddd; +} + +/* a declaration */ +.dec { + color: #EF5050; +} + +/* a variable name */ +.var { + color: #c82829; +} + +/* a function name */ +.fun { + color: #4271ae; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; + padding-bottom: 2px; +} diff --git a/docs/transports_ws2.js.html b/docs/transports_ws2.js.html new file mode 100644 index 00000000..dd17a87b --- /dev/null +++ b/docs/transports_ws2.js.html @@ -0,0 +1,2873 @@ + + + + + + transports/ws2.js - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

transports/ws2.js

+ + + + + + + +
+
+
'use strict'
+
+const { EventEmitter } = require('events')
+const debug = require('debug')('bfx:ws2')
+const WebSocket = require('ws')
+const CbQ = require('cbq')
+const _Throttle = require('lodash.throttle') // eslint-disable-line
+const _isArray = require('lodash/isArray')
+const _isEmpty = require('lodash/isEmpty')
+const _isString = require('lodash/isString')
+const _isNumber = require('lodash/isNumber')
+const _includes = require('lodash/includes')
+const _pick = require('lodash/pick')
+const _isEqual = require('lodash/isEqual')
+const _isFinite = require('lodash/isFinite')
+const { genAuthSig, nonce } = require('bfx-api-node-util')
+const LosslessJSON = require('lossless-json')
+const getMessagePayload = require('../util/ws2')
+
+const {
+  BalanceInfo,
+  FundingCredit,
+  FundingInfo,
+  FundingLoan,
+  FundingOffer,
+  FundingTrade,
+  MarginInfo,
+  Notification,
+  Order,
+  Position,
+  Trade,
+  PublicTrade,
+  Wallet,
+  OrderBook,
+  Candle,
+  TradingTicker,
+  FundingTicker
+} = require('bfx-api-node-models')
+
+const DATA_CHANNEL_TYPES = ['ticker', 'book', 'candles', 'trades']
+const UCM_NOTIFICATION_TYPE = 'ucm-notify-ui'
+const MAX_CALC_OPS = 8
+
+/**
+ * A Promise Throttle instance
+ *
+ * @typedef {object} PromiseThrottle
+ * @property {Function} add - add a promise to be throttled
+ */
+
+/**
+ * Communicates with v2 of the Bitfinex WebSocket API
+ *
+ * @class
+ */
+class WSv2 extends EventEmitter {
+  /**
+   * Instantiate a new ws2 transport. Does not auto-open
+   *
+   * @class WSv2
+   * @param {object} [opts] - instance options
+   * @param {string} [opts.affCode] - affiliate code to be applied to all orders
+   * @param {string} [opts.apiKey] - API key
+   * @param {string} [opts.apiSecret] - API secret
+   * @param {string} [opts.url] - ws connection url, defaults to {@link WSv2#url}
+   * @param {number} [opts.orderOpBufferDelay] - multi-order op batching timeout
+   * @param {boolean} [opts.transform] - if true, packets are converted to models
+   * @param {object} [opts.agent] - optional node agent for ws connection (proxy)
+   * @param {boolean} [opts.manageOrderBooks] - enable local OB persistence
+   * @param {boolean} [opts.manageCandles] - enable local candle persistence
+   * @param {boolean} [opts.seqAudit] - enable sequence numbers & verification
+   * @param {boolean} [opts.autoReconnect] - if true, we will reconnect on close
+   * @param {number} [opts.reconnectDelay] - optional, defaults to 1000 (ms)
+   * @param {PromiseThrottle} [opts.reconnectThrottler] - optional pt to limit reconnect freq
+   * @param {number} [opts.packetWDDelay] - watch-dog forced reconnection delay
+   * @example
+   *   const ws = new WSv2()
+   *
+   *   ws.on('open', async () => {
+   *     ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+   *       console.log('recv trades: %j', trades)
+   *     })
+   *
+   *     await ws.subscribeTrades('tBTCUSD')
+   *   })
+   *
+   *   await ws.open()
+   */
+  constructor (opts = {
+    apiKey: '',
+    apiSecret: '',
+    url: WSv2.url,
+    affCode: null
+  }) {
+    super()
+
+    this.setMaxListeners(1000)
+    this._affCode = opts.affCode
+    this._agent = opts.agent
+    this._url = opts.url || WSv2.url
+    this._transform = opts.transform === true
+    this._orderOpBufferDelay = opts.orderOpBufferDelay || -1
+    this._orderOpBuffer = []
+    this._orderOpTimeout = null
+    this._seqAudit = opts.seqAudit === true
+    this._autoReconnect = opts.autoReconnect === true
+    this._reconnectDelay = opts.reconnectDelay || 1000
+    this._reconnectThrottler = opts.reconnectThrottler
+    this._manageOrderBooks = opts.manageOrderBooks === true
+    this._manageCandles = opts.manageCandles === true
+    this._packetWDDelay = opts.packetWDDelay
+    this._packetWDTimeout = null
+    this._packetWDLastTS = 0
+    this._orderBooks = {}
+    this._losslessOrderBooks = {}
+    this._candles = {}
+    this._authArgs = {
+      apiKey: opts.apiKey,
+      apiSecret: opts.apiSecret
+    }
+
+    /**
+     * {
+     *   [groupID]: {
+     *     [eventName]: [{
+     *       modelClass: ..,
+     *       filter: { symbol: 'tBTCUSD' }, // only works w/ serialize
+     *       cb: () => {}
+     *     }]
+     *   }
+     * }
+     *
+     * @private
+     */
+    this._listeners = {}
+    this._infoListeners = {} // { [code]: <listeners> }
+    this._subscriptionRefs = {}
+    this._channelMap = {}
+    this._orderBooks = {}
+    this._enabledFlags = this._seqAudit ? WSv2.flags.SEQ_ALL : 0
+    this._eventCallbacks = new CbQ()
+    this._isAuthenticated = false
+    this._authOnReconnect = false // used for auto-auth on reconnect
+    this._lastPubSeq = -1
+    this._lastAuthSeq = -1
+    this._isOpen = false
+    this._ws = null
+    this._isClosing = false // used to block reconnect on direct close() call
+    this._isReconnecting = false
+
+    this._onWSOpen = this._onWSOpen.bind(this)
+    this._onWSClose = this._onWSClose.bind(this)
+    this._onWSError = this._onWSError.bind(this)
+    this._onWSMessage = this._onWSMessage.bind(this)
+    this._triggerPacketWD = this._triggerPacketWD.bind(this)
+    this._sendCalc = _Throttle(this._sendCalc.bind(this), 1000 / MAX_CALC_OPS)
+  }
+
+  /**
+   * @returns {string} url
+   */
+  getURL () {
+    return this._url
+  }
+
+  /**
+   * @returns {boolean} usesAgent
+   */
+  usesAgent () {
+    return !!this._agent
+  }
+
+  /**
+   * Set `calc` and `dms` values to be used on the next {@link WSv2#auth} call
+   *
+   * @param {object} args - arguments
+   * @param {number} [args.calc] - calc value
+   * @param {number} [args.dms] - dms value, active 4
+   * @param {number} [args.apiKey] API key
+   * @param {number} [args.apiSecret] API secret
+   * @see WSv2#auth
+   */
+  updateAuthArgs (args = {}) {
+    this._authArgs = {
+      ...this._authArgs,
+      ...args
+    }
+  }
+
+  /**
+   * Fetch the current default auth parameters
+   *
+   * @returns {object} authArgs
+   * @see WSv2#updateAuthArgs
+   * @see WSv2#auth
+   */
+  getAuthArgs () {
+    return this._authArgs
+  }
+
+  /**
+   * Get the total number of data channels this instance is currently
+   * subscribed too.
+   *
+   * @returns {number} count
+   * @see WSv2#subscribeTrades
+   * @see WSv2#subscribeTicker
+   * @see WSv2#subscribeCandles
+   * @see WSv2#subscribeOrderBook
+   */
+  getDataChannelCount () {
+    return Object
+      .values(this._channelMap)
+      .filter(c => _includes(DATA_CHANNEL_TYPES, c.channel))
+      .length
+  }
+
+  /**
+   * Check if the instance is subscribed to the specified channel ID
+   *
+   * @param {number} chanId - ID of channel to query
+   * @returns {boolean} isSubscribed
+   */
+  hasChannel (chanId) {
+    return !!this._channelMap[chanId]
+  }
+
+  /**
+   * Check if a channel/identifier pair has been subscribed too
+   *
+   * @param {string} channel - channel type
+   * @param {string} identifier - unique identifier for the reference
+   * @returns {boolean} hasRef
+   * @see WSv2#managedSubscribe
+   */
+  hasSubscriptionRef (channel, identifier) {
+    const key = `${channel}:${identifier}`
+    return !!Object.keys(this._subscriptionRefs).find(ref => ref === key)
+  }
+
+  /**
+   * Fetch the ID of a channel matched by type and channel data filter
+   *
+   * @param {string} type - channel type
+   * @param {object} filter - to be matched against channel data
+   * @returns {number} channelID
+   */
+  getDataChannelId (type, filter) {
+    return Object
+      .keys(this._channelMap)
+      .find(cid => {
+        const c = this._channelMap[cid]
+        const fv = _pick(c, Object.keys(filter))
+        return c.channel === type && _isEqual(fv, filter)
+      })
+  }
+
+  /**
+   * Check if the instance is subscribed to a data channel matching the
+   * specified type and filter.
+   *
+   * @param {string} type - channel type
+   * @param {object} filter - to be matched against channel data
+   * @returns {boolean} hasChannel
+   */
+  hasDataChannel (type, filter) {
+    return !!this.getDataChannelId(type, filter)
+  }
+
+  /**
+   * Opens a connection to the API server. Rejects with an error if a
+   * connection is already open. Resolves on success.
+   *
+   * @returns {Promise} p
+   */
+  async open () {
+    if (this._isOpen || this._ws !== null) {
+      throw new Error('already open')
+    }
+
+    debug('connecting to %s...', this._url)
+
+    this._ws = new WebSocket(this._url, {
+      agent: this._agent
+    })
+
+    this._subscriptionRefs = {}
+    this._candles = {}
+    this._orderBooks = {}
+
+    this._ws.on('message', this._onWSMessage)
+    this._ws.on('error', this._onWSError)
+    this._ws.on('close', this._onWSClose)
+
+    return new Promise((resolve) => {
+      this._ws.on('open', () => {
+        // call manually instead of binding to open event so it fires at the
+        // right time
+        this._onWSOpen()
+
+        if (this._enabledFlags !== 0) {
+          this.sendEnabledFlags()
+        }
+
+        debug('connected')
+        resolve()
+      })
+    })
+  }
+
+  /**
+   * Closes the active connection. If there is none, rejects with a promise.
+   * Resolves on success
+   *
+   * @param {number} code - passed to ws
+   * @param {string} reason - passed to ws
+   * @returns {Promise} p
+   */
+  async close (code, reason) {
+    if (!this._isOpen || this._ws === null) {
+      throw new Error('not open')
+    }
+
+    debug('disconnecting...')
+
+    return new Promise((resolve) => {
+      this._ws.once('close', () => {
+        this._isOpen = false
+        this._ws = null
+
+        debug('disconnected')
+        resolve()
+      })
+
+      if (!this._isClosing) {
+        this._isClosing = true
+        this._ws.close(code, reason)
+      }
+    })
+  }
+
+  /**
+   * Generates & sends an authentication packet to the server; if already
+   * authenticated, rejects with an error, resolves on success.
+   *
+   * If a DMS flag of 4 is provided, all open orders are cancelled when the
+   * connection terminates.
+   *
+   * @param {number?} calc - optional, default is 0
+   * @param {number?} dms - optional dead man switch flag, active 4
+   * @returns {Promise} p
+   */
+  async auth (calc, dms) {
+    this._authOnReconnect = true
+    if (!this._isOpen) {
+      throw new Error('not open')
+    }
+
+    if (this._isAuthenticated) {
+      throw new Error('already authenticated')
+    }
+
+    const authNonce = nonce()
+    const authPayload = `AUTH${authNonce}${authNonce}`
+    const { sig } = genAuthSig(this._authArgs.apiSecret, authPayload)
+    const authArgs = { ...this._authArgs }
+
+    if (_isFinite(calc)) authArgs.calc = calc
+    if (_isFinite(dms)) authArgs.dms = dms
+
+    return new Promise((resolve) => {
+      this.once('auth', () => {
+        debug('authenticated')
+        resolve()
+      })
+
+      this.send({
+        event: 'auth',
+        apiKey: this._authArgs.apiKey,
+        authSig: sig,
+        authPayload,
+        authNonce,
+        ...authArgs
+      })
+    })
+  }
+
+  /**
+   * Utility method to close & re-open the ws connection. Re-authenticates if
+   * previously authenticated
+   *
+   * @returns {Promise} p - resolves on completion
+   */
+  async reconnect () {
+    this._isReconnecting = true
+
+    if (this._ws !== null && this._isOpen) { // did we get a watchdog timeout and need to close the connection?
+      await this.close()
+
+      return new Promise((resolve) => {
+        this.once(this._authOnReconnect ? 'auth' : 'open', resolve)
+      })
+    }
+
+    return this.reconnectAfterClose() // we are already closed, so reopen and re-auth
+  }
+
+  /**
+   * @private
+   */
+  async reconnectAfterClose () {
+    if (!this._isReconnecting || this._ws !== null || this._isOpen) {
+      return this.reconnect()
+    }
+
+    await this.open()
+
+    if (this._authOnReconnect) {
+      await this.auth()
+    }
+  }
+
+  /**
+   * Returns an error if the message has an invalid (out of order) sequence #
+   * The last-seen sequence #s are updated internally.
+   *
+   * @param {Array} msg - incoming message
+   * @returns {Error} err - null if no error or sequencing not enabled
+   * @private
+   */
+  _validateMessageSeq (msg = []) {
+    if (!this._seqAudit) return null
+    if (!Array.isArray(msg)) return null
+    if (msg.length === 0) return null
+
+    // The auth sequence # is the last value in channel 0 non-heartbeat packets.
+    const authSeq = msg[0] === 0 && msg[1] !== 'hb'
+      ? msg[msg.length - 1]
+      : NaN
+
+    // *-req packets don't include public seq numbers
+    if (`${(msg[2] || [])[1] || ''}`.slice(-4) !== '-req') {
+      // All other packets provide a public sequence # as the last value. For chan
+      // 0 packets, these are included as the 2nd to last value
+      const seq = (
+        (msg[0] === 0) &&
+        (msg[1] !== 'hb') &&
+        !(msg[1] === 'n' && ((msg[2] || [])[1] || '').slice(-4) === '-req')
+      )
+        ? msg[msg.length - 2]
+        : msg[msg.length - 1]
+
+      if (!_isFinite(seq)) return null
+
+      if (this._lastPubSeq === -1) { // first pub seq received
+        this._lastPubSeq = seq
+        return null
+      }
+
+      if (seq !== this._lastPubSeq + 1) { // check pub seq
+        return new Error(`invalid pub seq #; last ${this._lastPubSeq}, got ${seq}`)
+      }
+
+      this._lastPubSeq = seq
+    }
+
+    if (!_isFinite(authSeq)) return null
+    if (authSeq === 0) return null // still syncing
+
+    // notifications don't advance seq
+    if (msg[1] === 'n') {
+      return authSeq !== this._lastAuthSeq
+        ? new Error(
+          `invalid auth seq #, expected no advancement but got ${authSeq}`
+        )
+        : null
+    }
+
+    if (authSeq === this._lastAuthSeq) {
+      return new Error(
+        `expected auth seq # advancement but got same seq: ${authSeq}`
+      )
+    }
+
+    // check
+    if (this._lastAuthSeq !== -1 && authSeq !== this._lastAuthSeq + 1) {
+      return new Error(
+        `invalid auth seq #; last ${this._lastAuthSeq}, got ${authSeq}`
+      )
+    }
+
+    this._lastAuthSeq = authSeq
+    return null
+  }
+
+  /**
+   * Trigger the packet watch-dog; called when we haven't seen a new WS packet
+   * for longer than our WD duration (if provided)
+   *
+   * @returns {Promise} p
+   * @private
+   */
+  async _triggerPacketWD () {
+    if (!this._packetWDDelay || !this._isOpen) {
+      return Promise.resolve()
+    }
+
+    debug(
+      'packet delay watchdog triggered [last packet %dms ago]',
+      Date.now() - this._packetWDLastTS
+    )
+
+    this._packetWDTimeout = null
+    return this.reconnect()
+  }
+
+  /**
+   * Reset the packet watch-dog timeout. Should be called on every new WS packet
+   * if the watch-dog is enabled
+   *
+   * @private
+   */
+  _resetPacketWD () {
+    if (!this._packetWDDelay) return
+    if (this._packetWDTimeout !== null) {
+      clearTimeout(this._packetWDTimeout)
+    }
+
+    if (!this._isOpen) return
+
+    this._packetWDTimeout = setTimeout(() => {
+      this._triggerPacketWD().catch((err) => {
+        debug('error triggering packet watchdog: %s', err.message)
+      })
+    }, this._packetWDDelay)
+  }
+
+  /**
+   * Subscribes to previously subscribed channels, used after reconnecting
+   *
+   * @private
+   */
+  resubscribePreviousChannels () {
+    Object.values(this._prevChannelMap).forEach((chan) => {
+      const { channel } = chan
+
+      switch (channel) {
+        case 'ticker': {
+          const { symbol } = chan
+          this.subscribeTicker(symbol)
+          break
+        }
+
+        case 'trades': {
+          const { symbol } = chan
+          this.subscribeTrades(symbol)
+          break
+        }
+
+        case 'book': {
+          const { symbol, len, prec } = chan
+          this.subscribeOrderBook(symbol, prec, len)
+          break
+        }
+
+        case 'candles': {
+          const { key } = chan
+          this.subscribeCandles(key)
+          break
+        }
+
+        default: {
+          debug('unknown previously subscribed channel type: %s', channel)
+        }
+      }
+    })
+  }
+
+  /**
+   * @private
+   */
+  _onWSOpen () {
+    this._isOpen = true
+    this._isReconnecting = false
+    this._packetWDLastTS = Date.now()
+    this._lastAuthSeq = -1
+    this._lastPubSeq = -1
+    this.emit('open')
+
+    if (!_isEmpty(this._prevChannelMap)) {
+      this.resubscribePreviousChannels()
+      this._prevChannelMap = {}
+    }
+
+    debug('connection open')
+  }
+
+  /**
+   * @private
+   */
+  async _onWSClose () {
+    this._isOpen = false
+    this._isAuthenticated = false
+    this._lastAuthSeq = -1
+    this._lastPubSeq = -1
+    this._enabledFlags = 0
+    this._ws = null
+    this._subscriptionRefs = {}
+    this.emit('close')
+
+    debug('connection closed')
+
+    // _isReconnecting = true - if a reconnection has been requested. In that case always call reconnectAfterClose
+    // _isClosing = true - if the user explicitly requested a close
+    // _autoReconnect = true - if the user likes to reconnect automatically
+    if (this._isReconnecting || (this._autoReconnect && !this._isClosing)) {
+      this._prevChannelMap = this._channelMap
+
+      setTimeout(async () => {
+        try {
+          if (this._reconnectThrottler) {
+            await this._reconnectThrottler.add(this.reconnectAfterClose.bind(this))
+          } else {
+            await this.reconnectAfterClose()
+          }
+        } catch (err) {
+          debug('error reconnectAfterClose: %s', err.stack)
+        }
+      }, this._reconnectDelay)
+    }
+
+    this._channelMap = {}
+    this._isClosing = false
+  }
+
+  /**
+   * @param {Error} err - error
+   * @private
+   */
+  _onWSError (err) {
+    this.emit('error', err)
+
+    debug('error: %s', err)
+  }
+
+  /**
+   * @param {Array} arrN - notification in ws array format
+   * @private
+   */
+  _onWSNotification (arrN) {
+    const status = arrN[6]
+    const msg = arrN[7]
+
+    if (!arrN[4]) return
+
+    if (arrN[1] === 'on-req') {
+      const [,, cid] = arrN[4]
+      const k = `order-new-${cid}`
+
+      if (status === 'SUCCESS') {
+        this._eventCallbacks.trigger(k, null, arrN[4])
+      } else {
+        this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4])
+      }
+    } else if (arrN[1] === 'oc-req') {
+      const [id] = arrN[4]
+      const k = `order-cancel-${id}`
+
+      if (status === 'SUCCESS') {
+        this._eventCallbacks.trigger(k, null, arrN[4])
+      } else {
+        this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4])
+      }
+    } else if (arrN[1] === 'ou-req') {
+      const [id] = arrN[4]
+      const k = `order-update-${id}`
+
+      if (status === 'SUCCESS') {
+        this._eventCallbacks.trigger(k, null, arrN[4])
+      } else {
+        this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4])
+      }
+    }
+  }
+
+  /**
+   * @param {string} rawMsg - incoming message JSON
+   * @param {string} flags - flags
+   * @private
+   */
+  _onWSMessage (rawMsg, flags) {
+    debug('recv msg: %s', rawMsg)
+
+    this._packetWDLastTS = Date.now()
+    this._resetPacketWD()
+
+    let msg
+
+    try {
+      msg = JSON.parse(rawMsg)
+    } catch (e) {
+      this.emit('error', `invalid message JSON: ${rawMsg}`)
+      return
+    }
+
+    debug('recv msg: %j', msg)
+
+    if (this._seqAudit) {
+      const seqErr = this._validateMessageSeq(msg)
+
+      if (seqErr !== null) {
+        this.emit('error', seqErr)
+        return
+      }
+    }
+
+    this.emit('message', msg, flags)
+
+    if (Array.isArray(msg)) {
+      this._handleChannelMessage(msg, rawMsg)
+    } else if (msg.event) {
+      this._handleEventMessage(msg)
+    } else {
+      debug('recv unidentified message: %j', msg)
+    }
+  }
+
+  /**
+   * @param {Array} msg - message
+   * @param {string} rawMsg - message JSON
+   * @private
+   */
+  _handleChannelMessage (msg, rawMsg) {
+    const [chanId, type] = msg
+    const channelData = this._channelMap[chanId]
+
+    if (!channelData) {
+      debug('recv msg from unknown channel %d: %j', chanId, msg)
+      return
+    }
+
+    if (msg.length < 2) return
+    if (msg[1] === 'hb') return
+
+    if (channelData.channel === 'book') {
+      if (type === 'cs') {
+        this._handleOBChecksumMessage(msg, channelData)
+      } else {
+        this._handleOBMessage(msg, channelData, rawMsg)
+      }
+    } else if (channelData.channel === 'trades') {
+      this._handleTradeMessage(msg, channelData)
+    } else if (channelData.channel === 'ticker') {
+      this._handleTickerMessage(msg, channelData)
+    } else if (channelData.channel === 'candles') {
+      this._handleCandleMessage(msg, channelData)
+    } else if (channelData.channel === 'status') {
+      this._handleStatusMessage(msg, channelData)
+    } else if (channelData.channel === 'auth') {
+      this._handleAuthMessage(msg, channelData)
+    } else {
+      this._propagateMessageToListeners(msg, channelData)
+      this.emit(channelData.channel, msg)
+    }
+  }
+
+  /**
+   * @param {Array} msg - message
+   * @param {object} chanData - channel definition
+   * @private
+   */
+  _handleOBChecksumMessage (msg, chanData) {
+    this.emit('cs', msg)
+
+    if (!this._manageOrderBooks) {
+      return
+    }
+
+    const { symbol, prec } = chanData
+    const cs = msg[2]
+
+    // NOTE: Checksums are temporarily disabled for funding books, due to
+    //       invalid book sorting on the backend. This change is temporary
+    if (symbol[0] === 't') {
+      const err = this._verifyManagedOBChecksum(symbol, prec, cs)
+
+      if (err) {
+        this.emit('error', err)
+        return
+      }
+    }
+
+    const internalMessage = [chanData.chanId, 'ob_checksum', cs]
+    internalMessage.filterOverride = [
+      chanData.symbol,
+      chanData.prec,
+      chanData.len
+    ]
+
+    this._propagateMessageToListeners(internalMessage, false)
+    this.emit('cs', symbol, cs)
+  }
+
+  /**
+   * Called for messages from the 'book' channel. Might be an update or a
+   * snapshot
+   *
+   * @param {Array|Array[]} msg - message
+   * @param {object} chanData - entry from _channelMap
+   * @param {string} rawMsg - message JSON
+   * @private
+   */
+  _handleOBMessage (msg, chanData, rawMsg) {
+    const { symbol, prec } = chanData
+    const raw = prec === 'R0'
+    let data = getMessagePayload(msg)
+
+    if (this._manageOrderBooks) {
+      const err = this._updateManagedOB(symbol, data, raw, rawMsg)
+
+      if (err) {
+        this.emit('error', err)
+        return
+      }
+
+      data = this._orderBooks[symbol]
+    }
+
+    // Always transform an array of entries
+    if (this._transform) {
+      data = new OrderBook((Array.isArray(data[0]) ? data : [data]), raw)
+    }
+
+    const internalMessage = [chanData.chanId, 'orderbook', data]
+    internalMessage.filterOverride = [
+      chanData.symbol,
+      chanData.prec,
+      chanData.len
+    ]
+
+    this._propagateMessageToListeners(internalMessage, chanData, false)
+    this.emit('orderbook', symbol, data)
+  }
+
+  /**
+   * @param {string} symbol - symbol for order book
+   * @param {number[]|number[][]} data - incoming data
+   * @param {boolean} raw - if true, the order book is considered R*
+   * @param {string} rawMsg - source message JSON
+   * @returns {Error} err - null on success
+   * @private
+   */
+  _updateManagedOB (symbol, data, raw, rawMsg) {
+    // parse raw string with lossless parse which takes
+    // the exact strict values rather than converting to floats
+    // [0.00001, [1, 2, 3]] -> ['0.00001', ['1', '2', '3']]
+    const rawLossless = LosslessJSON.parse(rawMsg, (key, value) => {
+      if (value && value.isLosslessNumber) {
+        return value.toString()
+      } else {
+        return value
+      }
+    })
+    const losslessUpdate = rawLossless[1]
+    // Snapshot, new OB. Note that we don't protect against duplicates, as they
+    // could come in on re-sub
+    if (Array.isArray(data[0])) {
+      this._orderBooks[symbol] = data
+      this._losslessOrderBooks[symbol] = losslessUpdate
+      return null
+    }
+
+    // entry, needs to be applied to OB
+    if (!this._orderBooks[symbol]) {
+      return new Error(`recv update for unknown OB: ${symbol}`)
+    }
+
+    OrderBook.updateArrayOBWith(this._orderBooks[symbol], data, raw)
+    OrderBook.updateArrayOBWith(this._losslessOrderBooks[symbol], losslessUpdate, raw)
+    return null
+  }
+
+  /**
+   * @param {string} symbol - symbol for order book
+   * @param {string} prec - precision
+   * @param {number} cs - expected checksum
+   * @returns {Error} err - null if none
+   * @private
+   */
+  _verifyManagedOBChecksum (symbol, prec, cs) {
+    const ob = this._losslessOrderBooks[symbol]
+
+    if (!ob) return null
+
+    const localCS = ob instanceof OrderBook
+      ? ob.checksum()
+      : OrderBook.checksumArr(ob, prec === 'R0')
+
+    return localCS !== cs
+      ? new Error(`OB checksum mismatch: got ${localCS}, want ${cs}`)
+      : null
+  }
+
+  /**
+   * Returns an up-to-date copy of the order book for the specified symbol, or
+   * null if no OB is managed for that symbol.
+   *
+   * Set `managedOrderBooks: true` in the constructor to use.
+   *
+   * @param {string} symbol - symbol for order book
+   * @returns {OrderBook} ob - null if not found
+   * @example
+   *   const ws = new WSv2({ managedOrderBooks: true })
+   *
+   *   ws.on('open', async () => {
+   *       ws.onOrderBook({ symbol: 'tBTCUSD' }, () => {
+   *         const book = ws.getOB('tBTCUSD')
+   *
+   *         if (!book) return
+   *
+   *         const spread = book.midPrice()
+   *         console.log('spread for tBTCUSD: %f', spread)
+   *       })
+   *
+   *       ws.subscribeOrderBook({ symbol: 'tBTCUSD' })
+   *   })
+   *
+   *   await ws.open()
+   */
+  getOB (symbol) {
+    if (!this._orderBooks[symbol]) return null
+
+    return new OrderBook(this._orderBooks[symbol])
+  }
+
+  /**
+   * Returns an up-to-date lossless copy of the order book for the specified symbol, or
+   * null if no OB is managed for that symbol. All amounts and prices are in original
+   * string format.
+   *
+   * Set `manageOrderBooks: true` in the constructor to use.
+   *
+   * @param {string} symbol - symbol for order book
+   * @returns {OrderBook} ob - null if not found
+   */
+  getLosslessOB (symbol) {
+    if (!this._losslessOrderBooks[symbol]) return null
+
+    return new OrderBook(this._losslessOrderBooks[symbol])
+  }
+
+  /**
+   * @param {Array} msg - incoming message
+   * @param {object} chanData - channel definition
+   * @private
+   */
+  _handleTradeMessage (msg, chanData) {
+    const eventName = msg[1][0] === 'f'
+      ? msg[1] // Funding trades are passed to fte/ftu handlers
+      : msg[1] === 'te'
+        ? 'trade-entry'
+        : 'trades'
+
+    let payload = getMessagePayload(msg)
+
+    if (!Array.isArray(payload[0])) {
+      payload = [payload]
+    }
+
+    let data = payload
+
+    if (this._transform) { // correctly parse single trade/array of trades
+      const M = eventName[0] === 'f' && msg[2].length === 8 ? FundingTrade : PublicTrade
+      const trades = M.unserialize(data)
+
+      if (_isArray(trades) && trades.length === 1) {
+        data = trades[0]
+      } else {
+        data = trades
+      }
+
+      data = new M(data)
+    }
+
+    const internalMessage = [chanData.chanId, eventName, data]
+    internalMessage.filterOverride = [chanData.symbol || chanData.pair]
+
+    this._propagateMessageToListeners(internalMessage, chanData, false)
+    this.emit('trades', chanData.symbol || chanData.pair, data)
+  }
+
+  /**
+   * @param {Array} msg - incoming message
+   * @param {object} chanData - channel definition
+   * @private
+   */
+  _handleTickerMessage (msg = [], chanData = {}) {
+    let data = getMessagePayload(msg)
+
+    if (this._transform) {
+      data = (chanData.symbol || '')[0] === 't'
+        ? new TradingTicker([chanData.symbol, ...msg[1]])
+        : new FundingTicker([chanData.symbol, ...msg[1]])
+    }
+
+    const internalMessage = [chanData.chanId, 'ticker', data]
+    internalMessage.filterOverride = [chanData.symbol]
+
+    this._propagateMessageToListeners(internalMessage, chanData, false)
+    this.emit('ticker', chanData.symbol, data)
+  }
+
+  /**
+   * Called for messages from a 'candles' channel. Might be an update or
+   * snapshot.
+   *
+   * @param {Array|Array[]} msg - incoming message
+   * @param {object} chanData - entry from _channelMap
+   * @private
+   */
+  _handleCandleMessage (msg, chanData) {
+    const { key } = chanData
+    let data = getMessagePayload(msg)
+
+    if (this._manageCandles) {
+      const err = this._updateManagedCandles(key, data)
+
+      if (err) {
+        this.emit('error', err)
+        return
+      }
+
+      data = this._candles[key]
+    } else if (data.length > 0 && !Array.isArray(data[0])) {
+      data = [data] // always pass on an array of candles
+    }
+
+    if (this._transform) {
+      data = Candle.unserialize(data)
+    }
+
+    const internalMessage = [chanData.chanId, 'candle', data]
+    internalMessage.filterOverride = [chanData.key]
+
+    this._propagateMessageToListeners(internalMessage, chanData, false)
+    this.emit('candle', data, key)
+  }
+
+  /**
+   * Called for messages from a 'status' channel.
+   *
+   * @param {Array|Array[]} msg - incoming message
+   * @param {object} chanData - entry from _channelMap
+   * @private
+   */
+  _handleStatusMessage (msg, chanData) {
+    const { key } = chanData
+    const data = getMessagePayload(msg)
+
+    const internalMessage = [chanData.chanId, 'status', data]
+    internalMessage.filterOverride = [chanData.key]
+
+    this._propagateMessageToListeners(internalMessage, chanData, false)
+    this.emit('status', data, key)
+  }
+
+  /**
+   * @param {string} key - key for candle set
+   * @param {number[]|number[][]} data - incoming dataset (single or multiple)
+   * @returns {Error} err - null on success
+   * @private
+   */
+  _updateManagedCandles (key, data) {
+    if (Array.isArray(data[0])) { // snapshot, new candles
+      data.sort((a, b) => b[0] - a[0])
+
+      this._candles[key] = data
+      return null
+    }
+
+    // entry, needs to be applied to candle set
+    if (!this._candles[key]) {
+      return new Error(`recv update for unknown candles: ${key}`)
+    }
+
+    const candles = this._candles[key]
+    let updated = false
+
+    for (let i = 0; i < candles.length; i++) {
+      if (data[0] === candles[i][0]) {
+        candles[i] = data
+        updated = true
+        break
+      }
+    }
+
+    if (!updated) {
+      candles.unshift(data)
+    }
+
+    return null
+  }
+
+  /**
+   * Fetch a reference to the full set of synced candles for the specified key.
+   * Set `managedCandles: true` in the constructor to use.
+   *
+   * @param {string} key - key for candle set
+   * @returns {Array} candles - empty array if none exist
+   * @example
+   *   const ws = new WSv2({ managedCandles: true })
+   *
+   *   ws.on('open', async () => {
+   *       ws.onCandles({ key: 'trade:1m:tBTCUSD' }, () => {
+   *         const candles = ws.getCandles('trade:1m:tBTCUSD')
+   *
+   *         if (!candles) return
+   *
+   *         console.log('%d candles in dataset', candles.length)
+   *       })
+   *
+   *       ws.subscribeCandles({ key: 'trade:1m:tBTCUSD' })
+   *   })
+   *
+   *   await ws.open()
+   */
+  getCandles (key) {
+    return this._candles[key] || []
+  }
+
+  /**
+   * @param {Array} msg - incoming message
+   * @param {object} chanData - channel data
+   * @private
+   */
+  _handleAuthMessage (msg, chanData) {
+    if (msg[1] === 'n') {
+      const payload = getMessagePayload(msg)
+
+      if (payload) {
+        this._onWSNotification(payload)
+      }
+    } else if (msg[1] === 'te') {
+      msg[1] = 'auth-te'
+    } else if (msg[1] === 'tu') {
+      msg[1] = 'auth-tu'
+    }
+
+    this._propagateMessageToListeners(msg, chanData)
+  }
+
+  /**
+   * @param {Array} msg - incoming message
+   * @param {object} chan - channel data
+   * @param {boolean} transform - defaults to internal flag
+   * @private
+   */
+  _propagateMessageToListeners (msg, chan, transform = this._transform) {
+    const listenerGroups = Object.values(this._listeners)
+
+    for (let i = 0; i < listenerGroups.length; i++) {
+      WSv2._notifyListenerGroup(listenerGroups[i], msg, transform, this, chan)
+    }
+  }
+
+  /**
+   * Applies filtering & transform to a packet before sending it out to matching
+   * listeners in the group.
+   *
+   * @param {object} lGroup - listener group to parse & notify
+   * @param {object} msg - passed to each matched listener
+   * @param {boolean} transform - whether or not to instantiate a model
+   * @param {WSv2} ws - instance to pass to models if transforming
+   * @param {object} chanData - channel data
+   * @private
+   */
+  static _notifyListenerGroup (lGroup, msg, transform, ws, chanData) {
+    const [, eventName, data = []] = msg
+    let filterByData
+
+    // Catch-all can't filter/transform
+    WSv2._notifyCatchAllListeners(lGroup, msg)
+
+    if (!lGroup[eventName] || lGroup[eventName].length === 0) return
+
+    const listeners = lGroup[eventName].filter((listener) => {
+      const { filter } = listener
+
+      if (!filter) return true
+
+      // inspect snapshots for matching packets
+      if (Array.isArray(data[0])) {
+        const matchingData = data.filter((item) => {
+          filterByData = msg.filterOverride ? msg.filterOverride : item
+
+          return WSv2._payloadPassesFilter(filterByData, filter)
+        })
+
+        return matchingData.length !== 0
+      }
+
+      // inspect single packet
+      filterByData = msg.filterOverride ? msg.filterOverride : data
+
+      return WSv2._payloadPassesFilter(filterByData, filter)
+    })
+
+    if (listeners.length === 0) return
+
+    listeners.forEach(({ cb, modelClass }) => {
+      const ModelClass = modelClass
+
+      if (!ModelClass || !transform || data.length === 0) {
+        cb(data, chanData)
+      } else if (Array.isArray(data[0])) {
+        cb(data.map((entry) => {
+          return new ModelClass(entry, ws)
+        }), chanData)
+      } else {
+        cb(new ModelClass(data, ws), chanData)
+      }
+    })
+  }
+
+  /**
+   * @param {Array} payload - payload to verify
+   * @param {object} filter - filter to match against payload
+   * @returns {boolean} pass
+   * @private
+   */
+  static _payloadPassesFilter (payload, filter) {
+    const filterIndices = Object.keys(filter)
+    let filterValue
+
+    for (let k = 0; k < filterIndices.length; k++) {
+      filterValue = filter[filterIndices[k]]
+
+      if (_isEmpty(filterValue) || filterValue === '*') {
+        continue
+      }
+
+      if (payload[+filterIndices[k]] !== filterValue) {
+        return false
+      }
+    }
+
+    return true
+  }
+
+  /**
+   * @param {object} lGroup - listener group keyed by event ('' in this case)
+   * @param {*} data - packet to pass to listeners
+   * @private
+   */
+  static _notifyCatchAllListeners (lGroup, data) {
+    if (!lGroup['']) return
+
+    for (let j = 0; j < lGroup[''].length; j++) {
+      lGroup[''][j].cb(data)
+    }
+  }
+
+  /**
+   * @param {object} msg - incoming message
+   * @private
+   */
+  _handleEventMessage (msg) {
+    if (msg.event === 'auth') {
+      this._handleAuthEvent(msg)
+    } else if (msg.event === 'subscribed') {
+      this._handleSubscribedEvent(msg)
+    } else if (msg.event === 'unsubscribed') {
+      this._handleUnsubscribedEvent(msg)
+    } else if (msg.event === 'info') {
+      this._handleInfoEvent(msg)
+    } else if (msg.event === 'conf') {
+      this._handleConfigEvent(msg)
+    } else if (msg.event === 'error') {
+      this._handleErrorEvent(msg)
+    } else if (msg.event === 'pong') {
+      this._handlePongEvent(msg)
+    } else {
+      debug('recv unknown event message: %j', msg)
+    }
+  }
+
+  /**
+   * Emits an error on config failure, otherwise updates the internal flag set
+   * and triggers any callbacks
+   *
+   * @param {object} msg - config message
+   * @private
+   */
+  _handleConfigEvent (msg = {}) {
+    const { status, flags } = msg
+    const k = this._getConfigEventKey(flags)
+
+    if (status !== 'OK') {
+      const err = new Error(`config failed (${status}) for flags ${flags}`)
+      debug('config failed: %s', err.message)
+
+      this.emit('error', err)
+      this._eventCallbacks.trigger(k, err)
+    } else {
+      debug('flags updated to %d', flags)
+
+      this._enabledFlags = flags
+      this._eventCallbacks.trigger(k, null, msg)
+    }
+  }
+
+  /**
+   * @param {object} msg - incoming message
+   * @private
+   */
+  _handlePongEvent (msg) {
+    debug('pong: %s', JSON.stringify(msg))
+
+    this.emit('pong', msg)
+  }
+
+  /**
+   * @param {object} msg - incoming message
+   * @private
+   */
+  _handleErrorEvent (msg) {
+    debug('error: %s', JSON.stringify(msg))
+
+    this.emit('error', msg)
+  }
+
+  /**
+   * @param {object} data - incoming message
+   * @private
+   */
+  _handleAuthEvent (data = {}) {
+    const { chanId, msg = '', status = '' } = data
+
+    if (status !== 'OK') {
+      const err = new Error(msg.match(/nonce/)
+        ? 'auth failed: nonce small; you may need to generate a new API key to reset the nonce counter'
+        : `auth failed: ${msg} (${status})`
+      )
+
+      debug('%s', err.message)
+      this.emit('error', err)
+      return
+    }
+
+    this._channelMap[chanId] = { channel: 'auth' }
+    this._isAuthenticated = true
+
+    this.emit('auth', data)
+    debug('authenticated!')
+  }
+
+  /**
+   * @param {object} msg - incoming message
+   * @private
+   */
+  _handleSubscribedEvent (msg) {
+    this._channelMap[msg.chanId] = msg
+
+    debug('subscribed to %s [%d]', msg.channel, msg.chanId)
+    this.emit('subscribed', msg)
+  }
+
+  /**
+   * @param {object} msg - incoming message
+   * @private
+   */
+  _handleUnsubscribedEvent (msg) {
+    delete this._channelMap[msg.chanId]
+    debug('unsubscribed from %d', msg.chanId)
+    this.emit('unsubscribed', msg)
+  }
+
+  /**
+   * @param {object} msg - incoming message
+   * @private
+   */
+  _handleInfoEvent (msg = {}) {
+    const { version, code } = msg
+
+    if (version) {
+      if (version !== 2) {
+        const err = new Error(`server not running API v2: v${version}`)
+
+        this.emit('error', err)
+        this.close().catch((err) => {
+          debug('error closing connection: %s', err.stack)
+        })
+        return
+      }
+
+      const { status } = msg.platform || {}
+
+      debug(
+        'server running API v2 (platform: %s (%d))',
+        status === 0 ? 'under maintenance' : 'operating normally', status
+      )
+    } else if (code) {
+      if (this._infoListeners[code]) {
+        this._infoListeners[code].forEach(cb => cb(msg))
+      }
+
+      if (code === WSv2.info.SERVER_RESTART) {
+        debug('server restarted, please reconnect')
+      } else if (code === WSv2.info.MAINTENANCE_START) {
+        debug('server maintenance period started!')
+      } else if (code === WSv2.info.MAINTENANCE_END) {
+        debug('server maintenance period ended!')
+      }
+    }
+
+    this.emit('info', msg)
+  }
+
+  /**
+   * Subscribes and tracks subscriptions per channel/identifier pair. If
+   * already subscribed to the specified pair, nothing happens.
+   *
+   * @param {string} channel - channel name
+   * @param {string} identifier - for uniquely identifying the ref count
+   * @param {object} payload - merged with sub packet
+   * @returns {boolean} subSent
+   * @todo will be refactored to return promise from subscribe() call instead
+   *   of sub action taken flag
+   * @see WSv2#subscribeTrades
+   * @see WSv2#subscribeTicker
+   * @see WSv2#subscribeCandles
+   * @see WSv2#subscribeOrderBook
+   * @example
+   * const ws = new WSv2()
+   *
+   * ws.on('open', async () => {
+   *   ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+   *     console.log('recv trades: %j', trades)
+   *  })
+   *
+   *   ws.managedSubscribe('trades', 'tBTCUSD', { symbol: 'tBTCUSD' })
+   * })
+   *
+   * await ws.open()
+   */
+  managedSubscribe (channel = '', identifier = '', payload = {}) {
+    const key = `${channel}:${identifier}`
+
+    if (this._subscriptionRefs[key]) {
+      this._subscriptionRefs[key]++
+      return false
+    }
+
+    this._subscriptionRefs[key] = 1
+    this.subscribe(channel, payload)
+
+    return true
+  }
+
+  /**
+   * Decreases the subscription ref count for the channel/identifier pair, and
+   * unsubscribes from the channel if it reaches 0.
+   *
+   * @param {string} channel - channel name
+   * @param {string} identifier - for uniquely identifying the ref count
+   * @returns {boolean} unsubSent
+   */
+  managedUnsubscribe (channel = '', identifier = '') {
+    const key = `${channel}:${identifier}`
+    const chanId = this._chanIdByIdentifier(channel, identifier)
+
+    if (chanId === null || isNaN(this._subscriptionRefs[key])) return false
+
+    this._subscriptionRefs[key]--
+    if (this._subscriptionRefs[key] > 0) return false
+
+    this.unsubscribe(chanId)
+    delete this._subscriptionRefs[key]
+
+    return true
+  }
+
+  /**
+   * Fetch a channel definition
+   *
+   * @param {object} opts - options
+   * @param {number} opts.chanId - channel ID
+   * @param {string} opts.channel - channel name
+   * @param {string} [opts.symbol] - match by symbol
+   * @param {string} [opts.key] - match by key (for candle channels)
+   * @returns {object} chanData - null if not found
+   */
+  getChannelData ({ chanId, channel, symbol, key }) {
+    const id = chanId || this._chanIdByIdentifier(channel, symbol || key)
+
+    return this._channelMap[id] || null
+  }
+
+  /**
+   * @param {string} channel - channel name
+   * @param {string} identifier - unique identifier for the channel
+   * @returns {number} channelID
+   * @private
+   */
+  _chanIdByIdentifier (channel, identifier) {
+    const channelIds = Object.keys(this._channelMap)
+    let chan
+
+    for (let i = 0; i < channelIds.length; i++) {
+      chan = this._channelMap[channelIds[i]]
+
+      if (chan.channel === channel && (
+        chan.symbol === identifier ||
+        chan.key === identifier
+      )) {
+        return channelIds[i]
+      }
+    }
+
+    return null
+  }
+
+  /**
+   * @param {string} key - key for the promise
+   * @returns {Promise} p - resolves on event
+   * @private
+   */
+  _getEventPromise (key) {
+    return new Promise((resolve, reject) => {
+      this._eventCallbacks.push(key, (err, res) => {
+        if (err) {
+          return reject(err)
+        }
+
+        resolve(res)
+      })
+    })
+  }
+
+  /**
+   * Send a packet to the WS server
+   *
+   * @param {*} msg - packet, gets stringified
+   */
+  send (msg) {
+    if (!this._ws || !this._isOpen) {
+      this.emit('error', new Error('no ws client or not open'))
+    } else if (this._isClosing) {
+      this.emit('error', new Error('connection currently closing'))
+    } else {
+      debug('sending %j', msg)
+      this._ws.send(JSON.stringify(msg))
+    }
+  }
+
+  /**
+   * @returns {boolean} sequencingEnabled
+   */
+  sequencingEnabled () {
+    return this._seqAudit
+  }
+
+  /**
+   * Configures the seq flag to enable sequencing (packet number) for this
+   * connection. When enabled, the seq number will be the last value of
+   * channel packet arrays.
+   *
+   * @param {object} args - params
+   * @param {boolean} [args.audit] - if true, an error is emitted on invalid seq
+   * @returns {Promise} p
+   * @see WSv2#flags
+   */
+  async enableSequencing (args = { audit: true }) {
+    this._seqAudit = args.audit === true
+
+    return this.enableFlag(WSv2.flags.SEQ_ALL)
+  }
+
+  /**
+   * Enables a configuration flag.
+   *
+   * @param {number} flag - flag to update, as numeric value
+   * @returns {Promise} p
+   * @see WSv2#flags
+   * @example
+   * const ws = new WSv2()
+   *
+   * ws.on('open', async () => {
+   *   await ws.enableFlag(WSv2.flags.CHECKSUM)
+   *   console.log('ob checkums enabled')
+   * })
+   *
+   * await ws.open()
+   */
+  async enableFlag (flag) {
+    this._enabledFlags = this._enabledFlags | flag
+
+    if (!this._isOpen) {
+      return
+    }
+
+    this.sendEnabledFlags()
+    return this._getEventPromise(this._getConfigEventKey(flag))
+  }
+
+  /**
+   * Sends the local flags value to the server, updating the config
+   *
+   * @private
+   */
+  sendEnabledFlags () {
+    this.send({
+      event: 'conf',
+      flags: this._enabledFlags
+    })
+  }
+
+  /**
+   * Checks local state, relies on successful server config responses
+   *
+   * @see enableFlag
+   *
+   * @param {number} flag - flag to check for
+   * @returns {boolean} enabled
+   */
+  isFlagEnabled (flag) {
+    return (this._enabledFlags & flag) === flag
+  }
+
+  /**
+   * @param {string} flag - flag to fetch event key for
+   * @returns {string} key
+   * @private
+   */
+  _getConfigEventKey (flag) {
+    return `conf-res-${flag}`
+  }
+
+  /**
+   * Register a callback in case of a ws server restart message; Use this to
+   * call reconnect() if needed. (code 20051)
+   *
+   * @param {Function} cb - called on event trigger
+   */
+  onServerRestart (cb) {
+    this.onInfoMessage(WSv2.info.SERVER_RESTART, cb)
+  }
+
+  /**
+   * Register a callback in case of a 'maintenance started' message from the
+   * server. This is a good time to pause server packets until maintenance ends
+   *
+   * @param {Function} cb - called on event trigger
+   */
+  onMaintenanceStart (cb) {
+    this.onInfoMessage(WSv2.info.MAINTENANCE_START, cb)
+  }
+
+  /**
+   * Register a callback to be notified of a maintenance period ending
+   *
+   * @param {Function} cb - called on event trigger
+   */
+  onMaintenanceEnd (cb) {
+    this.onInfoMessage(WSv2.info.MAINTENANCE_END, cb)
+  }
+
+  /**
+   * Subscribe to a channel with the given filter payload
+   *
+   * @param {string} channel - channel payload/data
+   * @param {object} payload - optional extra packet data
+   * @example
+   * const ws = new WSv2()
+   *
+   * ws.on('open', () => {
+   *   ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => {
+   *     // ...
+   *   })
+   *
+   *   ws.subscribe('trades', { symbol: 'tBTCUSD' })
+   * })
+   *
+   * await ws.open()
+   */
+  subscribe (channel, payload) {
+    this.send(Object.assign({
+      event: 'subscribe',
+      channel
+    }, payload))
+  }
+
+  /**
+   * Subscribe to a ticker data channel
+   *
+   * @param {string} symbol - symbol of ticker
+   * @returns {boolean} subscribed
+   * @see WSv2#managedSubscribe
+   * @example
+   * await ws.subscribeTicker('tBTCUSD')
+   */
+  async subscribeTicker (symbol) {
+    return this.managedSubscribe('ticker', symbol, { symbol })
+  }
+
+  /**
+   * Subscribe to a trades data channel
+   *
+   * @param {string} symbol - symbol of market to monitor
+   * @returns {boolean} subscribed
+   * @see WSv2#managedSubscribe
+   * @example
+   * await ws.subscribeTrades('tBTCUSD')
+   */
+  async subscribeTrades (symbol) {
+    return this.managedSubscribe('trades', symbol, { symbol })
+  }
+
+  /**
+   * Subscribe to an order book data channel
+   *
+   * @param {string} symbol - symbol of order book
+   * @param {string} prec - P0, P1, P2, or P3 (default P0)
+   * @param {string} len - 25 or 100 (default 25)
+   * @returns {boolean} subscribed
+   * @see WSv2#managedSubscribe
+   * @example
+   * await ws.subscribeOrderBook('tBTCUSD', 'R0', '25')
+   */
+  async subscribeOrderBook (symbol, prec = 'P0', len = '25') {
+    return this.managedSubscribe('book', symbol, { symbol, len, prec })
+  }
+
+  /**
+   * Subscribe to a candle data channel
+   *
+   * @param {string} key - 'trade:5m:tBTCUSD'
+   * @returns {boolean} subscribed
+   * @see WSv2#managedSubscribe
+   * @example
+   * await ws.subscribeCandles('trade:5m:tBTCUSD')
+   */
+  async subscribeCandles (key) {
+    return this.managedSubscribe('candles', key, { key })
+  }
+
+  /**
+   * Subscribe to a status data channel
+   *
+   * @param {string} key - i.e. 'liq:global'
+   * @returns {boolean} subscribed
+   * @see WSv2#managedSubscribe
+   * @example
+   * await ws.subscribeStatus('liq:global')
+   */
+  async subscribeStatus (key) {
+    return this.managedSubscribe('status', key, { key })
+  }
+
+  /**
+   * Unsubscribe from a channel by ID
+   *
+   * @param {number} chanId - ID of channel to unsubscribe from
+   * @example
+   * const id = ws.getDataChannelId('ticker', { symbol: 'tBTCUSD' })
+   *
+   * if (id) {
+   *   ws.unsubscribe(id)
+   * }
+   */
+  unsubscribe (chanId) {
+    this.send({
+      event: 'unsubscribe',
+      chanId: +chanId
+    })
+  }
+
+  /**
+   * Unsubscribe from a ticker data channel
+   *
+   * @param {string} symbol - symbol of ticker
+   * @returns {boolean} unsubscribed
+   * @see WSv2#subscribeTicker
+   * @example
+   * await ws.unsubscribeTicker('tBTCUSD')
+   */
+  async unsubscribeTicker (symbol) {
+    return this.managedUnsubscribe('ticker', symbol)
+  }
+
+  /**
+   * Unsubscribe from a trades data channel
+   *
+   * @param {string} symbol - symbol of market to unsubscribe from
+   * @returns {boolean} unsubscribed
+   * @see WSv2#subscribeTrades
+   * @example
+   * await ws.unsubcribeTrades('tBTCUSD')
+   */
+  async unsubscribeTrades (symbol) {
+    return this.managedUnsubscribe('trades', symbol)
+  }
+
+  /**
+   * Unsubscribe from an order book data channel
+   *
+   * @param {string} symbol - symbol of order book
+   * @returns {boolean} unsubscribed
+   * @see WSv2#subscribeOrderBook
+   * @example
+   * await ws.unsubcribeOrderBook('tBTCUSD')
+   */
+  async unsubscribeOrderBook (symbol) {
+    return this.managedUnsubscribe('book', symbol)
+  }
+
+  /**
+   * @param {string} symbol - symbol of candles
+   * @param {string} frame - time frame
+   * @returns {boolean} unsubscribed
+   * @see WSv2#subscribeCandles
+   * @example
+   *   await ws.unsubscribeCandles('tBTCUSD', '1m')
+   */
+  async unsubscribeCandles (symbol, frame) {
+    return this.managedUnsubscribe('candles', `trade:${frame}:${symbol}`)
+  }
+
+  /**
+   * @param {string} key - key that was used in initial {@link WSv2#subscribeStatus} call
+   * @returns {boolean} unsubscribed
+   * @see WSv2#subscribeStatus
+   */
+  async unsubscribeStatus (key) {
+    return this.managedUnsubscribe('status', key)
+  }
+
+  /**
+   * Remove all listeners by callback group ID
+   *
+   * @param {string} cbGID - callback group to remove
+   * @example
+   * await ws.subscribeTrades({ symbol: 'tBTCUSD', cbGID: 42 })
+   * await ws.subscribeTrades({ symbol: 'tLEOUSD', cbGID: 42 })
+   * await ws.subscribeTrades({ symbol: 'tETHUSD', cbGID: 42 })
+   *
+   * // ...
+   *
+   * ws.removeListeners(42)
+   */
+  removeListeners (cbGID) {
+    delete this._listeners[cbGID]
+  }
+
+  /**
+   * Request a calc operation to be performed on the specified indexes
+   *
+   * @param {string[]} prefixes - desired prefixes to be calculated
+   */
+  requestCalc (prefixes) {
+    this._sendCalc([0, 'calc', null, prefixes.map(p => [p])])
+  }
+
+  /**
+   * Throttled call to ws.send, max 8 op/s
+   *
+   * @param {Array} msg - message
+   * @private
+   */
+  _sendCalc (msg) {
+    debug('req calc: %j', msg)
+
+    this._ws.send(JSON.stringify(msg))
+  }
+
+  /**
+   * Sends a new order to the server and resolves the returned promise once the
+   * order submit is confirmed. Emits an error if not authenticated. The order
+   * can be either an array, key/value map, or Order object instance.
+   *
+   * @see WSv2#cancelOrders
+   * @see WSv2#cancelOrder
+   * @see WSv2#updateOrder
+   *
+   * @param {object|Array} params - parameters or order object model or array
+   * @param {object|Array} params.order - order object model or array
+   * @returns {Promise} p - resolves on submit notification
+   * @example
+   * const o = new Order({
+   *   type: Order.type.EXCHANGE_LIMIT,
+   *   amount: 18,
+   *   price: 0.75,
+   *   symbol: 'tBTCUSD',
+   *   hidden: true
+   * }, ws)
+   *
+   * await ws.submitOrder(o)
+   *
+   * console.log('order confirmed! status: %s', o.status)
+   */
+  async submitOrder (params) {
+    if (!this._isAuthenticated) {
+      throw new Error('not authenticated')
+    }
+
+    const order = params?.order ?? params
+    const packet = Array.isArray(order)
+      ? order
+      : order instanceof Order
+        ? order.toNewOrderPacket()
+        : new Order(order).toNewOrderPacket()
+
+    if (this._affCode) {
+      if (!packet.meta) {
+        packet.meta = {}
+      }
+
+      packet.meta.aff_code = packet.meta.aff_code || this._affCode // eslint-disable-line
+    }
+
+    this._sendOrderPacket([0, 'on', null, packet])
+
+    return this._getEventPromise(`order-new-${packet.cid}`)
+  }
+
+  /**
+   * Send a changeset to update an order in-place while maintaining position in
+   * the price queue. The changeset must contain the order ID, and supports a
+   * 'delta' key to increase/decrease the total amount.
+   *
+   * @see WSv2#submitOrder
+   *
+   * @param {object} changes - requires at least an 'id'
+   * @returns {Promise} p - resolves on receiving an confirmation notification
+   */
+  async updateOrder (changes = {}) {
+    const { id } = changes
+
+    if (!this._isAuthenticated) {
+      throw new Error('not authenticated')
+    } else if (!id) {
+      throw new Error('order ID required for update')
+    }
+
+    this._sendOrderPacket([0, 'ou', null, changes])
+
+    return this._getEventPromise(`order-update-${id}`)
+  }
+
+  /**
+   * Cancels an order by ID and resolves the returned promise once the cancel is
+   * confirmed. Emits an error if not authenticated. The ID can be passed as a
+   * number, or taken from an order array/object.
+   *
+   * @see WSv2#submitOrder
+   *
+   * @param {object|Array|number} order - order model, array, or ID to be cancelled
+   * @returns {Promise} p
+   */
+  async cancelOrder (order) {
+    if (!this._isAuthenticated) {
+      throw new Error('not authenticated')
+    }
+
+    const id = _isNumber(order)
+      ? order
+      : Array.isArray(order)
+        ? order[0]
+        : order.id
+
+    debug(`cancelling order ${id}`)
+    this._sendOrderPacket([0, 'oc', null, { id }])
+
+    return this._getEventPromise(`order-cancel-${id}`)
+  }
+
+  /**
+   * Cancels multiple orders, returns a promise that resolves once all
+   * operations are confirmed.
+   *
+   * @see WSv2#submitOrder
+   *
+   * @param {object|object[]|Array[]|number[]} params - parameters or array of order models, arrays
+   * @param {object[]|Array[]|number[]} params.ids - array of order models, arrays
+   *   or IDs to be cancelled
+   * @returns {Promise} p
+   */
+  async cancelOrders (params) {
+    if (!this._isAuthenticated) {
+      throw new Error('not authenticated')
+    }
+
+    const orders = params?.ids ?? params
+
+    return Promise.all(orders.map(o => {
+      return this.cancelOrder(o)
+    }))
+  }
+
+  /**
+   * Sends the op payloads to the server as an 'ox_multi' command. A promise is
+   * returned and resolves immediately if authenticated, as no confirmation is
+   * available for this message type.
+   *
+   * @param {object[]} opPayloads - order operations
+   * @returns {Promise} p - rejects if not authenticated
+   */
+  async submitOrderMultiOp (opPayloads) {
+    if (!this._isAuthenticated) {
+      throw new Error('not authenticated')
+    }
+
+    // TODO: multi-op tracking
+    this.send([0, 'ox_multi', null, opPayloads])
+  }
+
+  /**
+   * @param {Array} packet - new order packet to be submitted
+   * @private
+   */
+  _sendOrderPacket (packet) {
+    if (this._hasOrderBuff()) {
+      this._ensureOrderBuffTimeout()
+      this._orderOpBuffer.push(packet)
+    } else {
+      this.send(packet)
+    }
+  }
+
+  /**
+   * @returns {boolean} buffEnabled
+   * @private
+   */
+  _hasOrderBuff () {
+    return this._orderOpBufferDelay > 0
+  }
+
+  /**
+   * @private
+   */
+  _ensureOrderBuffTimeout () {
+    if (this._orderOpTimeout !== null) return
+
+    this._orderOpTimeout = setTimeout(
+      this._flushOrderOps.bind(this),
+      this._orderOpBufferDelay
+    )
+  }
+
+  /**
+   * Splits the op buffer into packets of max 15 ops each, and sends them down
+   * the wire.
+   *
+   * @returns {Promise} p - resolves after send
+   * @private
+   */
+  _flushOrderOps () {
+    this._orderOpTimeout = null
+
+    const packets = this._orderOpBuffer.map(p => [p[1], p[3]])
+    this._orderOpBuffer = []
+
+    if (packets.length <= 15) {
+      return this.submitOrderMultiOp(packets)
+    }
+
+    const promises = []
+
+    while (packets.length > 0) {
+      const opPackets = packets.splice(0, Math.min(packets.length, 15))
+      promises.push(this.submitOrderMultiOp(opPackets))
+    }
+
+    return Promise.all(promises)
+  }
+
+  /**
+   * @returns {boolean} authenticated
+   */
+  isAuthenticated () {
+    return this._isAuthenticated
+  }
+
+  /**
+   * @returns {boolean} open
+   */
+  isOpen () {
+    return this._isOpen
+  }
+
+  /**
+   * @returns {boolean} reconnecting
+   */
+  isReconnecting () {
+    return this._isReconnecting
+  }
+
+  /**
+   * Sends a broadcast notification, which will be received by any active UI
+   * websocket connections (at bitfinex.com), triggering a desktop notification.
+   *
+   * In the future our mobile app will also support spawning native push
+   * notifications in response to incoming ucm-notify-ui packets.
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.message] - message to display
+   * @param {string} [opts.type] - notification type, 'ucm-*' for broadcasts
+   * @param {string} [opts.level] - 'info', 'error', or 'success'
+   * @param {string} [opts.image] - link to an image to be shown
+   * @param {string} [opts.link] - URL the notification should forward too
+   * @param {string} [opts.sound] - URL of sound to play
+   * @throws an error if given no type or message, or the instance is not open
+   *   and authenticated
+   */
+  notifyUI (opts = {}) {
+    const { type, message, level, image, link, sound } = opts
+
+    if (!_isString(type) || !_isString(message)) {
+      throw new Error(`notified with invalid type/message: ${type}/${message}`)
+    }
+
+    if (!this._isOpen) {
+      throw new Error('socket not open')
+    }
+
+    if (!this._isAuthenticated) {
+      throw new Error('socket not authenticated')
+    }
+
+    this.send([0, 'n', null, {
+      type: UCM_NOTIFICATION_TYPE,
+      info: {
+        type,
+        message,
+        level,
+        image,
+        link,
+        sound
+      }
+    }])
+  }
+
+  /**
+   * Adds a listener to the internal listener set, with an optional grouping
+   * for batch unsubscribes (GID) & automatic ws packet matching (filterKey)
+   *
+   * @param {string} eventName - as received on ws stream
+   * @param {object} filter - map of index & value in ws packet
+   * @param {object} modelClass - model to use for serialization
+   * @param {string} cbGID - listener group ID for mass removal
+   * @param {Function} cb - listener
+   * @private
+   */
+  _registerListener (eventName, filter, modelClass, cbGID, cb) {
+    if (!cbGID) cbGID = null
+
+    if (!this._listeners[cbGID]) {
+      this._listeners[cbGID] = { [eventName]: [] }
+    }
+
+    const listeners = this._listeners[cbGID]
+
+    if (!listeners[eventName]) {
+      listeners[eventName] = []
+    }
+
+    const l = {
+      cb,
+      modelClass,
+      filter
+    }
+
+    listeners[eventName].push(l)
+  }
+
+  /**
+   * Registers a new callback to be called when a matching info message is
+   * received.
+   *
+   * @param {number} code - from #WSv2.info
+   * @param {Function} cb - callback
+   */
+  onInfoMessage (code, cb) {
+    if (!this._infoListeners[code]) {
+      this._infoListeners[code] = []
+    }
+
+    this._infoListeners[code].push(cb)
+  }
+
+  /**
+   * Register a generic handler to be called with each received message
+   *
+   * @param {object} opts - options
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   */
+  onMessage ({ cbGID }, cb) {
+    this._registerListener('', null, null, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called with each received candle
+   *
+   * @param {object} opts - options
+   * @param {string} opts.key - candle set key, i.e. trade:30m:tBTCUSD
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-candle
+   * @see WSv2#subscribeCandles
+   * @see WSv2#unsubscribeCandles
+   */
+  onCandle ({ key, cbGID }, cb) {
+    this._registerListener('candle', { 0: key }, Candle, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called with each received candle
+   *
+   * @param {object} opts - options
+   * @param {string} opts.symbol - book symbol
+   * @param {string} opts.prec - book precision, i.e. 'R0'
+   * @param {string} opts.len - book length, i.e. '25'
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books
+   * @see WSv2#subscribeOrderBook
+   * @see WSv2#unsubscribeOrderBook
+   */
+  onOrderBook ({ symbol, prec, len, cbGID }, cb) {
+    this._registerListener('orderbook', {
+      0: symbol,
+      1: prec,
+      2: len
+    }, OrderBook, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called with each received order book checksum
+   *
+   * @param {object} opts - options
+   * @param {string} opts.symbol - book symbol
+   * @param {string} opts.prec - book precision, i.e. 'R0'
+   * @param {string} opts.len - book length, i.e. '25'
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books
+   * @see WSv2#subscribeOrderBook
+   * @see WSv2#unsubscribeOrderBook
+   */
+  onOrderBookChecksum ({ symbol, prec, len, cbGID }, cb) {
+    this._registerListener('ob_checksum', {
+      0: symbol,
+      1: prec,
+      2: len
+    }, null, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called with each received trade (pair or symbol
+   * required)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.pair] - required if no symbol specified
+   * @param {string} [opts.symbol] - required if no pair specified
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-trades
+   * @see WSv2#subscribeTrades
+   * @see WSv2#unsubscribeTrades
+   */
+  onTrades ({ symbol, pair, cbGID }, cb) {
+    const id = pair || symbol || ''
+    const model = id[0] === 'f' ? FundingTrade : PublicTrade
+
+    this._registerListener('trades', { 0: id }, model, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each trade `'te'` event
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.pair] - required if no symbol specified
+   * @param {string} [opts.symbol] - required if no pair specified
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-trades
+   * @see WSv2#subscribeTrades
+   * @see WSv2#unsubscribeTrades
+   */
+  onTradeEntry ({ pair, symbol, cbGID }, cb) {
+    const id = pair || symbol || ''
+    this._registerListener('trade-entry', { 0: id }, PublicTrade, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each personal trade `'te'` event
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.pair] - required if no symbol specified
+   * @param {string} [opts.symbol] - required if no pair specified
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-trades
+   */
+  onAccountTradeEntry ({ symbol, cbGID }, cb) {
+    this._registerListener('auth-te', { 1: symbol }, Trade, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each personal trade `'tu'` event
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.pair] - required if no symbol specified
+   * @param {string} [opts.symbol] - required if no pair specified
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-trades
+   */
+  onAccountTradeUpdate ({ symbol, cbGID }, cb) {
+    this._registerListener('auth-tu', { 1: symbol }, Trade, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each received ticker
+   *
+   * @param {object} opts - options
+   * @param {string} opts.symbol - symbol for tickers
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-ticker
+   * @see WSv2#subscribeTicker
+   * @see WSv2#unsubscribeTicker
+   */
+  onTicker ({ symbol = '', cbGID } = {}, cb) {
+    const m = symbol[0] === 'f' ? FundingTicker : TradingTicker
+    this._registerListener('ticker', { 0: symbol }, m, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each message for the desired status
+   * feed.
+   *
+   * @param {object} opts - options
+   * @param {string} opts.key - key of feed to listen on
+   * @param {string|number} [opts.cbGID] - callback group ID
+   * @param {Function} cb - callback
+   * @see WSv2#subscribeStatus
+   */
+  onStatus ({ key = '', cbGID } = {}, cb) {
+    this._registerListener('status', { 0: key }, null, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each full order snapshot (sent on auth)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {number} [opts.id] - order ID to match
+   * @param {number} [opts.cid] - order client ID to match
+   * @param {number} [opts.gid] - order group ID to match
+   * @param {string|number} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
+   * @see WSv2#auth
+   */
+  onOrderSnapshot ({ symbol, id, cid, gid, cbGID }, cb) {
+    this._registerListener('os', {
+      0: id,
+      1: gid,
+      2: cid,
+      3: symbol
+    }, Order, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each new order packet
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {number} [opts.id] - order ID to match
+   * @param {number} [opts.cid] - order client ID to match
+   * @param {number} [opts.gid] - order group ID to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
+   * @see WSv2#submitOrder
+   */
+  onOrderNew ({ symbol, id, cid, gid, cbGID }, cb) {
+    this._registerListener('on', {
+      0: id,
+      1: gid,
+      2: cid,
+      3: symbol
+    }, Order, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each order update packet
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {number} [opts.id] - order ID to match
+   * @param {number} [opts.cid] - order client ID to match
+   * @param {number} [opts.gid] - order group ID to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
+   * @see WSv2#updateOrder
+   */
+  onOrderUpdate ({ symbol, id, cid, gid, cbGID }, cb) {
+    this._registerListener('ou', {
+      0: id,
+      1: gid,
+      2: cid,
+      3: symbol
+    }, Order, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each order close packet
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {number} [opts.id] - order ID to match
+   * @param {number} [opts.cid] - order client ID to match
+   * @param {number} [opts.gid] - order group ID to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
+   * @see WSv2#cancelOrder
+   */
+  onOrderClose ({ symbol, id, cid, gid, cbGID }, cb) {
+    this._registerListener('oc', {
+      0: id,
+      1: gid,
+      2: cid,
+      3: symbol
+    }, Order, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each position snapshot (sent on auth)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
+   * @see WSv2#auth
+   */
+  onPositionSnapshot ({ symbol, cbGID }, cb) {
+    this._registerListener('ps', { 0: symbol }, Position, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a position is opened
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
+   */
+  onPositionNew ({ symbol, cbGID }, cb) {
+    this._registerListener('pn', { 0: symbol }, Position, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a position is updated
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
+   */
+  onPositionUpdate ({ symbol, cbGID }, cb) {
+    this._registerListener('pu', { 0: symbol }, Position, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a position is closed
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
+   */
+  onPositionClose ({ symbol, cbGID }, cb) {
+    this._registerListener('pc', { 0: symbol }, Position, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each fundign offer snapshot (sent on
+   * auth)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
+   * @see WSv2#auth
+   */
+  onFundingOfferSnapshot ({ symbol, cbGID }, cb) {
+    this._registerListener('fos', { 1: symbol }, FundingOffer, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding offer is created
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
+   */
+  onFundingOfferNew ({ symbol, cbGID }, cb) {
+    this._registerListener('fon', { 1: symbol }, FundingOffer, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding offer is updated
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
+   */
+  onFundingOfferUpdate ({ symbol, cbGID }, cb) {
+    this._registerListener('fou', { 1: symbol }, FundingOffer, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding offer is closed
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
+   */
+  onFundingOfferClose ({ symbol, cbGID }, cb) {
+    this._registerListener('foc', { 1: symbol }, FundingOffer, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each funding credit snapshot (sent on
+   * auth)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
+   * @see WSv2#auth
+   */
+  onFundingCreditSnapshot ({ symbol, cbGID }, cb) {
+    this._registerListener('fcs', { 1: symbol }, FundingCredit, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding credit is created
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
+   */
+  onFundingCreditNew ({ symbol, cbGID }, cb) {
+    this._registerListener('fcn', { 1: symbol }, FundingCredit, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding credit is updated
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
+   */
+  onFundingCreditUpdate ({ symbol, cbGID }, cb) {
+    this._registerListener('fcu', { 1: symbol }, FundingCredit, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding credit is closed
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
+   */
+  onFundingCreditClose ({ symbol, cbGID }, cb) {
+    this._registerListener('fcc', { 1: symbol }, FundingCredit, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each funding loan snapshot (sent on
+   * auth)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
+   * @see WSv2#auth
+   */
+  onFundingLoanSnapshot ({ symbol, cbGID }, cb) {
+    this._registerListener('fls', { 1: symbol }, FundingLoan, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding loan is created
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
+   */
+  onFundingLoanNew ({ symbol, cbGID }, cb) {
+    this._registerListener('fln', { 1: symbol }, FundingLoan, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding loan is updated
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
+   */
+  onFundingLoanUpdate ({ symbol, cbGID }, cb) {
+    this._registerListener('flu', { 1: symbol }, FundingLoan, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called when a funding loan is closed
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
+   */
+  onFundingLoanClose ({ symbol, cbGID }, cb) {
+    this._registerListener('flc', { 1: symbol }, FundingLoan, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each wallet snapshot (sent on auth)
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-wallets
+   */
+  onWalletSnapshot ({ cbGID }, cb) {
+    this._registerListener('ws', null, Wallet, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each wallet update
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-wallets
+   */
+  onWalletUpdate ({ cbGID }, cb) {
+    this._registerListener('wu', null, Wallet, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each balance info update
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-balance
+   */
+  onBalanceInfoUpdate ({ cbGID }, cb) {
+    this._registerListener('bu', null, BalanceInfo, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each margin info update
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-margin
+   */
+  onMarginInfoUpdate ({ cbGID }, cb) {
+    this._registerListener('miu', null, MarginInfo, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each funding info update
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding
+   */
+  onFundingInfoUpdate ({ cbGID }, cb) {
+    this._registerListener('fiu', null, FundingInfo, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each funding trade `'te'` event
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding-trades
+   */
+  onFundingTradeEntry ({ symbol, cbGID }, cb) {
+    this._registerListener('fte', { 0: symbol }, FundingTrade, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each funding trade `'tu'` event
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding-trades
+   */
+  onFundingTradeUpdate ({ symbol, cbGID }, cb) {
+    this._registerListener('ftu', { 0: symbol }, FundingTrade, cbGID, cb)
+  }
+
+  /**
+   * Register a handler to be called on each notification
+   *
+   * @param {object} opts - options
+   * @param {string} [opts.type] - type to match
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @see https://docs.bitfinex.com/v2/reference#ws-auth-notifications
+   */
+  onNotification ({ type, cbGID }, cb) {
+    this._registerListener('n', { 1: type }, Notification, cbGID, cb)
+  }
+}
+
+/**
+ * Map of flag names and numeric values for usage with the API
+ *
+ * @constant
+ * @property {number} DEC_S - flag to request all decimals as strings
+ * @property {number} TIME_S - flag to request all timestamps as strings
+ * @property {number} TIMESTAMP - flag to request timestamp in milliseconds
+ * @property {number} SEQ_ALL - flag to enable sequence numbers & verification
+ * @property {number} CHECKSUM - flag to enable order book checksums
+ * @memberof! WSv2#
+ */
+WSv2.flags = {
+  DEC_S: 8, // enables all decimals as strings
+  TIME_S: 32, // enables all timestamps as strings
+  TIMESTAMP: 32768, // timestamps in milliseconds
+  SEQ_ALL: 65536, // enable sequencing
+  CHECKSUM: 131072 // enable checksum per OB change, top 25 levels per-side
+}
+
+/**
+ * Map of info event names and their respective codes for usage with the API
+ *
+ * @constant
+ * @property {number} SERVER_RESTART - server restart event
+ * @property {number} MAINTENANCE_START - maintenance start event
+ * @property {number} MAINTENANCE_END - maintenance end event
+ * @memberof! WSv2#
+ */
+WSv2.info = {
+  SERVER_RESTART: 20051,
+  MAINTENANCE_START: 20060,
+  MAINTENANCE_END: 20061
+}
+
+/**
+ * Default connection URL
+ *
+ * @constant
+ * @type {string}
+ * @memberof! WSv2#
+ * @default
+ */
+WSv2.url = 'wss://api.bitfinex.com/ws/2'
+
+module.exports = WSv2
+
+
+
+ + + + + + +
+ +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 17 2024 00:59:21 GMT+0200 (Central European Summer Time) using the docdash theme. +
+ + + + + + + + + diff --git a/docs/util_precision.js.html b/docs/util_precision.js.html new file mode 100644 index 00000000..d18ff9ad --- /dev/null +++ b/docs/util_precision.js.html @@ -0,0 +1,119 @@ + + + + + + util/precision.js - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

util/precision.js

+ + + + + + + +
+
+
const Big = require('bignumber.js')
+
+const DEFAULT_SIG_FIGS = 5
+const PRICE_SIG_FIGS = 5
+const AMOUNT_DECIMALS = 8
+
+/**
+ * Smartly set the precision (decimal) on a value based off of the significant
+ * digit maximum. For example, calling with 3.34 when the max sig figs allowed
+ * is 5 would return '3.3400', the representation number of decimals IF they
+ * weren't zeros.
+ *
+ * @param {number} number - number to manipulate
+ * @param {number} [maxSigs] - default 5
+ * @returns {string} str
+ */
+const setSigFig = (number = 0, maxSigs = DEFAULT_SIG_FIGS) => {
+  const n = +(number)
+  if (!isFinite(n)) {
+    return number
+  }
+  const value = n.toPrecision(maxSigs)
+
+  return /e/.test(value)
+    ? new Big(value).toString()
+    : value
+}
+
+const setPrecision = (number = 0, decimals = 0) => {
+  const n = +(number)
+
+  return (isFinite(n))
+    ? n.toFixed(decimals)
+    : number
+}
+
+const prepareAmount = (amount = 0) => {
+  return setPrecision(amount, AMOUNT_DECIMALS)
+}
+
+const preparePrice = (price = 0) => {
+  return setSigFig(price, PRICE_SIG_FIGS)
+}
+
+module.exports = {
+  setSigFig, setPrecision, prepareAmount, preparePrice
+}
+
+
+
+ + + + + + +
+ +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 17 2024 00:59:21 GMT+0200 (Central European Summer Time) using the docdash theme. +
+ + + + + + + + + diff --git a/docs/util_ws2.js.html b/docs/util_ws2.js.html new file mode 100644 index 00000000..05c8e87d --- /dev/null +++ b/docs/util_ws2.js.html @@ -0,0 +1,85 @@ + + + + + + util/ws2.js - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

util/ws2.js

+ + + + + + + +
+
+
'use strict'
+
+const _findLast = require('lodash/findLast')
+
+/**
+ * Resolves the message payload; useful for getting around sequence numbers
+ *
+ * @param {Array} msg - message to parse
+ * @returns {Array} payload - undefined if not found
+ */
+module.exports = (msg = []) => {
+  return _findLast(msg, i => Array.isArray(i))
+}
+
+
+
+ + + + + + +
+ +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 17 2024 00:59:21 GMT+0200 (Central European Summer Time) using the docdash theme. +
+ + + + + + + + + diff --git a/docs/ws2_manager.js.html b/docs/ws2_manager.js.html new file mode 100644 index 00000000..330c285a --- /dev/null +++ b/docs/ws2_manager.js.html @@ -0,0 +1,632 @@ + + + + + + ws2_manager.js - Documentation + + + + + + + + + + + + + + + + + + + + +
+ +

ws2_manager.js

+ + + + + + + +
+
+
'use strict'
+
+const { EventEmitter } = require('events')
+const debug = require('debug')('bfx:ws2:manager')
+const _isEqual = require('lodash/isEqual')
+const _isFinite = require('lodash/isFinite')
+const _includes = require('lodash/includes')
+const _pick = require('lodash/pick')
+const PromiseThrottle = require('promise-throttle')
+const WSv2 = require('./transports/ws2')
+
+const DATA_CHANNEL_LIMIT = 30
+const reconnectThrottler = new PromiseThrottle({
+  requestsPerSecond: 10 / 60.0,
+  promiseImplementation: Promise
+})
+
+/**
+ * Provides a wrapper around the WSv2 class, opening new sockets when a
+ * subscription would push a single socket over the data channel limit.
+ *
+ * For more complex operations, grab a socket reference with getSocket() or
+ * getFreeDataSocket(), or create a new WSv2 instance manually
+ *
+ * @example
+ * const rest = new RESTv2({ transform: true })
+ * const details = await rest.symbolDetails()
+ * const symbols = details.map(d => `t${d.pair.toUpperCase()}`)
+ * const timeFrames = ['1m', '5m', '30m', '1h', '6h']
+ * const keys = _flatten(symbols.map(s => {
+ *   return timeFrames.map(tf => `trade:${tf}:${s}`)
+ * }))
+ *
+ * const m = new Manager()
+ *
+ * m.on('error', (err) => {
+ *   debug('error: %s', err)
+ * })
+ *
+ * m.once('open', () => {
+ *   debug('open')
+ *
+ *   keys.forEach(key => {
+ *     m.subscribeCandles(key)
+ *     m.onCandle({ key }, (candles) => {
+ *       debug('recv %d candles on channel %s', candles.length, key)
+ *     })
+ *   })
+ *
+ *   symbols.forEach(symbol => {
+ *     m.subscribeTrades(symbol)
+ *     m.onTrades({ symbol }, (trades) => {
+ *       debug('recv %d trades on channel %s', trades.length, symbol)
+ *     })
+ *   })
+ *
+ *   symbols.forEach(symbol => {
+ *     m.subscribeTicker(symbol)
+ *     m.onTicker({ symbol }, (ticker) => {
+ *       debug('recv ticker on channel %s: %j', symbol, ticker)
+ *     })
+ *   })
+ *
+ *   symbols.forEach(symbol => {
+ *     m.subscribeOrderBook(symbol)
+ *     m.onOrderBook({ symbol }, (update) => {
+ *       debug('recv book update on channel %s: %j', symbol, update)
+ *     })
+ *   })
+ *
+ *   setInterval(() => {
+ *     debug('num keys: %d', keys.length)
+ *     debug('num sockets: %d', m.getNumSockets())
+ *     debug('socket info: %j', m.getSocketInfo())
+ *   }, 5000)
+ * })
+ *
+ * m.openSocket()
+ */
+class WS2Manager extends EventEmitter {
+  /**
+   * @param {object} socketArgs - passed to WSv2 constructors
+   * @param {object} [authArgs] - cached for all internal socket auth() calls
+   * @param {number} [authArgs.calc] - default 0
+   * @param {number} [authArgs.dms] - default 0
+   */
+  constructor (socketArgs, authArgs = { calc: 0, dms: 0 }) {
+    super()
+
+    this.setMaxListeners(1000)
+
+    this._authArgs = authArgs
+    this._sockets = []
+    this._socketArgs = {
+      ...(socketArgs || {}),
+      reconnectThrottler
+    }
+  }
+
+  /**
+   * Update authentication arguments on all sockets
+   *
+   * @param {object} args - arguments
+   * @param {number} [args.calc] - calc value
+   * @param {number} [args.dms] - active 4
+   */
+  setAuthArgs (args = {}) {
+    this._authArgs = {
+      ...this._authArgs,
+      ...args
+    }
+
+    this._sockets.forEach(socket => socket.ws.updateAuthArgs(this._authArgs))
+  }
+
+  /**
+   * Retrieve internal authentication arguments
+   *
+   * @returns {object} args
+   */
+  getAuthArgs () {
+    return this._authArgs
+  }
+
+  /**
+   * Reconnects all open sockets
+   *
+   * @returns {Promise} p
+   */
+  async reconnect () {
+    return Promise.all(this._sockets.map(socket => socket.ws.reconnect()))
+  }
+
+  /**
+   * Closes all open sockets
+   *
+   * @returns {Promise} p
+   */
+  async close () {
+    return Promise.all(this._sockets.map(socket => socket.ws.close()))
+  }
+
+  /**
+   * @param {object} s - socket state
+   * @returns {number} count - # of subscribed/pending data channels
+   */
+  static getDataChannelCount (s) {
+    let count = s.ws.getDataChannelCount()
+
+    count += s.pendingSubscriptions.length
+    count -= s.pendingUnsubscriptions.length
+
+    return count
+  }
+
+  /**
+   * @returns {number} n
+   */
+  getNumSockets () {
+    return this._sockets.length
+  }
+
+  /**
+   * @param {number} i - index into pool
+   * @returns {object} state
+   */
+  getSocket (i) {
+    return this._sockets[i]
+  }
+
+  /**
+   * Returns an object which can be logged to inspect the socket pool
+   *
+   * @returns {object[]} socketInfo
+   */
+  getSocketInfo () {
+    return this._sockets.map(s => ({
+      nChannels: WS2Manager.getDataChannelCount(s)
+    }))
+  }
+
+  /**
+   * Authenticates all existing & future sockets with the provided credentials.
+   * Does nothing if an apiKey/apiSecret pair are already known.
+   *
+   * @param {object} args - arguments
+   * @param {string} args.apiKey - saved if not already provided
+   * @param {string} args.apiSecret - saved if not already provided
+   * @param {number} [args.calc] - default 0
+   * @param {number} [args.dms] - dead man switch, active 4
+   */
+  auth ({ apiKey, apiSecret, calc, dms } = {}) {
+    if (this._socketArgs.apiKey || this._socketArgs.apiSecret) {
+      debug('error: auth credentials already provided! refusing auth')
+      return
+    }
+
+    this._socketArgs.apiKey = apiKey
+    this._socketArgs.apiSecret = apiSecret
+
+    if (_isFinite(calc)) this._authArgs.calc = calc
+    if (_isFinite(dms)) this._authArgs.dms = dms
+    if (apiKey) this._authArgs.apiKey = apiKey
+    if (apiSecret) this._authArgs.apiSecret = apiSecret
+
+    this._sockets.forEach(s => {
+      if (!s.ws.isAuthenticated()) {
+        s.ws.updateAuthArgs(this._authArgs)
+        s.ws.auth()
+      }
+    })
+  }
+
+  /**
+   * Creates a new socket/state instance and adds it to the internal pool. Binds
+   * event listeners to forward via our own event emitter, and to manage pending
+   * subs/unsubs.
+   *
+   * @returns {object} state
+   */
+  openSocket () {
+    const { apiKey, apiSecret } = this._socketArgs
+    const ws = new WSv2(this._socketArgs)
+    const wsState = {
+      pendingSubscriptions: [],
+      pendingUnsubscriptions: [],
+      ws
+    }
+
+    ws.updateAuthArgs(this._authArgs)
+    ws.on('open', () => this.emit('open', ws))
+    ws.on('message', (msg = {}) => this.emit('message', msg, ws))
+    ws.on('error', (error) => this.emit('error', error, ws))
+    ws.on('auth', () => this.emit('auth', ws))
+    ws.on('close', () => this.emit('close', ws))
+    ws.on('subscribed', (msg = {}) => {
+      this.emit('subscribed', msg)
+
+      const i = wsState.pendingSubscriptions.find(sub => {
+        const fv = _pick(msg, Object.keys(sub[1]))
+
+        return (
+          (sub[0] === msg.channel) &&
+          _isEqual(fv, sub[1])
+        )
+      })
+
+      if (i === -1) {
+        debug('error removing pending sub: %j', msg)
+        return
+      }
+
+      wsState.pendingSubscriptions.splice(i, 1)
+    })
+
+    ws.on('unsubscribed', (msg = {}) => {
+      this.emit('unsubscribed', msg)
+
+      const { chanId } = msg
+      const i = wsState.pendingUnsubscriptions.findIndex(cid => (
+        cid === `${chanId}`
+      ))
+
+      if (i === -1) {
+        debug('error removing pending unsub: %j', msg)
+        return
+      }
+
+      wsState.pendingUnsubscriptions.splice(i, 1)
+    })
+
+    if (apiKey && apiSecret) { // auto-auth
+      ws.once('open', () => {
+        debug('authenticating socket...')
+
+        ws.auth().then(() => {
+          return debug('socket authenticated')
+        }).catch((err) => {
+          debug('error authenticating socket: %s', err.message)
+        })
+      })
+    }
+
+    ws.open().then(() => {
+      return debug('socket connection opened')
+    }).catch((err) => {
+      debug('error opening socket: %s', err.stack)
+    })
+
+    this._sockets.push(wsState)
+    return wsState
+  }
+
+  /**
+   * @returns {object} state
+   */
+  getAuthenticatedSocket () {
+    return this._sockets.find(s => s.ws.isAuthenticated())
+  }
+
+  /**
+   * Returns the first socket that has less active/pending channels than the
+   * DATA_CHANNEL_LIMIT
+   *
+   * @returns {object} state - undefined if none found
+   */
+  getFreeDataSocket () {
+    return this._sockets.find(s => (
+      WS2Manager.getDataChannelCount(s) < DATA_CHANNEL_LIMIT
+    ))
+  }
+
+  /**
+   * Returns the first socket that is subscribed/pending sub to the specified
+   * channel.
+   *
+   * @param {string} type - i.e. 'book'
+   * @param {object} filter - i.e. { symbol: 'tBTCUSD', prec: 'R0' }
+   * @returns {object} wsState - undefined if not found
+   */
+  getSocketWithDataChannel (type, filter) {
+    return this._sockets.find(s => {
+      const subI = s.pendingSubscriptions.findIndex(s => (
+        s[0] === type && _isEqual(s[1], filter)
+      ))
+
+      if (subI !== -1) {
+        return true
+      }
+
+      // Confirm unsub is not pending
+      const cid = s.ws.getDataChannelId(type, filter)
+
+      if (!cid) {
+        return false
+      }
+
+      return cid && !_includes(s.pendingUnsubscriptions, cid)
+    })
+  }
+
+  /**
+   * NOTE: Cannot filter against pending subscriptions, due to unknown chanId
+   *
+   * @param {number} chanId - channel ID
+   * @returns {object} wsState - undefined if not found
+   */
+  getSocketWithChannel (chanId) {
+    return this._sockets.find(s => {
+      return (
+        s.ws.hasChannel(chanId) &&
+        !_includes(s.pendingUnsubscriptions, chanId)
+      )
+    })
+  }
+
+  /**
+   * @param {string} channel - channel type
+   * @param {string} identifier - unique channel identifier
+   * @returns {object} wsState - undefined if not found
+   */
+  getSocketWithSubRef (channel, identifier) {
+    return this._sockets.find(s => s.ws.hasSubscriptionRef(channel, identifier))
+  }
+
+  /**
+   * Calls the provided cb with all internal socket instances
+   *
+   * @param {Function} cb - callback
+   */
+  withAllSockets (cb) {
+    this._sockets.forEach((ws2) => {
+      cb(ws2)
+    })
+  }
+
+  /**
+   * Subscribes a free data socket if available to the specified channel, or
+   * opens a new socket & subs if needed.
+   *
+   * @param {string} type - i.e. 'book'
+   * @param {string} ident - i.e. 'tBTCUSD'
+   * @param {object} filter - i.e. { symbol: 'tBTCUSD', prec: 'R0' }
+   */
+  subscribe (type, ident, filter) {
+    let s = this.getFreeDataSocket()
+    if (!s) {
+      s = this.openSocket()
+    }
+
+    const doSub = () => {
+      s.ws.managedSubscribe(type, ident, filter)
+    }
+
+    if (!s.ws.isOpen()) {
+      s.ws.once('open', doSub)
+    } else {
+      doSub()
+    }
+
+    s.pendingSubscriptions.push([type, filter])
+  }
+
+  /**
+   * @param {string} channel - channel type
+   * @param {string} identifier - unique channel identifier
+   */
+  managedUnsubscribe (channel, identifier) {
+    const s = this.getSocketWithSubRef(channel, identifier)
+
+    if (!s) {
+      debug('cannot unsub from unknown channel %s: %s', channel, identifier)
+      return
+    }
+
+    const chanId = s.ws._chanIdByIdentifier(channel, identifier)
+    s.ws.managedUnsubscribe(channel, identifier)
+    s.pendingUnsubscriptions.push(chanId)
+  }
+
+  /**
+   * Unsubscribes the first socket w/ the specified channel. Does nothing if no
+   * such socket is found.
+   *
+   * @param {number} chanId - channel ID
+   */
+  unsubscribe (chanId) {
+    const s = this.getSocketWithChannel(chanId)
+
+    if (!s) {
+      debug('cannot unsub from unknown channel: %d', chanId)
+      return
+    }
+
+    s.ws.unsubscribe(chanId)
+    s.pendingUnsubscriptions.push(chanId)
+  }
+
+  /**
+   * @param {string} symbol - symbol for ticker
+   */
+  subscribeTicker (symbol) {
+    this.subscribe('ticker', symbol, { symbol })
+  }
+
+  /**
+   * @param {string} symbol - symbol for trades
+   */
+  subscribeTrades (symbol) {
+    this.subscribe('trades', symbol, { symbol })
+  }
+
+  /**
+   * @param {string} symbol - symbol for order book
+   * @param {string} [prec] - precision, i.e. 'R0', default 'P0'
+   * @param {string} [len] - length, default '25'
+   * @param {string} [freq] - default 'F0'
+   */
+  subscribeOrderBook (symbol, prec = 'P0', len = '25', freq = 'F0') {
+    const filter = {}
+
+    if (symbol) filter.symbol = symbol
+    if (prec) filter.prec = prec
+    if (len) filter.len = len
+    if (freq) filter.freq = freq
+
+    this.subscribe('book', symbol, filter)
+  }
+
+  /**
+   * @param {string} key - candle channel key
+   */
+  subscribeCandles (key) {
+    this.subscribe('candles', key, { key })
+  }
+
+  /**
+   * @param {object} opts - options
+   * @param {string} opts.key - candle set key, i.e. trade:30m:tBTCUSD
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @throws an error if no data socket is available
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-candle
+   */
+  onCandle ({ key, cbGID }, cb) {
+    const s = this.getSocketWithDataChannel('candles', { key })
+
+    if (!s) {
+      throw new Error('no data socket available; did you provide a key?')
+    }
+
+    s.ws.onCandle({ key, cbGID }, cb)
+  }
+
+  /**
+   * @param {object} opts - options
+   * @param {string} opts.symbol - order book symbol
+   * @param {string} [opts.prec] - precision, i.e. 'R0', default 'P0'
+   * @param {string} [opts.len] - length, default '25'
+   * @param {string} [opts.freq] - default 'F0'
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @throws an error if no data socket is available
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books
+   */
+  onOrderBook ({ symbol, prec = 'P0', len = '25', freq = 'F0', cbGID }, cb) {
+    const filter = {}
+
+    if (symbol) filter.symbol = symbol
+    if (prec) filter.prec = prec
+    if (len) filter.len = len
+    if (freq) filter.freq = freq
+
+    const s = this.getSocketWithDataChannel('book', filter)
+
+    if (!s) {
+      throw new Error('no data socket available; did you provide a symbol?')
+    }
+
+    s.ws.onOrderBook({ cbGID, ...filter }, cb)
+  }
+
+  /**
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol for trades
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @throws an error if no data socket is available
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-trades
+   */
+  onTrades ({ symbol, cbGID }, cb) {
+    const s = this.getSocketWithDataChannel('trades', { symbol })
+
+    if (!s) {
+      throw new Error('no data socket available; did you provide a symbol?')
+    }
+
+    s.ws.onTrades({ symbol, cbGID }, cb)
+  }
+
+  /**
+   * @param {object} opts - options
+   * @param {string} [opts.symbol] - symbol for ticker
+   * @param {string} [opts.cbGID] - callback group id
+   * @param {Function} cb - callback
+   * @throws an error if no data socket is available
+   * @see https://docs.bitfinex.com/v2/reference#ws-public-ticker
+   */
+  onTicker ({ symbol = '', cbGID } = {}, cb) {
+    const s = this.getSocketWithDataChannel('ticker', { symbol })
+
+    if (!s) {
+      throw new Error('no data socket available; did you provide a symbol?')
+    }
+
+    s.ws.onTicker({ symbol, cbGID }, cb)
+  }
+}
+
+module.exports = WS2Manager
+
+
+
+ + + + + + +
+ +
+ +
+ Documentation generated by JSDoc 4.0.3 on Tue Sep 17 2024 00:59:21 GMT+0200 (Central European Summer Time) using the docdash theme. +
+ + + + + + + + + diff --git a/examples/rest2/claim_positions.js b/examples/rest2/claim_positions.js new file mode 100644 index 00000000..5511f96b --- /dev/null +++ b/examples/rest2/claim_positions.js @@ -0,0 +1,53 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const _isEmpty = require('lodash/isEmpty') +const { args: { apiKey, apiSecret }, debug, debugTable, readline } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const filterByMarket = false + const allPositions = await rest.positions() + const positions = _isEmpty(filterByMarket) + ? allPositions + : allPositions.filter(({ symbol }) => symbol === filterByMarket) + + if (positions.length === 0) { + debug('no positions match filter') + return + } + + debug( + 'found %d open positions on market(s) %s\n', positions.length, + positions.map(({ symbol }) => symbol).join(',') + ) + + debugTable({ + headers: ['Symbol', 'Status', 'Amount', 'Base Price', 'P/L'], + rows: positions.map(p => ([ + p.symbol, p.status, p.amount, p.basePrice, p.pl + ])) + }) + + const confirm = await readline.questionAsync( + '> Are you sure you want to claim the position(s) listed above? ' + ) + + if (confirm.toLowerCase()[0] !== 'y') { + return + } + + debug('') + debug('claiming positions...') + + await Promise.all(positions.map(p => p.claim(rest))) + + debug('done!') + readline.close() +} + +execute() diff --git a/examples/rest2/close_positions.js b/examples/rest2/close_positions.js new file mode 100644 index 00000000..1ee47e3b --- /dev/null +++ b/examples/rest2/close_positions.js @@ -0,0 +1,88 @@ +'use strict' + +const PI = require('p-iteration') +const _isEmpty = require('lodash/isEmpty') +const WSv2 = require('../../lib/transports/ws2') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable, readline } = require('../util/setup') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const filterByMarket = null + const allPositions = await rest.positions() + await ws.open() + await ws.auth() + + if (allPositions.length === 0) { + debug('no open positions') + await ws.close() + readline.close() + return + } + + debug( + 'found %d open positions on market(s) %s', allPositions.length, + allPositions.map(({ symbol }) => symbol).join(',') + ) + + const positions = _isEmpty(filterByMarket) + ? allPositions + : allPositions.filter(({ symbol }) => symbol === filterByMarket) + + if (positions.length === 0) { + debug('no positions match filter') + await ws.close() + readline.close() + return + } + + const orders = positions.map(p => p.orderToClose(ws)) + + debugTable({ + headers: [ + 'ID', 'Symbol', 'Status', 'Amount', 'Base Price', 'P/L' + ], + + rows: positions.map(p => ([ + p.id, p.symbol, p.status, p.amount, p.basePrice, p.pl + ])) + }) + + orders.forEach(o => (debug('%s', o.toString()))) + debug('') + + const confirm = await readline.questionAsync( + '> Are you sure you want to submit the order(s) listed above? ' + ) + + if (confirm.toLowerCase()[0] !== 'y') { + await ws.close() + readline.close() + return + } + + debug('') + + ws.onOrderClose({}, ({ id, symbol, status }) => { + debug('received confirmation of order %d closed on %s: %s', id, symbol, status) + }) + + await PI.forEachSeries(orders, o => o.submit()) + + debug('') + debug('closed %d positions', positions.length) + + await ws.close() + readline.close() +} + +execute() diff --git a/examples/rest2/currencies.js b/examples/rest2/currencies.js new file mode 100644 index 00000000..bac9db81 --- /dev/null +++ b/examples/rest2/currencies.js @@ -0,0 +1,22 @@ +'use strict' + +const _chunk = require('lodash/chunk') +const { RESTv2 } = require('../../index') +const { debug } = require('../util/setup') + +async function execute () { + const rest = new RESTv2() + debug('fetching currency list...') + + const currencies = await rest.currencies() + + debug('received %d currencies', currencies[0].length) + + debug('') + _chunk(currencies[0], 10).forEach((currencyChunk) => { + debug('%s', currencyChunk.join(', ')) + }) + debug('') +} + +execute() diff --git a/examples/rest2/funding_credits.js b/examples/rest2/funding_credits.js new file mode 100644 index 00000000..589d4d59 --- /dev/null +++ b/examples/rest2/funding_credits.js @@ -0,0 +1,33 @@ +'use strict' + +const { prepareAmount } = require('bfx-api-node-util') +const argFromCLI = require('../util/arg_from_cli') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const symbol = argFromCLI(0, 'fUSD') + + debug('fetching funding credits for %s', symbol) + + const fcs = await rest.fundingCredits(symbol) + + if (fcs.length === 0) { + debug('none available') + } else { + debugTable({ + headers: ['Symbol', 'Amount', 'Status', 'Rate', 'Period', 'Renew'], + rows: fcs.map(fc => [ + fc.symbol, prepareAmount(fc.amount), fc.status, fc.rate * 100, + fc.period, fc.renew ? 'Y' : 'N' + ]) + }) + } +} + +execute() diff --git a/examples/rest2/funding_info.js b/examples/rest2/funding_info.js new file mode 100644 index 00000000..d2e9ceb0 --- /dev/null +++ b/examples/rest2/funding_info.js @@ -0,0 +1,32 @@ +'use strict' + +const { prepareAmount } = require('bfx-api-node-util') +const argFromCLI = require('../util/arg_from_cli') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const symbol = argFromCLI(0, 'fUSD') + + debug('fetching funding info for %s', symbol) + + const flu = await rest.fundingInfo(symbol) + const [,, [yieldLoan, yieldLend, durationLoan, durationLend]] = flu + + debugTable({ + headers: [ + 'Symbol', 'Yield Loan', 'Yield Lend', 'Duration Loan', 'Duration Lend' + ], + rows: [[ + symbol, prepareAmount(yieldLoan), prepareAmount(yieldLend), durationLoan, + durationLend + ]] + }) +} + +execute() diff --git a/examples/rest2/funding_loans.js b/examples/rest2/funding_loans.js new file mode 100644 index 00000000..17df637a --- /dev/null +++ b/examples/rest2/funding_loans.js @@ -0,0 +1,33 @@ +'use strict' + +const { prepareAmount } = require('bfx-api-node-util') +const argFromCLI = require('../util/arg_from_cli') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const symbol = argFromCLI(0, 'fUSD') + + debug('fetching funding loans for %s', symbol) + + const fls = await rest.fundingLoans(symbol) + + if (fls.length === 0) { + debug('none available') + } else { + debugTable({ + headers: ['Symbol', 'Amount', 'Status', 'Rate', 'Period', 'Renew'], + rows: fls.map(fl => [ + fl.symbol, prepareAmount(fl.amount), fl.status, fl.rate * 100, + fl.period, fl.renew ? 'Y' : 'N' + ]) + }) + } +} + +execute() diff --git a/examples/rest2/funding_offers.js b/examples/rest2/funding_offers.js new file mode 100644 index 00000000..ae40e5ab --- /dev/null +++ b/examples/rest2/funding_offers.js @@ -0,0 +1,33 @@ +'use strict' + +const { prepareAmount } = require('bfx-api-node-util') +const argFromCLI = require('../util/arg_from_cli') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const symbol = argFromCLI(0, 'fUSD') + + debug('fetching funding offers for %s', symbol) + + const fos = await rest.fundingOffers(symbol) + + if (fos.length === 0) { + debug('none available') + } else { + debugTable({ + headers: ['Symbol', 'Amount', 'Status', 'Rate', 'Period', 'Renew'], + rows: fos.map(fo => [ + fo.symbol, prepareAmount(fo.amount), fo.status, fo.rate * 100, + fo.period, fo.renew ? 'Y' : 'N' + ]) + }) + } +} + +execute() diff --git a/examples/rest2/key_permissions.js b/examples/rest2/key_permissions.js new file mode 100644 index 00000000..7fca64d6 --- /dev/null +++ b/examples/rest2/key_permissions.js @@ -0,0 +1,26 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + debug('fetching permissions') + + const perms = await rest.keyPermissions() + + const rows = perms.map(({ key, read, write }) => [ + key.toUpperCase(), read ? 'Y' : 'N', write ? 'Y' : 'N' + ]) + + debugTable({ + rows, + headers: ['Scope', 'Read', 'Write'] + }) +} + +execute() diff --git a/examples/rest2/ledgers.js b/examples/rest2/ledgers.js new file mode 100644 index 00000000..7d11ad9f --- /dev/null +++ b/examples/rest2/ledgers.js @@ -0,0 +1,35 @@ +'use strict' + +const { prepareAmount } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') +const argFromCLI = require('../util/arg_from_cli') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const params = { + ccy: argFromCLI(0, 'all') + } + const ccy = params.ccy === 'all' ? null : params.ccy + + debug('fetching ledger entries for %s...', ccy || 'all currencies') + + const entries = await rest.ledgers(ccy) + const rows = entries.map(e => [ + e.id, e.currency, new Date(e.mts).toLocaleString(), prepareAmount(e.amount), + prepareAmount(e.balance), e.description + ]) + + debugTable({ + rows, + headers: [ + 'Entry ID', 'Currency', 'Timestamp', 'Amount', 'Balance', 'Description' + ] + }) +} + +execute() diff --git a/examples/rest2/list_open_orders.js b/examples/rest2/list_open_orders.js new file mode 100644 index 00000000..ae068765 --- /dev/null +++ b/examples/rest2/list_open_orders.js @@ -0,0 +1,45 @@ +'use strict' + +const _capitalize = require('lodash/capitalize') +const _isEmpty = require('lodash/isEmpty') +const { prepareAmount, preparePrice } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const filterByMarket = null + + debug('fetching open orders...') + const allOrders = await rest.activeOrders() + + if (allOrders.length === 0) { + return debug('no open orders matching filters') + } + + const orders = _isEmpty(filterByMarket) + ? allOrders + : allOrders.filter(o => o.symbol === filterByMarket) + + debug('read %d open order(s)', orders.length) + + debugTable({ + headers: [ + 'Symbol', 'Type', 'Amount', 'Price', 'Status', 'ID', 'CID', + 'Created', 'Updated' + ], + + rows: orders.map((o) => [ + o.symbol, o.type, prepareAmount(o.amount), preparePrice(o.price), + _capitalize(o.status.split(':')[0]), o.id, o.cid, + new Date(o.mtsCreate).toLocaleString(), + new Date(o.mtsUpdate).toLocaleString() + ]) + }) +} + +execute() diff --git a/examples/rest2/margin_info.js b/examples/rest2/margin_info.js new file mode 100644 index 00000000..0363e833 --- /dev/null +++ b/examples/rest2/margin_info.js @@ -0,0 +1,26 @@ +'use strict' + +const { prepareAmount } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + debug('fetching margin info...') + + const info = await rest.marginInfo() + const { userPL, userSwaps, marginBalance, marginNet } = info + + debug('') + debug('Swaps: %d', userSwaps) + debug('P/L: %s', prepareAmount(userPL)) + debug('Balance: %s', prepareAmount(marginBalance)) + debug('Net Balance: %s', prepareAmount(marginNet)) + debug('') +} + +execute() diff --git a/examples/rest2/movements.js b/examples/rest2/movements.js new file mode 100644 index 00000000..5b7ea82e --- /dev/null +++ b/examples/rest2/movements.js @@ -0,0 +1,46 @@ +'use strict' + +const { prepareAmount, preparePrice } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +const argFromCLI = require('../util/arg_from_cli') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const params = { + ccy: argFromCLI(0, 'all') + } + const ccy = params.ccy === 'all' ? null : params.ccy + + debug('fetching movements for %s...', ccy || 'all currencies') + + const movements = await rest.movements(ccy) + + if (movements.length === 0) { + return debug('no movements found') + } + + debugTable({ + headers: [ + 'ID', 'Currency', 'Started', 'Updated', 'Status', 'Amount', 'Fees' + ], + + rows: movements.map((m) => { + const status = `${m.status[0].toUpperCase()}${m.status.substring(1).toLowerCase()}` + const started = new Date(m.mtsStarted).toLocaleString() + const updated = new Date(m.mtsUpdated).toLocaleString() + + return [ + m.id, m.currency, started, updated, status, prepareAmount(m.amount), + preparePrice(m.fees) + ] + }) + }) +} + +execute() diff --git a/examples/rest2/order-history.js b/examples/rest2/order-history.js new file mode 100644 index 00000000..817e4f52 --- /dev/null +++ b/examples/rest2/order-history.js @@ -0,0 +1,51 @@ +'use strict' + +const { prepareAmount, preparePrice } = require('bfx-api-node-util') +const _isEmpty = require('lodash/isEmpty') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +const START = Date.now() - (30 * 24 * 60 * 60 * 1000 * 1000) +const END = Date.now() +const LIMIT = 25 + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const market = 'tBTCUSD' + + if (_isEmpty(market)) { + return debug('market required') + } + + debug('fetching 30d order history for %s...', market) + + const orders = await rest.orderHistory(market, START, END, LIMIT) + + if (orders.length === 0) { + return debug('no historical orders for %s', market) + } + + debugTable({ + headers: [ + 'Order ID', 'Created', 'Updated', 'Amount', 'Filled', 'Price', 'Status' + ], + + rows: orders.map(o => { + o.status = `${o.status[0].toUpperCase()}${o.status.substring(1)}` + o.mtsCreate = new Date(o.mtsCreate).toLocaleString() + o.mtsUpdate = new Date(o.mtsUpdate).toLocaleString() + + return [ + o.id, o.mtsCreate, o.mtsUpdate, prepareAmount(o.amountOrig), + prepareAmount(o.amountOrig - o.amount), + preparePrice(o.price), o.status.split(':')[0] + ] + }) + }) +} + +execute() diff --git a/examples/rest2/positions.js b/examples/rest2/positions.js new file mode 100644 index 00000000..060f6503 --- /dev/null +++ b/examples/rest2/positions.js @@ -0,0 +1,53 @@ +'use strict' + +const _capitalize = require('lodash/capitalize') +const _map = require('lodash/map') +const { prepareAmount, preparePrice } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + debug('fetching positions...') + + const positions = await rest.positions() + const symbols = _map(positions, 'symbol') + + if (positions.length === 0) { + return debug('no open positions') + } + + debug('found %d open positions', positions.length) + debug('fetching tickers for: %s', symbols.join(', ')) + + const prices = {} + const rawTickers = await rest.tickers(symbols) + + rawTickers.forEach(({ symbol, lastPrice }) => (prices[symbol] = +lastPrice)) + + debugTable({ + headers: [ + 'ID', 'Symbol', 'Status', 'Amount', 'Base Price', 'Funding Cost', + 'Base Value', 'Net Value', 'P/L', 'P/L %' + ], + + rows: positions.map((p) => { + const nv = +prices[p.symbol] * +p.amount + const pl = nv - (+p.basePrice * +p.amount) + const plPerc = (pl / nv) * 100.0 + + return [ + p.id, p.symbol, _capitalize(p.status), prepareAmount(p.amount), + preparePrice(p.basePrice), prepareAmount(p.marginFunding), + prepareAmount(+p.marginFunding + (+p.amount * +p.basePrice)), + prepareAmount(nv), prepareAmount(pl), plPerc.toFixed(2) + ] + }) + }) +} + +execute() diff --git a/examples/rest2/status.js b/examples/rest2/status.js new file mode 100644 index 00000000..2b4260c4 --- /dev/null +++ b/examples/rest2/status.js @@ -0,0 +1,18 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const { debug } = require('../util/setup') + +async function execute () { + const rest = new RESTv2() + debug('fetching platform status...') + + const status = await rest.status() + + debug(status === 0 + ? 'Platform currently under maintenance' + : 'Platform operating normally' + ) +} + +execute() diff --git a/examples/rest2/submit_funding_offer.js b/examples/rest2/submit_funding_offer.js new file mode 100644 index 00000000..9e37d231 --- /dev/null +++ b/examples/rest2/submit_funding_offer.js @@ -0,0 +1,39 @@ +'use strict' + +const { FundingOffer } = require('bfx-api-node-models') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') + +const CLOSE_DELAY_MS = 5 * 1000 + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const fo = new FundingOffer({ + type: 'LIMIT', + symbol: 'fUSD', + rate: 0.0120000, + amount: 120, + period: 2 + }, rest) + + debug('submitting: %s', fo.toString()) + + try { + await fo.submit() + } catch (e) { + return debug('failed: %s', e.message) + } + + debug('done. closing in %ds...', CLOSE_DELAY_MS / 1000) + + await new Promise(resolve => setTimeout(resolve, CLOSE_DELAY_MS)) + await fo.close() + + debug('offer closed') +} + +execute() diff --git a/examples/rest2/submit_order.js b/examples/rest2/submit_order.js new file mode 100644 index 00000000..67329b95 --- /dev/null +++ b/examples/rest2/submit_order.js @@ -0,0 +1,88 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, readline } = require('../util/setup') + +const UPDATE_DELAY_MS = 5 * 1000 +const CANCEL_DELAY_MS = 10 * 1000 + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret + }) + const { + symbol, price, amount, type, affiliateCode, onlySubmitOrder, skipConfirm, + priceStop, distance + } = { + // needed in order to pipe data to the process, until we can figure out a + // workaround + skipConfirm: false, + onlySubmitOrder: false, // allows this script to be used only for submits + symbol: 'tLEOUSD', + price: 2, + amount: -6, + type: Order.type.LIMIT, + affiliateCode: 'xZvWHMNR' + } + + const o = new Order({ + cid: Date.now(), + symbol, + price, + priceAuxLimit: priceStop, + priceTrailing: distance, + amount, + type, + affiliateCode + }, rest) + + if (!skipConfirm) { + const confirm = await readline.questionAsync([ + '> Are you sure you want to submit this order?', + `> ${o.toString()}`, + '> ' + ].join('\n')) + + if (confirm.toLowerCase()[0] !== 'y') { + readline.close() + return + } + } + + debug('submitting order: %s', o.toString()) + + await o.submit() + + debug('order successfully submitted! (id %j, cid %j, gid %j)', o.id, o.cid, o.gid) + + if (onlySubmitOrder) { + readline.close() + return // for bfx-cli + } + + debug('') + debug('will update price to $3.00 in %fs...', UPDATE_DELAY_MS / 1000) + + await new Promise(resolve => setTimeout(resolve, UPDATE_DELAY_MS)) + + debug('') + debug('updating order price...') + + const updateNotification = await o.update({ price: 3 }) + debug('successfully updated! (%s: %s)', updateNotification.status, updateNotification.text) + debug('') + debug('will cancel the order in %fs', CANCEL_DELAY_MS / 1000) + + await new Promise(resolve => setTimeout(resolve, CANCEL_DELAY_MS)) + + debug('') + debug('cancelling order...') + + const cancelNotification = await o.cancel() + debug('successfully canceled! (%s: %s)', cancelNotification.status, cancelNotification.text) + readline.close() +} + +execute() diff --git a/examples/rest2/symbol-details.js b/examples/rest2/symbol-details.js new file mode 100644 index 00000000..db98cc90 --- /dev/null +++ b/examples/rest2/symbol-details.js @@ -0,0 +1,30 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const { debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + transform: true + }) + debug('fetching symbol details...') + + const details = await rest.symbolDetails() + + debugTable({ + headers: [ + 'Pair', 'Initial Margin', 'Min Margin', 'Max Order', + 'Min Order', 'Margin' + ], + + rows: details.map(({ + pair, initialMargin, minimumMargin, // eslint-disable-line + maximumOrderSize, minimumOrderSize, margin // eslint-disable-line + }) => [ + pair.toUpperCase(), initialMargin, minimumMargin, // eslint-disable-line + maximumOrderSize, minimumOrderSize, margin ? 'Y' : 'N' // eslint-disable-line + ]) + }) +} + +execute() diff --git a/examples/rest2/symbols.js b/examples/rest2/symbols.js new file mode 100644 index 00000000..2ad8d909 --- /dev/null +++ b/examples/rest2/symbols.js @@ -0,0 +1,18 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const { debug } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + transform: true + }) + debug('fetching symbol list...') + + const symbols = await rest.symbols() + + debug('read %d symbols', symbols.length) + debug('%s', symbols.map(s => `t${s.toUpperCase()}`).join(', ')) +} + +execute() diff --git a/examples/rest2/tickers.js b/examples/rest2/tickers.js new file mode 100644 index 00000000..a033ff8d --- /dev/null +++ b/examples/rest2/tickers.js @@ -0,0 +1,48 @@ +'use strict' + +const { preparePrice, prepareAmount } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + transform: true + }) + const filterByMarket = null + + debug('fetching symbol list...') + + const rawSymbols = await rest.symbols() + + debug('read %d symbols', rawSymbols.length) + + const symbols = rawSymbols + .map(s => `t${s.toUpperCase()}`) + .filter(s => ( + !filterByMarket || (s === filterByMarket) + )) + + if (symbols.length === 0) { + return debug('no tickers match provided filters') + } + + debug('fetching %d tickers...', symbols.length) + + const tickers = await rest.tickers(symbols) + + debugTable({ + colWidths: [10, 14, 14, 14, 14, 14, 14, 18, 18], + headers: [ + 'Symbol', 'Last', 'High', 'Low', 'Daily Change', 'Bid', 'Ask', 'Bid Size', + 'Ask Size' + ], + + rows: tickers.map(t => ([ + t.symbol, preparePrice(t.lastPrice), preparePrice(t.high), + preparePrice(t.low), (t.dailyChange * 100).toFixed(2), preparePrice(t.bid), + preparePrice(t.ask), prepareAmount(t.bidSize), prepareAmount(t.askSize) + ])) + }) +} + +execute() diff --git a/examples/rest2/trade-history.js b/examples/rest2/trade-history.js new file mode 100644 index 00000000..04659571 --- /dev/null +++ b/examples/rest2/trade-history.js @@ -0,0 +1,45 @@ +'use strict' + +const { prepareAmount, preparePrice } = require('bfx-api-node-util') +const _isEmpty = require('lodash/isEmpty') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +const START = Date.now() - (30 * 24 * 60 * 60 * 1000 * 1000) +const END = Date.now() +const LIMIT = 25 + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const symbol = 'tBTCUSD' + + if (_isEmpty(symbol)) { + return debug('symbol required') + } + + debug('fetching 30d trade history for %s...', symbol) + + const trades = await rest.accountTrades(symbol, START, END, LIMIT) + + if (trades.length === 0) { + return debug('no historical trades for %s', symbol) + } + + debugTable({ + headers: [ + 'Trade ID', 'Order ID', 'Created', 'Exec Amount', 'Exec Price', 'Fee' + ], + + rows: trades.map(t => [ + t.id, t.orderID, new Date(t.mtsCreate).toLocaleString(), + prepareAmount(t.execAmount), preparePrice(t.execPrice), + `${t.fee} ${t.feeCurrency}` + ]) + }) +} + +execute() diff --git a/examples/rest2/transfer.js b/examples/rest2/transfer.js new file mode 100644 index 00000000..a151b21d --- /dev/null +++ b/examples/rest2/transfer.js @@ -0,0 +1,36 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const { fromType, fromCCY, toType, toCCY, amount } = { + fromType: 'deposit', + fromCCY: 'USD', + toType: 'trading', + toCCY: 'USD', + amount: 1 + } + + debug( + 'transferring %f from %s %s to %s %s', + amount, fromType, fromCCY, toType, toCCY + ) + + await rest.transfer({ + amount: `${amount}`, + from: fromType, + currency: fromCCY, + to: toType, + currencyTo: toCCY + }) + + debug('done!') +} + +execute() diff --git a/examples/rest2/wallet.js b/examples/rest2/wallet.js new file mode 100644 index 00000000..f3c2a089 --- /dev/null +++ b/examples/rest2/wallet.js @@ -0,0 +1,45 @@ +'use strict' + +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + debug('Submitting new order...') + + // get new deposit address + const address = await rest.getDepositAddress({ + wallet: 'exchange', + method: 'bitcoin', + opRenew: 0 + }) + + debug(`new wallet address ${address}`) + + // transfer between accounts + const transferConfirmation = await rest.transfer({ + from: 'exchange', + to: 'margin', + amount: 10, + currency: 'BTC', + currencyTo: 'BTC' + }) + + debug('transfer confirmed: %j', transferConfirmation) + + // withdraw + const withdrawalConfirmation = await rest.withdraw({ + wallet: 'exchange', + method: 'bitcoin', + amount: 2, + address: '1MUz4VMYui5qY1mxUiG8BQ1Luv6tqkvaiL' + }) + + debug('withdraw confirmed: %j', withdrawalConfirmation) +} + +execute() diff --git a/examples/rest2/wallets.js b/examples/rest2/wallets.js new file mode 100644 index 00000000..f481ded5 --- /dev/null +++ b/examples/rest2/wallets.js @@ -0,0 +1,98 @@ +'use strict' + +const _uniq = require('lodash/uniq') +const _capitalize = require('lodash/capitalize') +const _isFinite = require('lodash/isFinite') +const _isEmpty = require('lodash/isEmpty') +const { prepareAmount } = require('bfx-api-node-util') +const { RESTv2 } = require('../../index') +const { args: { apiKey, apiSecret }, debug, debugTable } = require('../util/setup') + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + const { + valueCCY, hideZeroBalances, filterByType, filterByCurrency + } = { + hideZeroBalances: true, + filterByType: false, + filterByCurrency: false, + valueCCY: 'USD' + } + + const symbolForWallet = w => `t${w.currency}${valueCCY}` + + debug('fetching balances') + + const allWallets = await rest.wallets() // actual balance fetch + const balances = allWallets.filter(w => !( // filter as requested + (hideZeroBalances && +w.balance === 0) || + (!_isEmpty(filterByType) && (w.type.toLowerCase() !== filterByType.toLowerCase())) || + (!_isEmpty(filterByCurrency) && (w.currency.toLowerCase() !== filterByCurrency.toLowerCase())) + )).map(w => ({ + ...w, + currency: w.currency.toUpperCase(), + inValueCurrency: w.currency.toUpperCase() === valueCCY + })) + + if (balances.length === 0) { + return debug('no wallets match provided filters') + } + + debug('found %d balances', balances.length) + + // Pull in ticker data for balances which are not in the requested value ccy + // Balance in BTC, value in USD -> We need to fetch tBTCUSD (last price) + const lastPrices = {} + const balancesToConvert = balances.filter(w => w.currency !== valueCCY) + const symbols = _uniq(balancesToConvert.map(symbolForWallet)) + + if (symbols.length > 0) { + debug('fetching tickers for: %s', symbols.join(', ')) + const tickers = await rest.tickers(symbols) + tickers.forEach(({ symbol, lastPrice }) => (lastPrices[symbol] = +lastPrice)) + } + + let totalValue = 0 + const rows = balances.map(({ currency, type, balance, balanceAvailable }) => { + const value = currency !== valueCCY + ? (lastPrices[symbolForWallet({ currency })] * +balance) || 0 + : +balance + + totalValue += value + + return [ + _capitalize(type), + currency, + prepareAmount(balance), + prepareAmount(balanceAvailable), + + ...(_isFinite(value) + ? [ + prepareAmount(value), + currency !== valueCCY + ? prepareAmount(lastPrices[symbolForWallet({ currency })]) + : 1 + ] + : [ + '-', + '-' + ]) + ] + }) + + debugTable({ + rows, + headers: [ + 'Type', 'Symbol', 'Total', 'Available', `Value (${valueCCY})`, + `Unit Price (${valueCCY})` + ] + }) + + debug('total value: %d %s', prepareAmount(totalValue), valueCCY) +} + +execute() diff --git a/examples/trade.js b/examples/trade.js deleted file mode 100644 index c2444d78..00000000 --- a/examples/trade.js +++ /dev/null @@ -1,12 +0,0 @@ -const BitfinexWS = require('bitfinex-api-node').WS -const {version} = require('bitfinex-api-node/package.json') -const bws = new BitfinexWS() -console.log(process.cwd()) -console.log(version) -bws.on('open', () => { - bws.subscribeTrades('BTCUSD') -}) - -bws.on('trade', (pair, trade) => { - console.log('Trade:', trade) -}) diff --git a/examples/util/arg_from_cli.js b/examples/util/arg_from_cli.js new file mode 100644 index 00000000..e87cb679 --- /dev/null +++ b/examples/util/arg_from_cli.js @@ -0,0 +1,23 @@ +'use strict' + +const _isEmpty = require('lodash/isEmpty') +const _isFunction = require('lodash/isFunction') + +/** + * Grabs an argument from the arguments list if we've been executed via node or + * npm + * + * @param {number} index - starting after invocation (2) + * @param {string} def - fallback value if none found/not supported + * @param {Function?} parser - optional, used to process value if provided + * @returns {string} value + */ +module.exports = (index, def, parser) => { + const val = /node/.test(process.argv[0]) || /npm/.test(process.argv[0]) + ? _isEmpty(process.argv[2 + index]) ? def : process.argv[2 + index] + : def + + return _isFunction(parser) + ? parser(val) + : val +} diff --git a/examples/util/args_from_env.js b/examples/util/args_from_env.js new file mode 100644 index 00000000..f33e2df8 --- /dev/null +++ b/examples/util/args_from_env.js @@ -0,0 +1,28 @@ +'use strict' + +const _isString = require('lodash/isString') +const _isEmpty = require('lodash/isEmpty') +const { SocksProxyAgent } = require('socks-proxy-agent') + +const validArg = v => _isString(v) && !_isEmpty(v) + +/** + * Grabs RESTv2/WSv2 constructor arguments from the environment, configuring + * the api credentials, connection agent, and connection URL + * + * @param {string?} urlKey - name of env var holding the connection URL + * @returns {object} envArgs + */ +module.exports = (urlKey) => { + const { API_KEY, API_SECRET, SOCKS_PROXY_URL } = process.env + const URL = process.env[urlKey] + const agent = validArg(SOCKS_PROXY_URL) && new SocksProxyAgent(SOCKS_PROXY_URL) + const envArgs = {} + + if (agent) envArgs.agent = agent + if (validArg(URL)) envArgs.url = URL + if (validArg(API_KEY)) envArgs.apiKey = API_KEY + if (validArg(API_SECRET)) envArgs.apiSecret = API_SECRET + + return envArgs +} diff --git a/examples/util/debug.js b/examples/util/debug.js new file mode 100644 index 00000000..21e11a72 --- /dev/null +++ b/examples/util/debug.js @@ -0,0 +1,5 @@ +'use strict' + +// Proxy to allow external stubbing +const debug = require('debug') +module.exports = { get: () => debug } diff --git a/examples/util/debug_table.js b/examples/util/debug_table.js new file mode 100644 index 00000000..36a289f8 --- /dev/null +++ b/examples/util/debug_table.js @@ -0,0 +1,25 @@ +'use strict' + +const Table = require('cli-table3') + +/** + * Generates a CLI table and logs it to the console + * + * @param {object} args - arguments + * @param {object} args.rows - data + * @param {object} args.headers - column headers + * @param {object} args.debug - log function + * @returns {string} table + */ +module.exports = ({ rows, headers, debug }) => { + const t = new Table({ + head: headers, + colWidths: [] // auto-compute + }) + + rows.forEach(r => t.push(r)) + + const str = t.toString() + str.split('\n').map(l => debug('%s', l)) + return str +} diff --git a/examples/util/setup.js b/examples/util/setup.js new file mode 100644 index 00000000..1c5c9dbc --- /dev/null +++ b/examples/util/setup.js @@ -0,0 +1,45 @@ +'use strict' + +const dotenv = require('dotenv') +const Readline = require('readline-promise').default +const argsFromEnv = require('./args_from_env') +const debugTableUtil = require('./debug_table') +const D = require('./debug').get() +const debug = D('>') +debug.enabled = true + +dotenv.config() + +/** + * Log a table to the console + * + * @param {object} args - arguments + * @param {object[]} args.rows - data, can be specified as 2nd param + * @param {string[]} args.headers - column labels + * @param {number[]} args.widths - column widths + * @param {object[]} extraRows - optional row spec as 2nd param + */ +const debugTable = ({ rows = [], headers, widths }, extraRows = []) => { + debug('') + debugTableUtil({ + rows: [...rows, ...extraRows], + headers, + widths, + debug + }) + debug('') +} +module.exports = { + get args () { + return argsFromEnv() + }, + debug, + debugTable, + get readline () { + return Readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false + }) + } +} diff --git a/examples/websockets-2.js b/examples/websockets-2.js deleted file mode 100644 index 37547aa5..00000000 --- a/examples/websockets-2.js +++ /dev/null @@ -1,35 +0,0 @@ -// uses the latest version of the websocket API - -// const BFX = require ('../') -const BFX = require('bitfinex-api-node') - -const API_KEY = null -const API_SECRET = null - -const API_VERSION = 2 - -const bws = new BFX(API_KEY, API_SECRET, API_VERSION).ws - -bws.on('open', () => { - bws.subscribeTrades('BTCUSD') - bws.subscribeOrderBook('BTCUSD') - bws.subscribeTicker('LTCBTC') -}) - -bws.on('trade', (pair, trade) => { - console.log('Trade:', trade) -}) - -bws.on('orderbook', (pair, book) => { - console.log('Order book:', book) -}) - -bws.on('ticker', (pair, ticker) => { - console.log('Ticker:', ticker) -}) - -bws.on('subscribed', (data) => { - console.log('New subscription', data) -}) - -bws.on('error', console.error) diff --git a/examples/websockets.js b/examples/websockets.js deleted file mode 100644 index c7e73e31..00000000 --- a/examples/websockets.js +++ /dev/null @@ -1,27 +0,0 @@ -const BitfinexWS = require('bitfinex-api-node') - -const bws = new BitfinexWS().ws - -bws.on('open', () => { - bws.subscribeTrades('BTCUSD') - bws.subscribeOrderBook('BTCUSD') - bws.subscribeTicker('LTCBTC') -}) - -bws.on('trade', (pair, trade) => { - console.log('Trade:', trade) -}) - -bws.on('orderbook', (pair, book) => { - console.log('Order book:', book) -}) - -bws.on('ticker', (pair, ticker) => { - console.log('Ticker:', ticker) -}) - -bws.on('subscribed', (data) => { - console.log('New subscription', data) -}) - -bws.on('error', console.error) diff --git a/examples/ws2/atomic_order_update.js b/examples/ws2/atomic_order_update.js new file mode 100644 index 00000000..08d55973 --- /dev/null +++ b/examples/ws2/atomic_order_update.js @@ -0,0 +1,73 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +const SYMBOL = 'tBTCUSD' + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true, + manageOrderBooks: true, + + packetWDDelay: 10 * 1000, + autoReconnect: true, + seqAudit: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + const orderSent = false + + await ws.subscribeOrderBook(SYMBOL, 'P0', '25') + debug('subscribed to order book %s:P0:25', SYMBOL) + + ws.onOrderBook({ symbol: SYMBOL }, async (ob) => { + const topBidL = ob.topBidLevel() + + if (topBidL === null || orderSent) { + return + } + + debug('taking out price level: %j', topBidL) + + const o = new Order({ + symbol: SYMBOL, + type: Order.type.EXCHANGE_LIMIT, + price: topBidL[0], + amount: topBidL[2] * -1.1 // sell through top bid + }, ws) + + debug('submitting: %s', o.toString()) + + o.registerListeners() + await o.submit() + + debug('order submitted') + + o.once('update', async (o) => { + debug('got order update: %s', o.status) + + if (!o.isPartiallyFilled()) { + return + } + + debug('order is partially filled, amount %f', o.amount) + debug('increasing amount w/ delta %f', o.amount * 2) + + await o.update({ delta: `${o.amount * 2}` }) + debug('order updated, new amount %f', o.amount) + debug('setting price to %f', o.price * 1.05) + + await o.update({ price: `${o.price * 1.05}` }) + debug('order updated, new price %f', o.price) + }) + }) + await ws.close() +} + +execute() diff --git a/examples/ws2/auth.js b/examples/ws2/auth.js new file mode 100644 index 00000000..6bf3901d --- /dev/null +++ b/examples/ws2/auth.js @@ -0,0 +1,28 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + + // register a callback for any order snapshot that comes in (account orders) + ws.onOrderSnapshot({}, (orders) => { + debug(`order snapshot: ${JSON.stringify(orders, null, 2)}`) + }) + + await ws.open() + debug('open') + + await ws.auth() + debug('authenticated') + + // do something with authenticated ws stream + await ws.close() +} + +execute() diff --git a/examples/ws2/calc.js b/examples/ws2/calc.js new file mode 100644 index 00000000..099323c4 --- /dev/null +++ b/examples/ws2/calc.js @@ -0,0 +1,31 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + await new Promise(resolve => setTimeout(resolve, 5 * 1000)) + + ws.requestCalc([ + 'margin_sym_tBTCUSD', + 'position_tBTCUSD', + 'wallet_margin_BTC', + 'wallet_funding_USD' + ]) + + // Watch log output for balance update packets (wu, miu, etc) + debug('sent calc, closing in 3s...') + + await new Promise(resolve => setTimeout(resolve, 3 * 1000)) + await ws.close() +} + +execute() diff --git a/examples/ws2/cancel_all.js b/examples/ws2/cancel_all.js new file mode 100644 index 00000000..b418fbcd --- /dev/null +++ b/examples/ws2/cancel_all.js @@ -0,0 +1,62 @@ +'use strict' + +const _isEmpty = require('lodash/isEmpty') +const { args: { apiKey, apiSecret }, debug, readline } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + const filterByMarket = null + + debug('awaiting order snapshot...') + + const allOrders = await new Promise((resolve) => { + ws.onOrderSnapshot({}, resolve) + return ws.auth() + }) + + if (allOrders.length === 0) { + debug('no orders to cancel') + await ws.close() + readline.close() + return + } + + const orders = _isEmpty(filterByMarket) + ? allOrders + : allOrders.filter(o => o.symbol === filterByMarket) + + debug('received snapshot (%d orders)', orders.length) + debug('') + orders.forEach(o => debug('%s', o.toString())) + debug('') + + const confirm = await readline.questionAsync( + '> Are you sure you want to close the orders(s) listed above? ' + ) + + if (confirm.toLowerCase()[0] !== 'y') { + return + } + + debug('') + debug('cancelling all..') + + const confirmations = await ws.cancelOrders(orders) + + debug( + 'done! cancelled the following order IDs: %s', + confirmations.map(o => o[0]).join(', ') + ) + await ws.close() + readline.close() +} + +execute() diff --git a/examples/ws2/cancel_all_buf.js b/examples/ws2/cancel_all_buf.js new file mode 100644 index 00000000..cee7b7be --- /dev/null +++ b/examples/ws2/cancel_all_buf.js @@ -0,0 +1,30 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true, + orderOpBufferDelay: 250 + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + ws.onOrderSnapshot({}, async (snapshot) => { + if (snapshot.length === 0) { + debug('no orders to cancel') + } else { + debug('canceling %d orders', snapshot.length) + + await ws.cancelOrders(snapshot) + debug('cancelled all orders') + } + await ws.close() + }) +} + +execute() diff --git a/examples/ws2/candles.js b/examples/ws2/candles.js new file mode 100644 index 00000000..3f291c3d --- /dev/null +++ b/examples/ws2/candles.js @@ -0,0 +1,39 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + manageCandles: true, // enable candle dataset persistence/management + transform: true // converts ws data arrays to Candle models (and others) + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + const market = 'tBTCUSD' + const tf = '5m' + const candleKey = `trade:${tf}:${market}` + let prevTS = null + + ws.onCandle({ key: candleKey }, (candles) => { + if (candles[0].mts === prevTS) { + return + } + + const c = candles[1] // report previous candle + + debug('%s %s open: %f, high: %f, low: %f, close: %f, volume: %f', + candleKey, new Date(c.mts).toLocaleTimeString(), + c.open, c.high, c.low, c.close, c.volume + ) + + prevTS = candles[0].mts + }) + + await ws.subscribeCandles(candleKey) +} + +execute() diff --git a/examples/ws2/funding_info.js b/examples/ws2/funding_info.js new file mode 100644 index 00000000..74e76c9a --- /dev/null +++ b/examples/ws2/funding_info.js @@ -0,0 +1,25 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') +const symbol = 'fUSD' + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + ws.onFundingInfoUpdate({}, fi => { + debug('fl: %j', fi.toJS()) + ws.close() + }) + + ws.requestCalc([`funding_sym_${symbol}`]) +} + +execute() diff --git a/examples/ws2/info_events.js b/examples/ws2/info_events.js new file mode 100644 index 00000000..e2872e9d --- /dev/null +++ b/examples/ws2/info_events.js @@ -0,0 +1,33 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + autoReconnect: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + ws.onMaintenanceStart(() => { + debug('info: maintenance period started') + // pause activity untill further notice + }) + + ws.onMaintenanceEnd(() => { + debug('info: maintenance period ended') + // resume activity + }) + + ws.onServerRestart(() => { + debug('info: bitfinex ws server restarted') + // await ws.reconnect() // if not using autoReconnect + }) + await ws.close() +} + +execute() diff --git a/examples/ws2/liquidations.js b/examples/ws2/liquidations.js new file mode 100644 index 00000000..29944c1f --- /dev/null +++ b/examples/ws2/liquidations.js @@ -0,0 +1,24 @@ +'use strict' + +const { Liquidations } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + ws.onStatus({ key: 'liq:global' }, (data) => { + data.forEach(liq => ( + debug('liquidation: %s', new Liquidations(liq).toString()) + )) + }) + + await ws.subscribeStatus('liq:global') +} + +execute() diff --git a/examples/ws2/notifications.js b/examples/ws2/notifications.js new file mode 100644 index 00000000..1ff4ffc7 --- /dev/null +++ b/examples/ws2/notifications.js @@ -0,0 +1,21 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + ws.onNotification({ type: '*' }, (n) => { + debug('recv notification: %j', n.toJS()) + }) +} + +execute() diff --git a/examples/ws2/notify_ui.js b/examples/ws2/notify_ui.js new file mode 100644 index 00000000..04dccf96 --- /dev/null +++ b/examples/ws2/notify_ui.js @@ -0,0 +1,25 @@ +'use strict' + +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + ws.notifyUI({ + type: 'success', + message: 'This is a test notification sent via the WSv2 API' + }) + + debug('notification sent') + await ws.close() +} + +execute() diff --git a/examples/ws2/ob_checksum.js b/examples/ws2/ob_checksum.js new file mode 100644 index 00000000..2dd1a0a9 --- /dev/null +++ b/examples/ws2/ob_checksum.js @@ -0,0 +1,30 @@ +'use strict' + +const { debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +const SYMBOL = 'tXRPBTC' +const PRECISION = 'P0' +const LENGTH = '25' + +async function execute () { + const ws = new WSv2({ + transform: true, + manageOrderbooks: true // managed OBs are verified against incoming checksums + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + ws.onOrderBookChecksum({ + symbol: SYMBOL, + prec: PRECISION, + len: LENGTH + }, cs => { + debug('recv valid cs for %s:%s:%s %d', SYMBOL, PRECISION, LENGTH, cs) + }) + + await ws.enableFlag(WSv2.flags.CHECKSUM) + await ws.subscribeOrderBook(SYMBOL, PRECISION, LENGTH) +} + +execute() diff --git a/examples/ws2/oc_multi.js b/examples/ws2/oc_multi.js new file mode 100644 index 00000000..3ec22c6f --- /dev/null +++ b/examples/ws2/oc_multi.js @@ -0,0 +1,66 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +const oA = new Order({ + symbol: 'tBTCUSD', + price: 200, + amount: 1, + type: 'EXCHANGE LIMIT' +}) + +const oB = new Order({ + symbol: 'tETHUSD', + price: 50, + amount: 1, + type: 'EXCHANGE LIMIT' +}) + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + oA.registerListeners(ws) + oB.registerListeners(ws) + + await oA.submit() + debug('created order A') + + await oB.submit() + debug('created order B') + + let oAClosed = false + let oBClosed = false + + oA.on('close', async () => { + debug('order A cancelled: %s', oA.status) + + oAClosed = true + if (oBClosed) return ws.close() + }) + + oB.on('close', async () => { + debug('order B cancelled: %s', oB.status) + + oBClosed = true + if (oAClosed) return ws.close() + }) + + ws.send([0, 'oc_multi', null, { + id: [oA.id, oB.id] + }]) + + debug('sent oc_multi for orders A & B') + + await ws.close() +} + +execute() diff --git a/examples/ws2/oco-order.js b/examples/ws2/oco-order.js new file mode 100644 index 00000000..b0b70b4d --- /dev/null +++ b/examples/ws2/oco-order.js @@ -0,0 +1,60 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +// Build new order +const o = new Order({ + cid: Date.now(), + symbol: 'tBTCUSD', + type: Order.type.EXCHANGE_LIMIT, + amount: -0.05, + + oco: true, + price: 2000, + priceAuxLimit: 1000 +}) + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + o.registerListeners(ws) // enable automatic updates + + let orderClosed = false + + o.on('update', () => { + debug('updated: %s', o.toString()) + }) + + o.on('close', () => { + debug('order closed: %s', o.status) + orderClosed = true + }) + + debug('submitting order %d', o.cid) + await o.submit() + debug('got submit confirmation for order %d [%d]', o.cid, o.id) + + // wait a bit... + await new Promise(resolve => setTimeout(resolve, 2 * 1000)) + + if (orderClosed) { + return debug('order closed prematurely; did it auto-fill?') + } + + debug('canceling...') + + await o.cancel() + debug('got cancel confirmation for order %d', o.cid) + await ws.close() +} + +execute() diff --git a/examples/ws2/order_book_viz.js b/examples/ws2/order_book_viz.js new file mode 100644 index 00000000..9872ff1e --- /dev/null +++ b/examples/ws2/order_book_viz.js @@ -0,0 +1,62 @@ +'use strict' + +const blessed = require('blessed') +const blessedContrib = require('blessed-contrib') +const _isEmpty = require('lodash/isEmpty') +const _reverse = require('lodash/reverse') +const { preparePrice, prepareAmount } = require('bfx-api-node-util') +const { debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + transform: true, + manageOrderBooks: true // tell the ws client to maintain full sorted OBs + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + const market = 'tBTCUSD' + + if (_isEmpty(market)) { + throw new Error('market required') + } + + const screen = blessed.screen() + const bookTable = blessedContrib.table({ + fg: 'white', + label: `Order Book ${market}`, + border: { + type: 'line', + fg: 'green' + }, + + columnSpacing: 5, + columnWidth: [10, 20, 10] + }) + + screen.append(bookTable) + + ws.onOrderBook({ symbol: market }, (ob) => { + const data = [] + + _reverse(ob.asks).forEach(row => data.push([ + preparePrice(row[0]), prepareAmount(row[2]), row[1] + ])) + + ob.bids.forEach(row => data.push([ + preparePrice(row[0]), prepareAmount(row[2]), row[1] + ])) + + bookTable.setData({ + headers: ['Price', 'Amount', 'Count'], + data + }) + + screen.render() + }) + + await ws.subscribeOrderBook(market, 'P0', '25') +} + +execute() diff --git a/examples/ws2/order_books.js b/examples/ws2/order_books.js new file mode 100644 index 00000000..3347b91d --- /dev/null +++ b/examples/ws2/order_books.js @@ -0,0 +1,34 @@ +'use strict' + +const { debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + transform: true, // auto-transform array OBs to OrderBook objects + manageOrderBooks: true // tell the ws client to maintain full sorted OBs + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + let lastMidPrice = -1 + let midPrice + + // 'ob' is a full OrderBook instance, with sorted arrays 'bids' & 'asks' + ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => { + midPrice = ob.midPrice() + + if (midPrice !== lastMidPrice) { + debug( + 'BTCUSD mid price: %d (bid: %d, ask: %d)', + midPrice, ob.bids[0][0], ob.asks[0][0] + ) + } + + lastMidPrice = midPrice + }) + + await ws.subscribeOrderBook('tBTCUSD') +} + +execute() diff --git a/examples/ws2/order_tif.js b/examples/ws2/order_tif.js new file mode 100644 index 00000000..f3c425be --- /dev/null +++ b/examples/ws2/order_tif.js @@ -0,0 +1,41 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +const o = new Order({ + cid: Date.now(), + symbol: 'tBTCUSD', + price: 17833.5, + amount: -0.02, + type: Order.type.LIMIT, + tif: '2019-03-08 15:00:00' +}) + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + o.registerListeners(ws) + + o.on('update', () => debug('updated: %s', o.toString())) + o.on('close', () => debug('order closed: %s', o.status)) + + debug('submitting order %d', o.cid) + await o.submit() + + debug( + 'got submit confirmation for order %d [%d] [tif: %d]', + o.cid, o.id, o.mtsTIF + ) + await ws.close() +} + +execute() diff --git a/examples/ws2/orders.js b/examples/ws2/orders.js new file mode 100644 index 00000000..091850de --- /dev/null +++ b/examples/ws2/orders.js @@ -0,0 +1,56 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +// Build new order +const o = new Order({ + cid: Date.now(), + symbol: 'tBTCUSD', + price: 589.10, + amount: -0.02, + type: Order.type.EXCHANGE_LIMIT +}) + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + let orderClosed = false + + // Enable automatic updates + o.registerListeners(ws) + + o.on('update', () => debug('updated: %s', o.toString())) + o.on('close', () => { + debug('order closed: %s', o.status) + orderClosed = true + }) + + debug('submitting order %d', o.cid) + + await o.submit() + + debug('got submit confirmation for order %d [%d]', o.cid, o.id) + + // wait a bit... + await new Promise(resolve => setTimeout(resolve, 2 * 1000)) + + if (orderClosed) { + return debug('order closed prematurely; did it auto-fill?') + } + + debug('canceling...') + await o.cancel() + debug('got cancel confirmation for order %d', o.cid) + await ws.close() +} + +execute() diff --git a/examples/ws2/ox_multi.js b/examples/ws2/ox_multi.js new file mode 100644 index 00000000..09ab7f58 --- /dev/null +++ b/examples/ws2/ox_multi.js @@ -0,0 +1,84 @@ +'use strict' + +const { Order } = require('bfx-api-node-models') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +const oA = new Order({ + symbol: 'tBTCUSD', + price: 200, + amount: 1, + type: 'EXCHANGE LIMIT' +}) + +const oB = new Order({ + symbol: 'tETHUSD', + price: 50, + amount: 1, + type: 'EXCHANGE LIMIT' +}) + +const oC = new Order({ + symbol: 'tETHBTC', + price: 1, + amount: 1, + type: 'EXCHANGE LIMIT' +}) + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + oA.registerListeners(ws) + oB.registerListeners(ws) + oC.registerListeners(ws) + + let oAClosed = false + let oBClosed = false + let oCClosed = false + + oA.on('close', async () => { + debug('order A cancelled: %s', oA.status) + + oAClosed = true + if (oBClosed && oCClosed) return ws.close() + }) + + oB.on('close', async () => { + debug('order B cancelled: %s', oB.status) + + oBClosed = true + if (oAClosed && oCClosed) return ws.close() + }) + + oC.on('close', async () => { + debug('order C cancelled: %s', oC.status) + + oCClosed = true + if (oAClosed && oBClosed) return ws.close() + }) + + await oA.submit() + debug('created order A') + + await oB.submit() + debug('created order B') + + await oC.submit() + debug('created order C') + + ws.submitOrderMultiOp([ + ['oc', { id: oA.id }], + ['oc_multi', { id: [oB.id, oC.id] }] + ]) + + debug('sent ox_multi to cancel order A and orders [B, C]') +} + +execute() diff --git a/examples/ws2/sequencing.js b/examples/ws2/sequencing.js new file mode 100644 index 00000000..1aa6f2f4 --- /dev/null +++ b/examples/ws2/sequencing.js @@ -0,0 +1,52 @@ +'use strict' + +const _isArray = require('lodash/isArray') +const _isFinite = require('lodash/isFinite') +const { args: { apiKey, apiSecret }, debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + apiKey, + apiSecret, + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + await ws.auth() + + // Enables internal sequence tracking; an error will be emitted if there is a + // seq # mis-match + await ws.enableSequencing({ audit: true }) + + if (!ws.isFlagEnabled(65536)) { + throw new Error('seq enable succeeded, but flag not updated') + } + + debug('sequencing enabled') + + await ws.subscribeTrades('tBTCUSD') + + ws.on('message', (msg) => { + if (!_isArray(msg)) return // only array messages have sequence #s + + // auth seq number, available as the last element on chan 0 packets + const authSeq = msg[0] === 0 && msg[1] !== 'hb' + ? msg[msg.length - 1] + : NaN + + // public seq number, last or 2nd to last element on all packets + const seq = msg[0] === 0 && msg[1] !== 'hb' + ? msg[msg.length - 2] + : msg[msg.length - 1] + + if (!_isFinite(authSeq)) { + debug('recv public seq # %d', seq) + } else { + debug('recv public seq # %d, auth seq # %d', seq, authSeq) + } + }) + await ws.close() +} + +execute() diff --git a/examples/ws2/tickers.js b/examples/ws2/tickers.js new file mode 100644 index 00000000..ef601cfa --- /dev/null +++ b/examples/ws2/tickers.js @@ -0,0 +1,26 @@ +'use strict' + +const { debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + ws.onTicker({ symbol: 'tETHUSD' }, (ticker) => { + debug('ETH/USD ticker: %j', ticker.toJS()) + }) + + ws.onTicker({ symbol: 'fUSD' }, (ticker) => { + debug('fUSD ticker: %j', ticker.toJS()) + }) + + await ws.subscribeTicker('tETHUSD') + await ws.subscribeTicker('fUSD') + await ws.close() +} + +execute() diff --git a/examples/ws2/trades.js b/examples/ws2/trades.js new file mode 100644 index 00000000..c47a7b93 --- /dev/null +++ b/examples/ws2/trades.js @@ -0,0 +1,32 @@ +'use strict' + +const { debug } = require('../util/setup') +const WSv2 = require('../../lib/transports/ws2') + +async function execute () { + const ws = new WSv2({ + transform: true + }) + ws.on('error', e => debug('WSv2 error: %s', e.message | e)) + await ws.open() + + const market = 'tBTCUSD' + + if (market[0] === 't') { + ws.onTradeEntry({ symbol: market }, (trade) => { + debug('trade on %s: %s', market, trade.toString()) + }) + } else { + ws.onFundingTradeEntry({ symbol: market }, (trade) => { + debug('funding trade: %s', trade.toString()) + }) + } + + ws.onAccountTradeEntry({ symbol: market }, (trade) => { + debug('account trade: %s', trade.toString()) + }) + + await ws.subscribeTrades(market) +} + +execute() diff --git a/examples/ws2_manager.js b/examples/ws2_manager.js new file mode 100644 index 00000000..e9d3897c --- /dev/null +++ b/examples/ws2_manager.js @@ -0,0 +1,75 @@ +'use strict' + +process.env.DEBUG = 'bfx:examples:*' + +const _flatten = require('lodash/flatten') +const { RESTv2 } = require('../../index') +const { args, debug } = require('./util/setup') +const Manager = require('../lib/ws2_manager') + +const { apiKey, apiSecret } = args + +async function execute () { + const rest = new RESTv2({ + apiKey, + apiSecret, + transform: true + }) + + debug('fetching symbol details...') + + const details = await rest.symbolDetails() + const symbols = details.map(d => `t${d.pair.toUpperCase()}`) + const timeFrames = ['1m', '5m', '30m', '1h', '6h'] + const keys = _flatten(symbols.map(s => { + return timeFrames.map(tf => `trade:${tf}:${s}`) + })) + + const m = new Manager({ ...args }) + + m.on('error', (err) => { + debug('error: %s', err) + }) + + m.once('open', () => { + debug('open') + + keys.forEach(key => { + m.subscribeCandles(key) + m.onCandle({ key }, (candles) => { + debug('recv %d candles on channel %s', candles.length, key) + }) + }) + + symbols.forEach(symbol => { + m.subscribeTrades(symbol) + m.onTrades({ symbol }, (trades) => { + debug('recv %d trades on channel %s', trades.length, symbol) + }) + }) + + symbols.forEach(symbol => { + m.subscribeTicker(symbol) + m.onTicker({ symbol }, (ticker) => { + debug('recv ticker on channel %s: %j', symbol, ticker) + }) + }) + + symbols.forEach(symbol => { + m.subscribeOrderBook(symbol) + m.onOrderBook({ symbol }, (update) => { + debug('recv book update on channel %s: %j', symbol, update) + }) + }) + + setInterval(() => { + debug('num keys: %d', keys.length) + debug('num sockets: %d', m.getNumSockets()) + debug('socket info: %j', m.getSocketInfo()) + }, 5000) + }) + + m.openSocket() +} + +execute() diff --git a/index.js b/index.js index 0f5bac18..cfa040f2 100644 --- a/index.js +++ b/index.js @@ -1,25 +1,126 @@ 'use strict' -const REST = require('./rest.js') -const WS = require('./ws.js') -const REST2 = require('./rest2.js') -const WS2 = require('./ws2.js') +const { RESTv1, RESTv2 } = require('bfx-api-node-rest') +const WSv1 = require('bfx-api-node-ws1') +const WSv2 = require('./lib/transports/ws2') +const WS2Manager = require('./lib/ws2_manager') +/** + * Provides access to versions 1 & 2 of the HTTP & WebSocket Bitfinex APIs + */ class BFX { - constructor (apiKey, apiSecret, version) { - this.apiKey = apiKey - this.apiSecret = apiSecret - - // eslint-disable-next-line eqeqeq - if (version == 2) { // XXX make strict on next major version bump / breaking change - this.rest = new REST2(this.apiKey, this.apiSecret) - this.ws = new WS2(this.apiKey, this.apiSecret) - this.ws.open() - } else { - this.rest = new REST(this.apiKey, this.apiSecret) - this.ws = new WS(this.apiKey, this.apiSecret) + /** + * @param {object} [opts] - options + * @param {string} [opts.apiKey] - API key + * @param {string} [opts.apiSecret] - API secret + * @param {string} [opts.authToken] - optional auth option + * @param {string} [opts.company] - optional auth option + * @param {boolean} [opts.transform] - if true, packets are converted to models + * @param {object} [opts.ws] - ws transport options + * @param {object} [opts.rest] - rest transport options + */ + constructor (opts = { + apiKey: '', + apiSecret: '', + authToken: '', + company: '', + transform: false, + ws: {}, + rest: {} + }) { + if (opts.constructor.name !== 'Object') { + throw new Error([ + 'constructor takes an object since version 2.0.0, see:', + 'https://github.com/bitfinexcom/bitfinex-api-node#version-200-breaking-changes\n' + ].join('\n')) } + + this._apiKey = opts.apiKey || '' + this._apiSecret = opts.apiSecret || '' + this._authToken = opts.authToken || '' + this._company = opts.company || '' + this._transform = opts.transform === true + this._wsArgs = opts.ws || {} + this._restArgs = opts.rest || {} + this._transportCache = { + rest: {}, + ws: {} + } + } + + /** + * Returns an arguments map ready to pass to a transport constructor + * + * @param {object} extraOpts - options to pass to transport + * @returns {object} payload + */ + _getTransportPayload (extraOpts) { + return { + apiKey: this._apiKey, + apiSecret: this._apiSecret, + authToken: this._authToken, + company: this._company, + transform: this._transform, + ...extraOpts + } + } + + /** + * Returns a new REST API class instance (cached by version) + * + * @param {number} [version] - 1 or 2 (default) + * @param {object} [extraOpts] - passed to transport constructor + * @returns {RESTv1|RESTv2} transport + */ + rest (version = 2, extraOpts = {}) { + if (version !== 1 && version !== 2) { + throw new Error(`invalid http API version: ${version}`) + } + + const key = `${version}|${JSON.stringify(extraOpts)}` + + if (!this._transportCache.rest[key]) { + Object.assign(extraOpts, this._restArgs) + const payload = this._getTransportPayload(extraOpts) + + this._transportCache.rest[key] = version === 2 + ? new RESTv2(payload) + : new RESTv1(payload) + } + + return this._transportCache.rest[key] + } + + /** + * Returns a new WebSocket API class instance (cached by version) + * + * @param {number} [version] - 1 or 2 (default) + * @param {object} [extraOpts] - passed to transport constructor + * @returns {WSv1|WSv2} transport + */ + ws (version = 2, extraOpts = {}) { + if (version !== 1 && version !== 2) { + throw new Error(`invalid websocket API version: ${version}`) + } + + const key = `${version}|${JSON.stringify(extraOpts)}` + + if (!this._transportCache.ws[key]) { + Object.assign(extraOpts, this._wsArgs) + const payload = this._getTransportPayload(extraOpts) + + this._transportCache.ws[key] = version === 2 + ? new WSv2(payload) + : new WSv1(payload) + } + + return this._transportCache.ws[key] } } module.exports = BFX +module.exports.RESTv1 = RESTv1 +module.exports.RESTv2 = RESTv2 +module.exports.WSv1 = WSv1 +module.exports.WSv2 = WSv2 +module.exports.WS2Manager = WS2Manager diff --git a/lib/transports/ws2.js b/lib/transports/ws2.js new file mode 100644 index 00000000..2edefbfb --- /dev/null +++ b/lib/transports/ws2.js @@ -0,0 +1,2801 @@ +'use strict' + +const { EventEmitter } = require('events') +const debug = require('debug')('bfx:ws2') +const WebSocket = require('ws') +const CbQ = require('cbq') +const _Throttle = require('lodash.throttle') // eslint-disable-line +const _isArray = require('lodash/isArray') +const _isEmpty = require('lodash/isEmpty') +const _isString = require('lodash/isString') +const _isNumber = require('lodash/isNumber') +const _includes = require('lodash/includes') +const _pick = require('lodash/pick') +const _isEqual = require('lodash/isEqual') +const _isFinite = require('lodash/isFinite') +const { genAuthSig, nonce } = require('bfx-api-node-util') +const LosslessJSON = require('lossless-json') +const getMessagePayload = require('../util/ws2') + +const { + BalanceInfo, + FundingCredit, + FundingInfo, + FundingLoan, + FundingOffer, + FundingTrade, + MarginInfo, + Notification, + Order, + Position, + Trade, + PublicTrade, + Wallet, + OrderBook, + Candle, + TradingTicker, + FundingTicker +} = require('bfx-api-node-models') + +const DATA_CHANNEL_TYPES = ['ticker', 'book', 'candles', 'trades'] +const UCM_NOTIFICATION_TYPE = 'ucm-notify-ui' +const MAX_CALC_OPS = 8 + +/** + * A Promise Throttle instance + * + * @typedef {object} PromiseThrottle + * @property {Function} add - add a promise to be throttled + */ + +/** + * Communicates with v2 of the Bitfinex WebSocket API + * + * @class + */ +class WSv2 extends EventEmitter { + /** + * Instantiate a new ws2 transport. Does not auto-open + * + * @class WSv2 + * @param {object} [opts] - instance options + * @param {string} [opts.affCode] - affiliate code to be applied to all orders + * @param {string} [opts.apiKey] - API key + * @param {string} [opts.apiSecret] - API secret + * @param {string} [opts.url] - ws connection url, defaults to {@link WSv2#url} + * @param {number} [opts.orderOpBufferDelay] - multi-order op batching timeout + * @param {boolean} [opts.transform] - if true, packets are converted to models + * @param {object} [opts.agent] - optional node agent for ws connection (proxy) + * @param {boolean} [opts.manageOrderBooks] - enable local OB persistence + * @param {boolean} [opts.manageCandles] - enable local candle persistence + * @param {boolean} [opts.seqAudit] - enable sequence numbers & verification + * @param {boolean} [opts.autoReconnect] - if true, we will reconnect on close + * @param {number} [opts.reconnectDelay] - optional, defaults to 1000 (ms) + * @param {PromiseThrottle} [opts.reconnectThrottler] - optional pt to limit reconnect freq + * @param {number} [opts.packetWDDelay] - watch-dog forced reconnection delay + * @example + * const ws = new WSv2() + * + * ws.on('open', async () => { + * ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => { + * console.log('recv trades: %j', trades) + * }) + * + * await ws.subscribeTrades('tBTCUSD') + * }) + * + * await ws.open() + */ + constructor (opts = { + apiKey: '', + apiSecret: '', + url: WSv2.url, + affCode: null + }) { + super() + + this.setMaxListeners(1000) + this._affCode = opts.affCode + this._agent = opts.agent + this._url = opts.url || WSv2.url + this._transform = opts.transform === true + this._orderOpBufferDelay = opts.orderOpBufferDelay || -1 + this._orderOpBuffer = [] + this._orderOpTimeout = null + this._seqAudit = opts.seqAudit === true + this._autoReconnect = opts.autoReconnect === true + this._reconnectDelay = opts.reconnectDelay || 1000 + this._reconnectThrottler = opts.reconnectThrottler + this._manageOrderBooks = opts.manageOrderBooks === true + this._manageCandles = opts.manageCandles === true + this._packetWDDelay = opts.packetWDDelay + this._packetWDTimeout = null + this._packetWDLastTS = 0 + this._orderBooks = {} + this._losslessOrderBooks = {} + this._candles = {} + this._authArgs = { + apiKey: opts.apiKey, + apiSecret: opts.apiSecret + } + + /** + * { + * [groupID]: { + * [eventName]: [{ + * modelClass: .., + * filter: { symbol: 'tBTCUSD' }, // only works w/ serialize + * cb: () => {} + * }] + * } + * } + * + * @private + */ + this._listeners = {} + this._infoListeners = {} // { [code]: } + this._subscriptionRefs = {} + this._channelMap = {} + this._orderBooks = {} + this._enabledFlags = this._seqAudit ? WSv2.flags.SEQ_ALL : 0 + this._eventCallbacks = new CbQ() + this._isAuthenticated = false + this._authOnReconnect = false // used for auto-auth on reconnect + this._lastPubSeq = -1 + this._lastAuthSeq = -1 + this._isOpen = false + this._ws = null + this._isClosing = false // used to block reconnect on direct close() call + this._isReconnecting = false + + this._onWSOpen = this._onWSOpen.bind(this) + this._onWSClose = this._onWSClose.bind(this) + this._onWSError = this._onWSError.bind(this) + this._onWSMessage = this._onWSMessage.bind(this) + this._triggerPacketWD = this._triggerPacketWD.bind(this) + this._sendCalc = _Throttle(this._sendCalc.bind(this), 1000 / MAX_CALC_OPS) + } + + /** + * @returns {string} url + */ + getURL () { + return this._url + } + + /** + * @returns {boolean} usesAgent + */ + usesAgent () { + return !!this._agent + } + + /** + * Set `calc` and `dms` values to be used on the next {@link WSv2#auth} call + * + * @param {object} args - arguments + * @param {number} [args.calc] - calc value + * @param {number} [args.dms] - dms value, active 4 + * @param {number} [args.apiKey] API key + * @param {number} [args.apiSecret] API secret + * @see WSv2#auth + */ + updateAuthArgs (args = {}) { + this._authArgs = { + ...this._authArgs, + ...args + } + } + + /** + * Fetch the current default auth parameters + * + * @returns {object} authArgs + * @see WSv2#updateAuthArgs + * @see WSv2#auth + */ + getAuthArgs () { + return this._authArgs + } + + /** + * Get the total number of data channels this instance is currently + * subscribed too. + * + * @returns {number} count + * @see WSv2#subscribeTrades + * @see WSv2#subscribeTicker + * @see WSv2#subscribeCandles + * @see WSv2#subscribeOrderBook + */ + getDataChannelCount () { + return Object + .values(this._channelMap) + .filter(c => _includes(DATA_CHANNEL_TYPES, c.channel)) + .length + } + + /** + * Check if the instance is subscribed to the specified channel ID + * + * @param {number} chanId - ID of channel to query + * @returns {boolean} isSubscribed + */ + hasChannel (chanId) { + return !!this._channelMap[chanId] + } + + /** + * Check if a channel/identifier pair has been subscribed too + * + * @param {string} channel - channel type + * @param {string} identifier - unique identifier for the reference + * @returns {boolean} hasRef + * @see WSv2#managedSubscribe + */ + hasSubscriptionRef (channel, identifier) { + const key = `${channel}:${identifier}` + return !!Object.keys(this._subscriptionRefs).find(ref => ref === key) + } + + /** + * Fetch the ID of a channel matched by type and channel data filter + * + * @param {string} type - channel type + * @param {object} filter - to be matched against channel data + * @returns {number} channelID + */ + getDataChannelId (type, filter) { + return Object + .keys(this._channelMap) + .find(cid => { + const c = this._channelMap[cid] + const fv = _pick(c, Object.keys(filter)) + return c.channel === type && _isEqual(fv, filter) + }) + } + + /** + * Check if the instance is subscribed to a data channel matching the + * specified type and filter. + * + * @param {string} type - channel type + * @param {object} filter - to be matched against channel data + * @returns {boolean} hasChannel + */ + hasDataChannel (type, filter) { + return !!this.getDataChannelId(type, filter) + } + + /** + * Opens a connection to the API server. Rejects with an error if a + * connection is already open. Resolves on success. + * + * @returns {Promise} p + */ + async open () { + if (this._isOpen || this._ws !== null) { + throw new Error('already open') + } + + debug('connecting to %s...', this._url) + + this._ws = new WebSocket(this._url, { + agent: this._agent + }) + + this._subscriptionRefs = {} + this._candles = {} + this._orderBooks = {} + + this._ws.on('message', this._onWSMessage) + this._ws.on('error', this._onWSError) + this._ws.on('close', this._onWSClose) + + return new Promise((resolve) => { + this._ws.on('open', () => { + // call manually instead of binding to open event so it fires at the + // right time + this._onWSOpen() + + if (this._enabledFlags !== 0) { + this.sendEnabledFlags() + } + + debug('connected') + resolve() + }) + }) + } + + /** + * Closes the active connection. If there is none, rejects with a promise. + * Resolves on success + * + * @param {number} code - passed to ws + * @param {string} reason - passed to ws + * @returns {Promise} p + */ + async close (code, reason) { + if (!this._isOpen || this._ws === null) { + throw new Error('not open') + } + + debug('disconnecting...') + + return new Promise((resolve) => { + this._ws.once('close', () => { + this._isOpen = false + this._ws = null + + debug('disconnected') + resolve() + }) + + if (!this._isClosing) { + this._isClosing = true + this._ws.close(code, reason) + } + }) + } + + /** + * Generates & sends an authentication packet to the server; if already + * authenticated, rejects with an error, resolves on success. + * + * If a DMS flag of 4 is provided, all open orders are cancelled when the + * connection terminates. + * + * @param {number?} calc - optional, default is 0 + * @param {number?} dms - optional dead man switch flag, active 4 + * @returns {Promise} p + */ + async auth (calc, dms) { + this._authOnReconnect = true + if (!this._isOpen) { + throw new Error('not open') + } + + if (this._isAuthenticated) { + throw new Error('already authenticated') + } + + const authNonce = nonce() + const authPayload = `AUTH${authNonce}${authNonce}` + const { sig } = genAuthSig(this._authArgs.apiSecret, authPayload) + const authArgs = { ...this._authArgs } + + if (_isFinite(calc)) authArgs.calc = calc + if (_isFinite(dms)) authArgs.dms = dms + + return new Promise((resolve) => { + this.once('auth', () => { + debug('authenticated') + resolve() + }) + + this.send({ + event: 'auth', + apiKey: this._authArgs.apiKey, + authSig: sig, + authPayload, + authNonce, + ...authArgs + }) + }) + } + + /** + * Utility method to close & re-open the ws connection. Re-authenticates if + * previously authenticated + * + * @returns {Promise} p - resolves on completion + */ + async reconnect () { + this._isReconnecting = true + + if (this._ws !== null && this._isOpen) { // did we get a watchdog timeout and need to close the connection? + await this.close() + + return new Promise((resolve) => { + this.once(this._authOnReconnect ? 'auth' : 'open', resolve) + }) + } + + return this.reconnectAfterClose() // we are already closed, so reopen and re-auth + } + + /** + * @private + */ + async reconnectAfterClose () { + if (!this._isReconnecting || this._ws !== null || this._isOpen) { + return this.reconnect() + } + + await this.open() + + if (this._authOnReconnect) { + await this.auth() + } + } + + /** + * Returns an error if the message has an invalid (out of order) sequence # + * The last-seen sequence #s are updated internally. + * + * @param {Array} msg - incoming message + * @returns {Error} err - null if no error or sequencing not enabled + * @private + */ + _validateMessageSeq (msg = []) { + if (!this._seqAudit) return null + if (!Array.isArray(msg)) return null + if (msg.length === 0) return null + + // The auth sequence # is the last value in channel 0 non-heartbeat packets. + const authSeq = msg[0] === 0 && msg[1] !== 'hb' + ? msg[msg.length - 1] + : NaN + + // *-req packets don't include public seq numbers + if (`${(msg[2] || [])[1] || ''}`.slice(-4) !== '-req') { + // All other packets provide a public sequence # as the last value. For chan + // 0 packets, these are included as the 2nd to last value + const seq = ( + (msg[0] === 0) && + (msg[1] !== 'hb') && + !(msg[1] === 'n' && ((msg[2] || [])[1] || '').slice(-4) === '-req') + ) + ? msg[msg.length - 2] + : msg[msg.length - 1] + + if (!_isFinite(seq)) return null + + if (this._lastPubSeq === -1) { // first pub seq received + this._lastPubSeq = seq + return null + } + + if (seq !== this._lastPubSeq + 1) { // check pub seq + return new Error(`invalid pub seq #; last ${this._lastPubSeq}, got ${seq}`) + } + + this._lastPubSeq = seq + } + + if (!_isFinite(authSeq)) return null + if (authSeq === 0) return null // still syncing + + // notifications don't advance seq + if (msg[1] === 'n') { + return authSeq !== this._lastAuthSeq + ? new Error( + `invalid auth seq #, expected no advancement but got ${authSeq}` + ) + : null + } + + if (authSeq === this._lastAuthSeq) { + return new Error( + `expected auth seq # advancement but got same seq: ${authSeq}` + ) + } + + // check + if (this._lastAuthSeq !== -1 && authSeq !== this._lastAuthSeq + 1) { + return new Error( + `invalid auth seq #; last ${this._lastAuthSeq}, got ${authSeq}` + ) + } + + this._lastAuthSeq = authSeq + return null + } + + /** + * Trigger the packet watch-dog; called when we haven't seen a new WS packet + * for longer than our WD duration (if provided) + * + * @returns {Promise} p + * @private + */ + async _triggerPacketWD () { + if (!this._packetWDDelay || !this._isOpen) { + return Promise.resolve() + } + + debug( + 'packet delay watchdog triggered [last packet %dms ago]', + Date.now() - this._packetWDLastTS + ) + + this._packetWDTimeout = null + return this.reconnect() + } + + /** + * Reset the packet watch-dog timeout. Should be called on every new WS packet + * if the watch-dog is enabled + * + * @private + */ + _resetPacketWD () { + if (!this._packetWDDelay) return + if (this._packetWDTimeout !== null) { + clearTimeout(this._packetWDTimeout) + } + + if (!this._isOpen) return + + this._packetWDTimeout = setTimeout(() => { + this._triggerPacketWD().catch((err) => { + debug('error triggering packet watchdog: %s', err.message) + }) + }, this._packetWDDelay) + } + + /** + * Subscribes to previously subscribed channels, used after reconnecting + * + * @private + */ + resubscribePreviousChannels () { + Object.values(this._prevChannelMap).forEach((chan) => { + const { channel } = chan + + switch (channel) { + case 'ticker': { + const { symbol } = chan + this.subscribeTicker(symbol) + break + } + + case 'trades': { + const { symbol } = chan + this.subscribeTrades(symbol) + break + } + + case 'book': { + const { symbol, len, prec } = chan + this.subscribeOrderBook(symbol, prec, len) + break + } + + case 'candles': { + const { key } = chan + this.subscribeCandles(key) + break + } + + default: { + debug('unknown previously subscribed channel type: %s', channel) + } + } + }) + } + + /** + * @private + */ + _onWSOpen () { + this._isOpen = true + this._isReconnecting = false + this._packetWDLastTS = Date.now() + this._lastAuthSeq = -1 + this._lastPubSeq = -1 + this.emit('open') + + if (!_isEmpty(this._prevChannelMap)) { + this.resubscribePreviousChannels() + this._prevChannelMap = {} + } + + debug('connection open') + } + + /** + * @private + */ + async _onWSClose () { + this._isOpen = false + this._isAuthenticated = false + this._lastAuthSeq = -1 + this._lastPubSeq = -1 + this._enabledFlags = 0 + this._ws = null + this._subscriptionRefs = {} + this.emit('close') + + debug('connection closed') + + // _isReconnecting = true - if a reconnection has been requested. In that case always call reconnectAfterClose + // _isClosing = true - if the user explicitly requested a close + // _autoReconnect = true - if the user likes to reconnect automatically + if (this._isReconnecting || (this._autoReconnect && !this._isClosing)) { + this._prevChannelMap = this._channelMap + + setTimeout(async () => { + try { + if (this._reconnectThrottler) { + await this._reconnectThrottler.add(this.reconnectAfterClose.bind(this)) + } else { + await this.reconnectAfterClose() + } + } catch (err) { + debug('error reconnectAfterClose: %s', err.stack) + } + }, this._reconnectDelay) + } + + this._channelMap = {} + this._isClosing = false + } + + /** + * @param {Error} err - error + * @private + */ + _onWSError (err) { + this.emit('error', err) + + debug('error: %s', err) + } + + /** + * @param {Array} arrN - notification in ws array format + * @private + */ + _onWSNotification (arrN) { + const status = arrN[6] + const msg = arrN[7] + + if (!arrN[4]) return + + if (arrN[1] === 'on-req') { + const [,, cid] = arrN[4] + const k = `order-new-${cid}` + + if (status === 'SUCCESS') { + this._eventCallbacks.trigger(k, null, arrN[4]) + } else { + this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4]) + } + } else if (arrN[1] === 'oc-req') { + const [id] = arrN[4] + const k = `order-cancel-${id}` + + if (status === 'SUCCESS') { + this._eventCallbacks.trigger(k, null, arrN[4]) + } else { + this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4]) + } + } else if (arrN[1] === 'ou-req') { + const [id] = arrN[4] + const k = `order-update-${id}` + + if (status === 'SUCCESS') { + this._eventCallbacks.trigger(k, null, arrN[4]) + } else { + this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4]) + } + } + } + + /** + * @param {string} rawMsg - incoming message JSON + * @param {string} flags - flags + * @private + */ + _onWSMessage (rawMsg, flags) { + debug('recv msg: %s', rawMsg) + + this._packetWDLastTS = Date.now() + this._resetPacketWD() + + let msg + + try { + msg = JSON.parse(rawMsg) + } catch (e) { + this.emit('error', `invalid message JSON: ${rawMsg}`) + return + } + + debug('recv msg: %j', msg) + + if (this._seqAudit) { + const seqErr = this._validateMessageSeq(msg) + + if (seqErr !== null) { + this.emit('error', seqErr) + return + } + } + + this.emit('message', msg, flags) + + if (Array.isArray(msg)) { + this._handleChannelMessage(msg, rawMsg) + } else if (msg.event) { + this._handleEventMessage(msg) + } else { + debug('recv unidentified message: %j', msg) + } + } + + /** + * @param {Array} msg - message + * @param {string} rawMsg - message JSON + * @private + */ + _handleChannelMessage (msg, rawMsg) { + const [chanId, type] = msg + const channelData = this._channelMap[chanId] + + if (!channelData) { + debug('recv msg from unknown channel %d: %j', chanId, msg) + return + } + + if (msg.length < 2) return + if (msg[1] === 'hb') return + + if (channelData.channel === 'book') { + if (type === 'cs') { + this._handleOBChecksumMessage(msg, channelData) + } else { + this._handleOBMessage(msg, channelData, rawMsg) + } + } else if (channelData.channel === 'trades') { + this._handleTradeMessage(msg, channelData) + } else if (channelData.channel === 'ticker') { + this._handleTickerMessage(msg, channelData) + } else if (channelData.channel === 'candles') { + this._handleCandleMessage(msg, channelData) + } else if (channelData.channel === 'status') { + this._handleStatusMessage(msg, channelData) + } else if (channelData.channel === 'auth') { + this._handleAuthMessage(msg, channelData) + } else { + this._propagateMessageToListeners(msg, channelData) + this.emit(channelData.channel, msg) + } + } + + /** + * @param {Array} msg - message + * @param {object} chanData - channel definition + * @private + */ + _handleOBChecksumMessage (msg, chanData) { + this.emit('cs', msg) + + if (!this._manageOrderBooks) { + return + } + + const { symbol, prec } = chanData + const cs = msg[2] + + // NOTE: Checksums are temporarily disabled for funding books, due to + // invalid book sorting on the backend. This change is temporary + if (symbol[0] === 't') { + const err = this._verifyManagedOBChecksum(symbol, prec, cs) + + if (err) { + this.emit('error', err) + return + } + } + + const internalMessage = [chanData.chanId, 'ob_checksum', cs] + internalMessage.filterOverride = [ + chanData.symbol, + chanData.prec, + chanData.len + ] + + this._propagateMessageToListeners(internalMessage, false) + this.emit('cs', symbol, cs) + } + + /** + * Called for messages from the 'book' channel. Might be an update or a + * snapshot + * + * @param {Array|Array[]} msg - message + * @param {object} chanData - entry from _channelMap + * @param {string} rawMsg - message JSON + * @private + */ + _handleOBMessage (msg, chanData, rawMsg) { + const { symbol, prec } = chanData + const raw = prec === 'R0' + let data = getMessagePayload(msg) + + if (this._manageOrderBooks) { + const err = this._updateManagedOB(symbol, data, raw, rawMsg) + + if (err) { + this.emit('error', err) + return + } + + data = this._orderBooks[symbol] + } + + // Always transform an array of entries + if (this._transform) { + data = new OrderBook((Array.isArray(data[0]) ? data : [data]), raw) + } + + const internalMessage = [chanData.chanId, 'orderbook', data] + internalMessage.filterOverride = [ + chanData.symbol, + chanData.prec, + chanData.len + ] + + this._propagateMessageToListeners(internalMessage, chanData, false) + this.emit('orderbook', symbol, data) + } + + /** + * @param {string} symbol - symbol for order book + * @param {number[]|number[][]} data - incoming data + * @param {boolean} raw - if true, the order book is considered R* + * @param {string} rawMsg - source message JSON + * @returns {Error} err - null on success + * @private + */ + _updateManagedOB (symbol, data, raw, rawMsg) { + // parse raw string with lossless parse which takes + // the exact strict values rather than converting to floats + // [0.00001, [1, 2, 3]] -> ['0.00001', ['1', '2', '3']] + const rawLossless = LosslessJSON.parse(rawMsg, (key, value) => { + if (value && value.isLosslessNumber) { + return value.toString() + } else { + return value + } + }) + const losslessUpdate = rawLossless[1] + // Snapshot, new OB. Note that we don't protect against duplicates, as they + // could come in on re-sub + if (Array.isArray(data[0])) { + this._orderBooks[symbol] = data + this._losslessOrderBooks[symbol] = losslessUpdate + return null + } + + // entry, needs to be applied to OB + if (!this._orderBooks[symbol]) { + return new Error(`recv update for unknown OB: ${symbol}`) + } + + OrderBook.updateArrayOBWith(this._orderBooks[symbol], data, raw) + OrderBook.updateArrayOBWith(this._losslessOrderBooks[symbol], losslessUpdate, raw) + return null + } + + /** + * @param {string} symbol - symbol for order book + * @param {string} prec - precision + * @param {number} cs - expected checksum + * @returns {Error} err - null if none + * @private + */ + _verifyManagedOBChecksum (symbol, prec, cs) { + const ob = this._losslessOrderBooks[symbol] + + if (!ob) return null + + const localCS = ob instanceof OrderBook + ? ob.checksum() + : OrderBook.checksumArr(ob, prec === 'R0') + + return localCS !== cs + ? new Error(`OB checksum mismatch: got ${localCS}, want ${cs}`) + : null + } + + /** + * Returns an up-to-date copy of the order book for the specified symbol, or + * null if no OB is managed for that symbol. + * + * Set `managedOrderBooks: true` in the constructor to use. + * + * @param {string} symbol - symbol for order book + * @returns {OrderBook} ob - null if not found + * @example + * const ws = new WSv2({ managedOrderBooks: true }) + * + * ws.on('open', async () => { + * ws.onOrderBook({ symbol: 'tBTCUSD' }, () => { + * const book = ws.getOB('tBTCUSD') + * + * if (!book) return + * + * const spread = book.midPrice() + * console.log('spread for tBTCUSD: %f', spread) + * }) + * + * ws.subscribeOrderBook({ symbol: 'tBTCUSD' }) + * }) + * + * await ws.open() + */ + getOB (symbol) { + if (!this._orderBooks[symbol]) return null + + return new OrderBook(this._orderBooks[symbol]) + } + + /** + * Returns an up-to-date lossless copy of the order book for the specified symbol, or + * null if no OB is managed for that symbol. All amounts and prices are in original + * string format. + * + * Set `manageOrderBooks: true` in the constructor to use. + * + * @param {string} symbol - symbol for order book + * @returns {OrderBook} ob - null if not found + */ + getLosslessOB (symbol) { + if (!this._losslessOrderBooks[symbol]) return null + + return new OrderBook(this._losslessOrderBooks[symbol]) + } + + /** + * @param {Array} msg - incoming message + * @param {object} chanData - channel definition + * @private + */ + _handleTradeMessage (msg, chanData) { + const eventName = msg[1][0] === 'f' + ? msg[1] // Funding trades are passed to fte/ftu handlers + : msg[1] === 'te' + ? 'trade-entry' + : 'trades' + + let payload = getMessagePayload(msg) + + if (!Array.isArray(payload[0])) { + payload = [payload] + } + + let data = payload + + if (this._transform) { // correctly parse single trade/array of trades + const M = eventName[0] === 'f' && msg[2].length === 8 ? FundingTrade : PublicTrade + const trades = M.unserialize(data) + + if (_isArray(trades) && trades.length === 1) { + data = trades[0] + } else { + data = trades + } + + data = new M(data) + } + + const internalMessage = [chanData.chanId, eventName, data] + internalMessage.filterOverride = [chanData.symbol || chanData.pair] + + this._propagateMessageToListeners(internalMessage, chanData, false) + this.emit('trades', chanData.symbol || chanData.pair, data) + } + + /** + * @param {Array} msg - incoming message + * @param {object} chanData - channel definition + * @private + */ + _handleTickerMessage (msg = [], chanData = {}) { + let data = getMessagePayload(msg) + + if (this._transform) { + data = (chanData.symbol || '')[0] === 't' + ? new TradingTicker([chanData.symbol, ...msg[1]]) + : new FundingTicker([chanData.symbol, ...msg[1]]) + } + + const internalMessage = [chanData.chanId, 'ticker', data] + internalMessage.filterOverride = [chanData.symbol] + + this._propagateMessageToListeners(internalMessage, chanData, false) + this.emit('ticker', chanData.symbol, data) + } + + /** + * Called for messages from a 'candles' channel. Might be an update or + * snapshot. + * + * @param {Array|Array[]} msg - incoming message + * @param {object} chanData - entry from _channelMap + * @private + */ + _handleCandleMessage (msg, chanData) { + const { key } = chanData + let data = getMessagePayload(msg) + + if (this._manageCandles) { + const err = this._updateManagedCandles(key, data) + + if (err) { + this.emit('error', err) + return + } + + data = this._candles[key] + } else if (data.length > 0 && !Array.isArray(data[0])) { + data = [data] // always pass on an array of candles + } + + if (this._transform) { + data = Candle.unserialize(data) + } + + const internalMessage = [chanData.chanId, 'candle', data] + internalMessage.filterOverride = [chanData.key] + + this._propagateMessageToListeners(internalMessage, chanData, false) + this.emit('candle', data, key) + } + + /** + * Called for messages from a 'status' channel. + * + * @param {Array|Array[]} msg - incoming message + * @param {object} chanData - entry from _channelMap + * @private + */ + _handleStatusMessage (msg, chanData) { + const { key } = chanData + const data = getMessagePayload(msg) + + const internalMessage = [chanData.chanId, 'status', data] + internalMessage.filterOverride = [chanData.key] + + this._propagateMessageToListeners(internalMessage, chanData, false) + this.emit('status', data, key) + } + + /** + * @param {string} key - key for candle set + * @param {number[]|number[][]} data - incoming dataset (single or multiple) + * @returns {Error} err - null on success + * @private + */ + _updateManagedCandles (key, data) { + if (Array.isArray(data[0])) { // snapshot, new candles + data.sort((a, b) => b[0] - a[0]) + + this._candles[key] = data + return null + } + + // entry, needs to be applied to candle set + if (!this._candles[key]) { + return new Error(`recv update for unknown candles: ${key}`) + } + + const candles = this._candles[key] + let updated = false + + for (let i = 0; i < candles.length; i++) { + if (data[0] === candles[i][0]) { + candles[i] = data + updated = true + break + } + } + + if (!updated) { + candles.unshift(data) + } + + return null + } + + /** + * Fetch a reference to the full set of synced candles for the specified key. + * Set `managedCandles: true` in the constructor to use. + * + * @param {string} key - key for candle set + * @returns {Array} candles - empty array if none exist + * @example + * const ws = new WSv2({ managedCandles: true }) + * + * ws.on('open', async () => { + * ws.onCandles({ key: 'trade:1m:tBTCUSD' }, () => { + * const candles = ws.getCandles('trade:1m:tBTCUSD') + * + * if (!candles) return + * + * console.log('%d candles in dataset', candles.length) + * }) + * + * ws.subscribeCandles({ key: 'trade:1m:tBTCUSD' }) + * }) + * + * await ws.open() + */ + getCandles (key) { + return this._candles[key] || [] + } + + /** + * @param {Array} msg - incoming message + * @param {object} chanData - channel data + * @private + */ + _handleAuthMessage (msg, chanData) { + if (msg[1] === 'n') { + const payload = getMessagePayload(msg) + + if (payload) { + this._onWSNotification(payload) + } + } else if (msg[1] === 'te') { + msg[1] = 'auth-te' + } else if (msg[1] === 'tu') { + msg[1] = 'auth-tu' + } + + this._propagateMessageToListeners(msg, chanData) + } + + /** + * @param {Array} msg - incoming message + * @param {object} chan - channel data + * @param {boolean} transform - defaults to internal flag + * @private + */ + _propagateMessageToListeners (msg, chan, transform = this._transform) { + const listenerGroups = Object.values(this._listeners) + + for (let i = 0; i < listenerGroups.length; i++) { + WSv2._notifyListenerGroup(listenerGroups[i], msg, transform, this, chan) + } + } + + /** + * Applies filtering & transform to a packet before sending it out to matching + * listeners in the group. + * + * @param {object} lGroup - listener group to parse & notify + * @param {object} msg - passed to each matched listener + * @param {boolean} transform - whether or not to instantiate a model + * @param {WSv2} ws - instance to pass to models if transforming + * @param {object} chanData - channel data + * @private + */ + static _notifyListenerGroup (lGroup, msg, transform, ws, chanData) { + const [, eventName, data = []] = msg + let filterByData + + // Catch-all can't filter/transform + WSv2._notifyCatchAllListeners(lGroup, msg) + + if (!lGroup[eventName] || lGroup[eventName].length === 0) return + + const listeners = lGroup[eventName].filter((listener) => { + const { filter } = listener + + if (!filter) return true + + // inspect snapshots for matching packets + if (Array.isArray(data[0])) { + const matchingData = data.filter((item) => { + filterByData = msg.filterOverride ? msg.filterOverride : item + + return WSv2._payloadPassesFilter(filterByData, filter) + }) + + return matchingData.length !== 0 + } + + // inspect single packet + filterByData = msg.filterOverride ? msg.filterOverride : data + + return WSv2._payloadPassesFilter(filterByData, filter) + }) + + if (listeners.length === 0) return + + listeners.forEach(({ cb, modelClass }) => { + const ModelClass = modelClass + + if (!ModelClass || !transform || data.length === 0) { + cb(data, chanData) + } else if (Array.isArray(data[0])) { + cb(data.map((entry) => { + return new ModelClass(entry, ws) + }), chanData) + } else { + cb(new ModelClass(data, ws), chanData) + } + }) + } + + /** + * @param {Array} payload - payload to verify + * @param {object} filter - filter to match against payload + * @returns {boolean} pass + * @private + */ + static _payloadPassesFilter (payload, filter) { + const filterIndices = Object.keys(filter) + let filterValue + + for (let k = 0; k < filterIndices.length; k++) { + filterValue = filter[filterIndices[k]] + + if (_isEmpty(filterValue) || filterValue === '*') { + continue + } + + if (payload[+filterIndices[k]] !== filterValue) { + return false + } + } + + return true + } + + /** + * @param {object} lGroup - listener group keyed by event ('' in this case) + * @param {*} data - packet to pass to listeners + * @private + */ + static _notifyCatchAllListeners (lGroup, data) { + if (!lGroup['']) return + + for (let j = 0; j < lGroup[''].length; j++) { + lGroup[''][j].cb(data) + } + } + + /** + * @param {object} msg - incoming message + * @private + */ + _handleEventMessage (msg) { + if (msg.event === 'auth') { + this._handleAuthEvent(msg) + } else if (msg.event === 'subscribed') { + this._handleSubscribedEvent(msg) + } else if (msg.event === 'unsubscribed') { + this._handleUnsubscribedEvent(msg) + } else if (msg.event === 'info') { + this._handleInfoEvent(msg) + } else if (msg.event === 'conf') { + this._handleConfigEvent(msg) + } else if (msg.event === 'error') { + this._handleErrorEvent(msg) + } else if (msg.event === 'pong') { + this._handlePongEvent(msg) + } else { + debug('recv unknown event message: %j', msg) + } + } + + /** + * Emits an error on config failure, otherwise updates the internal flag set + * and triggers any callbacks + * + * @param {object} msg - config message + * @private + */ + _handleConfigEvent (msg = {}) { + const { status, flags } = msg + const k = this._getConfigEventKey(flags) + + if (status !== 'OK') { + const err = new Error(`config failed (${status}) for flags ${flags}`) + debug('config failed: %s', err.message) + + this.emit('error', err) + this._eventCallbacks.trigger(k, err) + } else { + debug('flags updated to %d', flags) + + this._enabledFlags = flags + this._eventCallbacks.trigger(k, null, msg) + } + } + + /** + * @param {object} msg - incoming message + * @private + */ + _handlePongEvent (msg) { + debug('pong: %s', JSON.stringify(msg)) + + this.emit('pong', msg) + } + + /** + * @param {object} msg - incoming message + * @private + */ + _handleErrorEvent (msg) { + debug('error: %s', JSON.stringify(msg)) + + this.emit('error', msg) + } + + /** + * @param {object} data - incoming message + * @private + */ + _handleAuthEvent (data = {}) { + const { chanId, msg = '', status = '' } = data + + if (status !== 'OK') { + const err = new Error(msg.match(/nonce/) + ? 'auth failed: nonce small; you may need to generate a new API key to reset the nonce counter' + : `auth failed: ${msg} (${status})` + ) + + debug('%s', err.message) + this.emit('error', err) + return + } + + this._channelMap[chanId] = { channel: 'auth' } + this._isAuthenticated = true + + this.emit('auth', data) + debug('authenticated!') + } + + /** + * @param {object} msg - incoming message + * @private + */ + _handleSubscribedEvent (msg) { + this._channelMap[msg.chanId] = msg + + debug('subscribed to %s [%d]', msg.channel, msg.chanId) + this.emit('subscribed', msg) + } + + /** + * @param {object} msg - incoming message + * @private + */ + _handleUnsubscribedEvent (msg) { + delete this._channelMap[msg.chanId] + debug('unsubscribed from %d', msg.chanId) + this.emit('unsubscribed', msg) + } + + /** + * @param {object} msg - incoming message + * @private + */ + _handleInfoEvent (msg = {}) { + const { version, code } = msg + + if (version) { + if (version !== 2) { + const err = new Error(`server not running API v2: v${version}`) + + this.emit('error', err) + this.close().catch((err) => { + debug('error closing connection: %s', err.stack) + }) + return + } + + const { status } = msg.platform || {} + + debug( + 'server running API v2 (platform: %s (%d))', + status === 0 ? 'under maintenance' : 'operating normally', status + ) + } else if (code) { + if (this._infoListeners[code]) { + this._infoListeners[code].forEach(cb => cb(msg)) + } + + if (code === WSv2.info.SERVER_RESTART) { + debug('server restarted, please reconnect') + } else if (code === WSv2.info.MAINTENANCE_START) { + debug('server maintenance period started!') + } else if (code === WSv2.info.MAINTENANCE_END) { + debug('server maintenance period ended!') + } + } + + this.emit('info', msg) + } + + /** + * Subscribes and tracks subscriptions per channel/identifier pair. If + * already subscribed to the specified pair, nothing happens. + * + * @param {string} channel - channel name + * @param {string} identifier - for uniquely identifying the ref count + * @param {object} payload - merged with sub packet + * @returns {boolean} subSent + * @todo will be refactored to return promise from subscribe() call instead + * of sub action taken flag + * @see WSv2#subscribeTrades + * @see WSv2#subscribeTicker + * @see WSv2#subscribeCandles + * @see WSv2#subscribeOrderBook + * @example + * const ws = new WSv2() + * + * ws.on('open', async () => { + * ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => { + * console.log('recv trades: %j', trades) + * }) + * + * ws.managedSubscribe('trades', 'tBTCUSD', { symbol: 'tBTCUSD' }) + * }) + * + * await ws.open() + */ + managedSubscribe (channel = '', identifier = '', payload = {}) { + const key = `${channel}:${identifier}` + + if (this._subscriptionRefs[key]) { + this._subscriptionRefs[key]++ + return false + } + + this._subscriptionRefs[key] = 1 + this.subscribe(channel, payload) + + return true + } + + /** + * Decreases the subscription ref count for the channel/identifier pair, and + * unsubscribes from the channel if it reaches 0. + * + * @param {string} channel - channel name + * @param {string} identifier - for uniquely identifying the ref count + * @returns {boolean} unsubSent + */ + managedUnsubscribe (channel = '', identifier = '') { + const key = `${channel}:${identifier}` + const chanId = this._chanIdByIdentifier(channel, identifier) + + if (chanId === null || isNaN(this._subscriptionRefs[key])) return false + + this._subscriptionRefs[key]-- + if (this._subscriptionRefs[key] > 0) return false + + this.unsubscribe(chanId) + delete this._subscriptionRefs[key] + + return true + } + + /** + * Fetch a channel definition + * + * @param {object} opts - options + * @param {number} opts.chanId - channel ID + * @param {string} opts.channel - channel name + * @param {string} [opts.symbol] - match by symbol + * @param {string} [opts.key] - match by key (for candle channels) + * @returns {object} chanData - null if not found + */ + getChannelData ({ chanId, channel, symbol, key }) { + const id = chanId || this._chanIdByIdentifier(channel, symbol || key) + + return this._channelMap[id] || null + } + + /** + * @param {string} channel - channel name + * @param {string} identifier - unique identifier for the channel + * @returns {number} channelID + * @private + */ + _chanIdByIdentifier (channel, identifier) { + const channelIds = Object.keys(this._channelMap) + let chan + + for (let i = 0; i < channelIds.length; i++) { + chan = this._channelMap[channelIds[i]] + + if (chan.channel === channel && ( + chan.symbol === identifier || + chan.key === identifier + )) { + return channelIds[i] + } + } + + return null + } + + /** + * @param {string} key - key for the promise + * @returns {Promise} p - resolves on event + * @private + */ + _getEventPromise (key) { + return new Promise((resolve, reject) => { + this._eventCallbacks.push(key, (err, res) => { + if (err) { + return reject(err) + } + + resolve(res) + }) + }) + } + + /** + * Send a packet to the WS server + * + * @param {*} msg - packet, gets stringified + */ + send (msg) { + if (!this._ws || !this._isOpen) { + this.emit('error', new Error('no ws client or not open')) + } else if (this._isClosing) { + this.emit('error', new Error('connection currently closing')) + } else { + debug('sending %j', msg) + this._ws.send(JSON.stringify(msg)) + } + } + + /** + * @returns {boolean} sequencingEnabled + */ + sequencingEnabled () { + return this._seqAudit + } + + /** + * Configures the seq flag to enable sequencing (packet number) for this + * connection. When enabled, the seq number will be the last value of + * channel packet arrays. + * + * @param {object} args - params + * @param {boolean} [args.audit] - if true, an error is emitted on invalid seq + * @returns {Promise} p + * @see WSv2#flags + */ + async enableSequencing (args = { audit: true }) { + this._seqAudit = args.audit === true + + return this.enableFlag(WSv2.flags.SEQ_ALL) + } + + /** + * Enables a configuration flag. + * + * @param {number} flag - flag to update, as numeric value + * @returns {Promise} p + * @see WSv2#flags + * @example + * const ws = new WSv2() + * + * ws.on('open', async () => { + * await ws.enableFlag(WSv2.flags.CHECKSUM) + * console.log('ob checkums enabled') + * }) + * + * await ws.open() + */ + async enableFlag (flag) { + this._enabledFlags = this._enabledFlags | flag + + if (!this._isOpen) { + return + } + + this.sendEnabledFlags() + return this._getEventPromise(this._getConfigEventKey(flag)) + } + + /** + * Sends the local flags value to the server, updating the config + * + * @private + */ + sendEnabledFlags () { + this.send({ + event: 'conf', + flags: this._enabledFlags + }) + } + + /** + * Checks local state, relies on successful server config responses + * + * @see enableFlag + * + * @param {number} flag - flag to check for + * @returns {boolean} enabled + */ + isFlagEnabled (flag) { + return (this._enabledFlags & flag) === flag + } + + /** + * @param {string} flag - flag to fetch event key for + * @returns {string} key + * @private + */ + _getConfigEventKey (flag) { + return `conf-res-${flag}` + } + + /** + * Register a callback in case of a ws server restart message; Use this to + * call reconnect() if needed. (code 20051) + * + * @param {Function} cb - called on event trigger + */ + onServerRestart (cb) { + this.onInfoMessage(WSv2.info.SERVER_RESTART, cb) + } + + /** + * Register a callback in case of a 'maintenance started' message from the + * server. This is a good time to pause server packets until maintenance ends + * + * @param {Function} cb - called on event trigger + */ + onMaintenanceStart (cb) { + this.onInfoMessage(WSv2.info.MAINTENANCE_START, cb) + } + + /** + * Register a callback to be notified of a maintenance period ending + * + * @param {Function} cb - called on event trigger + */ + onMaintenanceEnd (cb) { + this.onInfoMessage(WSv2.info.MAINTENANCE_END, cb) + } + + /** + * Subscribe to a channel with the given filter payload + * + * @param {string} channel - channel payload/data + * @param {object} payload - optional extra packet data + * @example + * const ws = new WSv2() + * + * ws.on('open', () => { + * ws.onTrades({ symbol: 'tBTCUSD' }, (trades) => { + * // ... + * }) + * + * ws.subscribe('trades', { symbol: 'tBTCUSD' }) + * }) + * + * await ws.open() + */ + subscribe (channel, payload) { + this.send(Object.assign({ + event: 'subscribe', + channel + }, payload)) + } + + /** + * Subscribe to a ticker data channel + * + * @param {string} symbol - symbol of ticker + * @returns {boolean} subscribed + * @see WSv2#managedSubscribe + * @example + * await ws.subscribeTicker('tBTCUSD') + */ + async subscribeTicker (symbol) { + return this.managedSubscribe('ticker', symbol, { symbol }) + } + + /** + * Subscribe to a trades data channel + * + * @param {string} symbol - symbol of market to monitor + * @returns {boolean} subscribed + * @see WSv2#managedSubscribe + * @example + * await ws.subscribeTrades('tBTCUSD') + */ + async subscribeTrades (symbol) { + return this.managedSubscribe('trades', symbol, { symbol }) + } + + /** + * Subscribe to an order book data channel + * + * @param {string} symbol - symbol of order book + * @param {string} prec - P0, P1, P2, or P3 (default P0) + * @param {string} len - 25 or 100 (default 25) + * @returns {boolean} subscribed + * @see WSv2#managedSubscribe + * @example + * await ws.subscribeOrderBook('tBTCUSD', 'R0', '25') + */ + async subscribeOrderBook (symbol, prec = 'P0', len = '25') { + return this.managedSubscribe('book', symbol, { symbol, len, prec }) + } + + /** + * Subscribe to a candle data channel + * + * @param {string} key - 'trade:5m:tBTCUSD' + * @returns {boolean} subscribed + * @see WSv2#managedSubscribe + * @example + * await ws.subscribeCandles('trade:5m:tBTCUSD') + */ + async subscribeCandles (key) { + return this.managedSubscribe('candles', key, { key }) + } + + /** + * Subscribe to a status data channel + * + * @param {string} key - i.e. 'liq:global' + * @returns {boolean} subscribed + * @see WSv2#managedSubscribe + * @example + * await ws.subscribeStatus('liq:global') + */ + async subscribeStatus (key) { + return this.managedSubscribe('status', key, { key }) + } + + /** + * Unsubscribe from a channel by ID + * + * @param {number} chanId - ID of channel to unsubscribe from + * @example + * const id = ws.getDataChannelId('ticker', { symbol: 'tBTCUSD' }) + * + * if (id) { + * ws.unsubscribe(id) + * } + */ + unsubscribe (chanId) { + this.send({ + event: 'unsubscribe', + chanId: +chanId + }) + } + + /** + * Unsubscribe from a ticker data channel + * + * @param {string} symbol - symbol of ticker + * @returns {boolean} unsubscribed + * @see WSv2#subscribeTicker + * @example + * await ws.unsubscribeTicker('tBTCUSD') + */ + async unsubscribeTicker (symbol) { + return this.managedUnsubscribe('ticker', symbol) + } + + /** + * Unsubscribe from a trades data channel + * + * @param {string} symbol - symbol of market to unsubscribe from + * @returns {boolean} unsubscribed + * @see WSv2#subscribeTrades + * @example + * await ws.unsubcribeTrades('tBTCUSD') + */ + async unsubscribeTrades (symbol) { + return this.managedUnsubscribe('trades', symbol) + } + + /** + * Unsubscribe from an order book data channel + * + * @param {string} symbol - symbol of order book + * @returns {boolean} unsubscribed + * @see WSv2#subscribeOrderBook + * @example + * await ws.unsubcribeOrderBook('tBTCUSD') + */ + async unsubscribeOrderBook (symbol) { + return this.managedUnsubscribe('book', symbol) + } + + /** + * @param {string} symbol - symbol of candles + * @param {string} frame - time frame + * @returns {boolean} unsubscribed + * @see WSv2#subscribeCandles + * @example + * await ws.unsubscribeCandles('tBTCUSD', '1m') + */ + async unsubscribeCandles (symbol, frame) { + return this.managedUnsubscribe('candles', `trade:${frame}:${symbol}`) + } + + /** + * @param {string} key - key that was used in initial {@link WSv2#subscribeStatus} call + * @returns {boolean} unsubscribed + * @see WSv2#subscribeStatus + */ + async unsubscribeStatus (key) { + return this.managedUnsubscribe('status', key) + } + + /** + * Remove all listeners by callback group ID + * + * @param {string} cbGID - callback group to remove + * @example + * await ws.subscribeTrades({ symbol: 'tBTCUSD', cbGID: 42 }) + * await ws.subscribeTrades({ symbol: 'tLEOUSD', cbGID: 42 }) + * await ws.subscribeTrades({ symbol: 'tETHUSD', cbGID: 42 }) + * + * // ... + * + * ws.removeListeners(42) + */ + removeListeners (cbGID) { + delete this._listeners[cbGID] + } + + /** + * Request a calc operation to be performed on the specified indexes + * + * @param {string[]} prefixes - desired prefixes to be calculated + */ + requestCalc (prefixes) { + this._sendCalc([0, 'calc', null, prefixes.map(p => [p])]) + } + + /** + * Throttled call to ws.send, max 8 op/s + * + * @param {Array} msg - message + * @private + */ + _sendCalc (msg) { + debug('req calc: %j', msg) + + this._ws.send(JSON.stringify(msg)) + } + + /** + * Sends a new order to the server and resolves the returned promise once the + * order submit is confirmed. Emits an error if not authenticated. The order + * can be either an array, key/value map, or Order object instance. + * + * @see WSv2#cancelOrders + * @see WSv2#cancelOrder + * @see WSv2#updateOrder + * + * @param {object|Array} params - parameters or order object model or array + * @param {object|Array} params.order - order object model or array + * @returns {Promise} p - resolves on submit notification + * @example + * const o = new Order({ + * type: Order.type.EXCHANGE_LIMIT, + * amount: 18, + * price: 0.75, + * symbol: 'tBTCUSD', + * hidden: true + * }, ws) + * + * await ws.submitOrder(o) + * + * console.log('order confirmed! status: %s', o.status) + */ + async submitOrder (params) { + if (!this._isAuthenticated) { + throw new Error('not authenticated') + } + + const order = params?.order ?? params + const packet = Array.isArray(order) + ? order + : order instanceof Order + ? order.toNewOrderPacket() + : new Order(order).toNewOrderPacket() + + if (this._affCode) { + if (!packet.meta) { + packet.meta = {} + } + + packet.meta.aff_code = packet.meta.aff_code || this._affCode // eslint-disable-line + } + + this._sendOrderPacket([0, 'on', null, packet]) + + return this._getEventPromise(`order-new-${packet.cid}`) + } + + /** + * Send a changeset to update an order in-place while maintaining position in + * the price queue. The changeset must contain the order ID, and supports a + * 'delta' key to increase/decrease the total amount. + * + * @see WSv2#submitOrder + * + * @param {object} changes - requires at least an 'id' + * @returns {Promise} p - resolves on receiving an confirmation notification + */ + async updateOrder (changes = {}) { + const { id } = changes + + if (!this._isAuthenticated) { + throw new Error('not authenticated') + } else if (!id) { + throw new Error('order ID required for update') + } + + this._sendOrderPacket([0, 'ou', null, changes]) + + return this._getEventPromise(`order-update-${id}`) + } + + /** + * Cancels an order by ID and resolves the returned promise once the cancel is + * confirmed. Emits an error if not authenticated. The ID can be passed as a + * number, or taken from an order array/object. + * + * @see WSv2#submitOrder + * + * @param {object|Array|number} order - order model, array, or ID to be cancelled + * @returns {Promise} p + */ + async cancelOrder (order) { + if (!this._isAuthenticated) { + throw new Error('not authenticated') + } + + const id = _isNumber(order) + ? order + : Array.isArray(order) + ? order[0] + : order.id + + debug(`cancelling order ${id}`) + this._sendOrderPacket([0, 'oc', null, { id }]) + + return this._getEventPromise(`order-cancel-${id}`) + } + + /** + * Cancels multiple orders, returns a promise that resolves once all + * operations are confirmed. + * + * @see WSv2#submitOrder + * + * @param {object|object[]|Array[]|number[]} params - parameters or array of order models, arrays + * @param {object[]|Array[]|number[]} params.ids - array of order models, arrays + * or IDs to be cancelled + * @returns {Promise} p + */ + async cancelOrders (params) { + if (!this._isAuthenticated) { + throw new Error('not authenticated') + } + + const orders = params?.ids ?? params + + return Promise.all(orders.map(o => { + return this.cancelOrder(o) + })) + } + + /** + * Sends the op payloads to the server as an 'ox_multi' command. A promise is + * returned and resolves immediately if authenticated, as no confirmation is + * available for this message type. + * + * @param {object[]} opPayloads - order operations + * @returns {Promise} p - rejects if not authenticated + */ + async submitOrderMultiOp (opPayloads) { + if (!this._isAuthenticated) { + throw new Error('not authenticated') + } + + // TODO: multi-op tracking + this.send([0, 'ox_multi', null, opPayloads]) + } + + /** + * @param {Array} packet - new order packet to be submitted + * @private + */ + _sendOrderPacket (packet) { + if (this._hasOrderBuff()) { + this._ensureOrderBuffTimeout() + this._orderOpBuffer.push(packet) + } else { + this.send(packet) + } + } + + /** + * @returns {boolean} buffEnabled + * @private + */ + _hasOrderBuff () { + return this._orderOpBufferDelay > 0 + } + + /** + * @private + */ + _ensureOrderBuffTimeout () { + if (this._orderOpTimeout !== null) return + + this._orderOpTimeout = setTimeout( + this._flushOrderOps.bind(this), + this._orderOpBufferDelay + ) + } + + /** + * Splits the op buffer into packets of max 15 ops each, and sends them down + * the wire. + * + * @returns {Promise} p - resolves after send + * @private + */ + _flushOrderOps () { + this._orderOpTimeout = null + + const packets = this._orderOpBuffer.map(p => [p[1], p[3]]) + this._orderOpBuffer = [] + + if (packets.length <= 15) { + return this.submitOrderMultiOp(packets) + } + + const promises = [] + + while (packets.length > 0) { + const opPackets = packets.splice(0, Math.min(packets.length, 15)) + promises.push(this.submitOrderMultiOp(opPackets)) + } + + return Promise.all(promises) + } + + /** + * @returns {boolean} authenticated + */ + isAuthenticated () { + return this._isAuthenticated + } + + /** + * @returns {boolean} open + */ + isOpen () { + return this._isOpen + } + + /** + * @returns {boolean} reconnecting + */ + isReconnecting () { + return this._isReconnecting + } + + /** + * Sends a broadcast notification, which will be received by any active UI + * websocket connections (at bitfinex.com), triggering a desktop notification. + * + * In the future our mobile app will also support spawning native push + * notifications in response to incoming ucm-notify-ui packets. + * + * @param {object} opts - options + * @param {string} [opts.message] - message to display + * @param {string} [opts.type] - notification type, 'ucm-*' for broadcasts + * @param {string} [opts.level] - 'info', 'error', or 'success' + * @param {string} [opts.image] - link to an image to be shown + * @param {string} [opts.link] - URL the notification should forward too + * @param {string} [opts.sound] - URL of sound to play + * @throws an error if given no type or message, or the instance is not open + * and authenticated + */ + notifyUI (opts = {}) { + const { type, message, level, image, link, sound } = opts + + if (!_isString(type) || !_isString(message)) { + throw new Error(`notified with invalid type/message: ${type}/${message}`) + } + + if (!this._isOpen) { + throw new Error('socket not open') + } + + if (!this._isAuthenticated) { + throw new Error('socket not authenticated') + } + + this.send([0, 'n', null, { + type: UCM_NOTIFICATION_TYPE, + info: { + type, + message, + level, + image, + link, + sound + } + }]) + } + + /** + * Adds a listener to the internal listener set, with an optional grouping + * for batch unsubscribes (GID) & automatic ws packet matching (filterKey) + * + * @param {string} eventName - as received on ws stream + * @param {object} filter - map of index & value in ws packet + * @param {object} modelClass - model to use for serialization + * @param {string} cbGID - listener group ID for mass removal + * @param {Function} cb - listener + * @private + */ + _registerListener (eventName, filter, modelClass, cbGID, cb) { + if (!cbGID) cbGID = null + + if (!this._listeners[cbGID]) { + this._listeners[cbGID] = { [eventName]: [] } + } + + const listeners = this._listeners[cbGID] + + if (!listeners[eventName]) { + listeners[eventName] = [] + } + + const l = { + cb, + modelClass, + filter + } + + listeners[eventName].push(l) + } + + /** + * Registers a new callback to be called when a matching info message is + * received. + * + * @param {number} code - from #WSv2.info + * @param {Function} cb - callback + */ + onInfoMessage (code, cb) { + if (!this._infoListeners[code]) { + this._infoListeners[code] = [] + } + + this._infoListeners[code].push(cb) + } + + /** + * Register a generic handler to be called with each received message + * + * @param {object} opts - options + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + */ + onMessage ({ cbGID }, cb) { + this._registerListener('', null, null, cbGID, cb) + } + + /** + * Register a handler to be called with each received candle + * + * @param {object} opts - options + * @param {string} opts.key - candle set key, i.e. trade:30m:tBTCUSD + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-candle + * @see WSv2#subscribeCandles + * @see WSv2#unsubscribeCandles + */ + onCandle ({ key, cbGID }, cb) { + this._registerListener('candle', { 0: key }, Candle, cbGID, cb) + } + + /** + * Register a handler to be called with each received candle + * + * @param {object} opts - options + * @param {string} opts.symbol - book symbol + * @param {string} opts.prec - book precision, i.e. 'R0' + * @param {string} opts.len - book length, i.e. '25' + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books + * @see WSv2#subscribeOrderBook + * @see WSv2#unsubscribeOrderBook + */ + onOrderBook ({ symbol, prec, len, cbGID }, cb) { + this._registerListener('orderbook', { + 0: symbol, + 1: prec, + 2: len + }, OrderBook, cbGID, cb) + } + + /** + * Register a handler to be called with each received order book checksum + * + * @param {object} opts - options + * @param {string} opts.symbol - book symbol + * @param {string} opts.prec - book precision, i.e. 'R0' + * @param {string} opts.len - book length, i.e. '25' + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books + * @see WSv2#subscribeOrderBook + * @see WSv2#unsubscribeOrderBook + */ + onOrderBookChecksum ({ symbol, prec, len, cbGID }, cb) { + this._registerListener('ob_checksum', { + 0: symbol, + 1: prec, + 2: len + }, null, cbGID, cb) + } + + /** + * Register a handler to be called with each received trade (pair or symbol + * required) + * + * @param {object} opts - options + * @param {string} [opts.pair] - required if no symbol specified + * @param {string} [opts.symbol] - required if no pair specified + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-trades + * @see WSv2#subscribeTrades + * @see WSv2#unsubscribeTrades + */ + onTrades ({ symbol, pair, cbGID }, cb) { + const id = pair || symbol || '' + const model = id[0] === 'f' ? FundingTrade : PublicTrade + + this._registerListener('trades', { 0: id }, model, cbGID, cb) + } + + /** + * Register a handler to be called on each trade `'te'` event + * + * @param {object} opts - options + * @param {string} [opts.pair] - required if no symbol specified + * @param {string} [opts.symbol] - required if no pair specified + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-trades + * @see WSv2#subscribeTrades + * @see WSv2#unsubscribeTrades + */ + onTradeEntry ({ pair, symbol, cbGID }, cb) { + const id = pair || symbol || '' + this._registerListener('trade-entry', { 0: id }, PublicTrade, cbGID, cb) + } + + /** + * Register a handler to be called on each personal trade `'te'` event + * + * @param {object} opts - options + * @param {string} [opts.pair] - required if no symbol specified + * @param {string} [opts.symbol] - required if no pair specified + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-trades + */ + onAccountTradeEntry ({ symbol, cbGID }, cb) { + this._registerListener('auth-te', { 1: symbol }, Trade, cbGID, cb) + } + + /** + * Register a handler to be called on each personal trade `'tu'` event + * + * @param {object} opts - options + * @param {string} [opts.pair] - required if no symbol specified + * @param {string} [opts.symbol] - required if no pair specified + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-trades + */ + onAccountTradeUpdate ({ symbol, cbGID }, cb) { + this._registerListener('auth-tu', { 1: symbol }, Trade, cbGID, cb) + } + + /** + * Register a handler to be called on each received ticker + * + * @param {object} opts - options + * @param {string} opts.symbol - symbol for tickers + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-public-ticker + * @see WSv2#subscribeTicker + * @see WSv2#unsubscribeTicker + */ + onTicker ({ symbol = '', cbGID } = {}, cb) { + const m = symbol[0] === 'f' ? FundingTicker : TradingTicker + this._registerListener('ticker', { 0: symbol }, m, cbGID, cb) + } + + /** + * Register a handler to be called on each message for the desired status + * feed. + * + * @param {object} opts - options + * @param {string} opts.key - key of feed to listen on + * @param {string|number} [opts.cbGID] - callback group ID + * @param {Function} cb - callback + * @see WSv2#subscribeStatus + */ + onStatus ({ key = '', cbGID } = {}, cb) { + this._registerListener('status', { 0: key }, null, cbGID, cb) + } + + /** + * Register a handler to be called on each full order snapshot (sent on auth) + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {number} [opts.id] - order ID to match + * @param {number} [opts.cid] - order client ID to match + * @param {number} [opts.gid] - order group ID to match + * @param {string|number} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders + * @see WSv2#auth + */ + onOrderSnapshot ({ symbol, id, cid, gid, cbGID }, cb) { + this._registerListener('os', { + 0: id, + 1: gid, + 2: cid, + 3: symbol + }, Order, cbGID, cb) + } + + /** + * Register a handler to be called on each new order packet + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {number} [opts.id] - order ID to match + * @param {number} [opts.cid] - order client ID to match + * @param {number} [opts.gid] - order group ID to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders + * @see WSv2#submitOrder + */ + onOrderNew ({ symbol, id, cid, gid, cbGID }, cb) { + this._registerListener('on', { + 0: id, + 1: gid, + 2: cid, + 3: symbol + }, Order, cbGID, cb) + } + + /** + * Register a handler to be called on each order update packet + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {number} [opts.id] - order ID to match + * @param {number} [opts.cid] - order client ID to match + * @param {number} [opts.gid] - order group ID to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders + * @see WSv2#updateOrder + */ + onOrderUpdate ({ symbol, id, cid, gid, cbGID }, cb) { + this._registerListener('ou', { + 0: id, + 1: gid, + 2: cid, + 3: symbol + }, Order, cbGID, cb) + } + + /** + * Register a handler to be called on each order close packet + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {number} [opts.id] - order ID to match + * @param {number} [opts.cid] - order client ID to match + * @param {number} [opts.gid] - order group ID to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders + * @see WSv2#cancelOrder + */ + onOrderClose ({ symbol, id, cid, gid, cbGID }, cb) { + this._registerListener('oc', { + 0: id, + 1: gid, + 2: cid, + 3: symbol + }, Order, cbGID, cb) + } + + /** + * Register a handler to be called on each position snapshot (sent on auth) + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-position + * @see WSv2#auth + */ + onPositionSnapshot ({ symbol, cbGID }, cb) { + this._registerListener('ps', { 0: symbol }, Position, cbGID, cb) + } + + /** + * Register a handler to be called when a position is opened + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-position + */ + onPositionNew ({ symbol, cbGID }, cb) { + this._registerListener('pn', { 0: symbol }, Position, cbGID, cb) + } + + /** + * Register a handler to be called when a position is updated + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-position + */ + onPositionUpdate ({ symbol, cbGID }, cb) { + this._registerListener('pu', { 0: symbol }, Position, cbGID, cb) + } + + /** + * Register a handler to be called when a position is closed + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-position + */ + onPositionClose ({ symbol, cbGID }, cb) { + this._registerListener('pc', { 0: symbol }, Position, cbGID, cb) + } + + /** + * Register a handler to be called on each fundign offer snapshot (sent on + * auth) + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers + * @see WSv2#auth + */ + onFundingOfferSnapshot ({ symbol, cbGID }, cb) { + this._registerListener('fos', { 1: symbol }, FundingOffer, cbGID, cb) + } + + /** + * Register a handler to be called when a funding offer is created + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers + */ + onFundingOfferNew ({ symbol, cbGID }, cb) { + this._registerListener('fon', { 1: symbol }, FundingOffer, cbGID, cb) + } + + /** + * Register a handler to be called when a funding offer is updated + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers + */ + onFundingOfferUpdate ({ symbol, cbGID }, cb) { + this._registerListener('fou', { 1: symbol }, FundingOffer, cbGID, cb) + } + + /** + * Register a handler to be called when a funding offer is closed + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers + */ + onFundingOfferClose ({ symbol, cbGID }, cb) { + this._registerListener('foc', { 1: symbol }, FundingOffer, cbGID, cb) + } + + /** + * Register a handler to be called on each funding credit snapshot (sent on + * auth) + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits + * @see WSv2#auth + */ + onFundingCreditSnapshot ({ symbol, cbGID }, cb) { + this._registerListener('fcs', { 1: symbol }, FundingCredit, cbGID, cb) + } + + /** + * Register a handler to be called when a funding credit is created + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits + */ + onFundingCreditNew ({ symbol, cbGID }, cb) { + this._registerListener('fcn', { 1: symbol }, FundingCredit, cbGID, cb) + } + + /** + * Register a handler to be called when a funding credit is updated + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits + */ + onFundingCreditUpdate ({ symbol, cbGID }, cb) { + this._registerListener('fcu', { 1: symbol }, FundingCredit, cbGID, cb) + } + + /** + * Register a handler to be called when a funding credit is closed + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits + */ + onFundingCreditClose ({ symbol, cbGID }, cb) { + this._registerListener('fcc', { 1: symbol }, FundingCredit, cbGID, cb) + } + + /** + * Register a handler to be called on each funding loan snapshot (sent on + * auth) + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans + * @see WSv2#auth + */ + onFundingLoanSnapshot ({ symbol, cbGID }, cb) { + this._registerListener('fls', { 1: symbol }, FundingLoan, cbGID, cb) + } + + /** + * Register a handler to be called when a funding loan is created + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans + */ + onFundingLoanNew ({ symbol, cbGID }, cb) { + this._registerListener('fln', { 1: symbol }, FundingLoan, cbGID, cb) + } + + /** + * Register a handler to be called when a funding loan is updated + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans + */ + onFundingLoanUpdate ({ symbol, cbGID }, cb) { + this._registerListener('flu', { 1: symbol }, FundingLoan, cbGID, cb) + } + + /** + * Register a handler to be called when a funding loan is closed + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans + */ + onFundingLoanClose ({ symbol, cbGID }, cb) { + this._registerListener('flc', { 1: symbol }, FundingLoan, cbGID, cb) + } + + /** + * Register a handler to be called on each wallet snapshot (sent on auth) + * + * @param {object} opts - options + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-wallets + */ + onWalletSnapshot ({ cbGID }, cb) { + this._registerListener('ws', null, Wallet, cbGID, cb) + } + + /** + * Register a handler to be called on each wallet update + * + * @param {object} opts - options + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-wallets + */ + onWalletUpdate ({ cbGID }, cb) { + this._registerListener('wu', null, Wallet, cbGID, cb) + } + + /** + * Register a handler to be called on each balance info update + * + * @param {object} opts - options + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-balance + */ + onBalanceInfoUpdate ({ cbGID }, cb) { + this._registerListener('bu', null, BalanceInfo, cbGID, cb) + } + + /** + * Register a handler to be called on each margin info update + * + * @param {object} opts - options + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-margin + */ + onMarginInfoUpdate ({ cbGID }, cb) { + this._registerListener('miu', null, MarginInfo, cbGID, cb) + } + + /** + * Register a handler to be called on each funding info update + * + * @param {object} opts - options + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding + */ + onFundingInfoUpdate ({ cbGID }, cb) { + this._registerListener('fiu', null, FundingInfo, cbGID, cb) + } + + /** + * Register a handler to be called on each funding trade `'te'` event + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding-trades + */ + onFundingTradeEntry ({ symbol, cbGID }, cb) { + this._registerListener('fte', { 0: symbol }, FundingTrade, cbGID, cb) + } + + /** + * Register a handler to be called on each funding trade `'tu'` event + * + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding-trades + */ + onFundingTradeUpdate ({ symbol, cbGID }, cb) { + this._registerListener('ftu', { 0: symbol }, FundingTrade, cbGID, cb) + } + + /** + * Register a handler to be called on each notification + * + * @param {object} opts - options + * @param {string} [opts.type] - type to match + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @see https://docs.bitfinex.com/v2/reference#ws-auth-notifications + */ + onNotification ({ type, cbGID }, cb) { + this._registerListener('n', { 1: type }, Notification, cbGID, cb) + } +} + +/** + * Map of flag names and numeric values for usage with the API + * + * @constant + * @property {number} DEC_S - flag to request all decimals as strings + * @property {number} TIME_S - flag to request all timestamps as strings + * @property {number} TIMESTAMP - flag to request timestamp in milliseconds + * @property {number} SEQ_ALL - flag to enable sequence numbers & verification + * @property {number} CHECKSUM - flag to enable order book checksums + * @memberof! WSv2# + */ +WSv2.flags = { + DEC_S: 8, // enables all decimals as strings + TIME_S: 32, // enables all timestamps as strings + TIMESTAMP: 32768, // timestamps in milliseconds + SEQ_ALL: 65536, // enable sequencing + CHECKSUM: 131072 // enable checksum per OB change, top 25 levels per-side +} + +/** + * Map of info event names and their respective codes for usage with the API + * + * @constant + * @property {number} SERVER_RESTART - server restart event + * @property {number} MAINTENANCE_START - maintenance start event + * @property {number} MAINTENANCE_END - maintenance end event + * @memberof! WSv2# + */ +WSv2.info = { + SERVER_RESTART: 20051, + MAINTENANCE_START: 20060, + MAINTENANCE_END: 20061 +} + +/** + * Default connection URL + * + * @constant + * @type {string} + * @memberof! WSv2# + * @default + */ +WSv2.url = 'wss://api.bitfinex.com/ws/2' + +module.exports = WSv2 diff --git a/lib/util/index.js b/lib/util/index.js new file mode 100644 index 00000000..439a1a37 --- /dev/null +++ b/lib/util/index.js @@ -0,0 +1,9 @@ +'use strict' + +const isClass = require('./is_class') +const isSnapshot = require('./is_snapshot') + +module.exports = { + isClass, + isSnapshot +} diff --git a/lib/util/is_class.js b/lib/util/is_class.js new file mode 100644 index 00000000..36156e49 --- /dev/null +++ b/lib/util/is_class.js @@ -0,0 +1,12 @@ +'use strict' + +const _isFunction = require('lodash/isFunction') + +const isClass = (f) => { + return ( + (_isFunction(f)) && + (/^class\s/.test(Function.prototype.toString.call(f))) + ) +} + +module.exports = isClass diff --git a/lib/util/is_snapshot.js b/lib/util/is_snapshot.js new file mode 100644 index 00000000..3ef52cab --- /dev/null +++ b/lib/util/is_snapshot.js @@ -0,0 +1,5 @@ +'use strict' + +const isSnapshot = msg => msg[0] && Array.isArray(msg[0]) + +module.exports = isSnapshot diff --git a/lib/util/precision.js b/lib/util/precision.js new file mode 100644 index 00000000..29352d03 --- /dev/null +++ b/lib/util/precision.js @@ -0,0 +1,47 @@ +const Big = require('bignumber.js') + +const DEFAULT_SIG_FIGS = 5 +const PRICE_SIG_FIGS = 5 +const AMOUNT_DECIMALS = 8 + +/** + * Smartly set the precision (decimal) on a value based off of the significant + * digit maximum. For example, calling with 3.34 when the max sig figs allowed + * is 5 would return '3.3400', the representation number of decimals IF they + * weren't zeros. + * + * @param {number} number - number to manipulate + * @param {number} [maxSigs] - default 5 + * @returns {string} str + */ +const setSigFig = (number = 0, maxSigs = DEFAULT_SIG_FIGS) => { + const n = +(number) + if (!isFinite(n)) { + return number + } + const value = n.toPrecision(maxSigs) + + return /e/.test(value) + ? new Big(value).toString() + : value +} + +const setPrecision = (number = 0, decimals = 0) => { + const n = +(number) + + return (isFinite(n)) + ? n.toFixed(decimals) + : number +} + +const prepareAmount = (amount = 0) => { + return setPrecision(amount, AMOUNT_DECIMALS) +} + +const preparePrice = (price = 0) => { + return setSigFig(price, PRICE_SIG_FIGS) +} + +module.exports = { + setSigFig, setPrecision, prepareAmount, preparePrice +} diff --git a/lib/util/ws2.js b/lib/util/ws2.js new file mode 100644 index 00000000..d4576c8c --- /dev/null +++ b/lib/util/ws2.js @@ -0,0 +1,13 @@ +'use strict' + +const _findLast = require('lodash/findLast') + +/** + * Resolves the message payload; useful for getting around sequence numbers + * + * @param {Array} msg - message to parse + * @returns {Array} payload - undefined if not found + */ +module.exports = (msg = []) => { + return _findLast(msg, i => Array.isArray(i)) +} diff --git a/lib/ws2_manager.js b/lib/ws2_manager.js new file mode 100644 index 00000000..f1e2c690 --- /dev/null +++ b/lib/ws2_manager.js @@ -0,0 +1,560 @@ +'use strict' + +const { EventEmitter } = require('events') +const debug = require('debug')('bfx:ws2:manager') +const _isEqual = require('lodash/isEqual') +const _isFinite = require('lodash/isFinite') +const _includes = require('lodash/includes') +const _pick = require('lodash/pick') +const PromiseThrottle = require('promise-throttle') +const WSv2 = require('./transports/ws2') + +const DATA_CHANNEL_LIMIT = 30 +const reconnectThrottler = new PromiseThrottle({ + requestsPerSecond: 10 / 60.0, + promiseImplementation: Promise +}) + +/** + * Provides a wrapper around the WSv2 class, opening new sockets when a + * subscription would push a single socket over the data channel limit. + * + * For more complex operations, grab a socket reference with getSocket() or + * getFreeDataSocket(), or create a new WSv2 instance manually + * + * @example + * const rest = new RESTv2({ transform: true }) + * const details = await rest.symbolDetails() + * const symbols = details.map(d => `t${d.pair.toUpperCase()}`) + * const timeFrames = ['1m', '5m', '30m', '1h', '6h'] + * const keys = _flatten(symbols.map(s => { + * return timeFrames.map(tf => `trade:${tf}:${s}`) + * })) + * + * const m = new Manager() + * + * m.on('error', (err) => { + * debug('error: %s', err) + * }) + * + * m.once('open', () => { + * debug('open') + * + * keys.forEach(key => { + * m.subscribeCandles(key) + * m.onCandle({ key }, (candles) => { + * debug('recv %d candles on channel %s', candles.length, key) + * }) + * }) + * + * symbols.forEach(symbol => { + * m.subscribeTrades(symbol) + * m.onTrades({ symbol }, (trades) => { + * debug('recv %d trades on channel %s', trades.length, symbol) + * }) + * }) + * + * symbols.forEach(symbol => { + * m.subscribeTicker(symbol) + * m.onTicker({ symbol }, (ticker) => { + * debug('recv ticker on channel %s: %j', symbol, ticker) + * }) + * }) + * + * symbols.forEach(symbol => { + * m.subscribeOrderBook(symbol) + * m.onOrderBook({ symbol }, (update) => { + * debug('recv book update on channel %s: %j', symbol, update) + * }) + * }) + * + * setInterval(() => { + * debug('num keys: %d', keys.length) + * debug('num sockets: %d', m.getNumSockets()) + * debug('socket info: %j', m.getSocketInfo()) + * }, 5000) + * }) + * + * m.openSocket() + */ +class WS2Manager extends EventEmitter { + /** + * @param {object} socketArgs - passed to WSv2 constructors + * @param {object} [authArgs] - cached for all internal socket auth() calls + * @param {number} [authArgs.calc] - default 0 + * @param {number} [authArgs.dms] - default 0 + */ + constructor (socketArgs, authArgs = { calc: 0, dms: 0 }) { + super() + + this.setMaxListeners(1000) + + this._authArgs = authArgs + this._sockets = [] + this._socketArgs = { + ...(socketArgs || {}), + reconnectThrottler + } + } + + /** + * Update authentication arguments on all sockets + * + * @param {object} args - arguments + * @param {number} [args.calc] - calc value + * @param {number} [args.dms] - active 4 + */ + setAuthArgs (args = {}) { + this._authArgs = { + ...this._authArgs, + ...args + } + + this._sockets.forEach(socket => socket.ws.updateAuthArgs(this._authArgs)) + } + + /** + * Retrieve internal authentication arguments + * + * @returns {object} args + */ + getAuthArgs () { + return this._authArgs + } + + /** + * Reconnects all open sockets + * + * @returns {Promise} p + */ + async reconnect () { + return Promise.all(this._sockets.map(socket => socket.ws.reconnect())) + } + + /** + * Closes all open sockets + * + * @returns {Promise} p + */ + async close () { + return Promise.all(this._sockets.map(socket => socket.ws.close())) + } + + /** + * @param {object} s - socket state + * @returns {number} count - # of subscribed/pending data channels + */ + static getDataChannelCount (s) { + let count = s.ws.getDataChannelCount() + + count += s.pendingSubscriptions.length + count -= s.pendingUnsubscriptions.length + + return count + } + + /** + * @returns {number} n + */ + getNumSockets () { + return this._sockets.length + } + + /** + * @param {number} i - index into pool + * @returns {object} state + */ + getSocket (i) { + return this._sockets[i] + } + + /** + * Returns an object which can be logged to inspect the socket pool + * + * @returns {object[]} socketInfo + */ + getSocketInfo () { + return this._sockets.map(s => ({ + nChannels: WS2Manager.getDataChannelCount(s) + })) + } + + /** + * Authenticates all existing & future sockets with the provided credentials. + * Does nothing if an apiKey/apiSecret pair are already known. + * + * @param {object} args - arguments + * @param {string} args.apiKey - saved if not already provided + * @param {string} args.apiSecret - saved if not already provided + * @param {number} [args.calc] - default 0 + * @param {number} [args.dms] - dead man switch, active 4 + */ + auth ({ apiKey, apiSecret, calc, dms } = {}) { + if (this._socketArgs.apiKey || this._socketArgs.apiSecret) { + debug('error: auth credentials already provided! refusing auth') + return + } + + this._socketArgs.apiKey = apiKey + this._socketArgs.apiSecret = apiSecret + + if (_isFinite(calc)) this._authArgs.calc = calc + if (_isFinite(dms)) this._authArgs.dms = dms + if (apiKey) this._authArgs.apiKey = apiKey + if (apiSecret) this._authArgs.apiSecret = apiSecret + + this._sockets.forEach(s => { + if (!s.ws.isAuthenticated()) { + s.ws.updateAuthArgs(this._authArgs) + s.ws.auth() + } + }) + } + + /** + * Creates a new socket/state instance and adds it to the internal pool. Binds + * event listeners to forward via our own event emitter, and to manage pending + * subs/unsubs. + * + * @returns {object} state + */ + openSocket () { + const { apiKey, apiSecret } = this._socketArgs + const ws = new WSv2(this._socketArgs) + const wsState = { + pendingSubscriptions: [], + pendingUnsubscriptions: [], + ws + } + + ws.updateAuthArgs(this._authArgs) + ws.on('open', () => this.emit('open', ws)) + ws.on('message', (msg = {}) => this.emit('message', msg, ws)) + ws.on('error', (error) => this.emit('error', error, ws)) + ws.on('auth', () => this.emit('auth', ws)) + ws.on('close', () => this.emit('close', ws)) + ws.on('subscribed', (msg = {}) => { + this.emit('subscribed', msg) + + const i = wsState.pendingSubscriptions.find(sub => { + const fv = _pick(msg, Object.keys(sub[1])) + + return ( + (sub[0] === msg.channel) && + _isEqual(fv, sub[1]) + ) + }) + + if (i === -1) { + debug('error removing pending sub: %j', msg) + return + } + + wsState.pendingSubscriptions.splice(i, 1) + }) + + ws.on('unsubscribed', (msg = {}) => { + this.emit('unsubscribed', msg) + + const { chanId } = msg + const i = wsState.pendingUnsubscriptions.findIndex(cid => ( + cid === `${chanId}` + )) + + if (i === -1) { + debug('error removing pending unsub: %j', msg) + return + } + + wsState.pendingUnsubscriptions.splice(i, 1) + }) + + if (apiKey && apiSecret) { // auto-auth + ws.once('open', () => { + debug('authenticating socket...') + + ws.auth().then(() => { + return debug('socket authenticated') + }).catch((err) => { + debug('error authenticating socket: %s', err.message) + }) + }) + } + + ws.open().then(() => { + return debug('socket connection opened') + }).catch((err) => { + debug('error opening socket: %s', err.stack) + }) + + this._sockets.push(wsState) + return wsState + } + + /** + * @returns {object} state + */ + getAuthenticatedSocket () { + return this._sockets.find(s => s.ws.isAuthenticated()) + } + + /** + * Returns the first socket that has less active/pending channels than the + * DATA_CHANNEL_LIMIT + * + * @returns {object} state - undefined if none found + */ + getFreeDataSocket () { + return this._sockets.find(s => ( + WS2Manager.getDataChannelCount(s) < DATA_CHANNEL_LIMIT + )) + } + + /** + * Returns the first socket that is subscribed/pending sub to the specified + * channel. + * + * @param {string} type - i.e. 'book' + * @param {object} filter - i.e. { symbol: 'tBTCUSD', prec: 'R0' } + * @returns {object} wsState - undefined if not found + */ + getSocketWithDataChannel (type, filter) { + return this._sockets.find(s => { + const subI = s.pendingSubscriptions.findIndex(s => ( + s[0] === type && _isEqual(s[1], filter) + )) + + if (subI !== -1) { + return true + } + + // Confirm unsub is not pending + const cid = s.ws.getDataChannelId(type, filter) + + if (!cid) { + return false + } + + return cid && !_includes(s.pendingUnsubscriptions, cid) + }) + } + + /** + * NOTE: Cannot filter against pending subscriptions, due to unknown chanId + * + * @param {number} chanId - channel ID + * @returns {object} wsState - undefined if not found + */ + getSocketWithChannel (chanId) { + return this._sockets.find(s => { + return ( + s.ws.hasChannel(chanId) && + !_includes(s.pendingUnsubscriptions, chanId) + ) + }) + } + + /** + * @param {string} channel - channel type + * @param {string} identifier - unique channel identifier + * @returns {object} wsState - undefined if not found + */ + getSocketWithSubRef (channel, identifier) { + return this._sockets.find(s => s.ws.hasSubscriptionRef(channel, identifier)) + } + + /** + * Calls the provided cb with all internal socket instances + * + * @param {Function} cb - callback + */ + withAllSockets (cb) { + this._sockets.forEach((ws2) => { + cb(ws2) + }) + } + + /** + * Subscribes a free data socket if available to the specified channel, or + * opens a new socket & subs if needed. + * + * @param {string} type - i.e. 'book' + * @param {string} ident - i.e. 'tBTCUSD' + * @param {object} filter - i.e. { symbol: 'tBTCUSD', prec: 'R0' } + */ + subscribe (type, ident, filter) { + let s = this.getFreeDataSocket() + if (!s) { + s = this.openSocket() + } + + const doSub = () => { + s.ws.managedSubscribe(type, ident, filter) + } + + if (!s.ws.isOpen()) { + s.ws.once('open', doSub) + } else { + doSub() + } + + s.pendingSubscriptions.push([type, filter]) + } + + /** + * @param {string} channel - channel type + * @param {string} identifier - unique channel identifier + */ + managedUnsubscribe (channel, identifier) { + const s = this.getSocketWithSubRef(channel, identifier) + + if (!s) { + debug('cannot unsub from unknown channel %s: %s', channel, identifier) + return + } + + const chanId = s.ws._chanIdByIdentifier(channel, identifier) + s.ws.managedUnsubscribe(channel, identifier) + s.pendingUnsubscriptions.push(chanId) + } + + /** + * Unsubscribes the first socket w/ the specified channel. Does nothing if no + * such socket is found. + * + * @param {number} chanId - channel ID + */ + unsubscribe (chanId) { + const s = this.getSocketWithChannel(chanId) + + if (!s) { + debug('cannot unsub from unknown channel: %d', chanId) + return + } + + s.ws.unsubscribe(chanId) + s.pendingUnsubscriptions.push(chanId) + } + + /** + * @param {string} symbol - symbol for ticker + */ + subscribeTicker (symbol) { + this.subscribe('ticker', symbol, { symbol }) + } + + /** + * @param {string} symbol - symbol for trades + */ + subscribeTrades (symbol) { + this.subscribe('trades', symbol, { symbol }) + } + + /** + * @param {string} symbol - symbol for order book + * @param {string} [prec] - precision, i.e. 'R0', default 'P0' + * @param {string} [len] - length, default '25' + * @param {string} [freq] - default 'F0' + */ + subscribeOrderBook (symbol, prec = 'P0', len = '25', freq = 'F0') { + const filter = {} + + if (symbol) filter.symbol = symbol + if (prec) filter.prec = prec + if (len) filter.len = len + if (freq) filter.freq = freq + + this.subscribe('book', symbol, filter) + } + + /** + * @param {string} key - candle channel key + */ + subscribeCandles (key) { + this.subscribe('candles', key, { key }) + } + + /** + * @param {object} opts - options + * @param {string} opts.key - candle set key, i.e. trade:30m:tBTCUSD + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @throws an error if no data socket is available + * @see https://docs.bitfinex.com/v2/reference#ws-public-candle + */ + onCandle ({ key, cbGID }, cb) { + const s = this.getSocketWithDataChannel('candles', { key }) + + if (!s) { + throw new Error('no data socket available; did you provide a key?') + } + + s.ws.onCandle({ key, cbGID }, cb) + } + + /** + * @param {object} opts - options + * @param {string} opts.symbol - order book symbol + * @param {string} [opts.prec] - precision, i.e. 'R0', default 'P0' + * @param {string} [opts.len] - length, default '25' + * @param {string} [opts.freq] - default 'F0' + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @throws an error if no data socket is available + * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books + */ + onOrderBook ({ symbol, prec = 'P0', len = '25', freq = 'F0', cbGID }, cb) { + const filter = {} + + if (symbol) filter.symbol = symbol + if (prec) filter.prec = prec + if (len) filter.len = len + if (freq) filter.freq = freq + + const s = this.getSocketWithDataChannel('book', filter) + + if (!s) { + throw new Error('no data socket available; did you provide a symbol?') + } + + s.ws.onOrderBook({ cbGID, ...filter }, cb) + } + + /** + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol for trades + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @throws an error if no data socket is available + * @see https://docs.bitfinex.com/v2/reference#ws-public-trades + */ + onTrades ({ symbol, cbGID }, cb) { + const s = this.getSocketWithDataChannel('trades', { symbol }) + + if (!s) { + throw new Error('no data socket available; did you provide a symbol?') + } + + s.ws.onTrades({ symbol, cbGID }, cb) + } + + /** + * @param {object} opts - options + * @param {string} [opts.symbol] - symbol for ticker + * @param {string} [opts.cbGID] - callback group id + * @param {Function} cb - callback + * @throws an error if no data socket is available + * @see https://docs.bitfinex.com/v2/reference#ws-public-ticker + */ + onTicker ({ symbol = '', cbGID } = {}, cb) { + const s = this.getSocketWithDataChannel('ticker', { symbol }) + + if (!s) { + throw new Error('no data socket available; did you provide a symbol?') + } + + s.ws.onTicker({ symbol, cbGID }, cb) + } +} + +module.exports = WS2Manager diff --git a/package.json b/package.json index 1aea61dd..7607f5cf 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,23 @@ { "name": "bitfinex-api-node", - "version": "0.3.0", + "version": "8.0.0", "description": "Node reference library for Bitfinex API", "engines": { - "node": ">=6" + "node": ">=18.0" }, "main": "index.js", + "husky": { + "hooks": { + "pre-commit": "npm test" + } + }, "scripts": { "lint": "standard", + "lint:fix": "standard --fix", "test": "npm run lint && npm run unit", - "unit": "NODE_ENV=test istanbul cover _mocha -- -R spec -b --recursive", - "test-without-coverage": "NODE_ENV=test mocha -R spec -b --recursive", - "docs": "node_modules/jsdoc-to-markdown/bin/cli.js ws.js > DOCS.md" + "unit": "NODE_ENV=test mocha -b --recursive", + "docs": "rm -rf docs && node_modules/.bin/jsdoc --configure .jsdoc.json --verbose", + "prepare": "husky install" }, "repository": { "type": "git", @@ -22,25 +28,52 @@ "bitcoin", "BTC" ], - "author": "Josh Rossi (https://www.bitfinex.com)", + "contributors": [ + "Ezequiel Wernicke (https://www.bitfinex.com)", + "Josh Rossi (https://www.bitfinex.com)", + "Cris Mihalache (https://www.bitfinex.com)", + "Robert Kowalski (https://www.bitfinex.com)", + "Simone Poggi (https://www.bitfinex.com)", + "Paolo Ardoino (https://www.bitfinex.com)", + "Abhishek Shrestha (https://www.bitfinex.com)" + ], "license": "MIT", "bugs": { "url": "https://github.com/bitfinexcom/bitfinex-api-node/issues" }, "homepage": "http://bitfinexcom.github.io/bitfinex-api-node/", "devDependencies": { - "chai": "^3.4.1", - "debug": "^2.2.0", - "istanbul": "^0.4.1", - "jsdoc-to-markdown": "^1.3.1", - "lodash": "^3.10.1", - "mocha": "^2.3.4", - "standard": "^10.0.2" + "bfx-api-mock-srv": "^2.0.0", + "blessed": "0.1.81", + "blessed-contrib": "^1.0.11", + "cli-table3": "^0.6.5", + "docdash": "^2.0.2", + "dotenv": "^16.4.5", + "husky": "^9.1.6", + "jsdoc-to-markdown": "^9.0.1", + "mocha": "^10.7.3", + "p-iteration": "1.1.8", + "readline-promise": "1.0.4", + "socks-proxy-agent": "^8.0.4", + "standard": "^17.1.2" }, "dependencies": { - "debug": "^2.2.0", - "request": "^2.67.0", - "request-promise": "^4.2.0", - "ws": "^0.8.1" + "bfx-api-node-models": "^2.0.0", + "bfx-api-node-rest": "^7.0.0", + "bfx-api-node-util": "^1.0.12", + "bfx-api-node-ws1": "^1.0.0", + "bignumber.js": "9.0.0", + "cbq": "0.0.1", + "debug": "4.3.3", + "lodash": "^4.17.4", + "lodash.throttle": "4.1.1", + "lossless-json": "1.0.3", + "promise-throttle": "1.0.1", + "ws": "7.5.10" + }, + "standard": { + "ignore": [ + "/docs/**/*.js" + ] } } diff --git a/rest.js b/rest.js deleted file mode 100644 index 7552a0be..00000000 --- a/rest.js +++ /dev/null @@ -1,432 +0,0 @@ -/* eslint-disable */ - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' - -const crypto = require('crypto') -const request = require('request') - -function rest (key, secret, nonceGenerator) { - this.url = 'https://api.bitfinex.com/' - this.version = 'v1' - this.key = key - this.secret = secret - this.nonce = new Date().getTime() - this._nonce = typeof nonceGenerator === 'function' ? nonceGenerator : function () { - // noinspection JSPotentiallyInvalidUsageOfThis - return ++this.nonce - } -} - -rest.prototype.make_request = function (path, params, cb) { - var headers, key, nonce, path, payload, signature, url, value - if (!this.key || !this.secret) { - return cb(new Error('missing api key or secret')) - } - url = `${this.url}/${this.version}/${path}` - nonce = JSON.stringify(this._nonce()) - payload = { - request: path, - nonce - } - for (key in params) { - value = params[key] - payload[key] = value - } - payload = new Buffer(JSON.stringify(payload)).toString('base64') - signature = crypto.createHmac('sha384', this.secret).update(payload).digest('hex') - headers = { - 'X-BFX-APIKEY': this.key, - 'X-BFX-PAYLOAD': payload, - 'X-BFX-SIGNATURE': signature - } - return request({ - url, - method: 'POST', - headers, - timeout: 15000 - }, (err, response, body) => { - let error, result - if (err || (response.statusCode !== 200 && response.statusCode !== 400)) { - return cb(new Error(err != null ? err : response.statusCode)) - } - try { - result = JSON.parse(body) - } catch (error1) { - error = error1 - return cb(null, { - message: body.toString() - }) - } - if (result.message != null) { - return cb(new Error(result.message)) - } - return cb(null, result) - }) -} - -rest.prototype.make_public_request = function (path, cb) { - const url = `${this.url}/${this.version}/${path}` - return request({ - url, - method: 'GET', - timeout: 15000 - }, (err, response, body) => { - let error, result - if (err || (response.statusCode !== 200 && response.statusCode !== 400)) { - return cb(new Error(err != null ? err : response.statusCode)) - } - try { - result = JSON.parse(body) - } catch (error1) { - error = error1 - return cb(null, { - message: body.toString() - }) - } - if (result.message != null) { - return cb(new Error(result.message)) - } - return cb(null, result) - }) -} - -rest.prototype.ticker = function (symbol, cb) { - if (arguments.length == 0) { - symbol = 'BTCUSD' - cb = function (error, data) { console.log(data) } - } - return this.make_public_request('pubticker/' + symbol, cb) -} - -rest.prototype.today = function (symbol, cb) { - return this.make_public_request('today/' + symbol, cb) -} - -rest.prototype.stats = function (symbol, cb) { - return this.make_public_request('stats/' + symbol, cb) -} - -// rest.prototype.candles = function (symbol, cb) { -// return this.make_public_request('candles/' + symbol, cb); -// }; - -rest.prototype.fundingbook = function (currency, options, cb) { - let err, index, option, query_string, uri, value - index = 0 - uri = 'lendbook/' + currency - if (typeof options === 'function') { - cb = options - } else { - try { - for (option in options) { - value = options[option] - if (index++ > 0) { - query_string += '&' + option + '=' + value - } else { - query_string = '/?' + option + '=' + value - } - } - if (index > 0) { - uri += query_string - } - } catch (error1) { - err = error1 - return cb(err) - } - } - return this.make_public_request(uri, cb) -} - -rest.prototype.orderbook = function (symbol, options, cb) { - let err, index, option, query_string, uri, value - index = 0 - uri = 'book/' + symbol - if (typeof options === 'function') { - cb = options - } else { - try { - for (option in options) { - value = options[option] - if (index++ > 0) { - query_string += '&' + option + '=' + value - } else { - query_string = '/?' + option + '=' + value - } - } - if (index > 0) { - uri += query_string - } - } catch (error1) { - err = error1 - return cb(err) - } - } - return this.make_public_request(uri, cb) -} -rest.prototype.trades = function (symbol, cb) { - return this.make_public_request('trades/' + symbol, cb) -} - -rest.prototype.lends = function (currency, cb) { - return this.make_public_request('lends/' + currency, cb) -} - -rest.prototype.get_symbols = function (cb) { - return this.make_public_request('symbols', cb) -} - -rest.prototype.symbols_details = function (cb) { - return this.make_public_request('symbols_details', cb) -} - -rest.prototype.new_order = function (symbol, amount, price, exchange, side, type, is_hidden, cb) { - if (typeof is_hidden === 'function') { - cb = is_hidden - is_hidden = false - } - const params = { - symbol, - amount, - price, - exchange, - side, - type - } - if (is_hidden) { - params['is_hidden'] = true - } - return this.make_request('order/new', params, cb) -} - -rest.prototype.multiple_new_orders = function (orders, cb) { - const params = { - orders - } - return this.make_request('order/new/multi', params, cb) -} - -rest.prototype.cancel_order = function (order_id, cb) { - const params = { - order_id: parseInt(order_id) - } - return this.make_request('order/cancel', params, cb) -} - -rest.prototype.cancel_all_orders = function (cb) { - return this.make_request('order/cancel/all', {}, cb) -} - -rest.prototype.cancel_multiple_orders = function (order_ids, cb) { - const params = { - order_ids: order_ids.map((id) => parseInt(id)) - } - return this.make_request('order/cancel/multi', params, cb) -} - -rest.prototype.replace_order = function (order_id, symbol, amount, price, exchange, side, type, cb) { - const params = { - order_id: parseInt(order_id), - symbol, - amount, - price, - exchange, - side, - type - } - return this.make_request('order/cancel/replace', params, cb) -} - -rest.prototype.order_status = function (order_id, cb) { - const params = { - order_id - } - return this.make_request('order/status', params, cb) -} - -rest.prototype.active_orders = function (cb) { - return this.make_request('orders', {}, cb) -} - -rest.prototype.active_positions = function (cb) { - return this.make_request('positions', {}, cb) -} - -rest.prototype.claim_position = function (position_id, cb) { - const params = { - position_id: parseInt(position_id) - } - return this.make_request('position/claim', params, cb) -} - -rest.prototype.balance_history = function (currency, options, cb) { - let err, option, value - const params = { - currency - } - if (typeof options === 'function') { - cb = options - } else { - try { - for (option in options) { - value = options[option] - params[option] = value - } - } catch (error1) { - err = error1 - return cb(err) - } - } - return this.make_request('history', params, cb) -} - -rest.prototype.movements = function (currency, options, cb) { - let err, option, value - const params = { - currency - } - if (typeof options === 'function') { - cb = options - } else { - try { - for (option in options) { - value = options[option] - params[option] = value - } - } catch (error1) { - err = error1 - return cb(err) - } - } - return this.make_request('history/movements', params, cb) -} - -rest.prototype.past_trades = function (symbol, options, cb) { - let err, option, value - const params = { - symbol - } - if (typeof options === 'function') { - cb = options - } else { - try { - for (option in options) { - value = options[option] - params[option] = value - } - } catch (error1) { - err = error1 - return cb(err) - } - } - return this.make_request('mytrades', params, cb) -} - -rest.prototype.new_deposit = function (currency, method, wallet_name, cb) { - const params = { - currency, - method, - wallet_name - } - return this.make_request('deposit/new', params, cb) -} - -rest.prototype.new_offer = function (currency, amount, rate, period, direction, cb) { - const params = { - currency, - amount, - rate, - period, - direction - } - return this.make_request('offer/new', params, cb) -} - -rest.prototype.cancel_offer = function (offer_id, cb) { - const params = { - offer_id - } - return this.make_request('offer/cancel', params, cb) -} - -rest.prototype.offer_status = function (offer_id, cb) { - const params = { - offer_id - } - return this.make_request('offer/status', params, cb) -} - -rest.prototype.active_offers = function (cb) { - return this.make_request('offers', {}, cb) -} - -rest.prototype.active_credits = function (cb) { - return this.make_request('credits', {}, cb) -} - -rest.prototype.wallet_balances = function (cb) { - return this.make_request('balances', {}, cb) -} - -rest.prototype.taken_swaps = function (cb) { - return this.make_request('taken_funds', {}, cb) -} - -rest.prototype.total_taken_swaps = function (cb) { - return this.make_request('total_taken_funds', {}, cb) -} - -rest.prototype.close_swap = function (swap_id, cb) { - return this.make_request('swap/close', { - swap_id - }, cb) -} - -rest.prototype.account_infos = function (cb) { - return this.make_request('account_infos', {}, cb) -} - -rest.prototype.margin_infos = function (cb) { - return this.make_request('margin_infos', {}, cb) -} - -/* - POST /v1/withdraw - - Parameters: - 'withdraw_type' :string (can be "bitcoin", "litecoin" or "darkcoin" or "mastercoin") - 'walletselected' :string (the origin of the wallet to withdraw from, can be "trading", "exchange", or "deposit") - 'amount' :decimal (amount to withdraw) - 'address' :address (destination address for withdrawal) - */ - -rest.prototype.withdraw = function (withdraw_type, walletselected, amount, address, cb) { - const params = { - withdraw_type, - walletselected, - amount, - address - } - return this.make_request('withdraw', params, cb) -} - -/* - POST /v1/transfer - - Parameters: - ‘amount’: decimal (amount to transfer) - ‘currency’: string, currency of funds to transfer - ‘walletfrom’: string. Wallet to transfer from - ‘walletto’: string. Wallet to transfer to - */ - -rest.prototype.transfer = function (amount, currency, walletfrom, walletto, cb) { - const params = { - amount, - currency, - walletfrom, - walletto - } - return this.make_request('transfer', params, cb) -} - -module.exports = rest diff --git a/rest2.js b/rest2.js deleted file mode 100644 index c2289baf..00000000 --- a/rest2.js +++ /dev/null @@ -1,114 +0,0 @@ -const rp = require('request-promise') -const crypto = require('crypto') -const BASE_TIMEOUT = 15000 - -class Rest2 { - constructor (key, secret, nonceGenerator) { - this.url = 'https://api.bitfinex.com/' - this.version = 'v2' - this.key = key - this.secret = secret - this.nonce = new Date().getTime() - this.generateNonce = (typeof nonceGenerator === 'function') - ? nonceGenerator - : function () { - // noinspection JSPotentiallyInvalidUsageOfThis - return ++this.nonce - } - } - - genericCallback (err, result) { - console.log(err, result) - } - - makeAuthRequest (path, payload = {}, cb = this.genericCallback) { - if (!this.key || !this.secret) { - return cb(new Error('missing api key or secret')) - } - const url = `${this.url}/${this.version}/${path}` - const nonce = JSON.stringify(this.generateNonce()) - const rawBody = JSON.stringify(payload) - - const signature = crypto - .createHmac('sha384', this.secret) - .update(`/api/${url}${nonce}${rawBody}`) - .digest('hex') - - return rp({ - url, - method: 'POST', - headers: { - 'bfx-nonce': nonce, - 'bfx-apikey': this.key, - 'bfx-signature': signature - }, - json: payload - }) - .then((response) => cb(null, JSON.parse(response))) - .catch((error) => cb(new Error(error))) - } - - makePublicRequest (name, cb = this.genericCallback) { - const url = `${this.url}/${this.version}/${name}` - return rp({ - url, - method: 'GET', - timeout: BASE_TIMEOUT - }) - .then((response) => cb(null, JSON.parse(response))) - .catch((error) => cb(new Error(error))) - } - - // Public endpoints - - ticker (symbol = 'tBTCUSD', cb) { - return this.makePublicRequest(`ticker/${symbol}`, cb) - } - - tickers (cb) { - return this.makePublicRequest(`tickers`, cb) - } - - stats (key = 'pos.size:1m:tBTCUSD:long', context = 'hist', cb) { - return this.makePublicRequest(`stats1/${key}/${context}`, cb) - } - - // timeframes: '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D', '1M' - // sections: 'last', 'hist' - // note: query params can be added: see - // http://docs.bitfinex.com/v2/reference#rest-public-candles - candles ({timeframe = '1m', symbol = 'tBTCUSD', section = 'hist'}, cb) { - return this.makePublicRequest(`stats1/trade:${timeframe}:${symbol}/${section}`, cb) - } - - // TODO - // - Trades - // - Books - - // Auth endpoints - - alertList (type = 'price', cb) { - return this.makeAuthRequest(`/auth/r/alerts?type=${type}`, null, cb) - } - - alertSet (type = 'price', symbol = 'tBTCUSD', price = 0) { - return this.makeAuthRequest(`/auth/w/alert/set`, {type, symbol, price}) - } - - alertDelete (symbol = 'tBTCUSD', price = 0) { - return this.makeAuthRequest(`/auth/w/alert/set`, {symbol, price}) - } - - // TODO - // - Wallets - // - Orders - // - Order Trades - // - Positions - // - Offers - // - Margin Info - // - Funding Info - // - Performance - // - Calc Available Balance -} - -module.exports = Rest2 diff --git a/test/example_testkeys.json b/test/example_testkeys.json deleted file mode 100644 index 6587810c..00000000 --- a/test/example_testkeys.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "read-only": { - "key_id": 0, - "api_key": "api_key", - "api_secret": "api_secret" - }, - "withdrawal-enabled": { - "key_id": 18259, - "api_key": "api_key", - "api_secret": "api_secret" - }, - "standard": { - "key_id": 18261, - "api_key": "api_key", - "api_secret": "api_secret" - } -} diff --git a/test/examples/args_from_env.js b/test/examples/args_from_env.js new file mode 100644 index 00000000..d55d737e --- /dev/null +++ b/test/examples/args_from_env.js @@ -0,0 +1,68 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const _isUndefined = require('lodash/isUndefined') +const _isObject = require('lodash/isObject') +const _isString = require('lodash/isString') +const _isEmpty = require('lodash/isEmpty') +const { SocksProxyAgent } = require('socks-proxy-agent') +const argsFromEnv = require('../../examples/util/args_from_env') + +describe('argsFromEnv', () => { + it('pulls api credentials from the environment only if available', () => { + delete process.env.API_KEY + delete process.env.API_SECRET + + let args = argsFromEnv() + + assert.ok(_isObject(args), 'did not return an object') + assert(_isUndefined(args.apiKey), 'api key parsed although not present on env') + assert(_isUndefined(args.apiSecret), 'api secret parsed although not present on env') + + process.env.API_KEY = '42' + process.env.API_SECRET = '9000' + + args = argsFromEnv() + + assert.ok(_isObject(args), 'did not return an object') + assert.strictEqual(args.apiKey, '42', 'api key not pulled from env') + assert.strictEqual(args.apiSecret, '9000', 'api secret not pull from env') + }) + + it('provides a connection agent if a socks proxy url is available on the env', () => { + const url = 'socks4://localhost:9998' + delete process.env.SOCKS_PROXY_URL + + let args = argsFromEnv() + + assert.ok(_isObject(args), 'did not return an object') + assert.ok(_isUndefined(args.agent), 'agent provided although no config on env') + + process.env.SOCKS_PROXY_URL = url + + args = argsFromEnv() + + assert.ok(_isObject(args), 'did not return an object') + assert.ok((args.agent instanceof SocksProxyAgent), 'did not provide a SocksProxyAgent instance') + assert.ok(/localhost/.test(args.agent.proxy.host), 'provided agent does not use proxy url from env') + }) + + it('provides a connection url only if available', () => { + const url = 'localhost:8080' + delete process.env.TEST_URL + + let args = argsFromEnv('TEST_URL') + + assert.ok(_isObject(args), 'did not return an object') + assert.ok(_isUndefined(args.url), 'url provided although no config on env') + + process.env.TEST_URL = url + + args = argsFromEnv('TEST_URL') + + assert.ok(_isObject(args), 'did not return an object') + assert.ok(_isString(args.url) && !_isEmpty(args.url), 'connection url not pulled from env') + assert.strictEqual(args.url, url, 'provided url does not match env var') + }) +}) diff --git a/test/examples/debug_table.js b/test/examples/debug_table.js new file mode 100644 index 00000000..d0ebc3f6 --- /dev/null +++ b/test/examples/debug_table.js @@ -0,0 +1,34 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const debugTable = require('../../examples/util/debug_table') + +describe('debugTable', () => { + it('throws an error if row, header, and column counts don\'t match', () => { + try { + debugTable({ + rows: [[1]], + headers: ['', ''], + widths: [20, 20, 20], + debug: () => {} + }) + assert.fail('no error was thrown') + } catch (e) { + assert.ok(true) + } + }) + + it('prints the table out line by line, and returns it as a multi-line string', () => { + let debugLineCount = 0 + + const str = debugTable({ + rows: [[1, 1, 1], [2, 2, 2], [3, 3, 3]], + headers: ['', '', ''], + widths: [20, 20, 20], + debug: () => debugLineCount++ + }) + + assert.strictEqual(str.split('\n').length, debugLineCount) + }) +}) diff --git a/test/examples/setup.js b/test/examples/setup.js new file mode 100644 index 00000000..d9c06a5a --- /dev/null +++ b/test/examples/setup.js @@ -0,0 +1,20 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const _isObject = require('lodash/isObject') +const _isFunction = require('lodash/isFunction') + +const { args, debug, debugTable, readline } = require('../../examples/util/setup') + +describe('setup', () => { + it('provides a debugger', () => { + assert.ok(_isObject(args), 'setup doesnt provide a tooling object') + assert.ok(_isFunction(debug), 'setup doesnt provide a debug() instance') + assert.ok(_isFunction(debugTable), 'setup doesnt provide a debugTable() instance') + }) + + it('provides a readline instance', () => { + assert.ok(_isFunction(readline.questionAsync), 'no readline instance provided') + }) +}).timeout(10 * 1000) // timeout for travis diff --git a/test/fixtures/response-ticker-funding.json b/test/fixtures/response-ticker-funding.json new file mode 100644 index 00000000..da1c8b99 --- /dev/null +++ b/test/fixtures/response-ticker-funding.json @@ -0,0 +1 @@ +[0.0009239,0.00071,30,5000,0.0009239,2,44568.06495174,0.00044901,0.3207,0.001849,14032554.7966796,0,0] diff --git a/test/fixtures/response-ticker-pairs.json b/test/fixtures/response-ticker-pairs.json new file mode 100644 index 00000000..7be7802c --- /dev/null +++ b/test/fixtures/response-ticker-pairs.json @@ -0,0 +1 @@ +[1781.8,3.10227283,1781.9,1.44527318,-35.7,-0.0196,1781.8,13402.66689773,1834.2,1726.3] diff --git a/test/fixtures/response-trades-funding.json b/test/fixtures/response-trades-funding.json new file mode 100644 index 00000000..fcdf06c0 --- /dev/null +++ b/test/fixtures/response-trades-funding.json @@ -0,0 +1 @@ +[[6735741,1494853437352,-7342.32130201,0.00179797,30],[6735740,1494853437232,-6.22726787,0.00179797,30],[6735734,1494853434026,-4078.57360794,0.00179797,30],[6735730,1494853432487,-1438.49972447,0.00179797,30],[6735729,1494853431991,-195.61999655,0.00179797,30],[6735728,1494853431651,-2219.23318615,0.00179797,30],[6735727,1494853431627,-74.47875318,0.00179797,5],[6735726,1494853431609,-1234.13448145,0.00179797,30],[6735725,1494853431595,-66.99,0.00179797,30],[6735724,1494853431581,-56.22,0.00179797,30],[6735723,1494853431564,-233.66,0.00179797,30],[6735722,1494853431549,-95.94,0.00179797,30],[6735721,1494853431532,-591.74236565,0.00179794,30],[6735720,1494853431514,-513.37126375,0.00179794,30],[6735719,1494853431501,-59.08787049,0.00179792,2],[6735718,1494853431486,-59.08787049,0.00179791,30],[6735717,1494853431473,-50.49751111,0.00179787,30],[6735716,1494853431461,-50.49751111,0.00179787,30],[6735715,1494853431445,-100.99502222,0.00179787,30],[6735714,1494853431426,-2039.12850002,0.00179,30],[6735713,1494853431405,-183.88803906,0.00179,30],[6735711,1494853430206,-1855.24046096,0.00179,30],[6735710,1494853430193,-53.4042344,0.00152855,2],[6735709,1494853430181,-53.4042344,0.00152855,30],[6735708,1494853430165,-52.02165252,0.00093527,30],[6735707,1494853430150,-499.56735807,0.00093527,15],[6735706,1494853430137,-818.34204076,0.00093527,30],[6735705,1494853430123,-195.62001798,0.00093405,2],[6735695,1494853428508,-37.16518202,0.00093405,2],[6735688,1494853141068,-124.34634957,0.00178999,30],[6735687,1494853140987,-50.48604426,0.0017899,30],[6735686,1494853140978,-50.48604426,0.0017899,30],[6735685,1494853140967,-50.48604426,0.0017898,30],[6735684,1494853140957,-51.25765085,0.00160388,30],[6735683,1494853140914,-50.48604426,0.00160378,30],[6735682,1494853140813,-50.48604426,0.00160378,30],[6735681,1494853140801,-50.48604426,0.00160378,30],[6735680,1494853140790,-49.47973402,0.00160378,30],[6735679,1494853140761,-1.00631024,0.00160378,30],[6735678,1494853140748,-50.48604426,0.00160378,30],[6735677,1494853140726,-373.08499213,0.00160085,2],[6735676,1494853140716,-373.08499213,0.00160085,2],[6735675,1494853140702,-373.08499213,0.00160085,2],[6735674,1494853140690,-373.08499213,0.00160085,2],[6735673,1494853140679,-373.08499213,0.00160085,2],[6735672,1494853140664,-373.08499213,0.00160085,2],[6735671,1494853140652,-373.08499213,0.00160085,2],[6735670,1494853140633,-373.08499213,0.00160085,2],[6735669,1494853140617,-373.08499213,0.00160085,2],[6735668,1494853140603,-373.08499213,0.00160085,2],[6735667,1494853140591,-373.08499213,0.00160085,2],[6735666,1494853140577,-373.08499213,0.00160085,2],[6735665,1494853140564,-373.08499213,0.00160085,2],[6735664,1494853140551,-373.08499213,0.00160085,2],[6735663,1494853140532,-373.08499213,0.00160085,2],[6735662,1494853140513,-70.43,0.00158165,2],[6735661,1494853140500,-50.48604426,0.00142968,30],[6735660,1494853140486,-50.48604426,0.00142968,30],[6735659,1494853140468,-50.48604426,0.00142968,30],[6735658,1494853140456,-50.48604426,0.00142968,30],[6735657,1494853140445,-182.48,0.00140436,30],[6735656,1494853140432,-2462.69,0.00140436,30],[6735655,1494853140417,-1480.11391365,0.00125,30],[6735654,1494853140399,-50.48604426,0.0012499,30],[6735653,1494853140378,-50.48604426,0.0012499,30],[6735652,1494853140363,-51.25765085,0.0012,30],[6735651,1494853140345,-3740.90039169,0.0012,30],[6735650,1494853140330,-245.20955164,0.0012,30],[6735649,1494853140316,-117.84692501,0.0012,2],[6735648,1494853140300,-240.40288527,0.00119999,30],[6735647,1494853140286,-74.42092493,0.0011999,30],[6735646,1494853140273,-171.64801902,0.0011999,30],[6735645,1494853140256,-50.48604426,0.0011999,30],[6735644,1494853140243,-50.48604426,0.0011999,30],[6735643,1494853140228,-50.48604426,0.0011999,30],[6735641,1494853140215,-50.48604426,0.0011999,30],[6735639,1494853140202,-50.48604426,0.0011999,30],[6735636,1494853140181,-6508.92016589,0.00114717,30],[6735634,1494853140139,-566.49659196,0.00114717,30],[6735632,1494853140079,-0.82473213,0.00114717,30],[6735631,1494853139695,-324.49232558,0.00114717,30],[6735630,1494853139675,-26.64483104,0.00114717,30],[6735629,1494853139644,-895.94471993,0.00114717,30],[6735628,1494853139625,-571.02559163,0.00114717,30],[6735627,1494853139607,-739.67795707,0.00114714,2],[6735626,1494853139594,-2068,0.00107009,7],[6735625,1494853139577,-50.48604426,0.00093634,7],[6735624,1494853139563,-50.48604426,0.00093634,7],[6735623,1494853139549,-50.48604426,0.00093634,7],[6735622,1494853139534,-50.48604426,0.00093634,7],[6735621,1494853139521,-50.48604426,0.00093634,7],[6735620,1494853139507,-50.48604426,0.00093631,7],[6735619,1494853139489,-147.90174828,0.000936,14],[6735617,1494853139470,-51.25765085,0.00093589,30],[6735615,1494853139454,-50.48604426,0.00093584,7],[6735612,1494853139425,-455.25010385,0.00093567,2],[6735610,1494853139409,-56.21,0.00093567,2],[6735609,1494853139397,-700.99,0.00093567,30],[6735607,1494853139385,-95.94,0.00093567,30],[6735605,1494853139373,-50.48604426,0.00093557,7],[6735603,1494853139360,-50.48604426,0.00093557,7],[6735602,1494853139347,-56.07612898,0.00093507,30],[6735601,1494853139330,-51.25765085,0.00093507,30],[6735600,1494853139314,-51.25765085,0.00093507,30],[6735599,1494853139303,-51.25765085,0.00093507,30],[6735598,1494853139285,-538.77227604,0.00093505,5],[6735597,1494853139265,-68.7371996,0.00093505,5],[6735596,1494853139251,-274.37411766,0.00093505,2],[6735595,1494853139240,-3773.43737221,0.00093505,30],[6735594,1494853139227,-6283.68710181,0.00093505,30],[6735593,1494853139213,-7302.52088507,0.00093505,30],[6735592,1494853139192,-72.69342505,0.00093505,30],[6735591,1494853139175,-3670.70993709,0.00093505,30],[6735590,1494853139161,-710.97877232,0.00093505,5],[6735589,1494853139139,-1750.68218205,0.00093505,30],[6735588,1494853139120,-5917.26229886,0.00093505,30],[6735587,1494853139105,-4477.98100335,0.00093505,2],[6735586,1494853139088,-52.21529333,0.00093505,30],[6735585,1494853139073,-590.02,0.00093504,2],[6735584,1494853139056,-54.52,0.00093504,2]] diff --git a/test/fixtures/response-trades-pairs.json b/test/fixtures/response-trades-pairs.json new file mode 100644 index 00000000..534ffae3 --- /dev/null +++ b/test/fixtures/response-trades-pairs.json @@ -0,0 +1 @@ +[[32179419,1494853783000,-0.04824433,1760.2],[32179413,1494853779000,0.03689378,1762.9],[32179410,1494853778000,-2,1763.1],[32179398,1494853774000,0.03694043,1763.4],[32179395,1494853772000,0.0170008,1763.4],[32179393,1494853768000,0.3134,1763.4],[32179392,1494853766000,0.1717,1763.4],[32179391,1494853765000,0.03674472,1763.4],[32179390,1494853765000,0.00009347,1763.4],[32179387,1494853763000,0.1717,1763.4],[32179385,1494853760000,0.5,1763.4],[32179384,1494853758000,0.2807,1763.4],[32179377,1494853756000,-0.248183,1762.9],[32179376,1494853754000,0.2582,1763.4],[32179375,1494853752000,-0.285902,1762.9],[32179374,1494853751000,0.21930653,1763.4],[32179373,1494853751000,0.28069347,1762.9],[32179372,1494853748000,0.5,1762.9],[32179371,1494853748000,0.01096553,1762.9],[32179359,1494853745000,0.1525,1762.9],[32179358,1494853731000,-0.101043,1762.7],[32179345,1494853700000,0.5,1762.9],[32179344,1494853698000,0.05,1763.4],[32179342,1494853696000,0.04711822,1763.3],[32179341,1494853696000,0.05288178,1762.8],[32179339,1494853695000,0.86873374,1762.8],[32179338,1494853694000,0.09,1762.8],[32179337,1494853693000,0.185,1762.8],[32179336,1494853690000,0.036,1762.8],[32179335,1494853689000,0.03595638,1762.8],[32179334,1494853689000,0.00004362,1762.3],[32179333,1494853689000,0.036,1762.3],[32179330,1494853686000,0.01868959,1762.3],[32179328,1494853686000,0.01868809,1762.3],[32179327,1494853686000,0.3,1762.3],[32179324,1494853685000,0.01868935,1762.3],[32179317,1494853685000,0.01868935,1762.3],[32179303,1494853683000,0.05,1762.3],[32179302,1494853681000,0.0721,1762.3],[32179301,1494853678000,0.1871,1762.3],[32179300,1494853676000,0.5,1762.3],[32179298,1494853673000,0.3,1762.3],[32179295,1494853663000,0.96,1762.4],[32179297,1494853663000,0.4070081,1762.8],[32179296,1494853663000,0.7534,1762.7],[32179294,1494853663000,0.24999,1762.4],[32179293,1494853663000,0.32344047,1762.3],[32179281,1494853657000,0.01616872,1762.3],[32179280,1494853657000,0.48020443,1762.2],[32179275,1494853656000,0.52038848,1762.2],[32179274,1494853656000,0.00830055,1761.7],[32179270,1494853651000,1.66872777,1761],[32179271,1494853651000,1.59169945,1761.7],[32179263,1494853649000,0.1742,1761],[32179262,1494853647000,0.4539,1761],[32179259,1494853645000,0.5,1761],[32179258,1494853644000,0.64862543,1761],[32179248,1494853643000,0.131,1761],[32179246,1494853642000,1.2488468,1761],[32179245,1494853641000,0.1747,1761],[32179243,1494853638000,1.00097535,1762],[32179242,1494853638000,0.1,1762],[32179241,1494853638000,0.00089345,1761.7],[32179240,1494853638000,0.39081416,1761.7],[32179236,1494853635000,0.5,1761.1],[32179234,1494853627000,0.079,1761.1],[32179233,1494853627000,0.421,1760],[32179232,1494853625000,0.0571,1760],[32179231,1494853621000,0.5,1760],[32179230,1494853619000,0.5,1760],[32179229,1494853618000,0.3587,1760],[32179228,1494853616000,0.5,1760],[32179227,1494853614000,0.5,1760],[32179226,1494853611000,0.5,1760],[32179225,1494853609000,0.5,1760],[32179224,1494853608000,0.5,1760],[32179223,1494853606000,0.5,1760],[32179222,1494853604000,0.076,1760],[32179221,1494853602000,0.0872,1760],[32179220,1494853594000,1.15228897,1760.4],[32179219,1494853594000,0.08998646,1760.2],[32179218,1494853594000,0.29275122,1760],[32179216,1494853593000,3.18635482,1760],[32179212,1494853568000,0.21838758,1760],[32179205,1494853562000,1.6,1759.9],[32179206,1494853562000,2.25250638,1760],[32179204,1494853562000,0.963884,1759.9],[32179203,1494853562000,1.2279,1759.8],[32179183,1494853513000,0.63707354,1759.2],[32179182,1494853513000,0.01209112,1758.4],[32179179,1494853508000,0.08790888,1758.4],[32179175,1494853499000,-0.92759884,1756.2],[32179172,1494853499000,-1.6,1756.8],[32179174,1494853499000,-0.9,1756.6],[32179173,1494853499000,-0.1,1756.6],[32179171,1494853499000,-1.23325116,1757.5],[32179170,1494853499000,-1.5752,1758.1],[32179169,1494853499000,-0.3882,1758.2],[32179168,1494853494000,0.01001354,1760.2],[32179166,1494853493000,-0.21,1759],[32179167,1494853493000,-0.33556,1757.9],[32179165,1494853493000,-0.7763,1759.1],[32179163,1494853489000,0.03756071,1760.3],[32179161,1494853487000,-0.32865189,1757.5],[32179160,1494853487000,-1.12193211,1757.7],[32179157,1494853487000,-0.1,1758.4],[32179158,1494853487000,-1.5033,1758],[32179159,1494853487000,-0.911706,1757.9],[32179155,1494853485000,0.02170315,1761.1],[32179149,1494853472000,0.5118,1761],[32179150,1494853472000,0.50505948,1761.1],[32179144,1494853468000,1.10829239,1761.7],[32179143,1494853468000,1.54315026,1761.1],[32179142,1494853468000,1.6273,1760.5],[32179141,1494853468000,0.1,1760.2],[32179140,1494853468000,1.71930627,1759.8],[32179138,1494853468000,0.08539655,1759.7],[32179139,1494853468000,0.92260345,1759.7],[32179124,1494853467000,-1.09071521,1756.8],[32179123,1494853467000,-0.02894644,1756.9]] diff --git a/test/fixtures/response-ws-1-orderbook-R0.json b/test/fixtures/response-ws-1-orderbook-R0.json new file mode 100644 index 00000000..48839afa --- /dev/null +++ b/test/fixtures/response-ws-1-orderbook-R0.json @@ -0,0 +1,51 @@ +[ 13242, + [ [ 2578842316, 1967.5, 0.1 ], + [ 2578789721, 1967, 2 ], + [ 2578787950, 1966.7, 0.1 ], + [ 2578841323, 1966.7, 1.48 ], + [ 2578790926, 1966.5, 0.1 ], + [ 2578787536, 1966.3, 2.13425 ], + [ 2578782502, 1965.6, 2 ], + [ 2578824150, 1965.5, 0.1 ], + [ 2578754476, 1964.9, 0.1 ], + [ 2578782291, 1964.8, 2 ], + [ 2578781657, 1964.1, 2 ], + [ 2578781578, 1963.4, 2 ], + [ 2578758200, 1962.7, 0.1 ], + [ 2578759464, 1962.7, 2.2418413 ], + [ 2578757543, 1962, 2.24436613 ], + [ 2578842121, 1962, 0.43104651 ], + [ 2578838572, 1961.5, 2 ], + [ 2578757448, 1961.4, 2 ], + [ 2578757373, 1960.7, 2.0164 ], + [ 2578758228, 1960.7, 0.1 ], + [ 2578764149, 1960.2, 3 ], + [ 2578841514, 1960.2, 0.25 ], + [ 2578754697, 1959.9, 0.1 ], + [ 2578757246, 1959.9, 2.1 ], + [ 2578753035, 1959.3, 2.3016 ], + [ 2578842390, 1968.8, -0.07953231 ], + [ 2578842404, 1968.8, -0.1 ], + [ 2578842259, 1968.9, -0.8787 ], + [ 2578840418, 1969, -11.67581916 ], + [ 2578842168, 1969.7, -0.01 ], + [ 2578840772, 1969.8, -10 ], + [ 2578833728, 1969.9, -18.60507973 ], + [ 2578760844, 1970, -0.07251047 ], + [ 2578814935, 1970, -0.07606679 ], + [ 2578828061, 1970, -2 ], + [ 2578734028, 1970.7, -0.1 ], + [ 2578842236, 1970.9, -2.08080841 ], + [ 2578841867, 1971.5, -2.31 ], + [ 2578841774, 1972.3, -2.09 ], + [ 2578732893, 1972.7, -0.1 ], + [ 2578841607, 1972.9, -2.2 ], + [ 2578840675, 1973.6, -2.06821658 ], + [ 2578842101, 1973.9, -0.58625479 ], + [ 2578760447, 1974, -1 ], + [ 2578763766, 1974, -1 ], + [ 2578840619, 1974.3, -2.147368 ], + [ 2578798094, 1974.6, -0.01 ], + [ 2578731148, 1974.7, -0.1 ], + [ 2578734894, 1974.8, -49.9 ], + [ 2578420769, 1975, -0.02 ] ] ] diff --git a/test/fixtures/response-ws2-server-order-book-P0.json b/test/fixtures/response-ws2-server-order-book-P0.json new file mode 100644 index 00000000..1092a5bc --- /dev/null +++ b/test/fixtures/response-ws2-server-order-book-P0.json @@ -0,0 +1 @@ +[300,[[1921.8,1,0.17974513],[1920.1,1,0.8809],[1920,4,3.61],[1919.2,1,1.4],[1919,2,1.09],[1918.4,1,1.58208707],[1918.1,2,1.05],[1918,2,1.03418],[1917.7,1,1.57],[1917.5,1,0.03],[1917,3,6.84188197],[1916.5,1,4.197819],[1916.4,1,1.6],[1916.2,1,0.1],[1915.7,1,1.58652],[1915,2,2.54],[1914.6,2,1.590214],[1914.5,2,0.43327161],[1914.3,1,0.1],[1914.2,2,1.580264],[1913.6,2,1.547905],[1913,1,0.012048],[1912.9,1,1.11],[1912.8,1,1.24],[1912.4,1,0.1],[1921.9,1,-5.12],[1922,2,-5.2353],[1922.4,1,-0.52759063],[1922.6,2,-1.271148],[1923,1,-1.4],[1923.4,1,-0.92932246],[1923.6,1,-5.2026],[1923.7,1,-1.98603296],[1923.8,1,-0.1],[1924,1,-1],[1924.5,1,-1.5],[1925.2,1,-1],[1925.6,1,-0.01],[1925.7,1,-0.1],[1925.8,1,-1.32],[1925.9,1,-1.4679],[1926.4,1,-0.62607888],[1926.5,1,-1.45],[1926.7,1,-6.2431],[1927,1,-1.74938207],[1927.3,1,-1.447971],[1927.6,1,-0.1],[1927.8,3,-2.58442],[1927.9,1,-1.6],[1928.3,1,-0.1]]] diff --git a/test/fixtures/response-ws2-server-order-book-P1.json b/test/fixtures/response-ws2-server-order-book-P1.json new file mode 100644 index 00000000..24280554 --- /dev/null +++ b/test/fixtures/response-ws2-server-order-book-P1.json @@ -0,0 +1 @@ +[31,[[1779,1,42.11518492],[1776,1,0.65],[1775,6,4.08689264],[1774,5,4.426],[1773,5,5.50443213],[1772,6,7.79304654],[1771,4,7.1125],[1770,4,10.32053939],[1769,2,2.3227],[1768,3,2.3928],[1767,2,2.6782],[1766,5,2.29046402],[1765,4,8.4198058],[1764,3,4.1],[1763,3,2.36],[1762,5,27.5415973],[1761,3,1.319864],[1760,8,6.61289691],[1759,5,2.63016337],[1758,2,1.34098036],[1757,4,13.8345],[1756,4,6.23052701],[1755,2,1.24530005],[1754,4,6.049529],[1753,3,4.503684],[1780,14,-33.0779031],[1781,4,-1.58324806],[1782,2,-2.4],[1783,5,-4.17911621],[1784,12,-28.4504812],[1785,14,-9.47617966],[1786,12,-64.80149549],[1787,8,-13.94001992],[1788,10,-45.1987484],[1789,12,-21.28205024],[1790,11,-8.97454227],[1791,8,-12.36341796],[1792,10,-11.23215846],[1793,7,-26.68999992],[1794,6,-5.64348706],[1795,7,-63.0999998],[1796,9,-70.67113304],[1797,7,-14.99802176],[1798,13,-40.14042763],[1799,13,-28.45315875],[1800,23,-430.5491778],[1801,4,-2.56285988],[1802,5,-1.20007984],[1803,8,-2.46653968],[1804,5,-95.64373988]]] diff --git a/test/fixtures/response-ws2-server-order-book-R0.json b/test/fixtures/response-ws2-server-order-book-R0.json new file mode 100644 index 00000000..384392f8 --- /dev/null +++ b/test/fixtures/response-ws2-server-order-book-R0.json @@ -0,0 +1 @@ +[999,[[2567606289,1876.5,1.4305],[2567590859,1876.1,0.99670598],[2567602139,1876.1,1],[2567605297,1875.1,4.76123353],[2567605344,1875.1,0.01],[2567519477,1875,15],[2567552066,1875,0.01],[2567506309,1874.5,0.1],[2567603711,1874.4,1.820442],[2567477077,1874.2,0.19553],[2567477404,1874.2,0.07873206],[2567472587,1873.8,0.136159],[2567602057,1873.6,1.58],[2566800765,1873,1],[2567601717,1873,1.82726983],[2566876255,1872.6,0.0641877],[2567601259,1872.2,1.8],[2567048549,1871.7,0.01252505],[2567601253,1871.7,0.01252505],[2567606086,1871.7,1.50318748],[2566877752,1871.5,2.01],[2567606069,1871.5,4.8],[2567606105,1871,4.8],[2566874995,1870.9,2.0822],[2566863093,1870.7,0.1],[2567594844,1878.1,-0.42265059],[2567600874,1878.3,-0.1],[2567604766,1878.3,-1],[2567593507,1878.9,-1.662207],[2567598418,1879.5,-1.733],[2567588789,1879.6,-1.85096707],[2567593692,1880,-2],[2567564697,1880.1,-50],[2567564825,1880.2,-0.1],[2567587247,1880.3,-1.8071],[2567490789,1881,-1.59002156],[2567572064,1881,-5],[2567490713,1881.7,-1.79858828],[2567484859,1882.1,-0.1],[2567490678,1882.4,-2.001883],[2567601620,1882.9,-1.5],[2567484581,1883,-2],[2567546551,1883,-2],[2567606075,1883.4,-0.28613703],[2567603128,1883.5,-3.95],[2567474435,1883.7,-1.9],[2567504132,1883.7,-0.04646614],[2567504179,1883.7,-0.13263695],[2567403182,1884,-0.1],[2567442532,1884.5,-1.9324]]] diff --git a/test/fixtures/response-ws2-server-ticker-funding.json b/test/fixtures/response-ws2-server-ticker-funding.json new file mode 100644 index 00000000..d12e73d8 --- /dev/null +++ b/test/fixtures/response-ws2-server-ticker-funding.json @@ -0,0 +1 @@ +[22,[0.00078458,0.00075,30,3045.59478528,0.0007825,2,2335880.06705868,-0.0000674,-0.0793,0.0007825,19326761.40360705,0,0]] diff --git a/test/fixtures/response-ws2-server-trades.json b/test/fixtures/response-ws2-server-trades.json new file mode 100644 index 00000000..59f21be5 --- /dev/null +++ b/test/fixtures/response-ws2-server-trades.json @@ -0,0 +1 @@ +[31,[[32288059,1494971706000,-0.00042864,1773.9],[32288058,1494971706000,-0.1,1775.6],[32288057,1494971706000,-0.04470386,1775.8],[32288054,1494971696000,-0.34748184,1776],[32288052,1494971693000,-0.08015163,1777.4],[32288047,1494971690000,-0.01984837,1777.4],[32288044,1494971687000,-0.18976977,1775.7],[32288040,1494971686000,0.26486383,1780.3],[32288039,1494971686000,0.95246765,1779.7],[32288038,1494971686000,0.1,1779.2],[32288037,1494971686000,0.8906,1779.1],[32288036,1494971686000,0.915224,1778.6],[32288035,1494971686000,1.6989623,1778.5],[32288034,1494971686000,0.53929331,1778.3],[32288033,1494971686000,1.9835,1778.2],[32288032,1494971686000,0.60187396,1778.1],[32288031,1494971686000,0.05321495,1778.1],[32288030,1494971684000,-1.79759839,1775.8],[32288029,1494971684000,-0.12600507,1777.4],[32288025,1494971683000,-0.05321495,1777.4],[32288024,1494971682000,0.1,1777.4],[32288023,1494971682000,0.04471204,1777.1],[32288022,1494971682000,0.1,1775.6],[32288021,1494971682000,0.67606794,1775.5],[32288014,1494971682000,-0.05321495,1775.4],[32288011,1494971681000,0.01840201,1775.5],[32288007,1494971678000,-0.21410901,1775.2],[32288006,1494971678000,-0.01986347,1775.2],[32288005,1494971678000,-0.78074942,1775.2],[32288003,1494971677000,-0.1544203,1775.2]]] diff --git a/test/index.js b/test/index.js index cc769dba..61680650 100644 --- a/test/index.js +++ b/test/index.js @@ -1,12 +1,130 @@ /* eslint-env mocha */ +'use strict' const assert = require('assert') const BFX = require('../index') +const { RESTv1, RESTv2 } = require('bfx-api-node-rest') +const WSv1 = require('bfx-api-node-ws1') +const WSv2 = require('../lib/transports/ws2') -describe('Loading Module', () => { - describe('#BFX', () => { - it('should be loaded', () => { - assert.equal(typeof BFX, 'function') +describe('BFX', () => { + it('should be loaded', () => { + assert.strictEqual(typeof BFX, 'function') + }) + + describe('constructor', () => { + it('throws on using the deprecated way to set options', () => { + assert.throws(() => new BFX(2, {})) + assert.throws(() => new BFX('dummy', 'dummy', 2)) + }) + }) + + describe('rest', () => { + it('throws an error if an invalid version is requested', () => { + const bfx = new BFX() + assert.throws(bfx.rest.bind(bfx, 0)) + assert.throws(bfx.rest.bind(bfx, 3)) + }) + + it('returns correct REST api by version', () => { + const bfx = new BFX() + const restDefault = bfx.rest() + const rest1 = bfx.rest(1) + const rest2 = bfx.rest(2) + + assert(restDefault instanceof RESTv2) + assert(rest1 instanceof RESTv1) + assert(rest2 instanceof RESTv2) + }) + + it('passes API keys & transform flag to new transport', () => { + const bfx = new BFX({ + apiKey: 'k', + apiSecret: 's', + transform: true, + rest: { + url: 'http://' + } + }) + + const rest1 = bfx.rest(1) + const rest2 = bfx.rest(2) + + assert.strictEqual(rest1._apiKey, 'k') + assert.strictEqual(rest2._apiKey, 'k') + assert.strictEqual(rest1._apiSecret, 's') + assert.strictEqual(rest2._apiSecret, 's') + assert.strictEqual(rest1._url, 'http://') + assert.strictEqual(rest2._url, 'http://') + assert.strictEqual(rest2._transform, true) + }) + + it('passes extra options to new transport', () => { + const bfx = new BFX() + const rest2 = bfx.rest(2, { url: '/dev/null' }) + assert.strictEqual(rest2._url, '/dev/null') + }) + + it('returns one instance if called twice for the same version', () => { + const bfx = new BFX() + const restA = bfx.rest(2) + const restB = bfx.rest(2) + assert(restA === restB) + }) + }) + + describe('ws', () => { + it('throws an error if an invalid version is requested', () => { + const bfx = new BFX() + assert.throws(bfx.ws.bind(bfx, 0)) + assert.throws(bfx.ws.bind(bfx, 3)) + }) + + it('returns correct WebSocket api by version', () => { + const bfx = new BFX() + const wsDefault = bfx.ws() + const ws1 = bfx.ws(1) + const ws2 = bfx.ws(2) + + assert(wsDefault instanceof WSv2) + assert(ws1 instanceof WSv1) + assert(ws2 instanceof WSv2) + }) + + it('passes API keys & transform flag to new transport', () => { + const bfx = new BFX({ + apiKey: 'k', + apiSecret: 's', + transform: true, + ws: { + url: 'wss://' + } + }) + + const ws1 = bfx.ws(1) + const ws2 = bfx.ws(2) + + assert.strictEqual(ws1._apiKey, 'k') + assert.strictEqual(ws2._authArgs.apiKey, 'k') + assert.strictEqual(ws1._apiSecret, 's') + assert.strictEqual(ws2._authArgs.apiSecret, 's') + assert.strictEqual(ws1._url, 'wss://') + assert.strictEqual(ws2._url, 'wss://') + assert.strictEqual(ws2._transform, true) + }) + + it('passes extra options to new transport', () => { + const bfx = new BFX() + const ws2 = bfx.ws(2, { url: '/dev/null' }) + assert.strictEqual(ws2._url, '/dev/null') + }) + + it('returns one instance if called twice for the same version', () => { + const bfx = new BFX() + const wsA = bfx.ws(2) + const wsB = bfx.ws(2) + + assert(wsA === wsB) }) }) }) diff --git a/test/keys.json b/test/keys.json deleted file mode 100644 index e0a344c8..00000000 --- a/test/keys.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "readonly": { - "key_id": 18257, - "api_key": "m5IPFtyZBphe0PwbVxwWo6onupqV8K8vSqLmzAS2rqB", - "api_secret": "vQG46W7Yv99jSwGgeJCn130XQm2qrGYJsOIT3Se8rhN" - }, - "withdrawalenabled": { - "key_id": 18259, - "api_key": "YaVLf2es6PRVNrJQ9ySjMTjczyZnJASFTsO0WgSs3t4", - "api_secret": "8ki2uK3lf96W8sYI2uy071JJ2biZxXjDtve1zBIR07m" - }, - "standard": { - "key_id": 18261, - "api_key": "7kGrX7bMkwKB16fzwHqKYMySuDAhah0OgUqTv0GvCmW", - "api_secret": "bfopPuyyNO56fSK9MB3wICgzM0MybrpsODZHseNL0yo" - } -} \ No newline at end of file diff --git a/test/lib/transports/channels.js b/test/lib/transports/channels.js new file mode 100644 index 00000000..84f12469 --- /dev/null +++ b/test/lib/transports/channels.js @@ -0,0 +1,43 @@ +/* eslint-env mocha */ + +'use strict' + +const assert = require('assert') + +const WSv2 = require('../../../lib/transports/ws2') + +const API_KEY = 'dummy' +const API_SECRET = 'dummy' + +const createTestWSv2Instance = (params = {}) => { + return new WSv2({ + apiKey: API_KEY, + apiSecret: API_SECRET, + url: 'ws://localhost:9997', + ...params + }) +} + +describe('WSv2 channels', () => { + it('numeric and string channel ids work', () => { + const ws = createTestWSv2Instance() + + ws._channelMap = { + 83297: { + event: 'subscribed', + channel: 'book', + chanId: 83297, + symbol: 'tADAUSD', + prec: 'P0', + freq: 'F0', + len: '25', + pair: 'ADAUSD' + } + } + + assert.strictEqual(ws.hasChannel(83297), true) + assert.strictEqual(ws.hasChannel('83297'), true) + assert.strictEqual(ws.hasChannel('1337'), false) + assert.strictEqual(ws.hasChannel(1337), false) + }) +}) diff --git a/test/lib/transports/ws2-integration.js b/test/lib/transports/ws2-integration.js new file mode 100644 index 00000000..50533afc --- /dev/null +++ b/test/lib/transports/ws2-integration.js @@ -0,0 +1,316 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const WSv2 = require('../../../lib/transports/ws2') +const { Order } = require('bfx-api-node-models') +const { MockWSv2Server } = require('bfx-api-mock-srv') + +const API_KEY = 'dummy' +const API_SECRET = 'dummy' + +const createTestWSv2Instance = (params = {}) => { + return new WSv2({ + apiKey: API_KEY, + apiSecret: API_SECRET, + url: 'ws://localhost:9997', + + ...params + }) +} + +describe('WSv2 integration', () => { + let ws = null + let wss = null + + afterEach(async () => { + try { // may fail due to being modified by a test, it's not a problem + if (ws && ws.isOpen()) { + await ws.close() + } + } catch (e) { + assert.ok(true) + } + + if (wss && wss.isOpen()) { + await wss.close() + } + + ws = null + wss = null + }) + + describe('orders', () => { + it('creates & confirms orders', async () => { + wss = new MockWSv2Server({ listen: true }) + ws = createTestWSv2Instance() + + await ws.open() + await ws.auth() + + const o = new Order({ + gid: null, + cid: 0, + type: 'EXCHANGE LIMIT', + price: 100, + amount: 1, + symbol: 'tBTCUSD' + }) + + return ws.submitOrder(o) + }) + + it('keeps orders up to date', async () => { + wss = new MockWSv2Server({ listen: true }) + ws = createTestWSv2Instance() + + await ws.open() + await ws.auth() + + const o = new Order({ + gid: null, + cid: 0, + type: 'EXCHANGE LIMIT', + price: 100, + amount: 1, + symbol: 'tBTCUSD' + }, ws) + + o.registerListeners() + + await o.submit() + + const arr = o.serialize() + arr[16] = 256 + + wss.send([0, 'ou', arr]) + + await new Promise(resolve => setTimeout(resolve, 100)) + + assert.strictEqual(o.price, 256) + arr[16] = 150 + + wss.send([0, 'oc', arr]) + + await new Promise(resolve => setTimeout(resolve, 100)) + + assert.strictEqual(o.price, 150) + o.removeListeners() + }) + + it('updateOrder: sends order changeset packet through', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + await ws.auth() + + let sawMessage = false + const o = new Order({ + id: Date.now(), + type: 'EXCHANGE LIMIT', + price: 100, + amount: 1, + symbol: 'tBTCUSD' + }, ws) + + ws._ws.send = (msgJSON) => { + const msg = JSON.parse(msgJSON) + + assert.strictEqual(msg[0], 0) + assert.strictEqual(msg[1], 'ou') + assert(msg[3]) + assert.strictEqual(msg[3].id, o.id) + assert.strictEqual(+msg[3].delta, 1) + assert.strictEqual(+msg[3].price, 200) + + sawMessage = true + } + + o.update({ price: 200, delta: 1 }) // note promise ignored + assert(sawMessage) + }) + + it('sends individual order packets when not buffering', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + await ws.auth() + + let sawBothOrders = false + const oA = new Order({ + gid: null, + cid: Date.now(), + type: 'EXCHANGE LIMIT', + price: 100, + amount: 1, + symbol: 'tBTCUSD' + }) + + const oB = new Order({ + gid: null, + cid: Date.now(), + type: 'EXCHANGE LIMIT', + price: 10, + amount: 1, + symbol: 'tETHUSD' + }) + + let sendN = 0 + + ws._ws.send = (msgJSON) => { + const msg = JSON.parse(msgJSON) + assert.strictEqual(msg[1], 'on') + sendN++ + + if (sendN === 2) { + sawBothOrders = true + } + } + + // note promises ignored + ws.submitOrder(oA) + ws.submitOrder(oB) + + assert(sawBothOrders) + }) + + it('buffers order packets', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance({ + orderOpBufferDelay: 100 + }) + + await ws.open() + await ws.auth() + + const oA = new Order({ + gid: null, + cid: Date.now(), + type: 'EXCHANGE LIMIT', + price: 100, + amount: 1, + symbol: 'tBTCUSD' + }) + + const oB = new Order({ + gid: null, + cid: Date.now(), + type: 'EXCHANGE LIMIT', + price: 10, + amount: 1, + symbol: 'tETHUSD' + }) + + return new Promise((resolve) => { + ws._ws.send = (msgJSON) => { + const msg = JSON.parse(msgJSON) + assert.strictEqual(msg[1], 'ox_multi') + + msg[3].forEach((payload) => { + assert.strictEqual(payload[0], 'on') + }) + + wss.close() + resolve() + } + + // note promises ignored + ws.submitOrder(oA) + ws.submitOrder(oB) + }) + }) + }) + + describe('listeners', () => { + it('manages listeners by cbGID', () => { + ws = createTestWSv2Instance() + ws._channelMap = { 0: { channel: 'auth' } } + + let updatesSeen = 0 + ws.onAccountTradeUpdate({ pair: 'BTCUSD', cbGID: 10 }, () => updatesSeen++) + ws.onOrderUpdate({ symbol: 'tBTCUSD', cbGID: 10 }, () => updatesSeen++) + + ws._handleChannelMessage([0, 'tu', [123, 'tBTCUSD']]) + ws._handleChannelMessage([0, 'ou', [0, 0, 0, 'tBTCUSD']]) + ws.removeListeners(10) + ws._handleChannelMessage([0, 'tu', [123, 'tBTCUSD']]) + ws._handleChannelMessage([0, 'ou', [0, 0, 0, 'tBTCUSD']]) + + assert.strictEqual(updatesSeen, 2) + }) + + it('tracks channel refs to auto sub/unsub', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + let subs = 0 + let unsubs = 0 + + await ws.open() + + wss.on('message', (ws, msg) => { + if (msg.event === 'subscribe' && msg.channel === 'trades') { + subs++ + ws.send(JSON.stringify({ + event: 'subscribed', + chanId: 42, + channel: 'trades', + symbol: msg.symbol + })) + } else if (msg.event === 'unsubscribe' && msg.chanId === 42) { + unsubs++ + ws.send(JSON.stringify({ + event: 'unsubscribed', + chanId: 42 + })) + } + }) + + ws.subscribeTrades('tBTCUSD') + ws.subscribeTrades('tBTCUSD') + ws.subscribeTrades('tBTCUSD') + + ws.on('subscribed', () => { + ws.unsubscribeTrades('tBTCUSD') + ws.unsubscribeTrades('tBTCUSD') + ws.unsubscribeTrades('tBTCUSD') + ws.unsubscribeTrades('tBTCUSD') + ws.unsubscribeTrades('tBTCUSD') + }) + + return new Promise((resolve) => { + ws.on('unsubscribed', () => { + assert.strictEqual(subs, 1) + assert.strictEqual(unsubs, 1) + resolve() + }) + }) + }) + }) + + describe('info message handling', () => { + it('notifies listeners on matching code', () => { + let sawMaintenanceEnd = false + ws = new WSv2() + + ws.onInfoMessage(WSv2.info.MAINTENANCE_END, () => { + sawMaintenanceEnd = true + }) + + ws._onWSMessage(JSON.stringify({ + event: 'info', + code: WSv2.info.MAINTENANCE_START, + msg: '' + })) + + ws._onWSMessage(JSON.stringify({ + event: 'info', + code: WSv2.info.MAINTENANCE_END, + msg: '' + })) + + assert(sawMaintenanceEnd) + }) + }) +}) diff --git a/test/lib/transports/ws2-unit.js b/test/lib/transports/ws2-unit.js new file mode 100644 index 00000000..5dc64805 --- /dev/null +++ b/test/lib/transports/ws2-unit.js @@ -0,0 +1,2320 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const { SocksProxyAgent } = require('socks-proxy-agent') +const { MockWSv2Server } = require('bfx-api-mock-srv') +const _isFunction = require('lodash/isFunction') +const _isObject = require('lodash/isObject') +const _isString = require('lodash/isString') +const _isEmpty = require('lodash/isEmpty') +const _isError = require('lodash/isError') +const _includes = require('lodash/includes') +const { + Position, FundingOffer, FundingCredit, FundingLoan, Wallet, BalanceInfo, + MarginInfo, FundingInfo, FundingTrade, Notification, Candle, PublicTrade, + Trade, TradingTicker, FundingTicker +} = require('bfx-api-node-models') + +const WSv2 = require('../../../lib/transports/ws2') + +const API_KEY = 'dummy' +const API_SECRET = 'dummy' + +const createTestWSv2Instance = (params = {}) => { + return new WSv2({ + apiKey: API_KEY, + apiSecret: API_SECRET, + url: 'ws://localhost:9997', + ...params + }) +} + +describe('WSv2 unit', () => { + let ws = null + let wss = null + + afterEach(async () => { + try { // may fail due to being modified by a test, it's not a problem + if (ws && ws.isOpen()) { + await ws.close() + } + } catch (e) { + assert(true) + } + + if (wss && wss.isOpen()) { + await wss.close() + } + + ws = null // eslint-disable-line + wss = null // eslint-disable-line + }) + + describe('event subscribers', () => { + const testSub = (name, eventName, filterIndex, filterKey, filterValue, model) => { + describe(name, () => { + it(`listens for ${eventName} event, passes valid filter, and uses correct model`, (done) => { + ws = createTestWSv2Instance() + ws._registerListener = (passedEventName, filter, passedModel) => { + assert.strictEqual(passedEventName, eventName, 'incorrect event name') + + if (filterIndex !== null) { + assert.deepStrictEqual(filter, { [filterIndex]: filterValue }, 'incorrect filter') + } + + if (model !== null) { + assert.strictEqual(model, passedModel) + } + + done() + } + + ws[name]({ [filterKey]: filterValue }) + }) + }) + } + + testSub('onCandle', 'candle', 0, 'key', 'test-key', Candle) + testSub('onTrades', 'trades', 0, 'symbol', 'tBTCUSD', PublicTrade) + testSub('onTrades', 'trades', 0, 'symbol', 'fUSD', FundingTrade) + testSub('onTradeEntry', 'trade-entry', 0, 'symbol', 'tBTCUSD', PublicTrade) + testSub('onAccountTradeEntry', 'auth-te', 1, 'symbol', 'tBTCUSD', Trade) + testSub('onAccountTradeUpdate', 'auth-tu', 1, 'symbol', 'tBTCUSD', Trade) + testSub('onTicker', 'ticker', 0, 'symbol', 'tBTCUSD', TradingTicker) + testSub('onTicker', 'ticker', 0, 'symbol', 'fUSD', FundingTicker) + testSub('onStatus', 'status', 0, 'key', 'test-key', null) + testSub('onPositionSnapshot', 'ps', 0, 'symbol', 'tBTCUSD', Position) + testSub('onPositionNew', 'pn', 0, 'symbol', 'tBTCUSD', Position) + testSub('onPositionUpdate', 'pu', 0, 'symbol', 'tBTCUSD', Position) + testSub('onPositionClose', 'pc', 0, 'symbol', 'tBTCUSD', Position) + testSub('onFundingOfferSnapshot', 'fos', 1, 'symbol', 'tBTCUSD', FundingOffer) + testSub('onFundingOfferNew', 'fon', 1, 'symbol', 'tBTCUSD', FundingOffer) + testSub('onFundingOfferUpdate', 'fou', 1, 'symbol', 'tBTCUSD', FundingOffer) + testSub('onFundingOfferClose', 'foc', 1, 'symbol', 'tBTCUSD', FundingOffer) + testSub('onFundingCreditSnapshot', 'fcs', 1, 'symbol', 'tBTCUSD', FundingCredit) + testSub('onFundingCreditNew', 'fcn', 1, 'symbol', 'tBTCUSD', FundingCredit) + testSub('onFundingCreditUpdate', 'fcu', 1, 'symbol', 'tBTCUSD', FundingCredit) + testSub('onFundingCreditClose', 'fcc', 1, 'symbol', 'tBTCUSD', FundingCredit) + testSub('onFundingLoanSnapshot', 'fls', 1, 'symbol', 'tBTCUSD', FundingLoan) + testSub('onFundingLoanNew', 'fln', 1, 'symbol', 'tBTCUSD', FundingLoan) + testSub('onFundingLoanUpdate', 'flu', 1, 'symbol', 'tBTCUSD', FundingLoan) + testSub('onFundingLoanClose', 'flc', null, null, null, FundingLoan) + testSub('onWalletSnapshot', 'ws', null, null, null, Wallet) + testSub('onWalletUpdate', 'wu', null, null, null, Wallet) + testSub('onBalanceInfoUpdate', 'bu', null, null, null, BalanceInfo) + testSub('onMarginInfoUpdate', 'miu', null, null, null, MarginInfo) + testSub('onFundingInfoUpdate', 'fiu', null, null, null, FundingInfo) + testSub('onFundingTradeEntry', 'fte', 0, 'symbol', 'tBTCUSD', FundingTrade) + testSub('onFundingTradeUpdate', 'ftu', 0, 'symbol', 'tBTCUSD', FundingTrade) + testSub('onNotification', 'n', 1, 'type', 'oc-req', Notification) + }) + + describe('utilities', () => { + it('sendEnabledFlags: sends the current flags value to the server', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + + await ws.open() + + ws._enabledFlags = WSv2.flags.CHECKSUM // eslint-disable-line + + return new Promise((resolve) => { + ws.send = (packet) => { + assert.strictEqual(packet.event, 'conf') + assert.strictEqual(packet.flags, WSv2.flags.CHECKSUM) + resolve() + } + + ws.sendEnabledFlags() + }) + }) + + it('enableFlag: saves enabled flag status', () => { + ws = createTestWSv2Instance() + + assert(!ws.isFlagEnabled(WSv2.flags.SEQ_ALL)) + assert(!ws.isFlagEnabled(WSv2.flags.CHECKSUM)) + + ws.enableFlag(WSv2.flags.SEQ_ALL) + + assert(ws.isFlagEnabled(WSv2.flags.SEQ_ALL)) + assert(!ws.isFlagEnabled(WSv2.flags.CHECKSUM)) + + ws.enableFlag(WSv2.flags.CHECKSUM) + + assert(ws.isFlagEnabled(WSv2.flags.SEQ_ALL)) + assert(ws.isFlagEnabled(WSv2.flags.CHECKSUM)) + }) + + it('enableFlag: sends conf packet if open', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + + await ws.open() + + return new Promise((resolve) => { + ws.send = (packet) => { + assert(_isObject(packet)) + assert.strictEqual(packet.event, 'conf') + resolve() + } + + ws.enableFlag(WSv2.flags.SEQ_ALL) + }) + }) + + it('_registerListener: correctly adds listener to internal map with cbGID', () => { + ws = createTestWSv2Instance() + ws._registerListener('trade', { 2: 'tBTCUSD' }, Map, 42, () => { }) + + const { _listeners } = ws + + assert.strictEqual(Object.keys(_listeners).length, 1) + assert.strictEqual(+Object.keys(_listeners)[0], 42) + assert.strictEqual(typeof _listeners[42], 'object') + + const listenerSet = _listeners[42] + + assert.strictEqual(Object.keys(listenerSet).length, 1) + assert.strictEqual(Object.keys(listenerSet)[0], 'trade') + assert.strictEqual(listenerSet.trade.constructor.name, 'Array') + assert.strictEqual(listenerSet.trade.length, 1) + + const listener = listenerSet.trade[0] + + assert.strictEqual(listener.modelClass, Map) + assert.deepStrictEqual(listener.filter, { 2: 'tBTCUSD' }) + assert.strictEqual(typeof listener.cb, 'function') + }) + + it('sequencingEnabled: returns sequencing status', () => { + ws = createTestWSv2Instance({ seqAudit: false }) + + assert.ok(_isFunction(ws.sequencingEnabled), 'WSv2 does not provide sequencingEnabled()') + assert.ok(!ws.sequencingEnabled(), 'sequencing enabled even though disabled in constructor') + ws.enableSequencing() + assert.ok(ws.sequencingEnabled(), 'sequencing status not reported by getter') + }) + + it('enableSequencing: sends the correct conf flag', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + let packetSent = false + + await ws.open() + + ws.send = (packet) => { + assert.strictEqual(packet.event, 'conf') + assert.strictEqual(packet.flags, 65536) + packetSent = true + } + + ws.enableSequencing() + + assert(packetSent) + }) + + it('getCandles: returns empty array if no candle set is available', () => { + ws = createTestWSv2Instance() + assert.deepStrictEqual(ws.getCandles('i.dont.exist'), []) + }) + + it('_sendCalc: stringifes payload & passes it to the ws client', (done) => { + ws = createTestWSv2Instance() + + ws._ws = {} + ws._ws.send = (data) => { + assert.strictEqual(data, '[]') + done() + } + + ws._sendCalc([]) + }) + + it('notifyUI: throws error if supplied invalid arguments', () => { + ws = createTestWSv2Instance() + + assert.throws(() => ws.notifyUI()) + assert.throws(() => ws.notifyUI(null)) + assert.throws(() => ws.notifyUI(null, null)) + }) + + it('notifyUI: throws error if socket closed or not authenticated', () => { + ws = createTestWSv2Instance() + const n = { type: 'info', message: 'test' } + + assert.throws(() => ws.notifyUI(n)) + ws._isOpen = true + assert.throws(() => ws.notifyUI(n)) + ws._isAuthenticated = true + ws.send = () => { } + assert.doesNotThrow(() => ws.notifyUI(n)) + }) + + it('notifyUI: sends the correct UCM broadcast notification', (done) => { + ws = createTestWSv2Instance() + ws._isOpen = true + ws._isAuthenticated = true + ws.send = (msg = []) => { + assert.deepStrictEqual(msg[0], 0) + assert.deepStrictEqual(msg[1], 'n') + assert.deepStrictEqual(msg[2], null) + + const data = msg[3] + + assert(_isObject(data)) + assert.deepStrictEqual(data.type, 'ucm-notify-ui') + assert(_isObject(data.info)) + assert.deepStrictEqual(data.info.type, 'success') + assert.deepStrictEqual(data.info.message, '42') + done() + } + + ws.notifyUI({ type: 'success', message: '42' }) + }) + }) + + describe('lifetime', () => { + it('starts unopened & unauthenticated', () => { + ws = createTestWSv2Instance() + + assert.strictEqual(ws.isOpen(), false) + assert.strictEqual(ws.isAuthenticated(), false) + }) + + it('open: fails to open twice', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + try { + await ws.open() + assert(false) + } catch (e) { + assert.ok(true, 'failed to open twice') + } + }) + + it('open: updates open flag', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + assert.strictEqual(ws.isOpen(), true) + }) + + it('open: sends flags value', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + let flagsSent = false + + ws.enableSequencing() + ws.sendEnabledFlags = () => { + assert.strictEqual(ws._enabledFlags, WSv2.flags.SEQ_ALL) + flagsSent = true + } + + await ws.open() + + assert(flagsSent) + }) + + it('close: doesn\'t close if not open', async () => { + ws = createTestWSv2Instance() + + try { + await ws.close() + assert(false) + } catch (e) { + assert.ok(true, 'did not close due to being open') + } + }) + + it('close: fails to close twice', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + return new Promise((resolve) => { + ws.on('close', async () => { + try { + await ws.close() + assert(false) + } catch (e) { + resolve() + } + }) + + return ws.close() + }) + }) + + it('close: clears connection state', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + ws._onWSClose = () => { } // disable fallback reset + + await ws.open() + + assert(ws._ws !== null) + assert(ws._isOpen) + + await ws.close() + + assert(ws._ws == null) + assert(!ws._isOpen) + }) + + it('auth: fails to auth twice', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + await ws.auth() + + try { + await ws.auth() + assert(false) + } catch (e) { + assert.ok(true, 'failed to auth twice') + } + }) + + it('auth: updates auth flag', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + await ws.auth() + + assert(ws.isAuthenticated()) + }) + + it('auth: forwards calc param', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + let sentCalc = false + + await ws.open() + + const send = ws.send + ws.send = (data) => { + assert.strictEqual(data.calc, 42) + sentCalc = true + + ws.send = send + ws.send(data) + } + + await ws.auth(42) + + assert(sentCalc) + }) + + it('auth: forwards dms param', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + let sentDMS = false + + await ws.open() + + const send = ws.send + ws.send = (data) => { + assert.strictEqual(data.dms, 42) + sentDMS = true + + ws.send = send + ws.send(data) + } + + await ws.auth(0, 42) + + assert(sentDMS) + }) + + it('reconnect: connects if not already connected', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + let sawClose = false + let sawOpen = false + + ws.on('close', () => { sawClose = true }) + ws.on('open', () => { sawOpen = true }) + + await ws.reconnect() + + assert(!sawClose) + assert(sawOpen) + }) + + it('reconnect: disconnects & connects back if currently connected', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + let sawClose = false + let sawOpen = false + + ws.on('close', () => { sawClose = true }) + ws.on('open', () => { sawOpen = true }) + + await ws.reconnect() + + assert(sawClose) + assert(sawOpen) + assert(ws.isOpen()) + }) + + it('reconnect: automatically auths on open if previously authenticated', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + let closed = false + let opened = false + let authenticated = false + + ws.on('error', (error) => { + throw error + }) + + await ws.open() + await ws.auth() + + ws.once('close', () => { closed = true }) + ws.once('open', () => { opened = true }) + ws.once('auth', () => { authenticated = true }) + + await ws.reconnect() + + assert(closed) + assert(opened) + assert(authenticated) + }) + }) + + describe('constructor', () => { + it('defaults to production WS url', () => { + ws = createTestWSv2Instance({ url: undefined }) + assert.notStrictEqual(ws._url.indexOf('api.bitfinex.com'), -1) + }) + + it('defaults to no transform', () => { + ws = createTestWSv2Instance() + const transWS = createTestWSv2Instance({ transform: true }) + assert.strictEqual(ws._transform, false) + assert.strictEqual(transWS._transform, true) + }) + }) + + describe('auto reconnect', () => { + it('reconnects on close if autoReconnect is enabled', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance({ autoReconnect: true }) + + await ws.open() + await ws.auth() + + return new Promise((resolve) => { + ws.reconnectAfterClose = Promise.resolve(resolve()) + wss.close() // trigger reconnect + }) + }) + + it('respects reconnectDelay', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance({ + autoReconnect: true, + reconnectDelay: 75 + }) + + await ws.open() + await ws.auth() + + return new Promise((resolve) => { + const now = Date.now() + + ws.reconnectAfterClose = () => { + assert((Date.now() - now) >= 70) + resolve() + + return Promise.resolve() + } + + wss.close() // trigger reconnect + }) + }) + + it('does not auto-reconnect if explicity closed', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance({ + autoReconnect: true + }) + + await ws.open() + await ws.auth() + + ws.reconnect = async () => assert(false) + await ws.close() + + await new Promise(resolve => setTimeout(resolve, 50)) + }) + }) + + it('reconnect with new credentials', async () => { + wss = new MockWSv2Server({ authMiddleware: ({ apiKey, apiSecret }) => apiKey === API_KEY && apiSecret === API_SECRET }) + ws = createTestWSv2Instance({ reconnectDelay: 10 }) + + await ws.open() + await ws.auth() + assert(ws.isAuthenticated()) + + ws.updateAuthArgs({ apiKey: 'wrong', apiSecret: 'wrong' }) + ws.reconnect() + await new Promise(resolve => setTimeout(resolve, 50)) + assert(!ws.isAuthenticated()) + + ws.updateAuthArgs({ apiKey: API_KEY, apiSecret: API_SECRET }) + ws.reconnect() + await new Promise(resolve => setTimeout(resolve, 50)) + assert(ws.isAuthenticated()) + }) + + describe('seq audit', () => { + it('automatically enables sequencing if seqAudit is true in constructor', () => { + ws = createTestWSv2Instance({ seqAudit: true }) + assert(ws.isFlagEnabled(WSv2.flags.SEQ_ALL)) + }) + + it('emits error on invalid seq number', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance({ seqAudit: true }) + + let errorsSeen = 0 + + await ws.open() + await ws.auth() + + ws.on('error', (err) => { + if (_includes(err.message, 'seq #')) errorsSeen++ + + return null + }) + + ws._channelMap[42] = { channel: 'trades', chanId: 42 } // eslint-disable-line + + ws._onWSMessage(JSON.stringify([0, 'tu', [], 0, 0])) + ws._onWSMessage(JSON.stringify([0, 'te', [], 1, 0])) + ws._onWSMessage(JSON.stringify([0, 'wu', [], 2, 1])) + ws._onWSMessage(JSON.stringify([0, 'tu', [], 3, 2])) // + ws._onWSMessage(JSON.stringify([0, 'tu', [], 4, 4])) // error + ws._onWSMessage(JSON.stringify([0, 'tu', [], 5, 5])) + ws._onWSMessage(JSON.stringify([0, 'tu', [], 6, 6])) + ws._onWSMessage(JSON.stringify([42, [], 7])) + ws._onWSMessage(JSON.stringify([42, [], 8])) + ws._onWSMessage(JSON.stringify([42, [], 9])) // + ws._onWSMessage(JSON.stringify([42, [], 13])) // error + ws._onWSMessage(JSON.stringify([42, [], 14])) + ws._onWSMessage(JSON.stringify([42, [], 15])) + + assert.strictEqual(errorsSeen, 6) + }) + }) + + describe('ws event handlers', () => { + it('_onWSOpen: updates open flag', () => { + ws = createTestWSv2Instance() + assert(!ws.isOpen()) + ws._onWSOpen() + assert(ws.isOpen()) + }) + + it('_onWSClose: updates open flag', () => { + ws = createTestWSv2Instance() + ws._onWSOpen() + assert(ws.isOpen()) + ws._onWSClose() + assert(!ws.isOpen()) + }) + + it('_onWSError: emits error', (done) => { + ws = createTestWSv2Instance() + ws.on('error', () => done()) + ws._onWSError(new Error()) + }) + + it('_onWSMessage: emits error on invalid packet', (done) => { + ws = createTestWSv2Instance() + ws.on('error', () => done()) + ws._onWSMessage('I can\'t believe it\'s not JSON!') + }) + + it('_onWSMessage: emits message', () => { + ws = createTestWSv2Instance() + const msg = [1] + const flags = 'flags' + let messageSeen = false + + ws.on('message', (m) => { + assert.deepStrictEqual(m, msg) + assert.strictEqual(flags, 'flags') + messageSeen = true + }) + + ws._onWSMessage(JSON.stringify(msg), flags) + assert(messageSeen) + }) + + it('_onWSNotification: triggers event callbacks for new orders', (done) => { + ws = createTestWSv2Instance() + const kNew = 'order-new-42' + + ws._eventCallbacks.push(kNew, (err, order) => { + assert(!err) + assert(order) + assert.deepStrictEqual(order, [0, 0, 42]) + + ws._eventCallbacks.push(kNew, (err, order) => { + assert(err) + assert.deepStrictEqual(order, [0, 0, 42]) + done() + }) + + ws._onWSNotification([0, 'on-req', null, null, [0, 0, 42], 0, 'ERROR']) + }) + + ws._onWSNotification([0, 'on-req', null, null, [0, 0, 42], 0, 'SUCCESS']) + }) + + it('_onWSNotification: triggers event callbacks for cancelled orders', (done) => { + ws = createTestWSv2Instance() + const kCancel = 'order-cancel-42' + + ws._eventCallbacks.push(kCancel, (err, order) => { + assert(!err) + assert(order) + assert.deepStrictEqual(order, [42]) + + ws._eventCallbacks.push(kCancel, (err, order) => { + assert(err) + assert.deepStrictEqual(order, [42]) + done() + }) + + ws._onWSNotification([0, 'oc-req', null, null, [42], 0, 'ERROR']) + }) + + ws._onWSNotification([0, 'oc-req', null, null, [42], 0, 'SUCCESS']) + }) + }) + + describe('WSv2 channel msg handling', () => { + it('_handleChannelMessage: emits message', () => { + ws = createTestWSv2Instance() + + const packet = [42, 'tu', []] + let packetSeen = false + + ws._channelMap = { + 42: { channel: 'meaning' } + } + + ws.on('meaning', (msg) => { + assert.deepStrictEqual(msg, packet) + packetSeen = true + }) + + ws._handleChannelMessage(packet) + assert(packetSeen) + }) + + it('_handleChannelMessage: calls all registered listeners (nofilter)', (done) => { + ws = createTestWSv2Instance() + ws._channelMap = { 0: { channel: 'auth' } } + let called = 0 + ws.onWalletUpdate({}, () => { + if (++called === 2) done() + }) + + ws.onWalletUpdate({}, () => { + if (++called === 2) done() + }) + + ws._handleChannelMessage([0, 'wu', []]) + }) + + const doFilterTest = (transform, done) => { + ws = new WSv2({ transform }) + ws._channelMap = { 0: { channel: 'auth' } } + let calls = 0 + let btcListenerCalled = false + + ws.onAccountTradeEntry({ symbol: 'tBTCUSD' }, () => { + assert(!btcListenerCalled) + btcListenerCalled = true + + if (++calls === 7) done() + }) + + ws.onAccountTradeEntry({}, () => { + if (++calls === 7) done() + }) + + ws.onAccountTradeEntry({}, () => { + if (++calls === 7) done() + }) + + ws._handleChannelMessage([0, 'te', [123, 'tETHUSD']]) + ws._handleChannelMessage([0, 'te', [123, 'tETHUSD']]) + ws._handleChannelMessage([0, 'te', [123, 'tBTCUSD']]) + } + + it('_handleChannelMessage: filters messages if listeners require it (transform)', (done) => { + doFilterTest(true, done) + }) + + it('_handleChannelMessage: filters messages if listeners require it (no transform)', (done) => { + doFilterTest(false, done) + }) + + it('_handleChannelMessage: transforms payloads if enabled', (done) => { + let calls = 0 + + const wsTransform = new WSv2({ transform: true }) + const wsNoTransform = new WSv2({ transform: false }) + wsTransform._channelMap = { 0: { channel: 'auth' } } + wsNoTransform._channelMap = { 0: { channel: 'auth' } } + + const tradeData = [ + 0, 'tBTCUSD', Date.now(), 0, 0.1, 1, 'type', 1, false, 0.001, 'USD' + ] + + wsNoTransform.onAccountTradeUpdate({}, (trade) => { + assert.strictEqual(trade.constructor.name, 'Array') + assert.deepStrictEqual(trade, tradeData) + + if (calls++ === 1) done() + }) + + wsTransform.onAccountTradeUpdate({}, (trade) => { + assert.strictEqual(trade.constructor.name, 'Trade') + assert.strictEqual(trade.id, tradeData[0]) + assert.strictEqual(trade.symbol, tradeData[1]) + assert.strictEqual(trade.mtsCreate, tradeData[2]) + assert.strictEqual(trade.orderID, tradeData[3]) + assert.strictEqual(trade.execAmount, tradeData[4]) + assert.strictEqual(trade.execPrice, tradeData[5]) + assert.strictEqual(trade.orderType, tradeData[6]) + assert.strictEqual(trade.orderPrice, tradeData[7]) + assert.strictEqual(trade.maker, tradeData[8]) + assert.strictEqual(trade.fee, tradeData[9]) + assert.strictEqual(trade.feeCurrency, tradeData[10]) + + if (calls++ === 1) done() + }) + + wsTransform._handleChannelMessage([0, 'tu', tradeData]) + wsNoTransform._handleChannelMessage([0, 'tu', tradeData]) + }) + + it('onMessage: calls the listener with all messages (no filter)', (done) => { + ws = createTestWSv2Instance() + ws._channelMap = { 0: { channel: 'auth' } } + + let calls = 0 + + ws.onMessage({}, () => { + if (++calls === 2) done() + }) + + ws._handleChannelMessage([0, 'wu', []]) + ws._handleChannelMessage([0, 'tu', []]) + }) + + it('_payloadPassesFilter: correctly detects matching payloads', () => { + const filter = { + 1: 'tBTCUSD' + } + + const goodPayloads = [ + [0, 'tBTCUSD', 42, ''], + [0, 'tBTCUSD', 3.14, ''] + ] + + const badPayloads = [ + [0, 'tETHUSD', 42, ''], + [0, 'tETHUSD', 3.14, ''] + ] + + goodPayloads.forEach(p => assert(WSv2._payloadPassesFilter(p, filter))) + badPayloads.forEach(p => assert(!WSv2._payloadPassesFilter(p, filter))) + }) + + it('_payloadPassesFilter: ignores filter if empty', () => { + const filterUndefined = { 1: undefined } + const filterNull = { 1: null } + const filterEmpty = { 1: '' } + const payload = [0, 'tBTCUSD', 42, ''] + + assert(WSv2._payloadPassesFilter(payload, filterUndefined)) + assert(WSv2._payloadPassesFilter(payload, filterNull)) + assert(WSv2._payloadPassesFilter(payload, filterEmpty)) + }) + + it('_payloadPassesFilter: ignores filter if *', () => { + const filter = { 1: '*' } + const payload = [0, 'tBTCUSD', 42, ''] + + assert(WSv2._payloadPassesFilter(payload, filter)) + }) + + it('_notifyListenerGroup: notifies all matching listeners in the group', (done) => { + let calls = 0 + const func = () => { + assert(calls < 3) + if (++calls === 2) done() + } + + const lg = { + '': [{ cb: func }], + test: [{ cb: func }], + nope: [{ cb: func }] + } + + WSv2._notifyListenerGroup(lg, [0, 'test', [0, 'tu']], false) + }) + + it('_notifyListenerGroup: doesn\'t fail on missing data if filtering', (done) => { + const lg = { + test: [{ + filter: { 1: 'on' }, + cb: () => { + done(new Error('filter should not have matched')) + } + }] + } + + WSv2._notifyListenerGroup(lg, [0, 'test'], false) + done() + }) + + it('_propagateMessageToListeners: notifies all matching listeners', () => { + const ws = createTestWSv2Instance() + let seenMessage = false + ws._channelMap = { 0: { channel: 'auth' } } + + ws.onAccountTradeEntry({ symbol: 'tBTCUSD' }, () => { + seenMessage = true + }) + + ws._propagateMessageToListeners([0, 'auth-te', [123, 'tBTCUSD']]) + assert(seenMessage) + }) + + it('_notifyCatchAllListeners: passes data to all listeners on the empty \'\' event', () => { + let s = 0 + + const lg = { + '': [ + { cb: d => { s += d } }, + { cb: d => { s += (d * 2) } } + ] + } + + WSv2._notifyCatchAllListeners(lg, 5) + assert.strictEqual(s, 15) + }) + + it('_handleOBMessage: maintains internal OB if management is enabled', () => { + ws = new WSv2({ + manageOrderBooks: true, + transform: true + }) + + ws._channelMapA = { + 42: { + channel: 'orderbook', + symbol: 'tBTCUSD' + } + } + ws._channelMapB = { + 43: { + channel: 'orderbook', + symbol: 'fUSD' + } + } + + let obMsg = [42, [ + [100, 2, -4], + [200, 4, -8], + [300, 1, 3] + ]] + + ws._handleOBMessage(obMsg, ws._channelMapA[42], JSON.stringify(obMsg)) + + obMsg = [43, [ + [0.0008, 2, 5, 200], + [0.00045, 30, 4, -300], + [0.0004, 15, 3, -600] + ]] + + ws._handleOBMessage(obMsg, ws._channelMapB[43], JSON.stringify(obMsg)) + + let obA = ws.getOB('tBTCUSD') + let obB = ws.getOB('fUSD') + + assert(obA !== null) + assert(obB !== null) + + assert.strictEqual(obA.bids.length, 1) + assert.strictEqual(obB.bids.length, 2) + assert.deepStrictEqual(obA.bids, [[300, 1, 3]]) + assert.deepStrictEqual(obB.bids, [[0.00045, 30, 4, -300], [0.0004, 15, 3, -600]]) + assert.strictEqual(obA.asks.length, 2) + assert.strictEqual(obB.asks.length, 1) + assert.deepStrictEqual(obA.getEntry(100), { price: 100, count: 2, amount: -4 }) + assert.deepStrictEqual(obA.getEntry(200), { price: 200, count: 4, amount: -8 }) + assert.deepStrictEqual(obB.getEntry(0.00045), { rate: 0.00045, count: 4, amount: -300, period: 30 }) + assert.deepStrictEqual(obB.getEntry(0.0008), { rate: 0.0008, count: 5, amount: 200, period: 2 }) + + obMsg = [42, [300, 0, 1]] + ws._handleOBMessage(obMsg, ws._channelMapA[42], JSON.stringify(obMsg)) + obA = ws.getOB('tBTCUSD') + assert.strictEqual(obA.bids.length, 0) + obMsg = [43, [0.0008, 2, 0, 1]] + ws._handleOBMessage(obMsg, ws._channelMapB[43], JSON.stringify(obMsg)) + obB = ws.getOB('fUSD') + assert.strictEqual(obB.asks.length, 0) + }) + + it('_handleOBMessage: emits error on internal OB update failure', (done) => { + const wsNoTransform = new WSv2({ manageOrderBooks: true }) + const wsTransform = new WSv2({ + manageOrderBooks: true, + transform: true + }) + + wsNoTransform._channelMap = { + 42: { + channel: 'orderbook', + symbol: 'tBTCUSD' + } + } + + wsTransform._channelMap = wsNoTransform._channelMap + + let errorsSeen = 0 + + wsNoTransform.on('error', () => { + if (++errorsSeen === 2) done() + }) + + wsTransform.on('error', () => { + if (++errorsSeen === 2) done() + }) + + const obMsg = [42, [100, 0, 1]] + wsTransform._handleOBMessage(obMsg, wsTransform._channelMap[42], JSON.stringify(obMsg)) + wsNoTransform._handleOBMessage(obMsg, wsNoTransform._channelMap[42], JSON.stringify(obMsg)) + }) + + it('_handleOBMessage: forwards managed ob to listeners', (done) => { + ws = new WSv2({ manageOrderBooks: true }) + ws._channelMap = { + 42: { + channel: 'orderbook', + symbol: 'tBTCUSD' + } + } + + let seen = 0 + ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => { + assert.deepStrictEqual(ob, [[100, 2, 3]]) + if (++seen === 2) done() + }) + + ws.onOrderBook({}, (ob) => { + assert.deepStrictEqual(ob, [[100, 2, 3]]) + if (++seen === 2) done() + }) + + const obMsg = [42, [[100, 2, 3]]] + ws._handleOBMessage(obMsg, ws._channelMap[42], JSON.stringify(obMsg)) + }) + + it('_handleOBMessage: filters by prec and len', (done) => { + ws = new WSv2({ manageOrderBooks: true }) + ws._channelMap = { + 40: { + channel: 'orderbook', + symbol: 'tBTCUSD', + prec: 'P0' + }, + + 41: { + channel: 'orderbook', + symbol: 'tBTCUSD', + prec: 'P1' + }, + + 42: { + channel: 'orderbook', + symbol: 'tBTCUSD', + prec: 'P2' + } + } + + let seen = 0 + ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P0' }, () => { + assert(false) + }) + + ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P1' }, () => { + assert(false) + }) + + ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P2' }, () => { + if (++seen === 2) done() + }) + + const obMsg = [42, [[100, 2, 3]]] + ws._handleOBMessage(obMsg, ws._channelMap[42], JSON.stringify(obMsg)) + ws._handleOBMessage(obMsg, ws._channelMap[42], JSON.stringify(obMsg)) + }) + + it('_handleOBMessage: emits managed ob', (done) => { + ws = new WSv2({ manageOrderBooks: true }) + ws._channelMap = { + 42: { + channel: 'orderbook', + symbol: 'tBTCUSD' + } + } + + ws.on('orderbook', (symbol, data) => { + assert.strictEqual(symbol, 'tBTCUSD') + assert.deepStrictEqual(data, [[100, 2, 3]]) + done() + }) + + const obMsg = [42, [[100, 2, 3]]] + ws._handleOBMessage(obMsg, ws._channelMap[42], JSON.stringify(obMsg)) + }) + + it('_handleOBMessage: forwards transformed data if transform enabled', (done) => { + ws = new WSv2({ transform: true }) + ws._channelMap = { + 42: { + chanId: 42, + channel: 'orderbook', + symbol: 'tBTCUSD' + } + } + + ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => { + assert(ob.asks) + assert(ob.bids) + assert.strictEqual(ob.asks.length, 0) + assert.deepStrictEqual(ob.bids, [[100, 2, 3]]) + done() + }) + + const obMsg = [42, [[100, 2, 3]]] + ws._handleOBMessage(obMsg, ws._channelMap[42], obMsg) + }) + + it('_updateManagedOB: does nothing on rm non-existent entry', () => { + ws = createTestWSv2Instance() + ws._orderBooks.tBTCUSD = [ + [100, 1, 1], + [200, 2, 1] + ] + ws._losslessOrderBooks.tBTCUSD = [ + ['100', '1', '1'], + ['200', '2', '1'] + ] + + const err = ws._updateManagedOB('tBTCUSD', [150, 0, -1], false, JSON.stringify([1, [150, 0, -1]])) + assert.strictEqual(err, null) + assert.deepStrictEqual(ws._orderBooks.tBTCUSD, [ + [100, 1, 1], + [200, 2, 1] + ]) + }) + + it('_updateManagedOB: correctly maintains transformed OBs', () => { + ws = new WSv2({ transform: true }) + ws._orderBooks.tBTCUSD = [] + ws._losslessOrderBooks.tBTCUSD = [] + + // symbol, data, raw, rawMsg + assert(!ws._updateManagedOB('tBTCUSD', [100, 1, 1], false, JSON.stringify([1, [100, 1, 1]]))) + assert(!ws._updateManagedOB('tBTCUSD', [200, 1, -1], false, JSON.stringify([1, [200, 1, -1]]))) + assert(!ws._updateManagedOB('tBTCUSD', [200, 0, -1], false, JSON.stringify([1, [200, 0, -1]]))) + + const ob = ws.getOB('tBTCUSD') + + assert.strictEqual(ob.bids.length, 1) + assert.strictEqual(ob.asks.length, 0) + assert.deepStrictEqual(ob.bids, [[100, 1, 1]]) + }) + + it('_updateManagedOB: correctly maintains non-transformed OBs', () => { + ws = createTestWSv2Instance() + ws._orderBooks.tBTCUSD = [] + ws._losslessOrderBooks.tBTCUSD = [] + + assert(!ws._updateManagedOB('tBTCUSD', [100, 1, 1], false, JSON.stringify([1, [100, 1, 1]]))) + assert(!ws._updateManagedOB('tBTCUSD', [200, 1, -1], false, JSON.stringify([1, [200, 1, -1]]))) + assert(!ws._updateManagedOB('tBTCUSD', [200, 0, -1], false, JSON.stringify([1, [200, 0, -1]]))) + + const ob = ws._orderBooks.tBTCUSD + + assert.strictEqual(ob.length, 1) + assert.deepStrictEqual(ob, [[100, 1, 1]]) + }) + + it('_handleCandleMessage: maintains internal candles if management is enabled', () => { + ws = new WSv2({ manageCandles: true }) + ws._channelMap = { + 64: { + channel: 'candles', + key: 'trade:1m:tBTCUSD' + } + } + + ws._handleCandleMessage([64, [ + [5, 100, 70, 150, 30, 1000], + [2, 200, 90, 150, 30, 1000], + [1, 130, 90, 150, 30, 1000], + [4, 104, 80, 150, 30, 1000] + ]], ws._channelMap[64]) + + const candles = ws._candles['trade:1m:tBTCUSD'] + + // maintains sort + assert.strictEqual(candles.length, 4) + assert.strictEqual(candles[0][0], 5) + assert.strictEqual(candles[1][0], 4) + assert.strictEqual(candles[2][0], 2) + assert.strictEqual(candles[3][0], 1) + + // updates existing candle + ws._handleCandleMessage([ + 64, + [5, 200, 20, 220, 20, 2000] + ], ws._channelMap[64]) + + assert.deepStrictEqual(candles[0], [5, 200, 20, 220, 20, 2000]) + + // inserts new candle + ws._handleCandleMessage([ + 64, + [10, 300, 20, 450, 10, 4000] + ], ws._channelMap[64]) + + assert.deepStrictEqual(candles[0], [10, 300, 20, 450, 10, 4000]) + }) + + it('_handleCandleMessage: emits error on internal candle update failure', (done) => { + ws = new WSv2({ manageCandles: true }) + ws._channelMap = { + 42: { + channel: 'candles', + key: 'trade:30m:tBTCUSD' + }, + + 64: { + channel: 'candles', + key: 'trade:1m:tBTCUSD' + } + } + + let errorsSeen = 0 + + ws.on('error', () => { + if (++errorsSeen === 1) done() + }) + + ws._handleCandleMessage([64, [ + [5, 100, 70, 150, 30, 1000], + [2, 200, 90, 150, 30, 1000], + [1, 130, 90, 150, 30, 1000], + [4, 104, 80, 150, 30, 1000] + ]], ws._channelMap[64]) + + // update for unknown key + ws._handleCandleMessage([ + 42, + [5, 10, 70, 150, 30, 10] + ], ws._channelMap[42]) + }) + + it('_handleCandleMessage: forwards managed candles to listeners', (done) => { + ws = new WSv2({ manageCandles: true }) + ws._channelMap = { + 42: { + chanId: 42, + channel: 'candles', + key: 'trade:1m:tBTCUSD' + } + } + + let seen = 0 + ws.onCandle({ key: 'trade:1m:tBTCUSD' }, (data) => { + assert.deepStrictEqual(data, [[5, 10, 70, 150, 30, 10]]) + if (++seen === 2) done() + }) + + ws.onCandle({}, (data) => { + assert.deepStrictEqual(data, [[5, 10, 70, 150, 30, 10]]) + if (++seen === 2) done() + }) + + ws._handleCandleMessage([ + 42, + [[5, 10, 70, 150, 30, 10]] + ], ws._channelMap[42]) + }) + + it('_handleCandleMessage: emits managed candles', () => { + let seenCandle = false + ws = new WSv2({ manageCandles: true }) + ws._channelMap = { + 42: { + channel: 'candles', + key: 'trade:1m:tBTCUSD' + } + } + + ws.on('candle', (data, key) => { + assert.strictEqual(key, 'trade:1m:tBTCUSD') + assert.deepStrictEqual(data, [[5, 10, 70, 150, 30, 10]]) + seenCandle = true + }) + + ws._handleCandleMessage([ + 42, + [[5, 10, 70, 150, 30, 10]] + ], ws._channelMap[42]) + + assert(seenCandle) + }) + + it('_handleCandleMessage: forwards transformed data if transform enabled', () => { + let seenCandle = false + ws = new WSv2({ transform: true }) + ws._channelMap = { + 42: { + chanId: 42, + channel: 'candles', + key: 'trade:1m:tBTCUSD' + } + } + + ws.onCandle({ key: 'trade:1m:tBTCUSD' }, (candles) => { + assert.strictEqual(candles.length, 1) + assert.deepStrictEqual(candles[0], { + mts: 5, + open: 10, + close: 70, + high: 150, + low: 30, + volume: 10 + }) + + seenCandle = true + }) + + ws._handleCandleMessage([ + 42, + [[5, 10, 70, 150, 30, 10]] + ], ws._channelMap[42]) + + assert(seenCandle) + }) + + it('_updateManagedCandles: returns an error on update for unknown key', () => { + ws = createTestWSv2Instance() + ws._candles['trade:1m:tBTCUSD'] = [] + + const err = ws._updateManagedCandles('trade:30m:tBTCUSD', [ + 1, 10, 70, 150, 30, 10 + ]) + + assert(err) + assert(_isError(err)) + }) + + it('_updateManagedCandles: correctly maintains transformed OBs', () => { + ws = new WSv2({ transform: true }) + + assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [ + [1, 10, 70, 150, 30, 10], + [2, 10, 70, 150, 30, 10] + ])) + + assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [ + 2, 10, 70, 150, 30, 500 + ])) + + assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [ + 3, 100, 70, 150, 30, 10 + ])) + + const candles = ws._candles['trade:1m:tBTCUSD'] + + assert.strictEqual(candles.length, 3) + assert.deepStrictEqual(candles[0], [ + 3, 100, 70, 150, 30, 10 + ]) + + assert.deepStrictEqual(candles[1], [ + 2, 10, 70, 150, 30, 500 + ]) + + assert.deepStrictEqual(candles[2], [ + 1, 10, 70, 150, 30, 10 + ]) + }) + + it('_updateManagedCandles: correctly maintains non-transformed OBs', () => { + ws = createTestWSv2Instance() + + assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [ + [1, 10, 70, 150, 30, 10], + [2, 10, 70, 150, 30, 10] + ])) + + assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [ + 2, 10, 70, 150, 30, 500 + ])) + + assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [ + 3, 100, 70, 150, 30, 10 + ])) + + const candles = ws._candles['trade:1m:tBTCUSD'] + + assert.strictEqual(candles.length, 3) + assert.deepStrictEqual(candles[0], [ + 3, 100, 70, 150, 30, 10 + ]) + + assert.deepStrictEqual(candles[1], [ + 2, 10, 70, 150, 30, 500 + ]) + + assert.deepStrictEqual(candles[2], [ + 1, 10, 70, 150, 30, 10 + ]) + }) + }) + + describe('event msg handling', () => { + it('_handleErrorEvent: emits error', (done) => { + ws = createTestWSv2Instance() + ws.on('error', (err) => { + if (err === 42) done() + }) + ws._handleErrorEvent(42) + }) + + it('_handleConfigEvent: emits error if config failed', (done) => { + ws = createTestWSv2Instance() + ws.on('error', (err) => { + if (_includes(err.message, '42')) done() + }) + ws._handleConfigEvent({ status: 'bad', flags: 42 }) + }) + + it('_handleAuthEvent: emits an error on auth fail', (done) => { + ws = createTestWSv2Instance() + ws.on('error', () => { + done() + }) + ws._handleAuthEvent({ status: 'FAIL' }) + }) + + it('_handleAuthEvent: updates auth flag on auth success', () => { + ws = createTestWSv2Instance() + assert(!ws.isAuthenticated()) + ws._handleAuthEvent({ status: 'OK' }) + assert(ws.isAuthenticated()) + }) + + it('_handleAuthEvent: adds auth channel to channel map', () => { + ws = createTestWSv2Instance() + assert(Object.keys(ws._channelMap).length === 0) + ws._handleAuthEvent({ chanId: 42, status: 'OK' }) + assert(ws._channelMap[42]) + assert.strictEqual(ws._channelMap[42].channel, 'auth') + }) + + it('_handleAuthEvent: emits auth message', (done) => { + ws = createTestWSv2Instance() + ws.once('auth', (msg) => { + assert.strictEqual(msg.chanId, 0) + assert.strictEqual(msg.status, 'OK') + done() + }) + ws._handleAuthEvent({ chanId: 0, status: 'OK' }) + }) + + it('_handleSubscribedEvent: adds channel to channel map', () => { + ws = createTestWSv2Instance() + assert(Object.keys(ws._channelMap).length === 0) + ws._handleSubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' }) + assert(ws._channelMap[42]) + assert.strictEqual(ws._channelMap[42].chanId, 42) + assert.strictEqual(ws._channelMap[42].channel, 'test') + assert.strictEqual(ws._channelMap[42].extra, 'stuff') + }) + + it('_handleUnsubscribedEvent: removes channel from channel map', () => { + ws = createTestWSv2Instance() + assert(Object.keys(ws._channelMap).length === 0) + ws._handleSubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' }) + ws._handleUnsubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' }) + assert(Object.keys(ws._channelMap).length === 0) + }) + + it('_handleInfoEvent: passes message to relevant listeners (raw access)', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + let n = 0 + + ws._infoListeners[42] = [ // eslint-disable-line + () => { n += 1 }, + () => { n += 2 } + ] + + ws._handleInfoEvent({ code: 42 }) + + assert.strictEqual(n, 3) + }) + + it('_handleInfoEvent: passes message to relevant listeners', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + let n = 0 + + ws.onInfoMessage(42, () => { n += 1 }) + ws.onInfoMessage(42, () => { n += 2 }) + ws._handleInfoEvent({ code: 42 }) + + assert.strictEqual(n, 3) + wss.close() + }) + + it('_handleInfoEvent: passes message to relevant named listeners', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + let n = 0 + + ws.onServerRestart(() => { n += 1 }) + ws.onMaintenanceStart(() => { n += 10 }) + ws.onMaintenanceEnd(() => { n += 100 }) + + ws._handleInfoEvent({ code: WSv2.info.SERVER_RESTART }) + ws._handleInfoEvent({ code: WSv2.info.MAINTENANCE_START }) + ws._handleInfoEvent({ code: WSv2.info.MAINTENANCE_END }) + + assert.strictEqual(n, 111) + wss.close() + }) + + it('_handleInfoEvent: closes & emits error if not on api v2', async () => { + wss = new MockWSv2Server() + ws = createTestWSv2Instance() + + await ws.open() + + return new Promise((resolve) => { + let seen = 0 + + ws.on('error', () => { if (++seen === 2) { resolve() } }) + ws.on('close', () => { if (++seen === 2) { resolve() } }) + + ws._handleInfoEvent({ version: 3 }) + }) + }) + + it('_flushOrderOps: returned promise rejects if not authorised', async () => { + ws = createTestWSv2Instance() + ws._orderOpBuffer = [[0, 'oc', null, []]] + + try { + await ws._flushOrderOps() + assert(false) + } catch (e) { + assert.ok(true, 'rejected due to being unauthorized') + } + }) + + it('_flushOrderOps: merges the buffer into a multi-op packet & sends', (done) => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + + ws._orderOpBuffer = [ + [0, 'oc', null, []], + [0, 'on', null, []], + [0, 'oc_multi', null, []], + [0, 'ou', null, []] + ] + + ws.send = (packet) => { + assert.strictEqual(packet[1], 'ox_multi') + assert.strictEqual(packet[3].length, 4) + done() + } + + ws._flushOrderOps().catch(() => assert(false)) + }) + + it('_flushOrderOps: splits up buffers greater than 15 ops in size', async () => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + + let seenCount = 0 + let seenAll = false + + for (let i = 0; i < 45; i++) { + ws._orderOpBuffer.push([0, 'oc', null, []]) + } + + ws.send = (packet) => { + assert.strictEqual(packet[1], 'ox_multi') + assert(packet[3].length <= 15) + seenCount += packet[3].length + + if (seenCount === 45) { + seenAll = true + } + } + + try { + await ws._flushOrderOps() + assert(false) + } catch (e) { + assert(seenAll) + } + }) + }) + + describe('WSv2 packet watch-dog', () => { + it('resets the WD timeout on every websocket message', (done) => { + ws = new WSv2({ packetWDDelay: 1000 }) + assert.strictEqual(ws._packetWDTimeout, null) + + ws.on('error', () => { }) // ignore json errors + + let wdResets = 0 + ws._resetPacketWD = () => { + if (++wdResets === 4) done() + } + + ws._onWSMessage('asdf') + ws._onWSMessage('asdf') + ws._onWSMessage('asdf') + ws._onWSMessage('asdf') + }) + + it('_resetPacketWD: clears existing wd timeout', (done) => { + ws = new WSv2({ packetWDDelay: 1000 }) + ws._packetWDTimeout = setTimeout(() => { + assert(false) + }, 100) + + ws._resetPacketWD() + setTimeout(done, 200) + }) + + it('_resetPacketWD: schedules new wd timeout', (done) => { + ws = new WSv2({ packetWDDelay: 500 }) + ws._isOpen = true + ws._triggerPacketWD = async () => { done() } + ws._resetPacketWD() + assert(ws._packetWDTimeout !== null) + }) + + it('_triggerPacketWD: does nothing if wd is disabled', (done) => { + ws = createTestWSv2Instance() + ws._isOpen = true + ws.reconnect = async () => { assert(false) } + ws._triggerPacketWD() + + setTimeout(done, 50) + }) + + it('_triggerPacketWD: calls reconnect()', (done) => { + ws = new WSv2({ packetWDDelay: 1000 }) + ws._isOpen = true + ws.reconnect = async () => { done() } + ws._triggerPacketWD() + }) + + it('triggers wd when no packet arrives after delay elapses', async () => { + const now = Date.now() + let wdTriggered = false + + ws = new WSv2({ packetWDDelay: 100 }) + ws._isOpen = true + + ws.on('error', () => { }) // invalid json to prevent message routing + ws._triggerPacketWD = () => { + assert((Date.now() - now) >= 95) + wdTriggered = true + + return Promise.resolve() + } + + ws._triggerPacketWD = ws._triggerPacketWD.bind(ws) + ws._onWSMessage('asdf') // send first packet, init wd + + await new Promise(resolve => setTimeout(resolve, 150)) + + assert(wdTriggered) + }) + + it('doesn\'t trigger wd when packets arrive as expected', async () => { + ws = new WSv2({ packetWDDelay: 100 }) + ws._isOpen = true + + ws.on('error', () => { }) // invalid json to prevent message routing + + const sendInterval = setInterval(() => { + ws._onWSMessage('asdf') + }, 50) + + ws._triggerPacketWD = async () => { assert(false) } + ws._onWSMessage('asdf') + + await new Promise(resolve => setTimeout(resolve, 200)) + + clearInterval(sendInterval) + clearTimeout(ws._packetWDTimeout) + }) + }) + + describe('message sending', () => { + it('emits error if no client available or open', async () => { + ws = createTestWSv2Instance() + + return new Promise((resolve) => { + ws.on('error', (e) => { + if (!_includes(e.message, 'no ws client')) { + throw new Error('received unexpected error') + } else { + resolve() + } + }) + + ws.send({}) + }) + }) + + it('emits error if connection is closing', async () => { + ws = createTestWSv2Instance() + + ws._ws = true + ws._isOpen = true + ws._isClosing = true + + return new Promise((resolve) => { + ws.on('error', (e) => { + if (!_includes(e.message, 'currently closing')) { + throw new Error('received unexpected error') + } else { + resolve() + } + }) + + ws.send({}) + }) + }) + + it('sends stringified payload', async () => { + ws = createTestWSv2Instance() + + ws._isOpen = true + ws._isClosing = false + + return new Promise((resolve) => { + ws._ws = { + send: (json) => { + const msg = JSON.parse(json) + + assert.strictEqual(msg.a, 42) + resolve() + } + } + + ws.send({ a: 42 }) + }) + }) + }) + + describe('WSv2 seq audit: _validateMessageSeq', () => { + it('returns an error on invalid pub seq', () => { + ws = createTestWSv2Instance() + + ws._seqAudit = true + ws._lastPubSeq = 0 + + assert.strictEqual(ws._validateMessageSeq([243, [252.12, 2, -1], 1]), null) + assert.strictEqual(ws._validateMessageSeq([243, [252.12, 2, -1], 2]), null) + assert(_isError(ws._validateMessageSeq([243, [252.12, 2, -1], 5]))) + }) + + it('returns an error on invalid auth seq', () => { + ws = createTestWSv2Instance() + + ws._seqAudit = true + ws._lastPubSeq = 0 + ws._lastAuthSeq = 0 + + assert.strictEqual(ws._validateMessageSeq([0, [252.12, 2, -1], 1, 1]), null) + assert.strictEqual(ws._validateMessageSeq([0, [252.12, 2, -1], 2, 2]), null) + assert(_isError(ws._validateMessageSeq([0, [252.12, 2, -1], 3, 5]))) + }) + + it('ignores heartbeats', () => { + ws = createTestWSv2Instance() + + ws._seqAudit = true + ws._lastPubSeq = 0 + + assert.strictEqual(ws._validateMessageSeq([243, [252.12, 2, -1], 1]), null) + assert.strictEqual(ws._validateMessageSeq([243, [252.12, 2, -1], 2]), null) + assert.strictEqual(ws._validateMessageSeq([243, 'hb']), null) + assert.strictEqual(ws._validateMessageSeq([243, 'hb']), null) + assert.strictEqual(ws._validateMessageSeq([243, [252.12, 2, -1], 3]), null) + assert.strictEqual(ws._validateMessageSeq([243, [252.12, 2, -1], 4]), null) + }) + + it.skip('all chan 0 messages except for notifications include, but do not advance the pub seq num', () => { + const ws = new WSv2() + + ws._seqAudit = true + ws._lastPubSeq = 1 + ws._lastAuthSeq = 1 + + assert.strictEqual(ws._validateMessageSeq([0, 'bu', [], 1, 2]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'bu', [], 1, 3]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'bu', [], 1, 4]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'n', [], 1, 5]), null) + assert.strictEqual(ws._lastPubSeq, 1) + assert.strictEqual(ws._lastAuthSeq, 5) + }) + + it.skip('non-*-req notifications advance the auth seq num and do not include a pub seq num', () => { + const ws = new WSv2() + + ws._seqAudit = true + ws._lastPubSeq = 4 + ws._lastAuthSeq = 4 + + const nonReqPayload = [null, null, null, null, null, null, null] + + assert.strictEqual(ws._validateMessageSeq([0, 'n', nonReqPayload, 5]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'n', nonReqPayload, 6]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'n', nonReqPayload, 7]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'n', nonReqPayload, 8]), null) + assert.strictEqual(ws._validateMessageSeq([0, 'n', nonReqPayload, 9]), null) + assert.strictEqual(ws._lastPubSeq, 9) + assert.strictEqual(ws._lastAuthSeq, 9) + }) + + it.skip('*-req notifications do not advance the auth sequence number', () => { + const ws = new WSv2() + const onReqPacket = [0, 'n', [0, 'on-req', null, null, [], null, '', ''], 1] + const ouReqPacket = [0, 'n', [0, 'ou-req', null, null, [], null, '', ''], 1] + const ocReqPacket = [0, 'n', [0, 'oc-req', null, null, [], null, '', ''], 1] + + ws._seqAudit = true + ws._lastPubSeq = -1 + ws._lastAuthSeq = -1 + + assert.strictEqual(ws._validateMessageSeq(onReqPacket), null) + assert.strictEqual(ws._validateMessageSeq(ouReqPacket), null) + assert.strictEqual(ws._validateMessageSeq(ocReqPacket), null) + assert.strictEqual(ws._lastPubSeq, -1) + assert.strictEqual(ws._lastAuthSeq, 3) + }) + }) + + describe('_handleTradeMessage', () => { + it('correctly forwards payloads w/ seq numbers', () => { + ws = createTestWSv2Instance() + + const payload = [ + [286614318, 1535531325604, 0.05, 7073.51178714], + [286614249, 1535531321436, 0.0215938, 7073.6], + [286614248, 1535531321430, 0.0284062, 7073.51178714] + ] + + const msg = [1710, payload, 1] + let sawTrade = false + + ws.onTrades({ pair: 'tBTCUSD' }, (data) => { + assert.deepStrictEqual(data, payload) + sawTrade = true + }) + + ws._handleTradeMessage(msg, { + channel: 'trades', + pair: 'tBTCUSD' + }) + + assert(sawTrade) + }) + + it('correctly forwards funding trades', () => { + ws = createTestWSv2Instance() + + const payload = [ + [286614318, 1535531325604, 0.05, 7073.51178714], + [286614249, 1535531321436, 0.0215938, 7073.6], + [286614248, 1535531321430, 0.0284062, 7073.51178714] + ] + + const msg = [1710, payload, 1] + let sawTrade = false + + ws.onTrades({ pair: 'fUSD' }, (data) => { + assert.deepStrictEqual(data, payload) + sawTrade = true + }) + + ws._handleTradeMessage(msg, { + channel: 'trades', + pair: 'fUSD' + }) + + assert(sawTrade) + }) + + it('correctly routes fte packets', () => { + ws = createTestWSv2Instance() + + const payload = [636854, 'fUSD', 1575282446000, 41238905, -1000, 0.002, 7, null] + const msg = [0, 'fte', payload] + let sawFTE = false + + ws.onFundingTradeEntry({ pair: 'tBTCUSD' }, (data) => { + assert.deepStrictEqual(data[0], payload) + sawFTE = true + }) + + ws._handleTradeMessage(msg, { + channel: 'fte', + pair: 'tBTCUSD' + }) + + assert(sawFTE) + }) + + it('correctly routes ftu packets', () => { + ws = createTestWSv2Instance() + + const payload = [636854, 'fUSD', 1575282446000, 41238905, -1000, 0.002, 7, null] + const msg = [0, 'ftu', payload] + let sawFTU = false + + ws.onFundingTradeUpdate({ pair: 'tBTCUSD' }, (data) => { + assert.deepStrictEqual(data[0], payload) + sawFTU = true + }) + + ws._handleTradeMessage(msg, { + channel: 'ftu', + pair: 'tBTCUSD' + }) + + assert(sawFTU) + }) + + it('uses funding trade model for funding symbols', (done) => { + ws = createTestWSv2Instance({ transform: true }) + + const payload = [636854, 'fUSD', 1575282446000, 41238905, -1000, 0.002, 7, null] + const msg = [0, 'ftu', payload] + + ws.on('trades', (_, data) => { + assert(data instanceof FundingTrade) + done() + }) + + ws._handleTradeMessage(msg, { + channel: 'ftu', + pair: 'fUSD' + }) + }) + + it('uses public trade model for trading symbols', (done) => { + ws = createTestWSv2Instance({ transform: true }) + + const payload = [636854, 'tBTCUSD', 1575282446000, 41238905, -1000, 0.002, 7, null] + const msg = [0, 'tu', payload] + + ws.on('trades', (_, data) => { + assert(data instanceof PublicTrade) + done() + }) + + ws._handleTradeMessage(msg, { + channel: 'tu', + pair: 'tBTCUSD' + }) + }) + }) + + describe('resubscribePreviousChannels', () => { + it('resubscribes to channels in prev channel map', () => { + ws = createTestWSv2Instance() + let subTicker = false + let subTrades = false + let subBook = false + let subCandles = false + + ws._prevChannelMap = { + 123: { channel: 'ticker', symbol: 'tBTCUSD' }, + 456: { channel: 'trades', symbol: 'tBTCUSD' }, + 789: { channel: 'candles', key: 'trade:1m:tBTCUSD' }, + 42: { channel: 'book', symbol: 'tBTCUSD', prec: 'R0', len: '25' } + } + + ws.subscribeTicker = (sym) => { + assert.strictEqual(sym, 'tBTCUSD') + subTicker = true + } + + ws.subscribeTrades = (sym) => { + assert.strictEqual(sym, 'tBTCUSD') + subTrades = true + } + + ws.subscribeCandles = (key) => { + assert.strictEqual(key, 'trade:1m:tBTCUSD') + subCandles = true + } + + ws.subscribeOrderBook = (sym, prec, len) => { + assert.strictEqual(sym, 'tBTCUSD') + assert.strictEqual(prec, 'R0') + assert.strictEqual(len, '25') + subBook = true + } + + ws.resubscribePreviousChannels() + + assert(subTicker) + assert(subTrades) + assert(subCandles) + assert(subBook) + }) + }) + + describe('getURL', () => { + it('returns the URL the instance was constructed with', () => { + const ws = new WSv2({ url: 'test' }) + assert.strictEqual(ws.getURL(), 'test', 'instance does not use provided URL') + }) + }) + + describe('auth args', () => { + it('provides getAuthArgs to read args', () => { + ws = createTestWSv2Instance() + ws.updateAuthArgs({ dms: 4 }) + assert.strictEqual(ws.getAuthArgs().dms, 4) + }) + + it('initializes auth args', () => { + ws = createTestWSv2Instance() + const initAuthArgs = ws.getAuthArgs() + + assert(_isObject(initAuthArgs)) + assert.deepStrictEqual(initAuthArgs, { + apiKey: API_KEY, + apiSecret: API_SECRET + }) + }) + + it('updates auth args with setAuthArgs', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + let sendCalled = false + + await ws.open() + + ws.updateAuthArgs({ dms: 4 }) + assert.strictEqual(ws.getAuthArgs().dms, 4) + + ws.send = (payload) => { + assert.strictEqual(payload.dms, 4) + sendCalled = true + ws.emit('auth') + } + + ws.auth() // note promise ignored + assert(sendCalled) + }) + }) + + describe('usesAgent', () => { + it('returns true if an agent was passed to the constructor', () => { + ws = createTestWSv2Instance({ + agent: new SocksProxyAgent('socks4://127.0.0.1:9998') + }) + + assert.ok(ws.usesAgent(), 'usesAgent() does not indicate agent presence when one was provided') + }) + + it('returns false if no agent was passed to the constructor', () => { + ws = createTestWSv2Instance() + + assert.ok(!ws.usesAgent(), 'usesAgent() indicates agent presence when none provided') + }) + }) + + describe('default connection url', () => { + it('is a static member on the class', () => { + assert.ok(_isString(WSv2.url) && !_isEmpty(WSv2.url)) + }) + }) + + describe('onMaintenanceStart', () => { + it('is called when receiving a 20060 info code', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + + await ws.open() + + return new Promise((resolve) => { + ws.onMaintenanceStart(() => resolve()) + wss.send({ + event: 'info', + code: '20060' + }) + }) + }) + }) + + describe('onMaintenanceEnd', () => { + it('is called when receiving a 20061 info code', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + + await ws.open() + + return new Promise((resolve) => { + ws.onMaintenanceEnd(() => resolve()) + wss.send({ + event: 'info', + code: '20061' + }) + }) + }) + }) + + describe('onServerRestart', () => { + it('is called when receiving a 20051 info code', async () => { + ws = createTestWSv2Instance() + wss = new MockWSv2Server() + + await ws.open() + + return new Promise((resolve) => { + ws.onServerRestart(() => resolve()) + wss.send({ + event: 'info', + code: '20051' + }) + }) + }) + }) + + describe('cancelOrder', () => { + it('throws an error if not authenticated', (done) => { + ws = createTestWSv2Instance() + ws.cancelOrder().catch(() => { done() }) + }) + + it('uses order as id if given number', (done) => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + ws._sendOrderPacket = (packet) => { + assert.deepStrictEqual(packet[3], { id: 42 }) + done() + } + + ws.cancelOrder(42) + }) + + it('parses id from order array', (done) => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + ws._sendOrderPacket = (packet) => { + assert.deepStrictEqual(packet[3], { id: 42 }) + done() + } + + ws.cancelOrder([42]) + }) + + it('parses id from order object', (done) => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + ws._sendOrderPacket = (packet) => { + assert.deepStrictEqual(packet[3], { id: 42 }) + done() + } + + ws.cancelOrder({ id: 42 }) + }) + + it('resolves on confirmation', (done) => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + ws._sendOrderPacket = () => { } + + ws.cancelOrder(42) + .then(() => done()) + .catch((e) => done(e)) + + ws._eventCallbacks.q.get('order-cancel-42')[0]() + }) + }) + + describe('cancelOrders', () => { + it('throws an error if not authenticated', (done) => { + ws = createTestWSv2Instance() + ws.cancelOrders([]).catch(() => done()) + }) + + it('calls cancelOrder with each order', () => { + ws = createTestWSv2Instance() + ws._isAuthenticated = true + + let seen = 0 + + ws.cancelOrder = (i) => { seen += i } + ws.cancelOrders([1, 2]) + assert.strictEqual(seen, 3) + }) + + it('resolves when all orders are cancelled', async () => { + const now = Date.now() + ws = createTestWSv2Instance() + ws._isAuthenticated = true + ws.cancelOrder = async () => { + return new Promise(resolve => setTimeout(resolve, 10)) + } + + await ws.cancelOrders([1, 2]) + + assert.ok(Date.now() - now >= 10, 'did not wait') // note 10 - parallel + }) + }) + + describe('_handleTickerMessage', () => { + it('forwards messages to relevant listeners', (done) => { + ws = createTestWSv2Instance() + ws._channelMap = { + 42: { + chanId: 42, + channel: 'ticker', + symbol: 'tBTCUSD' + } + } + + ws.onTicker({ symbol: 'tBTCUSD' }, () => { done() }) + ws._handleTickerMessage([42, ['test']], ws._channelMap[42]) + }) + + it('emits a ticker event with the symbol', (done) => { + ws = createTestWSv2Instance() + ws._channelMap = { + 42: { + chanId: 42, + channel: 'ticker', + symbol: 'tBTCUSD' + } + } + + ws.on('ticker', (symbol, data) => { + assert.strictEqual(symbol, 'tBTCUSD') + assert.strictEqual(data[0], 42) + done() + }) + + ws._handleTickerMessage([42, [42]], ws._channelMap[42]) + }) + + it('uses the trading ticker model for trade symbols', (done) => { + ws = createTestWSv2Instance({ transform: true }) + ws._channelMap = { + 42: { + chanId: 42, + channel: 'ticker', + symbol: 'tBTCUSD' + } + } + + ws.on('ticker', (symbol, data) => { + assert.ok(data instanceof TradingTicker) + done() + }) + + ws._handleTickerMessage([42, [42]], ws._channelMap[42]) + }) + + it('uses the funding ticker model for funding symbols', (done) => { + ws = createTestWSv2Instance({ transform: true }) + ws._channelMap = { + 42: { + chanId: 42, + channel: 'ticker', + symbol: 'fUSD' + } + } + + ws.on('ticker', (symbol, data) => { + assert.ok(data instanceof FundingTicker) + done() + }) + + ws._handleTickerMessage([42, [42]], ws._channelMap[42]) + }) + }) + + describe('getChannelId', () => { + it('matches the specified type and filter', () => { + ws = createTestWSv2Instance() + ws._channelMap = { + test: { + chanId: 42, + channel: 'ticker', + symbol: 'fUSD' + } + } + + assert.strictEqual(ws.getDataChannelId('ticker', { symbol: 'fUSD' }), 'test') + }) + }) +}) diff --git a/test/lib/util/is_class.js b/test/lib/util/is_class.js new file mode 100644 index 00000000..9933dd96 --- /dev/null +++ b/test/lib/util/is_class.js @@ -0,0 +1,28 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const { TradingTicker } = require('bfx-api-node-models') +const { isClass } = require('../../../lib/util') + +describe('isClass', () => { + it('returns true for classes', () => { + assert(isClass(TradingTicker)) + }) + + it('returns false for functions', () => { + assert(!isClass(() => {})) + }) + + it('returns false for class instances', () => { + const t = new TradingTicker() + assert(!isClass(t)) + }) + + it('returns false for primitives', () => { + assert(!isClass(42)) + assert(!isClass('42')) + assert(!isClass({})) + assert(!isClass([])) + }) +}) diff --git a/test/lib/util/is_snapshot.js b/test/lib/util/is_snapshot.js new file mode 100644 index 00000000..dd8033d8 --- /dev/null +++ b/test/lib/util/is_snapshot.js @@ -0,0 +1,20 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') + +const { isSnapshot } = require('../../../lib/util') + +describe('isSnapshot - detects snapshots by data structure', () => { + it('returns false for heartbeats', () => { + assert.strictEqual(isSnapshot(['hb']), false) + }) + + it('returns false simple lists (data updates)', () => { + assert.strictEqual(isSnapshot([1337]), false) + }) + + it('returns true for nested lists (snapshots)', () => { + assert.strictEqual(isSnapshot([['a'], ['b']]), true) + }) +}) diff --git a/test/lib/ws2_manager.js b/test/lib/ws2_manager.js new file mode 100644 index 00000000..2f7696cf --- /dev/null +++ b/test/lib/ws2_manager.js @@ -0,0 +1,653 @@ +/* eslint-env mocha */ +'use strict' + +const assert = require('assert') +const _isObject = require('lodash/isObject') +const _isArray = require('lodash/isArray') +const WS2Manager = require('../../lib/ws2_manager') +const WSv2 = require('../../lib/transports/ws2') + +describe('WS2Manager', () => { + let m + + afterEach(async () => { + if (m) { + try { + await m.close() + } catch (e) { + assert.ok(true, 'may fail due to being modified internally') + } finally { + m = null // eslint-disable-line + } + } + }) + + describe('setAuthArgs', () => { + it('updates the internal auth args', () => { + m = new WS2Manager() + m.setAuthArgs({ apiKey: '42' }) + assert.strictEqual(m.getAuthArgs().apiKey, '42') + }) + }) + + describe('getAuthArgs', () => { + it('returns internal auth args', () => { + m = new WS2Manager() + m.setAuthArgs({ apiKey: '42' }) + assert.strictEqual(m.getAuthArgs().apiKey, '42') + }) + }) + + describe('reconnect', () => { + it('calls reconnect on all sockets', async () => { + m = new WS2Manager() + let called = false + + m._sockets.push({ + ws: { reconnect: async () => { called = true } } + }) + + await m.reconnect() + assert.ok(called, 'reconnect not called on socket') + }) + + it('resolves when all sockets reconnect', async () => { + m = new WS2Manager() + let called = false + + m._sockets.push({ + ws: { + reconnect: async () => { + await new Promise(resolve => setTimeout(resolve, 10)) + called = true + } + } + }) + + await m.reconnect() + assert.ok(called, 'reconnect not called on socket') + }) + }) + + describe('close', () => { + it('calls close on all sockets', async () => { + m = new WS2Manager() + let called = false + + m._sockets.push({ + ws: { close: async () => { called = true } } + }) + + await m.close() + assert.ok(called, 'close not called on socket') + }) + + it('resolves when all sockets close', async () => { + m = new WS2Manager() + let called = false + + m._sockets.push({ + ws: { + close: async () => { + await new Promise(resolve => setTimeout(resolve, 10)) + called = true + } + } + }) + + await m.close() + assert.ok(called, 'close not called on socket') + }) + }) + + describe('getNumSockets', () => { + it('returns the number of sockets', () => { + m = new WS2Manager() + m._sockets.push({}) + m._sockets.push({}) + assert.strictEqual(m.getNumSockets(), 2, 'did not report correct number of sockets') + }) + }) + + describe('getSocket', () => { + it('returns the socket at the requested index', () => { + m = new WS2Manager() + m._sockets.push(1) + m._sockets.push(42) + assert.strictEqual(m.getSocket(1), 42) + }) + }) + + describe('getSocketInfo', () => { + it('returns an array of objects reporting number of data channels per socket', () => { + m = new WS2Manager() + + m._sockets.push({ + pendingSubscriptions: [[], [], []], + pendingUnsubscriptions: [[]], + ws: { getDataChannelCount: () => 2 } + }) + + m._sockets.push({ + pendingSubscriptions: [[], [], []], + pendingUnsubscriptions: [[]], + ws: { getDataChannelCount: () => 3 } + }) + + const info = m.getSocketInfo() + + assert.ok(_isArray(info), 'did not return array') + info.forEach(i => assert.ok(_isObject(i), 'socket info not an object')) + assert.strictEqual(info[0].nChannels, 4, 'socket info does not report correct number of channels') + assert.strictEqual(info[1].nChannels, 5, 'socket info does not report correct number of channels') + }) + }) + + describe('getDataChannelCount', () => { + it('takes pending subs & unsubs into account', () => { + const s = { + ws: new WSv2(), + pendingSubscriptions: [['book', {}]], + pendingUnsubscriptions: [] + } + + s.ws._channelMap = { + 0: { channel: 'trades' }, + 1: { channel: 'candles', key: 'test' }, + 2: { channel: 'auth' } + } + + const count = WS2Manager.getDataChannelCount(s) + + assert.strictEqual(s.ws.getDataChannelCount(), 2) + assert.strictEqual(count, 3) + }) + }) + + describe('auth', () => { + it('does nothing if api key/secret are already provided', () => { + m = new WS2Manager({ apiKey: 'x', apiSecret: 'x' }) + + m.auth({ apiKey: '42', apiSecret: '43' }) + assert.strictEqual(m._socketArgs.apiKey, 'x') + assert.strictEqual(m._socketArgs.apiSecret, 'x') + }) + + it('saves auth args', () => { + m = new WS2Manager() + + m.auth({ calc: 1, dms: 4 }) + assert.strictEqual(m._authArgs.calc, 1) + assert.strictEqual(m._authArgs.dms, 4) + }) + + it('calls auth on existing unauthenticated sockets', (done) => { + let cred = false + m = new WS2Manager() + + m._sockets = [{ + ws: { + isAuthenticated: () => false, + updateAuthArgs: ({ apiKey: key, apiSecret: secret }) => { cred = `${key}:${secret}` }, + auth: () => { + assert.strictEqual(cred, '41:42') + done() + } + } + }] + + m.auth({ apiKey: '41', apiSecret: '42' }) + }) + }) + + describe('openSocket', () => { + it('binds listeners to forward events', async () => { + const heardEvents = {} + const events = [ + 'open', 'message', 'auth', 'error', 'close', 'subscribed', + 'unsubscribed' + ] + + m = new WS2Manager() + const s = m.openSocket() + const { ws } = s + + events.forEach(e => { + m.on(e, () => { heardEvents[e] = true }) + }) + + events.forEach(e => ws.emit(e)) + events.forEach(e => { + assert(heardEvents[e]) + }) + + return new Promise((resolve, reject) => { + ws.on('open', () => ws.close().then(resolve).catch(reject)) + }) + }).timeout(4000) + + it('saves socket state', async () => { + m = new WS2Manager() + const s = m.openSocket() + const { ws } = s + + assert.deepStrictEqual(m._sockets[0], s) + + return new Promise((resolve, reject) => { + ws.on('open', () => ws.close().then(resolve).catch(reject)) + }) + }).timeout(4000) + + it('binds \'unsubscribed\' listener to remove channel from pending unsubs', async () => { + m = new WS2Manager() + const s = m.openSocket() + const { ws } = s + + s.pendingUnsubscriptions.push(`${42}`) + s.ws.emit('unsubscribed', { chanId: 42 }) + + assert.strictEqual(s.pendingUnsubscriptions.length, 0) + + return new Promise((resolve, reject) => { + ws.on('open', () => ws.close().then(resolve).catch(reject)) + }) + }).timeout(4000) + + it('binds \'subscribed\' listener to remove channel from pending subs', async () => { + m = new WS2Manager() + const s = m.openSocket() + const { ws } = s + + s.pendingSubscriptions.push(['book', { symbol: 'tBTCUSD', prec: 'R0' }]) + s.ws.emit('subscribed', { + channel: 'book', + symbol: 'tBTCUSD', + prec: 'R0', + len: '25' + }) + + assert.strictEqual(s.pendingSubscriptions.length, 0) + + return new Promise((resolve, reject) => { + ws.on('open', () => ws.close().then(resolve).catch(reject)) + }) + }).timeout(4000) + + it('auto-auths if manager has credentials configured', (done) => { + m = new WS2Manager({ + apiKey: 'key', + apiSecret: 'secret' + }) + + const s = m.openSocket() + const { ws } = s + + ws.auth = async () => { + assert.strictEqual(ws._authArgs.apiKey, 'key', 'api key not set') + assert.strictEqual(ws._authArgs.apiSecret, 'secret', 'api secret not set') + + await ws.close() + done() + } + }).timeout(4000) + }) + + describe('getAuthenticatedSocket', () => { + it('returns the first authenticated socket found', () => { + m = new WS2Manager() + + for (let i = 0; i < 3; i += 1) { + m._sockets.push({ + test: i, + ws: { isAuthenticated: () => i === 1 } + }) + } + + assert.strictEqual(m.getAuthenticatedSocket().test, 1, 'did not return correct socket') + }) + }) + + describe('getFreeDataSocket', () => { + it('returns the first socket below the data channel limit', () => { + m = new WS2Manager() + + m._sockets[0] = { + ws: { getDataChannelCount: () => 200 }, + pendingSubscriptions: new Array(70), + pendingUnsubscriptions: new Array(10) + } + + m._sockets[1] = { + ws: { getDataChannelCount: () => 5 }, + pendingSubscriptions: [], + pendingUnsubscriptions: [] + } + + const s = m.getFreeDataSocket() + assert.deepStrictEqual(s, m._sockets[1]) + }) + }) + + describe('getSocketWithDataChannel', () => { + it('returns socket subscribed to specified channel/filter pair', () => { + m = new WS2Manager() + m._sockets[0] = { + ws: {}, + pendingSubscriptions: [['candles', { key: 'test' }]], + pendingUnsubscriptions: [] + } + + let s = m.getSocketWithDataChannel('candles', { key: 'test' }) + assert.deepStrictEqual(s, m._sockets[0]) + + /// / + m._sockets[0] = { + ws: { getDataChannelId: () => false }, + pendingSubscriptions: [['auth', {}]], + pendingUnsubscriptions: [] + } + + s = m.getSocketWithDataChannel('candles', { key: 'test' }) + assert(!s) + + /// / + m._sockets[0] = { + ws: { + getDataChannelId: (type, filter) => { + assert.strictEqual(type, 'candles') + assert.deepStrictEqual(filter, { key: 'test' }) + return 1 + } + }, + pendingSubscriptions: [], + pendingUnsubscriptions: [] + } + + s = m.getSocketWithDataChannel('candles', { key: 'test' }) + assert.deepStrictEqual(s, m._sockets[0]) + + /// / + m._sockets[0] = { + ws: { + getDataChannelId: (type, filter) => { + assert.strictEqual(type, 'candles') + assert.deepStrictEqual(filter, { key: 'test' }) + return 1 + } + }, + pendingSubscriptions: [], + pendingUnsubscriptions: [1] + } + + s = m.getSocketWithDataChannel('candles', { key: 'test' }) + assert(!s) + }) + }) + + describe('getSocketWithChannel', () => { + it('returns correct socket', () => { + m = new WS2Manager() + m._sockets[0] = { + pendingUnsubscriptions: [], + ws: { + hasChannel: (id) => { + return id === 42 + } + } + } + + let s = m.getSocketWithChannel(42) + assert.deepStrictEqual(s, m._sockets[0]) + + /// / + m._sockets[0] = { + pendingUnsubscriptions: [42], + ws: { + hasChannel: (id) => { + return id === 42 + } + } + } + + s = m.getSocketWithChannel(42) + assert(!s) + }) + }) + + describe('getSocketWithSubRef', () => { + it('returns the first socket found that has the requested subscription ref', () => { + m = new WS2Manager() + + for (let i = 0; i < 3; i += 1) { + m._sockets.push({ + test: i, + ws: { + hasSubscriptionRef: (channel, identifier) => { + assert.strictEqual(channel, 'a', 'did not pass channel through') + assert.strictEqual(identifier, 'b', 'did not pass identifier through') + return i === 1 + } + } + }) + } + + const s = m.getSocketWithSubRef('a', 'b') + assert.ok(_isObject(s), 'did not return a socket') + assert.strictEqual(s.test, 1, 'did not return correct socket') + }) + }) + + describe('subscribe', () => { + it('delays sub for unopened sockets', () => { + m = new WS2Manager() + let onceOpenCalled = false + + m._sockets[0] = { + pendingSubscriptions: [], + pendingUnsubscriptions: [], + ws: { + getDataChannelCount: () => 0, + managedSubscribe: () => assert(false), + isOpen: () => false, + once: (eName) => { + assert.strictEqual(eName, 'open') + onceOpenCalled = true + } + } + } + + m.subscribe('candles', 'test', { key: 'test' }) + assert(onceOpenCalled) + }) + + it('saves pending sub', () => { + m = new WS2Manager() + m._sockets[0] = { + pendingSubscriptions: [], + pendingUnsubscriptions: [], + ws: { + getDataChannelCount: () => 0, + managedSubscribe: () => {}, + isOpen: () => true + } + } + + m.subscribe('candles', 'test', { key: 'test' }) + assert.deepStrictEqual(m._sockets[0].pendingSubscriptions, [ + ['candles', { key: 'test' }] + ]) + }) + + it('opens a new socket if no sockets are available', () => { + m = new WS2Manager() + let openCalled = false + + m.openSocket = () => { + openCalled = true + + return { + pendingSubscriptions: [], + ws: { + once: () => {}, + isOpen: () => false // to avoid managed sub + } + } + } + + m.subscribe('candles', 'test', { key: 'test' }) + assert(openCalled) + }) + + it('opens a new socket if no sockets are below data limit', () => { + m = new WS2Manager() + let openCalled = false + + m._sockets[0] = { + pendingSubscriptions: [], + pendingUnsubscriptions: [], + ws: { + getDataChannelCount: () => 255 + } + } + + m.openSocket = () => { + openCalled = true + + const state = { + pendingSubscriptions: [], + ws: { + once: () => {}, + isOpen: () => false // to avoid managed sub + } + } + + m._sockets.push(state) + return state + } + + m.subscribe('candles', 'test', { key: 'test' }) + + assert(openCalled) + assert.strictEqual(m._sockets.length, 2) + }) + }) + + describe('unsubscribe', () => { + it('saves pending unsub & calls unsub on socket', () => { + m = new WS2Manager() + let unsubCalled = false + + m._sockets[0] = { + pendingUnsubscriptions: [], + ws: { + unsubscribe: (cid) => { + assert.strictEqual(cid, 42) + unsubCalled = true + }, + + hasChannel: (cid) => { + return cid === 42 + } + } + } + + m.unsubscribe(42) + assert.deepStrictEqual(m._sockets[0].pendingUnsubscriptions, [42]) + assert(unsubCalled) + }) + }) + + describe('managedUnsubscribe', () => { + it('saves pending unsub and calls managed unsub on socket', () => { + m = new WS2Manager() + let unsubCalled = false + + m._sockets[0] = { + pendingUnsubscriptions: [], + ws: { + managedUnsubscribe: (cid) => { + assert.strictEqual(cid, 42) + unsubCalled = true + }, + + hasSubscriptionRef: (cid) => cid === 42, + _chanIdByIdentifier: () => 42 + } + } + + m.managedUnsubscribe(42) + assert.deepStrictEqual(m._sockets[0].pendingUnsubscriptions, [42]) + assert(unsubCalled) + }) + }) + + describe('withAllSockets', () => { + it('calls the provided cb with all internal sockets', () => { + m = new WS2Manager() + const socketsSeen = {} + + m._sockets = ['a', 'b', 'c'] + m.withAllSockets((sock) => { + socketsSeen[sock] = true + }) + + assert(socketsSeen.a) + assert(socketsSeen.b) + assert(socketsSeen.c) + }) + }) + + describe('subscribeOrderBook', () => { + it('calls subscribe with a valid filter and the provided symbol', (done) => { + m = new WS2Manager() + m.subscribe = (type, symbol, filter) => { + assert.ok(_isObject(filter), 'filter not an object') + assert.strictEqual(filter.symbol, 'tBTCUSD', 'symbol did not match') + assert.strictEqual(filter.prec, 'P0', 'prec did not match') + assert.strictEqual(filter.len, '25', 'len did not match') + assert.strictEqual(filter.freq, 'F0', 'freq did not match') + assert.strictEqual(symbol, 'tBTCUSD') + done() + } + + m.subscribeOrderBook('tBTCUSD', 'P0', '25', 'F0') + }) + }) + + describe('onOrderBook', () => { + it('passes a valid OB filter to the first socket with a book channel', (done) => { + const assertFilter = (filter) => { + assert.ok(_isObject(filter), 'filter not an object') + assert.strictEqual(filter.symbol, 'tBTCUSD', 'symbol did not match') + assert.strictEqual(filter.prec, 'P0', 'prec did not match') + assert.strictEqual(filter.len, '25', 'len did not match') + assert.strictEqual(filter.freq, 'F0', 'freq did not match') + } + + m = new WS2Manager() + m._sockets.push({ + pendingSubscriptions: [], + pendingUnsubscriptions: [], + ws: { + getDataChannelId: (type, filter) => { + assert.strictEqual(type, 'book') + assertFilter(filter) + return 42 + }, + + onOrderBook: (filter) => { + assertFilter(filter) + done() + } + } + }) + + m.onOrderBook({ + symbol: 'tBTCUSD', + prec: 'P0', + len: '25', + freq: 'F0' + }) + }) + }) +}) diff --git a/test/rest-2-smoke-test.js b/test/rest-2-smoke-test.js deleted file mode 100644 index 795c46bf..00000000 --- a/test/rest-2-smoke-test.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' -const PORT = 1337 - -const assert = require('assert') -const http = require('http') - -const API_KEY = 'dummy' -const API_SECRET = 'dummy' - -const REST2 = require('../rest2.js') - -const bhttp = new REST2(API_KEY, API_SECRET) -bhttp.url = `http://localhost:${PORT}` - -const testResBody = `[1765.3, - 0.56800816, - 1767.6, - 1.3874, - -62.2, - -0.034, - 1765.3, - 14063.54589155, - 1834.2, - 1726.3 ]` - -const server = http.createServer((req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/plain' - }) - res.end(testResBody) -}) - -server.listen(PORT, () => { - bhttp.ticker('tBTCUSD', (err, res) => { - assert.equal(err, null) - assert.deepEqual( - res, - JSON.parse(testResBody) - ) - - server.close() - }) -}) diff --git a/test/rest-test.js b/test/rest-test.js deleted file mode 100644 index 09146907..00000000 --- a/test/rest-test.js +++ /dev/null @@ -1,648 +0,0 @@ -/* eslint-env mocha */ -/* eslint-disable */ - -const {expect} = require('chai') -const BFX = require('../index') -const _ = require('lodash') -const keys = require('./keys.json') -const ws_test = require('./ws-test') - -describe('Errors', function () { - const bfx = new BFX() - const bfx_rest = bfx.rest - this.timeout(5000) - it('should error out if a bad endpoint is given', () => { - expect(bfx_rest.make_public_request).to.throw(Error) - }) - it('should fail on authenticated requests if no api_key and api_secret', () => { - expect(bfx_rest.account_infos).to.throw(Error) - }) -}) -describe('Public Endpoints', function () { - const bfx = new BFX() - const bfx_rest = bfx.rest - this.timeout(5000) - it('should get a ticker', (done) => { - bfx_rest.ticker('BTCUSD', (error, data) => { - expect(data).to.exist - expect(_.has(data, ['mid', - 'bid', - 'ask', - 'last_price', - 'low', - 'high', - 'volume', - 'timestamp'])) - done() - }) - }) - it('should get the today endpoint', (done) => { - bfx_rest.today('BTCUSD', (error, data) => { - expect(data).to.exist - done() - }) - }) - it('should get the stats', (done) => { - bfx_rest.stats('BTCUSD', (error, data) => { - expect(data).to.exist - expect(_.has(data[0], ['period', 'volume'])) - expect(_.has(data[1], ['period', 'volume'])) - expect(_.has(data[2], ['period', 'volume'])) - done() - }) - }) - it('should get the fundingbook', (done) => { - bfx_rest.fundingbook('USD', (error, data) => { - expect(data).to.exist - expect(_.has(data, ['bids', 'asks'])) - expect(_.keys(data.bids[0])).is.eql(['rate', 'amount', 'period', 'timestamp', 'frr']) - expect(_.keys(data.asks[0])).is.eql(['rate', 'amount', 'period', 'timestamp', 'frr']) - expect( - _.every( - [data.asks[0] + data.bids[0]] - ), !NaN).ok - done() - }) - }) - it('should get the fundingbook asks, zero bids, 100 asks', (done) => { - bfx_rest.fundingbook('USD', {limit_bids: 0, limit_asks: 100}, (error, data) => { - expect(data).to.exist - expect(_.has(data, ['bids', 'asks'])) - expect(data.bids.length).is.eql(0) - expect(data.asks.length).is.eql(100) - done() - }) - }) - it('should get the orderbook', (done) => { - bfx_rest.orderbook('BTCUSD', (error, data) => { - expect(data).to.exist - expect(_.keys(data)).is.eql(['bids', 'asks']) - expect(_.keys(data.bids[0])).is.eql(['price', 'amount', 'timestamp']) - expect(_.keys(data.asks[0])).is.eql(['price', 'amount', 'timestamp']) - expect( - _.every( - [data.asks[0] + data.bids[0]] - ), !NaN).ok - done() - }) - }) - // TODO API returns 1000 instead of 50` - it.skip('should get recent trades', (done) => { - bfx_rest.trades('BTCUSD', (error, data) => { - expect(data).is.an.array - expect(data.length).to.eql(50) - expect(_.keys(data[0])).to.eql(['timestamp', 'tid', 'price', 'amount', 'exchange', 'type']) - expect( - _.map( - _.values( - data[0] - ), (v) => typeof (v) - )).is.eql(['number', 'number', 'string', 'string', 'string', 'string']) - done() - }) - }) - it('should get recent lends', (done) => { - bfx_rest.lends('USD', (error, data) => { - expect(data).to.exist - expect(data).is.an.array - expect(data.length).to.eql(50) - expect(_.keys(data[0])).to.eql(['rate', 'amount_lent', 'amount_used', 'timestamp']) - expect( - _.map( - _.values( - data[0] - ), (v) => typeof (v) - )).is.eql(['string', 'string', 'string', 'number']) - done() - }) - }) - it('should get symbols', (done) => { - bfx_rest.get_symbols((error, data) => { - expect(data).to.eql([ - 'btcusd', - 'ltcusd', - 'ltcbtc', - 'ethusd', - 'ethbtc', - 'etcbtc', - 'etcusd', - 'rrtusd', - 'rrtbtc', - 'zecusd', - 'zecbtc', - 'xmrusd', - 'xmrbtc', - 'dshusd', - 'dshbtc', - 'bccbtc', - 'bcubtc', - 'bccusd', - 'bcuusd' - ]) - done() - }) - }) - it('should get symbol details', (done) => { - bfx_rest.symbols_details((error, data) => { - expect(data).to.exist - expect(data).to.eql([ - { - pair: 'btcusd', - price_precision: 5, - initial_margin: '30.0', - minimum_margin: '15.0', - maximum_order_size: '2000.0', - minimum_order_size: '0.01', - expiration: 'NA' - }, - { - pair: 'ltcusd', - price_precision: 5, - initial_margin: '30.0', - minimum_margin: '15.0', - maximum_order_size: '5000.0', - minimum_order_size: '0.1', - expiration: 'NA' - }, - { - pair: 'ltcbtc', - price_precision: 5, - initial_margin: '30.0', - minimum_margin: '15.0', - maximum_order_size: '5000.0', - minimum_order_size: '0.1', - expiration: 'NA' - }, - { - 'pair': 'ethusd', - 'price_precision': 5, - 'initial_margin': '30.0', - 'minimum_margin': '15.0', - 'maximum_order_size': '5000.0', - 'minimum_order_size': '0.1', - 'expiration': 'NA' - }, - { - 'pair': 'ethbtc', - 'price_precision': 5, - 'initial_margin': '30.0', - 'minimum_margin': '15.0', - 'maximum_order_size': '5000.0', - 'minimum_order_size': '0.1', - 'expiration': 'NA' - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '100000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.1', - 'pair': 'etcbtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '100000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.1', - 'pair': 'etcusd', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '100000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.1', - 'pair': 'rrtusd', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '100000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.1', - 'pair': 'rrtbtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '20000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.0001', - 'pair': 'zecusd', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '20000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.0001', - 'pair': 'zecbtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '5000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.1', - 'pair': 'xmrusd', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '5000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.1', - 'pair': 'xmrbtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '5000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.01', - 'pair': 'dshusd', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '5000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.01', - 'pair': 'dshbtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '2000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.01', - 'pair': 'bccbtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '2000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.01', - 'pair': 'bcubtc', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '2000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.01', - 'pair': 'bccusd', - 'price_precision': 5 - }, - { - 'expiration': 'NA', - 'initial_margin': '30.0', - 'maximum_order_size': '2000.0', - 'minimum_margin': '15.0', - 'minimum_order_size': '0.01', - 'pair': 'bcuusd', - 'price_precision': 5 - } - ]) - done() - }) - }) -}) - -// FIXME: The API key in keys.json is invalid, causing these tests to fail. -xdescribe('Authenticated Endpoints: standard key', function () { - this.timeout(5000) - const key = keys.standard.api_key - const secret = keys.standard.api_secret - const bfx = new BFX(key, secret) - const bfx_rest = bfx.rest - it('should get account info', (done) => { - bfx_rest.account_infos((error, data) => { - expect(data).to.exist - done() - }) - }) - it('should get a deposit address', (done) => { - bfx_rest.new_deposit('BTC', 'bitcoin', 'exchange', (err, data) => { - expect(data.result).to.eql('success') - done() - }) - }) - describe('orders', () => { - it('should place a new order', (done) => { - const errCB = function (err, value) { - expect(err.toString()).is.eql('Error: Invalid order: not enough exchange balance for 0.01 BTCUSD at 0.01') - expect(err instanceof Error).ok - return done() - } - bfx_rest.new_order('BTCUSD', '0.01', '0.01', 'bitfinex', 'buy', 'exchange limit', false, errCB) - }) - it('should place multiple orders', (done) => { - const errCB = function (err, value) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql("Error: Couldn't place an order: Invalid order: not enough exchange balance for 0.01 BTCUSD at 0.01") - return done() - } - const orders = [{ - 'symbol': 'BTCUSD', - 'amount': '0.01', - 'price': '0.01', - 'exchange': 'bitfinex', - 'side': 'buy', - 'type': 'exchange limit' - }, { - 'symbol': 'BTCUSD', - 'amount': '0.02', - 'price': '0.03', - 'exchange': 'bitfinex', - 'side': 'buy', - 'type': 'exchange limit' - }] - bfx_rest.multiple_new_orders(orders, errCB) - }) - it('should cancel an order', (done) => { - const errCB = function (err, value) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Order could not be cancelled.') - return done() - } - bfx_rest.cancel_order(1, errCB) - }) - // TODO API needs to be fixed, never throws error - it.skip('should cancel multiple orders', (done) => { - const errCB = function (err, value) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Order could not be cancelled.') - return done() - } - bfx_rest.cancel_multiple_orders([1, 2], errCB) - }) - // TODO API needs to be fixed, never throws error - it.skip('should cancel all orders', (done) => { - const errCB = function (err, value) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Order could not be cancelled.') - return done() - } - bfx_rest.cancel_all_orders(errCB) - }) - it('should replace an order', (done) => { - const errCB = function (err, value) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Order could not be cancelled.') - return done() - } - bfx_rest.replace_order(1, 'BTCUSD', '0.01', '0.01', 'bitfinex', 'buy', 'exchange limit', errCB) - }) - // TODO throws 404 error, is that intentional? - it.skip('should get an orders status', (done) => { - const errCB = function (err, value) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Order could not be cancelled.') - return done() - } - bfx_rest.order_status(1000, errCB) - }) - it('should get active orders', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.active_orders(cb) - }) - }) - describe('positions', () => { - it('should get active positions', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).is.empty - return done() - } - bfx_rest.active_positions(cb) - }) - // TODO returns 404 instead of JSON on failure - it.skip('should claim a position', (done) => { - const errCB = function (err, data) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Order could not be cancelled.') - return done() - } - bfx_rest.claim_position(12345, errCB) - }) - }) - describe('historical data', () => { - it('should get balance history', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.balance_history('USD', {}, cb) - }) - it('should get deposit/withdrawal history', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.movements('USD', {}, cb) - }) - it('should get past trades', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.past_trades('BTCUSD', {}, cb) - }) - }) - describe('margin funding', () => { - it('should place a new offer', (done) => { - const errCB = function (err, data) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Invalid offer: not enough balance') - return done() - } - bfx_rest.new_offer('USD', '0.01', '0.02', 2, 'lend', errCB) - }) - it('should cancel an offer', (done) => { - const errCB = function (err, data) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: Offer could not be cancelled.') - return done() - } - bfx_rest.cancel_offer(12345, errCB) - }) - // TODO returns 404 - it('should get an offer status', (done) => { - const errCB = function (err, data) { - expect(err instanceof Error).ok - expect(err.toString()).is.eql('Error: 404') - return done() - } - bfx_rest.offer_status(12345, errCB) - }) - it('should get active credits', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.active_credits(cb) - }) - it('should get active funding used in a margin position', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.taken_swaps(cb) - }) - it('should get total taken funds', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.total_taken_swaps(cb) - }) - }) - it('should get wallet balances', (done) => { - const cb = function (err, data) { - expect(data).to.be.an.array - expect(data).to.be.empty - return done() - } - bfx_rest.wallet_balances(cb) - }) - it('should get margin information', (done) => { - const cb = function (err, data) { - expect(data[0]).to.be.an.array - expect(_.keys(data[0])).to.eql( - [ - 'margin_balance', - 'tradable_balance', - 'unrealized_pl', - 'unrealized_swap', - 'net_value', - 'required_margin', - 'leverage', - 'margin_requirement', - 'margin_limits', - 'message']) - return done() - } - bfx_rest.margin_infos(cb) - }) - it('should transfer between wallets', (done) => { - const errCB = function (err, data) { - expect(err instanceof Error).ok - expect(err.toString()).to.eql('Error: 403') - return done() - } - bfx_rest.transfer(0.01, 'BTC', 'exchange', 'trading', errCB) - }) - it('should submit a withdrawal', (done) => { - const errCB = function (err, data) { - expect(err instanceof Error).ok - expect(err.toString()).to.eql('Error: 403') - return done() - } - bfx_rest.withdraw('bitcoin', 'exchange', 0.01, 'abc', errCB) - }) -}) -describe('Authenticated Endpoints: read-only key', () => { - let bfx, bfx_rest - before(() => { - bfx = new BFX() - bfx_rest = bfx.rest - }) - it('should get account info') - it('should get a deposit address') - describe('orders', () => { - it('should place a new order') - it('should place multiple orders') - it('should cancel an order') - it('should cancel multiple orders') - it('should cancel all orders') - it('should replace an order') - it('should get an orders status') - it('should get active orders') - }) - describe('positions', () => { - it('should get active positions') - it('should claim a position') - }) - describe('historical data', () => { - it('should get balance history') - it('should get deposit/withdrawal history') - it('should get past trades') - }) - describe('margin funding', () => { - it('should place a new offer') - it('should cancel an offer') - it('should get an offer status') - it('should get active credits') - it('should get active funding used in a margin position') - it('should get total taken funds') - }) - it('should get wallet balances') - it('should get margin information') - it('should transfer between wallets') - it('should submit a withdrawal') -}) -describe('Authenticated Endpoints: withdrawal-enabled key', () => { - let bfx, bfx_rest - before(() => { - bfx = new BFX() - bfx_rest = bfx.rest - }) - it('should get account info') - it('should get a deposit address') - describe('orders', () => { - it('should place a new order') - it('should place multiple orders') - it('should cancel an order') - it('should cancel multiple orders') - it('should cancel all orders') - it('should replace an order') - it('should get an orders status') - it('should get active orders') - }) - describe('positions', () => { - it('should get active positions') - it('should claim a position') - }) - describe('historical data', () => { - it('should get balance history') - it('should get deposit/withdrawal history') - it('should get past trades') - }) - describe('margin funding', () => { - it('should place a new offer') - it('should cancel an offer') - it('should get an offer status') - it('should get active credits') - it('should get active funding used in a margin position') - it('should get total taken funds') - }) - it('should get wallet balances') - it('should get margin information') - it('should transfer between wallets') - it('should submit a withdrawal') -}) diff --git a/test/ws-2-order-book.js b/test/ws-2-order-book.js deleted file mode 100644 index d8c8a57f..00000000 --- a/test/ws-2-order-book.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-env mocha */ - -'use strict' -const PORT = 1337 - -const assert = require('assert') - -const WebSocket = require('ws') -const BfxWs = require('../ws2.js') - -const API_KEY = 'dummy' -const API_SECRET = 'dummy' - -describe('websocket order books', () => { - it('sends the length parameter', (done) => { - const bfxWs = new BfxWs( - API_KEY, - API_SECRET, - { websocketURI: `ws://localhost:${PORT}` } - ) - - bfxWs.open() - - const wss = new WebSocket.Server({ - perMessageDeflate: false, - port: PORT - }) - - wss.on('connection', function connection (ws) { - ws.on('message', function incoming (msg) { - msg = JSON.parse(msg) - assert.equal(msg.len, '25') - wss.close() - done() - }) - - ws.send('[{}]') - }) - - bfxWs.on('open', () => { - bfxWs.subscribeOrderBook('BTCUSD', 'R0') - }) - }) -}) diff --git a/test/ws-test.js b/test/ws-test.js deleted file mode 100644 index 5d0034b5..00000000 --- a/test/ws-test.js +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-env mocha */ - -'use strict' - -const {expect} = require('chai') -const BFX = require('../index') -const testKeys = require('./keys.json') - -describe('WebSocket', function () { - this.timeout(3000) - - beforeEach(function () { - const bitfinex = new BFX( - testKeys.standard.api_key, - testKeys.standard.api_secret - ) - this.bitfinexWS = bitfinex.ws - // this.bitfinexWS.on('open', done); - }) - - afterEach(function (done) { - this.bitfinexWS.close() - this.bitfinexWS.on('close', () => { - done() - }) - }) - - it('should receive a pong after a ping', function (done) { - this.bitfinexWS.on('pong', () => { - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.send({event: 'ping'}) - }) - }) - - it('should receive a subscribed success messages', function (done) { - this.bitfinexWS.on('subscribed', (data) => { - expect(data).to.have.property('channel', 'trades') - expect(data).to.have.property('pair', 'BTCUSD') - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.subscribeTrades('BTCUSD') - }) - }) - - it('should receive a pong after a ping', function (done) { - this.bitfinexWS.on('pong', () => { - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.send({event: 'ping'}) - }) - }) - - it('should receive info message', function (done) { - this.bitfinexWS.on('info', (data) => { - expect(data).is.eql({ - event: 'info', - version: 1.1 - }) - done() - }) - }) - - it('#orderBook data should have the defined fields', function (done) { - this.bitfinexWS.once('orderbook', (pair, data) => { - expect(pair).to.equal('BTCUSD') - expect(data.price).to.be.a('number') - expect(data.count).to.be.a('number') - expect(data.amount).to.be.a('number') - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.subscribeOrderBook('BTCUSD') - }) - }) - - it('#trade data should have the defined fields', function (done) { - this.bitfinexWS.once('trade', (pair, data) => { - expect(pair).to.equal('BTCUSD') - expect(data.seq).to.be.a('number') - expect(data.timestamp).to.be.a('number') - expect(data.price).to.be.a('number') - expect(data.amount).to.be.a('number') - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.subscribeTrades('BTCUSD') - }) - }) - - it('#ticker data should have the defined fields', function (done) { - this.bitfinexWS.once('ticker', (pair, data) => { - expect(pair).to.equal('BTCUSD') - expect(data.bid).to.be.a('number') - expect(data.bidSize).to.be.a('number') - expect(data.ask).to.be.a('number') - expect(data.askSize).to.be.a('number') - expect(data.dailyChange).to.be.a('number') - expect(data.dailyChangePerc).to.be.a('number') - expect(data.lastPrice).to.be.a('number') - expect(data.volume).to.be.a('number') - expect(data.high).to.be.a('number') - expect(data.low).to.be.a('number') - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.subscribeTicker('BTCUSD') - }) - }) - - it('should unsubscribe by channelId', function (done) { - this.bitfinexWS.once('subscribed', (data) => { - const channelId = data.chanId - this.bitfinexWS.once('unsubscribed', (data) => { - expect(data.status).to.equal('OK') - expect(data.chanId).to.equal(channelId) - done() - }) - this.bitfinexWS.unsubscribe(channelId) - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.subscribeTicker('BTCUSD') - }) - }) - - // FIXME: The API key in keys.json is invalid, causing this test to fail. - xit('should emit an auth event when authorized', function (done) { - this.bitfinexWS.on('auth', (data) => { - expect(data.status).to.equal('OK') - done() - }) - this.bitfinexWS.on('open', () => { - this.bitfinexWS.auth() - }) - }) - - it('should emit an error when authorization fails', function (done) { - this.bitfinexWS.APIKey = '' - this.bitfinexWS.APISecret = '' - - this.bitfinexWS.on('error', () => { - done() - }) - - this.bitfinexWS.on('open', () => { - this.bitfinexWS.auth() - }) - }) -}) diff --git a/ws.js b/ws.js deleted file mode 100644 index 6f3c0245..00000000 --- a/ws.js +++ /dev/null @@ -1,411 +0,0 @@ -'use strict' - -const {EventEmitter} = require('events') -const debug = require('debug')('bitfinex:ws') -const crypto = require('crypto') -const WebSocket = require('ws') -const util = require('util') - -/** - * Handles communitaction with Bitfinex WebSocket API. - * @param {sting} APIKey - * @param {string} APISecret - * @event - * @class - */ -const BitfinexWS = function (APIKey, APISecret) { - EventEmitter.call(this) - - this.APIKey = APIKey - this.APISecret = APISecret - this.ws = new WebSocket(BitfinexWS.WebSocketURI) - /** - * @event BitfinexWS#message - * @type {object} - */ - this.ws.on('message', this.onMessage.bind(this)) - /** - * WebSocket connection is open. Ready to send. - * @event BitfinexWS#open - */ - this.ws.on('open', this.onOpen.bind(this)) - /** - * @event BitfinexWS#error - */ - this.ws.on('error', this.onError.bind(this)) - /** - * WebSocket connection is closed. - * @event BitfinexWS#close - */ - this.ws.on('close', this.onClose.bind(this)) -} - -util.inherits(BitfinexWS, EventEmitter) - -/** - * @constant - * @type {String} - */ -BitfinexWS.WebSocketURI = 'wss://api.bitfinex.com/ws/' - -BitfinexWS.prototype.onMessage = function (msg, flags) { - msg = JSON.parse(msg) - debug('Received message: %j', msg) - debug('Emmited message event') - this.emit('message', msg, flags) - - if (!Array.isArray(msg) && msg.event) { - if (msg.event === 'subscribed') { - debug('Subscription report received') - // Inform the user the new event name that will be triggered - const data = { - channel: msg.channel, - chanId: msg.chanId, - pair: msg.pair - } - // Save to event map - this.channelMap[msg.chanId] = data - debug('Emitting \'subscribed\' %j', data) - /** - * @event BitfinexWS#subscribed - * @type {object} - * @property {string} channel - Channel type - * @property {string} pair - Currency pair. - * @property {number} chanId - Channel ID sended by Bitfinex - */ - this.emit('subscribed', data) - } else if (msg.event === 'auth' && msg.status !== 'OK') { - this.emit('error', msg) - debug('Emitting \'error\' %j', msg) - } else if (msg.event === 'auth') { - this.channelMap[msg.chanId] = { - channel: 'auth' - } - debug('Emitting \'%s\' %j', msg.event, msg) - /** - * @event BitfinexWS#auth - */ - this.emit(msg.event, msg) - } else { - debug('Emitting \'%s\' %j', msg.event, msg) - this.emit(msg.event, msg) - } - } else { - debug('Received data from a channel') - // First element of Array is the channelId, the rest is the info. - const channelId = msg.shift() // Pop the first element - const event = this.channelMap[channelId] - if (event) { - debug('Message in \'%s\' channel', event.channel) - if (event.channel === 'book') { - this._processBookEvent(msg, event) - } else if (event.channel === 'trades') { - this._processTradeEvent(msg, event) - } else if (event.channel === 'ticker') { - this._processTickerEvent(msg, event) - } else if (event.channel === 'auth') { - this._processUserEvent(msg) - } else { - debug('Message in unknown channel') - } - } - } -} - -BitfinexWS.prototype._processUserEvent = function (msg) { - if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in user channel') - } else { - const event = msg[0] - const data = msg[1] - if (Array.isArray(data[0])) { - data[0].forEach((ele) => { - debug('Emitting \'%s\' %j', event, ele) - this.emit(event, ele) - }) - } else if (data.length) { - debug('Emitting \'%s\', %j', event, data) - /** - * position snapshot - * @event BitfinexWS#ps - */ - /** - * new position - * @event BitfinexWS#pn - */ - /** - * position update - * @event BitfinexWS#pu - */ - /** - * position close - * @event BitfinexWS#pc - */ - /** - * wallet snapshot - * @event BitfinexWS#ws - */ - /** - * wallet snapshot - * @event BitfinexWS#ws - */ - /** - * order snapshot - * @event BitfinexWS#os - */ - /** - * new order - * @event BitfinexWS#on - */ - /** - * order update - * @event BitfinexWS#ou - */ - /** - * order cancel - * @event BitfinexWS#oc - */ - /** - * trade executed - * @event BitfinexWS#te - */ - /** - * trade execution update - * @event BitfinexWS#tu - */ - // TODO: send Object with key: values - this.emit(event, data) - } - } -} - -BitfinexWS.prototype._processTickerEvent = function (msg, event) { - if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in %s ticker channel', event.pair) - } else if (msg.length > 9) { // Update - const update = { - bid: msg[0], - bidSize: msg[1], - ask: msg[2], - askSize: msg[3], - dailyChange: msg[4], - dailyChangePerc: msg[5], - lastPrice: msg[6], - volume: msg[7], - high: msg[8], - low: msg[9] - } - debug('Emitting ticker, %s, %j', event.pair, update) - /** - * @event BitfinexWS#ticker - * @type {string} - * @type {object} - * @property {number} bid - * @property {number} bidSize - * @property {number} ask - * @property {number} askSize - * @property {number} dailyChange - * @property {number} dailyChangePerc - * @property {number} lastPrice - * @property {number} volume - * @property {number} high - * @property {number} low - */ - this.emit('ticker', event.pair, update) - } -} - -BitfinexWS.prototype._processTradeEvent = function (msg, event) { - // Snapshot - if (Array.isArray(msg[0])) { - msg[0].forEach((update) => { - update = { - seq: update[0], - timestamp: update[1], - price: update[2], - amount: update[3] - } - debug('Emitting trade, %s, %j', event.pair, update) - this.emit('trade', event.pair, update) - }) - } else if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in %s trade channel', event.pair) - } else if (msg[0] === 'te') { // Trade executed - const update = { - seq: msg[1], - timestamp: msg[2], - price: msg[3], - amount: msg[4] - } - debug('Emitting trade, %s, %j', event.pair, update) - /** - * @event BitfinexWS#trade - * @type {string} - * @type {object} - * @property {string} seq - * @property {number} timestamp - * @property {number} price - * @property {number} amount - * @see http://docs.bitfinex.com/#trades75 - */ - this.emit('trade', event.pair, update) - } else if (msg[0] === 'tu') { // Trade executed - const update = { - seq: msg[1], - id: msg[2], - timestamp: msg[3], - price: msg[4], - amount: msg[5] - } - debug('Emitting trade, %s, %j', event.pair, update) - /** - * @event BitfinexWS#trade - * @type {string} - * @type {object} - * @property {string} seq - * @property {number} id - * @property {number} timestamp - * @property {number} price - * @property {number} amount - * @see http://docs.bitfinex.com/#trades75 - */ - this.emit('trade', event.pair, update) - } -} - -BitfinexWS.prototype._processBookEvent = function (msg, event) { - // Snapshot - if (Array.isArray(msg[0])) { - msg[0].forEach((update) => { - update = { - price: update[0], - count: update[1], - amount: update[2] - } - debug('Emitting orderbook, %s, %j', event.pair, update) - this.emit('orderbook', event.pair, update) - }) - } else if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in %s book channel', event.pair) - } else if (msg.length > 2) { // Update - const update = { - price: msg[0], - count: msg[1], - amount: msg[2] - } - debug('Emitting orderbook, %s, %j', event.pair, update) - /** - * @event BitfinexWS#orderbook - * @type {string} - * @type {object} - * @property {string} price - * @property {number} count - * @property {number} amount - * @see http://docs.bitfinex.com/#order-books - */ - this.emit('orderbook', event.pair, update) - } -} - -BitfinexWS.prototype.close = function () { - this.ws.close() -} - -BitfinexWS.prototype.onOpen = function () { - this.channelMap = {} // Map channels IDs to events - this.emit('open') -} - -BitfinexWS.prototype.onError = function (error) { - this.emit('error', error) -} - -BitfinexWS.prototype.onClose = function () { - this.emit('close') -} - -BitfinexWS.prototype.send = function (msg) { - debug('Sending %j', msg) - this.ws.send(JSON.stringify(msg)) -} - -/** - * Subscribe to Order book updates. Snapshot will be sended as multiple updates. - * Event will be emited as `PAIRNAME_book`. - * @param {string} [pair] BTCUSD, LTCUSD or LTCBTC. Default BTCUSD - * @param {string} [precision] Level of price aggregation (P0, P1, P2, P3). - * The default is P0. - * @param {string} [length] Number of price points. 25 (default) or 100. - * @see http://docs.bitfinex.com/#order-books - */ -BitfinexWS.prototype.subscribeOrderBook = - function (pair = 'BTCUSD', precision = 'P0', length = '25') { - this.send({ - event: 'subscribe', - channel: 'book', - pair, - prec: precision, - len: length - }) - } - -/** - * Subscribe to trades. Snapshot will be sended as multiple updates. - * Event will be emited as `PAIRNAME_trades`. - * @param {string} [pair] BTCUSD, LTCUSD or LTCBTC. Default BTCUSD - * @see http://docs.bitfinex.com/#trades75 - */ -BitfinexWS.prototype.subscribeTrades = function (pair = 'BTCUSD') { - this.send({ - event: 'subscribe', - channel: 'trades', - pair - }) -} - -/** - * Subscribe to ticker updates. The ticker is a high level overview of the state - * of the market. It shows you the current best bid and ask, as well as the last - * trade price. - * - * Event will be emited as `PAIRNAME_ticker`. - * @param {string} [pair] BTCUSD, LTCUSD or LTCBTC. Default BTCUSD - * @see http://docs.bitfinex.com/#ticker76 - */ -BitfinexWS.prototype.subscribeTicker = function (pair = 'BTCUSD') { - this.send({ - event: 'subscribe', - channel: 'ticker', - pair - }) -} - -/** - * Unsubscribe to a channel. - * @param {number} chanId ID of the channel received on `subscribed` event. - */ -BitfinexWS.prototype.unsubscribe = function (chanId) { - this.send({ - event: 'unsubscribe', - chanId - }) -} - -/** - * Autenticate the user. Will receive executed traded updates. - * @see http://docs.bitfinex.com/#wallet-updates - */ -BitfinexWS.prototype.auth = function () { - const payload = 'AUTH' + (new Date().getTime()) - const signature = crypto.createHmac('sha384', this.APISecret) - .update(payload) - .digest('hex') - this.send({ - event: 'auth', - apiKey: this.APIKey, - authSig: signature, - authPayload: payload - }) -} - -module.exports = BitfinexWS diff --git a/ws2.js b/ws2.js deleted file mode 100644 index bb17a007..00000000 --- a/ws2.js +++ /dev/null @@ -1,247 +0,0 @@ -'use strict' - -const {EventEmitter} = require('events') -const debug = require('debug')('bitfinex:ws') -const crypto = require('crypto') -const WebSocket = require('ws') - -/** - * Handles communitaction with Bitfinex WebSocket API. - * @param {string} APIKey - * @param {string} APISecret - * @event - * @class - */ -class BitfinexWS2 extends EventEmitter { - constructor (apiKey, apiSecret, opts = {}) { - super() - this.apiKey = apiKey - this.apiSecret = apiSecret - this.websocketURI = opts.websocketURI || 'wss://api.bitfinex.com/ws/2' - } - - open () { - this.ws = new WebSocket(this.websocketURI) - this.ws.on('message', this.onMessage.bind(this)) - this.ws.on('open', this.onOpen.bind(this)) - this.ws.on('error', this.onError.bind(this)) - this.ws.on('close', this.onClose.bind(this)) - } - - onMessage (msg, flags) { - msg = JSON.parse(msg) - debug('Received message: %j', msg) - debug('Emited message event') - this.emit('message', msg, flags) - - if (!Array.isArray(msg) && msg.event) { - if (msg.event === 'subscribed') { - debug('Subscription report received') - // Inform the user the new event name that will be triggered - const data = { - channel: msg.channel, - chanId: msg.chanId, - symbol: msg.symbol - } - // Save to event map - this.channelMap[msg.chanId] = data - debug('Emitting \'subscribed\' %j', data) - /** - * @event BitfinexWS#subscribed - * @type {object} - * @property {string} channel - Channel type - * @property {string} symbol - Symbol of the asset in question (either a trading pair or a funding currency) - * @property {number} chanId - Channel ID sended by Bitfinex - */ - this.emit('subscribed', data) - } else if (msg.event === 'auth' && msg.status !== 'OK') { - this.emit('error', msg) - debug('Emitting \'error\' %j', msg) - } else if (msg.event === 'auth') { - this.channelMap[msg.chanId] = { - channel: 'auth' - } - debug('Emitting \'%s\' %j', msg.event, msg) - /** - * @event BitfinexWS#auth - */ - this.emit(msg.event, msg) - } else { - debug('Emitting \'%s\' %j', msg.event, msg) - this.emit(msg.event, msg) - } - } else { - debug('Received data from a channel') - // First element of Array is the channelId, the rest is the info. - const channelId = msg.shift() // Pop the first element - const event = this.channelMap[channelId] - if (event) { - debug('Message in \'%s\' channel', event.channel) - if (event.channel === 'book') { - this._processBookEvent(msg, event) - } else if (event.channel === 'trades') { - this._processTradeEvent(msg, event) - } else if (event.channel === 'ticker') { - this._processTickerEvent(msg, event) - } else if (event.channel === 'auth') { - this._processUserEvent(msg) - } else { - debug('Message in unknown channel') - } - } - } - } - - _processUserEvent (msg) { - if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in user channel') - } else { - let event = msg[0] - const data = msg[1] - // Snapshot - if (Array.isArray(data[0])) { - data.forEach((ele) => { - debug('Emitting notification \'%s\' %j', event, ele) - this.emit(event, ele) - }) - } else if (event === 'n') { // Notification - event = data[1] - this.emit(event, data) - debug('Emitting \'%s\', %j', event, data) - } else if (data.length) { // Update - debug('Emitting \'%s\', %j', event, data) - this.emit(event, data) - } - } - } - - _processTickerEvent (msg, event) { - if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in %s ticker channel', event.symbol) - } else { - msg = msg[0] - debug('Emitting ticker, %s, %j', event.symbol, msg) - this.emit('ticker', event.symbol, msg) - } - } - - _processBookEvent (msg, event) { - if (Array.isArray(msg[0])) { - msg[0].forEach((bookLevel) => { - debug('Emitting orderbook, %s, %j', event.symbol, bookLevel) - this.emit('orderbook', event.symbol, bookLevel) - }) - } else if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in %s book channel', event.symbol) - } else if (msg.length > 2) { - debug('Emitting orderbook, %s, %j', event.symbol, msg) - this.emit('orderbook', event.symbol, msg) - } - } - - _processTradeEvent (msg, event) { - if (Array.isArray(msg[0])) { - msg[0].forEach((trade) => { - debug('Emitting trade, %s, %j', event.symbol, trade) - this.emit('trade', event.symbol, trade) - }) - } else if (msg[0] === 'hb') { // HeatBeart - debug('Received HeatBeart in %s trade channel', event.symbol) - } else if (msg[0] === 'te') { // Trade executed - debug('Emitting trade, %s, %j', event.symbol, msg) - this.emit('trade', event.symbol, msg) - } else if (msg[0] === 'tu') { // Trade executed - debug('Emitting trade, %s, %j', event.symbol, msg) - this.emit('trade', event.symbol, msg) - } - } - - close () { - this.ws.close() - } - - onOpen () { - this.channelMap = {} // Map channels IDs to events - debug('Connection opening, emitting open') - this.emit('open') - } - - onError (error) { - this.emit('error', error) - } - - onClose () { - this.emit('close') - } - - send (msg) { - debug('Sending %j', msg) - this.ws.send(JSON.stringify(msg)) - } - - subscribeOrderBook (symbol = 'tBTCUSD', precision = 'P0', length = '25') { - this.send({ - event: 'subscribe', - channel: 'book', - symbol, - len: length, - prec: precision - }) - } - - subscribeTrades (symbol = 'BTCUSD') { - this.send({ - event: 'subscribe', - channel: 'trades', - symbol - }) - } - - subscribeTicker (symbol = 'tBTCUSD') { - this.send({ - event: 'subscribe', - channel: 'ticker', - symbol - }) - } - - unsubscribe (chanId) { - this.send({ - event: 'unsubscribe', - chanId - }) - } - - submitOrder (order) { - this.send(order) - } - - cancelOrder (orderId) { - this.send([0, 'oc', null, { - id: orderId - }]) - } - - config (flags) { - this.send({ - flags, - 'event': 'conf' - }) - } - - auth (calc = 0) { - const authNonce = (new Date()).getTime() * 1000 - const payload = 'AUTH' + authNonce + authNonce - const signature = crypto.createHmac('sha384', this.apiSecret).update(payload).digest('hex') - this.send({ - event: 'auth', - apiKey: this.apiKey, - authSig: signature, - authPayload: payload, - authNonce: +authNonce + 1, - calc - }) - } -} - -module.exports = BitfinexWS2 diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index c1f8e86e..00000000 --- a/yarn.lock +++ /dev/null @@ -1,2148 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -abbrev@1, abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" - dependencies: - acorn "^3.0.4" - -acorn@^3.0.4, acorn@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" - -acorn@^5.0.1: - version "5.0.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" - -ajv-keywords@^1.0.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" - -ajv@^4.7.0, ajv@^4.9.1: - version "4.11.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.5.tgz#b6ee74657b993a01dce44b7944d56f485828d5bd" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - -ansi-escape-sequences@^2.2.1, ansi-escape-sequences@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-2.2.2.tgz#174c78d6f8b7de75f8957ae81c7f72210c701635" - dependencies: - array-back "^1.0.2" - collect-all "~0.2.1" - -ansi-escape-sequences@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz#1c18394b6af9b76ff9a63509fa497669fd2ce53e" - dependencies: - array-back "^1.0.3" - -ansi-escapes@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -app-usage-stats@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/app-usage-stats/-/app-usage-stats-0.4.1.tgz#97eb9b89b5678fa2ddc9793b1298628cc218429f" - dependencies: - array-back "^1.0.4" - core-js "^2.4.1" - feature-detect-es6 "^1.3.1" - home-path "^1.0.3" - test-value "^2.1.0" - usage-stats "^0.8.2" - -argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" - dependencies: - sprintf-js "~1.0.2" - -array-back@^1.0.2, array-back@^1.0.3, array-back@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b" - dependencies: - typical "^2.6.0" - -array-tools@^1.0.6, array-tools@^1.1.0, array-tools@^1.1.4, array-tools@^1.8.4: - version "1.8.6" - resolved "https://registry.yarnpkg.com/array-tools/-/array-tools-1.8.6.tgz#145771f7f9c94e98cc5ea4196a99b8323aee18ae" - dependencies: - object-tools "^1.6.1" - typical "^2.1" - -array-tools@^2: - version "2.0.9" - resolved "https://registry.yarnpkg.com/array-tools/-/array-tools-2.0.9.tgz#5a511de7a41be0eec9ffdcd4912d0af9f0caca35" - dependencies: - ansi-escape-sequences "^2.2.2" - array-back "^1.0.2" - collect-json "^1.0.7" - filter-where "^1.0.1" - object-get "^2.0.0" - reduce-extract "^1.0.0" - reduce-flatten "^1.0.0" - reduce-unique "^1.0.0" - reduce-without "^1.0.0" - sort-array "^1.0.0" - test-value "^1.0.1" - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - -asap@~2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - -assertion-error@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" - -async@1.x, async@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - -async@~0.2.6: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - -babel-code-frame@^6.16.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^3.0.0" - -babel-polyfill@^6.13.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" - dependencies: - babel-runtime "^6.22.0" - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - -babel-runtime@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - -balanced-match@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - dependencies: - tweetnacl "^0.14.3" - -bindings@1.2.x, bindings@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" - -bluebird@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" - -bluebird@~3.4.6: - version "3.4.7" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - -brace-expansion@^1.0.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - -buffer-shims@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" - -bufferutil@1.2.x: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-1.2.1.tgz#37be5d36e1e06492221e68d474b1ac58e510cbd7" - dependencies: - bindings "1.2.x" - nan "^2.0.5" - -cache-point@~0.3.3: - version "0.3.4" - resolved "https://registry.yarnpkg.com/cache-point/-/cache-point-0.3.4.tgz#152db502c6bb23b5aa3f663e230d5de8ec4e4f3f" - dependencies: - array-back "^1.0.3" - core-js "^2.4.1" - feature-detect-es6 "^1.3.1" - fs-then-native "^1.0.2" - mkdirp "~0.5.1" - -caller-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" - dependencies: - callsites "^0.2.0" - -callsites@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -catharsis@~0.8.8: - version "0.8.8" - resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.8.tgz#693479f43aac549d806bd73e924cd0d944951a06" - dependencies: - underscore-contrib "~0.3.0" - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chai@^3.4.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" - dependencies: - assertion-error "^1.0.1" - deep-eql "^0.1.3" - type-detect "^1.0.0" - -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -circular-json@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" - -cli-commands@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/cli-commands/-/cli-commands-0.1.0.tgz#c57cacc406bbcf9ee21646607161ed432ef5a05a" - dependencies: - ansi-escape-sequences "^3.0.0" - command-line-args "^3.0.1" - command-line-commands "^1.0.4" - command-line-usage "^3.0.5" - -cli-cursor@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - dependencies: - restore-cursor "^1.0.1" - -cli-width@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - -collect-all@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-all/-/collect-all-1.0.2.tgz#39450f1e7aa6086570a006bce93ccf1218a77ea1" - dependencies: - stream-connect "^1.0.2" - stream-via "^1.0.3" - -collect-all@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/collect-all/-/collect-all-0.2.1.tgz#7225fb4585c22d4ffac886f0abaf5abc563a1a6a" - dependencies: - stream-connect "^1.0.1" - stream-via "~0.1.0" - typical "^2.3.0" - -collect-json@^1.0.1, collect-json@^1.0.7, collect-json@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/collect-json/-/collect-json-1.0.8.tgz#aa2fa52b4d1d9444ce690f07a1e3617ab74bb827" - dependencies: - collect-all "^1.0.2" - stream-connect "^1.0.2" - stream-via "^1.0.3" - -column-layout@^2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/column-layout/-/column-layout-2.1.4.tgz#ed2857092ccf8338026fe538379d9672d70b3641" - dependencies: - ansi-escape-sequences "^2.2.2" - array-back "^1.0.3" - collect-json "^1.0.8" - command-line-args "^2.1.6" - core-js "^2.4.0" - deep-extend "~0.4.1" - feature-detect-es6 "^1.2.0" - object-tools "^2.0.6" - typical "^2.4.2" - wordwrapjs "^1.2.0" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" - -command-line-args@^2.1.4, command-line-args@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-2.1.6.tgz#f197d6eaff34c9085577484b2864375b294f5697" - dependencies: - array-back "^1.0.2" - command-line-usage "^2" - core-js "^2.0.1" - feature-detect-es6 "^1.2.0" - find-replace "^1" - typical "^2.3.0" - -command-line-args@^3.0.0, command-line-args@^3.0.1: - version "3.0.5" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-3.0.5.tgz#5bd4ad45e7983e5c1344918e40280ee2693c5ac0" - dependencies: - array-back "^1.0.4" - feature-detect-es6 "^1.3.1" - find-replace "^1.0.2" - typical "^2.6.0" - -command-line-commands@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/command-line-commands/-/command-line-commands-1.0.4.tgz#034f9b167b5188afbdcf6b2efbb150fc8442c32b" - dependencies: - array-back "^1.0.3" - feature-detect-es6 "^1.3.1" - -command-line-tool@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.1.0.tgz#91a11ba48ac63a4a687554367980f7c6423c149d" - dependencies: - ansi-escape-sequences "^2.2.1" - array-back "^1.0.2" - -command-line-tool@~0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.5.2.tgz#f87d6977f56bbdd2d5dfcf946345dd2cd9c6a53a" - dependencies: - ansi-escape-sequences "^2.2.2" - array-back "^1.0.3" - command-line-args "^3.0.0" - command-line-usage "^3.0.3" - feature-detect-es6 "^1.3.0" - typical "^2.4.2" - -command-line-usage@^2: - version "2.0.5" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-2.0.5.tgz#f80c35ca5e8624841923ea3be3b9bfbf4f7be27b" - dependencies: - ansi-escape-sequences "^2.2.2" - array-back "^1.0.3" - column-layout "^2.1.1" - feature-detect-es6 "^1.2.0" - typical "^2.4.2" - wordwrapjs "^1.2.0" - -command-line-usage@^3.0.3, command-line-usage@^3.0.5: - version "3.0.8" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-3.0.8.tgz#b6a20978c1b383477f5c11a529428b880bfe0f4d" - dependencies: - ansi-escape-sequences "^3.0.0" - array-back "^1.0.3" - feature-detect-es6 "^1.3.1" - table-layout "^0.3.0" - typical "^2.6.0" - -commander@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" - -commander@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" - -common-sequence@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/common-sequence/-/common-sequence-1.0.2.tgz#30e07f3f8f6f7f9b3dee854f20b2d39eee086de8" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -concat-stream@^1.5.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -config-master@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/config-master/-/config-master-2.0.4.tgz#e749505c5d3f946f2fad3c76dfe71fca689751dc" - dependencies: - babel-polyfill "^6.13.0" - feature-detect-es6 "^1.3.1" - walk-back "^2.0.1" - -core-js@^2.0.1, core-js@^2.4.0, core-js@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - -d@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" - dependencies: - es5-ext "^0.10.9" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -ddata@~0.1.25: - version "0.1.28" - resolved "https://registry.yarnpkg.com/ddata/-/ddata-0.1.28.tgz#53138fafa3f01749ea2451d12b6b6dd9df1d5b1f" - dependencies: - array-back "^1.0.3" - core-js "^2.4.1" - handlebars "^3.0.3" - marked "~0.3.6" - object-get "^2.1.0" - reduce-flatten "^1.0.1" - string-tools "^1.0.0" - test-value "^2.0.0" - -debug@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - -debug@^2.1.1, debug@^2.2.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" - dependencies: - ms "0.7.2" - -decamelize@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -deep-eql@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" - dependencies: - type-detect "0.1.1" - -deep-extend@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - -defer-promise@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/defer-promise/-/defer-promise-1.0.1.tgz#1ca6ffeddbcef1715dd7aae25c7616f9ae22932f" - -del@^2.0.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" - dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" - -dmd@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/dmd/-/dmd-1.4.2.tgz#b1304b98a5700a6bfe5dcf91be657c981700a4bc" - dependencies: - array-back "^1.0.3" - command-line-tool "~0.5.0" - common-sequence "^1.0.2" - ddata "~0.1.25" - file-set "^1.0.0" - handlebars-array "^0.2.0" - handlebars-comparison "^2.0.0" - handlebars-json "^1.0.0" - handlebars-regexp "^1.0.0" - handlebars-string "^2.0.1" - object-tools "^2.0.6" - reduce-unique "^1.0.0" - reduce-without "^1.0.1" - stream-handlebars "~0.1.6" - string-tools "^1.0.0" - walk-back "^2.0.1" - -doctrine@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.15" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6" - dependencies: - es6-iterator "2" - es6-symbol "~3.1" - -es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" - dependencies: - d "1" - es5-ext "^0.10.14" - es6-symbol "^3.1" - -es6-map@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - -es6-set@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - -es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-weak-map@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" - dependencies: - d "1" - es5-ext "^0.10.14" - es6-iterator "^2.0.1" - es6-symbol "^3.1.1" - -escape-string-regexp@1.0.2, escape-string-regexp@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" - -escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -escodegen@1.8.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" - dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint@^3.19.0: - version "3.19.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" - dependencies: - babel-code-frame "^6.16.0" - chalk "^1.1.3" - concat-stream "^1.5.2" - debug "^2.1.1" - doctrine "^2.0.0" - escope "^3.6.0" - espree "^3.4.0" - esquery "^1.0.0" - estraverse "^4.2.0" - esutils "^2.0.2" - file-entry-cache "^2.0.0" - glob "^7.0.3" - globals "^9.14.0" - ignore "^3.2.0" - imurmurhash "^0.1.4" - inquirer "^0.12.0" - is-my-json-valid "^2.10.0" - is-resolvable "^1.0.0" - js-yaml "^3.5.1" - json-stable-stringify "^1.0.0" - levn "^0.3.0" - lodash "^4.0.0" - mkdirp "^0.5.0" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.1" - pluralize "^1.2.1" - progress "^1.1.8" - require-uncached "^1.0.2" - shelljs "^0.7.5" - strip-bom "^3.0.0" - strip-json-comments "~2.0.1" - table "^3.7.8" - text-table "~0.2.0" - user-home "^2.0.0" - -espree@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.1.tgz#28a83ab4aaed71ed8fe0f5efe61b76a05c13c4d2" - dependencies: - acorn "^5.0.1" - acorn-jsx "^3.0.0" - -espree@~3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.1.7.tgz#fd5deec76a97a5120a9cd3a7cb1177a0923b11d2" - dependencies: - acorn "^3.3.0" - acorn-jsx "^3.0.0" - -esprima@2.7.x, esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - -esprima@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - -esquery@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" - dependencies: - estraverse "^4.0.0" - -esrecurse@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" - dependencies: - estraverse "~4.1.0" - object-assign "^4.0.1" - -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - -estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - -estraverse@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - dependencies: - d "1" - es5-ext "~0.10.14" - -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - -extend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" - -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" - -fast-levenshtein@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - -feature-detect-es6@^1.2.0, feature-detect-es6@^1.3.0, feature-detect-es6@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz#f888736af9cb0c91f55663bfa4762eb96ee7047f" - dependencies: - array-back "^1.0.3" - -figures@^1.3.5: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" - dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" - -file-set@^1.0.0, file-set@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/file-set/-/file-set-1.1.1.tgz#d3ec70c080ec8f18f204ba1de106780c9056926b" - dependencies: - array-back "^1.0.3" - glob "^7.1.0" - -file-set@~0.2.1: - version "0.2.8" - resolved "https://registry.yarnpkg.com/file-set/-/file-set-0.2.8.tgz#73a6571e9cbe51ac5926c88bd567d111f836f178" - dependencies: - array-tools "^2" - glob "^4" - -filter-where@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/filter-where/-/filter-where-1.0.1.tgz#1b042569edce36bc1c4e9f73740d2c4e2feef77d" - dependencies: - test-value "^1.0.1" - -find-replace@^1, find-replace@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" - dependencies: - array-back "^1.0.4" - test-value "^2.1.0" - -flat-cache@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" - dependencies: - circular-json "^0.3.1" - del "^2.0.2" - graceful-fs "^4.1.2" - write "^0.2.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -fs-then-native@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fs-then-native/-/fs-then-native-1.0.2.tgz#ac8d3807c9f1bbd1279607fb228e0ab649bb41fe" - dependencies: - feature-detect-es6 "^1.3.1" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - dependencies: - is-property "^1.0.0" - -getpass@^0.1.1: - version "0.1.6" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" - dependencies: - assert-plus "^1.0.0" - -glob@3.2.11: - version "3.2.11" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" - dependencies: - inherits "2" - minimatch "0.3" - -glob@^4: - version "4.5.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "^2.0.1" - once "^1.3.0" - -glob@^5.0.15: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^9.14.0: - version "9.17.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" - -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.9: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" - -handlebars-array@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/handlebars-array/-/handlebars-array-0.2.1.tgz#dd58395a5261d661988e8d77520ebbfaadc6bd24" - dependencies: - array-tools "^1.1.4" - -handlebars-comparison@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handlebars-comparison/-/handlebars-comparison-2.0.1.tgz#b17b95d2c298578e4aead38f5fac46e8f6005855" - dependencies: - array-tools "^1.1.0" - -handlebars-json@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/handlebars-json/-/handlebars-json-1.0.1.tgz#2ef87bb782551cd645bb4691b824e9653ec02504" - -handlebars-regexp@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/handlebars-regexp/-/handlebars-regexp-1.0.1.tgz#5f47f067260e9ba8e52f1a280917f70de39f11e4" - -handlebars-string@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/handlebars-string/-/handlebars-string-2.0.2.tgz#b9f92208a979cfcf51ff4a90defa183dc62942ca" - dependencies: - array-tools "^1.0.6" - string-tools "^0.1.4" - -handlebars@^3.0.0, handlebars@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-3.0.3.tgz#0e09651a2f0fb3c949160583710d551f92e6d2ad" - dependencies: - optimist "^0.6.1" - source-map "^0.1.40" - optionalDependencies: - uglify-js "~2.3" - -handlebars@^4.0.1: - version "4.0.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - -home-path@^1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/home-path/-/home-path-1.0.5.tgz#788b29815b12d53bacf575648476e6f9041d133f" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -ignore@^3.2.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.6.tgz#26e8da0644be0bb4cb39516f6c79f0e0f4ffe48c" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -inquirer@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" - dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" - cli-width "^2.0.0" - figures "^1.3.5" - lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" - through "^2.3.6" - -interpret@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.2.tgz#f4f623f0bb7122f15f5717c8e254b8161b5c5b2d" - -is-buffer@^1.0.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - -is-my-json-valid@^2.10.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - -is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" - dependencies: - path-is-inside "^1.0.1" - -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - -is-resolvable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" - dependencies: - tryit "^1.0.1" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -istanbul@^0.4.1: - version "0.4.5" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - glob "^5.0.15" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" - -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" - dependencies: - commander "0.6.1" - mkdirp "0.3.0" - -jodid25519@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" - dependencies: - jsbn "~0.1.0" - -js-tokens@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" - -js-yaml@3.x, js-yaml@^3.5.1: - version "3.8.3" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" - dependencies: - argparse "^1.0.7" - esprima "^3.1.1" - -js2xmlparser@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-1.0.0.tgz#5a170f2e8d6476ce45405e04823242513782fe30" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -jsdoc-75lb@^3.5.6: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jsdoc-75lb/-/jsdoc-75lb-3.6.0.tgz#a807119528b4009ccbcab49b7522f63fec6cd0bd" - dependencies: - bluebird "~3.4.6" - catharsis "~0.8.8" - escape-string-regexp "~1.0.5" - espree "~3.1.7" - js2xmlparser "~1.0.0" - klaw "~1.3.0" - marked "~0.3.6" - mkdirp "~0.5.1" - requizzle "~0.2.1" - strip-json-comments "~2.0.1" - taffydb "2.6.2" - underscore "~1.8.3" - -jsdoc-api@^1.0.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/jsdoc-api/-/jsdoc-api-1.2.4.tgz#5012235927bfad1e27bc88d07b0ddddb2d3a8a59" - dependencies: - array-back "^1.0.3" - cache-point "~0.3.3" - collect-all "^1.0.2" - core-js "^2.4.1" - feature-detect-es6 "^1.3.1" - file-set "^1.0.1" - jsdoc-75lb "^3.5.6" - object-to-spawn-args "^1.1.0" - promise.prototype.finally "^1.0.1" - temp-path "^1.0.0" - then-fs "^2.0.0" - walk-back "^2.0.1" - -jsdoc-parse@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/jsdoc-parse/-/jsdoc-parse-1.2.7.tgz#54b7481b3cd6bcb7c173dc4fa69ee92735ea2525" - dependencies: - ansi-escape-sequences "^2.2.1" - array-tools "^2" - collect-json "^1.0.1" - command-line-args "^2.1.4" - command-line-tool "^0.1.0" - core-js "^2.0.1" - feature-detect-es6 "^1.2.0" - file-set "~0.2.1" - jsdoc-api "^1.0.0" - object-tools "^2" - stream-connect "^1.0.1" - test-value "^1.0.1" - -jsdoc-to-markdown@^1.3.1: - version "1.3.9" - resolved "https://registry.yarnpkg.com/jsdoc-to-markdown/-/jsdoc-to-markdown-1.3.9.tgz#774c0ece0ebd0bcc3261b2c9a2aa8d1399a61472" - dependencies: - ansi-escape-sequences "^3.0.0" - command-line-args "^3.0.1" - command-line-usage "^3.0.5" - config-master "^2.0.4" - dmd "^1.4.1" - jsdoc-parse "^1.2.7" - jsdoc2md-stats "^1.0.3" - object-tools "^2.0.6" - stream-connect "^1.0.2" - -jsdoc2md-stats@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/jsdoc2md-stats/-/jsdoc2md-stats-1.0.6.tgz#dc0e002aebbd0fbae5123534f92732afbc651fbf" - dependencies: - app-usage-stats "^0.4.0" - feature-detect-es6 "^1.3.1" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - -jsprim@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" - dependencies: - assert-plus "1.0.0" - extsprintf "1.0.2" - json-schema "0.2.3" - verror "1.3.6" - -kind-of@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47" - dependencies: - is-buffer "^1.0.2" - -klaw@~1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - optionalDependencies: - graceful-fs "^4.1.9" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - -lodash@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.3.0: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - -marked@~0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" - -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" - -mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" - dependencies: - mime-db "~1.27.0" - -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" - dependencies: - lru-cache "2" - sigmund "~1.0.0" - -"minimatch@2 || 3", minimatch@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" - dependencies: - brace-expansion "^1.0.0" - -minimatch@^2.0.1: - version "2.0.10" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" - dependencies: - brace-expansion "^1.0.0" - -minimist@0.0.8, minimist@~0.0.1: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mocha@^2.3.4: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" - dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" - growl "1.9.2" - jade "0.26.3" - mkdirp "0.5.1" - supports-color "1.2.0" - to-iso-string "0.0.2" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" - -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - -nan@^2.0.5, nan@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - -nopt@3.x: - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - dependencies: - abbrev "1" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -oauth-sign@~0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -object-get@^2.0.0, object-get@^2.0.2, object-get@^2.0.4, object-get@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/object-get/-/object-get-2.1.0.tgz#722bbdb60039efa47cad3c6dc2ce51a85c02c5ae" - -object-to-spawn-args@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object-to-spawn-args/-/object-to-spawn-args-1.1.0.tgz#031a200e37db2c3dfc9b98074a0d69a5be253c1c" - -object-tools@^1.2.1, object-tools@^1.6.1: - version "1.6.7" - resolved "https://registry.yarnpkg.com/object-tools/-/object-tools-1.6.7.tgz#52d400fc875250993dbbb3ba298d7c79bb0698d0" - dependencies: - array-tools "^1.8.4" - typical "^2.2" - -object-tools@^2, object-tools@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/object-tools/-/object-tools-2.0.6.tgz#f3fe1c350cda4a6f5d99d9646dc4892a02476ddd" - dependencies: - array-back "^1.0.2" - collect-json "^1.0.7" - object-get "^2.0.2" - test-value "^1.1.0" - typical "^2.4.2" - -once@1.x, once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optimist@~0.3.5: - version "0.3.7" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" - dependencies: - wordwrap "~0.0.2" - -optionator@^0.8.1, optionator@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - -pluralize@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - -promise.prototype.finally@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-1.0.1.tgz#91182f91c92486995740fa05e0da942ac986befa" - -"promise@>=3.2 <8": - version "7.1.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" - dependencies: - asap "~2.0.3" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -readable-stream@^2.2.2: - version "2.2.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.6.tgz#8b43aed76e71483938d12a8d46c6cf1a00b1f816" - dependencies: - buffer-shims "^1.0.0" - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - dependencies: - resolve "^1.1.6" - -reduce-extract@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/reduce-extract/-/reduce-extract-1.0.0.tgz#67f2385beda65061b5f5f4312662e8b080ca1525" - dependencies: - test-value "^1.0.1" - -reduce-flatten@^1.0.0, reduce-flatten@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" - -reduce-unique@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/reduce-unique/-/reduce-unique-1.0.0.tgz#7e586bcf87a4e32b6d7abd8277fad6cdec9f4803" - -reduce-without@^1.0.0, reduce-without@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/reduce-without/-/reduce-without-1.0.1.tgz#68ad0ead11855c9a37d4e8256c15bbf87972fc8c" - dependencies: - test-value "^2.0.0" - -regenerator-runtime@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz#8c4367a904b51ea62a908ac310bf99ff90a82a3e" - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - -req-then@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/req-then/-/req-then-0.5.1.tgz#31c6e0b56f4ddd2acd6de0ba1bcea77b6079dfdf" - dependencies: - array-back "^1.0.3" - defer-promise "^1.0.0" - feature-detect-es6 "^1.3.1" - lodash.pick "^4.4.0" - typical "^2.6.0" - -request-promise-core@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" - dependencies: - lodash "^4.13.1" - -request-promise@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.0.tgz#684f77748d6b4617bee6a4ef4469906e6d074720" - dependencies: - bluebird "^3.5.0" - request-promise-core "1.1.1" - stealthy-require "^1.0.0" - -request@^2.67.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -require-uncached@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" - dependencies: - caller-path "^0.1.0" - resolve-from "^1.0.0" - -requizzle@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde" - dependencies: - underscore "~1.6.0" - -resolve-from@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" - -resolve@1.1.x, resolve@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - dependencies: - align-text "^0.1.1" - -rimraf@^2.2.8: - version "2.6.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" - dependencies: - glob "^7.0.5" - -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - dependencies: - once "^1.3.0" - -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" - -safe-buffer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" - -shelljs@^0.7.5: - version "0.7.7" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -sort-array@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/sort-array/-/sort-array-1.1.1.tgz#9032f6f0be284eecb12af98a3db02612828a66d1" - dependencies: - array-back "^1.0.3" - object-get "^2.0.4" - typical "^2.4.2" - -source-map@^0.1.40, source-map@~0.1.7: - version "0.1.43" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - dependencies: - amdefine ">=0.0.4" - -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - dependencies: - amdefine ">=0.0.4" - -source-map@~0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -sshpk@^1.7.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.11.0.tgz#2d8d5ebb4a6fab28ffba37fa62a90f4a3ea59d77" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jodid25519 "^1.0.0" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -stealthy-require@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.0.0.tgz#1a8ed8fc19a8b56268f76f5a1a3e3832b0c26200" - -stream-connect@^1.0.1, stream-connect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stream-connect/-/stream-connect-1.0.2.tgz#18bc81f2edb35b8b5d9a8009200a985314428a97" - dependencies: - array-back "^1.0.2" - -stream-handlebars@~0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/stream-handlebars/-/stream-handlebars-0.1.6.tgz#7305b5064203da171608c478acf642a149892a2f" - dependencies: - handlebars "^3.0.0" - object-tools "^1.2.1" - -stream-via@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-1.0.3.tgz#cebd32a5a59d74b3b68e3404942e867184ad4ac9" - -stream-via@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-0.1.1.tgz#0cee5df9c959fb1d3f4eda4819f289d5f9205afc" - -string-tools@^0.1.4: - version "0.1.8" - resolved "https://registry.yarnpkg.com/string-tools/-/string-tools-0.1.8.tgz#70884e86a26ee5103a078bef67033d558d36e337" - -string-tools@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string-tools/-/string-tools-1.0.0.tgz#c69a9d5788858997da66f1d923ba7113ea466b5a" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" - -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -supports-color@^3.1.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - dependencies: - has-flag "^1.0.0" - -table-layout@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.3.0.tgz#6ee20dc483db371b3e5c87f704ed2f7c799d2c9a" - dependencies: - array-back "^1.0.3" - core-js "^2.4.1" - deep-extend "~0.4.1" - feature-detect-es6 "^1.3.1" - typical "^2.6.0" - wordwrapjs "^2.0.0-0" - -table@^3.7.8: - version "3.8.3" - resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" - dependencies: - ajv "^4.7.0" - ajv-keywords "^1.0.0" - chalk "^1.1.1" - lodash "^4.0.0" - slice-ansi "0.0.4" - string-width "^2.0.0" - -taffydb@2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" - -temp-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/temp-path/-/temp-path-1.0.0.tgz#24b1543973ab442896d9ad367dd9cbdbfafe918b" - -test-value@^1.0.1, test-value@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/test-value/-/test-value-1.1.0.tgz#a09136f72ec043d27c893707c2b159bfad7de93f" - dependencies: - array-back "^1.0.2" - typical "^2.4.2" - -test-value@^2.0.0, test-value@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291" - dependencies: - array-back "^1.0.3" - typical "^2.6.0" - -text-table@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - -then-fs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/then-fs/-/then-fs-2.0.0.tgz#72f792dd9d31705a91ae19ebfcf8b3f968c81da2" - dependencies: - promise ">=3.2 <8" - -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" - -tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" - dependencies: - punycode "^1.4.1" - -tryit@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - dependencies: - prelude-ls "~1.1.2" - -type-detect@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" - -type-detect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - -typical@^2.1, typical@^2.2, typical@^2.3.0, typical@^2.4.2, typical@^2.5.0, typical@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.0.tgz#89d51554ab139848a65bcc2c8772f8fb450c40ed" - -uglify-js@^2.6: - version "2.8.21" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.21.tgz#1733f669ae6f82fc90c7b25ec0f5c783ee375314" - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-js@~2.3: - version "2.3.6" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.3.6.tgz#fa0984770b428b7a9b2a8058f46355d14fef211a" - dependencies: - async "~0.2.6" - optimist "~0.3.5" - source-map "~0.1.7" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - -ultron@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" - -underscore-contrib@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7" - dependencies: - underscore "1.6.0" - -underscore@1.6.0, underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - -underscore@~1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" - -usage-stats@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/usage-stats/-/usage-stats-0.8.2.tgz#d7be5203682e267f7696b354356c8c376aa12542" - dependencies: - array-back "^1.0.3" - cli-commands "0.1.0" - core-js "^2.4.1" - feature-detect-es6 "^1.3.1" - home-path "^1.0.3" - mkdirp "^0.5.1" - req-then "^0.5.1" - typical "^2.6.0" - uuid "^3.0.0" - -user-home@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" - dependencies: - os-homedir "^1.0.0" - -utf-8-validate@1.2.x: - version "1.2.2" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-1.2.2.tgz#8bb871a4741e085c70487ca7acdbd7d6d36029eb" - dependencies: - bindings "~1.2.1" - nan "~2.4.0" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -uuid@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" - -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" - dependencies: - extsprintf "1.0.2" - -walk-back@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-2.0.1.tgz#554e2a9d874fac47a8cb006bf44c2f0c4998a0a4" - -which@^1.1.1: - version "1.2.14" - resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" - dependencies: - isexe "^2.0.0" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - -wordwrap@^1.0.0, wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - -wordwrapjs@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-1.2.1.tgz#754a5ea0664cfbff50540dc32d67bda3289fc34b" - dependencies: - array-back "^1.0.3" - typical "^2.5.0" - -wordwrapjs@^2.0.0-0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-2.0.0.tgz#ab55f695e6118da93858fdd70c053d1c5e01ac20" - dependencies: - array-back "^1.0.3" - feature-detect-es6 "^1.3.1" - reduce-flatten "^1.0.1" - typical "^2.6.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - dependencies: - mkdirp "^0.5.1" - -ws@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-0.8.1.tgz#6b65273b99193c5f067a4cf5809598f777e3b759" - dependencies: - options ">=0.0.5" - ultron "1.0.x" - optionalDependencies: - bufferutil "1.2.x" - utf-8-validate "1.2.x" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0"