bot18
Version:
A high-frequency cryptocurrency trading bot by Zenbot creator @carlos8f
1,370 lines (1,100 loc) • 34.6 kB
JavaScript
/* eslint-env mocha */
const assert = require('assert')
const WSv2 = require('../../../lib/transports/ws2')
const { MockWSv2Server } = require('bfx-api-mock-srv')
const API_KEY = 'dummy'
const API_SECRET = 'dummy'
const createTestWSv2Instance = (params = {}) => {
return new WSv2(Object.assign({
apiKey: API_KEY,
apiSecret: API_SECRET,
url: 'ws://localhost:9997'
}, params))
}
describe('WSv2 utilities', () => {
it('_registerListener: correctly adds listener to internal map with cbGID', () => {
const ws = new WSv2()
ws._registerListener('trade', { 2: 'tBTCUSD' }, Map, 42, () => {})
const { _listeners } = ws
assert.equal(Object.keys(_listeners).length, 1)
assert.equal(Object.keys(_listeners)[0], 42)
assert.equal(typeof _listeners[42], 'object')
const listenerSet = _listeners[42]
assert.equal(Object.keys(listenerSet).length, 1)
assert.equal(Object.keys(listenerSet)[0], 'trade')
assert.equal(listenerSet.trade.constructor.name, 'Array')
assert.equal(listenerSet.trade.length, 1)
const listener = listenerSet.trade[0]
assert.equal(listener.modelClass, Map)
assert.deepEqual(listener.filter, { '2': 'tBTCUSD' })
assert.equal(typeof listener.cb, 'function')
})
it('enableSequencing: sends the correct conf flag', (done) => {
const ws = new WSv2()
ws.send = (packet) => {
assert.equal(packet.event, 'conf')
assert.equal(packet.flags, 65536)
done()
}
ws.enableSequencing()
})
it('getCandles: returns empty array if no candle set is available', () => {
const ws = new WSv2()
assert.deepEqual(ws.getCandles('i.dont.exist'), [])
})
it('_sendCalc: stringifes payload & passes it to the ws client', (done) => {
const ws = new WSv2()
ws._ws = {}
ws._ws.send = (data) => {
assert.equal(data, '[]')
done()
}
ws._sendCalc([])
})
})
describe('WSv2 lifetime', () => {
it('starts unopened & unauthenticated', () => {
const ws = createTestWSv2Instance()
assert.equal(ws.isOpen(), false)
assert.equal(ws.isAuthenticated(), false)
})
it('open: fails to open twice', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.on('open', () => {
ws.open().then(() => assert(false)).catch(() => {
wss.close()
done()
})
})
ws.open()
})
it('open: updates open flag', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.on('open', () => {
assert.equal(ws.isOpen(), true)
wss.close()
done()
})
ws.open()
})
it('close: doesn\'t close if not open', (done) => {
const ws = createTestWSv2Instance()
ws.close().then(() => assert(false)).catch(() => {
done()
})
})
it('close: fails to close twice', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.open()
ws.on('open', ws.close.bind(ws))
ws.on('close', () => {
ws.close().then(() => assert(false)).catch(() => {
wss.close()
done()
})
})
})
it('close: clears connection state', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws._onWSClose = () => {} // disable fallback reset
ws.open()
ws.on('open', () => {
assert(ws._ws !== null)
assert(ws._isOpen)
ws.close().then(() => {
assert(ws._ws == null)
assert(!ws._isOpen)
wss.close()
done()
})
})
})
it('auth: fails to auth twice', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.open()
ws.on('open', ws.auth.bind(ws))
ws.once('auth', () => {
ws.auth().then(() => assert(false)).catch(() => {
wss.close()
done()
})
})
})
it('auth: updates auth flag', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.open()
ws.on('open', ws.auth.bind(ws))
ws.once('auth', () => {
assert(ws.isAuthenticated())
wss.close()
done()
})
})
it('auth: forwards calc param', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.open()
ws.on('open', () => {
ws.send = (data) => {
assert.equal(data.calc, 42)
wss.close()
done()
}
ws.auth(42)
})
})
it('reconnect: connects if not already connected', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.on('close', () => {
assert(false)
})
ws.on('open', () => {
wss.close()
done()
})
ws.reconnect()
})
it('reconnect: disconnects & connects back if currently connected', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
let calls = 0
ws.on('close', () => {
if (++calls === 2) {
wss.close()
done()
}
})
ws.once('open', () => {
ws.reconnect()
ws.once('open', () => {
if (++calls === 2) {
wss.close()
done()
}
})
})
ws.open()
})
it('reconnect: automatically auths on open if previously authenticated', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
let closed = false
let opened = false
ws.on('error', done)
ws.once('open', ws.auth.bind(ws))
ws.once('auth', () => {
setTimeout(() => {
ws.once('close', () => { closed = true })
ws.once('open', () => { opened = true })
ws.once('auth', () => {
assert(closed)
assert(opened)
wss.close()
done()
})
ws.reconnect()
}, 50)
})
ws.open()
})
})
describe('WSv2 constructor', () => {
it('defaults to production WS url', () => {
const ws = new WSv2()
assert.notEqual(ws._url.indexOf('api.bitfinex.com'), -1)
})
it('defaults to no transform', () => {
const ws = createTestWSv2Instance()
const transWS = createTestWSv2Instance({ transform: true })
assert.equal(ws._transform, false)
assert.equal(transWS._transform, true)
})
})
describe('WSv2 auto reconnect', () => {
it('reconnects on close if autoReconnect is enabled', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance({
autoReconnect: true
})
ws.on('open', ws.auth.bind(ws))
ws.once('auth', () => {
ws.reconnect = () => done()
wss.close() // trigger reconnect
})
ws.open()
})
it('respects reconnectDelay', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance({
autoReconnect: true,
reconnectDelay: 75
})
ws.on('open', ws.auth.bind(ws))
ws.once('auth', () => {
let now = Date.now()
ws.reconnect = () => {
assert((Date.now() - now) >= 70)
done()
}
wss.close() // trigger reconnect
})
ws.open()
})
it('does not auto-reconnect if explicity closed', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance({
autoReconnect: true
})
ws.on('open', ws.auth.bind(ws))
ws.once('auth', () => {
ws.reconnect = () => assert(false)
ws.close()
setTimeout(() => {
wss.close()
done()
}, 50)
})
ws.open()
})
})
describe('WSv2 seq audit', () => {
it('automatically enables sequencing if seqAudit is true in constructor', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance({
seqAudit: true
})
wss._onClientMessage = (ws, msgJSON) => {
const msg = JSON.parse(msgJSON)
if (msg.event === 'conf' && msg.flags === 65536) {
wss.close()
done()
}
}
ws.open()
})
it('emits error on invalid seq number', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance({
seqAudit: true
})
let errorsSeen = 0
ws.once('open', ws.auth.bind(ws))
ws.on('error', (err) => {
if (err.message.indexOf('seq #') !== -1) errorsSeen++
return null
})
ws.once('auth', () => {
ws._channelMap[42] = { channel: 'trades', chanId: 42 }
ws._onWSMessage(JSON.stringify([0, 'tu', [], 0, 0]))
ws._onWSMessage(JSON.stringify([0, 'te', [], 1, 0]))
ws._onWSMessage(JSON.stringify([0, 'wu', [], 2, 1]))
ws._onWSMessage(JSON.stringify([0, 'tu', [], 3, 2])) //
ws._onWSMessage(JSON.stringify([0, 'tu', [], 4, 4])) // error
ws._onWSMessage(JSON.stringify([0, 'tu', [], 5, 5]))
ws._onWSMessage(JSON.stringify([0, 'tu', [], 6, 6]))
ws._onWSMessage(JSON.stringify([42, [], 7]))
ws._onWSMessage(JSON.stringify([42, [], 8]))
ws._onWSMessage(JSON.stringify([42, [], 9])) //
ws._onWSMessage(JSON.stringify([42, [], 13])) // error
ws._onWSMessage(JSON.stringify([42, [], 14]))
ws._onWSMessage(JSON.stringify([42, [], 15]))
assert.equal(errorsSeen, 6)
wss.close()
done()
})
ws.open()
})
})
describe('WSv2 ws event handlers', () => {
it('_onWSOpen: updates open flag', () => {
const ws = new WSv2()
assert(!ws.isOpen())
ws._onWSOpen()
assert(ws.isOpen())
})
it('_onWSClose: updates open flag', () => {
const ws = new WSv2()
ws._onWSOpen()
assert(ws.isOpen())
ws._onWSClose()
assert(!ws.isOpen())
})
it('_onWSError: emits error', (done) => {
const ws = new WSv2()
ws.on('error', () => done())
ws._onWSError(new Error())
})
it('_onWSMessage: emits error on invalid packet', (done) => {
const ws = new WSv2()
ws.on('error', () => done())
ws._onWSMessage('I can\'t believe it\'s not JSON!')
})
it('_onWSMessage: emits message', (done) => {
const ws = new WSv2()
const msg = [1]
const flags = 'flags'
ws.on('message', (m, f) => {
assert.deepEqual(m, msg)
assert.equal(flags, 'flags')
done()
})
ws._onWSMessage(JSON.stringify(msg), flags)
})
it('_onWSNotification: triggers event callbacks for new orders', (done) => {
const ws = new WSv2()
const kNew = 'order-new-42'
ws._eventCallbacks.push(kNew, (err, order) => {
assert(!err)
assert(order)
assert.deepEqual(order, [0, 0, 42])
ws._eventCallbacks.push(kNew, (err, order) => {
assert(err)
assert.deepEqual(order, [0, 0, 42])
done()
})
ws._onWSNotification([0, 'on-req', null, null, [0, 0, 42], 0, 'ERROR'])
})
ws._onWSNotification([0, 'on-req', null, null, [0, 0, 42], 0, 'SUCCESS'])
})
it('_onWSNotification: triggers event callbacks for cancelled orders', (done) => {
const ws = new WSv2()
const kCancel = 'order-cancel-42'
ws._eventCallbacks.push(kCancel, (err, order) => {
assert(!err)
assert(order)
assert.deepEqual(order, [42])
ws._eventCallbacks.push(kCancel, (err, order) => {
assert(err)
assert.deepEqual(order, [42])
done()
})
ws._onWSNotification([0, 'oc-req', null, null, [42], 0, 'ERROR'])
})
ws._onWSNotification([0, 'oc-req', null, null, [42], 0, 'SUCCESS'])
})
})
describe('WSv2 channel msg handling', () => {
it('_handleChannelMessage: emits message', (done) => {
const ws = new WSv2()
const packet = [42, 'tu', []]
ws._channelMap = {
42: { channel: 'meaning' }
}
ws.on('meaning', (msg) => {
assert.deepEqual(msg, packet)
done()
})
ws._handleChannelMessage(packet)
})
it('_handleChannelMessage: calls all registered listeners (nofilter)', (done) => {
const ws = new WSv2()
ws._channelMap = { 0: { channel: 'auth' } }
let called = 0
ws.onWalletUpdate({}, () => {
if (++called === 2) done()
})
ws.onWalletUpdate({}, () => {
if (++called === 2) done()
})
ws._handleChannelMessage([0, 'wu', []])
})
const doFilterTest = (transform, done) => {
const ws = new WSv2({ transform })
ws._channelMap = { 0: { channel: 'auth' } }
let calls = 0
let btcListenerCalled = false
ws.onTradeEntry({ pair: 'tBTCUSD' }, () => {
assert(!btcListenerCalled)
btcListenerCalled = true
if (++calls === 7) done()
})
ws.onTradeEntry({}, () => {
if (++calls === 7) done()
})
ws.onTradeEntry({}, () => {
if (++calls === 7) done()
})
ws._handleChannelMessage([0, 'te', ['tETHUSD']])
ws._handleChannelMessage([0, 'te', ['tETHUSD']])
ws._handleChannelMessage([0, 'te', ['tBTCUSD']])
}
it('_handleChannelMessage: filters messages if listeners require it (transform)', (done) => {
doFilterTest(true, done)
})
it('_handleChannelMessage: filters messages if listeners require it (no transform)', (done) => {
doFilterTest(false, done)
})
it('_handleChannelMessage: transforms payloads if enabled', (done) => {
let calls = 0
const wsTransform = new WSv2({ transform: true })
const wsNoTransform = new WSv2({ transform: false })
wsTransform._channelMap = { 0: { channel: 'auth' } }
wsNoTransform._channelMap = { 0: { channel: 'auth' } }
const tradeData = [
0, 'tBTCUSD', Date.now(), 0, 0.1, 1, 'type', 1, 1, 0.001, 'USD'
]
wsNoTransform.onTradeUpdate({}, (trade) => {
assert.equal(trade.constructor.name, 'Array')
assert.deepEqual(trade, tradeData)
if (calls++ === 1) done()
})
wsTransform.onTradeUpdate({}, (trade) => {
assert.equal(trade.constructor.name, 'Trade')
assert.equal(trade.id, tradeData[0])
assert.equal(trade.pair, tradeData[1])
assert.equal(trade.mtsCreate, tradeData[2])
assert.equal(trade.orderID, tradeData[3])
assert.equal(trade.execAmount, tradeData[4])
assert.equal(trade.execPrice, tradeData[5])
assert.equal(trade.orderType, tradeData[6])
assert.equal(trade.orderPrice, tradeData[7])
assert.equal(trade.maker, tradeData[8])
assert.equal(trade.fee, tradeData[9])
assert.equal(trade.feeCurrency, tradeData[10])
if (calls++ === 1) done()
})
wsTransform._handleChannelMessage([0, 'tu', tradeData])
wsNoTransform._handleChannelMessage([0, 'tu', tradeData])
})
it('onMessage: calls the listener with all messages (no filter)', (done) => {
const ws = new WSv2()
ws._channelMap = { 0: { channel: 'auth' } }
let calls = 0
ws.onMessage({}, (msg) => {
if (++calls === 2) done()
})
ws._handleChannelMessage([0, 'wu', []])
ws._handleChannelMessage([0, 'tu', []])
})
it('_payloadPassesFilter: correctly detects matching payloads', () => {
const filter = {
1: 'tBTCUSD'
}
const goodPayloads = [
[0, 'tBTCUSD', 42, ''],
[0, 'tBTCUSD', 3.14, '']
]
const badPayloads = [
[0, 'tETHUSD', 42, ''],
[0, 'tETHUSD', 3.14, '']
]
goodPayloads.forEach(p => assert(WSv2._payloadPassesFilter(p, filter)))
badPayloads.forEach(p => assert(!WSv2._payloadPassesFilter(p, filter)))
})
it('_notifyListenerGroup: notifies all matching listeners in the group', (done) => {
let calls = 0
const func = () => {
assert(calls < 3)
if (++calls === 2) done()
}
const lg = {
'': [{ cb: func }],
'test': [{ cb: func }],
'nope': [{ cb: func }]
}
WSv2._notifyListenerGroup(lg, [0, 'test', [0, 'tu']], false)
})
it('_notifyListenerGroup: doesn\'t fail on missing data if filtering', (done) => {
const lg = {
'test': [{
filter: { 1: 'on' },
cb: () => {
done(new Error('filter should not have matched'))
}
}]
}
WSv2._notifyListenerGroup(lg, [0, 'test'], false)
done()
})
it('_propagateMessageToListeners: notifies all matching listeners', (done) => {
const ws = new WSv2()
ws._channelMap = { 0: { channel: 'auth' } }
ws.onTradeEntry({ pair: 'tBTCUSD' }, () => {
done()
})
ws._propagateMessageToListeners([0, 'te', ['tBTCUSD']])
})
it('_notifyCatchAllListeners: passes data to all listeners on the empty \'\' event', () => {
let s = 0
const lg = {
'': [
{ cb: d => { s += d } },
{ cb: d => { s += (d * 2) } }
]
}
WSv2._notifyCatchAllListeners(lg, 5)
assert.equal(s, 15)
})
it('_handleOBMessage: maintains internal OB if management is enabled', () => {
const ws = new WSv2({
manageOrderBooks: true,
transform: true
})
ws._channelMap = { 42: {
channel: 'orderbook',
symbol: 'tBTCUSD'
}}
ws._handleOBMessage([42, [
[100, 2, -4],
[200, 4, -8],
[300, 1, 3]
]], ws._channelMap[42])
let ob = ws.getOB('tBTCUSD')
assert(ob !== null)
assert.equal(ob.bids.length, 1)
assert.deepEqual(ob.bids, [[300, 1, 3]])
assert.equal(ob.asks.length, 2)
assert.deepEqual(ob.getEntry(100), { price: 100, count: 2, amount: -4 })
assert.deepEqual(ob.getEntry(200), { price: 200, count: 4, amount: -8 })
ws._handleOBMessage([42, [300, 0, 1]], ws._channelMap[42])
ob = ws.getOB('tBTCUSD')
assert.equal(ob.bids.length, 0)
})
it('_handleOBMessage: emits error on internal OB update failure', (done) => {
const wsNoTransform = new WSv2({ manageOrderBooks: true })
const wsTransform = new WSv2({
manageOrderBooks: true,
transform: true
})
wsNoTransform._channelMap = { 42: {
channel: 'orderbook',
symbol: 'tBTCUSD'
}}
wsTransform._channelMap = wsNoTransform._channelMap
let errorsSeen = 0
wsNoTransform.on('error', () => {
if (++errorsSeen === 2) done()
})
wsTransform.on('error', () => {
if (++errorsSeen === 2) done()
})
wsTransform._handleOBMessage([42, [100, 0, 1]], wsTransform._channelMap[42])
wsNoTransform._handleOBMessage([42, [100, 0, 1]], wsNoTransform._channelMap[42])
})
it('_handleOBMessage: forwards managed ob to listeners', (done) => {
const ws = new WSv2({ manageOrderBooks: true })
ws._channelMap = { 42: {
channel: 'orderbook',
symbol: 'tBTCUSD'
}}
let seen = 0
ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => {
assert.deepEqual(ob, [[100, 2, 3]])
if (++seen === 2) done()
})
ws.onOrderBook({}, (ob) => {
assert.deepEqual(ob, [[100, 2, 3]])
if (++seen === 2) done()
})
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
})
it('_handleOBMessage: filters by prec and len', (done) => {
const ws = new WSv2({ manageOrderBooks: true })
ws._channelMap = {
40: {
channel: 'orderbook',
symbol: 'tBTCUSD',
prec: 'P0'
},
41: {
channel: 'orderbook',
symbol: 'tBTCUSD',
prec: 'P1'
},
42: {
channel: 'orderbook',
symbol: 'tBTCUSD',
prec: 'P2'
}
}
let seen = 0
ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P0' }, (ob) => {
assert(false)
})
ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P1' }, (ob) => {
assert(false)
})
ws.onOrderBook({ symbol: 'tBTCUSD', prec: 'P2' }, (ob) => {
if (++seen === 2) done()
})
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
ws._handleOBMessage([42, [100, 2, 3]], ws._channelMap[42])
})
it('_handleOBMessage: emits managed ob', (done) => {
const ws = new WSv2({ manageOrderBooks: true })
ws._channelMap = { 42: {
channel: 'orderbook',
symbol: 'tBTCUSD'
}}
ws.on('orderbook', (symbol, data) => {
assert.equal(symbol, 'tBTCUSD')
assert.deepEqual(data, [[100, 2, 3]])
done()
})
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
})
it('_handleOBMessage: forwards transformed data if transform enabled', (done) => {
const ws = new WSv2({ transform: true })
ws._channelMap = { 42: {
chanId: 42,
channel: 'orderbook',
symbol: 'tBTCUSD'
}}
ws.onOrderBook({ symbol: 'tBTCUSD' }, (ob) => {
assert(ob.asks)
assert(ob.bids)
assert.equal(ob.asks.length, 0)
assert.deepEqual(ob.bids, [[100, 2, 3]])
done()
})
ws._handleOBMessage([42, [[100, 2, 3]]], ws._channelMap[42])
})
it('_updateManagedOB: returns an error on rm non-existent entry', () => {
const ws = new WSv2()
ws._orderBooks.tBTCUSD = [
[100, 1, 1],
[200, 2, 1]
]
const err = ws._updateManagedOB('tBTCUSD', [150, 0, -1])
assert(err)
assert(err instanceof Error)
})
it('_updateManagedOB: correctly maintains transformed OBs', () => {
const ws = new WSv2({ transform: true })
ws._orderBooks.tBTCUSD = []
assert(!ws._updateManagedOB('tBTCUSD', [100, 1, 1]))
assert(!ws._updateManagedOB('tBTCUSD', [200, 1, -1]))
assert(!ws._updateManagedOB('tBTCUSD', [200, 0, -1]))
const ob = ws.getOB('tBTCUSD')
assert.equal(ob.bids.length, 1)
assert.equal(ob.asks.length, 0)
assert.deepEqual(ob.bids, [[100, 1, 1]])
})
it('_updateManagedOB: correctly maintains non-transformed OBs', () => {
const ws = new WSv2()
ws._orderBooks.tBTCUSD = []
assert(!ws._updateManagedOB('tBTCUSD', [100, 1, 1]))
assert(!ws._updateManagedOB('tBTCUSD', [200, 1, -1]))
assert(!ws._updateManagedOB('tBTCUSD', [200, 0, -1]))
const ob = ws._orderBooks.tBTCUSD
assert.equal(ob.length, 1)
assert.deepEqual(ob, [[100, 1, 1]])
})
it('_handleCandleMessage: maintains internal candles if management is enabled', () => {
const ws = new WSv2({ manageCandles: true })
ws._channelMap = { 64: {
channel: 'candles',
key: 'trade:1m:tBTCUSD'
}}
ws._handleCandleMessage([64, [
[5, 100, 70, 150, 30, 1000],
[2, 200, 90, 150, 30, 1000],
[1, 130, 90, 150, 30, 1000],
[4, 104, 80, 150, 30, 1000]
]], ws._channelMap[64])
const candles = ws._candles['trade:1m:tBTCUSD']
// maintains sort
assert.equal(candles.length, 4)
assert.equal(candles[0][0], 5)
assert.equal(candles[1][0], 4)
assert.equal(candles[2][0], 2)
assert.equal(candles[3][0], 1)
// updates existing candle
ws._handleCandleMessage([
64,
[5, 200, 20, 220, 20, 2000]
], ws._channelMap[64])
assert.deepEqual(candles[0], [5, 200, 20, 220, 20, 2000])
// inserts new candle
ws._handleCandleMessage([
64,
[10, 300, 20, 450, 10, 4000]
], ws._channelMap[64])
assert.deepEqual(candles[0], [10, 300, 20, 450, 10, 4000])
})
it('_handleCandleMessage: emits error on internal candle update failure', (done) => {
const ws = new WSv2({ manageCandles: true })
ws._channelMap = {
42: {
channel: 'candles',
key: 'trade:30m:tBTCUSD'
},
64: {
channel: 'candles',
key: 'trade:1m:tBTCUSD'
}
}
let errorsSeen = 0
ws.on('error', () => {
if (++errorsSeen === 1) done()
})
ws._handleCandleMessage([64, [
[5, 100, 70, 150, 30, 1000],
[2, 200, 90, 150, 30, 1000],
[1, 130, 90, 150, 30, 1000],
[4, 104, 80, 150, 30, 1000]
]], ws._channelMap[64])
// update for unknown key
ws._handleCandleMessage([
42,
[5, 10, 70, 150, 30, 10]
], ws._channelMap[42])
})
it('_handleCandleMessage: forwards managed candles to listeners', (done) => {
const ws = new WSv2({ manageCandles: true })
ws._channelMap = { 42: {
chanId: 42,
channel: 'candles',
key: 'trade:1m:tBTCUSD'
}}
let seen = 0
ws.onCandle({ key: 'trade:1m:tBTCUSD' }, (data) => {
assert.deepEqual(data, [[5, 10, 70, 150, 30, 10]])
if (++seen === 2) done()
})
ws.onCandle({}, (data) => {
assert.deepEqual(data, [[5, 10, 70, 150, 30, 10]])
if (++seen === 2) done()
})
ws._handleCandleMessage([
42,
[[5, 10, 70, 150, 30, 10]]
], ws._channelMap[42])
})
it('_handleCandleMessage: emits managed candles', (done) => {
const ws = new WSv2({ manageCandles: true })
ws._channelMap = { 42: {
channel: 'candles',
key: 'trade:1m:tBTCUSD'
}}
ws.on('candle', (data, key) => {
assert.equal(key, 'trade:1m:tBTCUSD')
assert.deepEqual(data, [[5, 10, 70, 150, 30, 10]])
done()
})
ws._handleCandleMessage([
42,
[[5, 10, 70, 150, 30, 10]]
], ws._channelMap[42])
})
it('_handleCandleMessage: forwards transformed data if transform enabled', (done) => {
const ws = new WSv2({ transform: true })
ws._channelMap = { 42: {
chanId: 42,
channel: 'candles',
key: 'trade:1m:tBTCUSD'
}}
ws.onCandle({ key: 'trade:1m:tBTCUSD' }, (candles) => {
assert.equal(candles.length, 1)
assert.deepEqual(candles[0], {
mts: 5,
open: 10,
close: 70,
high: 150,
low: 30,
volume: 10
})
done()
})
ws._handleCandleMessage([
42,
[[5, 10, 70, 150, 30, 10]]
], ws._channelMap[42])
})
it('_updateManagedCandles: returns an error on update for unknown key', () => {
const ws = new WSv2()
ws._candles['trade:1m:tBTCUSD'] = []
const err = ws._updateManagedCandles('trade:30m:tBTCUSD', [
1, 10, 70, 150, 30, 10
])
assert(err)
assert(err instanceof Error)
})
it('_updateManagedCandles: correctly maintains transformed OBs', () => {
const ws = new WSv2({ transform: true })
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
[1, 10, 70, 150, 30, 10],
[2, 10, 70, 150, 30, 10]
]))
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
2, 10, 70, 150, 30, 500
]))
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
3, 100, 70, 150, 30, 10
]))
const candles = ws._candles['trade:1m:tBTCUSD']
assert.equal(candles.length, 3)
assert.deepEqual(candles[0], [
3, 100, 70, 150, 30, 10
])
assert.deepEqual(candles[1], [
2, 10, 70, 150, 30, 500
])
assert.deepEqual(candles[2], [
1, 10, 70, 150, 30, 10
])
})
it('_updateManagedCandles: correctly maintains non-transformed OBs', () => {
const ws = new WSv2()
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
[1, 10, 70, 150, 30, 10],
[2, 10, 70, 150, 30, 10]
]))
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
2, 10, 70, 150, 30, 500
]))
assert(!ws._updateManagedCandles('trade:1m:tBTCUSD', [
3, 100, 70, 150, 30, 10
]))
const candles = ws._candles['trade:1m:tBTCUSD']
assert.equal(candles.length, 3)
assert.deepEqual(candles[0], [
3, 100, 70, 150, 30, 10
])
assert.deepEqual(candles[1], [
2, 10, 70, 150, 30, 500
])
assert.deepEqual(candles[2], [
1, 10, 70, 150, 30, 10
])
})
})
describe('WSv2 event msg handling', () => {
it('_handleErrorEvent: emits error', (done) => {
const ws = new WSv2()
ws.on('error', (err) => {
if (err === 42) done()
})
ws._handleErrorEvent(42)
})
it('_handleConfigEvent: emits error if config failed', (done) => {
const ws = new WSv2()
ws.on('error', (err) => {
if (err.message.indexOf('42') !== -1) done()
})
ws._handleConfigEvent({ status: 'bad', flags: 42 })
})
it('_handleAuthEvent: emits an error on auth fail', (done) => {
const ws = new WSv2()
ws.on('error', () => {
done()
})
ws._handleAuthEvent({ status: 'FAIL' })
})
it('_handleAuthEvent: updates auth flag on auth success', () => {
const ws = new WSv2()
assert(!ws.isAuthenticated())
ws._handleAuthEvent({ status: 'OK' })
assert(ws.isAuthenticated())
})
it('_handleAuthEvent: adds auth channel to channel map', () => {
const ws = new WSv2()
assert(Object.keys(ws._channelMap).length === 0)
ws._handleAuthEvent({ chanId: 42, status: 'OK' })
assert(ws._channelMap[42])
assert.equal(ws._channelMap[42].channel, 'auth')
})
it('_handleAuthEvent: emits auth message', (done) => {
const ws = new WSv2()
ws.once('auth', (msg) => {
assert.equal(msg.chanId, 0)
assert.equal(msg.status, 'OK')
done()
})
ws._handleAuthEvent({ chanId: 0, status: 'OK' })
})
it('_handleSubscribedEvent: adds channel to channel map', () => {
const ws = new WSv2()
assert(Object.keys(ws._channelMap).length === 0)
ws._handleSubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' })
assert(ws._channelMap[42])
assert.equal(ws._channelMap[42].chanId, 42)
assert.equal(ws._channelMap[42].channel, 'test')
assert.equal(ws._channelMap[42].extra, 'stuff')
})
it('_handleUnsubscribedEvent: removes channel from channel map', () => {
const ws = new WSv2()
assert(Object.keys(ws._channelMap).length === 0)
ws._handleSubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' })
ws._handleUnsubscribedEvent({ chanId: 42, channel: 'test', extra: 'stuff' })
assert(Object.keys(ws._channelMap).length === 0)
})
it('_handleInfoEvent: passes message to relevant listeners (raw access)', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.once('open', () => {
let n = 0
ws._infoListeners[42] = [
() => { n += 1 },
() => { n += 2 }
]
ws._handleInfoEvent({ code: 42 })
assert.equal(n, 3)
wss.close()
done()
})
ws.open()
})
it('_handleInfoEvent: passes message to relevant listeners', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.once('open', () => {
let n = 0
ws.onInfoMessage(42, () => { n += 1 })
ws.onInfoMessage(42, () => { n += 2 })
ws._handleInfoEvent({ code: 42 })
assert.equal(n, 3)
wss.close()
done()
})
ws.open()
})
it('_handleInfoEvent: passes message to relevant named listeners', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
ws.once('open', () => {
let n = 0
ws.onServerRestart(() => { n += 1 })
ws.onMaintenanceStart(() => { n += 10 })
ws.onMaintenanceEnd(() => { n += 100 })
ws._handleInfoEvent({ code: WSv2.info.SERVER_RESTART })
ws._handleInfoEvent({ code: WSv2.info.MAINTENANCE_START })
ws._handleInfoEvent({ code: WSv2.info.MAINTENANCE_END })
assert.equal(n, 111)
wss.close()
done()
})
ws.open()
})
it('_handleInfoEvent: closes & emits error if not on api v2', (done) => {
const wss = new MockWSv2Server()
const ws = createTestWSv2Instance()
let seen = 0
const d = () => {
wss.close()
done()
}
ws.once('open', () => {
ws.on('error', () => { if (++seen === 2) { d() } })
ws.on('close', () => { if (++seen === 2) { d() } })
ws._handleInfoEvent({ version: 3 })
})
ws.open()
})
it('_flushOrderOps: returned promise rejects if not authorised', (done) => {
const ws = new WSv2()
ws._orderOpBuffer = [
[0, 'oc', null, []]
]
ws._flushOrderOps().catch(() => done())
})
it('_flushOrderOps: merges the buffer into a multi-op packet & sends', (done) => {
const ws = new WSv2()
ws._isAuthenticated = true
ws._orderOpBuffer = [
[0, 'oc', null, []],
[0, 'on', null, []],
[0, 'oc_multi', null, []],
[0, 'ou', null, []]
]
ws.send = (packet) => {
assert.equal(packet[1], 'ox_multi')
assert.equal(packet[3].length, 4)
done()
}
ws._flushOrderOps().catch(() => assert(false))
})
it('_flushOrderOps: splits up buffers greater than 15 ops in size', (done) => {
const ws = new WSv2()
ws._isAuthenticated = true
let seenCount = 0
for (let i = 0; i < 45; i++) {
ws._orderOpBuffer.push([0, 'oc', null, []])
}
ws.send = (packet) => {
assert.equal(packet[1], 'ox_multi')
assert(packet[3].length <= 15)
seenCount += packet[3].length
if (seenCount === 45) done()
}
ws._flushOrderOps().catch(() => assert(false))
})
})
describe('WSv2 packet watch-dog', () => {
it('resets the WD timeout on every websocket message', (done) => {
const ws = new WSv2({ packetWDDelay: 1000 })
assert.equal(ws._packetWDTimeout, null)
ws.on('error', () => {}) // ignore json errors
let wdResets = 0
ws._resetPacketWD = () => {
if (++wdResets === 4) done()
}
ws._onWSMessage('asdf')
ws._onWSMessage('asdf')
ws._onWSMessage('asdf')
ws._onWSMessage('asdf')
})
it('_resetPacketWD: clears existing wd timeout', (done) => {
const ws = new WSv2({ packetWDDelay: 1000 })
ws._packetWDTimeout = setTimeout(() => {
assert(false)
}, 100)
ws._resetPacketWD()
setTimeout(done, 200)
})
it('_resetPacketWD: schedules new wd timeout', (done) => {
const ws = new WSv2({ packetWDDelay: 500 })
ws._isOpen = true
ws._triggerPacketWD = () => done()
ws._resetPacketWD()
assert(ws._packetWDTimeout !== null)
})
it('_triggerPacketWD: does nothing if wd is disabled', (done) => {
const ws = new WSv2()
ws._isOpen = true
ws.reconnect = () => assert(false)
ws._triggerPacketWD()
setTimeout(() => {
done()
}, 50)
})
it('_triggerPacketWD: calls reconnect()', (done) => {
const ws = new WSv2({ packetWDDelay: 1000 })
ws._isOpen = true
ws.reconnect = () => done()
ws._triggerPacketWD()
})
it('triggers wd when no packet arrives after delay elapses', (done) => {
const ws = new WSv2({ packetWDDelay: 100 })
const now = Date.now()
ws._isOpen = true
ws.on('error', () => {}) // invalid json to prevent message routing
ws._triggerPacketWD = () => {
assert((Date.now() - now) >= 95)
done()
}
ws._onWSMessage('asdf') // send first packet, init wd
})
it('doesn\'t trigger wd when packets arrive as expected', (done) => {
const ws = new WSv2({ packetWDDelay: 100 })
ws._isOpen = true
ws.on('error', () => {}) // invalid json to prevent message routing
const sendInterval = setInterval(() => {
ws._onWSMessage('asdf')
}, 50)
ws._triggerPacketWD = () => assert(false)
ws._onWSMessage('asdf')
setTimeout(() => {
clearInterval(sendInterval)
done()
}, 200)
})
})