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.
- package/.istanbul.yml +53 -0
- package/.travis.yml +5 -0
- package/CHANGELOG +33 -0
- package/LICENSE.md +21 -0
- package/README.md +211 -0
- package/doc/order.md +160 -0
- package/doc/rest2.md +573 -0
- package/doc/ws2.md +925 -0
- package/examples/bfx.js +26 -0
- package/examples/rest2/order_history.js +29 -0
- package/examples/rest2/symbols.js +15 -0
- package/examples/rest2/tickers.js +24 -0
- package/examples/rest2/trade_history.js +28 -0
- package/examples/ws2/auth.js +31 -0
- package/examples/ws2/calc.js +33 -0
- package/examples/ws2/cancel_all.js +35 -0
- package/examples/ws2/cancel_all_buf.js +39 -0
- package/examples/ws2/candles.js +36 -0
- package/examples/ws2/info_events.js +40 -0
- package/examples/ws2/oc_multi.js +50 -0
- package/examples/ws2/order_books.js +37 -0
- package/examples/ws2/orders.js +67 -0
- package/examples/ws2/ox_multi.js +61 -0
- package/examples/ws2/sequencing.js +23 -0
- package/examples/ws2/ticker.js +20 -0
- package/examples/ws2/trades.js +27 -0
- package/index.js +24 -0
- package/lib/model.js +25 -0
- package/lib/models/alert.js +25 -0
- package/lib/models/balance_info.js +21 -0
- package/lib/models/candle.js +33 -0
- package/lib/models/funding_credit.js +61 -0
- package/lib/models/funding_info.js +16 -0
- package/lib/models/funding_loan.js +64 -0
- package/lib/models/funding_offer.js +60 -0
- package/lib/models/funding_trade.js +33 -0
- package/lib/models/index.js +23 -0
- package/lib/models/margin_info.js +29 -0
- package/lib/models/notification.js +31 -0
- package/lib/models/order.js +288 -0
- package/lib/models/order_book.js +214 -0
- package/lib/models/position.js +43 -0
- package/lib/models/tick.js +83 -0
- package/lib/models/trade.js +43 -0
- package/lib/models/trade_tick.js +29 -0
- package/lib/models/wallet.js +34 -0
- package/lib/transports/rest.js +391 -0
- package/lib/transports/rest2.js +597 -0
- package/lib/transports/ws.js +323 -0
- package/lib/transports/ws2.js +1729 -0
- package/lib/util/gen_auth_sig.js +23 -0
- package/lib/util/index.js +11 -0
- package/lib/util/is_snapshot.js +5 -0
- package/lib/util/nonce.js +5 -0
- package/package.json +39 -0
- package/test/fixtures/response-ticker-funding.json +1 -0
- package/test/fixtures/response-ticker-pairs.json +1 -0
- package/test/fixtures/response-trades-funding.json +1 -0
- package/test/fixtures/response-trades-pairs.json +1 -0
- package/test/fixtures/response-ws-1-orderbook-R0.json +51 -0
- package/test/fixtures/response-ws2-server-order-book-P0.json +1 -0
- package/test/fixtures/response-ws2-server-order-book-P1.json +1 -0
- package/test/fixtures/response-ws2-server-order-book-R0.json +1 -0
- package/test/fixtures/response-ws2-server-ticker-funding.json +1 -0
- package/test/fixtures/response-ws2-server-trades.json +1 -0
- package/test/helpers/test_model.js +71 -0
- package/test/index.js +131 -0
- package/test/lib/models/alert.js +12 -0
- package/test/lib/models/balance_info.js +12 -0
- package/test/lib/models/candle.js +12 -0
- package/test/lib/models/funding_credit.js +17 -0
- package/test/lib/models/funding_info.js +7 -0
- package/test/lib/models/funding_loan.js +17 -0
- package/test/lib/models/funding_offer.js +17 -0
- package/test/lib/models/funding_trade.js +15 -0
- package/test/lib/models/margin_info.js +15 -0
- package/test/lib/models/notification.js +14 -0
- package/test/lib/models/order.js +395 -0
- package/test/lib/models/order_book.js +188 -0
- package/test/lib/models/position.js +15 -0
- package/test/lib/models/tick.js +34 -0
- package/test/lib/models/trade.js +16 -0
- package/test/lib/models/trade_tick.js +14 -0
- package/test/lib/models/wallet.js +14 -0
- package/test/lib/transports/rest-1-integration.js +131 -0
- package/test/lib/transports/rest-2-integration.js +80 -0
- package/test/lib/transports/rest-2-issue-80-argument-length.js +61 -0
- package/test/lib/transports/rest-2-smoke-test.js +49 -0
- package/test/lib/transports/rest-2-unit.js +26 -0
- package/test/lib/transports/rest1.js +152 -0
- package/test/lib/transports/ws-1-handle-channel.js +83 -0
- package/test/lib/transports/ws-1-parsing.js +40 -0
- package/test/lib/transports/ws-1-test.js +275 -0
- package/test/lib/transports/ws2-integration.js +259 -0
- package/test/lib/transports/ws2-unit.js +1295 -0
- package/test/lib/util/is_snapshot.js +20 -0
- package/test/lib/util/nonce.js +20 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
/* eslint-env mocha */
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const assert = require('assert')
|
|
5
|
+
const WSv2 = require('../../../lib/transports/ws2')
|
|
6
|
+
const { MockWSv2Server } = require('bfx-api-mock-srv')
|
|
7
|
+
|
|
8
|
+
const API_KEY = 'dummy'
|
|
9
|
+
const API_SECRET = 'dummy'
|
|
10
|
+
|
|
11
|
+
const createTestWSv2Instance = (params = {}) => {
|
|
12
|
+
return new WSv2(Object.assign({
|
|
13
|
+
apiKey: API_KEY,
|
|
14
|
+
apiSecret: API_SECRET,
|
|
15
|
+
url: 'ws://localhost:9997'
|
|
16
|
+
}, params))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('WSv2 utilities', () => {
|
|
20
|
+
it('_registerListener: correctly adds listener to internal map with cbGID', () => {
|
|
21
|
+
const ws = new WSv2()
|
|
22
|
+
ws._registerListener('trade', { 2: 'tBTCUSD' }, Map, 42, () => {})
|
|
23
|
+
|
|
24
|
+
const { _listeners } = ws
|
|
25
|
+
|
|
26
|
+
assert.equal(Object.keys(_listeners).length, 1)
|
|
27
|
+
assert.equal(Object.keys(_listeners)[0], 42)
|
|
28
|
+
assert.equal(typeof _listeners[42], 'object')
|
|
29
|
+
|
|
30
|
+
const listenerSet = _listeners[42]
|
|
31
|
+
|
|
32
|
+
assert.equal(Object.keys(listenerSet).length, 1)
|
|
33
|
+
assert.equal(Object.keys(listenerSet)[0], 'trade')
|
|
34
|
+
assert.equal(listenerSet.trade.constructor.name, 'Array')
|
|
35
|
+
assert.equal(listenerSet.trade.length, 1)
|
|
36
|
+
|
|
37
|
+
const listener = listenerSet.trade[0]
|
|
38
|
+
|
|
39
|
+
assert.equal(listener.modelClass, Map)
|
|
40
|
+
assert.deepEqual(listener.filter, { '2': 'tBTCUSD' })
|
|
41
|
+
assert.equal(typeof listener.cb, 'function')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('enableSequencing: sends the correct conf flag', (done) => {
|
|
45
|
+
const ws = new WSv2()
|
|
46
|
+
ws.send = (packet) => {
|
|
47
|
+
assert.equal(packet.event, 'conf')
|
|
48
|
+
assert.equal(packet.flags, 65536)
|
|
49
|
+
done()
|
|
50
|
+
}
|
|
51
|
+
ws.enableSequencing()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('getCandles: returns empty array if no candle set is available', () => {
|
|
55
|
+
const ws = new WSv2()
|
|
56
|
+
assert.deepEqual(ws.getCandles('i.dont.exist'), [])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('WSv2 lifetime', () => {
|
|
61
|
+
it('starts unopened & unauthenticated', () => {
|
|
62
|
+
const ws = createTestWSv2Instance()
|
|
63
|
+
|
|
64
|
+
assert.equal(ws.isOpen(), false)
|
|
65
|
+
assert.equal(ws.isAuthenticated(), false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('open: fails to open twice', (done) => {
|
|
69
|
+
const wss = new MockWSv2Server()
|
|
70
|
+
const ws = createTestWSv2Instance()
|
|
71
|
+
ws.on('open', () => {
|
|
72
|
+
ws.open().then(() => assert(false)).catch(() => {
|
|
73
|
+
wss.close()
|
|
74
|
+
done()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
ws.open()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('open: updates open flag', (done) => {
|
|
81
|
+
const wss = new MockWSv2Server()
|
|
82
|
+
const ws = createTestWSv2Instance()
|
|
83
|
+
ws.on('open', () => {
|
|
84
|
+
assert.equal(ws.isOpen(), true)
|
|
85
|
+
wss.close()
|
|
86
|
+
done()
|
|
87
|
+
})
|
|
88
|
+
ws.open()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('close: doesn\'t close if not open', (done) => {
|
|
92
|
+
const ws = createTestWSv2Instance()
|
|
93
|
+
ws.close().then(() => assert(false)).catch(() => {
|
|
94
|
+
done()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('close: fails to close twice', (done) => {
|
|
99
|
+
const wss = new MockWSv2Server()
|
|
100
|
+
const ws = createTestWSv2Instance()
|
|
101
|
+
ws.open()
|
|
102
|
+
ws.on('open', ws.close.bind(ws))
|
|
103
|
+
ws.on('close', () => {
|
|
104
|
+
ws.close().then(() => assert(false)).catch(() => {
|
|
105
|
+
wss.close()
|
|
106
|
+
done()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('auth: fails to auth twice', (done) => {
|
|
112
|
+
const wss = new MockWSv2Server()
|
|
113
|
+
const ws = createTestWSv2Instance()
|
|
114
|
+
ws.open()
|
|
115
|
+
ws.on('open', ws.auth.bind(ws))
|
|
116
|
+
ws.once('auth', () => {
|
|
117
|
+
ws.auth().then(() => assert(false)).catch(() => {
|
|
118
|
+
wss.close()
|
|
119
|
+
done()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('auth: updates auth flag', (done) => {
|
|
125
|
+
const wss = new MockWSv2Server()
|
|
126
|
+
const ws = createTestWSv2Instance()
|
|
127
|
+
ws.open()
|
|
128
|
+
ws.on('open', ws.auth.bind(ws))
|
|
129
|
+
ws.once('auth', () => {
|
|
130
|
+
assert(ws.isAuthenticated())
|
|
131
|
+
wss.close()
|
|
132
|
+
done()
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('auth: forwards calc param', (done) => {
|
|
137
|
+
const wss = new MockWSv2Server()
|
|
138
|
+
const ws = createTestWSv2Instance()
|
|
139
|
+
ws.open()
|
|
140
|
+
ws.on('open', () => {
|
|
141
|
+
ws.send = (data) => {
|
|
142
|
+
assert.equal(data.calc, 42)
|
|
143
|
+
wss.close()
|
|
144
|
+
done()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
ws.auth(42)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reconnect: connects if not already connected', (done) => {
|
|
152
|
+
const wss = new MockWSv2Server()
|
|
153
|
+
const ws = createTestWSv2Instance()
|
|
154
|
+
|
|
155
|
+
ws.on('close', () => {
|
|
156
|
+
assert(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
ws.on('open', () => {
|
|
160
|
+
wss.close()
|
|
161
|
+
done()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
ws.reconnect()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('reconnect: disconnects & connects back if currently connected', (done) => {
|
|
168
|
+
const wss = new MockWSv2Server()
|
|
169
|
+
const ws = createTestWSv2Instance()
|
|
170
|
+
|
|
171
|
+
let calls = 0
|
|
172
|
+
|
|
173
|
+
ws.on('close', () => {
|
|
174
|
+
if (++calls === 2) {
|
|
175
|
+
wss.close()
|
|
176
|
+
done()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
ws.once('open', () => {
|
|
181
|
+
ws.reconnect()
|
|
182
|
+
|
|
183
|
+
ws.once('open', () => {
|
|
184
|
+
if (++calls === 2) {
|
|
185
|
+
wss.close()
|
|
186
|
+
done()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
ws.open()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('reconnect: automatically auths on open if previously authenticated', (done) => {
|
|
195
|
+
const wss = new MockWSv2Server()
|
|
196
|
+
const ws = createTestWSv2Instance()
|
|
197
|
+
|
|
198
|
+
let closed = false
|
|
199
|
+
let opened = false
|
|
200
|
+
|
|
201
|
+
ws.on('error', done)
|
|
202
|
+
|
|
203
|
+
ws.once('open', ws.auth.bind(ws))
|
|
204
|
+
ws.once('auth', () => {
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
ws.once('close', () => { closed = true })
|
|
207
|
+
ws.once('open', () => { opened = true })
|
|
208
|
+
ws.once('auth', () => {
|
|
209
|
+
assert(closed)
|
|
210
|
+
assert(opened)
|
|
211
|
+
wss.close()
|
|
212
|
+
done()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
ws.reconnect()
|
|
216
|
+
}, 50)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
ws.open()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('WSv2 constructor', () => {
|
|
224
|
+
it('defaults to production WS url', () => {
|
|
225
|
+
const ws = new WSv2()
|
|
226
|
+
assert.notEqual(ws._url.indexOf('api.bitfinex.com'), -1)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('defaults to no transform', () => {
|
|
230
|
+
const ws = createTestWSv2Instance()
|
|
231
|
+
const transWS = createTestWSv2Instance({ transform: true })
|
|
232
|
+
assert.equal(ws._transform, false)
|
|
233
|
+
assert.equal(transWS._transform, true)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('WSv2 auto reconnect', () => {
|
|
238
|
+
it('reconnects on close if autoReconnect is enabled', (done) => {
|
|
239
|
+
const wss = new MockWSv2Server()
|
|
240
|
+
const ws = createTestWSv2Instance({
|
|
241
|
+
autoReconnect: true
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
ws.on('open', ws.auth.bind(ws))
|
|
245
|
+
ws.once('auth', () => {
|
|
246
|
+
ws.reconnect = () => done()
|
|
247
|
+
wss.close() // trigger reconnect
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
ws.open()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('respects reconnectDelay', (done) => {
|
|
254
|
+
const wss = new MockWSv2Server()
|
|
255
|
+
const ws = createTestWSv2Instance({
|
|
256
|
+
autoReconnect: true,
|
|
257
|
+
reconnectDelay: 75
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
ws.on('open', ws.auth.bind(ws))
|
|
261
|
+
ws.once('auth', () => {
|
|
262
|
+
let now = Date.now()
|
|
263
|
+
|
|
264
|
+
ws.reconnect = () => {
|
|
265
|
+
assert((Date.now() - now) >= 70)
|
|
266
|
+
done()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
wss.close() // trigger reconnect
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
ws.open()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('does not auto-reconnect if explicity closed', (done) => {
|
|
276
|
+
const wss = new MockWSv2Server()
|
|
277
|
+
const ws = createTestWSv2Instance({
|
|
278
|
+
autoReconnect: true
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
ws.on('open', ws.auth.bind(ws))
|
|
282
|
+
ws.once('auth', () => {
|
|
283
|
+
ws.reconnect = () => assert(false)
|
|
284
|
+
ws.close()
|
|
285
|
+
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
wss.close()
|
|
288
|
+
done()
|
|
289
|
+
}, 50)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
ws.open()
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe('WSv2 seq audit', () => {
|
|
297
|
+
it('automatically enables sequencing if seqAudit is true in constructor', (done) => {
|
|
298
|
+
const wss = new MockWSv2Server()
|
|
299
|
+
const ws = createTestWSv2Instance({
|
|
300
|
+
seqAudit: true
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
wss._onClientMessage = (ws, msgJSON) => {
|
|
304
|
+
const msg = JSON.parse(msgJSON)
|
|
305
|
+
|
|
306
|
+
if (msg.event === 'conf' && msg.flags === 65536) {
|
|
307
|
+
wss.close()
|
|
308
|
+
done()
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ws.open()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('emits error on invalid seq number', (done) => {
|
|
316
|
+
const wss = new MockWSv2Server()
|
|
317
|
+
const ws = createTestWSv2Instance({
|
|
318
|
+
seqAudit: true
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
let errorsSeen = 0
|
|
322
|
+
|
|
323
|
+
ws.once('open', ws.auth.bind(ws))
|
|
324
|
+
ws.on('error', (err) => {
|
|
325
|
+
if (err.message.indexOf('seq #') !== -1) errorsSeen++
|
|
326
|
+
|
|
327
|
+
return null
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
ws.once('auth', () => {
|
|
331
|
+
ws._channelMap[42] = { channel: 'trades', chanId: 42 }
|
|
332
|
+
|
|
333
|
+
ws._onWSMessage(JSON.stringify([0, 'tu', [], 0, 0]))
|
|
334
|
+
ws._onWSMessage(JSON.stringify([0, 'te', [], 1, 0]))
|
|
335
|
+
ws._onWSMessage(JSON.stringify([0, 'wu', [], 2, 1]))
|
|
336
|
+
ws._onWSMessage(JSON.stringify([0, 'tu', [], 3, 2])) //
|
|
337
|
+
ws._onWSMessage(JSON.stringify([0, 'tu', [], 4, 4])) // error
|
|
338
|
+
ws._onWSMessage(JSON.stringify([0, 'tu', [], 5, 5]))
|
|
339
|
+
ws._onWSMessage(JSON.stringify([0, 'tu', [], 6, 6]))
|
|
340
|
+
ws._onWSMessage(JSON.stringify([42, [], 7]))
|
|
341
|
+
ws._onWSMessage(JSON.stringify([42, [], 8]))
|
|
342
|
+
ws._onWSMessage(JSON.stringify([42, [], 9])) //
|
|
343
|
+
ws._onWSMessage(JSON.stringify([42, [], 13])) // error
|
|
344
|
+
ws._onWSMessage(JSON.stringify([42, [], 14]))
|
|
345
|
+
ws._onWSMessage(JSON.stringify([42, [], 15]))
|
|
346
|
+
|
|
347
|
+
assert.equal(errorsSeen, 2)
|
|
348
|
+
wss.close()
|
|
349
|
+
done()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
ws.open()
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('WSv2 ws event handlers', () => {
|
|
357
|
+
it('_onWSOpen: updates open flag', () => {
|
|
358
|
+
const ws = new WSv2()
|
|
359
|
+
assert(!ws.isOpen())
|
|
360
|
+
ws._onWSOpen()
|
|
361
|
+
assert(ws.isOpen())
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('_onWSClose: updates open flag', () => {
|
|
365
|
+
const ws = new WSv2()
|
|
366
|
+
ws._onWSOpen()
|
|
367
|
+
assert(ws.isOpen())
|
|
368
|
+
ws._onWSClose()
|
|
369
|
+
assert(!ws.isOpen())
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('_onWSError: emits error', (done) => {
|
|
373
|
+
const ws = new WSv2()
|
|
374
|
+
ws.on('error', () => done())
|
|
375
|
+
ws._onWSError(new Error())
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('_onWSMessage: emits error on invalid packet', (done) => {
|
|
379
|
+
const ws = new WSv2()
|
|
380
|
+
ws.on('error', () => done())
|
|
381
|
+
ws._onWSMessage('I can\'t believe it\'s not JSON!')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('_onWSMessage: emits message', (done) => {
|
|
385
|
+
const ws = new WSv2()
|
|
386
|
+
const msg = [1]
|
|
387
|
+
const flags = 'flags'
|
|
388
|
+
|
|
389
|
+
ws.on('message', (m, f) => {
|
|
390
|
+
assert.deepEqual(m, msg)
|
|
391
|
+
assert.equal(flags, 'flags')
|
|
392
|
+
done()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
ws._onWSMessage(JSON.stringify(msg), flags)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('_onWSNotification: triggers event callbacks for new orders', (done) => {
|
|
399
|
+
const ws = new WSv2()
|
|
400
|
+
const kNew = 'order-new-42'
|
|
401
|
+
|
|
402
|
+
ws._eventCallbacks.push(kNew, (err, order) => {
|
|
403
|
+
assert(!err)
|
|
404
|
+
assert(order)
|
|
405
|
+
assert.deepEqual(order, [0, 0, 42])
|
|
406
|
+
|
|
407
|
+
ws._eventCallbacks.push(kNew, (err, order) => {
|
|
408
|
+
assert(err)
|
|
409
|
+
assert.deepEqual(order, [0, 0, 42])
|
|
410
|
+
done()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
ws._onWSNotification([0, 'on-req', null, null, [0, 0, 42], 0, 'ERROR'])
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
ws._onWSNotification([0, 'on-req', null, null, [0, 0, 42], 0, 'SUCCESS'])
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('_onWSNotification: triggers event callbacks for cancelled orders', (done) => {
|
|
420
|
+
const ws = new WSv2()
|
|
421
|
+
const kCancel = 'order-cancel-42'
|
|
422
|
+
|
|
423
|
+
ws._eventCallbacks.push(kCancel, (err, order) => {
|
|
424
|
+
assert(!err)
|
|
425
|
+
assert(order)
|
|
426
|
+
assert.deepEqual(order, [42])
|
|
427
|
+
|
|
428
|
+
ws._eventCallbacks.push(kCancel, (err, order) => {
|
|
429
|
+
assert(err)
|
|
430
|
+
assert.deepEqual(order, [42])
|
|
431
|
+
done()
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
ws._onWSNotification([0, 'oc-req', null, null, [42], 0, 'ERROR'])
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
ws._onWSNotification([0, 'oc-req', null, null, [42], 0, 'SUCCESS'])
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
describe('WSv2 channel msg handling', () => {
|
|
442
|
+
it('_handleChannelMessage: emits message', (done) => {
|
|
443
|
+
const ws = new WSv2()
|
|
444
|
+
const packet = [42, 'tu', []]
|
|
445
|
+
ws._channelMap = {
|
|
446
|
+
42: { channel: 'meaning' }
|
|
447
|
+
}
|
|
448
|
+
ws.on('meaning', (msg) => {
|
|
449
|
+
assert.deepEqual(msg, packet)
|
|
450
|
+
done()
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
ws._handleChannelMessage(packet)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('_handleChannelMessage: calls all registered listeners (nofilter)', (done) => {
|
|
457
|
+
const ws = new WSv2()
|
|
458
|
+
ws._channelMap = { 0: { channel: 'auth' } }
|
|
459
|
+
let called = 0
|
|
460
|
+
ws.onWalletUpdate({}, () => {
|
|
461
|
+
if (++called === 2) done()
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
ws.onWalletUpdate({}, () => {
|
|
465
|
+
if (++called === 2) done()
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
ws._handleChannelMessage([0, 'wu', []])
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const doFilterTest = (transform, done) => {
|
|
472
|
+
const ws = new WSv2({ transform })
|
|
473
|
+
ws._channelMap = { 0: { channel: 'auth' } }
|
|
474
|
+
let calls = 0
|
|
475
|
+
let btcListenerCalled = false
|
|
476
|
+
|
|
477
|
+
ws.onTradeEntry({ pair: 'tBTCUSD' }, () => {
|
|
478
|
+
assert(!btcListenerCalled)
|
|
479
|
+
btcListenerCalled = true
|
|
480
|
+
|
|
481
|
+
if (++calls === 7) done()
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
ws.onTradeEntry({}, () => {
|
|
485
|
+
if (++calls === 7) done()
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
ws.onTradeEntry({}, () => {
|
|
489
|
+
if (++calls === 7) done()
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
ws._handleChannelMessage([0, 'te', ['tETHUSD']])
|
|
493
|
+
ws._handleChannelMessage([0, 'te', ['tETHUSD']])
|
|
494
|
+
ws._handleChannelMessage([0, 'te', ['tBTCUSD']])
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
it('_handleChannelMessage: filters messages if listeners require it (transform)', (done) => {
|
|
498
|
+
doFilterTest(true, done)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('_handleChannelMessage: filters messages if listeners require it (no transform)', (done) => {
|
|
502
|
+
doFilterTest(false, done)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('_handleChannelMessage: transforms payloads if enabled', (done) => {
|
|
506
|
+
let calls = 0
|
|
507
|
+
|
|
508
|
+
const wsTransform = new WSv2({ transform: true })
|
|
509
|
+
const wsNoTransform = new WSv2({ transform: false })
|
|
510
|
+
wsTransform._channelMap = { 0: { channel: 'auth' } }
|
|
511
|
+
wsNoTransform._channelMap = { 0: { channel: 'auth' } }
|
|
512
|
+
|
|
513
|
+
const tradeData = [
|
|
514
|
+
0, 'tBTCUSD', Date.now(), 0, 0.1, 1, 'type', 1, 1, 0.001, 'USD'
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
wsNoTransform.onTradeUpdate({}, (trade) => {
|
|
518
|
+
assert.equal(trade.constructor.name, 'Array')
|
|
519
|
+
assert.deepEqual(trade, tradeData)
|
|
520
|
+
|
|
521
|
+
if (calls++ === 1) done()
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
wsTransform.onTradeUpdate({}, (trade) => {
|
|
525
|
+
assert.equal(trade.constructor.name, 'Trade')
|
|
526
|
+
assert.equal(trade.id, tradeData[0])
|
|
527
|
+
assert.equal(trade.pair, tradeData[1])
|
|
528
|
+
assert.equal(trade.mtsCreate, tradeData[2])
|
|
529
|
+
assert.equal(trade.orderID, tradeData[3])
|
|
530
|
+
assert.equal(trade.execAmount, tradeData[4])
|
|
531
|
+
assert.equal(trade.execPrice, tradeData[5])
|
|
532
|
+
assert.equal(trade.orderType, tradeData[6])
|
|
533
|
+
assert.equal(trade.orderPrice, tradeData[7])
|
|
534
|
+
assert.equal(trade.maker, tradeData[8])
|
|
535
|
+
assert.equal(trade.fee, tradeData[9])
|
|
536
|
+
assert.equal(trade.feeCurrency, tradeData[10])
|
|
537
|
+
|
|
538
|
+
if (calls++ === 1) done()
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
wsTransform._handleChannelMessage([0, 'tu', tradeData])
|
|
542
|
+
wsNoTransform._handleChannelMessage([0, 'tu', tradeData])
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('onMessage: calls the listener with all messages (no filter)', (done) => {
|
|
546
|
+
const ws = new WSv2()
|
|
547
|
+
ws._channelMap = { 0: { channel: 'auth' } }
|
|
548
|
+
|
|
549
|
+
let calls = 0
|
|
550
|
+
|
|
551
|
+
ws.onMessage({}, (msg) => {
|
|
552
|
+
if (++calls === 2) done()
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
ws._handleChannelMessage([0, 'wu', []])
|
|
556
|
+
ws._handleChannelMessage([0, 'tu', []])
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('_payloadPassesFilter: correctly detects matching payloads', () => {
|
|
560
|
+
const filter = {
|
|
561
|
+
1: 'tBTCUSD'
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const goodPayloads = [
|
|
565
|
+
[0, 'tBTCUSD', 42, ''],
|
|
566
|
+
[0, 'tBTCUSD', 3.14, '']
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
const badPayloads = [
|
|
570
|
+
[0, 'tETHUSD', 42, ''],
|
|
571
|
+
[0, 'tETHUSD', 3.14, '']
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
goodPayloads.forEach(p => assert(WSv2._payloadPassesFilter(p, filter)))
|
|
575
|
+
badPayloads.forEach(p => assert(!WSv2._payloadPassesFilter(p, filter)))
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('_notifyListenerGroup: notifies all matching listeners in the group', (done) => {
|
|
579
|
+
let calls = 0
|
|
580
|
+
const func = () => {
|
|
581
|
+
assert(calls < 3)
|
|
582
|
+
if (++calls === 2) done()
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const lg = {
|
|
586
|
+
'': [{ cb: func }],
|
|
587
|
+
'test': [{ cb: func }],
|
|
588
|
+
'nope': [{ cb: func }]
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
WSv2._notifyListenerGroup(lg, [0, 'test', [0, 'tu']], false)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('_propagateMessageToListeners: notifies all matching listeners', (done) => {
|
|
595
|
+
const ws = new WSv2()
|
|
596
|
+
ws._channelMap = { 0: { channel: 'auth' } }
|
|
597
|
+
|
|
598
|
+
ws.onTradeEntry({ pair: 'tBTCUSD' }, () => {
|
|
599
|
+
done()
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
ws._propagateMessageToListeners([0, 'te', ['tBTCUSD']])
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('_notifyCatchAllListeners: passes data to all listeners on the empty \'\' event', () => {
|
|
606
|
+
let s = 0
|
|
607
|
+
|
|
608
|
+
const lg = {
|
|
609
|
+
'': [
|
|
610
|
+
{ cb: d => { s += d } },
|
|
611
|
+
{ cb: d => { s += (d * 2) } }
|
|
612
|
+
]
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
WSv2._notifyCatchAllListeners(lg, 5)
|
|
616
|
+
assert.equal(s, 15)
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
it('_handleOBMessage: maintains internal OB if management is enabled', () => {
|
|
620
|
+
const ws = new WSv2({
|
|
621
|
+
manageOrderBooks: true,
|
|
622
|
+
transform: true
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
ws._channelMap = { 42: {
|
|
626
|
+
channel: 'orderbook',
|
|
627
|
+
symbol: 'tBTCUSD'
|
|
628
|
+
}}
|
|
629
|
+
|
|
630
|
+
ws._handleOBMessage([42, [
|
|
631
|
+
[100, 2, -4],
|
|
632
|
+
[200, 4, -8],
|
|
633
|
+
[300, 1, 3]
|
|
634
|
+
]], ws._channelMap[42])
|
|
635
|
+
|
|
636
|
+
let ob = ws.getOB('tBTCUSD')
|
|
637
|
+
assert(ob !== null)
|
|
638
|
+
|
|
639
|
+
assert.equal(ob.bids.length, 1)
|
|
640
|
+
assert.deepEqual(ob.bids, [[300, 1, 3]])
|
|
641
|
+
assert.equal(ob.asks.length, 2)
|
|
642
|
+
assert.deepEqual(ob.getEntry(100), { price: 100, count: 2, amount: -4 })
|
|
643
|
+
assert.deepEqual(ob.getEntry(200), { price: 200, count: 4, amount: -8 })
|
|
644
|
+
|
|
645
|
+
ws._handleOBMessage([42, [300, 0, 1]], ws._channelMap[42])
|
|
646
|
+
ob = ws.getOB('tBTCUSD')
|
|
647
|
+
assert.equal(ob.bids.length, 0)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('_handleOBMessage: emits error on internal OB update failure', (done) => {
|
|
651
|
+
const wsNoTransform = new WSv2({ manageOrderBooks: true })
|
|
652
|
+
const wsTransform = new WSv2({
|
|
653
|
+
manageOrderBooks: true,
|
|
654
|
+
transform: true
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
wsNoTransform._channelMap = { 42: {
|
|
658
|
+
channel: 'orderbook',
|
|
659
|
+
symbol: 'tBTCUSD'
|
|
660
|
+
}}
|
|
661
|
+
|
|
662
|
+
wsTransform._channelMap = wsNoTransform._channelMap
|
|
663
|
+
|
|
664
|
+
let errorsSeen = 0
|
|
665
|
+
|
|
666
|
+
wsNoTransform.on('error', () => {
|
|
667
|
+
if (++errorsSeen === 2) done()
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
wsTransform.on('error', () => {
|
|
671
|
+
if (++errorsSeen === 2) done()
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
wsTransform._handleOBMessage([42, [100, 0, 1]], wsTransform._channelMap[42])
|
|
675
|
+
wsNoTransform._handleOBMessage([42, [100, 0, 1]], wsNoTransform._channelMap[42])
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('_handleOBMessage: forwards managed ob to listeners', (done) => {
|
|
679
|
+
const ws = new WSv2({ manageOrderBooks: true })
|
|
680
|
+
ws._channelMap = { 42: {
|
|
681
|
+
channel: 'orderbook',
|
|
682
|
+
symbol: 'tBTCUSD'
|
|
683
|
+
}}
|
|
684
|
+
|
|
685
|
+
let seen = 0
|
|
686
|
+
ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => {
|
|
687
|
+
assert.deepEqual(ob, [[100, 2, 3]])
|
|
688
|
+
if (++seen === 2) done()
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
ws.onOrderBook({}, (ob) => {
|
|
692
|
+
assert.deepEqual(ob, [[100, 2, 3]])
|
|
693
|
+
if (++seen === 2) done()
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('_handleOBMessage: filters by prec and len', (done) => {
|
|
700
|
+
const ws = new WSv2({ manageOrderBooks: true })
|
|
701
|
+
ws._channelMap = {
|
|
702
|
+
40: {
|
|
703
|
+
channel: 'orderbook',
|
|
704
|
+
symbol: 'tBTCUSD',
|
|
705
|
+
prec: 'P0'
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
41: {
|
|
709
|
+
channel: 'orderbook',
|
|
710
|
+
symbol: 'tBTCUSD',
|
|
711
|
+
prec: 'P1'
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
42: {
|
|
715
|
+
channel: 'orderbook',
|
|
716
|
+
symbol: 'tBTCUSD',
|
|
717
|
+
prec: 'P2'
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let seen = 0
|
|
722
|
+
ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P0' }, (ob) => {
|
|
723
|
+
assert(false)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P1' }, (ob) => {
|
|
727
|
+
assert(false)
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P2' }, (ob) => {
|
|
731
|
+
if (++seen === 2) done()
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
|
|
735
|
+
ws._handleOBMessage([42, [100, 2, 3]], ws._channelMap[42])
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('_handleOBMessage: emits managed ob', (done) => {
|
|
739
|
+
const ws = new WSv2({ manageOrderBooks: true })
|
|
740
|
+
ws._channelMap = { 42: {
|
|
741
|
+
channel: 'orderbook',
|
|
742
|
+
symbol: 'tBTCUSD'
|
|
743
|
+
}}
|
|
744
|
+
|
|
745
|
+
ws.on('orderbook', (symbol, data) => {
|
|
746
|
+
assert.equal(symbol, 'tBTCUSD')
|
|
747
|
+
assert.deepEqual(data, [[100, 2, 3]])
|
|
748
|
+
done()
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
it('_handleOBMessage: forwards transformed data if transform enabled', (done) => {
|
|
755
|
+
const ws = new WSv2({ transform: true })
|
|
756
|
+
ws._channelMap = { 42: {
|
|
757
|
+
chanId: 42,
|
|
758
|
+
channel: 'orderbook',
|
|
759
|
+
symbol: 'tBTCUSD'
|
|
760
|
+
}}
|
|
761
|
+
|
|
762
|
+
ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => {
|
|
763
|
+
assert(ob.asks)
|
|
764
|
+
assert(ob.bids)
|
|
765
|
+
assert.equal(ob.asks.length, 0)
|
|
766
|
+
assert.deepEqual(ob.bids, [[100, 2, 3]])
|
|
767
|
+
done()
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('_updateManagedOB: returns an error on rm non-existent entry', () => {
|
|
774
|
+
const ws = new WSv2()
|
|
775
|
+
ws._orderBooks.tBTCUSD = [
|
|
776
|
+
[100, 1, 1],
|
|
777
|
+
[200, 2, 1]
|
|
778
|
+
]
|
|
779
|
+
|
|
780
|
+
const err = ws._updateManagedOB('tBTCUSD', [150, 0, -1])
|
|
781
|
+
assert(err)
|
|
782
|
+
assert(err instanceof Error)
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
it('_updateManagedOB: returns error if update is snap & ob exists', () => {
|
|
786
|
+
const ws = new WSv2()
|
|
787
|
+
const errA = ws._updateManagedOB('tBTCUSD', [[150, 0, -1]])
|
|
788
|
+
const errB = ws._updateManagedOB('tBTCUSD', [[150, 0, -1]])
|
|
789
|
+
|
|
790
|
+
assert(!errA)
|
|
791
|
+
assert(errB)
|
|
792
|
+
assert(errB instanceof Error)
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
it('_updateManagedOB: correctly maintains transformed OBs', () => {
|
|
796
|
+
const ws = new WSv2({ transform: true })
|
|
797
|
+
ws._orderBooks.tBTCUSD = []
|
|
798
|
+
|
|
799
|
+
assert(!ws._updateManagedOB('tBTCUSD', [100, 1, 1]))
|
|
800
|
+
assert(!ws._updateManagedOB('tBTCUSD', [200, 1, -1]))
|
|
801
|
+
assert(!ws._updateManagedOB('tBTCUSD', [200, 0, -1]))
|
|
802
|
+
|
|
803
|
+
const ob = ws.getOB('tBTCUSD')
|
|
804
|
+
|
|
805
|
+
assert.equal(ob.bids.length, 1)
|
|
806
|
+
assert.equal(ob.asks.length, 0)
|
|
807
|
+
assert.deepEqual(ob.bids, [[100, 1, 1]])
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
it('_updateManagedOB: correctly maintains non-transformed OBs', () => {
|
|
811
|
+
const ws = new WSv2()
|
|
812
|
+
ws._orderBooks.tBTCUSD = []
|
|
813
|
+
|
|
814
|
+
assert(!ws._updateManagedOB('tBTCUSD', [100, 1, 1]))
|
|
815
|
+
assert(!ws._updateManagedOB('tBTCUSD', [200, 1, -1]))
|
|
816
|
+
assert(!ws._updateManagedOB('tBTCUSD', [200, 0, -1]))
|
|
817
|
+
|
|
818
|
+
const ob = ws._orderBooks.tBTCUSD
|
|
819
|
+
|
|
820
|
+
assert.equal(ob.length, 1)
|
|
821
|
+
assert.deepEqual(ob, [[100, 1, 1]])
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
it('_handleCandleMessage: maintains internal candles if management is enabled', () => {
|
|
825
|
+
const ws = new WSv2({ manageCandles: true })
|
|
826
|
+
ws._channelMap = { 64: {
|
|
827
|
+
channel: 'candles',
|
|
828
|
+
key: 'trade:1m:tBTCUSD'
|
|
829
|
+
}}
|
|
830
|
+
|
|
831
|
+
ws._handleCandleMessage([64, [
|
|
832
|
+
[5, 100, 70, 150, 30, 1000],
|
|
833
|
+
[2, 200, 90, 150, 30, 1000],
|
|
834
|
+
[1, 130, 90, 150, 30, 1000],
|
|
835
|
+
[4, 104, 80, 150, 30, 1000]
|
|
836
|
+
]], ws._channelMap[64])
|
|
837
|
+
|
|
838
|
+
const candles = ws._candles['trade:1m:tBTCUSD']
|
|
839
|
+
|
|
840
|
+
// maintains sort
|
|
841
|
+
assert.equal(candles.length, 4)
|
|
842
|
+
assert.equal(candles[0][0], 5)
|
|
843
|
+
assert.equal(candles[1][0], 4)
|
|
844
|
+
assert.equal(candles[2][0], 2)
|
|
845
|
+
assert.equal(candles[3][0], 1)
|
|
846
|
+
|
|
847
|
+
// updates existing candle
|
|
848
|
+
ws._handleCandleMessage([
|
|
849
|
+
64,
|
|
850
|
+
[5, 200, 20, 220, 20, 2000]
|
|
851
|
+
], ws._channelMap[64])
|
|
852
|
+
|
|
853
|
+
assert.deepEqual(candles[0], [5, 200, 20, 220, 20, 2000])
|
|
854
|
+
|
|
855
|
+
// inserts new candle
|
|
856
|
+
ws._handleCandleMessage([
|
|
857
|
+
64,
|
|
858
|
+
[10, 300, 20, 450, 10, 4000]
|
|
859
|
+
], ws._channelMap[64])
|
|
860
|
+
|
|
861
|
+
assert.deepEqual(candles[0], [10, 300, 20, 450, 10, 4000])
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
it('_handleCandleMessage: emits error on internal candle update failure', (done) => {
|
|
865
|
+
const ws = new WSv2({ manageCandles: true })
|
|
866
|
+
ws._channelMap = {
|
|
867
|
+
42: {
|
|
868
|
+
channel: 'candles',
|
|
869
|
+
key: 'trade:30m:tBTCUSD'
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
64: {
|
|
873
|
+
channel: 'candles',
|
|
874
|
+
key: 'trade:1m:tBTCUSD'
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
let errorsSeen = 0
|
|
879
|
+
|
|
880
|
+
ws.on('error', () => {
|
|
881
|
+
if (++errorsSeen === 2) done()
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
ws._handleCandleMessage([64, [
|
|
885
|
+
[5, 100, 70, 150, 30, 1000],
|
|
886
|
+
[2, 200, 90, 150, 30, 1000],
|
|
887
|
+
[1, 130, 90, 150, 30, 1000],
|
|
888
|
+
[4, 104, 80, 150, 30, 1000]
|
|
889
|
+
]], ws._channelMap[64])
|
|
890
|
+
|
|
891
|
+
// duplicate snapshot
|
|
892
|
+
ws._handleCandleMessage([64, [
|
|
893
|
+
[5, 100, 70, 150, 30, 1000],
|
|
894
|
+
[2, 200, 90, 150, 30, 1000],
|
|
895
|
+
[1, 130, 90, 150, 30, 1000],
|
|
896
|
+
[4, 104, 80, 150, 30, 1000]
|
|
897
|
+
]], ws._channelMap[64])
|
|
898
|
+
|
|
899
|
+
// update for unknown key
|
|
900
|
+
ws._handleCandleMessage([
|
|
901
|
+
42,
|
|
902
|
+
[5, 10, 70, 150, 30, 10]
|
|
903
|
+
], ws._channelMap[42])
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
it('_handleCandleMessage: forwards managed candles to listeners', (done) => {
|
|
907
|
+
const ws = new WSv2({ manageCandles: true })
|
|
908
|
+
ws._channelMap = { 42: {
|
|
909
|
+
chanId: 42,
|
|
910
|
+
channel: 'candles',
|
|
911
|
+
key: 'trade:1m:tBTCUSD'
|
|
912
|
+
}}
|
|
913
|
+
|
|
914
|
+
let seen = 0
|
|
915
|
+
ws.onCandle({ key: 'trade:1m:tBTCUSD' }, (data) => {
|
|
916
|
+
assert.deepEqual(data, [[5, 10, 70, 150, 30, 10]])
|
|
917
|
+
if (++seen === 2) done()
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
ws.onCandle({}, (data) => {
|
|
921
|
+
assert.deepEqual(data, [[5, 10, 70, 150, 30, 10]])
|
|
922
|
+
if (++seen === 2) done()
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
ws._handleCandleMessage([
|
|
926
|
+
42,
|
|
927
|
+
[[5, 10, 70, 150, 30, 10]]
|
|
928
|
+
], ws._channelMap[42])
|
|
929
|
+
})
|
|
930
|
+
|
|
931
|
+
it('_handleCandleMessage: emits managed candles', (done) => {
|
|
932
|
+
const ws = new WSv2({ manageCandles: true })
|
|
933
|
+
ws._channelMap = { 42: {
|
|
934
|
+
channel: 'candles',
|
|
935
|
+
key: 'trade:1m:tBTCUSD'
|
|
936
|
+
}}
|
|
937
|
+
|
|
938
|
+
ws.on('candle', (data, key) => {
|
|
939
|
+
assert.equal(key, 'trade:1m:tBTCUSD')
|
|
940
|
+
assert.deepEqual(data, [[5, 10, 70, 150, 30, 10]])
|
|
941
|
+
done()
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
ws._handleCandleMessage([
|
|
945
|
+
42,
|
|
946
|
+
[[5, 10, 70, 150, 30, 10]]
|
|
947
|
+
], ws._channelMap[42])
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it('_handleCandleMessage: forwards transformed data if transform enabled', (done) => {
|
|
951
|
+
const ws = new WSv2({ transform: true })
|
|
952
|
+
ws._channelMap = { 42: {
|
|
953
|
+
chanId: 42,
|
|
954
|
+
channel: 'candles',
|
|
955
|
+
key: 'trade:1m:tBTCUSD'
|
|
956
|
+
}}
|
|
957
|
+
|
|
958
|
+
ws.onCandle({ key: 'trade:1m:tBTCUSD' }, (candles) => {
|
|
959
|
+
assert.equal(candles.length, 1)
|
|
960
|
+
assert.deepEqual(candles[0], {
|
|
961
|
+
mts: 5,
|
|
962
|
+
open: 10,
|
|
963
|
+
close: 70,
|
|
964
|
+
high: 150,
|
|
965
|
+
low: 30,
|
|
966
|
+
volume: 10
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
done()
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
ws._handleCandleMessage([
|
|
973
|
+
42,
|
|
974
|
+
[[5, 10, 70, 150, 30, 10]]
|
|
975
|
+
], ws._channelMap[42])
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
it('_updateManagedCandles: returns an error on update for unknown key', () => {
|
|
979
|
+
const ws = new WSv2()
|
|
980
|
+
ws._candles['trade:1m:tBTCUSD'] = []
|
|
981
|
+
|
|
982
|
+
const err = ws._updateManagedCandles('trade:30m:tBTCUSD', [
|
|
983
|
+
1, 10, 70, 150, 30, 10
|
|
984
|
+
])
|
|
985
|
+
|
|
986
|
+
assert(err)
|
|
987
|
+
assert(err instanceof Error)
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
it('_updateManagedCandles: returns error if update is snap & candles exist', () => {
|
|
991
|
+
const ws = new WSv2()
|
|
992
|
+
ws._candles['trade:1m:tBTCUSD'] = [
|
|
993
|
+
[1, 10, 70, 150, 30, 10],
|
|
994
|
+
[2, 10, 70, 150, 30, 10]
|
|
995
|
+
]
|
|
996
|
+
|
|
997
|
+
const err = ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
998
|
+
[1, 10, 70, 150, 30, 10]
|
|
999
|
+
])
|
|
1000
|
+
|
|
1001
|
+
assert(err)
|
|
1002
|
+
assert(err instanceof Error)
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
it('_updateManagedCandles: correctly maintains transformed OBs', () => {
|
|
1006
|
+
const ws = new WSv2({ transform: true })
|
|
1007
|
+
|
|
1008
|
+
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
1009
|
+
[1, 10, 70, 150, 30, 10],
|
|
1010
|
+
[2, 10, 70, 150, 30, 10]
|
|
1011
|
+
]))
|
|
1012
|
+
|
|
1013
|
+
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
1014
|
+
2, 10, 70, 150, 30, 500
|
|
1015
|
+
]))
|
|
1016
|
+
|
|
1017
|
+
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
1018
|
+
3, 100, 70, 150, 30, 10
|
|
1019
|
+
]))
|
|
1020
|
+
|
|
1021
|
+
const candles = ws._candles['trade:1m:tBTCUSD']
|
|
1022
|
+
|
|
1023
|
+
assert.equal(candles.length, 3)
|
|
1024
|
+
assert.deepEqual(candles[0], [
|
|
1025
|
+
3, 100, 70, 150, 30, 10
|
|
1026
|
+
])
|
|
1027
|
+
|
|
1028
|
+
assert.deepEqual(candles[1], [
|
|
1029
|
+
2, 10, 70, 150, 30, 500
|
|
1030
|
+
])
|
|
1031
|
+
|
|
1032
|
+
assert.deepEqual(candles[2], [
|
|
1033
|
+
1, 10, 70, 150, 30, 10
|
|
1034
|
+
])
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
it('_updateManagedCandles: correctly maintains non-transformed OBs', () => {
|
|
1038
|
+
const ws = new WSv2()
|
|
1039
|
+
|
|
1040
|
+
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
1041
|
+
[1, 10, 70, 150, 30, 10],
|
|
1042
|
+
[2, 10, 70, 150, 30, 10]
|
|
1043
|
+
]))
|
|
1044
|
+
|
|
1045
|
+
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
1046
|
+
2, 10, 70, 150, 30, 500
|
|
1047
|
+
]))
|
|
1048
|
+
|
|
1049
|
+
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
|
|
1050
|
+
3, 100, 70, 150, 30, 10
|
|
1051
|
+
]))
|
|
1052
|
+
|
|
1053
|
+
const candles = ws._candles['trade:1m:tBTCUSD']
|
|
1054
|
+
|
|
1055
|
+
assert.equal(candles.length, 3)
|
|
1056
|
+
assert.deepEqual(candles[0], [
|
|
1057
|
+
3, 100, 70, 150, 30, 10
|
|
1058
|
+
])
|
|
1059
|
+
|
|
1060
|
+
assert.deepEqual(candles[1], [
|
|
1061
|
+
2, 10, 70, 150, 30, 500
|
|
1062
|
+
])
|
|
1063
|
+
|
|
1064
|
+
assert.deepEqual(candles[2], [
|
|
1065
|
+
1, 10, 70, 150, 30, 10
|
|
1066
|
+
])
|
|
1067
|
+
})
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
describe('WSv2 event msg handling', () => {
|
|
1071
|
+
it('_handleErrorEvent: emits error', (done) => {
|
|
1072
|
+
const ws = new WSv2()
|
|
1073
|
+
ws.on('error', (err) => {
|
|
1074
|
+
if (err === 42) done()
|
|
1075
|
+
})
|
|
1076
|
+
ws._handleErrorEvent(42)
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it('_handleConfigEvent: emits error if config failed', (done) => {
|
|
1080
|
+
const ws = new WSv2()
|
|
1081
|
+
ws.on('error', (err) => {
|
|
1082
|
+
if (err.code === 42) done()
|
|
1083
|
+
})
|
|
1084
|
+
ws._handleConfigEvent({ status: 'bad', code: 42 })
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
it('_handleAuthEvent: emits an error on auth fail', (done) => {
|
|
1088
|
+
const ws = new WSv2()
|
|
1089
|
+
ws.on('error', () => {
|
|
1090
|
+
done()
|
|
1091
|
+
})
|
|
1092
|
+
ws._handleAuthEvent({ status: 'FAIL' })
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it('_handleAuthEvent: updates auth flag on auth success', () => {
|
|
1096
|
+
const ws = new WSv2()
|
|
1097
|
+
assert(!ws.isAuthenticated())
|
|
1098
|
+
ws._handleAuthEvent({ status: 'OK' })
|
|
1099
|
+
assert(ws.isAuthenticated())
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
it('_handleAuthEvent: adds auth channel to channel map', () => {
|
|
1103
|
+
const ws = new WSv2()
|
|
1104
|
+
assert(Object.keys(ws._channelMap).length === 0)
|
|
1105
|
+
ws._handleAuthEvent({ chanId: 42, status: 'OK' })
|
|
1106
|
+
assert(ws._channelMap[42])
|
|
1107
|
+
assert.equal(ws._channelMap[42].channel, 'auth')
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
it('_handleAuthEvent: emits auth message', (done) => {
|
|
1111
|
+
const ws = new WSv2()
|
|
1112
|
+
ws.once('auth', (msg) => {
|
|
1113
|
+
assert.equal(msg.chanId, 0)
|
|
1114
|
+
assert.equal(msg.status, 'OK')
|
|
1115
|
+
done()
|
|
1116
|
+
})
|
|
1117
|
+
ws._handleAuthEvent({ chanId: 0, status: 'OK' })
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
it('_handleSubscribedEvent: adds channel to channel map', () => {
|
|
1121
|
+
const ws = new WSv2()
|
|
1122
|
+
assert(Object.keys(ws._channelMap).length === 0)
|
|
1123
|
+
ws._handleSubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' })
|
|
1124
|
+
assert(ws._channelMap[42])
|
|
1125
|
+
assert.equal(ws._channelMap[42].chanId, 42)
|
|
1126
|
+
assert.equal(ws._channelMap[42].channel, 'test')
|
|
1127
|
+
assert.equal(ws._channelMap[42].extra, 'stuff')
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
it('_handleUnsubscribedEvent: removes channel from channel map', () => {
|
|
1131
|
+
const ws = new WSv2()
|
|
1132
|
+
assert(Object.keys(ws._channelMap).length === 0)
|
|
1133
|
+
ws._handleSubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' })
|
|
1134
|
+
ws._handleUnsubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' })
|
|
1135
|
+
assert(Object.keys(ws._channelMap).length === 0)
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
it('_handleInfoEvent: closes & emits error if not on api v2', (done) => {
|
|
1139
|
+
const wss = new MockWSv2Server()
|
|
1140
|
+
const ws = createTestWSv2Instance()
|
|
1141
|
+
let seen = 0
|
|
1142
|
+
|
|
1143
|
+
const d = () => {
|
|
1144
|
+
wss.close()
|
|
1145
|
+
done()
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
ws.once('open', () => {
|
|
1149
|
+
ws.on('error', () => { if (++seen === 2) { d() } })
|
|
1150
|
+
ws.on('close', () => { if (++seen === 2) { d() } })
|
|
1151
|
+
|
|
1152
|
+
ws._handleInfoEvent({ version: 3 })
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
ws.open()
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
it('_flushOrderOps: returned promise rejects if not authorised', (done) => {
|
|
1159
|
+
const ws = new WSv2()
|
|
1160
|
+
ws._orderOpBuffer = [
|
|
1161
|
+
[0, 'oc', null, []]
|
|
1162
|
+
]
|
|
1163
|
+
|
|
1164
|
+
ws._flushOrderOps().catch(() => done())
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
it('_flushOrderOps: merges the buffer into a multi-op packet & sends', (done) => {
|
|
1168
|
+
const ws = new WSv2()
|
|
1169
|
+
ws._isAuthenticated = true
|
|
1170
|
+
|
|
1171
|
+
ws._orderOpBuffer = [
|
|
1172
|
+
[0, 'oc', null, []],
|
|
1173
|
+
[0, 'on', null, []],
|
|
1174
|
+
[0, 'oc_multi', null, []],
|
|
1175
|
+
[0, 'ou', null, []]
|
|
1176
|
+
]
|
|
1177
|
+
|
|
1178
|
+
ws.send = (packet) => {
|
|
1179
|
+
assert.equal(packet[1], 'ox_multi')
|
|
1180
|
+
assert.equal(packet[3].length, 4)
|
|
1181
|
+
done()
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
ws._flushOrderOps().catch(() => assert(false))
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
it('_flushOrderOps: splits up buffers greater than 15 ops in size', (done) => {
|
|
1188
|
+
const ws = new WSv2()
|
|
1189
|
+
ws._isAuthenticated = true
|
|
1190
|
+
|
|
1191
|
+
let seenCount = 0
|
|
1192
|
+
|
|
1193
|
+
for (let i = 0; i < 45; i++) {
|
|
1194
|
+
ws._orderOpBuffer.push([0, 'oc', null, []])
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
ws.send = (packet) => {
|
|
1198
|
+
assert.equal(packet[1], 'ox_multi')
|
|
1199
|
+
assert(packet[3].length <= 15)
|
|
1200
|
+
seenCount += packet[3].length
|
|
1201
|
+
|
|
1202
|
+
if (seenCount === 45) done()
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
ws._flushOrderOps().catch(() => assert(false))
|
|
1206
|
+
})
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
describe('WSv2 packet watch-dog', () => {
|
|
1210
|
+
it('resets the WD timeout on every websocket message', (done) => {
|
|
1211
|
+
const ws = new WSv2({ packetWDDelay: 1000 })
|
|
1212
|
+
assert.equal(ws._packetWDTimeout, null)
|
|
1213
|
+
|
|
1214
|
+
ws.on('error', () => {}) // ignore json errors
|
|
1215
|
+
|
|
1216
|
+
let wdResets = 0
|
|
1217
|
+
ws._resetPacketWD = () => {
|
|
1218
|
+
if (++wdResets === 4) done()
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
ws._onWSMessage('asdf')
|
|
1222
|
+
ws._onWSMessage('asdf')
|
|
1223
|
+
ws._onWSMessage('asdf')
|
|
1224
|
+
ws._onWSMessage('asdf')
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
it('_resetPacketWD: clears existing wd timeout', (done) => {
|
|
1228
|
+
const ws = new WSv2({ packetWDDelay: 1000 })
|
|
1229
|
+
ws._packetWDTimeout = setTimeout(() => {
|
|
1230
|
+
assert(false)
|
|
1231
|
+
}, 100)
|
|
1232
|
+
|
|
1233
|
+
ws._resetPacketWD()
|
|
1234
|
+
setTimeout(done, 200)
|
|
1235
|
+
})
|
|
1236
|
+
|
|
1237
|
+
it('_resetPacketWD: schedules new wd timeout', (done) => {
|
|
1238
|
+
const ws = new WSv2({ packetWDDelay: 500 })
|
|
1239
|
+
ws._isOpen = true
|
|
1240
|
+
ws._triggerPacketWD = () => done()
|
|
1241
|
+
ws._resetPacketWD()
|
|
1242
|
+
assert(ws._packetWDTimeout !== null)
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
it('_triggerPacketWD: does nothing if wd is disabled', (done) => {
|
|
1246
|
+
const ws = new WSv2()
|
|
1247
|
+
ws._isOpen = true
|
|
1248
|
+
ws.reconnect = () => assert(false)
|
|
1249
|
+
ws._triggerPacketWD()
|
|
1250
|
+
|
|
1251
|
+
setTimeout(() => {
|
|
1252
|
+
done()
|
|
1253
|
+
}, 50)
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
it('_triggerPacketWD: calls reconnect()', (done) => {
|
|
1257
|
+
const ws = new WSv2({ packetWDDelay: 1000 })
|
|
1258
|
+
ws._isOpen = true
|
|
1259
|
+
ws.reconnect = () => done()
|
|
1260
|
+
ws._triggerPacketWD()
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
it('triggers wd when no packet arrives after delay elapses', (done) => {
|
|
1264
|
+
const ws = new WSv2({ packetWDDelay: 100 })
|
|
1265
|
+
const now = Date.now()
|
|
1266
|
+
ws._isOpen = true
|
|
1267
|
+
|
|
1268
|
+
ws.on('error', () => {}) // invalid json to prevent message routing
|
|
1269
|
+
ws._triggerPacketWD = () => {
|
|
1270
|
+
assert((Date.now() - now) >= 95)
|
|
1271
|
+
done()
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
ws._onWSMessage('asdf') // send first packet, init wd
|
|
1275
|
+
})
|
|
1276
|
+
|
|
1277
|
+
it('doesn\'t trigger wd when packets arrive as expected', (done) => {
|
|
1278
|
+
const ws = new WSv2({ packetWDDelay: 100 })
|
|
1279
|
+
ws._isOpen = true
|
|
1280
|
+
|
|
1281
|
+
ws.on('error', () => {}) // invalid json to prevent message routing
|
|
1282
|
+
|
|
1283
|
+
const sendInterval = setInterval(() => {
|
|
1284
|
+
ws._onWSMessage('asdf')
|
|
1285
|
+
}, 50)
|
|
1286
|
+
|
|
1287
|
+
ws._triggerPacketWD = () => assert(false)
|
|
1288
|
+
ws._onWSMessage('asdf')
|
|
1289
|
+
|
|
1290
|
+
setTimeout(() => {
|
|
1291
|
+
clearInterval(sendInterval)
|
|
1292
|
+
done()
|
|
1293
|
+
}, 200)
|
|
1294
|
+
})
|
|
1295
|
+
})
|