bitget-api
Version:
Complete Node.js & JavaScript SDK for Bitget V1-V3 REST APIs & WebSockets, with TypeScript & end-to-end tests.
335 lines • 12.7 kB
JavaScript
import { BaseWebsocketClient, } from './util/BaseWSClient.js';
import { isWsPong } from './util/requestUtils.js';
import { signMessage, } from './util/webCryptoAPI.js';
import { getMaxTopicsPerSubscribeEvent, getNormalisedTopicRequests, getWsUrl, isPrivateChannel, WS_AUTH_ON_CONNECT_KEYS, WS_KEY_MAP, } from './util/websocket-util.js';
const WS_LOGGER_CATEGORY = { category: 'bitget-ws' };
const COIN_CHANNELS = [
'account',
'account-crossed',
'account-isolated',
];
export class WebsocketClientV2 extends BaseWebsocketClient {
/**
* Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
*/
connectAll() {
return [
this.connect(WS_KEY_MAP.v2Private),
this.connect(WS_KEY_MAP.v2Public),
];
}
/** Some private channels use `coin` instead of `instId`. This method handles building the sub/unsub request */
getSubRequest(instType, topic, coin = 'default') {
if (isPrivateChannel(topic)) {
if (COIN_CHANNELS.includes(topic)) {
const subscribeRequest = {
topic,
payload: {
instType,
coin,
},
};
return subscribeRequest;
}
const subscribeRequest = {
topic,
payload: {
instType,
instId: coin,
},
};
return subscribeRequest;
}
const subscribeRequest = {
topic,
payload: {
instType,
instId: coin,
},
};
return subscribeRequest;
}
/**
* Subscribe to a topic
* @param instType instrument type (refer to API docs).
* @param topic topic name (e.g. "ticker").
* @param instId instrument ID (e.g. "BTCUSDT"). Use "default" for private topics.
*/
subscribeTopic(instType, topic, coin = 'default') {
const subRequest = this.getSubRequest(instType, topic, coin);
const isPrivateTopic = isPrivateChannel(topic);
const wsKey = isPrivateTopic ? WS_KEY_MAP.v2Private : WS_KEY_MAP.v2Public;
return this.subscribe(subRequest, wsKey);
}
/**
* Unsubscribe from a topic
* @param instType instrument type (refer to API docs).
* @param topic topic name (e.g. "ticker").
* @param instId instrument ID (e.g. "BTCUSDT"). Use "default" for private topics to get all symbols.
*/
unsubscribeTopic(instType, topic, coin = 'default') {
const subRequest = this.getSubRequest(instType, topic, coin);
const isPrivateTopic = isPrivateChannel(topic);
const wsKey = isPrivateTopic ? WS_KEY_MAP.v2Private : WS_KEY_MAP.v2Public;
return this.unsubscribe(subRequest, wsKey);
}
/**
* Request subscription to one or more topics. Pass topics as either an array of strings,
* or array of objects (if the topic has parameters).
*
* Objects should be formatted as {topic: string, params: object, category: CategoryV5}.
*
* - Subscriptions are automatically routed to the correct websocket connection.
* - Authentication/connection is automatic.
* - Resubscribe after network issues is automatic.
*
* Call `unsubscribe(topics)` to remove topics
*/
subscribe(requests, wsKey) {
const topicRequests = Array.isArray(requests) ? requests : [requests];
const normalisedTopicRequests = getNormalisedTopicRequests(topicRequests);
return this.subscribeTopicsForWsKey(normalisedTopicRequests, wsKey);
}
/**
* Unsubscribe from one or more topics. Similar to subscribe() but in reverse.
*
* - Requests are automatically routed to the correct websocket connection.
* - These topics will be removed from the topic cache, so they won't be subscribed to again.
*/
unsubscribe(requests, wsKey) {
const topicRequests = Array.isArray(requests) ? requests : [requests];
const normalisedTopicRequests = getNormalisedTopicRequests(topicRequests);
return this.unsubscribeTopicsForWsKey(normalisedTopicRequests, wsKey);
}
/**
*
*
* Internal methods required to integrate with the BaseWSClient
*
*
*/
sendPingEvent(wsKey) {
this.tryWsSend(wsKey, 'ping');
}
sendPongEvent(wsKey) {
this.tryWsSend(wsKey, 'pong');
}
isWsPing(data) {
if (data?.data === 'ping') {
return true;
}
return false;
}
isWsPong(data) {
return isWsPong(data);
}
isPrivateTopicRequest(_request, wsKey) {
return WS_AUTH_ON_CONNECT_KEYS.includes(wsKey);
}
getPrivateWSKeys() {
return WS_AUTH_ON_CONNECT_KEYS;
}
isAuthOnConnectWsKey(wsKey) {
return WS_AUTH_ON_CONNECT_KEYS.includes(wsKey);
}
async getWsUrl(wsKey) {
return getWsUrl(wsKey, this.options, this.logger);
}
getMaxTopicsPerSubscribeEvent(wsKey) {
return getMaxTopicsPerSubscribeEvent(wsKey);
}
/**
* @returns one or more correctly structured request events for performing a operations over WS. This can vary per exchange spec.
*/
async getWsRequestEvents(operation, requests) {
const wsRequestBuildingErrors = [];
const topics = requests.map((r) => r.topic + ',' + Object.values(r.payload || {}).join(','));
// Previously used to track topics in a request. Keeping this for subscribe/unsubscribe requests, no need for incremental values
const req_id = ['subscribe', 'unsubscribe'].includes(operation) && topics.length
? topics.join(',')
: this.getNewRequestId().toFixed();
/**
{
"op":"subscribe",
"args":[
{
"instType":"SPOT",
"channel":"ticker",
"instId":"BTCUSDT"
},
{
"instType":"SPOT",
"channel":"candle5m",
"instId":"BTCUSDT"
}
]
}
*/
const wsEvent = {
op: operation,
args: requests.map((request) => {
// const request = {
// topic: 'ticker',
// payload: { instType: 'SPOT', instId: 'BTCUSDT' },
// };
// becomes:
// const request = {
// channel: 'ticker',
// instType: 'SPOT',
// instId: 'BTCUSDT',
// };
return {
channel: request.topic,
...request.payload,
};
}),
};
const midflightWsEvent = {
requestKey: req_id,
requestEvent: wsEvent,
};
if (wsRequestBuildingErrors.length) {
const label = wsRequestBuildingErrors.length === requests.length ? 'all' : 'some';
this.logger.error(`Failed to build/send ${wsRequestBuildingErrors.length} event(s) for ${label} WS requests due to exceptions`, {
...WS_LOGGER_CATEGORY,
wsRequestBuildingErrors,
wsRequestBuildingErrorsStringified: JSON.stringify(wsRequestBuildingErrors, null, 2),
});
}
return [midflightWsEvent];
}
async getWsAuthSignature(wsKey) {
const { apiKey, apiSecret, apiPass, recvWindow } = this.options;
if (!apiKey || !apiSecret || !apiPass) {
this.logger.error('Cannot authenticate websocket, either api key, secret or passphrase missing.', { ...WS_LOGGER_CATEGORY, wsKey });
throw new Error('Cannot auth - missing api or secret or pass in config');
}
this.logger.trace("Getting auth'd request params", {
...WS_LOGGER_CATEGORY,
wsKey,
});
const signatureExpiresAt = ((Date.now() + recvWindow) / 1000).toFixed(0);
const signature = await this.signMessage(signatureExpiresAt + 'GET' + '/user/verify', apiSecret, 'base64', 'SHA-256');
return {
expiresAt: +signatureExpiresAt,
signature,
};
}
async signMessage(paramsStr, secret, method, algorithm) {
if (typeof this.options.customSignMessageFn === 'function') {
return this.options.customSignMessageFn(paramsStr, secret);
}
return await signMessage(paramsStr, secret, method, algorithm);
}
async getWsAuthRequestEvent(wsKey) {
try {
const { apiKey, apiSecret, apiPass } = this.options;
const { signature, expiresAt } = await this.getWsAuthSignature(wsKey);
if (!apiKey || !apiSecret || !apiPass) {
this.logger.error('Cannot authenticate websocket, either api key, secret or passphrase missing.', { ...WS_LOGGER_CATEGORY, wsKey });
throw new Error('Cannot auth - missing api or secret or pass in config');
}
const request = {
op: 'login',
args: [
{
apiKey,
passphrase: apiPass,
timestamp: expiresAt,
sign: signature,
},
],
};
return request;
}
catch (e) {
this.logger.error(e, { ...WS_LOGGER_CATEGORY, wsKey });
throw e;
}
}
/**
* Abstraction called to sort ws events into emittable event types (response to a request, data update, etc)
*/
resolveEmittableEvents(wsKey, event) {
const results = [];
try {
const msg = JSON.parse(event.data);
const emittableEvent = { ...msg, wsKey };
// v2 event processing
if (typeof msg === 'object') {
if (typeof msg['code'] === 'number') {
// v2 authentication event
if (msg.event === 'login' && msg.code === 0) {
results.push({
eventType: 'response',
event: emittableEvent,
});
results.push({
eventType: 'authenticated',
event: emittableEvent,
});
return results;
}
}
if (msg['event']) {
results.push({
eventType: 'response',
event: emittableEvent,
});
if (msg.event === 'error') {
this.logger.error('WS Error received', {
...WS_LOGGER_CATEGORY,
wsKey,
message: msg || 'no message',
// messageType: typeof msg,
// messageString: JSON.stringify(msg),
event,
});
results.push({
eventType: 'exception',
event: emittableEvent,
});
}
return results;
}
if (msg['arg']) {
results.push({
eventType: 'update',
event: emittableEvent,
});
return results;
}
}
this.logger.info('Unhandled/unrecognised ws event message', {
...WS_LOGGER_CATEGORY,
message: msg || 'no message',
// messageType: typeof msg,
// messageString: JSON.stringify(msg),
event,
wsKey,
});
// fallback emit anyway
results.push({
eventType: 'update',
event: emittableEvent,
});
return results;
}
catch (e) {
this.logger.error('Failed to parse ws event message', {
...WS_LOGGER_CATEGORY,
error: e,
event,
wsKey,
});
}
return results;
}
/**
* @deprecrated not supported by Bitget's V2 API offering
*/
async sendWSAPIRequest() {
return;
}
}
//# sourceMappingURL=websocket-client-v2.js.map