bybit-api 3.10.30 → 4.0.0-beta.1

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.
Files changed (146) hide show
  1. package/README.md +301 -178
  2. package/lib/index.d.ts +0 -11
  3. package/lib/index.js +0 -11
  4. package/lib/index.js.map +1 -1
  5. package/lib/rest-client-v5.d.ts +1 -1
  6. package/lib/rest-client-v5.js +1 -1
  7. package/lib/spot-client-v3.d.ts +8 -81
  8. package/lib/spot-client-v3.js +7 -172
  9. package/lib/spot-client-v3.js.map +1 -1
  10. package/lib/types/index.d.ts +1 -2
  11. package/lib/types/index.js +1 -2
  12. package/lib/types/index.js.map +1 -1
  13. package/lib/types/request/index.d.ts +0 -1
  14. package/lib/types/request/index.js +0 -1
  15. package/lib/types/request/index.js.map +1 -1
  16. package/lib/types/request/v5-account.d.ts +1 -1
  17. package/lib/types/request/v5-asset.d.ts +1 -1
  18. package/lib/types/request/v5-market.d.ts +1 -1
  19. package/lib/types/request/v5-position.d.ts +1 -1
  20. package/lib/types/request/v5-pre-upgrade.d.ts +1 -1
  21. package/lib/types/request/v5-spot-leverage-token.d.ts +1 -1
  22. package/lib/types/request/v5-trade.d.ts +1 -1
  23. package/lib/types/request/v5-user.d.ts +1 -1
  24. package/lib/types/response/v5-account.d.ts +1 -1
  25. package/lib/types/response/v5-asset.d.ts +1 -1
  26. package/lib/types/response/v5-market.d.ts +1 -1
  27. package/lib/types/response/v5-position.d.ts +1 -1
  28. package/lib/types/response/v5-spot-leverage-token.d.ts +1 -1
  29. package/lib/types/response/v5-trade.d.ts +1 -1
  30. package/lib/types/response/v5-user.d.ts +1 -1
  31. package/lib/types/{request/spot.js → shared-v5.js} +1 -1
  32. package/lib/types/shared-v5.js.map +1 -0
  33. package/lib/types/shared.d.ts +1 -19
  34. package/lib/types/websockets/index.d.ts +3 -0
  35. package/lib/types/websockets/index.js +20 -0
  36. package/lib/types/websockets/index.js.map +1 -0
  37. package/lib/types/websockets/ws-api.d.ts +73 -0
  38. package/lib/types/websockets/ws-api.js +10 -0
  39. package/lib/types/websockets/ws-api.js.map +1 -0
  40. package/lib/types/websockets/ws-confirmations.d.ts +13 -0
  41. package/lib/types/{websocket.events.js → websockets/ws-confirmations.js} +1 -1
  42. package/lib/types/websockets/ws-confirmations.js.map +1 -0
  43. package/lib/types/{websocket.events.d.ts → websockets/ws-events.d.ts} +9 -3
  44. package/lib/types/websockets/ws-events.js +11 -0
  45. package/lib/types/websockets/ws-events.js.map +1 -0
  46. package/lib/types/{websockets.d.ts → websockets/ws-general.d.ts} +35 -10
  47. package/lib/types/{v5-shared.js → websockets/ws-general.js} +1 -1
  48. package/lib/types/websockets/ws-general.js.map +1 -0
  49. package/lib/util/BaseRestClient.d.ts +1 -1
  50. package/lib/util/BaseRestClient.js +15 -39
  51. package/lib/util/BaseRestClient.js.map +1 -1
  52. package/lib/util/BaseWSClient.d.ts +221 -0
  53. package/lib/util/BaseWSClient.js +733 -0
  54. package/lib/util/BaseWSClient.js.map +1 -0
  55. package/lib/util/index.d.ts +1 -2
  56. package/lib/util/index.js +1 -2
  57. package/lib/util/index.js.map +1 -1
  58. package/lib/util/logger.d.ts +2 -5
  59. package/lib/util/logger.js +3 -12
  60. package/lib/util/logger.js.map +1 -1
  61. package/lib/util/requestUtils.d.ts +7 -10
  62. package/lib/util/requestUtils.js +6 -33
  63. package/lib/util/requestUtils.js.map +1 -1
  64. package/lib/util/typeGuards.d.ts +7 -1
  65. package/lib/util/typeGuards.js +41 -5
  66. package/lib/util/typeGuards.js.map +1 -1
  67. package/lib/util/webCryptoAPI.d.ts +10 -0
  68. package/lib/util/webCryptoAPI.js +69 -0
  69. package/lib/util/webCryptoAPI.js.map +1 -0
  70. package/lib/util/websockets/WsStore.d.ts +71 -0
  71. package/lib/util/websockets/WsStore.js +269 -0
  72. package/lib/util/websockets/WsStore.js.map +1 -0
  73. package/lib/util/websockets/WsStore.types.d.ts +50 -0
  74. package/lib/util/websockets/WsStore.types.js +14 -0
  75. package/lib/util/websockets/WsStore.types.js.map +1 -0
  76. package/lib/util/websockets/index.d.ts +3 -0
  77. package/lib/util/websockets/index.js +20 -0
  78. package/lib/util/websockets/index.js.map +1 -0
  79. package/lib/util/websockets/websocket-util.d.ts +92 -0
  80. package/lib/util/websockets/websocket-util.js +280 -0
  81. package/lib/util/websockets/websocket-util.js.map +1 -0
  82. package/lib/websocket-client.d.ts +107 -114
  83. package/lib/websocket-client.js +427 -716
  84. package/lib/websocket-client.js.map +1 -1
  85. package/package.json +4 -4
  86. package/lib/account-asset-client-v3.d.ts +0 -87
  87. package/lib/account-asset-client-v3.js +0 -154
  88. package/lib/account-asset-client-v3.js.map +0 -1
  89. package/lib/account-asset-client.d.ts +0 -46
  90. package/lib/account-asset-client.js +0 -108
  91. package/lib/account-asset-client.js.map +0 -1
  92. package/lib/contract-client.d.ts +0 -136
  93. package/lib/contract-client.js +0 -251
  94. package/lib/contract-client.js.map +0 -1
  95. package/lib/copy-trading-client.d.ts +0 -60
  96. package/lib/copy-trading-client.js +0 -114
  97. package/lib/copy-trading-client.js.map +0 -1
  98. package/lib/inverse-client.d.ts +0 -104
  99. package/lib/inverse-client.js +0 -213
  100. package/lib/inverse-client.js.map +0 -1
  101. package/lib/inverse-futures-client.d.ts +0 -217
  102. package/lib/inverse-futures-client.js +0 -224
  103. package/lib/inverse-futures-client.js.map +0 -1
  104. package/lib/linear-client.d.ts +0 -112
  105. package/lib/linear-client.js +0 -225
  106. package/lib/linear-client.js.map +0 -1
  107. package/lib/spot-client.d.ts +0 -46
  108. package/lib/spot-client.js +0 -131
  109. package/lib/spot-client.js.map +0 -1
  110. package/lib/types/request/spot.d.ts +0 -79
  111. package/lib/types/request/spot.js.map +0 -1
  112. package/lib/types/v5-shared.js.map +0 -1
  113. package/lib/types/websocket.events.js.map +0 -1
  114. package/lib/types/websockets.js +0 -3
  115. package/lib/types/websockets.js.map +0 -1
  116. package/lib/types/ws-events/failed-topic-subscription-confirmation.d.ts +0 -4
  117. package/lib/types/ws-events/failed-topic-subscription-confirmation.js +0 -3
  118. package/lib/types/ws-events/failed-topic-subscription-confirmation.js.map +0 -1
  119. package/lib/types/ws-events/succeeded-topic-subscription-confirmation.d.ts +0 -4
  120. package/lib/types/ws-events/succeeded-topic-subscription-confirmation.js +0 -3
  121. package/lib/types/ws-events/succeeded-topic-subscription-confirmation.js.map +0 -1
  122. package/lib/types/ws-events/topic-subscription-confirmation.d.ts +0 -7
  123. package/lib/types/ws-events/topic-subscription-confirmation.js +0 -3
  124. package/lib/types/ws-events/topic-subscription-confirmation.js.map +0 -1
  125. package/lib/unified-margin-client.d.ts +0 -140
  126. package/lib/unified-margin-client.js +0 -270
  127. package/lib/unified-margin-client.js.map +0 -1
  128. package/lib/usdc-option-client.d.ts +0 -110
  129. package/lib/usdc-option-client.js +0 -196
  130. package/lib/usdc-option-client.js.map +0 -1
  131. package/lib/usdc-perpetual-client.d.ts +0 -97
  132. package/lib/usdc-perpetual-client.js +0 -191
  133. package/lib/usdc-perpetual-client.js.map +0 -1
  134. package/lib/util/WsStore.d.ts +0 -57
  135. package/lib/util/WsStore.js +0 -97
  136. package/lib/util/WsStore.js.map +0 -1
  137. package/lib/util/browser-support.d.ts +0 -1
  138. package/lib/util/browser-support.js +0 -26
  139. package/lib/util/browser-support.js.map +0 -1
  140. package/lib/util/node-support.d.ts +0 -2
  141. package/lib/util/node-support.js +0 -21
  142. package/lib/util/node-support.js.map +0 -1
  143. package/lib/util/websocket-util.d.ts +0 -68
  144. package/lib/util/websocket-util.js +0 -506
  145. package/lib/util/websocket-util.js.map +0 -1
  146. /package/lib/types/{v5-shared.d.ts → shared-v5.d.ts} +0 -0
