UNPKG

bfx-api-node-ws1

Version:

Legacy WSv1 transport for the Bitfinex Node API

380 lines (333 loc) 8.58 kB
'use strict' const { EventEmitter } = require('events') const { isSnapshot, genAuthSig } = require('bfx-api-node-util') const debug = require('debug')('bfx:api:ws1') const _isArray = require('lodash/isArray') const WebSocket = require('ws') const WS_URL = 'wss://api.bitfinex.com/ws/' /** * Communicates with v1 of the Bitfinex WebSocket API */ class WSv1 extends EventEmitter { /** * @param {sting} opts.apiKey * @param {string} opts.apiSecret * @param {string?} opts.url - ws connection url */ constructor (opts = { apiKey: '', apiSecret: '', url: WS_URL }) { super() this._apiKey = opts.apiKey || '' this._apiSecret = opts.apiSecret || '' this._url = opts.url || WS_URL this._channelMap = {} // map channel IDs to events } /** * Returns status of websocket * * @return {boolean} open */ isOpen () { return this._ws.isOpen() } /** * Opens a new websocket conection to the configured URL */ open () { this._ws = new WebSocket(this._url) this._ws.on('message', this._onWSMessage.bind(this)) this._ws.on('open', this._onWSOpen.bind(this)) this._ws.on('error', this._onWSError.bind(this)) this._ws.on('close', this._onWSClose.bind(this)) } /** * @param {string} msgJSON * @param {number} flags * @private */ _onWSMessage (msgJSON, flags) { let msg try { msg = JSON.parse(msgJSON) } catch (e) { debug('[bfx ws2 error] received invalid json') debug('[bfx ws2 error] %j', msgJSON) return } debug('Received message: %j', msg) this.emit('message', msg, flags) debug('Emmited message event') // Drop out early if channel data if (_isArray(msg) || !msg.event) { return this._handleChannel(msg) } if (msg.event === 'subscribed') { debug('Subscription report received') this._channelMap[msg.chanId] = msg } else if (msg.event === 'auth') { if (msg.status !== 'OK') { debug('Emitting \'error\' %j', msg) this.emit('error', msg) return } this._channelMap[msg.chanId] = { channel: 'auth' } } debug('Emitting \'%s\' %j', msg.event, msg) this.emit(msg.event, msg) } /** * @param {Array|Array[]} msg * @private */ _handleChannel (msg) { // 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 (msg[0] === 'hb') { return debug(`received heartbeat in ${event.channel}`) } 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') } } } /** * @param {Array} msg * @private */ _processUserEvent (msg) { const event = msg[0] const data = msg[1] if (_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) this.emit(event, data) } } /** * @param {Array} msg * @param {Object} event * @private */ _processTickerEvent (msg, event) { if (msg.length > 9) { // Update // All values are numbers 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) this.emit('ticker', event.pair, update) } } /** * @param {Array[]} msg * @param {Object} event * @private */ _processTradeEvent (msg, event) { if (isSnapshot(msg)) { const snapshot = msg[0].map(el => ({ seq: el[0], timestamp: el[1], price: el[2], amount: el[3] })) debug('Emitting trade snapshot, %s, %j', event.pair, snapshot) this.emit('trade', event.pair, snapshot) return } if (msg[0] !== 'te' && msg[0] !== 'tu') return // seq is a string, other payload members are nums const update = { seq: msg[1] } if (msg[0] === 'te') { // Trade executed update.timestamp = msg[2] update.price = msg[3] update.amount = msg[4] } else { // Trade updated update.id = msg[2] update.timestamp = msg[3] update.price = msg[4] update.amount = msg[5] } // See http://docs.bitfinex.com/#trades75 debug('Emitting trade, %s, %j', event.pair, update) this.emit('trade', event.pair, update) } /** * @param {Array[]} msg * @param {Object} event * @private */ _processBookEvent (msg, event) { if (!isSnapshot(msg[0]) && msg.length > 2) { let update if (event.prec === 'R0') { update = { price: msg[1], orderId: msg[0], amount: msg[2] } } else { update = { price: msg[0], count: msg[1], amount: msg[2] } } debug('Emitting orderbook, %s, %j', event.pair, update) this.emit('orderbook', event.pair, update) return } msg = msg[0] if (isSnapshot(msg)) { const snapshot = msg.map((el) => { if (event.prec === 'R0') { return { orderId: el[0], price: el[1], amount: el[2] } } return { price: el[0], count: el[1], amount: el[2] } }) debug('Emitting orderbook snapshot, %s, %j', event.pair, snapshot) this.emit('orderbook', event.pair, snapshot) } } /** * Close the websocket connection */ close () { this._ws.close() } /** * @private */ _onWSOpen () { this._channelMap = {} this.emit('open') } /** * @private */ _onWSError (error) { this.emit('error', error) } /** * @private */ _onWSClose () { this.emit('close') } /** * Send a packet via the ws connection; automatically converted to a JSON * string. * * @param {*} msg */ send (msg) { debug('Sending %j', msg) this._ws.send(JSON.stringify(msg)) } /** * Subscribe to order book updates. Snapshot will be sent as multiple updates. * Event will be emited as `PAIRNAME_book`. * * @param {string} pair * @param {string} precision - price aggregation level (P0 (def), P1, P2, P3) * @param {string} length - number of price points. 25 (default) or 100. * @see http://docs.bitfinex.com/#order-books */ subscribeOrderBook (pair = 'BTCUSD', prec = 'P0', len = '25') { this.send({ event: 'subscribe', channel: 'book', pair, prec, len }) } /** * Subscribe to trades. Snapshot will be sent as multiple updates. * Event will be emited as `PAIRNAME_trades`. * * @param {string} pair * @see http://docs.bitfinex.com/#trades75 */ subscribeTrades (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 * @see http://docs.bitfinex.com/#ticker76 */ subscribeTicker (pair = 'BTCUSD') { this.send({ event: 'subscribe', channel: 'ticker', pair }) } /** * Unsubscribe from a channel. * * @param {number} chanId - ID of the channel received on `subscribed` event */ unsubscribe (chanId) { this.send({ event: 'unsubscribe', chanId }) } /** * Authenticate the user. Will receive executed traded updates. * * @see http://docs.bitfinex.com/#wallet-updates */ auth () { const { sig, payload } = genAuthSig(this._apiSecret) this.send({ event: 'auth', apiKey: this._apiKey, authSig: sig, authPayload: payload }) } } module.exports = WSv1