btc-api-node 1.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of btc-api-node might be problematic. Click here for more details.

Files changed (97) hide show
  1. package/.istanbul.yml +53 -0
  2. package/.travis.yml +5 -0
  3. package/CHANGELOG +33 -0
  4. package/LICENSE.md +21 -0
  5. package/README.md +211 -0
  6. package/doc/order.md +160 -0
  7. package/doc/rest2.md +573 -0
  8. package/doc/ws2.md +925 -0
  9. package/examples/bfx.js +26 -0
  10. package/examples/rest2/order_history.js +29 -0
  11. package/examples/rest2/symbols.js +15 -0
  12. package/examples/rest2/tickers.js +24 -0
  13. package/examples/rest2/trade_history.js +28 -0
  14. package/examples/ws2/auth.js +31 -0
  15. package/examples/ws2/calc.js +33 -0
  16. package/examples/ws2/cancel_all.js +35 -0
  17. package/examples/ws2/cancel_all_buf.js +39 -0
  18. package/examples/ws2/candles.js +36 -0
  19. package/examples/ws2/info_events.js +40 -0
  20. package/examples/ws2/oc_multi.js +50 -0
  21. package/examples/ws2/order_books.js +37 -0
  22. package/examples/ws2/orders.js +67 -0
  23. package/examples/ws2/ox_multi.js +61 -0
  24. package/examples/ws2/sequencing.js +23 -0
  25. package/examples/ws2/ticker.js +20 -0
  26. package/examples/ws2/trades.js +27 -0
  27. package/index.js +24 -0
  28. package/lib/model.js +25 -0
  29. package/lib/models/alert.js +25 -0
  30. package/lib/models/balance_info.js +21 -0
  31. package/lib/models/candle.js +33 -0
  32. package/lib/models/funding_credit.js +61 -0
  33. package/lib/models/funding_info.js +16 -0
  34. package/lib/models/funding_loan.js +64 -0
  35. package/lib/models/funding_offer.js +60 -0
  36. package/lib/models/funding_trade.js +33 -0
  37. package/lib/models/index.js +23 -0
  38. package/lib/models/margin_info.js +29 -0
  39. package/lib/models/notification.js +31 -0
  40. package/lib/models/order.js +288 -0
  41. package/lib/models/order_book.js +214 -0
  42. package/lib/models/position.js +43 -0
  43. package/lib/models/tick.js +83 -0
  44. package/lib/models/trade.js +43 -0
  45. package/lib/models/trade_tick.js +29 -0
  46. package/lib/models/wallet.js +34 -0
  47. package/lib/transports/rest.js +391 -0
  48. package/lib/transports/rest2.js +597 -0
  49. package/lib/transports/ws.js +323 -0
  50. package/lib/transports/ws2.js +1729 -0
  51. package/lib/util/gen_auth_sig.js +23 -0
  52. package/lib/util/index.js +11 -0
  53. package/lib/util/is_snapshot.js +5 -0
  54. package/lib/util/nonce.js +5 -0
  55. package/package.json +39 -0
  56. package/test/fixtures/response-ticker-funding.json +1 -0
  57. package/test/fixtures/response-ticker-pairs.json +1 -0
  58. package/test/fixtures/response-trades-funding.json +1 -0
  59. package/test/fixtures/response-trades-pairs.json +1 -0
  60. package/test/fixtures/response-ws-1-orderbook-R0.json +51 -0
  61. package/test/fixtures/response-ws2-server-order-book-P0.json +1 -0
  62. package/test/fixtures/response-ws2-server-order-book-P1.json +1 -0
  63. package/test/fixtures/response-ws2-server-order-book-R0.json +1 -0
  64. package/test/fixtures/response-ws2-server-ticker-funding.json +1 -0
  65. package/test/fixtures/response-ws2-server-trades.json +1 -0
  66. package/test/helpers/test_model.js +71 -0
  67. package/test/index.js +131 -0
  68. package/test/lib/models/alert.js +12 -0
  69. package/test/lib/models/balance_info.js +12 -0
  70. package/test/lib/models/candle.js +12 -0
  71. package/test/lib/models/funding_credit.js +17 -0
  72. package/test/lib/models/funding_info.js +7 -0
  73. package/test/lib/models/funding_loan.js +17 -0
  74. package/test/lib/models/funding_offer.js +17 -0
  75. package/test/lib/models/funding_trade.js +15 -0
  76. package/test/lib/models/margin_info.js +15 -0
  77. package/test/lib/models/notification.js +14 -0
  78. package/test/lib/models/order.js +395 -0
  79. package/test/lib/models/order_book.js +188 -0
  80. package/test/lib/models/position.js +15 -0
  81. package/test/lib/models/tick.js +34 -0
  82. package/test/lib/models/trade.js +16 -0
  83. package/test/lib/models/trade_tick.js +14 -0
  84. package/test/lib/models/wallet.js +14 -0
  85. package/test/lib/transports/rest-1-integration.js +131 -0
  86. package/test/lib/transports/rest-2-integration.js +80 -0
  87. package/test/lib/transports/rest-2-issue-80-argument-length.js +61 -0
  88. package/test/lib/transports/rest-2-smoke-test.js +49 -0
  89. package/test/lib/transports/rest-2-unit.js +26 -0
  90. package/test/lib/transports/rest1.js +152 -0
  91. package/test/lib/transports/ws-1-handle-channel.js +83 -0
  92. package/test/lib/transports/ws-1-parsing.js +40 -0
  93. package/test/lib/transports/ws-1-test.js +275 -0
  94. package/test/lib/transports/ws2-integration.js +259 -0
  95. package/test/lib/transports/ws2-unit.js +1295 -0
  96. package/test/lib/util/is_snapshot.js +20 -0
  97. package/test/lib/util/nonce.js +20 -0
