v2.4.0-beta.1. remove circleci, cleaning in ws client

This commit is contained in:
tiagosiebler
2022-09-14 23:12:44 +01:00
parent 04fd7989dc
commit d1ed7971ad
9 changed files with 296 additions and 264 deletions

View File

@@ -1,31 +0,0 @@
version: 2.1
jobs:
test:
docker:
- image: cimg/node:15.1
steps:
- checkout
- restore_cache:
# See the configuration reference documentation for more details on using restore_cache and save_cache steps
# https://circleci.com/docs/2.0/configuration-reference/?section=reference#save_cache
keys:
- node-deps-v1-{{ .Branch }}-{{checksum "package-lock.json"}}
- run:
name: install packages
command: npm ci --ignore-scripts
- save_cache:
key: node-deps-v1-{{ .Branch }}-{{checksum "package-lock.json"}}
paths:
- ~/.npm
- run:
name: Run Build
command: npm run build
- run:
name: Run Tests
command: npm run test
workflows:
integrationtests:
jobs:
- test

View File

@@ -1,6 +1,6 @@
{
"name": "bybit-api",
"version": "2.3.2",
"version": "2.4.0-beta.1",
"description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@@ -1,3 +1,4 @@
export * from './response';
export * from './request';
export * from './shared';
export * from './websockets';

View File

@@ -1,3 +1,14 @@
import { InverseClient } from '../inverse-client';
import { LinearClient } from '../linear-client';
import { SpotClient } from '../spot-client';
import { SpotClientV3 } from '../spot-client-v3';
export type RESTClient =
| InverseClient
| LinearClient
| SpotClient
| SpotClientV3;
export type numberInString = string;
export type OrderSide = 'Buy' | 'Sell';

107
src/types/websockets.ts Normal file
View File

@@ -0,0 +1,107 @@
import { RestClientOptions } from '../util';
export type APIMarket = 'inverse' | 'linear' | 'spot' | 'v3';
// Same as inverse futures
export type WsPublicInverseTopic =
| 'orderBookL2_25'
| 'orderBookL2_200'
| 'trade'
| 'insurance'
| 'instrument_info'
| 'klineV2';
export type WsPublicUSDTPerpTopic =
| 'orderBookL2_25'
| 'orderBookL2_200'
| 'trade'
| 'insurance'
| 'instrument_info'
| 'kline';
export type WsPublicSpotV1Topic =
| 'trade'
| 'realtimes'
| 'kline'
| 'depth'
| 'mergedDepth'
| 'diffDepth';
export type WsPublicSpotV2Topic =
| 'depth'
| 'kline'
| 'trade'
| 'bookTicker'
| 'realtimes';
export type WsPublicTopics =
| WsPublicInverseTopic
| WsPublicUSDTPerpTopic
| WsPublicSpotV1Topic
| WsPublicSpotV2Topic
| string;
// Same as inverse futures
export type WsPrivateInverseTopic =
| 'position'
| 'execution'
| 'order'
| 'stop_order';
export type WsPrivateUSDTPerpTopic =
| 'position'
| 'execution'
| 'order'
| 'stop_order'
| 'wallet';
export type WsPrivateSpotTopic =
| 'outboundAccountInfo'
| 'executionReport'
| 'ticketInfo';
export type WsPrivateTopic =
| WsPrivateInverseTopic
| WsPrivateUSDTPerpTopic
| WsPrivateSpotTopic
| string;
export type WsTopic = WsPublicTopics | WsPrivateTopic;
/** This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets) */
export type WsKey =
| 'inverse'
| 'linearPrivate'
| 'linearPublic'
| 'spotPrivate'
| 'spotPublic';
export interface WSClientConfigurableOptions {
key?: string;
secret?: string;
livenet?: boolean;
/**
* The API group this client should connect to.
*
* For the V3 APIs use `v3` as the market (spot/unified margin/usdc/account asset/copy trading)
*/
market: APIMarket;
pongTimeout?: number;
pingInterval?: number;
reconnectTimeout?: number;
restOptions?: RestClientOptions;
requestOptions?: any;
wsUrl?: string;
/** If true, fetch server time before trying to authenticate (disabled by default) */
fetchTimeOffsetBeforeAuth?: boolean;
}
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
livenet: boolean;
market: APIMarket;
pongTimeout: number;
pingInterval: number;
reconnectTimeout: number;
}

