UNPKG

bitmart-api

Version:

Complete & robust Node.js SDK for BitMart's REST APIs and WebSockets, with TypeScript declarations.

390 lines 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebsocketClient = exports.PUBLIC_WS_KEYS = exports.WS_LOGGER_CATEGORY = void 0; const BaseWSClient_js_1 = require("./lib/BaseWSClient.js"); const misc_util_js_1 = require("./lib/misc-util.js"); const webCryptoAPI_js_1 = require("./lib/webCryptoAPI.js"); const websocket_util_js_1 = require("./lib/websocket/websocket-util.js"); exports.WS_LOGGER_CATEGORY = { category: 'bitmart-ws' }; /** Any WS keys in this list will trigger auth on connect, if credentials are available */ const PRIVATE_WS_KEYS = [ websocket_util_js_1.WS_KEY_MAP.spotPrivateV1, websocket_util_js_1.WS_KEY_MAP.futuresPrivateV1, websocket_util_js_1.WS_KEY_MAP.futuresPrivateV2, ]; /** Any WS keys in this list will ALWAYS skip the authentication process, even if credentials are available */ exports.PUBLIC_WS_KEYS = [ websocket_util_js_1.WS_KEY_MAP.spotPublicV1, websocket_util_js_1.WS_KEY_MAP.futuresPublicV1, websocket_util_js_1.WS_KEY_MAP.futuresPublicV2, ]; class WebsocketClient extends BaseWSClient_js_1.BaseWebsocketClient { /** * Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library */ connectAll() { return [ this.connect(websocket_util_js_1.WS_KEY_MAP.spotPublicV1), this.connect(websocket_util_js_1.WS_KEY_MAP.spotPrivateV1), this.connect(websocket_util_js_1.WS_KEY_MAP.futuresPublicV2), this.connect(websocket_util_js_1.WS_KEY_MAP.futuresPrivateV2), ]; } /** * Request subscription to one or more topics. * * - Subscriptions are automatically routed to the correct websocket connection. * - Authentication/connection is automatic. * - Resubscribe after network issues is automatic. * * Call `unsubscribeTopics(topics)` to remove topics */ subscribeTopics(topics) { const topicsByWsKey = this.arrangeTopicsIntoWsKeyGroups(topics); for (const untypedWsKey in topicsByWsKey) { const typedWsKey = untypedWsKey; const topics = topicsByWsKey[typedWsKey]; if (topics.length) { this.subscribeTopicsForWsKey(topics, typedWsKey); } } } /** * Unsubscribe from one or more topics. * * - 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. */ unsubscribeTopics(topics) { const topicsByWsKey = this.arrangeTopicsIntoWsKeyGroups(topics); for (const untypedWsKey in topicsByWsKey) { const typedWsKey = untypedWsKey; const topics = topicsByWsKey[typedWsKey]; if (topics.length) { this.unsubscribeTopicsForWsKey(topics, typedWsKey); } } } /** * * Internal methods * */ sendPingEvent(wsKey) { switch (wsKey) { case websocket_util_js_1.WS_KEY_MAP.spotPublicV1: case websocket_util_js_1.WS_KEY_MAP.spotPrivateV1: { return this.tryWsSend(wsKey, 'ping'); } case websocket_util_js_1.WS_KEY_MAP.futuresPublicV1: case websocket_util_js_1.WS_KEY_MAP.futuresPrivateV1: case websocket_util_js_1.WS_KEY_MAP.futuresPublicV2: case websocket_util_js_1.WS_KEY_MAP.futuresPrivateV2: { return this.tryWsSend(wsKey, '{"action":"ping"}'); } default: { throw (0, misc_util_js_1.neverGuard)(wsKey, `Unhandled ping format: "${wsKey}"`); } } } isWsPong(msg) { // bitmart spot if (msg?.data === 'pong') { return true; } // bitmart futures // if (typeof event?.data === 'string') { // return true; // } if (typeof msg?.event?.data === 'string' && msg.event.data.startsWith('pong')) { return true; } // this.logger.info(`Not a pong: `, msg); return false; } resolveEmittableEvents(event) { const results = []; try { const parsed = JSON.parse(event.data); const responseEvents = ['subscribe', 'unsubscribe']; const authenticatedEvents = ['login', 'access']; const eventAction = parsed.event || parsed.action; if (typeof eventAction === 'string') { if (parsed.success === false) { results.push({ eventType: 'exception', event: parsed, }); return results; } // These are request/reply pattern events (e.g. after subscribing to topics or authenticating) if (responseEvents.includes(eventAction)) { results.push({ eventType: 'response', event: parsed, }); return results; } // Request/reply pattern for authentication success if (authenticatedEvents.includes(eventAction)) { results.push({ eventType: 'authenticated', event: parsed, }); return results; } this.logger.error(`!! Unhandled string event type "${eventAction}. Defaulting to "update" channel...`, parsed); } results.push({ eventType: 'update', event: parsed, }); } catch (e) { results.push({ event: { message: 'Failed to parse event data due to exception', exception: e, eventData: event.data, }, eventType: 'exception', }); this.logger.error('Failed to parse event data due to exception: ', { exception: e, eventData: event.data, }); } return results; } /** * Determines if a topic is for a private channel, using a hardcoded list of strings */ isPrivateChannel(topic) { const splitTopic = topic.toLowerCase().split('/'); if (!splitTopic.length) { return false; } const topicName = splitTopic[1]; if (!topicName) { // console.error(`No topic name? "${topicName}" from topic "${topic}"?`); return false; } if ( /** Spot */ topicName.startsWith('user') || /** Futures */ topicName.startsWith('asset') || topicName.startsWith('position') || topicName.startsWith('order') || topicName.startsWith('position')) { return true; } return false; } getWsKeyForMarket(market, isPrivate) { return isPrivate ? market === 'spot' ? websocket_util_js_1.WS_KEY_MAP.spotPrivateV1 : websocket_util_js_1.WS_KEY_MAP.futuresPrivateV2 : market === 'spot' ? websocket_util_js_1.WS_KEY_MAP.spotPublicV1 : websocket_util_js_1.WS_KEY_MAP.futuresPublicV2; } getWsMarketForWsKey(key) { switch (key) { case 'futuresPrivateV1': case 'futuresPublicV1': case 'futuresPrivateV2': case 'futuresPublicV2': { return 'futures'; } case 'spotPrivateV1': case 'spotPublicV1': { return 'spot'; } default: { throw (0, misc_util_js_1.neverGuard)(key, `Unhandled ws key "${key}"`); } } } getWsKeyForTopic(topic) { const market = this.getMarketForTopic(topic); const isPrivateTopic = this.isPrivateChannel(topic); return this.getWsKeyForMarket(market, isPrivateTopic); } getPrivateWSKeys() { return PRIVATE_WS_KEYS; } getWsUrl(wsKey) { if (this.options.wsUrl) { return this.options.wsUrl; } const networkKey = 'livenet'; return websocket_util_js_1.WS_BASE_URL_MAP[wsKey][networkKey]; } /** Force subscription requests to be sent in smaller batches, if a number is returned */ getMaxTopicsPerSubscribeEvent(wsKey) { switch (wsKey) { case 'futuresPrivateV1': case 'futuresPublicV1': case 'spotPrivateV1': case 'spotPublicV1': case 'futuresPrivateV2': case 'futuresPublicV2': { // Return a number if there's a limit on the number of sub topics per rq return 20; } default: { throw (0, misc_util_js_1.neverGuard)(wsKey, 'getWsKeyForTopic(): Unhandled wsKey'); } } } /** * Map one or more topics into fully prepared "subscribe request" events (already stringified and ready to send) */ getWsSubscribeEventsForTopics(topics, wsKey) { if (!topics.length) { return []; } const market = this.getWsMarketForWsKey(wsKey); const subscribeEvents = []; const maxTopicsPerEvent = this.getMaxTopicsPerSubscribeEvent(wsKey); if (maxTopicsPerEvent && maxTopicsPerEvent !== null && topics.length > maxTopicsPerEvent) { for (let i = 0; i < topics.length; i += maxTopicsPerEvent) { const batch = topics.slice(i, i + maxTopicsPerEvent); const subscribeEvent = this.getWsRequestEvent(market, 'subscribe', batch); subscribeEvents.push(JSON.stringify(subscribeEvent)); } return subscribeEvents; } const subscribeEvent = this.getWsRequestEvent(market, 'subscribe', topics); return [JSON.stringify(subscribeEvent)]; } /** * Map one or more topics into fully prepared "unsubscribe request" events (already stringified and ready to send) */ getWsUnsubscribeEventsForTopics(topics, wsKey) { if (!topics.length) { return []; } const market = this.getWsMarketForWsKey(wsKey); const subscribeEvents = []; const maxTopicsPerEvent = this.getMaxTopicsPerSubscribeEvent(wsKey); if (maxTopicsPerEvent && maxTopicsPerEvent !== null && topics.length > maxTopicsPerEvent) { for (let i = 0; i < topics.length; i += maxTopicsPerEvent) { const batch = topics.slice(i, i + maxTopicsPerEvent); const subscribeEvent = this.getWsRequestEvent(market, 'unsubscribe', batch); subscribeEvents.push(JSON.stringify(subscribeEvent)); } return subscribeEvents; } const subscribeEvent = this.getWsRequestEvent(market, 'subscribe', topics); return [JSON.stringify(subscribeEvent)]; } /** * @returns a correctly structured events for performing an operation over WS. This can vary per exchange spec. */ getWsRequestEvent(market, operation, args) { switch (market) { case 'spot': { const wsRequestEvent = { op: operation, args: args, }; return wsRequestEvent; } case 'futures': { const wsRequestEvent = { action: operation, args: args, }; return wsRequestEvent; } default: { throw (0, misc_util_js_1.neverGuard)(market, `Unhandled market "${market}"`); } } } async getWsAuthRequestEvent(wsKey) { const market = this.getWsMarketForWsKey(wsKey); if (!this.options.apiKey || !this.options.apiSecret || !this.options.apiMemo) { throw new Error('Cannot auth - missing api key, secret or memo in config'); } const signTimestamp = Date.now() + this.options.recvWindow; const signMessageInput = signTimestamp + '#' + this.options.apiMemo + '#' + 'bitmart.WebSocket'; let signature; if (typeof this.options.customSignMessageFn === 'function') { signature = await this.options.customSignMessageFn(signMessageInput, this.options.apiSecret); } else { signature = await (0, webCryptoAPI_js_1.signMessage)(signMessageInput, this.options.apiSecret, 'hex'); } const authArgs = [this.options.apiKey, `${signTimestamp}`, signature]; if (market === 'futures') { authArgs.push('web'); } switch (market) { case 'spot': { const wsRequestEvent = { op: 'login', args: authArgs, }; return wsRequestEvent; } case 'futures': { // https://developer-pro.bitmart.com/en/futuresv2/#private-login const wsRequestEvent = { action: 'access', args: authArgs, }; return wsRequestEvent; } default: { throw (0, misc_util_js_1.neverGuard)(market, `Unhandled market "${market}"`); } } } /** * This exchange API is split into "markets" that behave differently (different base URLs). * The market can easily be resolved using the topic name. */ getMarketForTopic(topic) { if (topic.startsWith('futures')) { return 'futures'; } if (topic.startsWith('spot')) { return 'spot'; } throw new Error(`Could not resolve "market" for topic: "${topic}"`); } /** * Used to split sub/unsub logic by websocket connection */ arrangeTopicsIntoWsKeyGroups(topics) { const topicsByWsKey = { futuresPrivateV1: [], futuresPublicV1: [], futuresPrivateV2: [], futuresPublicV2: [], spotPrivateV1: [], spotPublicV1: [], }; for (const topic in topics) { const wsKeyForTopic = this.getWsKeyForTopic(topic); const wsKeyTopicList = topicsByWsKey[wsKeyForTopic]; if (!wsKeyTopicList.includes(topic)) { wsKeyTopicList.push(topic); } } return topicsByWsKey; } } exports.WebsocketClient = WebsocketClient; //# sourceMappingURL=WebsocketClient.js.map