Merge pull request #1 from tiagosiebler/wsCleaning

Cleaning on the WS and REST clients
nice work ;)
This commit is contained in:
CryptoCompiler
2021-01-30 18:43:41 +00:00
committed by GitHub
3 changed files with 331 additions and 254 deletions

View File

@@ -22,7 +22,7 @@ export class LinearClient extends SharedEndpoints {
livenet?: boolean, livenet?: boolean,
restInverseOptions:RestClientInverseOptions = {}, // TODO: Rename this type to be more general. restInverseOptions:RestClientInverseOptions = {}, // TODO: Rename this type to be more general.
requestOptions: AxiosRequestConfig = {} requestOptions: AxiosRequestConfig = {}
) { ) {
super() super()
this.requestWrapper = new RequestWrapper( this.requestWrapper = new RequestWrapper(
key, key,
@@ -32,10 +32,10 @@ export class LinearClient extends SharedEndpoints {
requestOptions requestOptions
); );
return this; return this;
} }
//------------Market Data Endpoints------------> //------------Market Data Endpoints------------>
getKline(params: { getKline(params: {
symbol: string; symbol: string;
interval: string; interval: string;
@@ -44,7 +44,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('/public/linear/kline', params); return this.requestWrapper.get('/public/linear/kline', params);
} }
/** /**
* @deprecated use getTrades() instead * @deprecated use getTrades() instead
*/ */
@@ -63,13 +63,13 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('public/linear/recent-trading-records', params); return this.requestWrapper.get('public/linear/recent-trading-records', params);
} }
getLastFundingRate(params: { getLastFundingRate(params: {
symbol: string; symbol: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('public/linear/funding/prev-funding-rate', params); return this.requestWrapper.get('public/linear/funding/prev-funding-rate', params);
} }
getMarkPriceKline(params: { getMarkPriceKline(params: {
symbol: string; symbol: string;
interval: string; interval: string;
@@ -78,7 +78,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('public/linear/mark-price-kline', params); return this.requestWrapper.get('public/linear/mark-price-kline', params);
} }
getIndexPriceKline(params: { getIndexPriceKline(params: {
symbol: string; symbol: string;
interval: string; interval: string;
@@ -87,7 +87,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('public/linear/index-price-kline', params); return this.requestWrapper.get('public/linear/index-price-kline', params);
} }
getPremiumIndexKline(params: { getPremiumIndexKline(params: {
symbol: string; symbol: string;
interval: string; interval: string;
@@ -96,12 +96,12 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('public/linear/premium-index-kline', params); return this.requestWrapper.get('public/linear/premium-index-kline', params);
} }
//------------Account Data Endpoints------------> //------------Account Data Endpoints------------>
//Active Orders //Active Orders
placeActiveOrder(orderRequest: { placeActiveOrder(params: {
side: string; side: string;
symbol: string; symbol: string;
order_type: string; order_type: string;
@@ -116,9 +116,9 @@ export class LinearClient extends SharedEndpoints {
close_on_trigger?: boolean; close_on_trigger?: boolean;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/create', orderRequest); return this.requestWrapper.post('private/linear/order/create', params);
} }
getActiveOrderList(params: { getActiveOrderList(params: {
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
@@ -127,11 +127,11 @@ export class LinearClient extends SharedEndpoints {
page?: number; page?: number;
limit?: number; limit?: number;
order_status?: string; order_status?: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/order/list', params); return this.requestWrapper.get('private/linear/order/list', params);
} }
cancelActiveOrder(params: { cancelActiveOrder(params: {
symbol: string; symbol: string;
order_id?: string; order_id?: string;
@@ -139,13 +139,13 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/cancel', params); return this.requestWrapper.post('private/linear/order/cancel', params);
} }
cancelAllActiveOrders(params: { cancelAllActiveOrders(params: {
symbol: string; symbol: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/cancel-all', params); return this.requestWrapper.post('private/linear/order/cancel-all', params);
} }
replaceActiveOrder(params: { replaceActiveOrder(params: {
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
@@ -159,7 +159,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/replace', params); return this.requestWrapper.post('private/linear/order/replace', params);
} }
queryActiveOrder(params: { queryActiveOrder(params: {
order_id?: string; order_id?: string;
order_link_id?: string; order_link_id?: string;
@@ -167,9 +167,9 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/order/search', params); return this.requestWrapper.get('private/linear/order/search', params);
} }
//Conditional Orders //Conditional Orders
placeConditionalOrder(params: { placeConditionalOrder(params: {
side: string; side: string;
symbol: string; symbol: string;
@@ -190,7 +190,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/stop-order/create', params); return this.requestWrapper.post('private/linear/stop-order/create', params);
} }
getConditionalOrder(params: { getConditionalOrder(params: {
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
@@ -202,7 +202,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/stop-order/list', params); return this.requestWrapper.get('private/linear/stop-order/list', params);
} }
cancelConditionalOrder(params: { cancelConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
@@ -210,13 +210,13 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/stop-order/cancel', params); return this.requestWrapper.post('private/linear/stop-order/cancel', params);
} }
cancelAllConditionalOrders(params: { cancelAllConditionalOrders(params: {
symbol: string; symbol: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/stop-order/cancel-all', params); return this.requestWrapper.post('private/linear/stop-order/cancel-all', params);
} }
replaceConditionalOrder(params: { replaceConditionalOrder(params: {
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
@@ -231,23 +231,23 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/stop-order/replace', params); return this.requestWrapper.post('private/linear/stop-order/replace', params);
} }
queryConditionalOrder(params: { queryConditionalOrder(params: {
symbol: string; symbol: string;
stop_order_id?: string; stop_order_id?: string;
order_link_id?: string; order_link_id?: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/stop-order/search', params); return this.requestWrapper.get('private/linear/stop-order/search', params);
} }
//Position //Position
getPosition(params?: { getPosition(params?: {
symbol?: string; symbol?: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/position/list', params); return this.requestWrapper.get('private/linear/position/list', params);
} }
setAutoAddMargin(params?: { setAutoAddMargin(params?: {
symbol: string; symbol: string;
side: string; side: string;
@@ -255,7 +255,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/position/set-auto-add-margin', params); return this.requestWrapper.post('private/linear/position/set-auto-add-margin', params);
} }
setMarginSwitch(params?: { setMarginSwitch(params?: {
symbol: string; symbol: string;
is_isolated: boolean; is_isolated: boolean;
@@ -264,14 +264,14 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/position/switch-isolated', params); return this.requestWrapper.post('private/linear/position/switch-isolated', params);
} }
setSwitchMode(params?: { setSwitchMode(params?: {
symbol: string; symbol: string;
tp_sl_mode: string; tp_sl_mode: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/tpsl/switch-mode', params); return this.requestWrapper.post('private/linear/tpsl/switch-mode', params);
} }
setAddReduceMargin(params?: { setAddReduceMargin(params?: {
symbol: string; symbol: string;
side: string; side: string;
@@ -279,7 +279,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/position/add-margin', params); return this.requestWrapper.post('private/linear/position/add-margin', params);
} }
setUserLeverage(params: { setUserLeverage(params: {
symbol: string; symbol: string;
buy_leverage: number; buy_leverage: number;
@@ -287,7 +287,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/position/set-leverage', params); return this.requestWrapper.post('private/linear/position/set-leverage', params);
} }
setTradingStop(params: { setTradingStop(params: {
symbol: string; symbol: string;
side: string; side: string;
@@ -301,7 +301,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.post('private/linear/position/trading-stop', params); return this.requestWrapper.post('private/linear/position/trading-stop', params);
} }
getTradeRecords(params: { getTradeRecords(params: {
symbol: string; symbol: string;
start_time?: number; start_time?: number;
@@ -312,7 +312,7 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/trade/execution/list', params); return this.requestWrapper.get('private/linear/trade/execution/list', params);
} }
getClosedPnl(params: { getClosedPnl(params: {
symbol: string; symbol: string;
start_time?: number; start_time?: number;
@@ -323,27 +323,27 @@ export class LinearClient extends SharedEndpoints {
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/tpsl/switch-mode', params); return this.requestWrapper.get('private/linear/tpsl/switch-mode', params);
} }
//Risk Limit //Risk Limit
getRiskLimitList(params: { getRiskLimitList(params: {
symbol: string; symbol: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('public/linear/risk-limit'); return this.requestWrapper.get('public/linear/risk-limit');
} }
//Funding //Funding
getPredictedFundingFee(params: { getPredictedFundingFee(params: {
symbol: string; symbol: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/funding/predicted-funding'); return this.requestWrapper.get('private/linear/funding/predicted-funding');
} }
getLastFundingFee(params: { getLastFundingFee(params: {
symbol: string; symbol: string;
}): GenericAPIResponse { }): GenericAPIResponse {
return this.requestWrapper.get('private/linear/funding/prev-funding'); return this.requestWrapper.get('private/linear/funding/prev-funding');
} }
} }

View File

@@ -1,121 +1,121 @@
//type Constructor = new (...args: any[]) => {};
import { GenericAPIResponse } from './util/requestUtils'; import { GenericAPIResponse } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper'; import RequestWrapper from './util/requestWrapper';
export default class SharedEndpoints { export default class SharedEndpoints {
protected requestWrapper: RequestWrapper; // XXX Is there a way to say that Base has to provide this? // TODO: Is there a way to say that Base has to provide this?
protected requestWrapper: RequestWrapper;
//------------Market Data Endpoints------------>
getOrderBook(params: {
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/orderBook/L2', params);
}
getTickers(params?: { //------------Market Data Endpoints------------>
symbol?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/tickers', params);
}
getSymbols(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/symbols');
}
getLiquidations(params: { getOrderBook(params: {
symbol: string; symbol: string;
from?: number; }): GenericAPIResponse {
limit?: number; return this.requestWrapper.get('v2/public/orderBook/L2', params);
start_time?: number; }
end_time?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/liq-records', params);
}
getOpenInterest(params: {
symbol: string;
period: string;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/open-interest', params);
}
getLatestBigDeal(params: { getTickers(params?: {
symbol: string; symbol?: string;
limit?: number; }): GenericAPIResponse {
}): GenericAPIResponse { return this.requestWrapper.get('v2/public/tickers', params);
return this.requestWrapper.get('v2/public/big-deal', params); }
}
getLongShortRatio(params: { getSymbols(): GenericAPIResponse {
symbol: string; return this.requestWrapper.get('v2/public/symbols');
period: string; }
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/account-ratio', params);
}
//------------Account Data Endpoints------------>
getApiKeyInfo(): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/api-key');
}
//------------Wallet Data Endpoints------------>
getWalletBalance(params: {
coin?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/balance',params)
}
getAssetExchangeRecords(params?: { getLiquidations(params: {
limit?: number; symbol: string;
from?: number; from?: number;
direction?: string; limit?: number;
}): GenericAPIResponse { start_time?: number;
return this.requestWrapper.get('v2/private/exchange-order/list', params); end_time?: number;
} }): GenericAPIResponse {
return this.requestWrapper.get('v2/public/liq-records', params);
}
getWalletFundRecords(params?: { getOpenInterest(params: {
start_date?: string; symbol: string;
end_date?: string; period: string;
currency?: string; limit?: number;
coin?: string; }): GenericAPIResponse {
wallet_fund_type?: string; return this.requestWrapper.get('v2/public/open-interest', params);
page?: number; }
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(params: { getLatestBigDeal(params: {
start_date?: string; symbol: string;
end_date?: string; limit?: number;
coin?: string; }): GenericAPIResponse {
status?: string; return this.requestWrapper.get('v2/public/big-deal', params);
page?: number; }
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/withdraw/list', params);
}
//-------------API Data Endpoints------------->
getServerTime(): GenericAPIResponse { getLongShortRatio(params: {
return this.requestWrapper.get('v2/public/time'); symbol: string;
} period: string;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/public/account-ratio', params);
}
getApiAnnouncements(): GenericAPIResponse { //------------Account Data Endpoints------------>
return this.requestWrapper.get('v2/public/announcement');
}
async getTimeOffset(): Promise<number> { getApiKeyInfo(): GenericAPIResponse {
const start = Date.now(); return this.requestWrapper.get('v2/private/account/api-key');
return this.getServerTime().then(result => { }
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2)); //------------Wallet Data Endpoints------------>
});
} getWalletBalance(params: {
} coin?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/balance', params)
}
getAssetExchangeRecords(params?: {
limit?: number;
from?: number;
direction?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/exchange-order/list', params);
}
getWalletFundRecords(params?: {
start_date?: string;
end_date?: string;
currency?: string;
coin?: string;
wallet_fund_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(params: {
start_date?: string;
end_date?: string;
coin?: string;
status?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/withdraw/list', params);
}
//-------------API Data Endpoints------------->
getServerTime(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/time');
}
getApiAnnouncements(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/announcement');
}
async getTimeOffset(): Promise < number > {
const start = Date.now();
return this.getServerTime().then(result => {
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
});
}
}

View File

@@ -3,15 +3,15 @@ import { InverseClient } from './inverse-client';
import { LinearClient } from './linear-client'; import { LinearClient } from './linear-client';
import { DefaultLogger } from './logger'; import { DefaultLogger } from './logger';
import { signMessage, serializeParams } from './util/requestUtils'; import { signMessage, serializeParams } from './util/requestUtils';
// import WebSocket from 'ws';
import WebSocket from 'isomorphic-ws'; import WebSocket from 'isomorphic-ws';
const iwsUrls = { const inverseEndpoints = {
livenet: 'wss://stream.bybit.com/realtime', livenet: 'wss://stream.bybit.com/realtime',
testnet: 'wss://stream-testnet.bybit.com/realtime' testnet: 'wss://stream-testnet.bybit.com/realtime'
}; };
const lwsUrls = { const linearEndpoints = {
livenet: 'wss://stream.bybit.com/realtime_public', livenet: 'wss://stream.bybit.com/realtime_public',
testnet: 'wss://stream-testnet.bybit.com/realtime_public' testnet: 'wss://stream-testnet.bybit.com/realtime_public'
}; };
@@ -22,7 +22,15 @@ const READY_STATE_CONNECTED = 2;
const READY_STATE_CLOSING = 3; const READY_STATE_CLOSING = 3;
const READY_STATE_RECONNECTING = 4; const READY_STATE_RECONNECTING = 4;
export interface WebsocketClientOptions { enum WsConnectionState {
READY_STATE_INITIAL,
READY_STATE_CONNECTING,
READY_STATE_CONNECTED,
READY_STATE_CLOSING,
READY_STATE_RECONNECTING
};
export interface WebsocketClientConfigurableOptions {
key?: string; key?: string;
secret?: string; secret?: string;
livenet?: boolean; livenet?: boolean;
@@ -35,27 +43,60 @@ export interface WebsocketClientOptions {
wsUrl?: string; wsUrl?: string;
}; };
export interface WebsocketClientOptions extends WebsocketClientConfigurableOptions {
livenet: boolean;
linear: boolean;
pongTimeout: number;
pingInterval: number;
reconnectTimeout: number;
};
type Logger = typeof DefaultLogger; type Logger = typeof DefaultLogger;
// class WsStore {
// private connections: {
// [key: string]: WebSocket
// };
// private logger: Logger;
// constructor(logger: Logger) {
// this.connections = {}
// this.logger = logger || DefaultLogger;
// }
// getConnection(key: string) {
// return this.connections[key];
// }
// setConnection(key: string, wsConnection: WebSocket) {
// const existingConnection = this.getConnection(key);
// if (existingConnection) {
// this.logger.info('WsStore setConnection() overwriting existing connection: ', existingConnection);
// }
// this.connections[key] = wsConnection;
// }
// }
export class WebsocketClient extends EventEmitter { export class WebsocketClient extends EventEmitter {
private activePingTimer?: number | undefined;
private activePongTimer?: number | undefined;
private logger: Logger; private logger: Logger;
private readyState: number; private wsState: WsConnectionState;
private pingInterval?: number | undefined;
private pongTimeout?: number | undefined;
private client: InverseClient | LinearClient; private client: InverseClient | LinearClient;
private _subscriptions: Set<unknown>; private subcribedTopics: Set<string>;
private ws: WebSocket;
private options: WebsocketClientOptions; private options: WebsocketClientOptions;
constructor(options: WebsocketClientOptions, logger?: Logger) { private ws: WebSocket;
// private wsStore: WsStore;
constructor(options: WebsocketClientConfigurableOptions, logger?: Logger) {
super(); super();
this.logger = logger || DefaultLogger; this.logger = logger || DefaultLogger;
this.readyState = READY_STATE_INITIAL; this.wsState = READY_STATE_INITIAL;
this.pingInterval = undefined; this.activePingTimer = undefined;
this.pongTimeout = undefined; this.activePongTimer = undefined;
this.options = { this.options = {
livenet: false, livenet: false,
@@ -68,73 +109,86 @@ export class WebsocketClient extends EventEmitter {
if (this.options.linear === true) { if (this.options.linear === true) {
this.client = new LinearClient(undefined, undefined, this.options.livenet, this.options.restOptions, this.options.requestOptions); this.client = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
}else{ }else{
this.client = new InverseClient(undefined, undefined, this.options.livenet, this.options.restOptions, this.options.requestOptions); this.client = new InverseClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
} }
this._subscriptions = new Set(); this.subcribedTopics = new Set();
this._connect(); // this.wsStore = new WsStore(this.logger);
this.connect();
} }
subscribe(topics) { isLivenet(): boolean {
if (!Array.isArray(topics)) topics = [topics]; return this.options.livenet === true;
topics.forEach(topic => this._subscriptions.add(topic));
// subscribe not necessary if not yet connected (will subscribe onOpen)
if (this.readyState === READY_STATE_CONNECTED) this._subscribe(topics);
} }
unsubscribe(topics) { /**
if (!Array.isArray(topics)) topics = [topics]; * Add topic/topics to WS subscription list
*/
public subscribe(wsTopics: string[] | string) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach(topic => this.subcribedTopics.add(topic));
topics.forEach(topic => this._subscriptions.delete(topic)); // subscribe not necessary if not yet connected (will automatically subscribe onOpen)
if (this.wsState === READY_STATE_CONNECTED) {
this.requestSubscribeTopics(topics);
}
}
/**
* Remove topic/topics from WS subscription list
*/
public unsubscribe(wsTopics: string[] | string) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach(topic => this.subcribedTopics.delete(topic));
// unsubscribe not necessary if not yet connected // unsubscribe not necessary if not yet connected
if (this.readyState === READY_STATE_CONNECTED) this._unsubscribe(topics); if (this.wsState === READY_STATE_CONNECTED) {
this.requestUnsubscribeTopics(topics);
}
} }
close() { close() {
this.logger.info('Closing connection', {category: 'bybit-ws'}); this.logger.info('Closing connection', {category: 'bybit-ws'});
this.readyState = READY_STATE_CLOSING; this.wsState = READY_STATE_CLOSING;
this._teardown(); this.teardown();
this.ws && this.ws.close(); this.ws && this.ws.close();
} }
_getWsUrl() { private getWsUrl() {
if (this.options.wsUrl) { if (this.options.wsUrl) {
return this.options.wsUrl; return this.options.wsUrl;
} }
if (this.options.linear){ if (this.options.linear){
return lwsUrls[this.options.livenet ? 'livenet' : 'testnet']; return linearEndpoints[this.options.livenet ? 'livenet' : 'testnet'];
} }
return iwsUrls[this.options.livenet ? 'livenet' : 'testnet']; return inverseEndpoints[this.options.livenet ? 'livenet' : 'testnet'];
} }
async _connect() { private async connect() {
try { try {
if (this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING; if (this.wsState === READY_STATE_INITIAL) {
this.wsState = READY_STATE_CONNECTING;
}
const authParams = await this._authenticate(); const authParams = await this.getAuthParams();
const url = this._getWsUrl() + authParams; const url = this.getWsUrl() + authParams;
const ws = new WebSocket(url);
ws.onopen = this._wsOpenHandler.bind(this); this.ws = this.connectToWsUrl(url, 'main');
ws.onmessage = this._wsMessageHandler.bind(this); return this.ws;
ws.onerror = this._wsOnErrorHandler.bind(this);
ws.onclose = this._wsCloseHandler.bind(this);
this.ws = ws;
} catch (err) { } catch (err) {
this.logger.error('Connection failed: ', err); this.logger.error('Connection failed: ', err);
this._reconnect(this.options.reconnectTimeout); this.reconnectWithDelay(this.options.reconnectTimeout!);
} }
} }
async _authenticate() { /**
* Return params required to make authorized request
*/
private async getAuthParams(): Promise<string> {
if (this.options.key && this.options.secret) { if (this.options.key && this.options.secret) {
this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'}); this.logger.debug('Getting auth\'d request params', {category: 'bybit-ws'});
const timeOffset = await this.client.getTimeOffset(); const timeOffset = await this.client.getTimeOffset();
@@ -147,7 +201,7 @@ export class WebsocketClient extends EventEmitter {
return '?' + serializeParams(params); return '?' + serializeParams(params);
} else if (this.options.key || this.options.secret) { } else if (this.options.key || this.options.secret) {
this.logger.warning('Could not authenticate websocket, either api key or private key missing.', { category: 'bybit-ws' }); this.logger.warning('Connot authenticate websocket, either api or private keys missing.', { category: 'bybit-ws' });
} else { } else {
this.logger.debug('Starting public only websocket client.', { category: 'bybit-ws' }); this.logger.debug('Starting public only websocket client.', { category: 'bybit-ws' });
} }
@@ -155,89 +209,130 @@ export class WebsocketClient extends EventEmitter {
return ''; return '';
} }
_reconnect(timeout) { private reconnectWithDelay(connectionDelay: number) {
this._teardown(); this.teardown();
if (this.readyState !== READY_STATE_CONNECTING) { if (this.wsState !== READY_STATE_CONNECTING) {
this.readyState = READY_STATE_RECONNECTING; this.wsState = READY_STATE_RECONNECTING;
} }
setTimeout(() => { setTimeout(() => {
this.logger.info('Reconnecting to server', { category: 'bybit-ws' }); this.logger.info('Reconnecting to server', { category: 'bybit-ws' });
this._connect(); this.connect();
}, timeout); }, connectionDelay);
} }
_ping() { private ping() {
clearTimeout(this.pongTimeout!); clearTimeout(this.activePongTimer!);
delete this.pongTimeout; delete this.activePongTimer;
this.logger.silly('Sending ping', { category: 'bybit-ws' }); this.logger.silly('Sending ping', { category: 'bybit-ws' });
this.ws.send(JSON.stringify({op: 'ping'})); this.ws.send(JSON.stringify({op: 'ping'}));
this.pongTimeout = <any>setTimeout(() => { this.activePongTimer = <any>setTimeout(() => {
this.logger.info('Pong timeout', { category: 'bybit-ws' }); this.logger.info('Pong timeout', { category: 'bybit-ws' });
this._teardown(); this.teardown();
// this.ws.terminate(); // this.ws.terminate();
// TODO: does this work? // TODO: does this work?
this.ws.close(); this.ws.close();
}, this.options.pongTimeout); }, this.options.pongTimeout);
} }
_teardown() { private teardown() {
if (this.pingInterval) clearInterval(this.pingInterval); if (this.activePingTimer) {
if (this.pongTimeout) clearTimeout(this.pongTimeout); clearInterval(this.activePingTimer);
}
if (this.activePongTimer) {
clearTimeout(this.activePongTimer);
}
this.pongTimeout = undefined; this.activePongTimer = undefined;
this.pingInterval = undefined; this.activePingTimer = undefined;
} }
_wsOpenHandler() { /**
if (this.readyState === READY_STATE_CONNECTING) { * Send WS message to subscribe to topics.
*/
private requestSubscribeTopics(topics: string[]) {
const wsMessage = JSON.stringify({
op: 'subscribe',
args: topics
});
this.ws.send(wsMessage);
}
/**
* Send WS message to unsubscribe from topics.
*/
private requestUnsubscribeTopics(topics: string[]) {
const wsMessage = JSON.stringify({
op: 'unsubscribe',
args: topics
});
this.ws.send(wsMessage);
}
private connectToWsUrl(url: string, wsKey: string): WebSocket {
const ws = new WebSocket(url);
ws.onopen = event => this.onWsOpen(event, wsKey);
ws.onmessage = event => this.onWsMessage(event, wsKey);
ws.onerror = event => this.onWsError(event, wsKey);
ws.onclose = event => this.onWsClose(event, wsKey);
return ws;
}
private onWsOpen(event, wsRef?: string) {
if (this.wsState === READY_STATE_CONNECTING) {
this.logger.info('Websocket connected', { category: 'bybit-ws', livenet: this.options.livenet, linear: this.options.linear }); this.logger.info('Websocket connected', { category: 'bybit-ws', livenet: this.options.livenet, linear: this.options.linear });
this.emit('open'); this.emit('open');
} else if (this.readyState === READY_STATE_RECONNECTING) { } else if (this.wsState === READY_STATE_RECONNECTING) {
this.logger.info('Websocket reconnected', { category: 'bybit-ws', livenet: this.options.livenet }); this.logger.info('Websocket reconnected', { category: 'bybit-ws', livenet: this.options.livenet });
this.emit('reconnected'); this.emit('reconnected');
} }
this.readyState = READY_STATE_CONNECTED; this.wsState = READY_STATE_CONNECTED;
this._subscribe([...this._subscriptions]); this.requestSubscribeTopics([...this.subcribedTopics]);
this.pingInterval = <any>setInterval(this._ping.bind(this), this.options.pingInterval); this.activePingTimer = <any>setInterval(this.ping.bind(this), this.options.pingInterval);
} }
_wsMessageHandler(message) { private onWsMessage(event, wsRef?: string) {
const msg = JSON.parse(message && message.data || message); const msg = JSON.parse(event && event.data || event);
if ('success' in msg) { if ('success' in msg) {
this._handleResponse(msg); this.onWsMessageResponse(msg);
} else if (msg.topic) { } else if (msg.topic) {
this._handleUpdate(msg); this.onWsMessageUpdate(msg);
} else { } else {
this.logger.warning('Got unhandled ws message', msg); this.logger.warning('Got unhandled ws message', msg);
} }
} }
_wsOnErrorHandler(err) { private onWsError(err, wsRef?: string) {
this.logger.error('Websocket error', {category: 'bybit-ws', err}); this.logger.error('Websocket error', {category: 'bybit-ws', err});
if (this.readyState === READY_STATE_CONNECTED) this.emit('error', err); if (this.wsState === READY_STATE_CONNECTED) {
this.emit('error', err);
}
} }
_wsCloseHandler() { private onWsClose(event, wsRef?: string) {
this.logger.info('Websocket connection closed', {category: 'bybit-ws'}); this.logger.info('Websocket connection closed', {category: 'bybit-ws'});
if (this.readyState !== READY_STATE_CLOSING) { if (this.wsState !== READY_STATE_CLOSING) {
this._reconnect(this.options.reconnectTimeout); this.reconnectWithDelay(this.options.reconnectTimeout!);
this.emit('reconnect'); this.emit('reconnect');
} else { } else {
this.readyState = READY_STATE_INITIAL; this.wsState = READY_STATE_INITIAL;
this.emit('close'); this.emit('close');
} }
} }
_handleResponse(response) { private onWsMessageResponse(response) {
if ( if (
response.request && response.request &&
response.request.op === 'ping' && response.request.op === 'ping' &&
@@ -245,31 +340,13 @@ export class WebsocketClient extends EventEmitter {
response.success === true response.success === true
) { ) {
this.logger.silly('pong recieved', {category: 'bybit-ws'}); this.logger.silly('pong recieved', {category: 'bybit-ws'});
clearTimeout(this.pongTimeout); clearTimeout(this.activePongTimer);
} else { } else {
this.emit('response', response); this.emit('response', response);
} }
} }
_handleUpdate(message) { private onWsMessageUpdate(message) {
this.emit('update', message); this.emit('update', message);
} }
_subscribe(topics) {
const msgStr = JSON.stringify({
op: 'subscribe',
'args': topics
});
this.ws.send(msgStr);
}
_unsubscribe(topics) {
const msgStr = JSON.stringify({
op: 'unsubscribe',
'args': topics
});
this.ws.send(msgStr);
}
}; };