View File

@@ -1,16 +1,35 @@
import WebSocket from 'isomorphic-ws';
import { WsConnectionState } from '../websocket-client';
import { DefaultLogger } from './logger';
export enum WsConnectionStateEnum {
INITIAL = 0,
CONNECTING = 1,
CONNECTED = 2,
CLOSING = 3,
RECONNECTING = 4,
}
/** A "topic" is always a string */
type WsTopic = string;
/**
* A "Set" is used to ensure we only subscribe to a topic once (tracking a list of unique topics we're expected to be connected to)
* TODO: do any WS topics allow parameters? If so, we need a way to track those (see FTX implementation)
*/
type WsTopicList = Set<WsTopic>;
interface WsStoredState {
/** The currently active websocket connection */
ws?: WebSocket;
connectionState?: WsConnectionState;
/** The current lifecycle state of the connection (enum) */
connectionState?: WsConnectionStateEnum;
/** A timer that will send an upstream heartbeat (ping) when it expires */
activePingTimer?: ReturnType<typeof setTimeout> | undefined;
/** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */
activePongTimer?: ReturnType<typeof setTimeout> | undefined;
/**
* All the topics we are expected to be subscribed to (and we automatically resubscribe to if the connection drops)
*/
subscribedTopics: WsTopicList;
}
@@ -23,6 +42,9 @@ export default class WsStore {
this.wsState = {};
}
/** Get WS stored state for key, optionally create if missing */
get(key: string, createIfMissing?: true): WsStoredState;
get(key: string, createIfMissing?: false): WsStoredState | undefined;
get(key: string, createIfMissing?: boolean): WsStoredState | undefined {
if (this.wsState[key]) {
return this.wsState[key];
@@ -46,7 +68,7 @@ export default class WsStore {
}
this.wsState[key] = {
subscribedTopics: new Set(),
connectionState: WsConnectionState.READY_STATE_INITIAL,
connectionState: WsConnectionStateEnum.INITIAL,
};
return this.get(key);
}
@@ -94,22 +116,22 @@ export default class WsStore {
);
}
getConnectionState(key: string): WsConnectionState {
getConnectionState(key: string): WsConnectionStateEnum {
return this.get(key, true)!.connectionState!;
}
setConnectionState(key: string, state: WsConnectionState) {
setConnectionState(key: string, state: WsConnectionStateEnum) {
this.get(key, true)!.connectionState = state;
}
isConnectionState(key: string, state: WsConnectionState): boolean {
isConnectionState(key: string, state: WsConnectionStateEnum): boolean {
return this.getConnectionState(key) === state;
}
/* subscribed topics */
getTopics(key: string): WsTopicList {
return this.get(key, true)!.subscribedTopics;
return this.get(key, true).subscribedTopics;
}
getTopicsByKey(): Record<string, WsTopicList> {

View File

@@ -2,3 +2,4 @@ export * from './BaseRestClient';
export * from './requestUtils';
export * from './WsStore';
export * from './logger';
export * from './websocket-util';

View File

@@ -0,0 +1,40 @@
import { WsKey } from '../types';
export const wsKeyInverse = 'inverse';
export const wsKeyLinearPrivate = 'linearPrivate';
export const wsKeyLinearPublic = 'linearPublic';
export const wsKeySpotPrivate = 'spotPrivate';
export const wsKeySpotPublic = 'spotPublic';
export function getLinearWsKeyForTopic(topic: string): WsKey {
const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'wallet',
];
if (privateLinearTopics.includes(topic)) {
return wsKeyLinearPrivate;
}
return wsKeyLinearPublic;
}
export function getSpotWsKeyForTopic(topic: string): WsKey {
const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
];
if (privateLinearTopics.includes(topic)) {
return wsKeySpotPrivate;
}
return wsKeySpotPublic;
}

View File

@@ -3,17 +3,35 @@ import WebSocket from 'isomorphic-ws';
import { InverseClient } from './inverse-client';
import { LinearClient } from './linear-client';
import { DefaultLogger } from './util/logger';
import { SpotClientV3 } from './spot-client-v3';
import { SpotClient } from './spot-client';
import { KlineInterval } from './types/shared';
import { DefaultLogger } from './util/logger';
import {
APIMarket,
KlineInterval,
RESTClient,
WebsocketClientOptions,
WSClientConfigurableOptions,
WsKey,
WsTopic,
} from './types';
import { signMessage } from './util/node-support';
import WsStore from './util/WsStore';
import {
serializeParams,
isWsPong,
RestClientOptions,
} from './util/requestUtils';
import WsStore from './util/WsStore';
getLinearWsKeyForTopic,
getSpotWsKeyForTopic,
wsKeyInverse,
wsKeyLinearPrivate,
wsKeyLinearPublic,
wsKeySpotPrivate,
wsKeySpotPublic,
WsConnectionStateEnum,
} from './util';
const inverseEndpoints = {
livenet: 'wss://stream.bybit.com/realtime',
@@ -48,169 +66,6 @@ const spotEndpoints = {
const loggerCategory = { category: 'bybit-ws' };
const READY_STATE_INITIAL = 0;
const READY_STATE_CONNECTING = 1;
const READY_STATE_CONNECTED = 2;
const READY_STATE_CLOSING = 3;
const READY_STATE_RECONNECTING = 4;
export enum WsConnectionState {
READY_STATE_INITIAL,
READY_STATE_CONNECTING,
READY_STATE_CONNECTED,
READY_STATE_CLOSING,
READY_STATE_RECONNECTING,
}
export type APIMarket = 'inverse' | 'linear' | 'spot';
// Same as inverse futures
export type WsPublicInverseTopic =
| 'orderBookL2_25'
| 'orderBookL2_200'
| 'trade'
| 'insurance'
| 'instrument_info'
| 'klineV2';
export type WsPublicUSDTPerpTopic =
| 'orderBookL2_25'
| 'orderBookL2_200'
| 'trade'
| 'insurance'
| 'instrument_info'
| 'kline';
export type WsPublicSpotV1Topic =
| 'trade'
| 'realtimes'
| 'kline'
| 'depth'
| 'mergedDepth'
| 'diffDepth';
export type WsPublicSpotV2Topic =
| 'depth'
| 'kline'
| 'trade'
| 'bookTicker'
| 'realtimes';
export type WsPublicTopics =
| WsPublicInverseTopic
| WsPublicUSDTPerpTopic
| WsPublicSpotV1Topic
| WsPublicSpotV2Topic
| string;
// Same as inverse futures
export type WsPrivateInverseTopic =
| 'position'
| 'execution'
| 'order'
| 'stop_order';
export type WsPrivateUSDTPerpTopic =
| 'position'
| 'execution'
| 'order'
| 'stop_order'
| 'wallet';
export type WsPrivateSpotTopic =
| 'outboundAccountInfo'
| 'executionReport'
| 'ticketInfo';
export type WsPrivateTopic =
| WsPrivateInverseTopic
| WsPrivateUSDTPerpTopic
| WsPrivateSpotTopic
| string;
export type WsTopic = WsPublicTopics | WsPrivateTopic;
export interface WSClientConfigurableOptions {
key?: string;
secret?: string;
livenet?: boolean;
// defaults to inverse.
/**
* @deprecated Use the property { market: 'linear' } instead
*/
linear?: boolean;
market?: APIMarket;
pongTimeout?: number;
pingInterval?: number;
reconnectTimeout?: number;
restOptions?: RestClientOptions;
requestOptions?: any;
wsUrl?: string;
/** If true, fetch server time before trying to authenticate (disabled by default) */
fetchTimeOffsetBeforeAuth?: boolean;
}
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
livenet: boolean;
/**
* @deprecated Use the property { market: 'linear' } instead
*/
linear?: boolean;
market?: APIMarket;
pongTimeout: number;
pingInterval: number;
reconnectTimeout: number;
}
export const wsKeyInverse = 'inverse';
export const wsKeyLinearPrivate = 'linearPrivate';
export const wsKeyLinearPublic = 'linearPublic';
export const wsKeySpotPrivate = 'spotPrivate';
export const wsKeySpotPublic = 'spotPublic';
// This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets)
export type WsKey =
| 'inverse'
| 'linearPrivate'
| 'linearPublic'
| 'spotPrivate'
| 'spotPublic';
const getLinearWsKeyForTopic = (topic: string): WsKey => {
const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'wallet',
];
if (privateLinearTopics.includes(topic)) {
return wsKeyLinearPrivate;
}
return wsKeyLinearPublic;
};
const getSpotWsKeyForTopic = (topic: string): WsKey => {
const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
];
if (privateLinearTopics.includes(topic)) {
return wsKeySpotPrivate;
}
return wsKeySpotPublic;
};
export declare interface WebsocketClient {
on(
event: 'open' | 'reconnected',
@@ -223,16 +78,10 @@ export declare interface WebsocketClient {
on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this;
}
function resolveMarket(options: WSClientConfigurableOptions): APIMarket {
if (options.linear) {
return 'linear';
}
return 'inverse';
}
export class WebsocketClient extends EventEmitter {
private logger: typeof DefaultLogger;
private restClient: InverseClient | LinearClient | SpotClient;
/** Purely used */
private restClient: RESTClient;
private options: WebsocketClientOptions;
private wsStore: WsStore;
@@ -254,11 +103,15 @@ export class WebsocketClient extends EventEmitter {
...options,
};
if (!this.options.market) {
this.options.market = resolveMarket(this.options);
}
if (this.isLinear()) {
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,
@@ -299,7 +152,12 @@ export class WebsocketClient extends EventEmitter {
}
public isInverse(): boolean {
return !this.isLinear() && !this.isSpot();
return this.options.market === 'inverse';
}
/** USDC, spot v3, unified margin, account asset */
public isV3(): boolean {
return this.options.market === 'v3';
}
/**
@@ -314,14 +172,22 @@ export class WebsocketClient extends EventEmitter {
// attempt to send subscription topic per websocket
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
// if connected, send subscription request
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) {
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, READY_STATE_CONNECTING) &&
!this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
!this.wsStore.isConnectionState(
wsKey,
WsConnectionStateEnum.CONNECTING
) &&
!this.wsStore.isConnectionState(
wsKey,
WsConnectionStateEnum.RECONNECTING
)
) {
return this.connect(wsKey);
}
@@ -339,7 +205,9 @@ export class WebsocketClient extends EventEmitter {
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
// unsubscribe request only necessary if active connection exists
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) {
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
) {
this.requestUnsubscribeTopics(wsKey, topics);
}
});
@@ -347,14 +215,14 @@ export class WebsocketClient extends EventEmitter {
public close(wsKey: WsKey) {
this.logger.info('Closing connection', { ...loggerCategory, wsKey });
this.setWsState(wsKey, READY_STATE_CLOSING);
this.setWsState(wsKey, WsConnectionStateEnum.CLOSING);
this.clearTimers(wsKey);
this.getWs(wsKey)?.close();
}
/**
* Request connection of all dependent 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 {
if (this.isInverse()) {
@@ -411,7 +279,9 @@ export class WebsocketClient extends EventEmitter {
return this.wsStore.getWs(wsKey);
}
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
) {
this.logger.error(
'Refused to connect to ws, connection attempt already active',
{ ...loggerCategory, wsKey }
@@ -421,9 +291,9 @@ export class WebsocketClient extends EventEmitter {
if (
!this.wsStore.getConnectionState(wsKey) ||
this.wsStore.isConnectionState(wsKey, READY_STATE_INITIAL)
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.INITIAL)
) {
this.setWsState(wsKey, READY_STATE_CONNECTING);
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTING);
}
const authParams = await this.getAuthParams(wsKey);
@@ -481,19 +351,23 @@ export class WebsocketClient extends EventEmitter {
? await this.restClient.fetchTimeOffset()
: 0;
const params: any = {
api_key: this.options.key,
expires: Date.now() + timeOffset + 5000,
};
const signatureExpires = Date.now() + timeOffset + 5000;
params.signature = await signMessage(
'GET/realtime' + params.expires,
const signature = await signMessage(
'GET/realtime' + signatureExpires,
secret
);
return '?' + serializeParams(params);
const authParams = {
api_key: this.options.key,
expires: signatureExpires,
signature,
};
return '?' + serializeParams(authParams);
} else if (!key || !secret) {
this.logger.warning(
'Connot authenticate websocket, either api or private keys missing.',
'Cannot authenticate websocket, either api or private keys missing.',
{ ...loggerCategory, wsKey }
);
} else {
@@ -508,8 +382,11 @@ export class WebsocketClient extends EventEmitter {
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
this.clearTimers(wsKey);
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CONNECTING) {
this.setWsState(wsKey, READY_STATE_RECONNECTING);
if (
this.wsStore.getConnectionState(wsKey) !==
WsConnectionStateEnum.CONNECTING
) {
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
}
setTimeout(() => {
@@ -635,7 +512,9 @@ export class WebsocketClient extends EventEmitter {
}
private onWsOpen(event, wsKey: WsKey) {
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
) {
this.logger.info('Websocket connected', {
...loggerCategory,
wsKey,
@@ -645,13 +524,13 @@ export class WebsocketClient extends EventEmitter {
});
this.emit('open', { wsKey, event });
} else if (
this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING)
) {
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
this.emit('reconnected', { wsKey, event });
}
this.setWsState(wsKey, READY_STATE_CONNECTED);
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
// TODO: persistence not working yet for spot topics
if (wsKey !== 'spotPublic' && wsKey !== 'spotPrivate') {
@@ -670,18 +549,20 @@ export class WebsocketClient extends EventEmitter {
this.clearPongTimer(wsKey);
const msg = JSON.parse((event && event.data) || event);
if ('success' in msg || msg?.pong) {
this.onWsMessageResponse(msg, wsKey);
} else if (msg.topic) {
this.onWsMessageUpdate(msg);
} else {
this.logger.warning('Got unhandled ws message', {
...loggerCategory,
message: msg,
event,
wsKey,
});
if (msg['success'] || msg?.pong) {
return this.onWsMessageResponse(msg, wsKey);
}
if (msg.topic) {
return this.emit('update', msg);
}
this.logger.warning('Got unhandled ws message', {
...loggerCategory,
message: msg,
event,
wsKey,
});
} catch (e) {
this.logger.error('Failed to parse ws event message', {
...loggerCategory,
@@ -694,7 +575,9 @@ export class WebsocketClient extends EventEmitter {
private onWsError(error: any, wsKey: WsKey) {
this.parseWsError('Websocket error', error, wsKey);
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTED)) {
if (
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
) {
this.emit('error', error);
}
}
@@ -705,11 +588,13 @@ export class WebsocketClient extends EventEmitter {
wsKey,
});
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) {
if (
this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING
) {
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
this.emit('reconnect', { wsKey });
} else {
this.setWsState(wsKey, READY_STATE_INITIAL);
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
this.emit('close', { wsKey });
}
}
@@ -722,15 +607,11 @@ export class WebsocketClient extends EventEmitter {
}
}
private onWsMessageUpdate(message: any) {
this.emit('update', message);
}
private getWs(wsKey: string) {
return this.wsStore.getWs(wsKey);
}
private setWsState(wsKey: WsKey, state: WsConnectionState) {
private setWsState(wsKey: WsKey, state: WsConnectionStateEnum) {
this.wsStore.setConnectionState(wsKey, state);
}
@@ -740,7 +621,7 @@ export class WebsocketClient extends EventEmitter {
}
const networkKey = this.isLivenet() ? 'livenet' : 'testnet';
// TODO: reptitive
// TODO: repetitive
if (this.isLinear() || wsKey.startsWith('linear')) {
if (wsKey === wsKeyLinearPublic) {
return linearEndpoints.public[networkKey];