@@ -0,0 +1,1729 @@
1
+ 'use strict'
2
+
3
+ const { EventEmitter } = require('events')
4
+ const debug = require('debug')('bitfinex:ws')
5
+ const WebSocket = require('ws')
6
+ const Promise = require('bluebird')
7
+ const CbQ = require('cbq')
8
+ const _Throttle = require('lodash.throttle')
9
+ const { genAuthSig, nonce } = require('../util')
10
+
11
+ const {
12
+ BalanceInfo,
13
+ FundingCredit,
14
+ FundingInfo,
15
+ FundingLoan,
16
+ FundingOffer,
17
+ FundingTrade,
18
+ MarginInfo,
19
+ Notification,
20
+ Order,
21
+ Position,
22
+ Trade,
23
+ TradeTick,
24
+ Wallet,
25
+ OrderBook,
26
+ Candle,
27
+ Tick
28
+ } = require('../models')
29
+
30
+ const WS_URL = 'wss://api.bitfinex.com/ws/2'
31
+ const MAX_CALC_OPS = 8
32
+ const INFO_CODES = {
33
+ SERVER_RESTART: 20051,
34
+ MAINTENANCE_START: 20060,
35
+ MAINTENANCE_END: 20061
36
+ }
37
+
38
+ /**
39
+ * Communicates with v2 of the Bitfinex WebSocket API
40
+ */
41
+ class WSv2 extends EventEmitter {
42
+ /**
43
+ * Instantiate a new ws2 transport. Does not auto-open
44
+ *
45
+ * @param {string} opts.apiKey
46
+ * @param {string} opts.apiSecret
47
+ * @param {string} opts.url - ws connection url
48
+ * @param {number} opts.orderOpBufferDelay - multi-order op batching timeout
49
+ * @param {boolean} opts.transform - if true, packets are converted to models
50
+ * @param {Object} opts.agent - optional node agent for ws connection (proxy)
51
+ * @param {boolean} opts.manageOrderBooks - enable local OB persistence
52
+ * @param {boolean} opts.manageCandles - enable local candle persistence
53
+ * @param {boolean} opts.seqAudit - enable sequence numbers & verification
54
+ * @param {boolean} opts.autoReconnect - if true, we will reconnect on close
55
+ * @param {number} opts.reconnectDelay - optional, defaults to 1000 (ms)
56
+ * @param {number} opts.packetWDDelay - watch-dog forced reconnection delay
57
+ */
58
+ constructor (opts = { apiKey: '', apiSecret: '', url: WS_URL }) {
59
+ super()
60
+
61
+ this._apiKey = opts.apiKey || ''
62
+ this._apiSecret = opts.apiSecret || ''
63
+ this._agent = opts.agent
64
+ this._url = opts.url || WS_URL
65
+ this._transform = opts.transform === true
66
+ this._orderOpBufferDelay = opts.orderOpBufferDelay || -1
67
+ this._orderOpBuffer = []
68
+ this._orderOpTimeout = null
69
+ this._seqAudit = opts.seqAudit === true
70
+ this._autoReconnect = opts.autoReconnect === true
71
+ this._reconnectDelay = opts.reconnectDelay || 1000
72
+ this._manageOrderBooks = opts.manageOrderBooks === true
73
+ this._manageCandles = opts.manageCandles === true
74
+ this._packetWDDelay = opts.packetWDDelay
75
+ this._packetWDTimeout = null
76
+ this._packetWDLastTS = 0
77
+ this._orderBooks = {}
78
+ this._candles = {}
79
+
80
+ /**
81
+ * {
82
+ * [groupID]: {
83
+ * [eventName]: [{
84
+ * modelClass: ..,
85
+ * filter: { symbol: 'tBTCUSD' }, // only works w/ serialize
86
+ * cb: () => {}
87
+ * }]
88
+ * }
89
+ * }
90
+ * @private
91
+ */
92
+ this._listeners = {}
93
+ this._infoListeners = {} // { [code]: <listeners> }
94
+ this._subscriptionRefs = {}
95
+ this._channelMap = {}
96
+ this._orderBooks = {}
97
+ this._eventCallbacks = new CbQ()
98
+ this._isAuthenticated = false
99
+ this._wasEverAuthenticated = false // used for auto-auth on reconnect
100
+ this._lastPubSeq = -1
101
+ this._lastAuthSeq = -1
102
+ this._isOpen = false
103
+ this._ws = null
104
+ this._isClosing = false // used to block reconnect on direct close() call
105
+ this._isReconnecting = false
106
+
107
+ this._onWSOpen = this._onWSOpen.bind(this)
108
+ this._onWSClose = this._onWSClose.bind(this)
109
+ this._onWSError = this._onWSError.bind(this)
110
+ this._onWSMessage = this._onWSMessage.bind(this)
111
+ this._triggerPacketWD = this._triggerPacketWD.bind(this)
112
+ this._sendCalc = _Throttle(this._sendCalc.bind(this), 1000 / MAX_CALC_OPS)
113
+ }
114
+
115
+ /**
116
+ * Opens a connection to the API server. Rejects with an error if a
117
+ * connection is already open. Resolves on success
118
+ *
119
+ * @return {Promise} p
120
+ */
121
+ open () {
122
+ if (this._isOpen || this._ws !== null) {
123
+ return Promise.reject(new Error('already open'))
124
+ }
125
+
126
+ this._ws = new WebSocket(this._url, {
127
+ agent: this._agent
128
+ })
129
+
130
+ if (this._seqAudit) {
131
+ this._ws.once('open', this.enableSequencing.bind(this))
132
+ }
133
+
134
+ this._ws.on('message', this._onWSMessage)
135
+ this._ws.on('open', this._onWSOpen)
136
+ this._ws.on('error', this._onWSError)
137
+ this._ws.on('close', this._onWSClose)
138
+
139
+ return new Promise((resolve, reject) => {
140
+ this._ws.once('open', () => resolve())
141
+ })
142
+ }
143
+
144
+ /**
145
+ * Closes the active connection. If there is none, rejects with a promise.
146
+ * Resolves on success
147
+ *
148
+ * @param {number} code - passed to ws
149
+ * @param {string} reason - passed to ws
150
+ * @return {Promise}
151
+ */
152
+ close (code, reason) {
153
+ if (!this._isOpen || this._ws === null) {
154
+ return Promise.reject(new Error('not open'))
155
+ }
156
+
157
+ this._isClosing = true
158
+
159
+ return new Promise((resolve, reject) => {
160
+ this._ws.once('close', () => resolve())
161
+ this._ws.close(code, reason)
162
+ })
163
+ }
164
+
165
+ /**
166
+ * Generates & sends an authentication packet to the server; if already
167
+ * authenticated, rejects with an error. Resolves on success
168
+ *
169
+ * @param {number} calc - optional, default is 0
170
+ * @return {Promise} p
171
+ */
172
+ auth (calc = 0) {
173
+ if (this._isAuthenticated) {
174
+ return Promise.reject(new Error('already authenticated'))
175
+ }
176
+
177
+ const authNonce = nonce()
178
+ const authPayload = `AUTH${authNonce}${authNonce}`
179
+ const { sig } = genAuthSig(this._apiSecret, authPayload)
180
+
181
+ return new Promise((resolve, reject) => {
182
+ this._ws.once('auth', () => resolve())
183
+
184
+ this.send({
185
+ event: 'auth',
186
+ apiKey: this._apiKey,
187
+ authSig: sig,
188
+ authPayload,
189
+ authNonce,
190
+ calc
191
+ })
192
+ })
193
+ }
194
+
195
+ /**
196
+ * Utility method to close & re-open the ws connection. Re-authenticates if
197
+ * previously authenticated
198
+ *
199
+ * @return {Promise} p - resolves on completion
200
+ */
201
+ reconnect () {
202
+ if (!this._ws) return this.open()
203
+
204
+ return new Promise((resolve, reject) => {
205
+ this._ws.once('close', () => {
206
+ this.open()
207
+
208
+ if (!this._wasEverAuthenticated) {
209
+ return resolve()
210
+ }
211
+
212
+ this._ws.once('open', this.auth.bind(this))
213
+ this._ws.once('auth', () => resolve())
214
+ })
215
+
216
+ this.close()
217
+ })
218
+ }
219
+
220
+ /**
221
+ * Trigger the packet watch-dog; called when we haven't seen a new WS packet
222
+ * for longer than our WD duration (if provided)
223
+ * @private
224
+ */
225
+ _triggerPacketWD () {
226
+ if (!this._packetWDDelay || !this._isOpen) return
227
+
228
+ debug(
229
+ 'packet delay watchdog triggered [last packet %dms ago]',
230
+ Date.now() - this._packetWDLastTS
231
+ )
232
+
233
+ this._packetWDTimeout = null
234
+ this.reconnect()
235
+ }
236
+
237
+ /**
238
+ * Reset the packet watch-dog timeout. Should be called on every new WS packet
239
+ * if the watch-dog is enabled
240
+ * @private
241
+ */
242
+ _resetPacketWD () {
243
+ if (!this._packetWDDelay) return
244
+ if (this._packetWDTimeout !== null) {
245
+ clearTimeout(this._packetWDTimeout)
246
+ }
247
+
248
+ if (!this._isOpen) return
249
+
250
+ this._packetWDTimeout = setTimeout(
251
+ this._triggerPacketWD,
252
+ this._packetWDDelay
253
+ )
254
+ }
255
+
256
+ /**
257
+ * @private
258
+ */
259
+ _onWSOpen () {
260
+ this._isOpen = true
261
+ this._isReconnecting = false
262
+ this._packetWDLastTS = Date.now()
263
+ this.emit('open')
264
+
265
+ debug('connection open')
266
+ }
267
+
268
+ /**
269
+ * @private
270
+ */
271
+ _onWSClose () {
272
+ this._isOpen = false
273
+ this._isAuthenticated = false
274
+ this._lastAuthSeq = -1
275
+ this._lastPubSeq = -1
276
+ this._ws = null
277
+ this.emit('close')
278
+
279
+ debug('connection closed')
280
+
281
+ if (this._autoReconnect && !this._isClosing) {
282
+ setTimeout(this.reconnect.bind(this), this._reconnectDelay)
283
+ }
284
+
285
+ this._isClosing = false
286
+ }
287
+
288
+ /**
289
+ * @private
290
+ */
291
+ _onWSError (err) {
292
+ this.emit('error', err)
293
+
294
+ debug('error: %j', err)
295
+ }
296
+
297
+ /**
298
+ * @param {Array} arrN - notification in ws array format
299
+ * @private
300
+ */
301
+ _onWSNotification (arrN) {
302
+ const status = arrN[6]
303
+ const msg = arrN[7]
304
+
305
+ if (!arrN[4]) return
306
+
307
+ if (arrN[1] === 'on-req') {
308
+ const [,, cid] = arrN[4]
309
+ const k = `order-new-${cid}`
310
+
311
+ if (status === 'SUCCESS') {
312
+ return this._eventCallbacks.trigger(k, null, arrN[4])
313
+ }
314
+
315
+ this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4])
316
+ } else if (arrN[1] === 'oc-req') {
317
+ const [id] = arrN[4]
318
+ const k = `order-cancel-${id}`
319
+
320
+ if (status === 'SUCCESS') {
321
+ return this._eventCallbacks.trigger(k, null, arrN[4])
322
+ }
323
+
324
+ this._eventCallbacks.trigger(k, new Error(`${status}: ${msg}`), arrN[4])
325
+ }
326
+ }
327
+
328
+ /**
329
+ * @param {string} msgJSON
330
+ * @param {string} flags
331
+ * @private
332
+ */
333
+ _onWSMessage (msgJSON, flags) {
334
+ this._packetWDLastTS = Date.now()
335
+ this._resetPacketWD()
336
+
337
+ let msg
338
+
339
+ try {
340
+ msg = JSON.parse(msgJSON)
341
+ } catch (e) {
342
+ this.emit('error', `invalid message JSON: ${msgJSON}`)
343
+ return
344
+ }
345
+
346
+ const seq = msg[msg.length - 1]
347
+
348
+ if (this._seqAudit && seq !== 0 && !isNaN(seq)) {
349
+ if (msg[0] === 0) {
350
+ if (this._lastAuthSeq !== -1 && seq > (this._lastAuthSeq + 1)) {
351
+ this.emit('error', new Error(
352
+ `invalid auth seq #; last ${this._lastAuthSeq}, got ${seq}`
353
+ ))
354
+
355
+ this._lastAuthSeq = seq
356
+ return
357
+ }
358
+
359
+ this._lastAuthSeq = seq
360
+ } else {
361
+ if (this._lastPubSeq !== -1 && seq > (this._lastPubSeq + 1)) {
362
+ this.emit('error', new Error(
363
+ `invalid seq #; last ${this._lastPubSeq}, got ${seq}`
364
+ ))
365
+
366
+ this._lastPubSeq = seq
367
+ return
368
+ }
369
+
370
+ this._lastPubSeq = seq
371
+ }
372
+ }
373
+
374
+ this.emit('message', msg, flags)
375
+
376
+ if (Array.isArray(msg)) {
377
+ this._handleChannelMessage(msg)
378
+ } else if (msg.event) {
379
+ this._handleEventMessage(msg)
380
+ } else {
381
+ debug('recv unidentified message: %j', msg)
382
+ }
383
+ }
384
+
385
+ /**
386
+ * @param {array} msg
387
+ * @private
388
+ */
389
+ _handleChannelMessage (msg) {
390
+ const [chanId] = msg
391
+ const channelData = this._channelMap[chanId]
392
+
393
+ if (!channelData) {
394
+ debug('recv msg from unknown channel %d: %j', chanId, msg)
395
+ return
396
+ }
397
+
398
+ debug('recv msg: %j', msg)
399
+
400
+ if (msg.length < 2) return
401
+ if (msg[1] === 'hb') return // TODO: optionally track seq
402
+
403
+ if (channelData.channel === 'book') {
404
+ return this._handleOBMessage(msg, channelData)
405
+ } else if (channelData.channel === 'trades') {
406
+ return this._handleTradeMessage(msg, channelData)
407
+ } else if (channelData.channel === 'ticker') {
408
+ return this._handleTickerMessage(msg, channelData)
409
+ } else if (channelData.channel === 'candles') {
410
+ return this._handleCandleMessage(msg, channelData)
411
+ } else if (channelData.channel === 'auth') {
412
+ return this._handleAuthMessage(msg, channelData)
413
+ } else {
414
+ this._propagateMessageToListeners(msg)
415
+ this.emit(channelData.channel, msg)
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Called for messages from the 'book' channel. Might be an update or a
421
+ * snapshot
422
+ *
423
+ * @param {Array|Array[]} msg
424
+ * @param {Object} chanData - entry from _channelMap
425
+ * @private
426
+ */
427
+ _handleOBMessage (msg, chanData) {
428
+ const { symbol } = chanData
429
+ let data = msg[1]
430
+
431
+ if (this._manageOrderBooks) {
432
+ const err = this._updateManagedOB(symbol, data)
433
+
434
+ if (err) {
435
+ this.emit('error', err)
436
+ return
437
+ }
438
+
439
+ data = this._orderBooks[symbol]
440
+ } else if (data.length > 0 && !Array.isArray(data[0])) {
441
+ data = [data] // always pass on an array of entries
442
+ }
443
+
444
+ if (this._transform) {
445
+ data = new OrderBook(data)
446
+ }
447
+
448
+ const internalMessage = [chanData.chanId, 'orderbook', data]
449
+ internalMessage.filterOverride = [
450
+ chanData.symbol,
451
+ chanData.prec,
452
+ chanData.len
453
+ ]
454
+
455
+ this._propagateMessageToListeners(internalMessage, false)
456
+ this.emit('orderbook', symbol, data)
457
+ }
458
+
459
+ /**
460
+ * @param {string} symbol
461
+ * @param {number[]|number[][]} data
462
+ * @return {Error} err - null on success
463
+ * @private
464
+ */
465
+ _updateManagedOB (symbol, data) {
466
+ if (Array.isArray(data[0])) { // snapshot, new OB
467
+ if (this._orderBooks[symbol]) {
468
+ return new Error(`recv snapshot for known OB: ${symbol}`)
469
+ }
470
+
471
+ this._orderBooks[symbol] = data
472
+ return null
473
+ }
474
+
475
+ // entry, needs to be applied to OB
476
+ if (!this._orderBooks[symbol]) {
477
+ return new Error(`recv update for unknown OB: ${symbol}`)
478
+ }
479
+
480
+ const success = OrderBook.updateArrayOBWith(this._orderBooks[symbol], data)
481
+
482
+ if (!success) {
483
+ return new Error(
484
+ `ob update for unknown price level: ${JSON.stringify(data)}`
485
+ )
486
+ }
487
+
488
+ return null
489
+ }
490
+
491
+ /**
492
+ * Returns an up-to-date copy of the order book for the specified symbol, or
493
+ * null if no OB is managed for that symbol.
494
+ * Set `manageOrderBooks: true` in the constructor to use.
495
+ *
496
+ * @param {string} symbol
497
+ * @return {OrderBook} ob - null if not found
498
+ */
499
+ getOB (symbol) {
500
+ if (!this._orderBooks[symbol]) return null
501
+
502
+ return new OrderBook(this._orderBooks[symbol])
503
+ }
504
+
505
+ /**
506
+ * @param {Array} msg
507
+ * @param {Object} chanData
508
+ * @private
509
+ */
510
+ _handleTradeMessage (msg, chanData) {
511
+ const eventName = msg.length === 3 ? msg[1] : 'trades'
512
+ let payload = msg[msg.length - 1]
513
+
514
+ if (!Array.isArray(payload[0])) {
515
+ payload = [payload]
516
+ }
517
+
518
+ const model = msg[0] === 0 ? Trade : TradeTick // auth trades have more data
519
+ const data = this._transform ? model.unserialize(payload) : payload
520
+ const internalMessage = [chanData.chanId, eventName, data]
521
+ internalMessage.filterOverride = [chanData.pair]
522
+
523
+ this._propagateMessageToListeners(internalMessage, false)
524
+ this.emit('trades', chanData.pair, data)
525
+ }
526
+
527
+ /**
528
+ * @param {Array} msg
529
+ * @param {Object} chanData
530
+ * @private
531
+ */
532
+ _handleTickerMessage (msg, chanData) {
533
+ let data = msg[1]
534
+
535
+ if (this._transform) {
536
+ msg[1].splice(0, 0, chanData.symbol)
537
+ data = Tick.unserialize(msg[1])
538
+ }
539
+
540
+ const internalMessage = [chanData.chanId, 'ticker', data]
541
+ internalMessage.filterOverride = [chanData.symbol]
542
+
543
+ this._propagateMessageToListeners(internalMessage, false)
544
+ this.emit('ticker', chanData.symbol, data)
545
+ }
546
+
547
+ /**
548
+ * Called for messages from a 'candles' channel. Might be an update or
549
+ * snapshot.
550
+ *
551
+ * @param {Array|Array[]} msg
552
+ * @param {Object} chanData - entry from _channelMap
553
+ * @private
554
+ */
555
+ _handleCandleMessage (msg, chanData) {
556
+ const { key } = chanData
557
+ let data = msg[1]
558
+
559
+ if (this._manageCandles) {
560
+ const err = this._updateManagedCandles(key, data)
561
+
562
+ if (err) {
563
+ this.emit('error', err)
564
+ return
565
+ }
566
+
567
+ data = this._candles[key]
568
+ } else if (data.length > 0 && !Array.isArray(data[0])) {
569
+ data = [data] // always pass on an array of candles
570
+ }
571
+
572
+ if (this._transform) {
573
+ data = Candle.unserialize(data)
574
+ }
575
+
576
+ const internalMessage = [chanData.chanId, 'candle', data]
577
+ internalMessage.filterOverride = [chanData.key]
578
+
579
+ this._propagateMessageToListeners(internalMessage, false)
580
+ this.emit('candle', data, key)
581
+ }
582
+
583
+ /**
584
+ * @param {string} symbol
585
+ * @param {number[]|number[][]} data
586
+ * @return {Error} err - null on success
587
+ * @private
588
+ */
589
+ _updateManagedCandles (key, data) {
590
+ if (Array.isArray(data[0])) { // snapshot, new candles
591
+ if (this._candles[key]) {
592
+ return new Error(`recv snapshot for known candles: ${key}`)
593
+ }
594
+
595
+ data.sort((a, b) => b[0] - a[0])
596
+
597
+ this._candles[key] = data
598
+ return null
599
+ }
600
+
601
+ // entry, needs to be applied to candle set
602
+ if (!this._candles[key]) {
603
+ return new Error(`recv update for unknown candles: ${key}`)
604
+ }
605
+
606
+ const candles = this._candles[key]
607
+ let updated = false
608
+
609
+ for (let i = 0; i < candles.length; i++) {
610
+ if (data[0] === candles[i][0]) {
611
+ candles[i] = data
612
+ updated = true
613
+ break
614
+ }
615
+ }
616
+
617
+ if (!updated) {
618
+ candles.unshift(data)
619
+ }
620
+
621
+ return null
622
+ }
623
+
624
+ /**
625
+ * Fetch a reference to the full set of synced candles for the specified key.
626
+ * Set `manageCandles: true` in the constructor to use.
627
+ *
628
+ * @param {string} key
629
+ * @return {Array} candles - empty array if none exist
630
+ */
631
+ getCandles (key) {
632
+ return this._candles[key] || []
633
+ }
634
+
635
+ /**
636
+ * @param {Array} msg
637
+ * @param {Object} chanData
638
+ * @private
639
+ */
640
+ _handleAuthMessage (msg, chanData) {
641
+ if (msg[1] === 'n') {
642
+ this._onWSNotification(msg[2])
643
+ }
644
+
645
+ this._propagateMessageToListeners(msg)
646
+ }
647
+
648
+ /**
649
+ * @param {Array} msg
650
+ * @param {boolean} transform - defaults to internal flag
651
+ * @private
652
+ */
653
+ _propagateMessageToListeners (msg, transform = this._transform) {
654
+ const listenerGroups = Object.values(this._listeners)
655
+
656
+ for (let i = 0; i < listenerGroups.length; i++) {
657
+ WSv2._notifyListenerGroup(listenerGroups[i], msg, transform, this)
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Applies filtering & transform to a packet before sending it out to matching
663
+ * listeners in the group.
664
+ *
665
+ * @param {Object} lGroup - listener group to parse & notify
666
+ * @param {Object} msg - passed to each matched listener
667
+ * @param {boolean} transform - whether or not to instantiate a model
668
+ * @param {WSv2} ws - instance to pass to models if transforming
669
+ * @private
670
+ */
671
+ static _notifyListenerGroup (lGroup, msg, transform, ws) {
672
+ const [, eventName, data] = msg
673
+ let filterByData
674
+
675
+ // Catch-all can't filter/transform
676
+ WSv2._notifyCatchAllListeners(lGroup, msg)
677
+
678
+ if (!lGroup[eventName] || lGroup[eventName].length === 0) return
679
+
680
+ const listeners = lGroup[eventName].filter((listener) => {
681
+ const { filter } = listener
682
+
683
+ if (!filter) return true
684
+
685
+ // inspect snapshots for matching packets
686
+ if (Array.isArray(data[0])) {
687
+ const matchingData = data.filter((item) => {
688
+ filterByData = msg.filterOverride ? msg.filterOverride : item
689
+
690
+ return WSv2._payloadPassesFilter(filterByData, filter)
691
+ })
692
+
693
+ return matchingData.length !== 0
694
+ }
695
+
696
+ // inspect single packet
697
+ filterByData = msg.filterOverride ? msg.filterOverride : data
698
+
699
+ return WSv2._payloadPassesFilter(filterByData, filter)
700
+ })
701
+
702
+ if (listeners.length === 0) return
703
+
704
+ listeners.forEach(({ cb, modelClass }) => {
705
+ const ModelClass = modelClass
706
+
707
+ if (!transform || data.length === 0) {
708
+ cb(data)
709
+ } else if (Array.isArray(data[0])) {
710
+ cb(data.map((entry) => {
711
+ return new ModelClass(entry, ws)
712
+ }))
713
+ } else {
714
+ cb(new ModelClass(data, ws))
715
+ }
716
+ })
717
+ }
718
+
719
+ /**
720
+ * @param {Array} payload
721
+ * @param {Object} filter
722
+ * @return {boolean} pass
723
+ * @private
724
+ */
725
+ static _payloadPassesFilter (payload, filter) {
726
+ const filterIndices = Object.keys(filter)
727
+
728
+ for (let k = 0; k < filterIndices.length; k++) {
729
+ if (!filter[filterIndices[k]]) continue // no value provided
730
+
731
+ if (payload[+filterIndices[k]] !== filter[filterIndices[k]]) {
732
+ return false
733
+ }
734
+ }
735
+
736
+ return true
737
+ }
738
+
739
+ /**
740
+ * @param {Object} lGroup - listener group keyed by event ('' in this case)
741
+ * @param {*} data - packet to pass to listeners
742
+ * @private
743
+ */
744
+ static _notifyCatchAllListeners (lGroup, data) {
745
+ if (!lGroup['']) return
746
+
747
+ for (let j = 0; j < lGroup[''].length; j++) {
748
+ lGroup[''][j].cb(data)
749
+ }
750
+ }
751
+
752
+ /**
753
+ * @param {Object} msg
754
+ * @private
755
+ */
756
+ _handleEventMessage (msg) {
757
+ if (msg.event === 'auth') {
758
+ return this._handleAuthEvent(msg)
759
+ } else if (msg.event === 'subscribed') {
760
+ return this._handleSubscribedEvent(msg)
761
+ } else if (msg.event === 'unsubscribed') {
762
+ return this._handleUnsubscribedEvent(msg)
763
+ } else if (msg.event === 'info') {
764
+ return this._handleInfoEvent(msg)
765
+ } else if (msg.event === 'conf') {
766
+ return this._handleConfigEvent(msg)
767
+ } else if (msg.event === 'error') {
768
+ return this._handleErrorEvent(msg)
769
+ }
770
+
771
+ debug('recv unknown event message: %j', msg)
772
+ return null
773
+ }
774
+
775
+ /**
776
+ * @param {Object} msg
777
+ * @private
778
+ */
779
+ _handleConfigEvent (msg) {
780
+ if (msg.status !== 'OK') {
781
+ debug('config failed: %j', msg)
782
+ return this.emit('error', msg)
783
+ }
784
+ }
785
+
786
+ /**
787
+ * @param {Object} msg
788
+ * @private
789
+ */
790
+ _handleErrorEvent (msg) {
791
+ this.emit('error', msg)
792
+ }
793
+
794
+ /**
795
+ * @param {Object} msg
796
+ * @private
797
+ */
798
+ _handleAuthEvent (msg) {
799
+ if (msg.status !== 'OK') {
800
+ debug('auth failed: %j', msg)
801
+ return this.emit('error', msg)
802
+ }
803
+
804
+ this._channelMap[msg.chanId] = { channel: 'auth' }
805
+ this._isAuthenticated = true
806
+ this._wasEverAuthenticated = true
807
+
808
+ this.emit('auth', msg)
809
+ debug('authenticated!')
810
+ }
811
+
812
+ /**
813
+ * @param {Object} msg
814
+ * @private
815
+ */
816
+ _handleSubscribedEvent (msg) {
817
+ this._channelMap[msg.chanId] = msg
818
+
819
+ debug('subscribed to %s [%d]', msg.channel, msg.chanId)
820
+ this.emit('subscribed', msg)
821
+ }
822
+
823
+ /**
824
+ * @param {Object} msg
825
+ * @private
826
+ */
827
+ _handleUnsubscribedEvent (msg) {
828
+ delete this._channelMap[msg.chanId]
829
+
830
+ debug('unsubscribed from %s [%d]', msg.channel, msg.chanId)
831
+ this.emit('unsubscribed', msg)
832
+ }
833
+
834
+ /**
835
+ * @param {Object} msg
836
+ * @private
837
+ */
838
+ _handleInfoEvent (msg) {
839
+ if (msg.version) {
840
+ if (msg.version !== 2) {
841
+ const err = new Error(`server not running API v2: v${msg.version}`)
842
+
843
+ this.emit('error', err)
844
+ this.close()
845
+ return
846
+ } else {
847
+ debug('server running API v2')
848
+ }
849
+ } else if (msg.code && this._infoListeners[msg.code]) {
850
+ this._infoListeners[msg.code].forEach(cb => cb(msg))
851
+ }
852
+
853
+ this.emit('info', msg)
854
+ }
855
+
856
+ /**
857
+ * Subscribes and tracks subscriptions per channel/identifier pair. If
858
+ * already subscribed to the specified pair, nothing happens.
859
+ *
860
+ * @param {string} channel
861
+ * @param {string} identifier - for uniquely identifying the ref count
862
+ * @param {Object} payload - merged with sub packet
863
+ * @return {boolean} subSent
864
+ */
865
+ managedSubscribe (channel = '', identifier = '', payload = {}) {
866
+ const key = `${channel}:${identifier}`
867
+
868
+ if (this._subscriptionRefs[key]) {
869
+ this._subscriptionRefs[key]++
870
+ return false
871
+ }
872
+
873
+ this._subscriptionRefs[key] = 1
874
+ this.subscribe(channel, payload)
875
+
876
+ return true
877
+ }
878
+
879
+ /**
880
+ * @param {string} channel
881
+ * @param {string} identifier
882
+ * @return {boolean} unsubSent
883
+ */
884
+ managedUnsubscribe (channel = '', identifier = '') {
885
+ const key = `${channel}:${identifier}`
886
+ const chanId = this._chanIdByIdentifier(channel, identifier)
887
+
888
+ if (chanId === null || isNaN(this._subscriptionRefs[key])) return false
889
+
890
+ this._subscriptionRefs[key]--
891
+ if (this._subscriptionRefs[key] > 0) return false
892
+
893
+ this.unsubscribe(chanId)
894
+ delete this._subscriptionRefs[key]
895
+
896
+ return true
897
+ }
898
+
899
+ /**
900
+ * @param {Object} opts
901
+ * @param {number} opts.chanId
902
+ * @param {string} opts.channel - optional
903
+ * @param {string} opts.symbol - optional
904
+ * @param {string} opts.key - optional
905
+ * @return {Object} chanData - null if not found
906
+ */
907
+ getChannelData ({ chanId, channel, symbol, key }) {
908
+ const id = chanId || this._chanIdByIdentifier(channel, symbol || key)
909
+
910
+ return this._channelMap[id] || null
911
+ }
912
+
913
+ /**
914
+ * @param {string} channel
915
+ * @param {string} identifier
916
+ * @private
917
+ */
918
+ _chanIdByIdentifier (channel, identifier) {
919
+ const channelIds = Object.keys(this._channelMap)
920
+ let chan
921
+
922
+ for (let i = 0; i < channelIds.length; i++) {
923
+ chan = this._channelMap[channelIds[i]]
924
+
925
+ if (chan.channel === channel && (
926
+ chan.symbol === identifier ||
927
+ chan.key === identifier
928
+ )) {
929
+ return channelIds[i]
930
+ }
931
+ }
932
+
933
+ return null
934
+ }
935
+
936
+ /**
937
+ * @param {string} key
938
+ * @private
939
+ */
940
+ _getEventPromise (key) {
941
+ return new Promise((resolve, reject) => {
942
+ this._eventCallbacks.push(key, (err, res) => {
943
+ if (err) {
944
+ return reject(err)
945
+ }
946
+
947
+ resolve(res)
948
+ })
949
+ })
950
+ }
951
+
952
+ /**
953
+ * Send a packet to the WS server
954
+ *
955
+ * @param {*} msg - packet, gets stringified
956
+ */
957
+ send (msg) {
958
+ if (!this._ws) {
959
+ return this.emit('error', new Error('ws not open'))
960
+ }
961
+
962
+ debug('sending %j', msg)
963
+ this._ws.send(JSON.stringify(msg))
964
+ }
965
+
966
+ /**
967
+ * Configures the seq flag to enable sequencing (packet number) for this
968
+ * connection. When enabled, the seq number will be the last value of
969
+ * channel packet arrays.
970
+ *
971
+ * @param {Object} args
972
+ * @param {boolean} args.audit - if true, an error is emitted on invalid seq
973
+ */
974
+ enableSequencing (args = { audit: true }) {
975
+ this._seqAudit = args.audit === true
976
+
977
+ this.send({
978
+ event: 'conf',
979
+ flags: 65536
980
+ })
981
+ }
982
+
983
+ /**
984
+ * Register a callback in case of a ws server restart message; Use this to
985
+ * call reconnect() if needed. (code 20051)
986
+ *
987
+ * @param {method} cb
988
+ */
989
+ onServerRestart (cb) {
990
+ this.onInfoMessage(WSv2.info.SERVER_RESTART, cb)
991
+ }
992
+
993
+ /**
994
+ * Register a callback in case of a 'maintenance started' message from the
995
+ * server. This is a good time to pause server packets until maintenance ends
996
+ *
997
+ * @param {method} cb
998
+ */
999
+ onMaintenanceStart (cb) {
1000
+ this.onInfoMessage(WSv2.info.MAINTENANCE_START, cb)
1001
+ }
1002
+
1003
+ /**
1004
+ * Register a callback to be notified of a maintenance period ending
1005
+ *
1006
+ * @param {method} cb
1007
+ */
1008
+ onMaintenanceEnd (cb) {
1009
+ this.onInfoMessage(WSv2.info.MAINTENANCE_END, cb)
1010
+ }
1011
+
1012
+ /**
1013
+ * @param {string} channel
1014
+ * @param {Object} payload - optional extra packet data
1015
+ */
1016
+ subscribe (channel, payload) {
1017
+ this.send(Object.assign({
1018
+ event: 'subscribe',
1019
+ channel
1020
+ }, payload))
1021
+ }
1022
+
1023
+ /**
1024
+ * @param {string} symbol
1025
+ * @return {boolean} subscribed
1026
+ */
1027
+ subscribeTicker (symbol) {
1028
+ return this.managedSubscribe('ticker', symbol, { symbol })
1029
+ }
1030
+
1031
+ /**
1032
+ * @param {string} symbol
1033
+ * @return {boolean} subscribed
1034
+ */
1035
+ subscribeTrades (symbol) {
1036
+ return this.managedSubscribe('trades', symbol, { symbol })
1037
+ }
1038
+
1039
+ /**
1040
+ * @param {string} symbol
1041
+ * @param {string} prec - P0, P1, P2, or P3 (default P0)
1042
+ * @param {string} len - 25 or 100 (default 25)
1043
+ * @return {boolean} subscribed
1044
+ */
1045
+ subscribeOrderBook (symbol, prec = 'P0', len = '25') {
1046
+ return this.managedSubscribe('book', symbol, { symbol, len, prec })
1047
+ }
1048
+
1049
+ /**
1050
+ * @param {string} key
1051
+ * @return {boolean} subscribed
1052
+ */
1053
+ subscribeCandles (key) {
1054
+ return this.managedSubscribe('candles', key, { key })
1055
+ }
1056
+
1057
+ /**
1058
+ * @param {number} chanId
1059
+ */
1060
+ unsubscribe (chanId) {
1061
+ this.send({
1062
+ event: 'unsubscribe',
1063
+ chanId: +chanId
1064
+ })
1065
+ }
1066
+
1067
+ /**
1068
+ * @param {string} symbol
1069
+ * @return {boolean} unsubscribed
1070
+ */
1071
+ unsubscribeTicker (symbol) {
1072
+ return this.managedUnsubscribe('ticker', symbol)
1073
+ }
1074
+
1075
+ /**
1076
+ * @param {string} symbol
1077
+ * @return {boolean} unsubscribed
1078
+ */
1079
+ unsubscribeTrades (symbol) {
1080
+ return this.managedUnsubscribe('trades', symbol)
1081
+ }
1082
+
1083
+ /**
1084
+ * @param {string} symbol
1085
+ * @param {string} prec - P0, P1, P2, or P3 (default P0)
1086
+ * @param {string} len - 25 or 100 (default 25)
1087
+ * @return {boolean} unsubscribed
1088
+ */
1089
+ unsubscribeOrderBook (symbol, prec = 'P0', len = '25') {
1090
+ return this.managedUnsubscribe('book', symbol)
1091
+ }
1092
+
1093
+ /**
1094
+ * @param {string} symbol
1095
+ * @param {string} frame - time frame
1096
+ * @return {boolean} unsubscribed
1097
+ */
1098
+ unsubscribeCandles (symbol, frame) {
1099
+ return this.managedUnsubscribe('candles', `trade:${frame}:${symbol}`)
1100
+ }
1101
+
1102
+ /**
1103
+ * @param {string} cbGID
1104
+ */
1105
+ removeListeners (cbGID) {
1106
+ delete this._listeners[cbGID]
1107
+ }
1108
+
1109
+ /**
1110
+ * @param {string[]} prefixes
1111
+ */
1112
+ requestCalc (prefixes) {
1113
+ this._sendCalc([0, 'calc', null, prefixes.map(p => [p])])
1114
+ }
1115
+
1116
+ /**
1117
+ * Throttled call to ws.send, max 8 op/s
1118
+ *
1119
+ * @param {Array} msg
1120
+ * @private
1121
+ */
1122
+ _sendCalc (msg) {
1123
+ debug('req calc: %j', msg)
1124
+
1125
+ this._ws.send(msg)
1126
+ }
1127
+
1128
+ /**
1129
+ * Sends a new order to the server and resolves the returned promise once the
1130
+ * order submit is confirmed. Emits an error if not authenticated. The order
1131
+ * can be either an array, key/value map, or Order object instance.
1132
+ *
1133
+ * @param {Object|Array} order
1134
+ * @return {Promise} p - resolves on submit notification
1135
+ */
1136
+ submitOrder (order) {
1137
+ if (!this._isAuthenticated) {
1138
+ return Promise.reject(new Error('not authenticated'))
1139
+ }
1140
+
1141
+ const packet = Array.isArray(order)
1142
+ ? order
1143
+ : order instanceof Order
1144
+ ? order.toNewOrderPacket()
1145
+ : new Order(order).toNewOrderPacket()
1146
+
1147
+ this._sendOrderPacket([0, 'on', null, packet])
1148
+
1149
+ return this._getEventPromise(`order-new-${packet.cid}`)
1150
+ }
1151
+
1152
+ /**
1153
+ * Cancels an order by ID and resolves the returned promise once the cancel is
1154
+ * confirmed. Emits an error if not authenticated. The ID can be passed as a
1155
+ * number, or taken from an order array/object.
1156
+ *
1157
+ * @param {Object|Array|number} order
1158
+ * @return {Promise} p
1159
+ */
1160
+ cancelOrder (order) {
1161
+ if (!this._isAuthenticated) {
1162
+ return Promise.reject(new Error('not authenticated'))
1163
+ }
1164
+
1165
+ const id = typeof order === 'number'
1166
+ ? order
1167
+ : Array.isArray(order)
1168
+ ? order[0]
1169
+ : order.id
1170
+
1171
+ debug(`cancelling order ${id}`)
1172
+ this._sendOrderPacket([0, 'oc', null, { id }])
1173
+
1174
+ return this._getEventPromise(`order-cancel-${id}`)
1175
+ }
1176
+
1177
+ /**
1178
+ * Cancels multiple orders, returns a promise that resolves once all
1179
+ * operations are confirmed.
1180
+ *
1181
+ * @see cancelOrder
1182
+ *
1183
+ * @param {Object[]|Array[]|number[]} orders
1184
+ * @return {Promise} p
1185
+ */
1186
+ cancelOrders (orders) {
1187
+ if (!this._isAuthenticated) {
1188
+ return Promise.reject(new Error('not authenticated'))
1189
+ }
1190
+
1191
+ return Promise.all(orders.map((order) => {
1192
+ return this.cancelOrder(order)
1193
+ }))
1194
+ }
1195
+
1196
+ /**
1197
+ * Sends the op payloads to the server as an 'ox_multi' command. A promise is
1198
+ * returned and resolves immediately if authenticated, as no confirmation is
1199
+ * available for this message type.
1200
+ *
1201
+ * @param {Object[]} opPayloads
1202
+ * @return {Promise} p - rejects if not authenticated
1203
+ */
1204
+ submitOrderMultiOp (opPayloads) {
1205
+ if (!this._isAuthenticated) {
1206
+ return Promise.reject(new Error('not authenticated'))
1207
+ }
1208
+
1209
+ this.send([0, 'ox_multi', null, opPayloads])
1210
+
1211
+ return Promise.resolve() // TODO: multi-op tracking
1212
+ }
1213
+
1214
+ /**
1215
+ * @param {array} packet
1216
+ * @private
1217
+ */
1218
+ _sendOrderPacket (packet) {
1219
+ if (this._hasOrderBuff()) {
1220
+ this._ensureOrderBuffTimeout()
1221
+ this._orderOpBuffer.push(packet)
1222
+ } else {
1223
+ this.send(packet)
1224
+ }
1225
+ }
1226
+
1227
+ /**
1228
+ * @return {boolean} buffEnabled
1229
+ * @private
1230
+ */
1231
+ _hasOrderBuff () {
1232
+ return this._orderOpBufferDelay > 0
1233
+ }
1234
+
1235
+ /**
1236
+ * @private
1237
+ */
1238
+ _ensureOrderBuffTimeout () {
1239
+ if (this._orderOpTimeout !== null) return
1240
+
1241
+ this._orderOpTimeout = setTimeout(
1242
+ this._flushOrderOps.bind(this),
1243
+ this._orderOpBufferDelay
1244
+ )
1245
+ }
1246
+
1247
+ /**
1248
+ * Splits the op buffer into packets of max 15 ops each, and sends them down
1249
+ * the wire.
1250
+ *
1251
+ * @return {Promise} p - resolves after send
1252
+ * @private
1253
+ */
1254
+ _flushOrderOps () {
1255
+ this._orderOpTimeout = null
1256
+
1257
+ const packets = this._orderOpBuffer.map(p => [p[1], p[3]])
1258
+ this._orderOpBuffer = []
1259
+
1260
+ if (packets.length <= 15) {
1261
+ return this.submitOrderMultiOp(packets)
1262
+ }
1263
+
1264
+ const promises = []
1265
+
1266
+ while (packets.length > 0) {
1267
+ const opPackets = packets.splice(0, Math.min(packets.length, 15))
1268
+ promises.push(this.submitOrderMultiOp(opPackets))
1269
+ }
1270
+
1271
+ return Promise.all(promises)
1272
+ }
1273
+
1274
+ /**
1275
+ * @return {boolean} authenticated
1276
+ */
1277
+ isAuthenticated () {
1278
+ return this._isAuthenticated
1279
+ }
1280
+
1281
+ /**
1282
+ * @return {boolean} open
1283
+ */
1284
+ isOpen () {
1285
+ return this._isOpen
1286
+ }
1287
+
1288
+ /**
1289
+ * Adds a listener to the internal listener set, with an optional grouping
1290
+ * for batch unsubscribes (GID) & automatic ws packet matching (filterKey)
1291
+ *
1292
+ * @param {string} eventName - as received on ws stream
1293
+ * @param {Object} filter - map of index & value in ws packet
1294
+ * @param {object} modelClass - model to use for serialization
1295
+ * @param {string} cbGID - listener group ID for mass removal
1296
+ * @param {method} cb - listener
1297
+ * @private
1298
+ */
1299
+ _registerListener (eventName, filter, modelClass, cbGID, cb) {
1300
+ if (!cbGID) cbGID = null
1301
+
1302
+ if (!this._listeners[cbGID]) {
1303
+ this._listeners[cbGID] = { [eventName]: [] }
1304
+ }
1305
+
1306
+ const listeners = this._listeners[cbGID]
1307
+
1308
+ if (!listeners[eventName]) {
1309
+ listeners[eventName] = []
1310
+ }
1311
+
1312
+ const l = {
1313
+ cb,
1314
+ modelClass,
1315
+ filter
1316
+ }
1317
+
1318
+ listeners[eventName].push(l)
1319
+ }
1320
+
1321
+ /**
1322
+ * Registers a new callback to be called when a matching info message is
1323
+ * received.
1324
+ *
1325
+ * @param {number} code - from WSv2.info.*
1326
+ * @param {method} cb
1327
+ */
1328
+ onInfoMessage (code, cb) {
1329
+ if (!this._infoListeners[code]) {
1330
+ this._infoListeners[code] = []
1331
+ }
1332
+
1333
+ this._infoListeners[code].push(cb)
1334
+ }
1335
+
1336
+ /**
1337
+ * @param {Object} opts
1338
+ * @param {string} opts.cbGID - callback group id
1339
+ * @param {Method} cb
1340
+ */
1341
+ onMessage ({ cbGID }, cb) {
1342
+ this._registerListener('', null, null, cbGID, cb)
1343
+ }
1344
+
1345
+ /**
1346
+ * @param {Object} opts
1347
+ * @param {string} opts.key - candle set key, i.e. trade:30m:tBTCUSD
1348
+ * @param {string} opts.cbGID - callback group id
1349
+ * @param {Method} cb
1350
+ * @see https://docs.bitfinex.com/v2/reference#ws-public-candle
1351
+ */
1352
+ onCandle ({ key, cbGID }, cb) {
1353
+ this._registerListener('candle', { 0: key }, Candle, cbGID, cb)
1354
+ }
1355
+
1356
+ /**
1357
+ * @param {Object} opts
1358
+ * @param {string} opts.symbol
1359
+ * @param {string} opts.prec
1360
+ * @param {string} opts.len
1361
+ * @param {string} opts.cbGID - callback group id
1362
+ * @param {Method} cb
1363
+ * @see https://docs.bitfinex.com/v2/reference#ws-public-order-books
1364
+ */
1365
+ onOrderBook ({ symbol, prec, len, cbGID }, cb) {
1366
+ this._registerListener('orderbook', {
1367
+ 0: symbol,
1368
+ 1: prec,
1369
+ 2: len
1370
+ }, OrderBook, cbGID, cb)
1371
+ }
1372
+
1373
+ /**
1374
+ * @param {Object} opts
1375
+ * @param {string} opts.pair
1376
+ * @param {string} opts.cbGID - callback group id
1377
+ * @param {Method} cb
1378
+ * @see https://docs.bitfinex.com/v2/reference#ws-public-trades
1379
+ */
1380
+ onTrades ({ pair, cbGID }, cb) {
1381
+ this._registerListener('trades', { 0: pair }, TradeTick, cbGID, cb)
1382
+ }
1383
+
1384
+ /**
1385
+ * @param {Object} opts
1386
+ * @param {string} opts.symbol
1387
+ * @param {string} opts.cbGID - callback group id
1388
+ * @param {Method} cb
1389
+ * @see https://docs.bitfinex.com/v2/reference#ws-public-ticker
1390
+ */
1391
+ onTicker ({ symbol, cbGID }, cb) {
1392
+ this._registerListener('ticker', { 0: symbol }, Tick, cbGID, cb)
1393
+ }
1394
+
1395
+ /**
1396
+ * @param {Object} opts
1397
+ * @param {string} opts.symbol
1398
+ * @param {number} opts.gid
1399
+ * @param {string} opts.cbGID - callback group id
1400
+ * @param {Method} cb
1401
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
1402
+ */
1403
+ onOrderSnapshot ({ symbol, gid, cbGID }, cb) {
1404
+ this._registerListener('os', { 3: symbol, 1: gid }, Order, cbGID, cb)
1405
+ }
1406
+
1407
+ /**
1408
+ * @param {Object} opts
1409
+ * @param {string} opts.symbol
1410
+ * @param {number} opts.gid
1411
+ * @param {string} opts.cbGID - callback group id
1412
+ * @param {Method} cb
1413
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
1414
+ */
1415
+ onOrderNew ({ symbol, gid, cbGID }, cb) {
1416
+ this._registerListener('on', { 3: symbol, 1: gid }, Order, cbGID, cb)
1417
+ }
1418
+
1419
+ /**
1420
+ * @param {Object} opts
1421
+ * @param {string} opts.symbol
1422
+ * @param {number} opts.gid
1423
+ * @param {number} opts.cid
1424
+ * @param {string} opts.cbGID - callback group id
1425
+ * @param {Method} cb
1426
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
1427
+ */
1428
+ onOrderUpdate ({ symbol, gid, cid, cbGID }, cb) {
1429
+ this._registerListener('ou', { 3: symbol, 1: gid, 2: cid }, Order, cbGID, cb)
1430
+ }
1431
+
1432
+ /**
1433
+ * @param {Object} opts
1434
+ * @param {string} opts.symbol
1435
+ * @param {number} opts.gid
1436
+ * @param {number} opts.cid
1437
+ * @param {string} opts.cbGID - callback group id
1438
+ * @param {Method} cb
1439
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-orders
1440
+ */
1441
+ onOrderClose ({ symbol, gid, cid, cbGID }, cb) {
1442
+ this._registerListener('oc', { 3: symbol, 1: gid, 2: cid }, Order, cbGID, cb)
1443
+ }
1444
+
1445
+ /**
1446
+ * @param {Object} opts
1447
+ * @param {string} opts.symbol
1448
+ * @param {string} opts.cbGID - callback group id
1449
+ * @param {Method} cb
1450
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
1451
+ */
1452
+ onPositionSnapshot ({ symbol, cbGID }, cb) {
1453
+ this._registerListener('ps', { 0: symbol }, Position, cbGID, cb)
1454
+ }
1455
+
1456
+ /**
1457
+ * @param {Object} opts
1458
+ * @param {string} opts.symbol
1459
+ * @param {string} opts.cbGID - callback group id
1460
+ * @param {Method} cb
1461
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
1462
+ */
1463
+ onPositionNew ({ symbol, cbGID }, cb) {
1464
+ this._registerListener('pn', { 0: symbol }, Position, cbGID, cb)
1465
+ }
1466
+
1467
+ /**
1468
+ * @param {Object} opts
1469
+ * @param {string} opts.symbol
1470
+ * @param {string} opts.cbGID - callback group id
1471
+ * @param {Method} cb
1472
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
1473
+ */
1474
+ onPositionUpdate ({ symbol, cbGID }, cb) {
1475
+ this._registerListener('pu', { 0: symbol }, Position, cbGID, cb)
1476
+ }
1477
+
1478
+ /**
1479
+ * @param {Object} opts
1480
+ * @param {string} opts.symbol
1481
+ * @param {string} opts.cbGID - callback group id
1482
+ * @param {Method} cb
1483
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-position
1484
+ */
1485
+ onPositionClose ({ symbol, cbGID }, cb) {
1486
+ this._registerListener('pc', { 0: symbol }, Position, cbGID, cb)
1487
+ }
1488
+
1489
+ /**
1490
+ * @param {Object} opts
1491
+ * @param {string} opts.pair
1492
+ * @param {string} opts.cbGID - callback group id
1493
+ * @param {Method} cb
1494
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-trades
1495
+ */
1496
+ onTradeEntry ({ pair, cbGID }, cb) {
1497
+ this._registerListener('te', { 0: pair }, Trade, cbGID, cb)
1498
+ }
1499
+
1500
+ /**
1501
+ * @param {Object} opts
1502
+ * @param {string} opts.pair
1503
+ * @param {string} opts.cbGID - callback group id
1504
+ * @param {Method} cb
1505
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-trades
1506
+ */
1507
+ onTradeUpdate ({ pair, cbGID }, cb) {
1508
+ this._registerListener('tu', { 0: pair }, Trade, cbGID, cb)
1509
+ }
1510
+
1511
+ /**
1512
+ * @param {Object} opts
1513
+ * @param {string} opts.symbol
1514
+ * @param {string} opts.cbGID - callback group id
1515
+ * @param {Method} cb
1516
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
1517
+ */
1518
+ onFundingOfferSnapshot ({ symbol, cbGID }, cb) {
1519
+ this._registerListener('fos', { 1: symbol }, FundingOffer, cbGID, cb)
1520
+ }
1521
+
1522
+ /**
1523
+ * @param {Object} opts
1524
+ * @param {string} opts.symbol
1525
+ * @param {string} opts.cbGID - callback group id
1526
+ * @param {Method} cb
1527
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
1528
+ */
1529
+ onFundingOfferNew ({ symbol, cbGID }, cb) {
1530
+ this._registerListener('fon', { 1: symbol }, FundingOffer, cbGID, cb)
1531
+ }
1532
+
1533
+ /**
1534
+ * @param {Object} opts
1535
+ * @param {string} opts.symbol
1536
+ * @param {string} opts.cbGID - callback group id
1537
+ * @param {Method} cb
1538
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
1539
+ */
1540
+ onFundingOfferUpdate ({ symbol, cbGID }, cb) {
1541
+ this._registerListener('fou', { 1: symbol }, FundingOffer, cbGID, cb)
1542
+ }
1543
+
1544
+ /**
1545
+ * @param {Object} opts
1546
+ * @param {string} opts.symbol
1547
+ * @param {string} opts.cbGID - callback group id
1548
+ * @param {Method} cb
1549
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-offers
1550
+ */
1551
+ onFundingOfferClose ({ symbol, cbGID }, cb) {
1552
+ this._registerListener('foc', { 1: symbol }, FundingOffer, cbGID, cb)
1553
+ }
1554
+
1555
+ /**
1556
+ * @param {Object} opts
1557
+ * @param {string} opts.symbol
1558
+ * @param {string} opts.cbGID - callback group id
1559
+ * @param {Method} cb
1560
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
1561
+ */
1562
+ onFundingCreditSnapshot ({ symbol, cbGID }, cb) {
1563
+ this._registerListener('fcs', { 1: symbol }, FundingCredit, cbGID, cb)
1564
+ }
1565
+
1566
+ /**
1567
+ * @param {Object} opts
1568
+ * @param {string} opts.symbol
1569
+ * @param {string} opts.cbGID - callback group id
1570
+ * @param {Method} cb
1571
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
1572
+ */
1573
+ onFundingCreditNew ({ symbol, cbGID }, cb) {
1574
+ this._registerListener('fcn', { 1: symbol }, FundingCredit, cbGID, cb)
1575
+ }
1576
+
1577
+ /**
1578
+ * @param {Object} opts
1579
+ * @param {string} opts.symbol
1580
+ * @param {string} opts.cbGID - callback group id
1581
+ * @param {Method} cb
1582
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
1583
+ */
1584
+ onFundingCreditUpdate ({ symbol, cbGID }, cb) {
1585
+ this._registerListener('fcu', { 1: symbol }, FundingCredit, cbGID, cb)
1586
+ }
1587
+
1588
+ /**
1589
+ * @param {Object} opts
1590
+ * @param {string} opts.symbol
1591
+ * @param {string} opts.cbGID - callback group id
1592
+ * @param {Method} cb
1593
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-credits
1594
+ */
1595
+ onFundingCreditClose ({ symbol, cbGID }, cb) {
1596
+ this._registerListener('fcc', { 1: symbol }, FundingCredit, cbGID, cb)
1597
+ }
1598
+
1599
+ /**
1600
+ * @param {Object} opts
1601
+ * @param {string} opts.symbol
1602
+ * @param {string} opts.cbGID - callback group id
1603
+ * @param {Method} cb
1604
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
1605
+ */
1606
+ onFundingLoanSnapshot ({ symbol, cbGID }, cb) {
1607
+ this._registerListener('fls', { 1: symbol }, FundingLoan, cbGID, cb)
1608
+ }
1609
+
1610
+ /**
1611
+ * @param {Object} opts
1612
+ * @param {string} opts.symbol
1613
+ * @param {string} opts.cbGID - callback group id
1614
+ * @param {Method} cb
1615
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
1616
+ */
1617
+ onFundingLoanNew ({ symbol, cbGID }, cb) {
1618
+ this._registerListener('fln', { 1: symbol }, FundingLoan, cbGID, cb)
1619
+ }
1620
+
1621
+ /**
1622
+ * @param {Object} opts
1623
+ * @param {string} opts.symbol
1624
+ * @param {string} opts.cbGID - callback group id
1625
+ * @param {Method} cb
1626
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
1627
+ */
1628
+ onFundingLoanUpdate ({ symbol, cbGID }, cb) {
1629
+ this._registerListener('flu', { 1: symbol }, FundingLoan, cbGID, cb)
1630
+ }
1631
+
1632
+ /**
1633
+ * @param {Object} opts
1634
+ * @param {string} opts.symbol
1635
+ * @param {string} opts.cbGID - callback group id
1636
+ * @param {Method} cb
1637
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-loans
1638
+ */
1639
+ onFundingLoanClose ({ symbol, cbGID }, cb) {
1640
+ this._registerListener('flc', { 1: symbol }, FundingLoan, cbGID, cb)
1641
+ }
1642
+
1643
+ /**
1644
+ * @param {Object} opts
1645
+ * @param {string} opts.cbGID - callback group id
1646
+ * @param {Method} cb
1647
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-wallets
1648
+ */
1649
+ onWalletSnapshot ({ cbGID }, cb) {
1650
+ this._registerListener('ws', null, Wallet, cbGID, cb)
1651
+ }
1652
+
1653
+ /**
1654
+ * @param {Object} opts
1655
+ * @param {string} opts.cbGID - callback group id
1656
+ * @param {Method} cb
1657
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-wallets
1658
+ */
1659
+ onWalletUpdate ({ cbGID }, cb) {
1660
+ this._registerListener('wu', null, Wallet, cbGID, cb)
1661
+ }
1662
+
1663
+ /**
1664
+ * @param {Object} opts
1665
+ * @param {string} opts.cbGID - callback group id
1666
+ * @param {Method} cb
1667
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-balance
1668
+ */
1669
+ onBalanceInfoUpdate ({ cbGID }, cb) {
1670
+ this._registerListener('bu', null, BalanceInfo, cbGID, cb)
1671
+ }
1672
+
1673
+ /**
1674
+ * @param {Object} opts
1675
+ * @param {string} opts.cbGID - callback group id
1676
+ * @param {Method} cb
1677
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-margin
1678
+ */
1679
+ onMarginInfoUpdate ({ cbGID }, cb) {
1680
+ this._registerListener('miu', null, MarginInfo, cbGID, cb)
1681
+ }
1682
+
1683
+ /**
1684
+ * @param {Object} opts
1685
+ * @param {string} opts.cbGID - callback group id
1686
+ * @param {Method} cb
1687
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding
1688
+ */
1689
+ onFundingInfoUpdate ({ cbGID }, cb) {
1690
+ this._registerListener('fiu', null, FundingInfo, cbGID, cb)
1691
+ }
1692
+
1693
+ /**
1694
+ * @param {Object} opts
1695
+ * @param {string} opts.symbol
1696
+ * @param {string} opts.cbGID - callback group id
1697
+ * @param {Method} cb
1698
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding-trades
1699
+ */
1700
+ onFundingTradeEntry ({ symbol, cbGID }, cb) {
1701
+ this._registerListener('fte', { 1: symbol }, FundingTrade, cbGID, cb)
1702
+ }
1703
+
1704
+ /**
1705
+ * @param {Object} opts
1706
+ * @param {string} opts.symbol
1707
+ * @param {string} opts.cbGID - callback group id
1708
+ * @param {Method} cb
1709
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-funding-trades
1710
+ */
1711
+ onFundingTradeUpdate ({ symbol, cbGID }, cb) {
1712
+ this._registerListener('ftu', { 1: symbol }, FundingTrade, cbGID, cb)
1713
+ }
1714
+
1715
+ /**
1716
+ * @param {Object} opts
1717
+ * @param {string} opts.type
1718
+ * @param {string} opts.cbGID - callback group id
1719
+ * @param {Method} cb
1720
+ * @see https://docs.bitfinex.com/v2/reference#ws-auth-notifications
1721
+ */
1722
+ onNotification ({ type, cbGID }, cb) {
1723
+ this._registerListener('n', { 1: type }, Notification, cbGID, cb)
1724
+ }
1725
+ }
1726
+
1727
+ WSv2.info = INFO_CODES
1728
+
1729
+ module.exports = WSv2