From 0e05a8d0ef0da80dbc36b80643636383d3a86af8 Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Wed, 14 Sep 2022 23:55:24 +0100 Subject: [PATCH] tidying ws client --- src/types/websockets.ts | 2 +- src/util/WsStore.ts | 1 + src/util/node-support.ts | 1 + src/util/websocket-util.ts | 10 ++ src/websocket-client.ts | 326 ++++++++++++++++++++++--------------- 5 files changed, 205 insertions(+), 135 deletions(-) diff --git a/src/types/websockets.ts b/src/types/websockets.ts index 963375f..a2a1cdf 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets.ts @@ -1,6 +1,6 @@ import { RestClientOptions } from '../util'; -export type APIMarket = 'inverse' | 'linear' | 'spot' | 'v3'; +export type APIMarket = 'inverse' | 'linear' | 'spot'; //| 'v3'; // Same as inverse futures export type WsPublicInverseTopic = diff --git a/src/util/WsStore.ts b/src/util/WsStore.ts index f27cb56..7c421ce 100644 --- a/src/util/WsStore.ts +++ b/src/util/WsStore.ts @@ -8,6 +8,7 @@ export enum WsConnectionStateEnum { CONNECTED = 2, CLOSING = 3, RECONNECTING = 4, + // ERROR = 5, } /** A "topic" is always a string */ type WsTopic = string; diff --git a/src/util/node-support.ts b/src/util/node-support.ts index bc3b159..bbbf8bd 100644 --- a/src/util/node-support.ts +++ b/src/util/node-support.ts @@ -1,5 +1,6 @@ import { createHmac } from 'crypto'; +/** This is async because the browser version uses a promise (browser-support) */ export async function signMessage( message: string, secret: string diff --git a/src/util/websocket-util.ts b/src/util/websocket-util.ts index 1724dff..0b639af 100644 --- a/src/util/websocket-util.ts +++ b/src/util/websocket-util.ts @@ -6,6 +6,16 @@ export const wsKeyLinearPublic = 'linearPublic'; export const wsKeySpotPrivate = 'spotPrivate'; export const wsKeySpotPublic = 'spotPublic'; +export const WS_KEY_MAP = { + inverse: wsKeyInverse, + linearPrivate: wsKeyLinearPrivate, + linearPublic: wsKeyLinearPublic, + spotPrivate: wsKeySpotPrivate, + spotPublic: wsKeySpotPublic, +}; + +export const PUBLIC_WS_KEYS = [WS_KEY_MAP.linearPublic, WS_KEY_MAP.spotPublic]; + export function getLinearWsKeyForTopic(topic: string): WsKey { const privateLinearTopics = [ 'position', diff --git a/src/websocket-client.ts b/src/websocket-client.ts index 6fc0bdd..759a4a3 100644 --- a/src/websocket-client.ts +++ b/src/websocket-client.ts @@ -31,6 +31,7 @@ import { wsKeySpotPrivate, wsKeySpotPublic, WsConnectionStateEnum, + PUBLIC_WS_KEYS, } from './util'; const inverseEndpoints = { @@ -38,7 +39,35 @@ const inverseEndpoints = { testnet: 'wss://stream-testnet.bybit.com/realtime', }; -const linearEndpoints = { +interface NetworkMapV3 { + livenet: string; + livenet2?: string; + testnet: string; + testnet2?: string; +} + +type NetworkType = 'public' | 'private'; + +function neverGuard(x: never, msg: string): Error { + return new Error(`Unhandled value exception "x", ${msg}`); +} + +const WS_BASE_URL_MAP: Record> = { + 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', + }, + }, +}; + +const linearEndpoints: Record = { private: { livenet: 'wss://stream.bybit.com/realtime_private', livenet2: 'wss://stream.bytick.com/realtime_private', @@ -51,7 +80,7 @@ const linearEndpoints = { }, }; -const spotEndpoints = { +const spotEndpoints: Record = { private: { livenet: 'wss://stream.bybit.com/spot/ws', testnet: 'wss://stream-testnet.bybit.com/spot/ws', @@ -80,8 +109,7 @@ export declare interface WebsocketClient { export class WebsocketClient extends EventEmitter { private logger: typeof DefaultLogger; - /** Purely used */ - private restClient: RESTClient; + private restClient?: RESTClient; private options: WebsocketClientOptions; private wsStore: WsStore; @@ -103,39 +131,64 @@ export class WebsocketClient extends EventEmitter { ...options, }; - if (this.isV3()) { - this.restClient = new SpotClientV3( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); - } else if (this.isLinear()) { - this.restClient = new LinearClient( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); - } else if (this.isSpot()) { - this.restClient = new SpotClient( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); - this.connectPublic(); - } else { - this.restClient = new InverseClient( - undefined, - undefined, - this.isLivenet(), - this.options.restOptions, - this.options.requestOptions - ); + if (this.options.fetchTimeOffsetBeforeAuth) { + this.prepareRESTClient(); + } + } + + /** + * Only used if we fetch exchange time before attempting auth. + * Disabled by default. + * I've removed this for ftx and it's working great, tempted to remove this here + */ + prepareRESTClient(): void { + switch (this.options.market) { + case 'inverse': { + this.restClient = new InverseClient( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + break; + } + case 'linear': { + this.restClient = new LinearClient( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + break; + } + case 'spot': { + this.restClient = new SpotClient( + undefined, + undefined, + this.isLivenet(), + this.options.restOptions, + this.options.requestOptions + ); + this.connectPublic(); + break; + } + // if (this.isV3()) { + // this.restClient = new SpotClientV3( + // undefined, + // undefined, + // this.isLivenet(), + // this.options.restOptions, + // this.options.requestOptions + // ); + // } + default: { + throw neverGuard( + this.options.market, + `prepareRESTClient(): Unhandled market` + ); + } } } @@ -156,9 +209,9 @@ export class WebsocketClient extends EventEmitter { } /** USDC, spot v3, unified margin, account asset */ - public isV3(): boolean { - return this.options.market === 'v3'; - } + // public isV3(): boolean { + // return this.options.market === 'v3'; + // } /** * Add topic/topics to WS subscription list @@ -224,48 +277,63 @@ export class WebsocketClient extends EventEmitter { /** * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ - public connectAll(): Promise[] | undefined { - if (this.isInverse()) { - return [this.connect(wsKeyInverse)]; - } - - if (this.isLinear()) { - return [ - this.connect(wsKeyLinearPublic), - this.connect(wsKeyLinearPrivate), - ]; - } - - if (this.isSpot()) { - return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; + public connectAll(): Promise[] { + switch (this.options.market) { + case 'inverse': { + return [this.connect(wsKeyInverse)]; + } + case 'linear': { + return [ + this.connect(wsKeyLinearPublic), + this.connect(wsKeyLinearPrivate), + ]; + } + case 'spot': { + return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; + } + default: { + throw neverGuard(this.options.market, `connectAll(): Unhandled market`); + } } } - public connectPublic(): Promise | undefined { - if (this.isInverse()) { - return this.connect(wsKeyInverse); - } - - if (this.isLinear()) { - return this.connect(wsKeyLinearPublic); - } - - if (this.isSpot()) { - return this.connect(wsKeySpotPublic); + public connectPublic(): Promise { + switch (this.options.market) { + case 'inverse': { + return this.connect(wsKeyInverse); + } + case 'linear': { + return this.connect(wsKeyLinearPublic); + } + case 'spot': { + return this.connect(wsKeySpotPublic); + } + default: { + throw neverGuard( + this.options.market, + `connectPublic(): Unhandled market` + ); + } } } public connectPrivate(): Promise | undefined { - if (this.isInverse()) { - return this.connect(wsKeyInverse); - } - - if (this.isLinear()) { - return this.connect(wsKeyLinearPrivate); - } - - if (this.isSpot()) { - return this.connect(wsKeySpotPrivate); + switch (this.options.market) { + case 'inverse': { + return this.connect(wsKeyInverse); + } + case 'linear': { + return this.connect(wsKeyLinearPrivate); + } + case 'spot': { + return this.connect(wsKeySpotPrivate); + } + default: { + throw neverGuard( + this.options.market, + `connectPrivate(): Unhandled market` + ); + } } } @@ -336,48 +404,45 @@ export class WebsocketClient extends EventEmitter { private async getAuthParams(wsKey: WsKey): Promise { const { key, secret } = this.options; - if ( - key && - secret && - wsKey !== wsKeyLinearPublic && - wsKey !== wsKeySpotPublic - ) { - this.logger.debug("Getting auth'd request params", { - ...loggerCategory, - wsKey, - }); - - const timeOffset = this.options.fetchTimeOffsetBeforeAuth - ? await this.restClient.fetchTimeOffset() - : 0; - - const signatureExpires = Date.now() + timeOffset + 5000; - - const signature = await signMessage( - 'GET/realtime' + signatureExpires, - secret - ); - - const authParams = { - api_key: this.options.key, - expires: signatureExpires, - signature, - }; - - return '?' + serializeParams(authParams); - } else if (!key || !secret) { - this.logger.warning( - 'Cannot authenticate websocket, either api or private keys missing.', - { ...loggerCategory, wsKey } - ); - } else { + if (PUBLIC_WS_KEYS.includes(wsKey)) { this.logger.debug('Starting public only websocket client.', { ...loggerCategory, wsKey, }); + return ''; } - return ''; + if (!key || !secret) { + this.logger.warning( + 'Cannot authenticate websocket, either api or private keys missing.', + { ...loggerCategory, wsKey } + ); + return ''; + } + + this.logger.debug("Getting auth'd request params", { + ...loggerCategory, + wsKey, + }); + + const timeOffset = this.options.fetchTimeOffsetBeforeAuth + ? (await this.restClient?.fetchTimeOffset()) || 0 + : 0; + + const signatureExpiresAt = Date.now() + timeOffset + 5000; + + const signature = await signMessage( + 'GET/realtime' + signatureExpiresAt, + secret + ); + + const authParams = { + api_key: this.options.key, + expires: signatureExpiresAt, + signature, + }; + + return '?' + serializeParams(authParams); } private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) { @@ -621,38 +686,31 @@ export class WebsocketClient extends EventEmitter { } const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; - // TODO: repetitive - if (this.isLinear() || wsKey.startsWith('linear')) { - if (wsKey === wsKeyLinearPublic) { + + switch (wsKey) { + case wsKeyLinearPublic: { return linearEndpoints.public[networkKey]; } - - if (wsKey === wsKeyLinearPrivate) { + case wsKeyLinearPrivate: { return linearEndpoints.private[networkKey]; } - - this.logger.error('Unhandled linear wsKey: ', { - ...loggerCategory, - wsKey, - }); - return linearEndpoints[networkKey]; - } - - if (this.isSpot() || wsKey.startsWith('spot')) { - if (wsKey === wsKeySpotPublic) { + case wsKeySpotPublic: { return spotEndpoints.public[networkKey]; } - - if (wsKey === wsKeySpotPrivate) { + case wsKeySpotPrivate: { return spotEndpoints.private[networkKey]; } - - this.logger.error('Unhandled spot wsKey: ', { ...loggerCategory, wsKey }); - return spotEndpoints[networkKey]; + case wsKeyInverse: { + return inverseEndpoints[networkKey]; + } + default: { + this.logger.error('getWsUrl(): Unhandled wsKey: ', { + ...loggerCategory, + wsKey, + }); + throw neverGuard(wsKey, `getWsUrl(): Unhandled wsKey`); + } } - - // fallback to inverse - return inverseEndpoints[networkKey]; } private getWsKeyForTopic(topic: string) {