bybit-api 3.7.1-beta.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -6
- package/lib/constants/enum.d.ts +4 -1
- package/lib/constants/enum.js +4 -1
- package/lib/constants/enum.js.map +1 -1
- package/lib/rest-client-v5.d.ts +3 -3
- package/lib/rest-client-v5.js.map +1 -1
- package/lib/types/request/v5-trade.d.ts +2 -2
- package/lib/types/response/v5-market.d.ts +4 -1
- package/package.json +1 -13
- package/src/account-asset-client-v3.ts +0 -310
- package/src/account-asset-client.ts +0 -149
- package/src/constants/enum.ts +0 -105
- package/src/contract-client.ts +0 -360
- package/src/copy-trading-client.ts +0 -159
- package/src/index.ts +0 -18
- package/src/inverse-client.ts +0 -337
- package/src/inverse-futures-client.ts +0 -403
- package/src/linear-client.ts +0 -387
- package/src/rest-client-v5.ts +0 -1638
- package/src/spot-client-v3.ts +0 -303
- package/src/spot-client.ts +0 -180
- package/src/types/index.ts +0 -5
- package/src/types/request/account-asset.ts +0 -258
- package/src/types/request/contract.ts +0 -139
- package/src/types/request/copy-trading.ts +0 -53
- package/src/types/request/index.ts +0 -18
- package/src/types/request/inverse.ts +0 -135
- package/src/types/request/linear.ts +0 -201
- package/src/types/request/spot.ts +0 -89
- package/src/types/request/unified-margin.ts +0 -247
- package/src/types/request/usdc-options.ts +0 -134
- package/src/types/request/usdc-perp.ts +0 -100
- package/src/types/request/usdc-shared.ts +0 -39
- package/src/types/request/v5-account.ts +0 -40
- package/src/types/request/v5-asset.ts +0 -133
- package/src/types/request/v5-market.ts +0 -127
- package/src/types/request/v5-position.ts +0 -107
- package/src/types/request/v5-pre-upgrade.ts +0 -37
- package/src/types/request/v5-spot-leverage-token.ts +0 -23
- package/src/types/request/v5-trade.ts +0 -133
- package/src/types/request/v5-user.ts +0 -21
- package/src/types/response/account-asset.ts +0 -236
- package/src/types/response/contract.ts +0 -74
- package/src/types/response/index.ts +0 -13
- package/src/types/response/shared.ts +0 -19
- package/src/types/response/spot.ts +0 -17
- package/src/types/response/unified-margin.ts +0 -71
- package/src/types/response/usdt-perp.ts +0 -58
- package/src/types/response/v5-account.ts +0 -122
- package/src/types/response/v5-asset.ts +0 -170
- package/src/types/response/v5-market.ts +0 -271
- package/src/types/response/v5-position.ts +0 -118
- package/src/types/response/v5-spot-leverage-token.ts +0 -70
- package/src/types/response/v5-trade.ts +0 -103
- package/src/types/response/v5-user.ts +0 -58
- package/src/types/shared.ts +0 -175
- package/src/types/v5-shared.ts +0 -213
- package/src/types/websockets.ts +0 -115
- package/src/types/ws-events/failed-topic-subscription-confirmation.ts +0 -6
- package/src/types/ws-events/succeeded-topic-subscription-confirmation.ts +0 -6
- package/src/types/ws-events/topic-subscription-confirmation.ts +0 -7
- package/src/unified-margin-client.ts +0 -400
- package/src/usdc-option-client.ts +0 -333
- package/src/usdc-perpetual-client.ts +0 -312
- package/src/util/BaseRestClient.ts +0 -502
- package/src/util/WsStore.ts +0 -160
- package/src/util/browser-support.ts +0 -29
- package/src/util/index.ts +0 -5
- package/src/util/logger.ts +0 -26
- package/src/util/node-support.ts +0 -9
- package/src/util/requestUtils.ts +0 -171
- package/src/util/websocket-util.ts +0 -550
- package/src/websocket-client.ts +0 -1262
package/src/websocket-client.ts
DELETED
|
@@ -1,1262 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
-
import { EventEmitter } from 'events';
|
|
4
|
-
import WebSocket from 'isomorphic-ws';
|
|
5
|
-
|
|
6
|
-
import { InverseClient } from './inverse-client';
|
|
7
|
-
import { LinearClient } from './linear-client';
|
|
8
|
-
import { SpotClientV3 } from './spot-client-v3';
|
|
9
|
-
import { SpotClient } from './spot-client';
|
|
10
|
-
import { USDCOptionClient } from './usdc-option-client';
|
|
11
|
-
import { USDCPerpetualClient } from './usdc-perpetual-client';
|
|
12
|
-
import { UnifiedMarginClient } from './unified-margin-client';
|
|
13
|
-
import { ContractClient } from './contract-client';
|
|
14
|
-
|
|
15
|
-
import { signMessage } from './util/node-support';
|
|
16
|
-
import WsStore from './util/WsStore';
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
APIMarket,
|
|
20
|
-
CategoryV5,
|
|
21
|
-
KlineInterval,
|
|
22
|
-
RESTClient,
|
|
23
|
-
WSClientConfigurableOptions,
|
|
24
|
-
WebsocketClientOptions,
|
|
25
|
-
WsKey,
|
|
26
|
-
WsTopic,
|
|
27
|
-
} from './types';
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
DefaultLogger,
|
|
31
|
-
PUBLIC_WS_KEYS,
|
|
32
|
-
WS_AUTH_ON_CONNECT_KEYS,
|
|
33
|
-
WS_KEY_MAP,
|
|
34
|
-
WsConnectionStateEnum,
|
|
35
|
-
getMaxTopicsPerSubscribeEvent,
|
|
36
|
-
getWsKeyForTopic,
|
|
37
|
-
getWsUrl,
|
|
38
|
-
isPrivateWsTopic,
|
|
39
|
-
isTopicSubscriptionConfirmation,
|
|
40
|
-
isTopicSubscriptionSuccess,
|
|
41
|
-
isWsPong,
|
|
42
|
-
neverGuard,
|
|
43
|
-
serializeParams,
|
|
44
|
-
} from './util';
|
|
45
|
-
import { RestClientV5 } from './rest-client-v5';
|
|
46
|
-
import { WebsocketTopicSubscriptionConfirmationEvent } from './types/ws-events/topic-subscription-confirmation';
|
|
47
|
-
|
|
48
|
-
const loggerCategory = { category: 'bybit-ws' };
|
|
49
|
-
|
|
50
|
-
export type WsClientEvent =
|
|
51
|
-
| 'open'
|
|
52
|
-
| 'update'
|
|
53
|
-
| 'close'
|
|
54
|
-
| 'error'
|
|
55
|
-
| 'reconnect'
|
|
56
|
-
| 'reconnected'
|
|
57
|
-
| 'response';
|
|
58
|
-
|
|
59
|
-
interface WebsocketClientEvents {
|
|
60
|
-
/** Connection opened. If this connection was previously opened and reconnected, expect the reconnected event instead */
|
|
61
|
-
open: (evt: { wsKey: WsKey; event: any }) => void;
|
|
62
|
-
/** Reconnecting a dropped connection */
|
|
63
|
-
reconnect: (evt: { wsKey: WsKey; event: any }) => void;
|
|
64
|
-
/** Successfully reconnected a connection that dropped */
|
|
65
|
-
reconnected: (evt: { wsKey: WsKey; event: any }) => void;
|
|
66
|
-
/** Connection closed */
|
|
67
|
-
close: (evt: { wsKey: WsKey; event: any }) => void;
|
|
68
|
-
/** Received reply to websocket command (e.g. after subscribing to topics) */
|
|
69
|
-
response: (response: any) => void;
|
|
70
|
-
/** Received data for topic */
|
|
71
|
-
update: (response: any) => void;
|
|
72
|
-
/** Exception from ws client OR custom listeners */
|
|
73
|
-
error: (response: any) => void;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
type TopicsPendingSubscriptionsResolver = () => void;
|
|
77
|
-
type TopicsPendingSubscriptionsRejector = (reason: string) => void;
|
|
78
|
-
|
|
79
|
-
interface TopicsPendingSubscriptions {
|
|
80
|
-
wsKey: string;
|
|
81
|
-
failedTopicsSubscriptions: Set<string>;
|
|
82
|
-
pendingTopicsSubscriptions: Set<string>;
|
|
83
|
-
resolver: TopicsPendingSubscriptionsResolver;
|
|
84
|
-
rejector: TopicsPendingSubscriptionsRejector;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Type safety for on and emit handlers: https://stackoverflow.com/a/61609010/880837
|
|
88
|
-
export declare interface WebsocketClient {
|
|
89
|
-
on<U extends keyof WebsocketClientEvents>(
|
|
90
|
-
event: U,
|
|
91
|
-
listener: WebsocketClientEvents[U],
|
|
92
|
-
): this;
|
|
93
|
-
|
|
94
|
-
emit<U extends keyof WebsocketClientEvents>(
|
|
95
|
-
event: U,
|
|
96
|
-
...args: Parameters<WebsocketClientEvents[U]>
|
|
97
|
-
): boolean;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// eslint-disable-next-line no-redeclare
|
|
101
|
-
export class WebsocketClient extends EventEmitter {
|
|
102
|
-
private logger: typeof DefaultLogger;
|
|
103
|
-
|
|
104
|
-
private restClient?: RESTClient;
|
|
105
|
-
|
|
106
|
-
private options: WebsocketClientOptions;
|
|
107
|
-
|
|
108
|
-
private wsStore: WsStore;
|
|
109
|
-
|
|
110
|
-
private pendingTopicsSubscriptions: TopicsPendingSubscriptions[] = [];
|
|
111
|
-
|
|
112
|
-
constructor(
|
|
113
|
-
options: WSClientConfigurableOptions,
|
|
114
|
-
logger?: typeof DefaultLogger,
|
|
115
|
-
) {
|
|
116
|
-
super();
|
|
117
|
-
|
|
118
|
-
this.logger = logger || DefaultLogger;
|
|
119
|
-
this.wsStore = new WsStore(this.logger);
|
|
120
|
-
|
|
121
|
-
this.options = {
|
|
122
|
-
testnet: false,
|
|
123
|
-
pongTimeout: 1000,
|
|
124
|
-
pingInterval: 10000,
|
|
125
|
-
reconnectTimeout: 500,
|
|
126
|
-
fetchTimeOffsetBeforeAuth: false,
|
|
127
|
-
...options,
|
|
128
|
-
};
|
|
129
|
-
this.options.restOptions = {
|
|
130
|
-
...this.options.restOptions,
|
|
131
|
-
testnet: this.options.testnet,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
this.prepareRESTClient();
|
|
135
|
-
|
|
136
|
-
// add default error handling so this doesn't crash node (if the user didn't set a handler)
|
|
137
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
138
|
-
this.on('error', () => {});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Get the WsStore that tracks websockets & topics */
|
|
142
|
-
public getWsStore(): WsStore {
|
|
143
|
-
return this.wsStore;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
public isTestnet(): boolean {
|
|
147
|
-
return this.options.testnet === true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Subscribe to V5 topics & track/persist them.
|
|
152
|
-
* @param wsTopics - topic or list of topics
|
|
153
|
-
* @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics.
|
|
154
|
-
* @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
|
155
|
-
*/
|
|
156
|
-
public subscribeV5(
|
|
157
|
-
wsTopics: WsTopic[] | WsTopic,
|
|
158
|
-
category: CategoryV5,
|
|
159
|
-
isPrivateTopic?: boolean,
|
|
160
|
-
) {
|
|
161
|
-
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
|
162
|
-
|
|
163
|
-
return new Promise<void>((resolver, rejector) => {
|
|
164
|
-
topics.forEach((topic) => {
|
|
165
|
-
const wsKey = getWsKeyForTopic(
|
|
166
|
-
this.options.market,
|
|
167
|
-
topic,
|
|
168
|
-
isPrivateTopic,
|
|
169
|
-
category,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Persist topic for reconnects
|
|
173
|
-
this.wsStore.addTopic(wsKey, topic);
|
|
174
|
-
this.upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector);
|
|
175
|
-
|
|
176
|
-
// if connected, send subscription request
|
|
177
|
-
if (
|
|
178
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
|
|
179
|
-
) {
|
|
180
|
-
return this.requestSubscribeTopics(wsKey, [topic]);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
|
|
184
|
-
if (
|
|
185
|
-
!this.wsStore.isConnectionState(
|
|
186
|
-
wsKey,
|
|
187
|
-
WsConnectionStateEnum.CONNECTING,
|
|
188
|
-
) &&
|
|
189
|
-
!this.wsStore.isConnectionState(
|
|
190
|
-
wsKey,
|
|
191
|
-
WsConnectionStateEnum.RECONNECTING,
|
|
192
|
-
)
|
|
193
|
-
) {
|
|
194
|
-
return this.connect(wsKey);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Subscribe to V1-V3 topics & track/persist them.
|
|
202
|
-
*
|
|
203
|
-
* Note: for public V5 topics use the `subscribeV5()` method.
|
|
204
|
-
*
|
|
205
|
-
* Topics will be automatically resubscribed to if the connection resets/drops/reconnects.
|
|
206
|
-
* @param wsTopics - topic or list of topics
|
|
207
|
-
* @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
|
208
|
-
*/
|
|
209
|
-
public subscribe(
|
|
210
|
-
wsTopics: WsTopic[] | WsTopic,
|
|
211
|
-
isPrivateTopic?: boolean,
|
|
212
|
-
): Promise<void> {
|
|
213
|
-
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
|
214
|
-
if (this.options.market === 'v5') {
|
|
215
|
-
topics.forEach((topic) => {
|
|
216
|
-
if (!isPrivateWsTopic(topic)) {
|
|
217
|
-
throw new Error(
|
|
218
|
-
'For public "v5" websocket topics, use the subscribeV5() method & provide the category parameter',
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return new Promise<void>((resolver, rejector) => {
|
|
225
|
-
topics.forEach((topic) => {
|
|
226
|
-
const wsKey = getWsKeyForTopic(
|
|
227
|
-
this.options.market,
|
|
228
|
-
topic,
|
|
229
|
-
isPrivateTopic,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
// Persist topic for reconnects
|
|
233
|
-
this.wsStore.addTopic(wsKey, topic);
|
|
234
|
-
this.upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector);
|
|
235
|
-
|
|
236
|
-
// if connected, send subscription request
|
|
237
|
-
if (
|
|
238
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
|
|
239
|
-
) {
|
|
240
|
-
return this.requestSubscribeTopics(wsKey, [topic]);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
|
|
244
|
-
if (
|
|
245
|
-
!this.wsStore.isConnectionState(
|
|
246
|
-
wsKey,
|
|
247
|
-
WsConnectionStateEnum.CONNECTING,
|
|
248
|
-
) &&
|
|
249
|
-
!this.wsStore.isConnectionState(
|
|
250
|
-
wsKey,
|
|
251
|
-
WsConnectionStateEnum.RECONNECTING,
|
|
252
|
-
)
|
|
253
|
-
) {
|
|
254
|
-
return this.connect(wsKey);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private upsertPendingTopicsSubscriptions(
|
|
261
|
-
wsKey: string,
|
|
262
|
-
topic: string,
|
|
263
|
-
resolver: TopicsPendingSubscriptionsResolver,
|
|
264
|
-
rejector: TopicsPendingSubscriptionsRejector,
|
|
265
|
-
) {
|
|
266
|
-
const existingWsKeyPendingSubscriptions =
|
|
267
|
-
this.pendingTopicsSubscriptions.find((s) => s.wsKey === wsKey);
|
|
268
|
-
if (!existingWsKeyPendingSubscriptions) {
|
|
269
|
-
this.pendingTopicsSubscriptions.push({
|
|
270
|
-
wsKey,
|
|
271
|
-
resolver,
|
|
272
|
-
rejector,
|
|
273
|
-
failedTopicsSubscriptions: new Set(),
|
|
274
|
-
pendingTopicsSubscriptions: new Set([topic]),
|
|
275
|
-
});
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
existingWsKeyPendingSubscriptions.pendingTopicsSubscriptions.add(topic);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Unsubscribe from V5 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
|
|
284
|
-
* @param wsTopics - topic or list of topics
|
|
285
|
-
* @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics.
|
|
286
|
-
* @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
|
287
|
-
*/
|
|
288
|
-
public unsubscribeV5(
|
|
289
|
-
wsTopics: WsTopic[] | WsTopic,
|
|
290
|
-
category: CategoryV5,
|
|
291
|
-
isPrivateTopic?: boolean,
|
|
292
|
-
) {
|
|
293
|
-
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
|
294
|
-
topics.forEach((topic) => {
|
|
295
|
-
const wsKey = getWsKeyForTopic(
|
|
296
|
-
this.options.market,
|
|
297
|
-
topic,
|
|
298
|
-
isPrivateTopic,
|
|
299
|
-
category,
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
// Remove topic from persistence for reconnects
|
|
303
|
-
this.wsStore.deleteTopic(wsKey, topic);
|
|
304
|
-
this.removeTopicPendingSubscription(wsKey, topic);
|
|
305
|
-
|
|
306
|
-
// unsubscribe request only necessary if active connection exists
|
|
307
|
-
if (
|
|
308
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
|
|
309
|
-
) {
|
|
310
|
-
this.requestUnsubscribeTopics(wsKey, [topic]);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private removeTopicPendingSubscription(wsKey: string, topic: string) {
|
|
316
|
-
const existingWsKeyPendingSubscriptions =
|
|
317
|
-
this.pendingTopicsSubscriptions.find((s) => s.wsKey === wsKey);
|
|
318
|
-
if (existingWsKeyPendingSubscriptions) {
|
|
319
|
-
existingWsKeyPendingSubscriptions.pendingTopicsSubscriptions.delete(
|
|
320
|
-
topic,
|
|
321
|
-
);
|
|
322
|
-
if (!existingWsKeyPendingSubscriptions.pendingTopicsSubscriptions.size) {
|
|
323
|
-
this.pendingTopicsSubscriptions =
|
|
324
|
-
this.pendingTopicsSubscriptions.filter((s) => s.wsKey !== wsKey);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private clearTopicsPendingSubscriptions(wsKey: string) {
|
|
330
|
-
this.pendingTopicsSubscriptions = this.pendingTopicsSubscriptions.filter(
|
|
331
|
-
(s) => s.wsKey !== wsKey,
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Unsubscribe from V1-V3 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
|
|
337
|
-
*
|
|
338
|
-
* Note: For public V5 topics, use `unsubscribeV5()` instead!
|
|
339
|
-
*
|
|
340
|
-
* @param wsTopics topic or list of topics
|
|
341
|
-
* @param isPrivateTopic optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
|
342
|
-
*/
|
|
343
|
-
public unsubscribe(wsTopics: WsTopic[] | WsTopic, isPrivateTopic?: boolean) {
|
|
344
|
-
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
|
345
|
-
if (this.options.market === 'v5') {
|
|
346
|
-
topics.forEach((topic) => {
|
|
347
|
-
if (!isPrivateWsTopic(topic)) {
|
|
348
|
-
throw new Error(
|
|
349
|
-
'For public "v5" websocket topics, use the unsubscribeV5() method & provide the category parameter',
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
topics.forEach((topic) => {
|
|
356
|
-
const wsKey = getWsKeyForTopic(
|
|
357
|
-
this.options.market,
|
|
358
|
-
topic,
|
|
359
|
-
isPrivateTopic,
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
// Remove topic from persistence for reconnects
|
|
363
|
-
this.wsStore.deleteTopic(wsKey, topic);
|
|
364
|
-
this.removeTopicPendingSubscription(wsKey, topic);
|
|
365
|
-
|
|
366
|
-
// unsubscribe request only necessary if active connection exists
|
|
367
|
-
if (
|
|
368
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED)
|
|
369
|
-
) {
|
|
370
|
-
this.requestUnsubscribeTopics(wsKey, [topic]);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* @private Only used if we fetch exchange time before attempting auth. Disabled by default.
|
|
377
|
-
* I've removed this for ftx and it's working great, tempted to remove this here
|
|
378
|
-
*/
|
|
379
|
-
private prepareRESTClient(): void {
|
|
380
|
-
switch (this.options.market) {
|
|
381
|
-
case 'inverse': {
|
|
382
|
-
this.restClient = new InverseClient(
|
|
383
|
-
this.options.restOptions,
|
|
384
|
-
this.options.requestOptions,
|
|
385
|
-
);
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
case 'linear': {
|
|
389
|
-
this.restClient = new LinearClient(
|
|
390
|
-
this.options.restOptions,
|
|
391
|
-
this.options.requestOptions,
|
|
392
|
-
);
|
|
393
|
-
break;
|
|
394
|
-
}
|
|
395
|
-
case 'spot': {
|
|
396
|
-
this.restClient = new SpotClient(
|
|
397
|
-
this.options.restOptions,
|
|
398
|
-
this.options.requestOptions,
|
|
399
|
-
);
|
|
400
|
-
this.connectPublic();
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
case 'spotv3': {
|
|
404
|
-
this.restClient = new SpotClientV3(
|
|
405
|
-
this.options.restOptions,
|
|
406
|
-
this.options.requestOptions,
|
|
407
|
-
);
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
case 'usdcOption': {
|
|
411
|
-
this.restClient = new USDCOptionClient(
|
|
412
|
-
this.options.restOptions,
|
|
413
|
-
this.options.requestOptions,
|
|
414
|
-
);
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
case 'usdcPerp': {
|
|
418
|
-
this.restClient = new USDCPerpetualClient(
|
|
419
|
-
this.options.restOptions,
|
|
420
|
-
this.options.requestOptions,
|
|
421
|
-
);
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
case 'unifiedOption':
|
|
425
|
-
case 'unifiedPerp': {
|
|
426
|
-
this.restClient = new UnifiedMarginClient(
|
|
427
|
-
this.options.restOptions,
|
|
428
|
-
this.options.requestOptions,
|
|
429
|
-
);
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
case 'contractInverse':
|
|
433
|
-
case 'contractUSDT': {
|
|
434
|
-
this.restClient = new ContractClient(
|
|
435
|
-
this.options.restOptions,
|
|
436
|
-
this.options.requestOptions,
|
|
437
|
-
);
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
case 'v5': {
|
|
441
|
-
this.restClient = new RestClientV5(
|
|
442
|
-
this.options.restOptions,
|
|
443
|
-
this.options.requestOptions,
|
|
444
|
-
);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
default: {
|
|
448
|
-
throw neverGuard(
|
|
449
|
-
this.options.market,
|
|
450
|
-
'prepareRESTClient(): Unhandled market',
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
public close(wsKey: WsKey, force?: boolean) {
|
|
457
|
-
this.logger.info('Closing connection', { ...loggerCategory, wsKey });
|
|
458
|
-
this.setWsState(wsKey, WsConnectionStateEnum.CLOSING);
|
|
459
|
-
this.clearTimers(wsKey);
|
|
460
|
-
|
|
461
|
-
const ws = this.getWs(wsKey);
|
|
462
|
-
ws?.close();
|
|
463
|
-
if (force) {
|
|
464
|
-
ws?.terminate();
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
public closeAll(force?: boolean) {
|
|
469
|
-
const keys = this.wsStore.getKeys();
|
|
470
|
-
this.logger.info(`Closing all ws connections: ${keys}`);
|
|
471
|
-
keys.forEach((key) => {
|
|
472
|
-
this.close(key, force);
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
|
|
478
|
-
*/
|
|
479
|
-
public connectAll(): Promise<WebSocket | undefined>[] {
|
|
480
|
-
switch (this.options.market) {
|
|
481
|
-
case 'inverse': {
|
|
482
|
-
// only one for inverse
|
|
483
|
-
return [...this.connectPublic()];
|
|
484
|
-
}
|
|
485
|
-
// these all have separate public & private ws endpoints
|
|
486
|
-
case 'linear':
|
|
487
|
-
case 'spot':
|
|
488
|
-
case 'spotv3':
|
|
489
|
-
case 'usdcOption':
|
|
490
|
-
case 'usdcPerp':
|
|
491
|
-
case 'unifiedPerp':
|
|
492
|
-
case 'unifiedOption':
|
|
493
|
-
case 'contractUSDT':
|
|
494
|
-
case 'contractInverse': {
|
|
495
|
-
return [...this.connectPublic(), this.connectPrivate()];
|
|
496
|
-
}
|
|
497
|
-
case 'v5': {
|
|
498
|
-
return [this.connectPrivate()];
|
|
499
|
-
}
|
|
500
|
-
default: {
|
|
501
|
-
throw neverGuard(this.options.market, 'connectAll(): Unhandled market');
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
public connectPublic(): Promise<WebSocket | undefined>[] {
|
|
507
|
-
switch (this.options.market) {
|
|
508
|
-
case 'inverse': {
|
|
509
|
-
return [this.connect(WS_KEY_MAP.inverse)];
|
|
510
|
-
}
|
|
511
|
-
case 'linear': {
|
|
512
|
-
return [this.connect(WS_KEY_MAP.linearPublic)];
|
|
513
|
-
}
|
|
514
|
-
case 'spot': {
|
|
515
|
-
return [this.connect(WS_KEY_MAP.spotPublic)];
|
|
516
|
-
}
|
|
517
|
-
case 'spotv3': {
|
|
518
|
-
return [this.connect(WS_KEY_MAP.spotV3Public)];
|
|
519
|
-
}
|
|
520
|
-
case 'usdcOption': {
|
|
521
|
-
return [this.connect(WS_KEY_MAP.usdcOptionPublic)];
|
|
522
|
-
}
|
|
523
|
-
case 'usdcPerp': {
|
|
524
|
-
return [this.connect(WS_KEY_MAP.usdcPerpPublic)];
|
|
525
|
-
}
|
|
526
|
-
case 'unifiedOption': {
|
|
527
|
-
return [this.connect(WS_KEY_MAP.unifiedOptionPublic)];
|
|
528
|
-
}
|
|
529
|
-
case 'unifiedPerp': {
|
|
530
|
-
return [
|
|
531
|
-
this.connect(WS_KEY_MAP.unifiedPerpUSDTPublic),
|
|
532
|
-
this.connect(WS_KEY_MAP.unifiedPerpUSDCPublic),
|
|
533
|
-
];
|
|
534
|
-
}
|
|
535
|
-
case 'contractUSDT':
|
|
536
|
-
return [this.connect(WS_KEY_MAP.contractUSDTPublic)];
|
|
537
|
-
case 'contractInverse':
|
|
538
|
-
return [this.connect(WS_KEY_MAP.contractInversePublic)];
|
|
539
|
-
case 'v5': {
|
|
540
|
-
return [
|
|
541
|
-
this.connect(WS_KEY_MAP.v5SpotPublic),
|
|
542
|
-
this.connect(WS_KEY_MAP.v5LinearPublic),
|
|
543
|
-
this.connect(WS_KEY_MAP.v5InversePublic),
|
|
544
|
-
this.connect(WS_KEY_MAP.v5OptionPublic),
|
|
545
|
-
];
|
|
546
|
-
}
|
|
547
|
-
default: {
|
|
548
|
-
throw neverGuard(
|
|
549
|
-
this.options.market,
|
|
550
|
-
'connectPublic(): Unhandled market',
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
public connectPrivate(): Promise<WebSocket | undefined> {
|
|
557
|
-
switch (this.options.market) {
|
|
558
|
-
case 'inverse': {
|
|
559
|
-
return this.connect(WS_KEY_MAP.inverse);
|
|
560
|
-
}
|
|
561
|
-
case 'linear': {
|
|
562
|
-
return this.connect(WS_KEY_MAP.linearPrivate);
|
|
563
|
-
}
|
|
564
|
-
case 'spot': {
|
|
565
|
-
return this.connect(WS_KEY_MAP.spotPrivate);
|
|
566
|
-
}
|
|
567
|
-
case 'spotv3': {
|
|
568
|
-
return this.connect(WS_KEY_MAP.spotV3Private);
|
|
569
|
-
}
|
|
570
|
-
case 'usdcOption': {
|
|
571
|
-
return this.connect(WS_KEY_MAP.usdcOptionPrivate);
|
|
572
|
-
}
|
|
573
|
-
case 'usdcPerp': {
|
|
574
|
-
return this.connect(WS_KEY_MAP.usdcPerpPrivate);
|
|
575
|
-
}
|
|
576
|
-
case 'unifiedPerp':
|
|
577
|
-
case 'unifiedOption': {
|
|
578
|
-
return this.connect(WS_KEY_MAP.unifiedPrivate);
|
|
579
|
-
}
|
|
580
|
-
case 'contractUSDT':
|
|
581
|
-
return this.connect(WS_KEY_MAP.contractUSDTPrivate);
|
|
582
|
-
case 'contractInverse':
|
|
583
|
-
return this.connect(WS_KEY_MAP.contractInversePrivate);
|
|
584
|
-
case 'v5': {
|
|
585
|
-
return this.connect(WS_KEY_MAP.v5Private);
|
|
586
|
-
}
|
|
587
|
-
default: {
|
|
588
|
-
throw neverGuard(
|
|
589
|
-
this.options.market,
|
|
590
|
-
'connectPrivate(): Unhandled market',
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
private async connect(wsKey: WsKey): Promise<WebSocket | undefined> {
|
|
597
|
-
try {
|
|
598
|
-
if (this.wsStore.isWsOpen(wsKey)) {
|
|
599
|
-
this.logger.error(
|
|
600
|
-
'Refused to connect to ws with existing active connection',
|
|
601
|
-
{ ...loggerCategory, wsKey },
|
|
602
|
-
);
|
|
603
|
-
return this.wsStore.getWs(wsKey);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
|
|
608
|
-
) {
|
|
609
|
-
this.logger.error(
|
|
610
|
-
'Refused to connect to ws, connection attempt already active',
|
|
611
|
-
{ ...loggerCategory, wsKey },
|
|
612
|
-
);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
!this.wsStore.getConnectionState(wsKey) ||
|
|
618
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.INITIAL)
|
|
619
|
-
) {
|
|
620
|
-
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTING);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const authParams = await this.getAuthParams(wsKey);
|
|
624
|
-
const url = getWsUrl(
|
|
625
|
-
wsKey,
|
|
626
|
-
this.options.wsUrl,
|
|
627
|
-
this.isTestnet(),
|
|
628
|
-
this.logger,
|
|
629
|
-
);
|
|
630
|
-
const ws = this.connectToWsUrl(url + authParams, wsKey);
|
|
631
|
-
|
|
632
|
-
return this.wsStore.setWs(wsKey, ws);
|
|
633
|
-
} catch (err) {
|
|
634
|
-
this.parseWsError('Connection failed', err, wsKey);
|
|
635
|
-
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
private parseWsError(context: string, error: any, wsKey: WsKey) {
|
|
640
|
-
if (!error.message) {
|
|
641
|
-
this.logger.error(`${context} due to unexpected error: `, error);
|
|
642
|
-
this.emit('error', error);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
switch (error.message) {
|
|
647
|
-
case 'Unexpected server response: 401':
|
|
648
|
-
this.logger.error(`${context} due to 401 authorization failure.`, {
|
|
649
|
-
...loggerCategory,
|
|
650
|
-
wsKey,
|
|
651
|
-
});
|
|
652
|
-
break;
|
|
653
|
-
|
|
654
|
-
default:
|
|
655
|
-
if (
|
|
656
|
-
this.wsStore.getConnectionState(wsKey) !==
|
|
657
|
-
WsConnectionStateEnum.CLOSING
|
|
658
|
-
) {
|
|
659
|
-
this.logger.error(
|
|
660
|
-
`${context} due to unexpected response error: "${
|
|
661
|
-
error?.msg || error?.message || error
|
|
662
|
-
}"`,
|
|
663
|
-
{ ...loggerCategory, wsKey, error },
|
|
664
|
-
);
|
|
665
|
-
this.executeReconnectableClose(wsKey, 'unhandled onWsError');
|
|
666
|
-
} else {
|
|
667
|
-
this.logger.info(
|
|
668
|
-
`${wsKey} socket forcefully closed. Will not reconnect.`,
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
break;
|
|
672
|
-
}
|
|
673
|
-
this.emit('error', error);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Return params required to make authorized request
|
|
678
|
-
*/
|
|
679
|
-
private async getAuthParams(wsKey: WsKey): Promise<string> {
|
|
680
|
-
if (PUBLIC_WS_KEYS.includes(wsKey)) {
|
|
681
|
-
this.logger.debug('Starting public only websocket client.', {
|
|
682
|
-
...loggerCategory,
|
|
683
|
-
wsKey,
|
|
684
|
-
});
|
|
685
|
-
return '';
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
|
690
|
-
|
|
691
|
-
const authParams = {
|
|
692
|
-
api_key: this.options.key,
|
|
693
|
-
expires: expiresAt,
|
|
694
|
-
signature,
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
return '?' + serializeParams(authParams);
|
|
698
|
-
} catch (e) {
|
|
699
|
-
this.logger.error(e, { ...loggerCategory, wsKey });
|
|
700
|
-
return '';
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
private async sendAuthRequest(wsKey: WsKey): Promise<void> {
|
|
705
|
-
try {
|
|
706
|
-
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
|
|
707
|
-
|
|
708
|
-
const request = {
|
|
709
|
-
op: 'auth',
|
|
710
|
-
args: [this.options.key, expiresAt, signature],
|
|
711
|
-
req_id: `${wsKey}-auth`,
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
return this.tryWsSend(wsKey, JSON.stringify(request));
|
|
715
|
-
} catch (e) {
|
|
716
|
-
this.logger.error(e, { ...loggerCategory, wsKey });
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
private async getWsAuthSignature(
|
|
721
|
-
wsKey: WsKey,
|
|
722
|
-
): Promise<{ expiresAt: number; signature: string }> {
|
|
723
|
-
const { key, secret } = this.options;
|
|
724
|
-
|
|
725
|
-
if (!key || !secret) {
|
|
726
|
-
this.logger.warning(
|
|
727
|
-
'Cannot authenticate websocket, either api or private keys missing.',
|
|
728
|
-
{ ...loggerCategory, wsKey },
|
|
729
|
-
);
|
|
730
|
-
throw new Error('Cannot auth - missing api or secret in config');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
this.logger.debug("Getting auth'd request params", {
|
|
734
|
-
...loggerCategory,
|
|
735
|
-
wsKey,
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
const timeOffset = this.options.fetchTimeOffsetBeforeAuth
|
|
739
|
-
? (await this.restClient?.fetchTimeOffset()) || 0
|
|
740
|
-
: 0;
|
|
741
|
-
|
|
742
|
-
const signatureExpiresAt = Date.now() + timeOffset + 5000;
|
|
743
|
-
|
|
744
|
-
const signature = await signMessage(
|
|
745
|
-
'GET/realtime' + signatureExpiresAt,
|
|
746
|
-
secret,
|
|
747
|
-
);
|
|
748
|
-
|
|
749
|
-
return {
|
|
750
|
-
expiresAt: signatureExpiresAt,
|
|
751
|
-
signature,
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
private reconnectWithDelay(wsKey: WsKey, connectionDelayMs: number) {
|
|
756
|
-
this.clearTimers(wsKey);
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
this.wsStore.getConnectionState(wsKey) !==
|
|
760
|
-
WsConnectionStateEnum.CONNECTING
|
|
761
|
-
) {
|
|
762
|
-
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (this.wsStore.get(wsKey)?.activeReconnectTimer) {
|
|
766
|
-
this.clearReconnectTimer(wsKey);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
|
|
770
|
-
this.logger.info('Reconnecting to websocket', {
|
|
771
|
-
...loggerCategory,
|
|
772
|
-
wsKey,
|
|
773
|
-
});
|
|
774
|
-
this.clearReconnectTimer(wsKey);
|
|
775
|
-
this.connect(wsKey);
|
|
776
|
-
}, connectionDelayMs);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
private ping(wsKey: WsKey) {
|
|
780
|
-
if (this.wsStore.get(wsKey, true).activePongTimer) {
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
this.clearPongTimer(wsKey);
|
|
785
|
-
|
|
786
|
-
this.logger.silly('Sending ping', { ...loggerCategory, wsKey });
|
|
787
|
-
this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' }));
|
|
788
|
-
|
|
789
|
-
this.wsStore.get(wsKey, true).activePongTimer = setTimeout(
|
|
790
|
-
() => this.executeReconnectableClose(wsKey, 'Pong timeout'),
|
|
791
|
-
this.options.pongTimeout,
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
|
|
797
|
-
* If closed, trigger a reconnect immediately
|
|
798
|
-
*/
|
|
799
|
-
private executeReconnectableClose(wsKey: WsKey, reason: string) {
|
|
800
|
-
this.logger.info(`${reason} - closing socket to reconnect`, {
|
|
801
|
-
...loggerCategory,
|
|
802
|
-
wsKey,
|
|
803
|
-
reason,
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
const wasOpen = this.wsStore.isWsOpen(wsKey);
|
|
807
|
-
|
|
808
|
-
this.getWs(wsKey)?.terminate();
|
|
809
|
-
delete this.wsStore.get(wsKey, true).activePongTimer;
|
|
810
|
-
this.clearPingTimer(wsKey);
|
|
811
|
-
this.clearPongTimer(wsKey);
|
|
812
|
-
|
|
813
|
-
if (!wasOpen) {
|
|
814
|
-
this.logger.info(
|
|
815
|
-
`${reason} - socket already closed - trigger immediate reconnect`,
|
|
816
|
-
{
|
|
817
|
-
...loggerCategory,
|
|
818
|
-
wsKey,
|
|
819
|
-
reason,
|
|
820
|
-
},
|
|
821
|
-
);
|
|
822
|
-
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
private clearTimers(wsKey: WsKey) {
|
|
827
|
-
this.clearPingTimer(wsKey);
|
|
828
|
-
this.clearPongTimer(wsKey);
|
|
829
|
-
this.clearReconnectTimer(wsKey);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Send a ping at intervals
|
|
833
|
-
private clearPingTimer(wsKey: WsKey) {
|
|
834
|
-
const wsState = this.wsStore.get(wsKey);
|
|
835
|
-
if (wsState?.activePingTimer) {
|
|
836
|
-
clearInterval(wsState.activePingTimer);
|
|
837
|
-
wsState.activePingTimer = undefined;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Expect a pong within a time limit
|
|
842
|
-
private clearPongTimer(wsKey: WsKey) {
|
|
843
|
-
const wsState = this.wsStore.get(wsKey);
|
|
844
|
-
if (wsState?.activePongTimer) {
|
|
845
|
-
clearTimeout(wsState.activePongTimer);
|
|
846
|
-
wsState.activePongTimer = undefined;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
private clearReconnectTimer(wsKey: WsKey) {
|
|
851
|
-
const wsState = this.wsStore.get(wsKey);
|
|
852
|
-
if (wsState?.activeReconnectTimer) {
|
|
853
|
-
clearTimeout(wsState.activeReconnectTimer);
|
|
854
|
-
wsState.activeReconnectTimer = undefined;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
/**
|
|
859
|
-
* @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics.
|
|
860
|
-
*/
|
|
861
|
-
private requestSubscribeTopics(wsKey: WsKey, topics: string[]) {
|
|
862
|
-
if (!topics.length) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const maxTopicsPerEvent = getMaxTopicsPerSubscribeEvent(
|
|
867
|
-
this.options.market,
|
|
868
|
-
wsKey,
|
|
869
|
-
);
|
|
870
|
-
if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) {
|
|
871
|
-
this.logger.silly(
|
|
872
|
-
`Subscribing to topics in batches of ${maxTopicsPerEvent}`,
|
|
873
|
-
);
|
|
874
|
-
for (let i = 0; i < topics.length; i += maxTopicsPerEvent) {
|
|
875
|
-
const batch = topics.slice(i, i + maxTopicsPerEvent);
|
|
876
|
-
this.logger.silly(`Subscribing to batch of ${batch.length}`);
|
|
877
|
-
this.requestSubscribeTopics(wsKey, batch);
|
|
878
|
-
}
|
|
879
|
-
this.logger.silly(
|
|
880
|
-
`Finished batch subscribing to ${topics.length} topics`,
|
|
881
|
-
);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const wsMessage = JSON.stringify({
|
|
886
|
-
req_id: topics.join(','),
|
|
887
|
-
op: 'subscribe',
|
|
888
|
-
args: topics,
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
this.tryWsSend(wsKey, wsMessage);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics.
|
|
896
|
-
*/
|
|
897
|
-
private requestUnsubscribeTopics(wsKey: WsKey, topics: string[]) {
|
|
898
|
-
if (!topics.length) {
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const maxTopicsPerEvent = getMaxTopicsPerSubscribeEvent(
|
|
903
|
-
this.options.market,
|
|
904
|
-
wsKey,
|
|
905
|
-
);
|
|
906
|
-
if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) {
|
|
907
|
-
this.logger.silly(
|
|
908
|
-
`Unsubscribing to topics in batches of ${maxTopicsPerEvent}`,
|
|
909
|
-
);
|
|
910
|
-
for (let i = 0; i < topics.length; i += maxTopicsPerEvent) {
|
|
911
|
-
const batch = topics.slice(i, i + maxTopicsPerEvent);
|
|
912
|
-
this.logger.silly(`Unsubscribing to batch of ${batch.length}`);
|
|
913
|
-
this.requestUnsubscribeTopics(wsKey, batch);
|
|
914
|
-
}
|
|
915
|
-
this.logger.silly(
|
|
916
|
-
`Finished batch unsubscribing to ${topics.length} topics`,
|
|
917
|
-
);
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const wsMessage = JSON.stringify({
|
|
922
|
-
op: 'unsubscribe',
|
|
923
|
-
args: topics,
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
this.tryWsSend(wsKey, wsMessage);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
public tryWsSend(wsKey: WsKey, wsMessage: string) {
|
|
930
|
-
try {
|
|
931
|
-
this.logger.silly('Sending upstream ws message: ', {
|
|
932
|
-
...loggerCategory,
|
|
933
|
-
wsMessage,
|
|
934
|
-
wsKey,
|
|
935
|
-
});
|
|
936
|
-
if (!wsKey) {
|
|
937
|
-
throw new Error(
|
|
938
|
-
'Cannot send message due to no known websocket for this wsKey',
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
const ws = this.getWs(wsKey);
|
|
942
|
-
if (!ws) {
|
|
943
|
-
throw new Error(
|
|
944
|
-
`${wsKey} socket not connected yet, call "connect(${wsKey}) first then try again when the "open" event arrives`,
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
ws.send(wsMessage);
|
|
948
|
-
} catch (e) {
|
|
949
|
-
this.logger.error('Failed to send WS message', {
|
|
950
|
-
...loggerCategory,
|
|
951
|
-
wsMessage,
|
|
952
|
-
wsKey,
|
|
953
|
-
exception: e,
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
private connectToWsUrl(url: string, wsKey: WsKey): WebSocket {
|
|
959
|
-
this.logger.silly(`Opening WS connection to URL: ${url}`, {
|
|
960
|
-
...loggerCategory,
|
|
961
|
-
wsKey,
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
const agent = this.options.requestOptions?.agent;
|
|
965
|
-
const ws = new WebSocket(url, undefined, agent ? { agent } : undefined);
|
|
966
|
-
ws.onopen = (event) => this.onWsOpen(event, wsKey);
|
|
967
|
-
ws.onmessage = (event) => this.onWsMessage(event, wsKey);
|
|
968
|
-
ws.onerror = (event) =>
|
|
969
|
-
this.parseWsError('Websocket onWsError', event, wsKey);
|
|
970
|
-
ws.onclose = (event) => this.onWsClose(event, wsKey);
|
|
971
|
-
|
|
972
|
-
return ws;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private async onWsOpen(event, wsKey: WsKey) {
|
|
976
|
-
if (
|
|
977
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)
|
|
978
|
-
) {
|
|
979
|
-
this.logger.info('Websocket connected', {
|
|
980
|
-
...loggerCategory,
|
|
981
|
-
wsKey,
|
|
982
|
-
testnet: this.isTestnet(),
|
|
983
|
-
market: this.options.market,
|
|
984
|
-
});
|
|
985
|
-
this.emit('open', { wsKey, event });
|
|
986
|
-
} else if (
|
|
987
|
-
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING)
|
|
988
|
-
) {
|
|
989
|
-
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
|
|
990
|
-
this.emit('reconnected', { wsKey, event });
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
994
|
-
|
|
995
|
-
// Some websockets require an auth packet to be sent after opening the connection
|
|
996
|
-
if (WS_AUTH_ON_CONNECT_KEYS.includes(wsKey)) {
|
|
997
|
-
this.logger.info('Sending auth request...');
|
|
998
|
-
await this.sendAuthRequest(wsKey);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// TODO: persistence not working yet for spot v1 topics
|
|
1002
|
-
if (wsKey !== WS_KEY_MAP.spotPublic && wsKey !== WS_KEY_MAP.spotPrivate) {
|
|
1003
|
-
const topics = [...this.wsStore.getTopics(wsKey)];
|
|
1004
|
-
this.logger.info('Subscribing to topics', {
|
|
1005
|
-
...loggerCategory,
|
|
1006
|
-
wsKey,
|
|
1007
|
-
topics,
|
|
1008
|
-
});
|
|
1009
|
-
this.requestSubscribeTopics(wsKey, topics);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
this.wsStore.get(wsKey, true)!.activePingTimer = setInterval(
|
|
1013
|
-
() => this.ping(wsKey),
|
|
1014
|
-
this.options.pingInterval,
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
private onWsMessage(event, wsKey: WsKey) {
|
|
1019
|
-
try {
|
|
1020
|
-
// any message can clear the pong timer - wouldn't get a message if the ws dropped
|
|
1021
|
-
this.clearPongTimer(wsKey);
|
|
1022
|
-
|
|
1023
|
-
const msg = JSON.parse((event && event.data) || event);
|
|
1024
|
-
// this.logger.silly('Received event', {
|
|
1025
|
-
// ...loggerCategory,
|
|
1026
|
-
// wsKey,
|
|
1027
|
-
// msg: JSON.stringify(msg),
|
|
1028
|
-
// });
|
|
1029
|
-
|
|
1030
|
-
if (isTopicSubscriptionConfirmation(msg)) {
|
|
1031
|
-
this.updatePendingTopicSubscriptionStatus(wsKey, msg);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// TODO: cleanme
|
|
1035
|
-
if (msg['success'] || msg?.pong || isWsPong(msg)) {
|
|
1036
|
-
if (isWsPong(msg)) {
|
|
1037
|
-
this.logger.silly('Received pong', { ...loggerCategory, wsKey });
|
|
1038
|
-
} else {
|
|
1039
|
-
this.emit('response', { ...msg, wsKey });
|
|
1040
|
-
}
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (msg['finalFragment']) {
|
|
1045
|
-
return this.emit('response', { ...msg, wsKey });
|
|
1046
|
-
}
|
|
1047
|
-
if (msg?.topic) {
|
|
1048
|
-
return this.emit('update', { ...msg, wsKey });
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
// spot v1
|
|
1053
|
-
msg?.code ||
|
|
1054
|
-
// spot v3
|
|
1055
|
-
msg?.type === 'error' ||
|
|
1056
|
-
// usdc options
|
|
1057
|
-
msg?.success === false
|
|
1058
|
-
) {
|
|
1059
|
-
return this.emit('error', { ...msg, wsKey });
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
this.logger.warning('Unhandled/unrecognised ws event message', {
|
|
1063
|
-
...loggerCategory,
|
|
1064
|
-
message: msg,
|
|
1065
|
-
event,
|
|
1066
|
-
wsKey,
|
|
1067
|
-
});
|
|
1068
|
-
} catch (e) {
|
|
1069
|
-
this.logger.error('Failed to parse ws event message', {
|
|
1070
|
-
...loggerCategory,
|
|
1071
|
-
error: e,
|
|
1072
|
-
event,
|
|
1073
|
-
wsKey,
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
private updatePendingTopicSubscriptionStatus(
|
|
1079
|
-
wsKey: string,
|
|
1080
|
-
msg: WebsocketTopicSubscriptionConfirmationEvent,
|
|
1081
|
-
) {
|
|
1082
|
-
const requestsIds = msg.req_id as string;
|
|
1083
|
-
const pendingTopicsSubscriptions = this.pendingTopicsSubscriptions.find(
|
|
1084
|
-
(s) => s.wsKey === wsKey,
|
|
1085
|
-
);
|
|
1086
|
-
|
|
1087
|
-
if (!pendingTopicsSubscriptions) return;
|
|
1088
|
-
|
|
1089
|
-
const splitRequestsIds = requestsIds.split(',');
|
|
1090
|
-
if (!isTopicSubscriptionSuccess(msg)) {
|
|
1091
|
-
splitRequestsIds.forEach((req_id) =>
|
|
1092
|
-
pendingTopicsSubscriptions.failedTopicsSubscriptions.add(req_id),
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
splitRequestsIds.forEach((req_id) => {
|
|
1097
|
-
this.removeTopicPendingSubscription(wsKey, req_id);
|
|
1098
|
-
|
|
1099
|
-
if (
|
|
1100
|
-
!pendingTopicsSubscriptions.pendingTopicsSubscriptions.size &&
|
|
1101
|
-
!pendingTopicsSubscriptions.failedTopicsSubscriptions.size
|
|
1102
|
-
) {
|
|
1103
|
-
// all topics have been subscribed successfully, so we can resolve the subscription request
|
|
1104
|
-
pendingTopicsSubscriptions.resolver();
|
|
1105
|
-
this.clearTopicsPendingSubscriptions(wsKey);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
if (
|
|
1109
|
-
!pendingTopicsSubscriptions.pendingTopicsSubscriptions.size &&
|
|
1110
|
-
pendingTopicsSubscriptions.failedTopicsSubscriptions.size
|
|
1111
|
-
) {
|
|
1112
|
-
// not all topics have been subscribed successfully, so we reject the subscription request
|
|
1113
|
-
// and let the caller handle the situation by providing the list of failed subscriptions requests
|
|
1114
|
-
const failedSubscriptionsMessage = `(${[
|
|
1115
|
-
...pendingTopicsSubscriptions.failedTopicsSubscriptions,
|
|
1116
|
-
].toString()}) failed to subscribe`;
|
|
1117
|
-
pendingTopicsSubscriptions.rejector(failedSubscriptionsMessage);
|
|
1118
|
-
this.clearTopicsPendingSubscriptions(wsKey);
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
private onWsClose(event, wsKey: WsKey) {
|
|
1124
|
-
this.logger.info('Websocket connection closed', {
|
|
1125
|
-
...loggerCategory,
|
|
1126
|
-
wsKey,
|
|
1127
|
-
});
|
|
1128
|
-
|
|
1129
|
-
if (
|
|
1130
|
-
this.wsStore.getConnectionState(wsKey) !== WsConnectionStateEnum.CLOSING
|
|
1131
|
-
) {
|
|
1132
|
-
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
|
|
1133
|
-
this.emit('reconnect', { wsKey, event });
|
|
1134
|
-
} else {
|
|
1135
|
-
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
|
|
1136
|
-
this.emit('close', { wsKey, event });
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
private getWs(wsKey: WsKey) {
|
|
1141
|
-
return this.wsStore.getWs(wsKey);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
private setWsState(wsKey: WsKey, state: WsConnectionStateEnum) {
|
|
1145
|
-
this.wsStore.setConnectionState(wsKey, state);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
private wrongMarketError(market: APIMarket) {
|
|
1149
|
-
return new Error(
|
|
1150
|
-
`This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}'" to listen to ${market} topics`,
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
/** @deprecated use "market: 'spotv3" client */
|
|
1155
|
-
public subscribePublicSpotTrades(symbol: string, binary?: boolean) {
|
|
1156
|
-
if (this.options.market !== 'spot') {
|
|
1157
|
-
throw this.wrongMarketError('spot');
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
return this.tryWsSend(
|
|
1161
|
-
WS_KEY_MAP.spotPublic,
|
|
1162
|
-
JSON.stringify({
|
|
1163
|
-
topic: 'trade',
|
|
1164
|
-
event: 'sub',
|
|
1165
|
-
symbol,
|
|
1166
|
-
params: {
|
|
1167
|
-
binary: !!binary,
|
|
1168
|
-
},
|
|
1169
|
-
}),
|
|
1170
|
-
);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
/** @deprecated use "market: 'spotv3" client */
|
|
1174
|
-
public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) {
|
|
1175
|
-
if (this.options.market !== 'spot') {
|
|
1176
|
-
throw this.wrongMarketError('spot');
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
return this.tryWsSend(
|
|
1180
|
-
WS_KEY_MAP.spotPublic,
|
|
1181
|
-
JSON.stringify({
|
|
1182
|
-
symbol,
|
|
1183
|
-
topic: 'realtimes',
|
|
1184
|
-
event: 'sub',
|
|
1185
|
-
params: {
|
|
1186
|
-
binary: !!binary,
|
|
1187
|
-
},
|
|
1188
|
-
}),
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
/** @deprecated use "market: 'spotv3" client */
|
|
1193
|
-
public subscribePublicSpotV1Kline(
|
|
1194
|
-
symbol: string,
|
|
1195
|
-
candleSize: KlineInterval,
|
|
1196
|
-
binary?: boolean,
|
|
1197
|
-
) {
|
|
1198
|
-
if (this.options.market !== 'spot') {
|
|
1199
|
-
throw this.wrongMarketError('spot');
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
return this.tryWsSend(
|
|
1203
|
-
WS_KEY_MAP.spotPublic,
|
|
1204
|
-
JSON.stringify({
|
|
1205
|
-
symbol,
|
|
1206
|
-
topic: 'kline_' + candleSize,
|
|
1207
|
-
event: 'sub',
|
|
1208
|
-
params: {
|
|
1209
|
-
binary: !!binary,
|
|
1210
|
-
},
|
|
1211
|
-
}),
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
//ws.send('{"symbol":"BTCUSDT","topic":"depth","event":"sub","params":{"binary":false}}');
|
|
1216
|
-
//ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}');
|
|
1217
|
-
//ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}');
|
|
1218
|
-
|
|
1219
|
-
/** @deprecated use "market: 'spotv3" client */
|
|
1220
|
-
public subscribePublicSpotOrderbook(
|
|
1221
|
-
symbol: string,
|
|
1222
|
-
depth: 'full' | 'merge' | 'delta',
|
|
1223
|
-
dumpScale?: number,
|
|
1224
|
-
binary?: boolean,
|
|
1225
|
-
) {
|
|
1226
|
-
if (this.options.market !== 'spot') {
|
|
1227
|
-
throw this.wrongMarketError('spot');
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
let topic: string;
|
|
1231
|
-
switch (depth) {
|
|
1232
|
-
case 'full': {
|
|
1233
|
-
topic = 'depth';
|
|
1234
|
-
break;
|
|
1235
|
-
}
|
|
1236
|
-
case 'merge': {
|
|
1237
|
-
topic = 'mergedDepth';
|
|
1238
|
-
if (!dumpScale) {
|
|
1239
|
-
throw new Error('Dumpscale must be provided for merged orderbooks');
|
|
1240
|
-
}
|
|
1241
|
-
break;
|
|
1242
|
-
}
|
|
1243
|
-
case 'delta': {
|
|
1244
|
-
topic = 'diffDepth';
|
|
1245
|
-
break;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
const msg: any = {
|
|
1250
|
-
symbol,
|
|
1251
|
-
topic,
|
|
1252
|
-
event: 'sub',
|
|
1253
|
-
params: {
|
|
1254
|
-
binary: !!binary,
|
|
1255
|
-
},
|
|
1256
|
-
};
|
|
1257
|
-
if (dumpScale) {
|
|
1258
|
-
msg.params.dumpScale = dumpScale;
|
|
1259
|
-
}
|
|
1260
|
-
return this.tryWsSend(WS_KEY_MAP.spotPublic, JSON.stringify(msg));
|
|
1261
|
-
}
|
|
1262
|
-
}
|