tidying ws client

This commit is contained in:
tiagosiebler
2022-09-14 23:55:24 +01:00
parent d1ed7971ad
commit 0e05a8d0ef
5 changed files with 205 additions and 135 deletions

View File

@@ -1,6 +1,6 @@
import { RestClientOptions } from '../util'; import { RestClientOptions } from '../util';
export type APIMarket = 'inverse' | 'linear' | 'spot' | 'v3'; export type APIMarket = 'inverse' | 'linear' | 'spot'; //| 'v3';
// Same as inverse futures // Same as inverse futures
export type WsPublicInverseTopic = export type WsPublicInverseTopic =

View File

@@ -8,6 +8,7 @@ export enum WsConnectionStateEnum {
CONNECTED = 2, CONNECTED = 2,
CLOSING = 3, CLOSING = 3,
RECONNECTING = 4, RECONNECTING = 4,
// ERROR = 5,
} }
/** A "topic" is always a string */ /** A "topic" is always a string */
type WsTopic = string; type WsTopic = string;

View File

@@ -1,5 +1,6 @@
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
/** This is async because the browser version uses a promise (browser-support) */
export async function signMessage( export async function signMessage(
message: string, message: string,
secret: string secret: string

View File

@@ -6,6 +6,16 @@ export const wsKeyLinearPublic = 'linearPublic';
export const wsKeySpotPrivate = 'spotPrivate'; export const wsKeySpotPrivate = 'spotPrivate';
export const wsKeySpotPublic = 'spotPublic'; 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 { export function getLinearWsKeyForTopic(topic: string): WsKey {
const privateLinearTopics = [ const privateLinearTopics = [
'position', 'position',

View File

@@ -31,6 +31,7 @@ import {
wsKeySpotPrivate, wsKeySpotPrivate,
wsKeySpotPublic, wsKeySpotPublic,
WsConnectionStateEnum, WsConnectionStateEnum,
PUBLIC_WS_KEYS,
} from './util'; } from './util';
const inverseEndpoints = { const inverseEndpoints = {
@@ -38,7 +39,35 @@ const inverseEndpoints = {
testnet: 'wss://stream-testnet.bybit.com/realtime', 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<string, Record<NetworkType, NetworkMapV3>> = {
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<NetworkType, NetworkMapV3> = {
private: { private: {
livenet: 'wss://stream.bybit.com/realtime_private', livenet: 'wss://stream.bybit.com/realtime_private',
livenet2: 'wss://stream.bytick.com/realtime_private', livenet2: 'wss://stream.bytick.com/realtime_private',
@@ -51,7 +80,7 @@ const linearEndpoints = {
}, },
}; };
const spotEndpoints = { const spotEndpoints: Record<NetworkType, NetworkMapV3> = {
private: { private: {
livenet: 'wss://stream.bybit.com/spot/ws', livenet: 'wss://stream.bybit.com/spot/ws',
testnet: 'wss://stream-testnet.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 { export class WebsocketClient extends EventEmitter {
private logger: typeof DefaultLogger; private logger: typeof DefaultLogger;
/** Purely used */ private restClient?: RESTClient;
private restClient: RESTClient;
private options: WebsocketClientOptions; private options: WebsocketClientOptions;
private wsStore: WsStore; private wsStore: WsStore;
@@ -103,15 +131,29 @@ export class WebsocketClient extends EventEmitter {
...options, ...options,
}; };
if (this.isV3()) { if (this.options.fetchTimeOffsetBeforeAuth) {
this.restClient = new SpotClientV3( 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,
undefined, undefined,
this.isLivenet(), this.isLivenet(),
this.options.restOptions, this.options.restOptions,
this.options.requestOptions this.options.requestOptions
); );
} else if (this.isLinear()) { break;
}
case 'linear': {
this.restClient = new LinearClient( this.restClient = new LinearClient(
undefined, undefined,
undefined, undefined,
@@ -119,7 +161,9 @@ export class WebsocketClient extends EventEmitter {
this.options.restOptions, this.options.restOptions,
this.options.requestOptions this.options.requestOptions
); );
} else if (this.isSpot()) { break;
}
case 'spot': {
this.restClient = new SpotClient( this.restClient = new SpotClient(
undefined, undefined,
undefined, undefined,
@@ -128,16 +172,25 @@ export class WebsocketClient extends EventEmitter {
this.options.requestOptions this.options.requestOptions
); );
this.connectPublic(); this.connectPublic();
} else { break;
this.restClient = new InverseClient( }
undefined, // if (this.isV3()) {
undefined, // this.restClient = new SpotClientV3(
this.isLivenet(), // undefined,
this.options.restOptions, // undefined,
this.options.requestOptions // this.isLivenet(),
// this.options.restOptions,
// this.options.requestOptions
// );
// }
default: {
throw neverGuard(
this.options.market,
`prepareRESTClient(): Unhandled market`
); );
} }
} }
}
public isLivenet(): boolean { public isLivenet(): boolean {
return this.options.livenet === true; return this.options.livenet === true;
@@ -156,9 +209,9 @@ export class WebsocketClient extends EventEmitter {
} }
/** USDC, spot v3, unified margin, account asset */ /** USDC, spot v3, unified margin, account asset */
public isV3(): boolean { // public isV3(): boolean {
return this.options.market === 'v3'; // return this.options.market === 'v3';
} // }
/** /**
* Add topic/topics to WS subscription list * Add topic/topics to WS subscription list
@@ -224,49 +277,64 @@ export class WebsocketClient extends EventEmitter {
/** /**
* Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
*/ */
public connectAll(): Promise<WebSocket | undefined>[] | undefined { public connectAll(): Promise<WebSocket | undefined>[] {
if (this.isInverse()) { switch (this.options.market) {
case 'inverse': {
return [this.connect(wsKeyInverse)]; return [this.connect(wsKeyInverse)];
} }
case 'linear': {
if (this.isLinear()) {
return [ return [
this.connect(wsKeyLinearPublic), this.connect(wsKeyLinearPublic),
this.connect(wsKeyLinearPrivate), this.connect(wsKeyLinearPrivate),
]; ];
} }
case 'spot': {
if (this.isSpot()) {
return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)]; return [this.connect(wsKeySpotPublic), this.connect(wsKeySpotPrivate)];
} }
default: {
throw neverGuard(this.options.market, `connectAll(): Unhandled market`);
}
}
} }
public connectPublic(): Promise<WebSocket | undefined> | undefined { public connectPublic(): Promise<WebSocket | undefined> {
if (this.isInverse()) { switch (this.options.market) {
case 'inverse': {
return this.connect(wsKeyInverse); return this.connect(wsKeyInverse);
} }
case 'linear': {
if (this.isLinear()) {
return this.connect(wsKeyLinearPublic); return this.connect(wsKeyLinearPublic);
} }
case 'spot': {
if (this.isSpot()) {
return this.connect(wsKeySpotPublic); return this.connect(wsKeySpotPublic);
} }
default: {
throw neverGuard(
this.options.market,
`connectPublic(): Unhandled market`
);
}
}
} }
public connectPrivate(): Promise<WebSocket | undefined> | undefined { public connectPrivate(): Promise<WebSocket | undefined> | undefined {
if (this.isInverse()) { switch (this.options.market) {
case 'inverse': {
return this.connect(wsKeyInverse); return this.connect(wsKeyInverse);
} }
case 'linear': {
if (this.isLinear()) {
return this.connect(wsKeyLinearPrivate); return this.connect(wsKeyLinearPrivate);
} }
case 'spot': {
if (this.isSpot()) {
return this.connect(wsKeySpotPrivate); return this.connect(wsKeySpotPrivate);
} }
default: {
throw neverGuard(
this.options.market,
`connectPrivate(): Unhandled market`
);
}
}
} }
private async connect(wsKey: WsKey): Promise<WebSocket | undefined> { private async connect(wsKey: WsKey): Promise<WebSocket | undefined> {
@@ -336,48 +404,45 @@ export class WebsocketClient extends EventEmitter {
private async getAuthParams(wsKey: WsKey): Promise<string> { private async getAuthParams(wsKey: WsKey): Promise<string> {
const { key, secret } = this.options; const { key, secret } = this.options;
if ( if (PUBLIC_WS_KEYS.includes(wsKey)) {
key && this.logger.debug('Starting public only websocket client.', {
secret && ...loggerCategory,
wsKey !== wsKeyLinearPublic && wsKey,
wsKey !== wsKeySpotPublic });
) { 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", { this.logger.debug("Getting auth'd request params", {
...loggerCategory, ...loggerCategory,
wsKey, wsKey,
}); });
const timeOffset = this.options.fetchTimeOffsetBeforeAuth const timeOffset = this.options.fetchTimeOffsetBeforeAuth
? await this.restClient.fetchTimeOffset() ? (await this.restClient?.fetchTimeOffset()) || 0
: 0; : 0;
const signatureExpires = Date.now() + timeOffset + 5000; const signatureExpiresAt = Date.now() + timeOffset + 5000;
const signature = await signMessage( const signature = await signMessage(
'GET/realtime' + signatureExpires, 'GET/realtime' + signatureExpiresAt,
secret secret
); );
const authParams = { const authParams = {
api_key: this.options.key, api_key: this.options.key,
expires: signatureExpires, expires: signatureExpiresAt,
signature, signature,
}; };
return '?' + serializeParams(authParams); return '?' + serializeParams(authParams);
} else if (!key || !secret) {
this.logger.warning(
'Cannot authenticate websocket, either api or private keys missing.',
{ ...loggerCategory, wsKey }
);
} else {
this.logger.debug('Starting public only websocket client.', {
...loggerCategory,
wsKey,
});
}
return '';
} }
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) { private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
@@ -621,38 +686,31 @@ export class WebsocketClient extends EventEmitter {
} }
const networkKey = this.isLivenet() ? 'livenet' : 'testnet'; const networkKey = this.isLivenet() ? 'livenet' : 'testnet';
// TODO: repetitive
if (this.isLinear() || wsKey.startsWith('linear')) { switch (wsKey) {
if (wsKey === wsKeyLinearPublic) { case wsKeyLinearPublic: {
return linearEndpoints.public[networkKey]; return linearEndpoints.public[networkKey];
} }
case wsKeyLinearPrivate: {
if (wsKey === wsKeyLinearPrivate) {
return linearEndpoints.private[networkKey]; return linearEndpoints.private[networkKey];
} }
case wsKeySpotPublic: {
this.logger.error('Unhandled linear wsKey: ', { return spotEndpoints.public[networkKey];
}
case wsKeySpotPrivate: {
return spotEndpoints.private[networkKey];
}
case wsKeyInverse: {
return inverseEndpoints[networkKey];
}
default: {
this.logger.error('getWsUrl(): Unhandled wsKey: ', {
...loggerCategory, ...loggerCategory,
wsKey, wsKey,
}); });
return linearEndpoints[networkKey]; throw neverGuard(wsKey, `getWsUrl(): Unhandled wsKey`);
} }
if (this.isSpot() || wsKey.startsWith('spot')) {
if (wsKey === wsKeySpotPublic) {
return spotEndpoints.public[networkKey];
} }
if (wsKey === wsKeySpotPrivate) {
return spotEndpoints.private[networkKey];
}
this.logger.error('Unhandled spot wsKey: ', { ...loggerCategory, wsKey });
return spotEndpoints[networkKey];
}
// fallback to inverse
return inverseEndpoints[networkKey];
} }
private getWsKeyForTopic(topic: string) { private getWsKeyForTopic(topic: string) {