From f61e79934de2feac58ba5e6871c52c9aa1f49210 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 15 Sep 2022 19:06:35 +0100 Subject: [PATCH] cleaning around tests --- src/types/websockets.ts | 2 +- src/util/websocket-util.ts | 50 ++++-- src/websocket-client.ts | 144 +++++++++++------- ...{private.ws.test.ts => ws.private.test.ts} | 0 .../{public.ws.test.ts => ws.public.test.ts} | 2 +- ...{private.ws.test.ts => ws.private.test.ts} | 2 +- .../{public.ws.test.ts => ws.public.test.ts} | 5 +- test/spot/ws.private.v1.test.ts | 58 +++++++ test/spot/ws.public.v1.test.ts | 64 ++++++++ test/spot/ws.public.v3.test.ts | 72 +++++++++ 10 files changed, 323 insertions(+), 76 deletions(-) rename test/inverse/{private.ws.test.ts => ws.private.test.ts} (100%) rename test/inverse/{public.ws.test.ts => ws.public.test.ts} (96%) rename test/linear/{private.ws.test.ts => ws.private.test.ts} (97%) rename test/linear/{public.ws.test.ts => ws.public.test.ts} (94%) create mode 100644 test/spot/ws.private.v1.test.ts create mode 100644 test/spot/ws.public.v1.test.ts create mode 100644 test/spot/ws.public.v3.test.ts diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 4b713db..180f518 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,6 +1,6 @@ import { RestClientOptions, WS_KEY_MAP } from '../util'; -export type APIMarket = 'inverse' | 'linear' | 'spot'; //| 'v3'; +export type APIMarket = 'inverse' | 'linear' | 'spot' | 'spotV3'; //| 'v3'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 5dafc45..72f6d53 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -1,4 +1,4 @@ -import { WsKey } from '../types'; +import { APIMarket, WsKey } from '../types'; interface NetworkMapV3 { livenet: string; @@ -10,42 +10,52 @@ interface NetworkMapV3 { type PublicPrivateNetwork = 'public' | 'private'; export const WS_BASE_URL_MAP: Record< - string, + APIMarket, Record > = { inverse: { - private: { + public: { livenet: 'wss://stream.bybit.com/realtime', testnet: 'wss://stream-testnet.bybit.com/realtime', }, - public: { + private: { livenet: 'wss://stream.bybit.com/realtime', testnet: 'wss://stream-testnet.bybit.com/realtime', }, }, linear: { - private: { - livenet: 'wss://stream.bybit.com/realtime_private', - livenet2: 'wss://stream.bytick.com/realtime_private', - testnet: 'wss://stream-testnet.bybit.com/realtime_private', - }, public: { livenet: 'wss://stream.bybit.com/realtime_public', livenet2: 'wss://stream.bytick.com/realtime_public', testnet: 'wss://stream-testnet.bybit.com/realtime_public', }, + private: { + livenet: 'wss://stream.bybit.com/realtime_private', + livenet2: 'wss://stream.bytick.com/realtime_private', + testnet: 'wss://stream-testnet.bybit.com/realtime_private', + }, }, spot: { - private: { - livenet: 'wss://stream.bybit.com/spot/ws', - testnet: 'wss://stream-testnet.bybit.com/spot/ws', - }, public: { livenet: 'wss://stream.bybit.com/spot/quote/ws/v1', livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2', testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1', testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2', }, + private: { + livenet: 'wss://stream.bybit.com/spot/ws', + testnet: 'wss://stream-testnet.bybit.com/spot/ws', + }, + }, + spotV3: { + public: { + livenet: 'wss://stream.bybit.com/spot/public/v3', + testnet: 'wss://stream-testnet.bybit.com/spot/public/v3', + }, + private: { + livenet: 'wss://stream.bybit.com/spot/private/v3', + testnet: 'wss://stream-testnet.bybit.com/spot/private/v3', + }, }, }; @@ -55,6 +65,8 @@ export const WS_KEY_MAP = { linearPublic: 'linearPublic', spotPrivate: 'spotPrivate', spotPublic: 'spotPublic', + spotV3Private: 'spotV3Private', + spotV3Public: 'spotV3Public', } as const; export const PUBLIC_WS_KEYS = [ @@ -77,7 +89,10 @@ export function getLinearWsKeyForTopic(topic: string): WsKey { return WS_KEY_MAP.linearPublic; } -export function getSpotWsKeyForTopic(topic: string): WsKey { +export function getSpotWsKeyForTopic( + topic: string, + apiVersion: 'v1' | 'v3' +): WsKey { const privateTopics = [ 'position', 'execution', @@ -88,6 +103,13 @@ export function getSpotWsKeyForTopic(topic: string): WsKey { 'ticketInfo', ]; + if (apiVersion === 'v3') { + if (privateTopics.includes(topic)) { + return WS_KEY_MAP.spotV3Private; + } + return WS_KEY_MAP.spotV3Public; + } + if (privateTopics.includes(topic)) { return WS_KEY_MAP.spotPrivate; } diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 1e81c01..3f5c92e 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -136,6 +136,17 @@ export class WebsocketClient extends EventEmitter { this.connectPublic(); break; } + case 'spotV3': { + this.restClient = new SpotClientV3( + undefined, + undefined, + !this.isTestnet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } // if (this.isV3()) { // this.restClient = new SpotClientV3( // undefined, @@ -175,59 +186,6 @@ export class WebsocketClient extends EventEmitter { // return this.options.market === 'v3'; // } - /** - * Add topic/topics to WS subscription list - */ - public subscribe(wsTopics: WsTopic[] | WsTopic) { - const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; - topics.forEach((topic) => - this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic) - ); - - // attempt to send subscription topic per websocket - this.wsStore.getKeys().forEach((wsKey: WsKey) => { - // if connected, send subscription request - if ( - this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) - ) { - return this.requestSubscribeTopics(wsKey, topics); - } - - // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect - if ( - !this.wsStore.isConnectionState( - wsKey, - WsConnectionStateEnum.CONNECTING - ) && - !this.wsStore.isConnectionState( - wsKey, - WsConnectionStateEnum.RECONNECTING - ) - ) { - return this.connect(wsKey); - } - }); - } - - /** - * Remove topic/topics from WS subscription list - */ - public unsubscribe(wsTopics: WsTopic[] | WsTopic) { - const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; - topics.forEach((topic) => - this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic) - ); - - this.wsStore.getKeys().forEach((wsKey: WsKey) => { - // unsubscribe request only necessary if active connection exists - if ( - this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) - ) { - this.requestUnsubscribeTopics(wsKey, topics); - } - }); - } - public close(wsKey: WsKey) { this.logger.info('Closing connection', { ...loggerCategory, wsKey }); this.setWsState(wsKey, WsConnectionStateEnum.CLOSING); @@ -263,6 +221,12 @@ export class WebsocketClient extends EventEmitter { this.connect(WS_KEY_MAP.spotPrivate), ]; } + case 'spotV3': { + return [ + this.connect(WS_KEY_MAP.spotV3Public), + this.connect(WS_KEY_MAP.spotV3Private), + ]; + } default: { throw neverGuard(this.options.market, `connectAll(): Unhandled market`); } @@ -280,6 +244,9 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return this.connect(WS_KEY_MAP.spotPublic); } + case 'spotV3': { + return this.connect(WS_KEY_MAP.spotV3Public); + } default: { throw neverGuard( this.options.market, @@ -300,6 +267,9 @@ export class WebsocketClient extends EventEmitter { case 'spot': { return this.connect(WS_KEY_MAP.spotPrivate); } + case 'spotV3': { + return this.connect(WS_KEY_MAP.spotV3Private); + } default: { throw neverGuard( this.options.market, @@ -503,7 +473,7 @@ export class WebsocketClient extends EventEmitter { this.tryWsSend(wsKey, wsMessage); } - private tryWsSend(wsKey: WsKey, wsMessage: string) { + public tryWsSend(wsKey: WsKey, wsMessage: string) { try { this.logger.silly(`Sending upstream ws message: `, { ...loggerCategory, @@ -666,7 +636,13 @@ export class WebsocketClient extends EventEmitter { return WS_BASE_URL_MAP.spot.public[networkKey]; } case WS_KEY_MAP.spotPrivate: { - return WS_BASE_URL_MAP.linear.private[networkKey]; + return WS_BASE_URL_MAP.spot.private[networkKey]; + } + case WS_KEY_MAP.spotV3Public: { + return WS_BASE_URL_MAP.spot.public[networkKey]; + } + case WS_KEY_MAP.spotV3Private: { + return WS_BASE_URL_MAP.spot.private[networkKey]; } case WS_KEY_MAP.inverse: { // private and public are on the same WS connection @@ -691,7 +667,10 @@ export class WebsocketClient extends EventEmitter { return getLinearWsKeyForTopic(topic); } case 'spot': { - return getSpotWsKeyForTopic(topic); + return getSpotWsKeyForTopic(topic, 'v1'); + } + case 'spotV3': { + return getSpotWsKeyForTopic(topic, 'v3'); } default: { throw neverGuard( @@ -708,6 +687,59 @@ export class WebsocketClient extends EventEmitter { ); } + /** + * Add topic/topics to WS subscription list + */ + public subscribe(wsTopics: WsTopic[] | WsTopic) { + const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + topics.forEach((topic) => + this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic) + ); + + // attempt to send subscription topic per websocket + this.wsStore.getKeys().forEach((wsKey: WsKey) => { + // if connected, send subscription request + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { + return this.requestSubscribeTopics(wsKey, topics); + } + + // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect + if ( + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.CONNECTING + ) && + !this.wsStore.isConnectionState( + wsKey, + WsConnectionStateEnum.RECONNECTING + ) + ) { + return this.connect(wsKey); + } + }); + } + + /** + * Remove topic/topics from WS subscription list + */ + public unsubscribe(wsTopics: WsTopic[] | WsTopic) { + const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics]; + topics.forEach((topic) => + this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic) + ); + + this.wsStore.getKeys().forEach((wsKey: WsKey) => { + // unsubscribe request only necessary if active connection exists + if ( + this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED) + ) { + this.requestUnsubscribeTopics(wsKey, topics); + } + }); + } + // TODO: persistance for subbed topics. Look at ftx-api implementation. public subscribePublicSpotTrades(symbol: string, binary?: boolean) { if (!this.isSpot()) { diff --git a/test/inverse/private.ws.test.ts b/test/inverse/ws.private.test.ts similarity index 100% rename from test/inverse/private.ws.test.ts rename to test/inverse/ws.private.test.ts diff --git a/test/inverse/public.ws.test.ts b/test/inverse/ws.public.test.ts similarity index 96% rename from test/inverse/public.ws.test.ts rename to test/inverse/ws.public.test.ts index cb75af7..6d522fc 100644 --- a/test/inverse/public.ws.test.ts +++ b/test/inverse/ws.public.test.ts @@ -27,7 +27,7 @@ describe('Public Inverse Perps Websocket Client', () => { wsClient.closeAll(); }); - it('should open a private ws connection', async () => { + it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); expect(wsOpenPromise).resolves.toMatchObject({ diff --git a/test/linear/private.ws.test.ts b/test/linear/ws.private.test.ts similarity index 97% rename from test/linear/private.ws.test.ts rename to test/linear/ws.private.test.ts index fa197ff..e3b3fe2 100644 --- a/test/linear/private.ws.test.ts +++ b/test/linear/ws.private.test.ts @@ -9,7 +9,7 @@ import { WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; -describe('Private Linear Websocket Client', () => { +describe('Private Linear Perps Websocket Client', () => { const API_KEY = process.env.API_KEY_COM; const API_SECRET = process.env.API_SECRET_COM; diff --git a/test/linear/public.ws.test.ts b/test/linear/ws.public.test.ts similarity index 94% rename from test/linear/public.ws.test.ts rename to test/linear/ws.public.test.ts index e506d5a..cf57156 100644 --- a/test/linear/public.ws.test.ts +++ b/test/linear/ws.public.test.ts @@ -1,5 +1,4 @@ import { - LinearClient, WebsocketClient, WSClientConfigurableOptions, WS_KEY_MAP, @@ -10,7 +9,7 @@ import { WS_OPEN_EVENT_PARTIAL, } from '../ws.util'; -describe('Public Linear Websocket Client', () => { +describe('Public Linear Perps Websocket Client', () => { let wsClient: WebsocketClient; const wsClientOptions: WSClientConfigurableOptions = { @@ -26,7 +25,7 @@ describe('Public Linear Websocket Client', () => { wsClient.closeAll(); }); - it('should open a private ws connection', async () => { + it('should open a public ws connection', async () => { const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); expect(wsOpenPromise).resolves.toMatchObject({ diff --git a/test/spot/ws.private.v1.test.ts b/test/spot/ws.private.v1.test.ts new file mode 100644 index 0000000..8242c61 --- /dev/null +++ b/test/spot/ws.private.v1.test.ts @@ -0,0 +1,58 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + promiseSleep, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Private Spot V1 Websocket Client', () => { + let wsClient: WebsocketClient; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spot', + key: API_KEY, + secret: API_SECRET, + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + // TODO: how to detect if auth failed for the v1 spot ws + it('should open a private ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + // const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + wsClient.connectPrivate(); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotPrivate, + }); + // expect(wsUpdatePromise).resolves.toMatchObject({ + // topic: 'wsTopic', + // data: expect.any(Array), + // }); + await Promise.all([wsOpenPromise]); + // await Promise.all([wsUpdatePromise]); + // await promiseSleep(4000); + }); +}); diff --git a/test/spot/ws.public.v1.test.ts b/test/spot/ws.public.v1.test.ts new file mode 100644 index 0000000..8a9f94a --- /dev/null +++ b/test/spot/ws.public.v1.test.ts @@ -0,0 +1,64 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Spot V1 Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spot', + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPublic(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotPublic, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public orderbook events', async () => { + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const symbol = 'BTCUSDT'; + expect(wsUpdatePromise).resolves.toMatchObject({ + symbol: symbol, + symbolName: symbol, + topic: 'diffDepth', + params: { + realtimeInterval: '24h', + binary: 'false', + }, + data: expect.any(Array), + }); + + wsClient.subscribePublicSpotOrderbook(symbol, 'delta'); + + try { + await wsUpdatePromise; + } catch (e) { + console.error(`Wait for spot v1 orderbook event exception: `, e); + } + }); +}); diff --git a/test/spot/ws.public.v3.test.ts b/test/spot/ws.public.v3.test.ts new file mode 100644 index 0000000..6ae44e9 --- /dev/null +++ b/test/spot/ws.public.v3.test.ts @@ -0,0 +1,72 @@ +import { + WebsocketClient, + WSClientConfigurableOptions, + WS_KEY_MAP, +} from '../../src'; +import { + logAllEvents, + silentLogger, + waitForSocketEvent, + WS_OPEN_EVENT_PARTIAL, +} from '../ws.util'; + +describe('Public Spot V3 Websocket Client', () => { + let wsClient: WebsocketClient; + + const wsClientOptions: WSClientConfigurableOptions = { + market: 'spotV3', + }; + + beforeAll(() => { + wsClient = new WebsocketClient(wsClientOptions, silentLogger); + wsClient.connectPublic(); + // logAllEvents(wsClient); + }); + + afterAll(() => { + wsClient.closeAll(); + }); + + it('should open a public ws connection', async () => { + const wsOpenPromise = waitForSocketEvent(wsClient, 'open'); + + expect(wsOpenPromise).resolves.toMatchObject({ + event: WS_OPEN_EVENT_PARTIAL, + wsKey: WS_KEY_MAP.spotV3Public, + }); + + await Promise.all([wsOpenPromise]); + }); + + it('should subscribe to public orderbook events', async () => { + const wsResponsePromise = waitForSocketEvent(wsClient, 'response'); + const wsUpdatePromise = waitForSocketEvent(wsClient, 'update'); + + const wsTopic = 'orderbook.40.BTCUSDT'; + expect(wsResponsePromise).resolves.toMatchObject({ + request: { + args: [wsTopic], + op: 'subscribe', + }, + success: true, + }); + expect(wsUpdatePromise).resolves.toStrictEqual(''); + + wsClient.subscribe(wsTopic); + + try { + await wsResponsePromise; + } catch (e) { + console.error( + `Wait for "${wsTopic}" subscription response exception: `, + e + ); + } + + try { + await wsUpdatePromise; + } catch (e) { + console.error(`Wait for "${wsTopic}" event exception: `, e); + } + }); +});