@@ -8,316 +8,41 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
12
  exports.WebsocketClient = void 0;
16
- /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
17
- /* eslint-disable max-len */
18
- /* eslint-disable @typescript-eslint/no-explicit-any */
19
- const events_1 = require("events");
20
- const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
21
- const contract_client_1 = require("./contract-client");
22
- const inverse_client_1 = require("./inverse-client");
23
- const linear_client_1 = require("./linear-client");
24
- const rest_client_v5_1 = require("./rest-client-v5");
25
- const spot_client_1 = require("./spot-client");
26
- const spot_client_v3_1 = require("./spot-client-v3");
27
- const unified_margin_client_1 = require("./unified-margin-client");
28
- const usdc_option_client_1 = require("./usdc-option-client");
29
- const usdc_perpetual_client_1 = require("./usdc-perpetual-client");
30
13
  const util_1 = require("./util");
31
- const node_support_1 = require("./util/node-support");
32
- const WsStore_1 = __importDefault(require("./util/WsStore"));
33
- const loggerCategory = { category: 'bybit-ws' };
34
- // eslint-disable-next-line no-redeclare
35
- class WebsocketClient extends events_1.EventEmitter {
36
- constructor(options, logger) {
37
- super();
38
- this.pendingTopicsSubscriptions = [];
39
- this.logger = logger || util_1.DefaultLogger;
40
- this.wsStore = new WsStore_1.default(this.logger);
41
- this.options = Object.assign({ testnet: false, pongTimeout: 1000, pingInterval: 10000, reconnectTimeout: 500, recvWindow: 5000, fetchTimeOffsetBeforeAuth: false }, options);
42
- this.options.restOptions = Object.assign(Object.assign({}, this.options.restOptions), { testnet: this.options.testnet });
43
- this.prepareRESTClient();
44
- // add default error handling so this doesn't crash node (if the user didn't set a handler)
45
- // eslint-disable-next-line @typescript-eslint/no-empty-function
46
- this.on('error', () => { });
47
- }
48
- /** Get the WsStore that tracks websockets & topics */
49
- getWsStore() {
50
- return this.wsStore;
51
- }
52
- isTestnet() {
53
- return this.options.testnet === true;
54
- }
55
- /**
56
- * Subscribe to V5 topics & track/persist them.
57
- * @param wsTopics - topic or list of topics
58
- * @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.
59
- * @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)
60
- */
61
- subscribeV5(wsTopics, category, isPrivateTopic) {
62
- const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
63
- return new Promise((resolver, rejector) => {
64
- topics.forEach((topic) => {
65
- const wsKey = (0, util_1.getWsKeyForTopic)(this.options.market, topic, isPrivateTopic, category);
66
- // Persist topic for reconnects
67
- this.wsStore.addTopic(wsKey, topic);
68
- this.upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector);
69
- // if connected, send subscription request
70
- if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTED)) {
71
- return this.requestSubscribeTopics(wsKey, [topic]);
72
- }
73
- // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
74
- if (!this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTING) &&
75
- !this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.RECONNECTING)) {
76
- return this.connect(wsKey);
77
- }
78
- });
79
- });
80
- }
14
+ const BaseWSClient_1 = require("./util/BaseWSClient");
15
+ const webCryptoAPI_1 = require("./util/webCryptoAPI");
16
+ const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
17
+ class WebsocketClient extends BaseWSClient_1.BaseWebsocketClient {
81
18
  /**
82
- * Subscribe to V1-V3 topics & track/persist them.
83
- *
84
- * Note: for public V5 topics use the `subscribeV5()` method.
85
- *
86
- * Topics will be automatically resubscribed to if the connection resets/drops/reconnects.
87
- * @param wsTopics - topic or list of topics
88
- * @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)
89
- */
90
- subscribe(wsTopics, isPrivateTopic) {
91
- const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
92
- if (this.options.market === 'v5') {
93
- topics.forEach((topic) => {
94
- if (!(0, util_1.isPrivateWsTopic)(topic)) {
95
- throw new Error('For public "v5" websocket topics, use the subscribeV5() method & provide the category parameter');
96
- }
97
- });
98
- }
99
- return new Promise((resolver, rejector) => {
100
- topics.forEach((topic) => {
101
- const wsKey = (0, util_1.getWsKeyForTopic)(this.options.market, topic, isPrivateTopic);
102
- // Persist topic for reconnects
103
- this.wsStore.addTopic(wsKey, topic);
104
- this.upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector);
105
- // if connected, send subscription request
106
- if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTED)) {
107
- return this.requestSubscribeTopics(wsKey, [topic]);
108
- }
109
- // start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
110
- if (!this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTING) &&
111
- !this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.RECONNECTING)) {
112
- return this.connect(wsKey);
113
- }
114
- });
115
- });
116
- }
117
- upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector) {
118
- const existingWsKeyPendingSubscriptions = this.pendingTopicsSubscriptions.find((s) => s.wsKey === wsKey);
119
- if (!existingWsKeyPendingSubscriptions) {
120
- this.pendingTopicsSubscriptions.push({
121
- wsKey,
122
- resolver,
123
- rejector,
124
- failedTopicsSubscriptions: new Set(),
125
- pendingTopicsSubscriptions: new Set([topic]),
126
- });
127
- return;
128
- }
129
- existingWsKeyPendingSubscriptions.pendingTopicsSubscriptions.add(topic);
130
- }
131
- /**
132
- * Unsubscribe from V5 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
133
- * @param wsTopics - topic or list of topics
134
- * @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.
135
- * @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)
19
+ * Request connection of all dependent (public & private) websockets, instead of waiting
20
+ * for automatic connection by SDK.
136
21
  */
