v2.4.0-beta.1. remove circleci, cleaning in ws client
This commit is contained in:
@@ -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
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bybit-api",
|
"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.",
|
"description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './response';
|
export * from './response';
|
||||||
export * from './request';
|
export * from './request';
|
||||||
export * from './shared';
|
export * from './shared';
|
||||||
|
export * from './websockets';
|
||||||
|
|||||||
@@ -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 numberInString = string;
|
||||||
|
|
||||||
export type OrderSide = 'Buy' | 'Sell';
|
export type OrderSide = 'Buy' | 'Sell';
|
||||||
|
|||||||
107
src/types/websockets.ts
Normal file
107
src/types/websockets.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,16 +1,35 @@
|
|||||||
import WebSocket from 'isomorphic-ws';
|
import WebSocket from 'isomorphic-ws';
|
||||||
|
|
||||||
import { WsConnectionState } from '../websocket-client';
|
|
||||||
import { DefaultLogger } from './logger';
|
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;
|
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>;
|
type WsTopicList = Set<WsTopic>;
|
||||||
|
|
||||||
interface WsStoredState {
|
interface WsStoredState {
|
||||||
|
/** The currently active websocket connection */
|
||||||
ws?: WebSocket;
|
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;
|
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;
|
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;
|
subscribedTopics: WsTopicList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +42,9 @@ export default class WsStore {
|
|||||||
this.wsState = {};
|
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 {
|
get(key: string, createIfMissing?: boolean): WsStoredState | undefined {
|
||||||
if (this.wsState[key]) {
|
if (this.wsState[key]) {
|
||||||
return this.wsState[key];
|
return this.wsState[key];
|
||||||
@@ -46,7 +68,7 @@ export default class WsStore {
|
|||||||
}
|
}
|
||||||
this.wsState[key] = {
|
this.wsState[key] = {
|
||||||
subscribedTopics: new Set(),
|
subscribedTopics: new Set(),
|
||||||
connectionState: WsConnectionState.READY_STATE_INITIAL,
|
connectionState: WsConnectionStateEnum.INITIAL,
|
||||||
};
|
};
|
||||||
return this.get(key);
|
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!;
|
return this.get(key, true)!.connectionState!;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConnectionState(key: string, state: WsConnectionState) {
|
setConnectionState(key: string, state: WsConnectionStateEnum) {
|
||||||
this.get(key, true)!.connectionState = state;
|
this.get(key, true)!.connectionState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnectionState(key: string, state: WsConnectionState): boolean {
|
isConnectionState(key: string, state: WsConnectionStateEnum): boolean {
|
||||||
return this.getConnectionState(key) === state;
|
return this.getConnectionState(key) === state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* subscribed topics */
|
/* subscribed topics */
|
||||||
|
|
||||||
getTopics(key: string): WsTopicList {
|
getTopics(key: string): WsTopicList {
|
||||||
return this.get(key, true)!.subscribedTopics;
|
return this.get(key, true).subscribedTopics;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTopicsByKey(): Record<string, WsTopicList> {
|
getTopicsByKey(): Record<string, WsTopicList> {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from './BaseRestClient';
|
|||||||
export * from './requestUtils';
|
export * from './requestUtils';
|
||||||
export * from './WsStore';
|
export * from './WsStore';
|
||||||
export * from './logger';
|
export * from './logger';
|
||||||
|
export * from './websocket-util';
|
||||||
|
|||||||
40
src/util/websocket-util.ts
Normal file
40
src/util/websocket-util.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -3,17 +3,35 @@ import WebSocket from 'isomorphic-ws';
|
|||||||
|
|
||||||
import { InverseClient } from './inverse-client';
|
import { InverseClient } from './inverse-client';
|
||||||
import { LinearClient } from './linear-client';
|
import { LinearClient } from './linear-client';
|
||||||
import { DefaultLogger } from './util/logger';
|
import { SpotClientV3 } from './spot-client-v3';
|
||||||
import { SpotClient } from './spot-client';
|
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 { signMessage } from './util/node-support';
|
||||||
|
|
||||||
|
import WsStore from './util/WsStore';
|
||||||
import {
|
import {
|
||||||
serializeParams,
|
serializeParams,
|
||||||
isWsPong,
|
isWsPong,
|
||||||
RestClientOptions,
|
getLinearWsKeyForTopic,
|
||||||
} from './util/requestUtils';
|
getSpotWsKeyForTopic,
|
||||||
|
wsKeyInverse,
|
||||||
import WsStore from './util/WsStore';
|
wsKeyLinearPrivate,
|
||||||
|
wsKeyLinearPublic,
|
||||||
|
wsKeySpotPrivate,
|
||||||
|
wsKeySpotPublic,
|
||||||
|
WsConnectionStateEnum,
|
||||||
|
} from './util';
|
||||||
|
|
||||||
const inverseEndpoints = {
|
const inverseEndpoints = {
|
||||||
livenet: 'wss://stream.bybit.com/realtime',
|
livenet: 'wss://stream.bybit.com/realtime',
|
||||||
@@ -48,169 +66,6 @@ const spotEndpoints = {
|
|||||||
|
|
||||||
const loggerCategory = { category: 'bybit-ws' };
|
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 {
|
export declare interface WebsocketClient {
|
||||||
on(
|
on(
|
||||||
event: 'open' | 'reconnected',
|
event: 'open' | 'reconnected',
|
||||||
@@ -223,16 +78,10 @@ export declare interface WebsocketClient {
|
|||||||
on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this;
|
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 {
|
export class WebsocketClient extends EventEmitter {
|
||||||
private logger: typeof DefaultLogger;
|
private logger: typeof DefaultLogger;
|
||||||
private restClient: InverseClient | LinearClient | SpotClient;
|
/** Purely used */
|
||||||
|
private restClient: RESTClient;
|
||||||
private options: WebsocketClientOptions;
|
private options: WebsocketClientOptions;
|
||||||
private wsStore: WsStore;
|
private wsStore: WsStore;
|
||||||
|
|
||||||
@@ -254,11 +103,15 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.options.market) {
|
if (this.isV3()) {
|
||||||
this.options.market = resolveMarket(this.options);
|
this.restClient = new SpotClientV3(
|
||||||
}
|
undefined,
|
||||||
|
undefined,
|
||||||
if (this.isLinear()) {
|
this.isLivenet(),
|
||||||
|
this.options.restOptions,
|
||||||
|
this.options.requestOptions
|
||||||
|
);
|
||||||
|
} else if (this.isLinear()) {
|
||||||
this.restClient = new LinearClient(
|
this.restClient = new LinearClient(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -299,7 +152,12 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isInverse(): boolean {
|
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
|
// attempt to send subscription topic per websocket
|
||||||
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
|
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
|
||||||
// if connected, send subscription request
|
// 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);
|
return this.requestSubscribeTopics(wsKey, topics);
|
||||||
}
|
}
|
||||||
|
|
||||||
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
|
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
|
||||||
if (
|
if (
|
||||||
!this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING) &&
|
!this.wsStore.isConnectionState(
|
||||||
!this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
|
wsKey,
|
||||||
|
WsConnectionStateEnum.CONNECTING
|
||||||
|
) &&
|
||||||
|
!this.wsStore.isConnectionState(
|
||||||
|
wsKey,
|
||||||
|
WsConnectionStateEnum.RECONNECTING
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return this.connect(wsKey);
|
return this.connect(wsKey);
|
||||||
}
|
}
|
||||||
@@ -339,7 +205,9 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
|
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
|
||||||
// unsubscribe request only necessary if active connection exists
|
// 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);
|
this.requestUnsubscribeTopics(wsKey, topics);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -347,14 +215,14 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
public close(wsKey: WsKey) {
|
public close(wsKey: WsKey) {
|
||||||
this.logger.info('Closing connection', { ...loggerCategory, wsKey });
|
this.logger.info('Closing connection', { ...loggerCategory, wsKey });
|
||||||
this.setWsState(wsKey, READY_STATE_CLOSING);
|
this.setWsState(wsKey, WsConnectionStateEnum.CLOSING);
|
||||||
this.clearTimers(wsKey);
|
this.clearTimers(wsKey);
|
||||||
|
|
||||||
this.getWs(wsKey)?.close();
|
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 {
|
public connectAll(): Promise<WebSocket | undefined>[] | undefined {
|
||||||
if (this.isInverse()) {
|
if (this.isInverse()) {
|
||||||
@@ -411,7 +279,9 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
return this.wsStore.getWs(wsKey);
|
return this.wsStore.getWs(wsKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
|
if (
|
||||||
|
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
|
||||||
|
) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'Refused to connect to ws, connection attempt already active',
|
'Refused to connect to ws, connection attempt already active',
|
||||||
{ ...loggerCategory, wsKey }
|
{ ...loggerCategory, wsKey }
|
||||||
@@ -421,9 +291,9 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!this.wsStore.getConnectionState(wsKey) ||
|
!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);
|
const authParams = await this.getAuthParams(wsKey);
|
||||||
@@ -481,19 +351,23 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
? await this.restClient.fetchTimeOffset()
|
? await this.restClient.fetchTimeOffset()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const params: any = {
|
const signatureExpires = Date.now() + timeOffset + 5000;
|
||||||
api_key: this.options.key,
|
|
||||||
expires: Date.now() + timeOffset + 5000,
|
|
||||||
};
|
|
||||||
|
|
||||||
params.signature = await signMessage(
|
const signature = await signMessage(
|
||||||
'GET/realtime' + params.expires,
|
'GET/realtime' + signatureExpires,
|
||||||
secret
|
secret
|
||||||
);
|
);
|
||||||
return '?' + serializeParams(params);
|
|
||||||
|
const authParams = {
|
||||||
|
api_key: this.options.key,
|
||||||
|
expires: signatureExpires,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
|
||||||
|
return '?' + serializeParams(authParams);
|
||||||
} else if (!key || !secret) {
|
} else if (!key || !secret) {
|
||||||
this.logger.warning(
|
this.logger.warning(
|
||||||
'Connot authenticate websocket, either api or private keys missing.',
|
'Cannot authenticate websocket, either api or private keys missing.',
|
||||||
{ ...loggerCategory, wsKey }
|
{ ...loggerCategory, wsKey }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -508,8 +382,11 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
|
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
|
||||||
this.clearTimers(wsKey);
|
this.clearTimers(wsKey);
|
||||||
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CONNECTING) {
|
if (
|
||||||
this.setWsState(wsKey, READY_STATE_RECONNECTING);
|
this.wsStore.getConnectionState(wsKey) !==
|
||||||
|
WsConnectionStateEnum.CONNECTING
|
||||||
|
) {
|
||||||
|
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -635,7 +512,9 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onWsOpen(event, wsKey: WsKey) {
|
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', {
|
this.logger.info('Websocket connected', {
|
||||||
...loggerCategory,
|
...loggerCategory,
|
||||||
wsKey,
|
wsKey,
|
||||||
@@ -645,13 +524,13 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
});
|
});
|
||||||
this.emit('open', { wsKey, event });
|
this.emit('open', { wsKey, event });
|
||||||
} else if (
|
} else if (
|
||||||
this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
|
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING)
|
||||||
) {
|
) {
|
||||||
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
|
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
|
||||||
this.emit('reconnected', { wsKey, event });
|
this.emit('reconnected', { wsKey, event });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setWsState(wsKey, READY_STATE_CONNECTED);
|
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
|
||||||
|
|
||||||
// TODO: persistence not working yet for spot topics
|
// TODO: persistence not working yet for spot topics
|
||||||
if (wsKey !== 'spotPublic' && wsKey !== 'spotPrivate') {
|
if (wsKey !== 'spotPublic' && wsKey !== 'spotPrivate') {
|
||||||
@@ -670,18 +549,20 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
this.clearPongTimer(wsKey);
|
this.clearPongTimer(wsKey);
|
||||||
|
|
||||||
const msg = JSON.parse((event && event.data) || event);
|
const msg = JSON.parse((event && event.data) || event);
|
||||||
if ('success' in msg || msg?.pong) {
|
if (msg['success'] || msg?.pong) {
|
||||||
this.onWsMessageResponse(msg, wsKey);
|
return 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.topic) {
|
||||||
|
return this.emit('update', msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warning('Got unhandled ws message', {
|
||||||
|
...loggerCategory,
|
||||||
|
message: msg,
|
||||||
|
event,
|
||||||
|
wsKey,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error('Failed to parse ws event message', {
|
this.logger.error('Failed to parse ws event message', {
|
||||||
...loggerCategory,
|
...loggerCategory,
|
||||||
@@ -694,7 +575,9 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
|
|
||||||
private onWsError(error: any, wsKey: WsKey) {
|
private onWsError(error: any, wsKey: WsKey) {
|
||||||
this.parseWsError('Websocket error', error, 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);
|
this.emit('error', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -705,11 +588,13 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
wsKey,
|
wsKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) {
|
if (
|
||||||
|
this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING
|
||||||
|
) {
|
||||||
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
|
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
|
||||||
this.emit('reconnect', { wsKey });
|
this.emit('reconnect', { wsKey });
|
||||||
} else {
|
} else {
|
||||||
this.setWsState(wsKey, READY_STATE_INITIAL);
|
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
|
||||||
this.emit('close', { wsKey });
|
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) {
|
private getWs(wsKey: string) {
|
||||||
return this.wsStore.getWs(wsKey);
|
return this.wsStore.getWs(wsKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setWsState(wsKey: WsKey, state: WsConnectionState) {
|
private setWsState(wsKey: WsKey, state: WsConnectionStateEnum) {
|
||||||
this.wsStore.setConnectionState(wsKey, state);
|
this.wsStore.setConnectionState(wsKey, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +621,7 @@ export class WebsocketClient extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const networkKey = this.isLivenet() ? 'livenet' : 'testnet';
|
const networkKey = this.isLivenet() ? 'livenet' : 'testnet';
|
||||||
// TODO: reptitive
|
// TODO: repetitive
|
||||||
if (this.isLinear() || wsKey.startsWith('linear')) {
|
if (this.isLinear() || wsKey.startsWith('linear')) {
|
||||||
if (wsKey === wsKeyLinearPublic) {
|
if (wsKey === wsKeyLinearPublic) {
|
||||||
return linearEndpoints.public[networkKey];
|
return linearEndpoints.public[networkKey];
|
||||||
|
|||||||
Reference in New Issue
Block a user