137
- unsubscribeV5(wsTopics, category, isPrivateTopic) {
138
- const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
139
- topics.forEach((topic) => {
140
- const wsKey = (0, util_1.getWsKeyForTopic)(this.options.market, topic, isPrivateTopic, category);
141
- // Remove topic from persistence for reconnects
142
- this.wsStore.deleteTopic(wsKey, topic);
143
- this.removeTopicPendingSubscription(wsKey, topic);
144
- // unsubscribe request only necessary if active connection exists
145
- if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTED)) {
146
- this.requestUnsubscribeTopics(wsKey, [topic]);
147
- }
148
- });
149
- }
150
- removeTopicPendingSubscription(wsKey, topic) {
151
- const existingWsKeyPendingSubscriptions = this.pendingTopicsSubscriptions.find((s) => s.wsKey === wsKey);
152
- if (existingWsKeyPendingSubscriptions) {
153
- existingWsKeyPendingSubscriptions.pendingTopicsSubscriptions.delete(topic);
154
- if (!existingWsKeyPendingSubscriptions.pendingTopicsSubscriptions.size) {
155
- this.pendingTopicsSubscriptions =
156
- this.pendingTopicsSubscriptions.filter((s) => s.wsKey !== wsKey);
157
- }
158
- }
159
- }
160
- clearTopicsPendingSubscriptions(wsKey) {
161
- this.pendingTopicsSubscriptions = this.pendingTopicsSubscriptions.filter((s) => s.wsKey !== wsKey);
162
- }
163
- /**
164
- * Unsubscribe from V1-V3 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
165
- *
166
- * Note: For public V5 topics, use `unsubscribeV5()` instead!
167
- *
168
- * @param wsTopics topic or list of topics
169
- * @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)
170
- */
171
- unsubscribe(wsTopics, isPrivateTopic) {
172
- const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
173
- if (this.options.market === 'v5') {
174
- topics.forEach((topic) => {
175
- if (!(0, util_1.isPrivateWsTopic)(topic)) {
176
- throw new Error('For public "v5" websocket topics, use the unsubscribeV5() method & provide the category parameter');
177
- }
178
- });
179
- }
180
- topics.forEach((topic) => {
181
- const wsKey = (0, util_1.getWsKeyForTopic)(this.options.market, topic, isPrivateTopic);
182
- // Remove topic from persistence for reconnects
183
- this.wsStore.deleteTopic(wsKey, topic);
184
- this.removeTopicPendingSubscription(wsKey, topic);
185
- // unsubscribe request only necessary if active connection exists
186
- if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTED)) {
187
- this.requestUnsubscribeTopics(wsKey, [topic]);
188
- }
189
- });
190
- }
191
- /**
192
- * @private Only used if we fetch exchange time before attempting auth. Disabled by default.
193
- * I've removed this for ftx and it's working great, tempted to remove this here
194
- */
195
- prepareRESTClient() {
22
+ connectAll() {
196
23
  switch (this.options.market) {
197
- case 'inverse': {
198
- this.restClient = new inverse_client_1.InverseClient(this.options.restOptions, this.options.requestOptions);
199
- break;
200
- }
201
- case 'linear': {
202
- this.restClient = new linear_client_1.LinearClient(this.options.restOptions, this.options.requestOptions);
203
- break;
204
- }
205
- case 'spot': {
206
- this.restClient = new spot_client_1.SpotClient(this.options.restOptions, this.options.requestOptions);
207
- this.connectPublic();
208
- break;
209
- }
210
- case 'spotv3': {
211
- this.restClient = new spot_client_v3_1.SpotClientV3(this.options.restOptions, this.options.requestOptions);
212
- break;
213
- }
214
- case 'usdcOption': {
215
- this.restClient = new usdc_option_client_1.USDCOptionClient(this.options.restOptions, this.options.requestOptions);
216
- break;
217
- }
218
- case 'usdcPerp': {
219
- this.restClient = new usdc_perpetual_client_1.USDCPerpetualClient(this.options.restOptions, this.options.requestOptions);
220
- break;
221
- }
222
- case 'unifiedOption':
223
- case 'unifiedPerp': {
224
- this.restClient = new unified_margin_client_1.UnifiedMarginClient(this.options.restOptions, this.options.requestOptions);
225
- break;
226
- }
227
- case 'contractInverse':
228
- case 'contractUSDT': {
229
- this.restClient = new contract_client_1.ContractClient(this.options.restOptions, this.options.requestOptions);
230
- break;
231
- }
232
24
  case 'v5': {
233
- this.restClient = new rest_client_v5_1.RestClientV5(this.options.restOptions, this.options.requestOptions);
234
- break;
25
+ return [...this.connectPublic(), this.connectPrivate()];
235
26
  }
236
27
  default: {
237
- throw (0, util_1.neverGuard)(this.options.market, 'prepareRESTClient(): Unhandled market');
28
+ throw (0, util_1.neverGuard)(this.options.market, 'connectAll(): Unhandled market');
238
29
  }
239
30
  }
240
31
  }
241
- close(wsKey, force) {
242
- this.logger.info('Closing connection', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
243
- this.setWsState(wsKey, util_1.WsConnectionStateEnum.CLOSING);
244
- this.clearTimers(wsKey);
245
- const ws = this.getWs(wsKey);
246
- ws === null || ws === void 0 ? void 0 : ws.close();
247
- if (force) {
248
- (0, util_1.safeTerminateWs)(ws);
249
- }
250
- }
251
- closeAll(force) {
252
- const keys = this.wsStore.getKeys();
253
- this.logger.info(`Closing all ws connections: ${keys}`);
254
- keys.forEach((key) => {
255
- this.close(key, force);
256
- });
257
- }
258
32
  /**
259
- * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
33
+ * Ensures the WS API connection is active and ready.
34
+ *
35
+ * You do not need to call this, but if you call this before making any WS API requests,
36
+ * it can accelerate the first request (by preparing the connection in advance).
260
37
  */
261
- connectAll() {
262
- switch (this.options.market) {
263
- case 'inverse': {
264
- // only one for inverse
265
- return [...this.connectPublic()];
266
- }
267
- // these all have separate public & private ws endpoints
268
- case 'linear':
269
- case 'spot':
270
- case 'spotv3':
271
- case 'usdcOption':
272
- case 'usdcPerp':
273
- case 'unifiedPerp':
274
- case 'unifiedOption':
275
- case 'contractUSDT':
276
- case 'contractInverse': {
277
- return [...this.connectPublic(), this.connectPrivate()];
278
- }
279
- case 'v5': {
280
- return [this.connectPrivate()];
281
- }
282
- default: {
283
- throw (0, util_1.neverGuard)(this.options.market, 'connectAll(): Unhandled market');
284
- }
285
- }
38
+ connectWSAPI() {
39
+ /** This call automatically ensures the connection is active AND authenticated before resolving */
40
+ return this.assertIsAuthenticated(util_1.WS_KEY_MAP.v5PrivateTrade);
286
41
  }
287
42
  connectPublic() {
288
43
  switch (this.options.market) {
289
- case 'inverse': {
290
- return [this.connect(util_1.WS_KEY_MAP.inverse)];
291
- }
292
- case 'linear': {
293
- return [this.connect(util_1.WS_KEY_MAP.linearPublic)];
294
- }
295
- case 'spot': {
296
- return [this.connect(util_1.WS_KEY_MAP.spotPublic)];
297
- }
298
- case 'spotv3': {
299
- return [this.connect(util_1.WS_KEY_MAP.spotV3Public)];
300
- }
301
- case 'usdcOption': {
302
- return [this.connect(util_1.WS_KEY_MAP.usdcOptionPublic)];
303
- }
304
- case 'usdcPerp': {
305
- return [this.connect(util_1.WS_KEY_MAP.usdcPerpPublic)];
306
- }
307
- case 'unifiedOption': {
308
- return [this.connect(util_1.WS_KEY_MAP.unifiedOptionPublic)];
309
- }
310
- case 'unifiedPerp': {
311
- return [
312
- this.connect(util_1.WS_KEY_MAP.unifiedPerpUSDTPublic),
313
- this.connect(util_1.WS_KEY_MAP.unifiedPerpUSDCPublic),
314
- ];
315
- }
316
- case 'contractUSDT':
317
- return [this.connect(util_1.WS_KEY_MAP.contractUSDTPublic)];
318
- case 'contractInverse':
319
- return [this.connect(util_1.WS_KEY_MAP.contractInversePublic)];
320
- case 'v5': {
44
+ case 'v5':
45
+ default: {
321
46
  return [
322
47
  this.connect(util_1.WS_KEY_MAP.v5SpotPublic),
323
48
  this.connect(util_1.WS_KEY_MAP.v5LinearPublic),
@@ -325,121 +50,213 @@ class WebsocketClient extends events_1.EventEmitter {
325
50
  this.connect(util_1.WS_KEY_MAP.v5OptionPublic),
326
51
  ];
327
52
  }
328
- default: {
329
- throw (0, util_1.neverGuard)(this.options.market, 'connectPublic(): Unhandled market');
330
- }
331
53
  }
332
54
  }
333
55
  connectPrivate() {
334
56
  switch (this.options.market) {
335
- case 'inverse': {
336
- return this.connect(util_1.WS_KEY_MAP.inverse);
337
- }
338
- case 'linear': {
339
- return this.connect(util_1.WS_KEY_MAP.linearPrivate);
340
- }
341
- case 'spot': {
342
- return this.connect(util_1.WS_KEY_MAP.spotPrivate);
343
- }
344
- case 'spotv3': {
345
- return this.connect(util_1.WS_KEY_MAP.spotV3Private);
346
- }
347
- case 'usdcOption': {
348
- return this.connect(util_1.WS_KEY_MAP.usdcOptionPrivate);
349
- }
350
- case 'usdcPerp': {
351
- return this.connect(util_1.WS_KEY_MAP.usdcPerpPrivate);
352
- }
353
- case 'unifiedPerp':
354
- case 'unifiedOption': {
355
- return this.connect(util_1.WS_KEY_MAP.unifiedPrivate);
356
- }
357
- case 'contractUSDT':
358
- return this.connect(util_1.WS_KEY_MAP.contractUSDTPrivate);
359
- case 'contractInverse':
360
- return this.connect(util_1.WS_KEY_MAP.contractInversePrivate);
361
- case 'v5': {
362
- return this.connect(util_1.WS_KEY_MAP.v5Private);
363
- }
57
+ case 'v5':
364
58
  default: {
365
- throw (0, util_1.neverGuard)(this.options.market, 'connectPrivate(): Unhandled market');
59
+ return this.connect(util_1.WS_KEY_MAP.v5Private);
366
60
  }
367
61
  }
368
62
  }
369
- connect(wsKey) {
370
- return __awaiter(this, void 0, void 0, function* () {
371
- try {
372
- if (this.wsStore.isWsOpen(wsKey)) {
373
- this.logger.error('Refused to connect to ws with existing active connection', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
374
- return this.wsStore.getWs(wsKey);
375
- }
376
- if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTING)) {
377
- this.logger.error('Refused to connect to ws, connection attempt already active', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
378
- return;
63
+ /**
64
+ * Subscribe to V5 topics & track/persist them.
65
+ * @param wsTopics - topic or list of topics
66
+ * @param category - the API category this topic is for (e.g. "linear").
67
+ * The value is only important when connecting to public topics and will be ignored for private topics.
68
+ * @param isPrivateTopic - optional - the library will try to detect private topics, you can use this
69
+ * to mark a topic as private (if the topic isn't recognised yet)
70
+ */
71
+ subscribeV5(wsTopics, category, isPrivateTopic) {
72
+ const topicRequests = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
73
+ const perWsKeyTopics = {};
74
+ // Sort into per-WsKey batches, in case there is a mix of topics here
75
+ for (const topic of topicRequests) {
76
+ const derivedWsKey = (0, util_1.getWsKeyForTopic)(this.options.market, topic, isPrivateTopic, category);
77
+ const wsRequest = {
78
+ topic: topic,
79
+ category: category,
80
+ };
81
+ if (!perWsKeyTopics[derivedWsKey] ||
82
+ !Array.isArray(perWsKeyTopics[derivedWsKey])) {
83
+ perWsKeyTopics[derivedWsKey] = [];
84
+ }
85
+ perWsKeyTopics[derivedWsKey].push(wsRequest);
86
+ }
87
+ const promises = [];
88
+ // Batch sub topics per ws key
89
+ for (const wsKey in perWsKeyTopics) {
90
+ const wsKeyTopicRequests = perWsKeyTopics[wsKey];
91
+ if (wsKeyTopicRequests === null || wsKeyTopicRequests === void 0 ? void 0 : wsKeyTopicRequests.length) {
92
+ const requestPromise = this.subscribeTopicsForWsKey(wsKeyTopicRequests, wsKey);
93
+ if (Array.isArray(requestPromise)) {
94
+ promises.push(...requestPromise);
379
95
  }
380
- if (!this.wsStore.getConnectionState(wsKey) ||
381
- this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.INITIAL)) {
382
- this.setWsState(wsKey, util_1.WsConnectionStateEnum.CONNECTING);
96
+ else {
97
+ promises.push(requestPromise);
383
98
  }
384
- const authParams = yield this.getAuthParams(wsKey);
385
- const url = (0, util_1.getWsUrl)(wsKey, this.options, this.logger);
386
- const ws = this.connectToWsUrl(url + authParams, wsKey);
387
- return this.wsStore.setWs(wsKey, ws);
388
99
  }
389
- catch (err) {
390
- this.parseWsError('Connection failed', err, wsKey);
391
- this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
392
- }
393
- });
394
- }
395
- parseWsError(context, error, wsKey) {
396
- if (!error.message) {
397
- this.logger.error(`${context} due to unexpected error: `, error);
398
- this.emit('error', error);
399
- return;
400
100
  }
401
- switch (error.message) {
402
- case 'Unexpected server response: 401':
403
- this.logger.error(`${context} due to 401 authorization failure.`, Object.assign(Object.assign({}, loggerCategory), { wsKey }));
404
- break;
405
- default:
406
- if (this.wsStore.getConnectionState(wsKey) !==
407
- util_1.WsConnectionStateEnum.CLOSING) {
408
- this.logger.error(`${context} due to unexpected response error: "${(error === null || error === void 0 ? void 0 : error.msg) || (error === null || error === void 0 ? void 0 : error.message) || error}"`, Object.assign(Object.assign({}, loggerCategory), { wsKey, error }));
409
- this.executeReconnectableClose(wsKey, 'unhandled onWsError');
101
+ // Return promise to resolve midflight WS request (only works if already connected before request)
102
+ return promises;
103
+ }
104
+ /**
105
+ * Unsubscribe from V5 topics & remove them from memory. They won't be re-subscribed to if the
106
+ * connection reconnects.
107
+ *
108
+ * @param wsTopics - topic or list of topics
109
+ * @param category - the API category this topic is for (e.g. "linear"). The value is only
110
+ * important when connecting to public topics and will be ignored for private topics.
111
+ * @param isPrivateTopic - optional - the library will try to detect private topics, you can
112
+ * use this to mark a topic as private (if the topic isn't recognised yet)
113
+ */
114
+ unsubscribeV5(wsTopics, category, isPrivateTopic) {
115
+ const topicRequests = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
116
+ const perWsKeyTopics = {};
117
+ // Sort into per-WsKey batches, in case there is a mix of topics here
118
+ for (const topic of topicRequests) {
119
+ const derivedWsKey = (0, util_1.getWsKeyForTopic)(this.options.market, topic, isPrivateTopic, category);
120
+ const wsRequest = {
121
+ topic: topic,
122
+ category: category,
123
+ };
124
+ if (!perWsKeyTopics[derivedWsKey] ||
125
+ !Array.isArray(perWsKeyTopics[derivedWsKey])) {
126
+ perWsKeyTopics[derivedWsKey] = [];
127
+ }
128
+ perWsKeyTopics[derivedWsKey].push(wsRequest);
129
+ }
130
+ const promises = [];
131
+ // Batch sub topics per ws key
132
+ for (const wsKey in perWsKeyTopics) {
133
+ const wsKeyTopicRequests = perWsKeyTopics[wsKey];
134
+ if (wsKeyTopicRequests === null || wsKeyTopicRequests === void 0 ? void 0 : wsKeyTopicRequests.length) {
135
+ const requestPromise = this.unsubscribeTopicsForWsKey(wsKeyTopicRequests, wsKey);
136
+ if (Array.isArray(requestPromise)) {
137
+ promises.push(...requestPromise);
410
138
  }
411
139
  else {
412
- this.logger.info(`${wsKey} socket forcefully closed. Will not reconnect.`);
140
+ promises.push(requestPromise);
413
141
  }
414
- break;
142
+ }
415
143
  }
416
- this.emit('error', error);
144
+ // Return promise to resolve midflight WS request (only works if already connected before request)
145
+ return promises;
417
146
  }
418
147
  /**
419
- * Return params required to make authorized request
148
+ * Note: subscribeV5() might be simpler to use. The end result is the same.
149
+ *
150
+ * Request subscription to one or more topics. Pass topics as either an array of strings,
151
+ * or array of objects (if the topic has parameters).
152
+ *
153
+ * Objects should be formatted as {topic: string, params: object, category: CategoryV5}.
154
+ *
155
+ * - Subscriptions are automatically routed to the correct websocket connection.
156
+ * - Authentication/connection is automatic.
157
+ * - Resubscribe after network issues is automatic.
158
+ *
159
+ * Call `unsubscribe(topics)` to remove topics
420
160
  */
421
- getAuthParams(wsKey) {
422
- return __awaiter(this, void 0, void 0, function* () {
423
- if (util_1.PUBLIC_WS_KEYS.includes(wsKey)) {
424
- this.logger.debug('Starting public only websocket client.', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
425
- return '';
161
+ subscribe(requests, wsKey) {
162
+ const topicRequests = Array.isArray(requests) ? requests : [requests];
163
+ const normalisedTopicRequests = (0, util_1.getNormalisedTopicRequests)(topicRequests);
164
+ const perWsKeyTopics = (0, util_1.getTopicsPerWSKey)(this.options.market, normalisedTopicRequests, wsKey);
165
+ // Batch sub topics per ws key
166
+ for (const wsKey in perWsKeyTopics) {
167
+ const wsKeyTopicRequests = perWsKeyTopics[wsKey];
168
+ if (wsKeyTopicRequests === null || wsKeyTopicRequests === void 0 ? void 0 : wsKeyTopicRequests.length) {
169
+ this.subscribeTopicsForWsKey(wsKeyTopicRequests, wsKey);
426
170
  }
427
- try {
428
- const { signature, expiresAt } = yield this.getWsAuthSignature(wsKey);
429
- const authParams = {
430
- api_key: this.options.key,
431
- expires: expiresAt,
432
- signature,
433
- };
434
- return '?' + (0, util_1.serializeParams)(authParams);
171
+ }
172
+ }
173
+ /**
174
+ * Note: unsubscribe() might be simpler to use. The end result is the same.
175
+ * Unsubscribe from one or more topics. Similar to subscribe() but in reverse.
176
+ *
177
+ * - Requests are automatically routed to the correct websocket connection.
178
+ * - These topics will be removed from the topic cache, so they won't be subscribed to again.
179
+ */
180
+ unsubscribe(requests, wsKey) {
181
+ const topicRequests = Array.isArray(requests) ? requests : [requests];
182
+ const normalisedTopicRequests = (0, util_1.getNormalisedTopicRequests)(topicRequests);
183
+ const perWsKeyTopics = (0, util_1.getTopicsPerWSKey)(this.options.market, normalisedTopicRequests, wsKey);
184
+ // Batch sub topics per ws key
185
+ for (const wsKey in perWsKeyTopics) {
186
+ const wsKeyTopicRequests = perWsKeyTopics[wsKey];
187
+ if (wsKeyTopicRequests === null || wsKeyTopicRequests === void 0 ? void 0 : wsKeyTopicRequests.length) {
188
+ this.unsubscribeTopicsForWsKey(wsKeyTopicRequests, wsKey);
189
+ }
190
+ }
191
+ }
192
+ sendWSAPIRequest() {
193
+ return __awaiter(this, arguments, void 0, function* (wsKey = util_1.WS_KEY_MAP.v5PrivateTrade, operation, params) {
194
+ this.logger.trace(`sendWSAPIRequest(): assert "${wsKey}" is connected`);
195
+ yield this.assertIsConnected(wsKey);
196
+ this.logger.trace('sendWSAPIRequest()->assertIsConnected() ok');
197
+ yield this.assertIsAuthenticated(wsKey);
198
+ this.logger.trace('sendWSAPIRequest()->assertIsAuthenticated() ok');
199
+ const requestEvent = {
200
+ reqId: this.getNewRequestId(),
201
+ header: {
202
+ 'X-BAPI-RECV-WINDOW': `${this.options.recvWindow}`,
203
+ 'X-BAPI-TIMESTAMP': `${Date.now()}`,
204
+ Referer: util_1.APIID,
205
+ },
206
+ op: operation,
207
+ args: [params],
208
+ };
209
+ // Sign, if needed
210
+ const signedEvent = yield this.signWSAPIRequest(requestEvent);
211
+ // Store deferred promise, resolved within the "resolveEmittableEvents" method while parsing incoming events
212
+ const promiseRef = (0, util_1.getPromiseRefForWSAPIRequest)(requestEvent);
213
+ const deferredPromise = this.getWsStore().createDeferredPromise(wsKey, promiseRef, false);
214
+ this.logger.trace(`sendWSAPIRequest(): sending raw request: ${JSON.stringify(signedEvent, null, 2)}`);
215
+ // Send event
216
+ this.tryWsSend(wsKey, JSON.stringify(signedEvent));
217
+ this.logger.trace(`sendWSAPIRequest(): sent ${operation} event`);
218
+ // Return deferred promise, so caller can await this call
219
+ return deferredPromise.promise;
220
+ });
221
+ }
222
+ /**
223
+ *
224
+ *
225
+ * Internal methods - not intended for public use
226
+ *
227
+ *
228
+ */
229
+ /**
230
+ * @returns The WS URL to connect to for this WS key
231
+ */
232
+ getWsUrl(wsKey) {
233
+ return __awaiter(this, void 0, void 0, function* () {
234
+ const wsBaseURL = (0, util_1.getWsUrl)(wsKey, this.options, this.logger);
235
+ // If auth is needed for this wsKey URL, this returns a suffix
236
+ const authParams = yield this.getWsAuthURLSuffix();
237
+ if (!authParams) {
238
+ return wsBaseURL;
435
239
  }
436
- catch (e) {
437
- this.logger.error(e, Object.assign(Object.assign({}, loggerCategory), { wsKey }));
438
- return '';
240
+ return wsBaseURL + '?' + authParams;
241
+ });
242
+ }
243
+ /**
244
+ * Return params required to make authorized request
245
+ */
246
+ getWsAuthURLSuffix() {
247
+ return __awaiter(this, void 0, void 0, function* () {
248
+ return '';
249
+ });
250
+ }
251
+ signMessage(paramsStr, secret, method, algorithm) {
252
+ return __awaiter(this, void 0, void 0, function* () {
253
+ if (typeof this.options.customSignMessageFn === 'function') {
254
+ return this.options.customSignMessageFn(paramsStr, secret);
439
255
  }
256
+ return yield (0, webCryptoAPI_1.signMessage)(paramsStr, secret, method, algorithm);
440
257
  });
441
258
  }
442
- sendAuthRequest(wsKey) {
259
+ getWsAuthRequestEvent(wsKey) {
443
260
  return __awaiter(this, void 0, void 0, function* () {
444
261
  try {
445
262
  const { signature, expiresAt } = yield this.getWsAuthSignature(wsKey);
@@ -448,387 +265,281 @@ class WebsocketClient extends events_1.EventEmitter {
448
265
  args: [this.options.key, expiresAt, signature],
449
266
  req_id: `${wsKey}-auth`,
450
267
  };
451
- return this.tryWsSend(wsKey, JSON.stringify(request));
268
+ return request;
452
269
  }
453
270
  catch (e) {
454
- this.logger.error(e, Object.assign(Object.assign({}, loggerCategory), { wsKey }));
271
+ this.logger.error(e, Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey }));
272
+ throw e;
455
273
  }
456
274
  });
457
275
  }
458
276
  getWsAuthSignature(wsKey) {
459
- var _a;
460
277
  return __awaiter(this, void 0, void 0, function* () {
461
278
  const { key, secret } = this.options;
462
279
  if (!key || !secret) {
463
- this.logger.warning('Cannot authenticate websocket, either api or private keys missing.', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
280
+ this.logger.error('Cannot authenticate websocket, either api or private keys missing.', Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey }));
464
281
  throw new Error('Cannot auth - missing api or secret in config');
465
282
  }
466
- this.logger.debug("Getting auth'd request params", Object.assign(Object.assign({}, loggerCategory), { wsKey }));
467
- const timeOffset = this.options.fetchTimeOffsetBeforeAuth
468
- ? (yield ((_a = this.restClient) === null || _a === void 0 ? void 0 : _a.fetchTimeOffset())) || 0
469
- : 0;
283
+ this.logger.trace("Getting auth'd request params", Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsKey }));
470
284
  const recvWindow = this.options.recvWindow || 5000;
471
- const signatureExpiresAt = Date.now() + timeOffset + recvWindow;
472
- const signature = yield (0, node_support_1.signMessage)('GET/realtime' + signatureExpiresAt, secret);
285
+ const signatureExpiresAt = Date.now() + this.getTimeOffsetMs() + recvWindow;
286
+ const signature = yield this.signMessage('GET/realtime' + signatureExpiresAt, secret, 'hex', 'SHA-256');
473
287
  return {
474
288
  expiresAt: signatureExpiresAt,
475
289
  signature,
476
290
  };
477
291
  });
478
292
  }
479
- reconnectWithDelay(wsKey, connectionDelayMs) {
480
- var _a;
481
- this.clearTimers(wsKey);
482
- if (this.wsStore.getConnectionState(wsKey) !==
483
- util_1.WsConnectionStateEnum.CONNECTING) {
484
- this.setWsState(wsKey, util_1.WsConnectionStateEnum.RECONNECTING);
485
- }
486
- if ((_a = this.wsStore.get(wsKey)) === null || _a === void 0 ? void 0 : _a.activeReconnectTimer) {
487
- this.clearReconnectTimer(wsKey);
488
- }
489
- this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
490
- this.logger.info('Reconnecting to websocket', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
491
- this.clearReconnectTimer(wsKey);
492
- this.connect(wsKey);
493
- }, connectionDelayMs);
293
+ signWSAPIRequest(requestEvent) {
294
+ return __awaiter(this, void 0, void 0, function* () {
295
+ // Not needed for Bybit. Auth happens only on connection open, automatically.
296
+ return requestEvent;
297
+ });
494
298
  }
495
- ping(wsKey) {
496
- if (this.wsStore.get(wsKey, true).activePongTimer) {
497
- return;
498
- }
499
- this.clearPongTimer(wsKey);
500
- this.logger.silly('Sending ping', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
299
+ sendPingEvent(wsKey) {
501
300
  this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' }));
502
- this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => this.executeReconnectableClose(wsKey, 'Pong timeout'), this.options.pongTimeout);
503
- }
504
- /**
505
- * Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
506
- * If closed, trigger a reconnect immediately
507
- */
508
- executeReconnectableClose(wsKey, reason) {
509
- this.logger.info(`${reason} - closing socket to reconnect`, Object.assign(Object.assign({}, loggerCategory), { wsKey,
510
- reason }));
511
- const wasOpen = this.wsStore.isWsOpen(wsKey);
512
- this.clearPingTimer(wsKey);
513
- this.clearPongTimer(wsKey);
514
- const ws = this.getWs(wsKey);
515
- if (ws) {
516
- ws.close();
517
- (0, util_1.safeTerminateWs)(ws);
518
- }
519
- if (!wasOpen) {
520
- this.logger.info(`${reason} - socket already closed - trigger immediate reconnect`, Object.assign(Object.assign({}, loggerCategory), { wsKey,
521
- reason }));
522
- this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
523
- }
524
301
  }
525
- clearTimers(wsKey) {
526
- this.clearPingTimer(wsKey);
527
- this.clearPongTimer(wsKey);
528
- this.clearReconnectTimer(wsKey);
302
+ sendPongEvent(wsKey) {
303
+ this.tryWsSend(wsKey, JSON.stringify({ op: 'pong' }));
529
304
  }
530
- // Send a ping at intervals
531
- clearPingTimer(wsKey) {
532
- const wsState = this.wsStore.get(wsKey);
533
- if (wsState === null || wsState === void 0 ? void 0 : wsState.activePingTimer) {
534
- clearInterval(wsState.activePingTimer);
535
- wsState.activePingTimer = undefined;
536
- }
537
- }
538
- // Expect a pong within a time limit
539
- clearPongTimer(wsKey) {
540
- const wsState = this.wsStore.get(wsKey);
541
- if (wsState === null || wsState === void 0 ? void 0 : wsState.activePongTimer) {
542
- clearTimeout(wsState.activePongTimer);
543
- wsState.activePongTimer = undefined;
544
- }
545
- }
546
- clearReconnectTimer(wsKey) {
547
- const wsState = this.wsStore.get(wsKey);
548
- if (wsState === null || wsState === void 0 ? void 0 : wsState.activeReconnectTimer) {
549
- clearTimeout(wsState.activeReconnectTimer);
550
- wsState.activeReconnectTimer = undefined;
551
- }
305
+ /** Force subscription requests to be sent in smaller batches, if a number is returned */
306
+ getMaxTopicsPerSubscribeEvent(wsKey) {
307
+ return (0, util_1.getMaxTopicsPerSubscribeEvent)(this.options.market, wsKey);
552
308
  }
553
309
  /**
554
- * @private Use the `subscribe(topics)` method to subscribe to topics. Send WS message to subscribe to topics.
310
+ * @returns one or more correctly structured request events for performing a operations over WS. This can vary per exchange spec.
555
311
  */
556
- requestSubscribeTopics(wsKey, topics) {
557
- if (!topics.length) {
558
- return;
559
- }
560
- const maxTopicsPerEvent = (0, util_1.getMaxTopicsPerSubscribeEvent)(this.options.market, wsKey);
561
- if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) {
562
- this.logger.silly(`Subscribing to topics in batches of ${maxTopicsPerEvent}`);
563
- for (let i = 0; i < topics.length; i += maxTopicsPerEvent) {
564
- const batch = topics.slice(i, i + maxTopicsPerEvent);
565
- this.logger.silly(`Subscribing to batch of ${batch.length}`);
566
- this.requestSubscribeTopics(wsKey, batch);
312
+ getWsRequestEvents(market, operation, requests,
313
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
314
+ wsKey) {
315
+ return __awaiter(this, void 0, void 0, function* () {
316
+ const wsRequestEvents = [];
317
+ const wsRequestBuildingErrors = [];
318
+ switch (market) {
319
+ case 'all': {
320
+ const topics = requests.map((r) => r.topic);
321
+ // Previously used to track topics in a request. Keeping this for subscribe/unsubscribe requests, no need for incremental values
322
+ const req_id = ['subscribe', 'unsubscribe'].includes(operation) && topics.length
323
+ ? topics.join('_')
324
+ : this.getNewRequestId();
325
+ const wsEvent = {
326
+ req_id: req_id,
327
+ op: operation,
328
+ args: topics,
329
+ };
330
+ const midflightWsEvent = {
331
+ requestKey: wsEvent.req_id,
332
+ requestEvent: wsEvent,
333
+ };
334
+ wsRequestEvents.push(Object.assign({}, midflightWsEvent));
335
+ break;
336
+ }
337
+ default: {
338
+ throw (0, util_1.neverGuard)(market, `Unhandled market "${market}"`);
339
+ }
567
340
  }
568
- this.logger.silly(`Finished batch subscribing to ${topics.length} topics`);
569
- return;
570
- }
571
- const wsMessage = JSON.stringify({
572
- req_id: topics.join(','),
573
- op: 'subscribe',
574
- args: topics,
341
+ if (wsRequestBuildingErrors.length) {
342
+ const label = wsRequestBuildingErrors.length === requests.length ? 'all' : 'some';
343
+ this.logger.error(`Failed to build/send ${wsRequestBuildingErrors.length} event(s) for ${label} WS requests due to exceptions`, Object.assign(Object.assign({}, WS_LOGGER_CATEGORY), { wsRequestBuildingErrors, wsRequestBuildingErrorsStringified: JSON.stringify(wsRequestBuildingErrors, null, 2) }));
344
+ }
345
+ return wsRequestEvents;
575
346
  });
576
- this.tryWsSend(wsKey, wsMessage);
347
+ }
348
+ getPrivateWSKeys() {
349
+ return util_1.WS_AUTH_ON_CONNECT_KEYS;
350
+ }
351
+ isAuthOnConnectWsKey(wsKey) {
352
+ return util_1.WS_AUTH_ON_CONNECT_KEYS.includes(wsKey);
577
353
  }
578
354
  /**
579
- * @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics.
355
+ * Determines if a topic is for a private channel, using a hardcoded list of strings
580
356
  */
581
- requestUnsubscribeTopics(wsKey, topics) {
582
- if (!topics.length) {
583
- return;
584
- }
585
- const maxTopicsPerEvent = (0, util_1.getMaxTopicsPerSubscribeEvent)(this.options.market, wsKey);
586
- if (maxTopicsPerEvent && topics.length > maxTopicsPerEvent) {
587
- this.logger.silly(`Unsubscribing to topics in batches of ${maxTopicsPerEvent}`);
588
- for (let i = 0; i < topics.length; i += maxTopicsPerEvent) {
589
- const batch = topics.slice(i, i + maxTopicsPerEvent);
590
- this.logger.silly(`Unsubscribing to batch of ${batch.length}`);
591
- this.requestUnsubscribeTopics(wsKey, batch);
592
- }
593
- this.logger.silly(`Finished batch unsubscribing to ${topics.length} topics`);
594
- return;
357
+ isPrivateTopicRequest(request) {
358
+ var _a;
359
+ const topicName = (_a = request === null || request === void 0 ? void 0 : request.topic) === null || _a === void 0 ? void 0 : _a.toLowerCase();
360
+ if (!topicName) {
361
+ return false;
595
362
  }
596
- const wsMessage = JSON.stringify({
597
- op: 'unsubscribe',
598
- args: topics,
599
- });
600
- this.tryWsSend(wsKey, wsMessage);
363
+ return (0, util_1.isPrivateWsTopic)(topicName);
601
364
  }
602
- tryWsSend(wsKey, wsMessage) {
603
- try {
604
- this.logger.silly('Sending upstream ws message: ', Object.assign(Object.assign({}, loggerCategory), { wsMessage,
605
- wsKey }));
606
- if (!wsKey) {
607
- throw new Error('Cannot send message due to no known websocket for this wsKey');
608
- }
609
- const ws = this.getWs(wsKey);
610
- if (!ws) {
611
- throw new Error(`${wsKey} socket not connected yet, call "connect(${wsKey}) first then try again when the "open" event arrives`);
612
- }
613
- ws.send(wsMessage);
365
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
366
+ isWsPing(msg) {
367
+ if (!msg) {
368
+ return false;
614
369
  }
615
- catch (e) {
616
- this.logger.error('Failed to send WS message', Object.assign(Object.assign({}, loggerCategory), { wsMessage,
617
- wsKey, exception: e }));
370
+ if (typeof (msg === null || msg === void 0 ? void 0 : msg.data) === 'string') {
371
+ if (msg.data.includes('op": "ping')) {
372
+ return true;
373
+ }
374
+ // console.log('isWsPing?', {
375
+ // data: msg.data,
376
+ // });
377
+ return false;
618
378
  }
379
+ return false;
619
380
  }
620
- connectToWsUrl(url, wsKey) {
381
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
382
+ isWsPong(msg) {
621
383
  var _a;
622
- this.logger.silly(`Opening WS connection to URL: ${url}`, Object.assign(Object.assign({}, loggerCategory), { wsKey }));
623
- const agent = (_a = this.options.requestOptions) === null || _a === void 0 ? void 0 : _a.agent;
624
- const ws = new isomorphic_ws_1.default(url, undefined, agent ? { agent } : undefined);
625
- ws.onopen = (event) => this.onWsOpen(event, wsKey);
626
- ws.onmessage = (event) => this.onWsMessage(event, wsKey);
627
- ws.onerror = (event) => this.parseWsError('Websocket onWsError', event, wsKey);
628
- ws.onclose = (event) => this.onWsClose(event, wsKey);
629
- return ws;
630
- }
631
- onWsOpen(event, wsKey) {
632
- return __awaiter(this, void 0, void 0, function* () {
633
- if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.CONNECTING)) {
634
- this.logger.info('Websocket connected', Object.assign(Object.assign({}, loggerCategory), { wsKey, testnet: this.isTestnet(), market: this.options.market }));
635
- this.emit('open', { wsKey, event });
636
- }
637
- else if (this.wsStore.isConnectionState(wsKey, util_1.WsConnectionStateEnum.RECONNECTING)) {
638
- this.logger.info('Websocket reconnected', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
639
- this.emit('reconnected', { wsKey, event });
640
- }
641
- this.setWsState(wsKey, util_1.WsConnectionStateEnum.CONNECTED);
642
- // Some websockets require an auth packet to be sent after opening the connection
643
- if (util_1.WS_AUTH_ON_CONNECT_KEYS.includes(wsKey)) {
644
- this.logger.info('Sending auth request...');
645
- yield this.sendAuthRequest(wsKey);
384
+ if (!msg) {
385
+ return false;
386
+ }
387
+ if (typeof (msg === null || msg === void 0 ? void 0 : msg.data) === 'string') {
388
+ // public ws connections
389
+ if (msg.data.includes('ret_msg":"pong')) {
390
+ return true;
646
391
  }
647
- // TODO: persistence not working yet for spot v1 topics
648
- if (wsKey !== util_1.WS_KEY_MAP.spotPublic && wsKey !== util_1.WS_KEY_MAP.spotPrivate) {
649
- const topics = [...this.wsStore.getTopics(wsKey)];
650
- this.logger.info('Subscribing to topics', Object.assign(Object.assign({}, loggerCategory), { wsKey,
651
- topics }));
652
- this.requestSubscribeTopics(wsKey, topics);
392
+ // private ws connections
393
+ if (msg.data.includes('op":"pong')) {
394
+ return true;
653
395
  }
654
- this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval);
655
- });
396
+ // console.log('isWsPong?', {
397
+ // data: msg.data,
398
+ // });
399
+ return false;
400
+ }
401
+ if (((_a = msg.event) === null || _a === void 0 ? void 0 : _a.ret_msg) === 'pong') {
402
+ return true;
403
+ }
404
+ return (msg === null || msg === void 0 ? void 0 : msg.pong) || (0, util_1.isWsPong)(msg);
656
405
  }
657
- onWsMessage(event, wsKey) {
406
+ /**
407
+ * Abstraction called to sort ws events into emittable event types (response to a request, data update, etc)
408
+ */
409
+ resolveEmittableEvents(wsKey, event) {
410
+ const results = [];
658
411
  try {
659
- // any message can clear the pong timer - wouldn't get a message if the ws dropped
660
- this.clearPongTimer(wsKey);
661
- const msg = JSON.parse((event && event.data) || event);
662
- // this.logger.silly('Received event', {
663
- // ...loggerCategory,
412
+ const parsed = JSON.parse(event.data);
413
+ // this.logger.trace('resolveEmittableEvents', {
414
+ // ...WS_LOGGER_CATEGORY,
664
415
  // wsKey,
665
- // msg: JSON.stringify(msg),
416
+ // parsed: JSON.stringify(parsed),
666
417
  // });
667
- if ((0, util_1.isTopicSubscriptionConfirmation)(msg)) {
668
- this.updatePendingTopicSubscriptionStatus(wsKey, msg);
669
- }
670
- // TODO: cleanme
671
- if (msg['success'] || (msg === null || msg === void 0 ? void 0 : msg.pong) || (0, util_1.isWsPong)(msg)) {
672
- if ((0, util_1.isWsPong)(msg)) {
673
- this.logger.silly('Received pong', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
418
+ // Only applies to the V5 WS topics
419
+ if ((0, util_1.isTopicSubscriptionConfirmation)(parsed) && parsed.req_id) {
420
+ const isTopicSubscriptionSuccessEvent = (0, util_1.isTopicSubscriptionSuccess)(parsed);
421
+ this.updatePendingTopicSubscriptionStatus(wsKey, parsed.req_id, parsed, isTopicSubscriptionSuccessEvent);
422
+ }
423
+ const EVENTS_AUTHENTICATED = ['auth'];
424
+ const EVENTS_RESPONSES = [
425
+ 'subscribe',
426
+ 'unsubscribe',
427
+ 'COMMAND_RESP',
428
+ 'ping',
429
+ 'pong',
430
+ ];
431
+ const eventTopic = parsed === null || parsed === void 0 ? void 0 : parsed.topic;
432
+ const eventOperation = parsed === null || parsed === void 0 ? void 0 : parsed.op;
433
+ // WS API response
434
+ if ((0, util_1.isWSAPIResponse)(parsed)) {
435
+ const retCode = parsed.retCode;
436
+ const reqId = parsed.reqId;
437
+ const isError = retCode !== 0;
438
+ const promiseRef = [parsed.op, reqId].join('_');
439
+ if (!reqId) {
440
+ this.logger.error('WS API response is missing reqId - promisified workflow could get stuck. If this happens, please get in touch with steps to reproduce. Trace:', {
441
+ wsKey,
442
+ promiseRef,
443
+ parsedEvent: parsed,
444
+ });
674
445
  }
675
- else {
676
- this.emit('response', Object.assign(Object.assign({}, msg), { wsKey }));
446
+ // WS API Exception
447
+ if (isError) {
448
+ try {
449
+ this.getWsStore().rejectDeferredPromise(wsKey, promiseRef, Object.assign({ wsKey }, parsed), true);
450
+ }
451
+ catch (e) {
452
+ this.logger.error('Exception trying to reject WSAPI promise', {
453
+ wsKey,
454
+ promiseRef,
455
+ parsedEvent: parsed,
456
+ });
457
+ }
458
+ results.push({
459
+ eventType: 'error',
460
+ event: parsed,
461
+ isWSAPIResponse: true,
462
+ });
463
+ return results;
677
464
  }
678
- return;
679
- }
680
- if (msg['finalFragment']) {
681
- return this.emit('response', Object.assign(Object.assign({}, msg), { wsKey }));
682
- }
683
- if (msg === null || msg === void 0 ? void 0 : msg.topic) {
684
- return this.emit('update', Object.assign(Object.assign({}, msg), { wsKey }));
685
- }
686
- if (
687
- // spot v1
688
- (msg === null || msg === void 0 ? void 0 : msg.code) ||
689
- // spot v3
690
- (msg === null || msg === void 0 ? void 0 : msg.type) === 'error' ||
691
- // usdc options
692
- (msg === null || msg === void 0 ? void 0 : msg.success) === false) {
693
- return this.emit('error', Object.assign(Object.assign({}, msg), { wsKey }));
694
- }
695
- this.logger.warning('Unhandled/unrecognised ws event message', Object.assign(Object.assign({}, loggerCategory), { message: msg, event,
696
- wsKey }));
697
- }
698
- catch (e) {
699
- this.logger.error('Failed to parse ws event message', Object.assign(Object.assign({}, loggerCategory), { error: e, event,
700
- wsKey }));
701
- }
702
- }
703
- updatePendingTopicSubscriptionStatus(wsKey, msg) {
704
- const requestsIds = msg.req_id;
705
- const pendingTopicsSubscriptions = this.pendingTopicsSubscriptions.find((s) => s.wsKey === wsKey);
706
- if (!pendingTopicsSubscriptions)
707
- return;
708
- const splitRequestsIds = requestsIds.split(',');
709
- if (!(0, util_1.isTopicSubscriptionSuccess)(msg)) {
710
- splitRequestsIds.forEach((req_id) => pendingTopicsSubscriptions.failedTopicsSubscriptions.add(req_id));
711
- }
712
- splitRequestsIds.forEach((req_id) => {
713
- this.removeTopicPendingSubscription(wsKey, req_id);
714
- if (!pendingTopicsSubscriptions.pendingTopicsSubscriptions.size &&
715
- !pendingTopicsSubscriptions.failedTopicsSubscriptions.size) {
716
- // all topics have been subscribed successfully, so we can resolve the subscription request
717
- pendingTopicsSubscriptions.resolver();
718
- this.clearTopicsPendingSubscriptions(wsKey);
719
- }
720
- if (!pendingTopicsSubscriptions.pendingTopicsSubscriptions.size &&
721
- pendingTopicsSubscriptions.failedTopicsSubscriptions.size) {
722
- // not all topics have been subscribed successfully, so we reject the subscription request
723
- // and let the caller handle the situation by providing the list of failed subscriptions requests
724
- const failedSubscriptionsMessage = `(${[
725
- ...pendingTopicsSubscriptions.failedTopicsSubscriptions,
726
- ].toString()}) failed to subscribe`;
727
- pendingTopicsSubscriptions.rejector(failedSubscriptionsMessage);
728
- this.clearTopicsPendingSubscriptions(wsKey);
729
- }
730
- });
731
- }
732
- onWsClose(event, wsKey) {
733
- this.logger.info('Websocket connection closed', Object.assign(Object.assign({}, loggerCategory), { wsKey }));
734
- if (this.wsStore.getConnectionState(wsKey) !== util_1.WsConnectionStateEnum.CLOSING) {
735
- this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
736
- this.emit('reconnect', { wsKey, event });
737
- }
738
- else {
739
- this.setWsState(wsKey, util_1.WsConnectionStateEnum.INITIAL);
740
- this.emit('close', { wsKey, event });
741
- }
742
- }
743
- getWs(wsKey) {
744
- return this.wsStore.getWs(wsKey);
745
- }
746
- setWsState(wsKey, state) {
747
- this.wsStore.setConnectionState(wsKey, state);
748
- }
749
- wrongMarketError(market) {
750
- return new Error(`This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}'" to listen to ${market} topics`);
751
- }
752
- /** @deprecated use "market: 'spotv3" client */
753
- subscribePublicSpotTrades(symbol, binary) {
754
- if (this.options.market !== 'spot') {
755
- throw this.wrongMarketError('spot');
756
- }
757
- return this.tryWsSend(util_1.WS_KEY_MAP.spotPublic, JSON.stringify({
758
- topic: 'trade',
759
- event: 'sub',
760
- symbol,
761
- params: {
762
- binary: !!binary,
763
- },
764
- }));
765
- }
766
- /** @deprecated use "market: 'spotv3" client */
767
- subscribePublicSpotTradingPair(symbol, binary) {
768
- if (this.options.market !== 'spot') {
769
- throw this.wrongMarketError('spot');
770
- }
771
- return this.tryWsSend(util_1.WS_KEY_MAP.spotPublic, JSON.stringify({
772
- symbol,
773
- topic: 'realtimes',
774
- event: 'sub',
775
- params: {
776
- binary: !!binary,
777
- },
778
- }));
779
- }
780
- /** @deprecated use "market: 'spotv3" client */
781
- subscribePublicSpotV1Kline(symbol, candleSize, binary) {
782
- if (this.options.market !== 'spot') {
783
- throw this.wrongMarketError('spot');
784
- }
785
- return this.tryWsSend(util_1.WS_KEY_MAP.spotPublic, JSON.stringify({
786
- symbol,
787
- topic: 'kline_' + candleSize,
788
- event: 'sub',
789
- params: {
790
- binary: !!binary,
791
- },
792
- }));
793
- }
794
- //ws.send('{"symbol":"BTCUSDT","topic":"depth","event":"sub","params":{"binary":false}}');
795
- //ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}');
796
- //ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}');
797
- /** @deprecated use "market: 'spotv3" client */
798
- subscribePublicSpotOrderbook(symbol, depth, dumpScale, binary) {
799
- if (this.options.market !== 'spot') {
800
- throw this.wrongMarketError('spot');
801
- }
802
- let topic;
803
- switch (depth) {
804
- case 'full': {
805
- topic = 'depth';
806
- break;
807
- }
808
- case 'merge': {
809
- topic = 'mergedDepth';
810
- if (!dumpScale) {
811
- throw new Error('Dumpscale must be provided for merged orderbooks');
465
+ // WS API Success
466
+ try {
467
+ this.getWsStore().resolveDeferredPromise(wsKey, promiseRef, Object.assign({ wsKey }, parsed), true);
468
+ }
469
+ catch (e) {
470
+ this.logger.error('Exception trying to resolve WSAPI promise', {
471
+ wsKey,
472
+ promiseRef,
473
+ parsedEvent: parsed,
474
+ });
812
475
  }
813
- break;
476
+ results.push({
477
+ eventType: 'response',
478
+ event: parsed,
479
+ isWSAPIResponse: true,
480
+ });
481
+ return results;
482
+ }
483
+ // Messages for a subscribed topic all include the "topic" property
484
+ if (typeof eventTopic === 'string') {
485
+ results.push({
486
+ eventType: 'update',
487
+ event: parsed,
488
+ });
489
+ return results;
490
+ }
491
+ // Messages that are a "reply" to a request/command (e.g. subscribe to these topics) typically include the "op" property
492
+ if (typeof eventOperation === 'string') {
493
+ // Failed request
494
+ if (parsed.success === false) {
495
+ results.push({
496
+ eventType: 'error',
497
+ event: parsed,
498
+ });
499
+ return results;
500
+ }
501
+ // These are r equest/reply pattern events (e.g. after subscribing to topics or authenticating)
502
+ if (EVENTS_RESPONSES.includes(eventOperation)) {
503
+ results.push({
504
+ eventType: 'response',
505
+ event: parsed,
506
+ });
507
+ return results;
508
+ }
509
+ // Request/reply pattern for authentication success
510
+ if (EVENTS_AUTHENTICATED.includes(eventOperation)) {
511
+ results.push({
512
+ eventType: 'authenticated',
513
+ event: parsed,
514
+ });
515
+ return results;
516
+ }
517
+ this.logger.error(`!! Unhandled string operation type "${eventOperation}". Defaulting to "update" channel...`, parsed);
814
518
  }
815
- case 'delta': {
816
- topic = 'diffDepth';
817
- break;
519
+ else {
520
+ this.logger.error(`!! Unhandled non-string event type "${eventOperation}". Defaulting to "update" channel...`, parsed);
818
521
  }
522
+ // In case of catastrophic failure, fallback to noisy emit update
523
+ results.push({
524
+ eventType: 'update',
525
+ event: parsed,
526
+ });
819
527
  }
820
- const msg = {
821
- symbol,
822
- topic,
823
- event: 'sub',
824
- params: {
825
- binary: !!binary,
826
- },
827
- };
828
- if (dumpScale) {
829
- msg.params.dumpScale = dumpScale;
528
+ catch (e) {
529
+ results.push({
530
+ event: {
531
+ message: 'Failed to parse event data due to exception',
532
+ exception: e,
533
+ eventData: event.data,
534
+ },
535
+ eventType: 'error',
536
+ });
537
+ this.logger.error('Failed to parse event data due to exception: ', {
538
+ exception: e,
539
+ eventData: event.data,
540
+ });
830
541
  }
831
- return this.tryWsSend(util_1.WS_KEY_MAP.spotPublic, JSON.stringify(msg));
542
+ return results;
832
543
  }
833
544
  }
834
545
  exports.WebsocketClient = WebsocketClient;